summaryrefslogtreecommitdiff
path: root/python/helpers/pycharm/docrunner.py
blob: ad619be0428f00b3a3bec174a16c4977a4e0cbe7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
import imp
import sys
import datetime
import os
helpers_dir = os.getenv("PYCHARM_HELPERS_DIR", sys.path[0])
if sys.path[0] != helpers_dir:
    sys.path.insert(0, helpers_dir)

from tcunittest import TeamcityTestResult
from tcmessages import TeamcityServiceMessages

from pycharm_run_utils import import_system_module
from pycharm_run_utils import adjust_sys_path, debug, getModuleName, PYTHON_VERSION_MAJOR

adjust_sys_path()

re = import_system_module("re")
doctest = import_system_module("doctest")
traceback = import_system_module("traceback")

class TeamcityDocTestResult(TeamcityTestResult):
  """
  DocTests Result extends TeamcityTestResult,
  overrides some methods, specific for doc tests,
  such as getTestName, getTestId.
  """
  def getTestName(self, test):
    name = self.current_suite.name + test.source
    return name

  def getSuiteName(self, suite):
    if test.source.rfind(".") == -1:
      name = self.current_suite.name + test.source
    else:
      name = test.source
    return name

  def getTestId(self, test):
    file = os.path.realpath(self.current_suite.filename) if self.current_suite.filename else ""
    line_no = test.lineno
    if self.current_suite.lineno:
      line_no += self.current_suite.lineno
    return "file://" + file + ":" + str(line_no)

  def getSuiteLocation(self):
    file = os.path.realpath(self.current_suite.filename) if self.current_suite.filename else ""
    location = "file://" + file
    if self.current_suite.lineno:
      location += ":" + str(self.current_suite.lineno)
    return location

  def startTest(self, test):
    setattr(test, "startTime", datetime.datetime.now())
    id = self.getTestId(test)
    self.messages.testStarted(self.getTestName(test), location=id)

  def startSuite(self, suite):
    self.current_suite = suite
    self.messages.testSuiteStarted(suite.name, location=self.getSuiteLocation())

  def stopSuite(self, suite):
    self.messages.testSuiteFinished(suite.name)

  def addFailure(self, test, err = ''):
    self.messages.testFailed(self.getTestName(test),
      message='Failure', details=err)

  def addError(self, test, err = ''):
    self.messages.testError(self.getTestName(test),
      message='Error', details=err)

class DocTestRunner(doctest.DocTestRunner):
  """
  Special runner for doctests,
  overrides __run method to report results using TeamcityDocTestResult
  """
  def __init__(self, verbose=None, optionflags=0):
    doctest.DocTestRunner.__init__(self, verbose, optionflags)
    self.stream = sys.stdout
    self.result = TeamcityDocTestResult(self.stream)
    #self.result.messages.testMatrixEntered()
    self._tests = []

  def addTests(self, tests):
    self._tests.extend(tests)

  def addTest(self, test):
    self._tests.append(test)

  def countTests(self):
    return len(self._tests)

  def start(self):
    for test in self._tests:
      self.run(test)

  def __run(self, test, compileflags, out):
    failures = tries = 0

    original_optionflags = self.optionflags
    SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
    check = self._checker.check_output
    self.result.startSuite(test)
    for examplenum, example in enumerate(test.examples):

      quiet = (self.optionflags & doctest.REPORT_ONLY_FIRST_FAILURE and
               failures > 0)

      self.optionflags = original_optionflags
      if example.options:
        for (optionflag, val) in example.options.items():
          if val:
            self.optionflags |= optionflag
          else:
            self.optionflags &= ~optionflag

      if hasattr(doctest, 'SKIP'):
        if self.optionflags & doctest.SKIP:
          continue

      tries += 1
      if not quiet:
        self.report_start(out, test, example)

      filename = '<doctest %s[%d]>' % (test.name, examplenum)

      try:
        exec(compile(example.source, filename, "single",
          compileflags, 1), test.globs)
        self.debugger.set_continue() # ==== Example Finished ====
        exception = None
      except KeyboardInterrupt:
        raise
      except:
        exception = sys.exc_info()
        self.debugger.set_continue() # ==== Example Finished ====

      got = self._fakeout.getvalue()  # the actual output
      self._fakeout.truncate(0)
      outcome = FAILURE   # guilty until proved innocent or insane

      if exception is None:
        if check(example.want, got, self.optionflags):
          outcome = SUCCESS

      else:
        exc_msg = traceback.format_exception_only(*exception[:2])[-1]
        if not quiet:
          got += doctest._exception_traceback(exception)

        if example.exc_msg is None:
          outcome = BOOM

        elif check(example.exc_msg, exc_msg, self.optionflags):
          outcome = SUCCESS

        elif self.optionflags & doctest.IGNORE_EXCEPTION_DETAIL:
          m1 = re.match(r'[^:]*:', example.exc_msg)
          m2 = re.match(r'[^:]*:', exc_msg)
          if m1 and m2 and check(m1.group(0), m2.group(0),
            self.optionflags):
            outcome = SUCCESS

      # Report the outcome.
      if outcome is SUCCESS:
        self.result.startTest(example)
        self.result.stopTest(example)
      elif outcome is FAILURE:
        self.result.startTest(example)
        err = self._failure_header(test, example) +\
              self._checker.output_difference(example, got, self.optionflags)
        self.result.addFailure(example, err)

      elif outcome is BOOM:
        self.result.startTest(example)
        err=self._failure_header(test, example) +\
            'Exception raised:\n' + doctest._indent(doctest._exception_traceback(exception))
        self.result.addError(example, err)

      else:
        assert False, ("unknown outcome", outcome)

    self.optionflags = original_optionflags

    self.result.stopSuite(test)


modules = {}



runner = DocTestRunner()

def loadSource(fileName):
  """
  loads source from fileName,
  we can't use tat function from utrunner, because of we
  store modules in global variable.
  """
  baseName = os.path.basename(fileName)
  moduleName = os.path.splitext(baseName)[0]

  # for users wanted to run simple doctests under django
  #because of django took advantage of module name
  settings_file = os.getenv('DJANGO_SETTINGS_MODULE')
  if settings_file and moduleName=="models":
    baseName = os.path.realpath(fileName)
    moduleName = ".".join((baseName.split(os.sep)[-2], "models"))

  if moduleName in modules: # add unique number to prevent name collisions
    cnt = 2
    prefix = moduleName
    while getModuleName(prefix, cnt) in modules:
      cnt += 1
    moduleName = getModuleName(prefix, cnt)
  debug("/ Loading " + fileName + " as " + moduleName)
  module = imp.load_source(moduleName, fileName)
  modules[moduleName] = module
  return module

def testfile(filename):
  if PYTHON_VERSION_MAJOR == 3:
    text, filename = doctest._load_testfile(filename, None, False, "utf-8")
  else:
    text, filename = doctest._load_testfile(filename, None, False)

  name = os.path.basename(filename)
  globs = {'__name__': '__main__'}

  parser = doctest.DocTestParser()
  # Read the file, convert it to a test, and run it.
  test = parser.get_doctest(text, globs, name, filename, 0)
  if test.examples:
    runner.addTest(test)

def testFilesInFolder(folder):
  return testFilesInFolderUsingPattern(folder)

def testFilesInFolderUsingPattern(folder, pattern = ".*"):
  ''' loads modules from folder ,
      check if module name matches given pattern'''
  modules = []
  prog = re.compile(pattern)

  for root, dirs, files in os.walk(folder):
    for name in files:
      path = os.path.join(root, name)
      if prog.match(name):
        if name.endswith(".py"):
          modules.append(loadSource(path))
        elif not name.endswith(".pyc") and not name.endswith("$py.class") and os.path.isfile(path):
          testfile(path)

    return modules

if __name__ == "__main__":
  finder = doctest.DocTestFinder()

  for arg in sys.argv[1:]:
    arg = arg.strip()
    if len(arg) == 0:
      continue

    a = arg.split("::")
    if len(a) == 1:
      # From module or folder
      a_splitted = a[0].split(";")
      if len(a_splitted) != 1:
        # means we have pattern to match against
        if a_splitted[0].endswith("/"):
          debug("/ from folder " + a_splitted[0] + ". Use pattern: " + a_splitted[1])
          modules = testFilesInFolderUsingPattern(a_splitted[0], a_splitted[1])
      else:
        if a[0].endswith("/"):
          debug("/ from folder " + a[0])
          modules = testFilesInFolder(a[0])
        else:
          # from file
          debug("/ from module " + a[0])
          # for doctests from non-python file
          if a[0].rfind(".py") == -1:
            testfile(a[0])
            modules = []
          else:
            modules = [loadSource(a[0])]

      # for doctests
      for module in modules:
        tests = finder.find(module, module.__name__)
        for test in tests:
          if test.examples:
            runner.addTest(test)

    elif len(a) == 2:
      # From testcase
      debug("/ from class " + a[1] + " in " + a[0])
      try:
        module = loadSource(a[0])
      except SyntaxError:
        raise NameError('File "%s" is not python file' % (a[0], ))
      if hasattr(module, a[1]):
        testcase = getattr(module, a[1])
        tests = finder.find(testcase, getattr(testcase, "__name__", None))
        runner.addTests(tests)
      else:
        raise NameError('Module "%s" has no class "%s"' % (a[0], a[1]))
    else:
      # From method in class or from function
      try:
        module = loadSource(a[0])
      except SyntaxError:
        raise NameError('File "%s" is not python file' % (a[0], ))
      if a[1] == "":
        # test function, not method
        debug("/ from method " + a[2] + " in " + a[0])
        if hasattr(module, a[2]):
          testcase = getattr(module, a[2])
          tests = finder.find(testcase, getattr(testcase, "__name__", None))
          runner.addTests(tests)
        else:
          raise NameError('Module "%s" has no method "%s"' % (a[0], a[2]))
      else:
        debug("/ from method " + a[2] + " in class " +  a[1] + " in " + a[0])
        if hasattr(module, a[1]):
          testCaseClass = getattr(module, a[1])
          if hasattr(testCaseClass, a[2]):
            testcase = getattr(testCaseClass, a[2])
            name = getattr(testcase, "__name__", None)
            if not name:
              name = testCaseClass.__name__
            tests = finder.find(testcase, name)
            runner.addTests(tests)
          else:
            raise NameError('Class "%s" has no function "%s"' % (testCaseClass, a[2]))
        else:
          raise NameError('Module "%s" has no class "%s"' % (module, a[1]))

  debug("/ Loaded " + str(runner.countTests()) + " tests")
  TeamcityServiceMessages(sys.stdout).testCount(runner.countTests())
  runner.start()