Vendor debugpy for VS Code attach debugging
Bundles debugpy 1.7.0 directly in python/ so "Maya: Attach (debugpy)" works with no per-machine pip install step. Installed via Maya 2022's own pip so it resolved a version actually compatible with Python 3.7 (Maya 2022's interpreter), then verified import + listen() succeeds under all three target Maya Python versions (3.7/3.9/3.10). Also fixes a latent __file__-under-exec() bug in start_debug_server.py (same pitfall as Maya's own plugin loader, never hit until this exercised it) and corrects the README's Maya Python version claim -- 2022 ships Python 3.7, not 3.9 as previously stated, which was never independently verified until now. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,857 @@
|
||||
from __future__ import nested_scopes
|
||||
|
||||
import fnmatch
|
||||
import os.path
|
||||
from _pydev_runfiles.pydev_runfiles_coverage import start_coverage_support
|
||||
from _pydevd_bundle.pydevd_constants import * # @UnusedWildImport
|
||||
import re
|
||||
import time
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# Configuration
|
||||
#=======================================================================================================================
|
||||
class Configuration:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files_or_dirs='',
|
||||
verbosity=2,
|
||||
include_tests=None,
|
||||
tests=None,
|
||||
port=None,
|
||||
files_to_tests=None,
|
||||
jobs=1,
|
||||
split_jobs='tests',
|
||||
coverage_output_dir=None,
|
||||
coverage_include=None,
|
||||
coverage_output_file=None,
|
||||
exclude_files=None,
|
||||
exclude_tests=None,
|
||||
include_files=None,
|
||||
django=False,
|
||||
):
|
||||
self.files_or_dirs = files_or_dirs
|
||||
self.verbosity = verbosity
|
||||
self.include_tests = include_tests
|
||||
self.tests = tests
|
||||
self.port = port
|
||||
self.files_to_tests = files_to_tests
|
||||
self.jobs = jobs
|
||||
self.split_jobs = split_jobs
|
||||
self.django = django
|
||||
|
||||
if include_tests:
|
||||
assert isinstance(include_tests, (list, tuple))
|
||||
|
||||
if exclude_files:
|
||||
assert isinstance(exclude_files, (list, tuple))
|
||||
|
||||
if exclude_tests:
|
||||
assert isinstance(exclude_tests, (list, tuple))
|
||||
|
||||
self.exclude_files = exclude_files
|
||||
self.include_files = include_files
|
||||
self.exclude_tests = exclude_tests
|
||||
|
||||
self.coverage_output_dir = coverage_output_dir
|
||||
self.coverage_include = coverage_include
|
||||
self.coverage_output_file = coverage_output_file
|
||||
|
||||
def __str__(self):
|
||||
return '''Configuration
|
||||
- files_or_dirs: %s
|
||||
- verbosity: %s
|
||||
- tests: %s
|
||||
- port: %s
|
||||
- files_to_tests: %s
|
||||
- jobs: %s
|
||||
- split_jobs: %s
|
||||
|
||||
- include_files: %s
|
||||
- include_tests: %s
|
||||
|
||||
- exclude_files: %s
|
||||
- exclude_tests: %s
|
||||
|
||||
- coverage_output_dir: %s
|
||||
- coverage_include_dir: %s
|
||||
- coverage_output_file: %s
|
||||
|
||||
- django: %s
|
||||
''' % (
|
||||
self.files_or_dirs,
|
||||
self.verbosity,
|
||||
self.tests,
|
||||
self.port,
|
||||
self.files_to_tests,
|
||||
self.jobs,
|
||||
self.split_jobs,
|
||||
|
||||
self.include_files,
|
||||
self.include_tests,
|
||||
|
||||
self.exclude_files,
|
||||
self.exclude_tests,
|
||||
|
||||
self.coverage_output_dir,
|
||||
self.coverage_include,
|
||||
self.coverage_output_file,
|
||||
|
||||
self.django,
|
||||
)
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# parse_cmdline
|
||||
#=======================================================================================================================
|
||||
def parse_cmdline(argv=None):
|
||||
"""
|
||||
Parses command line and returns test directories, verbosity, test filter and test suites
|
||||
|
||||
usage:
|
||||
runfiles.py -v|--verbosity <level> -t|--tests <Test.test1,Test2> dirs|files
|
||||
|
||||
Multiprocessing options:
|
||||
jobs=number (with the number of jobs to be used to run the tests)
|
||||
split_jobs='module'|'tests'
|
||||
if == module, a given job will always receive all the tests from a module
|
||||
if == tests, the tests will be split independently of their originating module (default)
|
||||
|
||||
--exclude_files = comma-separated list of patterns with files to exclude (fnmatch style)
|
||||
--include_files = comma-separated list of patterns with files to include (fnmatch style)
|
||||
--exclude_tests = comma-separated list of patterns with test names to exclude (fnmatch style)
|
||||
|
||||
Note: if --tests is given, --exclude_files, --include_files and --exclude_tests are ignored!
|
||||
"""
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
verbosity = 2
|
||||
include_tests = None
|
||||
tests = None
|
||||
port = None
|
||||
jobs = 1
|
||||
split_jobs = 'tests'
|
||||
files_to_tests = {}
|
||||
coverage_output_dir = None
|
||||
coverage_include = None
|
||||
exclude_files = None
|
||||
exclude_tests = None
|
||||
include_files = None
|
||||
django = False
|
||||
|
||||
from _pydev_bundle._pydev_getopt import gnu_getopt
|
||||
optlist, dirs = gnu_getopt(
|
||||
argv[1:], "",
|
||||
[
|
||||
"verbosity=",
|
||||
"tests=",
|
||||
|
||||
"port=",
|
||||
"config_file=",
|
||||
|
||||
"jobs=",
|
||||
"split_jobs=",
|
||||
|
||||
"include_tests=",
|
||||
"include_files=",
|
||||
|
||||
"exclude_files=",
|
||||
"exclude_tests=",
|
||||
|
||||
"coverage_output_dir=",
|
||||
"coverage_include=",
|
||||
|
||||
"django="
|
||||
]
|
||||
)
|
||||
|
||||
for opt, value in optlist:
|
||||
if opt in ("-v", "--verbosity"):
|
||||
verbosity = value
|
||||
|
||||
elif opt in ("-p", "--port"):
|
||||
port = int(value)
|
||||
|
||||
elif opt in ("-j", "--jobs"):
|
||||
jobs = int(value)
|
||||
|
||||
elif opt in ("-s", "--split_jobs"):
|
||||
split_jobs = value
|
||||
if split_jobs not in ('module', 'tests'):
|
||||
raise AssertionError('Expected split to be either "module" or "tests". Was :%s' % (split_jobs,))
|
||||
|
||||
elif opt in ("-d", "--coverage_output_dir",):
|
||||
coverage_output_dir = value.strip()
|
||||
|
||||
elif opt in ("-i", "--coverage_include",):
|
||||
coverage_include = value.strip()
|
||||
|
||||
elif opt in ("-I", "--include_tests"):
|
||||
include_tests = value.split(',')
|
||||
|
||||
elif opt in ("-E", "--exclude_files"):
|
||||
exclude_files = value.split(',')
|
||||
|
||||
elif opt in ("-F", "--include_files"):
|
||||
include_files = value.split(',')
|
||||
|
||||
elif opt in ("-e", "--exclude_tests"):
|
||||
exclude_tests = value.split(',')
|
||||
|
||||
elif opt in ("-t", "--tests"):
|
||||
tests = value.split(',')
|
||||
|
||||
elif opt in ("--django",):
|
||||
django = value.strip() in ['true', 'True', '1']
|
||||
|
||||
elif opt in ("-c", "--config_file"):
|
||||
config_file = value.strip()
|
||||
if os.path.exists(config_file):
|
||||
f = open(config_file, 'r')
|
||||
try:
|
||||
config_file_contents = f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
if config_file_contents:
|
||||
config_file_contents = config_file_contents.strip()
|
||||
|
||||
if config_file_contents:
|
||||
for line in config_file_contents.splitlines():
|
||||
file_and_test = line.split('|')
|
||||
if len(file_and_test) == 2:
|
||||
file, test = file_and_test
|
||||
if file in files_to_tests:
|
||||
files_to_tests[file].append(test)
|
||||
else:
|
||||
files_to_tests[file] = [test]
|
||||
|
||||
else:
|
||||
sys.stderr.write('Could not find config file: %s\n' % (config_file,))
|
||||
|
||||
if type([]) != type(dirs):
|
||||
dirs = [dirs]
|
||||
|
||||
ret_dirs = []
|
||||
for d in dirs:
|
||||
if '|' in d:
|
||||
# paths may come from the ide separated by |
|
||||
ret_dirs.extend(d.split('|'))
|
||||
else:
|
||||
ret_dirs.append(d)
|
||||
|
||||
verbosity = int(verbosity)
|
||||
|
||||
if tests:
|
||||
if verbosity > 4:
|
||||
sys.stdout.write('--tests provided. Ignoring --exclude_files, --exclude_tests and --include_files\n')
|
||||
exclude_files = exclude_tests = include_files = None
|
||||
|
||||
config = Configuration(
|
||||
ret_dirs,
|
||||
verbosity,
|
||||
include_tests,
|
||||
tests,
|
||||
port,
|
||||
files_to_tests,
|
||||
jobs,
|
||||
split_jobs,
|
||||
coverage_output_dir,
|
||||
coverage_include,
|
||||
exclude_files=exclude_files,
|
||||
exclude_tests=exclude_tests,
|
||||
include_files=include_files,
|
||||
django=django,
|
||||
)
|
||||
|
||||
if verbosity > 5:
|
||||
sys.stdout.write(str(config) + '\n')
|
||||
return config
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# PydevTestRunner
|
||||
#=======================================================================================================================
|
||||
class PydevTestRunner(object):
|
||||
""" finds and runs a file or directory of files as a unit test """
|
||||
|
||||
__py_extensions = ["*.py", "*.pyw"]
|
||||
__exclude_files = ["__init__.*"]
|
||||
|
||||
# Just to check that only this attributes will be written to this file
|
||||
__slots__ = [
|
||||
'verbosity', # Always used
|
||||
|
||||
'files_to_tests', # If this one is given, the ones below are not used
|
||||
|
||||
'files_or_dirs', # Files or directories received in the command line
|
||||
'include_tests', # The filter used to collect the tests
|
||||
'tests', # Strings with the tests to be run
|
||||
|
||||
'jobs', # Integer with the number of jobs that should be used to run the test cases
|
||||
'split_jobs', # String with 'tests' or 'module' (how should the jobs be split)
|
||||
|
||||
'configuration',
|
||||
'coverage',
|
||||
]
|
||||
|
||||
def __init__(self, configuration):
|
||||
self.verbosity = configuration.verbosity
|
||||
|
||||
self.jobs = configuration.jobs
|
||||
self.split_jobs = configuration.split_jobs
|
||||
|
||||
files_to_tests = configuration.files_to_tests
|
||||
if files_to_tests:
|
||||
self.files_to_tests = files_to_tests
|
||||
self.files_or_dirs = list(files_to_tests.keys())
|
||||
self.tests = None
|
||||
else:
|
||||
self.files_to_tests = {}
|
||||
self.files_or_dirs = configuration.files_or_dirs
|
||||
self.tests = configuration.tests
|
||||
|
||||
self.configuration = configuration
|
||||
self.__adjust_path()
|
||||
|
||||
def __adjust_path(self):
|
||||
""" add the current file or directory to the python path """
|
||||
path_to_append = None
|
||||
for n in range(len(self.files_or_dirs)):
|
||||
dir_name = self.__unixify(self.files_or_dirs[n])
|
||||
if os.path.isdir(dir_name):
|
||||
if not dir_name.endswith("/"):
|
||||
self.files_or_dirs[n] = dir_name + "/"
|
||||
path_to_append = os.path.normpath(dir_name)
|
||||
elif os.path.isfile(dir_name):
|
||||
path_to_append = os.path.dirname(dir_name)
|
||||
else:
|
||||
if not os.path.exists(dir_name):
|
||||
block_line = '*' * 120
|
||||
sys.stderr.write('\n%s\n* PyDev test runner error: %s does not exist.\n%s\n' % (block_line, dir_name, block_line))
|
||||
return
|
||||
msg = ("unknown type. \n%s\nshould be file or a directory.\n" % (dir_name))
|
||||
raise RuntimeError(msg)
|
||||
if path_to_append is not None:
|
||||
# Add it as the last one (so, first things are resolved against the default dirs and
|
||||
# if none resolves, then we try a relative import).
|
||||
sys.path.append(path_to_append)
|
||||
|
||||
def __is_valid_py_file(self, fname):
|
||||
""" tests that a particular file contains the proper file extension
|
||||
and is not in the list of files to exclude """
|
||||
is_valid_fname = 0
|
||||
for invalid_fname in self.__class__.__exclude_files:
|
||||
is_valid_fname += int(not fnmatch.fnmatch(fname, invalid_fname))
|
||||
if_valid_ext = 0
|
||||
for ext in self.__class__.__py_extensions:
|
||||
if_valid_ext += int(fnmatch.fnmatch(fname, ext))
|
||||
return is_valid_fname > 0 and if_valid_ext > 0
|
||||
|
||||
def __unixify(self, s):
|
||||
""" stupid windows. converts the backslash to forwardslash for consistency """
|
||||
return os.path.normpath(s).replace(os.sep, "/")
|
||||
|
||||
def __importify(self, s, dir=False):
|
||||
""" turns directory separators into dots and removes the ".py*" extension
|
||||
so the string can be used as import statement """
|
||||
if not dir:
|
||||
dirname, fname = os.path.split(s)
|
||||
|
||||
if fname.count('.') > 1:
|
||||
# if there's a file named xxx.xx.py, it is not a valid module, so, let's not load it...
|
||||
return
|
||||
|
||||
imp_stmt_pieces = [dirname.replace("\\", "/").replace("/", "."), os.path.splitext(fname)[0]]
|
||||
|
||||
if len(imp_stmt_pieces[0]) == 0:
|
||||
imp_stmt_pieces = imp_stmt_pieces[1:]
|
||||
|
||||
return ".".join(imp_stmt_pieces)
|
||||
|
||||
else: # handle dir
|
||||
return s.replace("\\", "/").replace("/", ".")
|
||||
|
||||
def __add_files(self, pyfiles, root, files):
|
||||
""" if files match, appends them to pyfiles. used by os.path.walk fcn """
|
||||
for fname in files:
|
||||
if self.__is_valid_py_file(fname):
|
||||
name_without_base_dir = self.__unixify(os.path.join(root, fname))
|
||||
pyfiles.append(name_without_base_dir)
|
||||
|
||||
def find_import_files(self):
|
||||
""" return a list of files to import """
|
||||
if self.files_to_tests:
|
||||
pyfiles = self.files_to_tests.keys()
|
||||
else:
|
||||
pyfiles = []
|
||||
|
||||
for base_dir in self.files_or_dirs:
|
||||
if os.path.isdir(base_dir):
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
# Note: handling directories that should be excluded from the search because
|
||||
# they don't have __init__.py
|
||||
exclude = {}
|
||||
for d in dirs:
|
||||
for init in ['__init__.py', '__init__.pyo', '__init__.pyc', '__init__.pyw', '__init__$py.class']:
|
||||
if os.path.exists(os.path.join(root, d, init).replace('\\', '/')):
|
||||
break
|
||||
else:
|
||||
exclude[d] = 1
|
||||
|
||||
if exclude:
|
||||
new = []
|
||||
for d in dirs:
|
||||
if d not in exclude:
|
||||
new.append(d)
|
||||
|
||||
dirs[:] = new
|
||||
|
||||
self.__add_files(pyfiles, root, files)
|
||||
|
||||
elif os.path.isfile(base_dir):
|
||||
pyfiles.append(base_dir)
|
||||
|
||||
if self.configuration.exclude_files or self.configuration.include_files:
|
||||
ret = []
|
||||
for f in pyfiles:
|
||||
add = True
|
||||
basename = os.path.basename(f)
|
||||
if self.configuration.include_files:
|
||||
add = False
|
||||
|
||||
for pat in self.configuration.include_files:
|
||||
if fnmatch.fnmatchcase(basename, pat):
|
||||
add = True
|
||||
break
|
||||
|
||||
if not add:
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('Skipped file: %s (did not match any include_files pattern: %s)\n' % (f, self.configuration.include_files))
|
||||
|
||||
elif self.configuration.exclude_files:
|
||||
for pat in self.configuration.exclude_files:
|
||||
if fnmatch.fnmatchcase(basename, pat):
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('Skipped file: %s (matched exclude_files pattern: %s)\n' % (f, pat))
|
||||
|
||||
elif self.verbosity > 2:
|
||||
sys.stdout.write('Skipped file: %s\n' % (f,))
|
||||
|
||||
add = False
|
||||
break
|
||||
|
||||
if add:
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('Adding file: %s for test discovery.\n' % (f,))
|
||||
ret.append(f)
|
||||
|
||||
pyfiles = ret
|
||||
|
||||
return pyfiles
|
||||
|
||||
def __get_module_from_str(self, modname, print_exception, pyfile):
|
||||
""" Import the module in the given import path.
|
||||
* Returns the "final" module, so importing "coilib40.subject.visu"
|
||||
returns the "visu" module, not the "coilib40" as returned by __import__ """
|
||||
try:
|
||||
mod = __import__(modname)
|
||||
for part in modname.split('.')[1:]:
|
||||
mod = getattr(mod, part)
|
||||
return mod
|
||||
except:
|
||||
if print_exception:
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
from _pydevd_bundle import pydevd_io
|
||||
buf_err = pydevd_io.start_redirect(keep_original_redirection=True, std='stderr')
|
||||
buf_out = pydevd_io.start_redirect(keep_original_redirection=True, std='stdout')
|
||||
try:
|
||||
import traceback;traceback.print_exc()
|
||||
sys.stderr.write('ERROR: Module: %s could not be imported (file: %s).\n' % (modname, pyfile))
|
||||
finally:
|
||||
pydevd_io.end_redirect('stderr')
|
||||
pydevd_io.end_redirect('stdout')
|
||||
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'error', buf_out.getvalue(), buf_err.getvalue(), pyfile, modname, 0)
|
||||
|
||||
return None
|
||||
|
||||
def remove_duplicates_keeping_order(self, seq):
|
||||
seen = set()
|
||||
seen_add = seen.add
|
||||
return [x for x in seq if not (x in seen or seen_add(x))]
|
||||
|
||||
def find_modules_from_files(self, pyfiles):
|
||||
""" returns a list of modules given a list of files """
|
||||
# let's make sure that the paths we want are in the pythonpath...
|
||||
imports = [(s, self.__importify(s)) for s in pyfiles]
|
||||
|
||||
sys_path = [os.path.normpath(path) for path in sys.path]
|
||||
sys_path = self.remove_duplicates_keeping_order(sys_path)
|
||||
|
||||
system_paths = []
|
||||
for s in sys_path:
|
||||
system_paths.append(self.__importify(s, True))
|
||||
|
||||
ret = []
|
||||
for pyfile, imp in imports:
|
||||
if imp is None:
|
||||
continue # can happen if a file is not a valid module
|
||||
choices = []
|
||||
for s in system_paths:
|
||||
if imp.startswith(s):
|
||||
add = imp[len(s) + 1:]
|
||||
if add:
|
||||
choices.append(add)
|
||||
# sys.stdout.write(' ' + add + ' ')
|
||||
|
||||
if not choices:
|
||||
sys.stdout.write('PYTHONPATH not found for file: %s\n' % imp)
|
||||
else:
|
||||
for i, import_str in enumerate(choices):
|
||||
print_exception = i == len(choices) - 1
|
||||
mod = self.__get_module_from_str(import_str, print_exception, pyfile)
|
||||
if mod is not None:
|
||||
ret.append((pyfile, mod, import_str))
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
#===================================================================================================================
|
||||
# GetTestCaseNames
|
||||
#===================================================================================================================
|
||||
class GetTestCaseNames:
|
||||
"""Yes, we need a class for that (cannot use outer context on jython 2.1)"""
|
||||
|
||||
def __init__(self, accepted_classes, accepted_methods):
|
||||
self.accepted_classes = accepted_classes
|
||||
self.accepted_methods = accepted_methods
|
||||
|
||||
def __call__(self, testCaseClass):
|
||||
"""Return a sorted sequence of method names found within testCaseClass"""
|
||||
testFnNames = []
|
||||
className = testCaseClass.__name__
|
||||
|
||||
if className in self.accepted_classes:
|
||||
for attrname in dir(testCaseClass):
|
||||
# If a class is chosen, we select all the 'test' methods'
|
||||
if attrname.startswith('test') and hasattr(getattr(testCaseClass, attrname), '__call__'):
|
||||
testFnNames.append(attrname)
|
||||
|
||||
else:
|
||||
for attrname in dir(testCaseClass):
|
||||
# If we have the class+method name, we must do a full check and have an exact match.
|
||||
if className + '.' + attrname in self.accepted_methods:
|
||||
if hasattr(getattr(testCaseClass, attrname), '__call__'):
|
||||
testFnNames.append(attrname)
|
||||
|
||||
# sorted() is not available in jython 2.1
|
||||
testFnNames.sort()
|
||||
return testFnNames
|
||||
|
||||
def _decorate_test_suite(self, suite, pyfile, module_name):
|
||||
import unittest
|
||||
if isinstance(suite, unittest.TestSuite):
|
||||
add = False
|
||||
suite.__pydev_pyfile__ = pyfile
|
||||
suite.__pydev_module_name__ = module_name
|
||||
|
||||
for t in suite._tests:
|
||||
t.__pydev_pyfile__ = pyfile
|
||||
t.__pydev_module_name__ = module_name
|
||||
if self._decorate_test_suite(t, pyfile, module_name):
|
||||
add = True
|
||||
|
||||
return add
|
||||
|
||||
elif isinstance(suite, unittest.TestCase):
|
||||
return True
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
def find_tests_from_modules(self, file_and_modules_and_module_name):
|
||||
""" returns the unittests given a list of modules """
|
||||
# Use our own suite!
|
||||
from _pydev_runfiles import pydev_runfiles_unittest
|
||||
import unittest
|
||||
unittest.TestLoader.suiteClass = pydev_runfiles_unittest.PydevTestSuite
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
ret = []
|
||||
if self.files_to_tests:
|
||||
for pyfile, m, module_name in file_and_modules_and_module_name:
|
||||
accepted_classes = {}
|
||||
accepted_methods = {}
|
||||
tests = self.files_to_tests[pyfile]
|
||||
for t in tests:
|
||||
accepted_methods[t] = t
|
||||
|
||||
loader.getTestCaseNames = self.GetTestCaseNames(accepted_classes, accepted_methods)
|
||||
|
||||
suite = loader.loadTestsFromModule(m)
|
||||
if self._decorate_test_suite(suite, pyfile, module_name):
|
||||
ret.append(suite)
|
||||
return ret
|
||||
|
||||
if self.tests:
|
||||
accepted_classes = {}
|
||||
accepted_methods = {}
|
||||
|
||||
for t in self.tests:
|
||||
splitted = t.split('.')
|
||||
if len(splitted) == 1:
|
||||
accepted_classes[t] = t
|
||||
|
||||
elif len(splitted) == 2:
|
||||
accepted_methods[t] = t
|
||||
|
||||
loader.getTestCaseNames = self.GetTestCaseNames(accepted_classes, accepted_methods)
|
||||
|
||||
for pyfile, m, module_name in file_and_modules_and_module_name:
|
||||
suite = loader.loadTestsFromModule(m)
|
||||
if self._decorate_test_suite(suite, pyfile, module_name):
|
||||
ret.append(suite)
|
||||
|
||||
return ret
|
||||
|
||||
def filter_tests(self, test_objs, internal_call=False):
|
||||
""" based on a filter name, only return those tests that have
|
||||
the test case names that match """
|
||||
import unittest
|
||||
if not internal_call:
|
||||
if not self.configuration.include_tests and not self.tests and not self.configuration.exclude_tests:
|
||||
# No need to filter if we have nothing to filter!
|
||||
return test_objs
|
||||
|
||||
if self.verbosity > 1:
|
||||
if self.configuration.include_tests:
|
||||
sys.stdout.write('Tests to include: %s\n' % (self.configuration.include_tests,))
|
||||
|
||||
if self.tests:
|
||||
sys.stdout.write('Tests to run: %s\n' % (self.tests,))
|
||||
|
||||
if self.configuration.exclude_tests:
|
||||
sys.stdout.write('Tests to exclude: %s\n' % (self.configuration.exclude_tests,))
|
||||
|
||||
test_suite = []
|
||||
for test_obj in test_objs:
|
||||
|
||||
if isinstance(test_obj, unittest.TestSuite):
|
||||
# Note: keep the suites as they are and just 'fix' the tests (so, don't use the iter_tests).
|
||||
if test_obj._tests:
|
||||
test_obj._tests = self.filter_tests(test_obj._tests, True)
|
||||
if test_obj._tests: # Only add the suite if we still have tests there.
|
||||
test_suite.append(test_obj)
|
||||
|
||||
elif isinstance(test_obj, unittest.TestCase):
|
||||
try:
|
||||
testMethodName = test_obj._TestCase__testMethodName
|
||||
except AttributeError:
|
||||
# changed in python 2.5
|
||||
testMethodName = test_obj._testMethodName
|
||||
|
||||
add = True
|
||||
if self.configuration.exclude_tests:
|
||||
for pat in self.configuration.exclude_tests:
|
||||
if fnmatch.fnmatchcase(testMethodName, pat):
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('Skipped test: %s (matched exclude_tests pattern: %s)\n' % (testMethodName, pat))
|
||||
|
||||
elif self.verbosity > 2:
|
||||
sys.stdout.write('Skipped test: %s\n' % (testMethodName,))
|
||||
|
||||
add = False
|
||||
break
|
||||
|
||||
if add:
|
||||
if self.__match_tests(self.tests, test_obj, testMethodName):
|
||||
include = True
|
||||
if self.configuration.include_tests:
|
||||
include = False
|
||||
for pat in self.configuration.include_tests:
|
||||
if fnmatch.fnmatchcase(testMethodName, pat):
|
||||
include = True
|
||||
break
|
||||
if include:
|
||||
test_suite.append(test_obj)
|
||||
else:
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('Skipped test: %s (did not match any include_tests pattern %s)\n' % (
|
||||
testMethodName, self.configuration.include_tests,))
|
||||
return test_suite
|
||||
|
||||
def iter_tests(self, test_objs):
|
||||
# Note: not using yield because of Jython 2.1.
|
||||
import unittest
|
||||
tests = []
|
||||
for test_obj in test_objs:
|
||||
if isinstance(test_obj, unittest.TestSuite):
|
||||
tests.extend(self.iter_tests(test_obj._tests))
|
||||
|
||||
elif isinstance(test_obj, unittest.TestCase):
|
||||
tests.append(test_obj)
|
||||
return tests
|
||||
|
||||
def list_test_names(self, test_objs):
|
||||
names = []
|
||||
for tc in self.iter_tests(test_objs):
|
||||
try:
|
||||
testMethodName = tc._TestCase__testMethodName
|
||||
except AttributeError:
|
||||
# changed in python 2.5
|
||||
testMethodName = tc._testMethodName
|
||||
names.append(testMethodName)
|
||||
return names
|
||||
|
||||
def __match_tests(self, tests, test_case, test_method_name):
|
||||
if not tests:
|
||||
return 1
|
||||
|
||||
for t in tests:
|
||||
class_and_method = t.split('.')
|
||||
if len(class_and_method) == 1:
|
||||
# only class name
|
||||
if class_and_method[0] == test_case.__class__.__name__:
|
||||
return 1
|
||||
|
||||
elif len(class_and_method) == 2:
|
||||
if class_and_method[0] == test_case.__class__.__name__ and class_and_method[1] == test_method_name:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def __match(self, filter_list, name):
|
||||
""" returns whether a test name matches the test filter """
|
||||
if filter_list is None:
|
||||
return 1
|
||||
for f in filter_list:
|
||||
if re.match(f, name):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def run_tests(self, handle_coverage=True):
|
||||
""" runs all tests """
|
||||
sys.stdout.write("Finding files... ")
|
||||
files = self.find_import_files()
|
||||
if self.verbosity > 3:
|
||||
sys.stdout.write('%s ... done.\n' % (self.files_or_dirs))
|
||||
else:
|
||||
sys.stdout.write('done.\n')
|
||||
sys.stdout.write("Importing test modules ... ")
|
||||
|
||||
if handle_coverage:
|
||||
coverage_files, coverage = start_coverage_support(self.configuration)
|
||||
|
||||
file_and_modules_and_module_name = self.find_modules_from_files(files)
|
||||
sys.stdout.write("done.\n")
|
||||
|
||||
all_tests = self.find_tests_from_modules(file_and_modules_and_module_name)
|
||||
all_tests = self.filter_tests(all_tests)
|
||||
|
||||
from _pydev_runfiles import pydev_runfiles_unittest
|
||||
test_suite = pydev_runfiles_unittest.PydevTestSuite(all_tests)
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
pydev_runfiles_xml_rpc.notifyTestsCollected(test_suite.countTestCases())
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
def run_tests():
|
||||
executed_in_parallel = False
|
||||
if self.jobs > 1:
|
||||
from _pydev_runfiles import pydev_runfiles_parallel
|
||||
|
||||
# What may happen is that the number of jobs needed is lower than the number of jobs requested
|
||||
# (e.g.: 2 jobs were requested for running 1 test) -- in which case execute_tests_in_parallel will
|
||||
# return False and won't run any tests.
|
||||
executed_in_parallel = pydev_runfiles_parallel.execute_tests_in_parallel(
|
||||
all_tests, self.jobs, self.split_jobs, self.verbosity, coverage_files, self.configuration.coverage_include)
|
||||
|
||||
if not executed_in_parallel:
|
||||
# If in coverage, we don't need to pass anything here (coverage is already enabled for this execution).
|
||||
runner = pydev_runfiles_unittest.PydevTextTestRunner(stream=sys.stdout, descriptions=1, verbosity=self.verbosity)
|
||||
sys.stdout.write('\n')
|
||||
runner.run(test_suite)
|
||||
|
||||
if self.configuration.django:
|
||||
get_django_test_suite_runner()(run_tests).run_tests([])
|
||||
else:
|
||||
run_tests()
|
||||
|
||||
if handle_coverage:
|
||||
coverage.stop()
|
||||
coverage.save()
|
||||
|
||||
total_time = 'Finished in: %.2f secs.' % (time.time() - start_time,)
|
||||
pydev_runfiles_xml_rpc.notifyTestRunFinished(total_time)
|
||||
|
||||
|
||||
DJANGO_TEST_SUITE_RUNNER = None
|
||||
|
||||
|
||||
def get_django_test_suite_runner():
|
||||
global DJANGO_TEST_SUITE_RUNNER
|
||||
if DJANGO_TEST_SUITE_RUNNER:
|
||||
return DJANGO_TEST_SUITE_RUNNER
|
||||
try:
|
||||
# django >= 1.8
|
||||
import django
|
||||
from django.test.runner import DiscoverRunner
|
||||
|
||||
class MyDjangoTestSuiteRunner(DiscoverRunner):
|
||||
|
||||
def __init__(self, on_run_suite):
|
||||
django.setup()
|
||||
DiscoverRunner.__init__(self)
|
||||
self.on_run_suite = on_run_suite
|
||||
|
||||
def build_suite(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def suite_result(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def run_suite(self, *args, **kwargs):
|
||||
self.on_run_suite()
|
||||
|
||||
except:
|
||||
# django < 1.8
|
||||
try:
|
||||
from django.test.simple import DjangoTestSuiteRunner
|
||||
except:
|
||||
|
||||
class DjangoTestSuiteRunner:
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def run_tests(self, *args, **kwargs):
|
||||
raise AssertionError("Unable to run suite with django.test.runner.DiscoverRunner nor django.test.simple.DjangoTestSuiteRunner because it couldn't be imported.")
|
||||
|
||||
class MyDjangoTestSuiteRunner(DjangoTestSuiteRunner):
|
||||
|
||||
def __init__(self, on_run_suite):
|
||||
DjangoTestSuiteRunner.__init__(self)
|
||||
self.on_run_suite = on_run_suite
|
||||
|
||||
def build_suite(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def suite_result(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def run_suite(self, *args, **kwargs):
|
||||
self.on_run_suite()
|
||||
|
||||
DJANGO_TEST_SUITE_RUNNER = MyDjangoTestSuiteRunner
|
||||
return DJANGO_TEST_SUITE_RUNNER
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# main
|
||||
#=======================================================================================================================
|
||||
def main(configuration):
|
||||
PydevTestRunner(configuration).run_tests()
|
||||
@@ -0,0 +1,76 @@
|
||||
import os.path
|
||||
import sys
|
||||
from _pydevd_bundle.pydevd_constants import Null
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# get_coverage_files
|
||||
#=======================================================================================================================
|
||||
def get_coverage_files(coverage_output_dir, number_of_files):
|
||||
base_dir = coverage_output_dir
|
||||
ret = []
|
||||
i = 0
|
||||
while len(ret) < number_of_files:
|
||||
while True:
|
||||
f = os.path.join(base_dir, '.coverage.%s' % i)
|
||||
i += 1
|
||||
if not os.path.exists(f):
|
||||
ret.append(f)
|
||||
break #Break only inner for.
|
||||
return ret
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# start_coverage_support
|
||||
#=======================================================================================================================
|
||||
def start_coverage_support(configuration):
|
||||
return start_coverage_support_from_params(
|
||||
configuration.coverage_output_dir,
|
||||
configuration.coverage_output_file,
|
||||
configuration.jobs,
|
||||
configuration.coverage_include,
|
||||
)
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# start_coverage_support_from_params
|
||||
#=======================================================================================================================
|
||||
def start_coverage_support_from_params(coverage_output_dir, coverage_output_file, jobs, coverage_include):
|
||||
coverage_files = []
|
||||
coverage_instance = Null()
|
||||
if coverage_output_dir or coverage_output_file:
|
||||
try:
|
||||
import coverage #@UnresolvedImport
|
||||
except:
|
||||
sys.stderr.write('Error: coverage module could not be imported\n')
|
||||
sys.stderr.write('Please make sure that the coverage module (http://nedbatchelder.com/code/coverage/)\n')
|
||||
sys.stderr.write('is properly installed in your interpreter: %s\n' % (sys.executable,))
|
||||
|
||||
import traceback;traceback.print_exc()
|
||||
else:
|
||||
if coverage_output_dir:
|
||||
if not os.path.exists(coverage_output_dir):
|
||||
sys.stderr.write('Error: directory for coverage output (%s) does not exist.\n' % (coverage_output_dir,))
|
||||
|
||||
elif not os.path.isdir(coverage_output_dir):
|
||||
sys.stderr.write('Error: expected (%s) to be a directory.\n' % (coverage_output_dir,))
|
||||
|
||||
else:
|
||||
n = jobs
|
||||
if n <= 0:
|
||||
n += 1
|
||||
n += 1 #Add 1 more for the current process (which will do the initial import).
|
||||
coverage_files = get_coverage_files(coverage_output_dir, n)
|
||||
os.environ['COVERAGE_FILE'] = coverage_files.pop(0)
|
||||
|
||||
coverage_instance = coverage.coverage(source=[coverage_include])
|
||||
coverage_instance.start()
|
||||
|
||||
elif coverage_output_file:
|
||||
#Client of parallel run.
|
||||
os.environ['COVERAGE_FILE'] = coverage_output_file
|
||||
coverage_instance = coverage.coverage(source=[coverage_include])
|
||||
coverage_instance.start()
|
||||
|
||||
return coverage_files, coverage_instance
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
from nose.plugins.multiprocess import MultiProcessTestRunner # @UnresolvedImport
|
||||
from nose.plugins.base import Plugin # @UnresolvedImport
|
||||
import sys
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
import time
|
||||
from _pydev_runfiles.pydev_runfiles_coverage import start_coverage_support
|
||||
from contextlib import contextmanager
|
||||
from io import StringIO
|
||||
import traceback
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# PydevPlugin
|
||||
#=======================================================================================================================
|
||||
class PydevPlugin(Plugin):
|
||||
|
||||
def __init__(self, configuration):
|
||||
self.configuration = configuration
|
||||
Plugin.__init__(self)
|
||||
|
||||
def begin(self):
|
||||
# Called before any test is run (it's always called, with multiprocess or not)
|
||||
self.start_time = time.time()
|
||||
self.coverage_files, self.coverage = start_coverage_support(self.configuration)
|
||||
|
||||
def finalize(self, result):
|
||||
# Called after all tests are run (it's always called, with multiprocess or not)
|
||||
self.coverage.stop()
|
||||
self.coverage.save()
|
||||
|
||||
pydev_runfiles_xml_rpc.notifyTestRunFinished('Finished in: %.2f secs.' % (time.time() - self.start_time,))
|
||||
|
||||
#===================================================================================================================
|
||||
# Methods below are not called with multiprocess (so, we monkey-patch MultiProcessTestRunner.consolidate
|
||||
# so that they're called, but unfortunately we loose some info -- i.e.: the time for each test in this
|
||||
# process).
|
||||
#===================================================================================================================
|
||||
|
||||
class Sentinel(object):
|
||||
pass
|
||||
|
||||
@contextmanager
|
||||
def _without_user_address(self, test):
|
||||
# #PyDev-1095: Conflict between address in test and test.address() in PydevPlugin().report_cond()
|
||||
user_test_instance = test.test
|
||||
user_address = self.Sentinel
|
||||
user_class_address = self.Sentinel
|
||||
try:
|
||||
if 'address' in user_test_instance.__dict__:
|
||||
user_address = user_test_instance.__dict__.pop('address')
|
||||
except:
|
||||
# Just ignore anything here.
|
||||
pass
|
||||
try:
|
||||
user_class_address = user_test_instance.__class__.address
|
||||
del user_test_instance.__class__.address
|
||||
except:
|
||||
# Just ignore anything here.
|
||||
pass
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if user_address is not self.Sentinel:
|
||||
user_test_instance.__dict__['address'] = user_address
|
||||
|
||||
if user_class_address is not self.Sentinel:
|
||||
user_test_instance.__class__.address = user_class_address
|
||||
|
||||
def _get_test_address(self, test):
|
||||
try:
|
||||
if hasattr(test, 'address'):
|
||||
with self._without_user_address(test):
|
||||
address = test.address()
|
||||
|
||||
# test.address() is something as:
|
||||
# ('D:\\workspaces\\temp\\test_workspace\\pytesting1\\src\\mod1\\hello.py', 'mod1.hello', 'TestCase.testMet1')
|
||||
#
|
||||
# and we must pass: location, test
|
||||
# E.g.: ['D:\\src\\mod1\\hello.py', 'TestCase.testMet1']
|
||||
address = address[0], address[2]
|
||||
else:
|
||||
# multiprocess
|
||||
try:
|
||||
address = test[0], test[1]
|
||||
except TypeError:
|
||||
# It may be an error at setup, in which case it's not really a test, but a Context object.
|
||||
f = test.context.__file__
|
||||
if f.endswith('.pyc'):
|
||||
f = f[:-1]
|
||||
elif f.endswith('$py.class'):
|
||||
f = f[:-len('$py.class')] + '.py'
|
||||
address = f, '?'
|
||||
except:
|
||||
sys.stderr.write("PyDev: Internal pydev error getting test address. Please report at the pydev bug tracker\n")
|
||||
traceback.print_exc()
|
||||
sys.stderr.write("\n\n\n")
|
||||
address = '?', '?'
|
||||
return address
|
||||
|
||||
def report_cond(self, cond, test, captured_output, error=''):
|
||||
'''
|
||||
@param cond: fail, error, ok
|
||||
'''
|
||||
|
||||
address = self._get_test_address(test)
|
||||
|
||||
error_contents = self.get_io_from_error(error)
|
||||
try:
|
||||
time_str = '%.2f' % (time.time() - test._pydev_start_time)
|
||||
except:
|
||||
time_str = '?'
|
||||
|
||||
pydev_runfiles_xml_rpc.notifyTest(cond, captured_output, error_contents, address[0], address[1], time_str)
|
||||
|
||||
def startTest(self, test):
|
||||
test._pydev_start_time = time.time()
|
||||
file, test = self._get_test_address(test)
|
||||
pydev_runfiles_xml_rpc.notifyStartTest(file, test)
|
||||
|
||||
def get_io_from_error(self, err):
|
||||
if type(err) == type(()):
|
||||
if len(err) != 3:
|
||||
if len(err) == 2:
|
||||
return err[1] # multiprocess
|
||||
s = StringIO()
|
||||
etype, value, tb = err
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
traceback.print_exception(etype, value, tb, file=s)
|
||||
return s.getvalue()
|
||||
return err
|
||||
|
||||
def get_captured_output(self, test):
|
||||
if hasattr(test, 'capturedOutput') and test.capturedOutput:
|
||||
return test.capturedOutput
|
||||
return ''
|
||||
|
||||
def addError(self, test, err):
|
||||
self.report_cond(
|
||||
'error',
|
||||
test,
|
||||
self.get_captured_output(test),
|
||||
err,
|
||||
)
|
||||
|
||||
def addFailure(self, test, err):
|
||||
self.report_cond(
|
||||
'fail',
|
||||
test,
|
||||
self.get_captured_output(test),
|
||||
err,
|
||||
)
|
||||
|
||||
def addSuccess(self, test):
|
||||
self.report_cond(
|
||||
'ok',
|
||||
test,
|
||||
self.get_captured_output(test),
|
||||
'',
|
||||
)
|
||||
|
||||
|
||||
PYDEV_NOSE_PLUGIN_SINGLETON = None
|
||||
|
||||
|
||||
def start_pydev_nose_plugin_singleton(configuration):
|
||||
global PYDEV_NOSE_PLUGIN_SINGLETON
|
||||
PYDEV_NOSE_PLUGIN_SINGLETON = PydevPlugin(configuration)
|
||||
return PYDEV_NOSE_PLUGIN_SINGLETON
|
||||
|
||||
|
||||
original = MultiProcessTestRunner.consolidate
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# new_consolidate
|
||||
#=======================================================================================================================
|
||||
def new_consolidate(self, result, batch_result):
|
||||
'''
|
||||
Used so that it can work with the multiprocess plugin.
|
||||
Monkeypatched because nose seems a bit unsupported at this time (ideally
|
||||
the plugin would have this support by default).
|
||||
'''
|
||||
ret = original(self, result, batch_result)
|
||||
|
||||
parent_frame = sys._getframe().f_back
|
||||
# addr is something as D:\pytesting1\src\mod1\hello.py:TestCase.testMet4
|
||||
# so, convert it to what report_cond expects
|
||||
addr = parent_frame.f_locals['addr']
|
||||
i = addr.rindex(':')
|
||||
addr = [addr[:i], addr[i + 1:]]
|
||||
|
||||
output, testsRun, failures, errors, errorClasses = batch_result
|
||||
if failures or errors:
|
||||
for failure in failures:
|
||||
PYDEV_NOSE_PLUGIN_SINGLETON.report_cond('fail', addr, output, failure)
|
||||
|
||||
for error in errors:
|
||||
PYDEV_NOSE_PLUGIN_SINGLETON.report_cond('error', addr, output, error)
|
||||
else:
|
||||
PYDEV_NOSE_PLUGIN_SINGLETON.report_cond('ok', addr, output)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
MultiProcessTestRunner.consolidate = new_consolidate
|
||||
@@ -0,0 +1,267 @@
|
||||
import unittest
|
||||
from _pydev_bundle._pydev_saved_modules import thread
|
||||
import queue as Queue
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
import time
|
||||
import os
|
||||
import threading
|
||||
import sys
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# flatten_test_suite
|
||||
#=======================================================================================================================
|
||||
def flatten_test_suite(test_suite, ret):
|
||||
if isinstance(test_suite, unittest.TestSuite):
|
||||
for t in test_suite._tests:
|
||||
flatten_test_suite(t, ret)
|
||||
|
||||
elif isinstance(test_suite, unittest.TestCase):
|
||||
ret.append(test_suite)
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# execute_tests_in_parallel
|
||||
#=======================================================================================================================
|
||||
def execute_tests_in_parallel(tests, jobs, split, verbosity, coverage_files, coverage_include):
|
||||
'''
|
||||
@param tests: list(PydevTestSuite)
|
||||
A list with the suites to be run
|
||||
|
||||
@param split: str
|
||||
Either 'module' or the number of tests that should be run in each batch
|
||||
|
||||
@param coverage_files: list(file)
|
||||
A list with the files that should be used for giving coverage information (if empty, coverage information
|
||||
should not be gathered).
|
||||
|
||||
@param coverage_include: str
|
||||
The pattern that should be included in the coverage.
|
||||
|
||||
@return: bool
|
||||
Returns True if the tests were actually executed in parallel. If the tests were not executed because only 1
|
||||
should be used (e.g.: 2 jobs were requested for running 1 test), False will be returned and no tests will be
|
||||
run.
|
||||
|
||||
It may also return False if in debug mode (in which case, multi-processes are not accepted)
|
||||
'''
|
||||
try:
|
||||
from _pydevd_bundle.pydevd_comm import get_global_debugger
|
||||
if get_global_debugger() is not None:
|
||||
return False
|
||||
except:
|
||||
pass # Ignore any error here.
|
||||
|
||||
# This queue will receive the tests to be run. Each entry in a queue is a list with the tests to be run together When
|
||||
# split == 'tests', each list will have a single element, when split == 'module', each list will have all the tests
|
||||
# from a given module.
|
||||
tests_queue = []
|
||||
|
||||
queue_elements = []
|
||||
if split == 'module':
|
||||
module_to_tests = {}
|
||||
for test in tests:
|
||||
lst = []
|
||||
flatten_test_suite(test, lst)
|
||||
for test in lst:
|
||||
key = (test.__pydev_pyfile__, test.__pydev_module_name__)
|
||||
module_to_tests.setdefault(key, []).append(test)
|
||||
|
||||
for key, tests in module_to_tests.items():
|
||||
queue_elements.append(tests)
|
||||
|
||||
if len(queue_elements) < jobs:
|
||||
# Don't create jobs we will never use.
|
||||
jobs = len(queue_elements)
|
||||
|
||||
elif split == 'tests':
|
||||
for test in tests:
|
||||
lst = []
|
||||
flatten_test_suite(test, lst)
|
||||
for test in lst:
|
||||
queue_elements.append([test])
|
||||
|
||||
if len(queue_elements) < jobs:
|
||||
# Don't create jobs we will never use.
|
||||
jobs = len(queue_elements)
|
||||
|
||||
else:
|
||||
raise AssertionError('Do not know how to handle: %s' % (split,))
|
||||
|
||||
for test_cases in queue_elements:
|
||||
test_queue_elements = []
|
||||
for test_case in test_cases:
|
||||
try:
|
||||
test_name = test_case.__class__.__name__ + "." + test_case._testMethodName
|
||||
except AttributeError:
|
||||
# Support for jython 2.1 (__testMethodName is pseudo-private in the test case)
|
||||
test_name = test_case.__class__.__name__ + "." + test_case._TestCase__testMethodName
|
||||
|
||||
test_queue_elements.append(test_case.__pydev_pyfile__ + '|' + test_name)
|
||||
|
||||
tests_queue.append(test_queue_elements)
|
||||
|
||||
if jobs < 2:
|
||||
return False
|
||||
|
||||
sys.stdout.write('Running tests in parallel with: %s jobs.\n' % (jobs,))
|
||||
|
||||
queue = Queue.Queue()
|
||||
for item in tests_queue:
|
||||
queue.put(item, block=False)
|
||||
|
||||
providers = []
|
||||
clients = []
|
||||
for i in range(jobs):
|
||||
test_cases_provider = CommunicationThread(queue)
|
||||
providers.append(test_cases_provider)
|
||||
|
||||
test_cases_provider.start()
|
||||
port = test_cases_provider.port
|
||||
|
||||
if coverage_files:
|
||||
clients.append(ClientThread(i, port, verbosity, coverage_files.pop(0), coverage_include))
|
||||
else:
|
||||
clients.append(ClientThread(i, port, verbosity))
|
||||
|
||||
for client in clients:
|
||||
client.start()
|
||||
|
||||
client_alive = True
|
||||
while client_alive:
|
||||
client_alive = False
|
||||
for client in clients:
|
||||
# Wait for all the clients to exit.
|
||||
if not client.finished:
|
||||
client_alive = True
|
||||
time.sleep(.2)
|
||||
break
|
||||
|
||||
for provider in providers:
|
||||
provider.shutdown()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# CommunicationThread
|
||||
#=======================================================================================================================
|
||||
class CommunicationThread(threading.Thread):
|
||||
|
||||
def __init__(self, tests_queue):
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.queue = tests_queue
|
||||
self.finished = False
|
||||
from _pydev_bundle.pydev_imports import SimpleXMLRPCServer
|
||||
from _pydev_bundle import pydev_localhost
|
||||
|
||||
# Create server
|
||||
server = SimpleXMLRPCServer((pydev_localhost.get_localhost(), 0), logRequests=False)
|
||||
server.register_function(self.GetTestsToRun)
|
||||
server.register_function(self.notifyStartTest)
|
||||
server.register_function(self.notifyTest)
|
||||
server.register_function(self.notifyCommands)
|
||||
self.port = server.socket.getsockname()[1]
|
||||
self.server = server
|
||||
|
||||
def GetTestsToRun(self, job_id):
|
||||
'''
|
||||
@param job_id:
|
||||
|
||||
@return: list(str)
|
||||
Each entry is a string in the format: filename|Test.testName
|
||||
'''
|
||||
try:
|
||||
ret = self.queue.get(block=False)
|
||||
return ret
|
||||
except: # Any exception getting from the queue (empty or not) means we finished our work on providing the tests.
|
||||
self.finished = True
|
||||
return []
|
||||
|
||||
def notifyCommands(self, job_id, commands):
|
||||
# Batch notification.
|
||||
for command in commands:
|
||||
getattr(self, command[0])(job_id, *command[1], **command[2])
|
||||
|
||||
return True
|
||||
|
||||
def notifyStartTest(self, job_id, *args, **kwargs):
|
||||
pydev_runfiles_xml_rpc.notifyStartTest(*args, **kwargs)
|
||||
return True
|
||||
|
||||
def notifyTest(self, job_id, *args, **kwargs):
|
||||
pydev_runfiles_xml_rpc.notifyTest(*args, **kwargs)
|
||||
return True
|
||||
|
||||
def shutdown(self):
|
||||
if hasattr(self.server, 'shutdown'):
|
||||
self.server.shutdown()
|
||||
else:
|
||||
self._shutdown = True
|
||||
|
||||
def run(self):
|
||||
if hasattr(self.server, 'shutdown'):
|
||||
self.server.serve_forever()
|
||||
else:
|
||||
self._shutdown = False
|
||||
while not self._shutdown:
|
||||
self.server.handle_request()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# Client
|
||||
#=======================================================================================================================
|
||||
class ClientThread(threading.Thread):
|
||||
|
||||
def __init__(self, job_id, port, verbosity, coverage_output_file=None, coverage_include=None):
|
||||
threading.Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.port = port
|
||||
self.job_id = job_id
|
||||
self.verbosity = verbosity
|
||||
self.finished = False
|
||||
self.coverage_output_file = coverage_output_file
|
||||
self.coverage_include = coverage_include
|
||||
|
||||
def _reader_thread(self, pipe, target):
|
||||
while True:
|
||||
target.write(pipe.read(1))
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from _pydev_runfiles import pydev_runfiles_parallel_client
|
||||
# TODO: Support Jython:
|
||||
#
|
||||
# For jython, instead of using sys.executable, we should use:
|
||||
# r'D:\bin\jdk_1_5_09\bin\java.exe',
|
||||
# '-classpath',
|
||||
# 'D:/bin/jython-2.2.1/jython.jar',
|
||||
# 'org.python.util.jython',
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
pydev_runfiles_parallel_client.__file__,
|
||||
str(self.job_id),
|
||||
str(self.port),
|
||||
str(self.verbosity),
|
||||
]
|
||||
|
||||
if self.coverage_output_file and self.coverage_include:
|
||||
args.append(self.coverage_output_file)
|
||||
args.append(self.coverage_include)
|
||||
|
||||
import subprocess
|
||||
if False:
|
||||
proc = subprocess.Popen(args, env=os.environ, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
thread.start_new_thread(self._reader_thread, (proc.stdout, sys.stdout))
|
||||
|
||||
thread.start_new_thread(target=self._reader_thread, args=(proc.stderr, sys.stderr))
|
||||
else:
|
||||
proc = subprocess.Popen(args, env=os.environ, shell=False)
|
||||
proc.wait()
|
||||
|
||||
finally:
|
||||
self.finished = True
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
from _pydev_bundle.pydev_imports import xmlrpclib, _queue
|
||||
Queue = _queue.Queue
|
||||
import traceback
|
||||
import sys
|
||||
from _pydev_runfiles.pydev_runfiles_coverage import start_coverage_support_from_params
|
||||
import threading
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ParallelNotification
|
||||
#=======================================================================================================================
|
||||
class ParallelNotification(object):
|
||||
|
||||
def __init__(self, method, args, kwargs):
|
||||
self.method = method
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def to_tuple(self):
|
||||
return self.method, self.args, self.kwargs
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# KillServer
|
||||
#=======================================================================================================================
|
||||
class KillServer(object):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ServerComm
|
||||
#=======================================================================================================================
|
||||
class ServerComm(threading.Thread):
|
||||
|
||||
|
||||
|
||||
def __init__(self, job_id, server):
|
||||
self.notifications_queue = Queue()
|
||||
threading.Thread.__init__(self)
|
||||
self.setDaemon(False) #Wait for all the notifications to be passed before exiting!
|
||||
assert job_id is not None
|
||||
assert port is not None
|
||||
self.job_id = job_id
|
||||
|
||||
self.finished = False
|
||||
self.server = server
|
||||
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
kill_found = False
|
||||
commands = []
|
||||
command = self.notifications_queue.get(block=True)
|
||||
if isinstance(command, KillServer):
|
||||
kill_found = True
|
||||
else:
|
||||
assert isinstance(command, ParallelNotification)
|
||||
commands.append(command.to_tuple())
|
||||
|
||||
try:
|
||||
while True:
|
||||
command = self.notifications_queue.get(block=False) #No block to create a batch.
|
||||
if isinstance(command, KillServer):
|
||||
kill_found = True
|
||||
else:
|
||||
assert isinstance(command, ParallelNotification)
|
||||
commands.append(command.to_tuple())
|
||||
except:
|
||||
pass #That's OK, we're getting it until it becomes empty so that we notify multiple at once.
|
||||
|
||||
|
||||
if commands:
|
||||
try:
|
||||
#Batch notification.
|
||||
self.server.lock.acquire()
|
||||
try:
|
||||
self.server.notifyCommands(self.job_id, commands)
|
||||
finally:
|
||||
self.server.lock.release()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
if kill_found:
|
||||
self.finished = True
|
||||
return
|
||||
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ServerFacade
|
||||
#=======================================================================================================================
|
||||
class ServerFacade(object):
|
||||
|
||||
|
||||
def __init__(self, notifications_queue):
|
||||
self.notifications_queue = notifications_queue
|
||||
|
||||
|
||||
def notifyTestsCollected(self, *args, **kwargs):
|
||||
pass #This notification won't be passed
|
||||
|
||||
|
||||
def notifyTestRunFinished(self, *args, **kwargs):
|
||||
pass #This notification won't be passed
|
||||
|
||||
|
||||
def notifyStartTest(self, *args, **kwargs):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyStartTest', args, kwargs))
|
||||
|
||||
|
||||
def notifyTest(self, *args, **kwargs):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyTest', args, kwargs))
|
||||
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# run_client
|
||||
#=======================================================================================================================
|
||||
def run_client(job_id, port, verbosity, coverage_output_file, coverage_include):
|
||||
job_id = int(job_id)
|
||||
|
||||
from _pydev_bundle import pydev_localhost
|
||||
server = xmlrpclib.Server('http://%s:%s' % (pydev_localhost.get_localhost(), port))
|
||||
server.lock = threading.Lock()
|
||||
|
||||
|
||||
server_comm = ServerComm(job_id, server)
|
||||
server_comm.start()
|
||||
|
||||
try:
|
||||
server_facade = ServerFacade(server_comm.notifications_queue)
|
||||
from _pydev_runfiles import pydev_runfiles
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
pydev_runfiles_xml_rpc.set_server(server_facade)
|
||||
|
||||
#Starts None and when the 1st test is gotten, it's started (because a server may be initiated and terminated
|
||||
#before receiving any test -- which would mean a different process got all the tests to run).
|
||||
coverage = None
|
||||
|
||||
try:
|
||||
tests_to_run = [1]
|
||||
while tests_to_run:
|
||||
#Investigate: is it dangerous to use the same xmlrpclib server from different threads?
|
||||
#It seems it should be, as it creates a new connection for each request...
|
||||
server.lock.acquire()
|
||||
try:
|
||||
tests_to_run = server.GetTestsToRun(job_id)
|
||||
finally:
|
||||
server.lock.release()
|
||||
|
||||
if not tests_to_run:
|
||||
break
|
||||
|
||||
if coverage is None:
|
||||
_coverage_files, coverage = start_coverage_support_from_params(
|
||||
None, coverage_output_file, 1, coverage_include)
|
||||
|
||||
|
||||
files_to_tests = {}
|
||||
for test in tests_to_run:
|
||||
filename_and_test = test.split('|')
|
||||
if len(filename_and_test) == 2:
|
||||
files_to_tests.setdefault(filename_and_test[0], []).append(filename_and_test[1])
|
||||
|
||||
configuration = pydev_runfiles.Configuration(
|
||||
'',
|
||||
verbosity,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
files_to_tests,
|
||||
1, #Always single job here
|
||||
None,
|
||||
|
||||
#The coverage is handled in this loop.
|
||||
coverage_output_file=None,
|
||||
coverage_include=None,
|
||||
)
|
||||
test_runner = pydev_runfiles.PydevTestRunner(configuration)
|
||||
sys.stdout.flush()
|
||||
test_runner.run_tests(handle_coverage=False)
|
||||
finally:
|
||||
if coverage is not None:
|
||||
coverage.stop()
|
||||
coverage.save()
|
||||
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
server_comm.notifications_queue.put_nowait(KillServer())
|
||||
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# main
|
||||
#=======================================================================================================================
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) -1 == 3:
|
||||
job_id, port, verbosity = sys.argv[1:]
|
||||
coverage_output_file, coverage_include = None, None
|
||||
|
||||
elif len(sys.argv) -1 == 5:
|
||||
job_id, port, verbosity, coverage_output_file, coverage_include = sys.argv[1:]
|
||||
|
||||
else:
|
||||
raise AssertionError('Could not find out how to handle the parameters: '+sys.argv[1:])
|
||||
|
||||
job_id = int(job_id)
|
||||
port = int(port)
|
||||
verbosity = int(verbosity)
|
||||
run_client(job_id, port, verbosity, coverage_output_file, coverage_include)
|
||||
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
import pickle
|
||||
import zlib
|
||||
import base64
|
||||
import os
|
||||
from pydevd_file_utils import canonical_normalized_path
|
||||
import pytest
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
#=========================================================================
|
||||
# Load filters with tests we should skip
|
||||
#=========================================================================
|
||||
py_test_accept_filter = None
|
||||
|
||||
|
||||
def _load_filters():
|
||||
global py_test_accept_filter
|
||||
if py_test_accept_filter is None:
|
||||
py_test_accept_filter = os.environ.get('PYDEV_PYTEST_SKIP')
|
||||
if py_test_accept_filter:
|
||||
py_test_accept_filter = pickle.loads(
|
||||
zlib.decompress(base64.b64decode(py_test_accept_filter)))
|
||||
|
||||
# Newer versions of pytest resolve symlinks, so, we
|
||||
# may need to filter with a resolved path too.
|
||||
new_dct = {}
|
||||
for filename, value in py_test_accept_filter.items():
|
||||
new_dct[canonical_normalized_path(str(Path(filename).resolve()))] = value
|
||||
|
||||
py_test_accept_filter.update(new_dct)
|
||||
|
||||
else:
|
||||
py_test_accept_filter = {}
|
||||
|
||||
|
||||
def is_in_xdist_node():
|
||||
main_pid = os.environ.get('PYDEV_MAIN_PID')
|
||||
if main_pid and main_pid != str(os.getpid()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
connected = False
|
||||
|
||||
|
||||
def connect_to_server_for_communication_to_xml_rpc_on_xdist():
|
||||
global connected
|
||||
if connected:
|
||||
return
|
||||
connected = True
|
||||
if is_in_xdist_node():
|
||||
port = os.environ.get('PYDEV_PYTEST_SERVER')
|
||||
if not port:
|
||||
sys.stderr.write(
|
||||
'Error: no PYDEV_PYTEST_SERVER environment variable defined.\n')
|
||||
else:
|
||||
pydev_runfiles_xml_rpc.initialize_server(int(port), daemon=True)
|
||||
|
||||
|
||||
PY2 = sys.version_info[0] <= 2
|
||||
PY3 = not PY2
|
||||
|
||||
|
||||
class State:
|
||||
start_time = time.time()
|
||||
buf_err = None
|
||||
buf_out = None
|
||||
|
||||
|
||||
def start_redirect():
|
||||
if State.buf_out is not None:
|
||||
return
|
||||
from _pydevd_bundle import pydevd_io
|
||||
State.buf_err = pydevd_io.start_redirect(keep_original_redirection=True, std='stderr')
|
||||
State.buf_out = pydevd_io.start_redirect(keep_original_redirection=True, std='stdout')
|
||||
|
||||
|
||||
def get_curr_output():
|
||||
buf_out = State.buf_out
|
||||
buf_err = State.buf_err
|
||||
return buf_out.getvalue() if buf_out is not None else '', buf_err.getvalue() if buf_err is not None else ''
|
||||
|
||||
|
||||
def pytest_unconfigure():
|
||||
if is_in_xdist_node():
|
||||
return
|
||||
# Only report that it finished when on the main node (we don't want to report
|
||||
# the finish on each separate node).
|
||||
pydev_runfiles_xml_rpc.notifyTestRunFinished(
|
||||
'Finished in: %.2f secs.' % (time.time() - State.start_time,))
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
# A note: in xdist, this is not called on the main process, only in the
|
||||
# secondary nodes, so, we'll actually make the filter and report it multiple
|
||||
# times.
|
||||
connect_to_server_for_communication_to_xml_rpc_on_xdist()
|
||||
|
||||
_load_filters()
|
||||
if not py_test_accept_filter:
|
||||
pydev_runfiles_xml_rpc.notifyTestsCollected(len(items))
|
||||
return # Keep on going (nothing to filter)
|
||||
|
||||
new_items = []
|
||||
for item in items:
|
||||
f = canonical_normalized_path(str(item.parent.fspath))
|
||||
name = item.name
|
||||
|
||||
if f not in py_test_accept_filter:
|
||||
# print('Skip file: %s' % (f,))
|
||||
continue # Skip the file
|
||||
|
||||
i = name.find('[')
|
||||
name_without_parametrize = None
|
||||
if i > 0:
|
||||
name_without_parametrize = name[:i]
|
||||
|
||||
accept_tests = py_test_accept_filter[f]
|
||||
|
||||
if item.cls is not None:
|
||||
class_name = item.cls.__name__
|
||||
else:
|
||||
class_name = None
|
||||
for test in accept_tests:
|
||||
if test == name:
|
||||
# Direct match of the test (just go on with the default
|
||||
# loading)
|
||||
new_items.append(item)
|
||||
break
|
||||
|
||||
if name_without_parametrize is not None and test == name_without_parametrize:
|
||||
# This happens when parameterizing pytest tests on older versions
|
||||
# of pytest where the test name doesn't include the fixture name
|
||||
# in it.
|
||||
new_items.append(item)
|
||||
break
|
||||
|
||||
if class_name is not None:
|
||||
if test == class_name + '.' + name:
|
||||
new_items.append(item)
|
||||
break
|
||||
|
||||
if name_without_parametrize is not None and test == class_name + '.' + name_without_parametrize:
|
||||
new_items.append(item)
|
||||
break
|
||||
|
||||
if class_name == test:
|
||||
new_items.append(item)
|
||||
break
|
||||
else:
|
||||
pass
|
||||
# print('Skip test: %s.%s. Accept: %s' % (class_name, name, accept_tests))
|
||||
|
||||
# Modify the original list
|
||||
items[:] = new_items
|
||||
pydev_runfiles_xml_rpc.notifyTestsCollected(len(items))
|
||||
|
||||
|
||||
try:
|
||||
"""
|
||||
pytest > 5.4 uses own version of TerminalWriter based on py.io.TerminalWriter
|
||||
and assumes there is a specific method TerminalWriter._write_source
|
||||
so try load pytest version first or fallback to default one
|
||||
"""
|
||||
from _pytest._io import TerminalWriter
|
||||
except ImportError:
|
||||
from py.io import TerminalWriter
|
||||
|
||||
|
||||
def _get_error_contents_from_report(report):
|
||||
if report.longrepr is not None:
|
||||
try:
|
||||
tw = TerminalWriter(stringio=True)
|
||||
stringio = tw.stringio
|
||||
except TypeError:
|
||||
import io
|
||||
stringio = io.StringIO()
|
||||
tw = TerminalWriter(file=stringio)
|
||||
tw.hasmarkup = False
|
||||
report.toterminal(tw)
|
||||
exc = stringio.getvalue()
|
||||
s = exc.strip()
|
||||
if s:
|
||||
return s
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def pytest_collectreport(report):
|
||||
error_contents = _get_error_contents_from_report(report)
|
||||
if error_contents:
|
||||
report_test('fail', '<collect errors>', '<collect errors>', '', error_contents, 0.0)
|
||||
|
||||
|
||||
def append_strings(s1, s2):
|
||||
if s1.__class__ == s2.__class__:
|
||||
return s1 + s2
|
||||
|
||||
# Prefer str
|
||||
if isinstance(s1, bytes):
|
||||
s1 = s1.decode('utf-8', 'replace')
|
||||
|
||||
if isinstance(s2, bytes):
|
||||
s2 = s2.decode('utf-8', 'replace')
|
||||
|
||||
return s1 + s2
|
||||
|
||||
|
||||
def pytest_runtest_logreport(report):
|
||||
if is_in_xdist_node():
|
||||
# When running with xdist, we don't want the report to be called from the node, only
|
||||
# from the main process.
|
||||
return
|
||||
report_duration = report.duration
|
||||
report_when = report.when
|
||||
report_outcome = report.outcome
|
||||
|
||||
if hasattr(report, 'wasxfail'):
|
||||
if report_outcome != 'skipped':
|
||||
report_outcome = 'passed'
|
||||
|
||||
if report_outcome == 'passed':
|
||||
# passed on setup/teardown: no need to report if in setup or teardown
|
||||
# (only on the actual test if it passed).
|
||||
if report_when in ('setup', 'teardown'):
|
||||
return
|
||||
|
||||
status = 'ok'
|
||||
|
||||
elif report_outcome == 'skipped':
|
||||
status = 'skip'
|
||||
|
||||
else:
|
||||
# It has only passed, skipped and failed (no error), so, let's consider
|
||||
# error if not on call.
|
||||
if report_when in ('setup', 'teardown'):
|
||||
status = 'error'
|
||||
|
||||
else:
|
||||
# any error in the call (not in setup or teardown) is considered a
|
||||
# regular failure.
|
||||
status = 'fail'
|
||||
|
||||
# This will work if pytest is not capturing it, if it is, nothing will
|
||||
# come from here...
|
||||
captured_output, error_contents = getattr(report, 'pydev_captured_output', ''), getattr(report, 'pydev_error_contents', '')
|
||||
for type_section, value in report.sections:
|
||||
if value:
|
||||
if type_section in ('err', 'stderr', 'Captured stderr call'):
|
||||
error_contents = append_strings(error_contents, value)
|
||||
else:
|
||||
captured_output = append_strings(error_contents, value)
|
||||
|
||||
filename = getattr(report, 'pydev_fspath_strpath', '<unable to get>')
|
||||
test = report.location[2]
|
||||
|
||||
if report_outcome != 'skipped':
|
||||
# On skipped, we'll have a traceback for the skip, which is not what we
|
||||
# want.
|
||||
exc = _get_error_contents_from_report(report)
|
||||
if exc:
|
||||
if error_contents:
|
||||
error_contents = append_strings(error_contents, '----------------------------- Exceptions -----------------------------\n')
|
||||
error_contents = append_strings(error_contents, exc)
|
||||
|
||||
report_test(status, filename, test, captured_output, error_contents, report_duration)
|
||||
|
||||
|
||||
def report_test(status, filename, test, captured_output, error_contents, duration):
|
||||
'''
|
||||
@param filename: 'D:\\src\\mod1\\hello.py'
|
||||
@param test: 'TestCase.testMet1'
|
||||
@param status: fail, error, ok
|
||||
'''
|
||||
time_str = '%.2f' % (duration,)
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
status, captured_output, error_contents, filename, test, time_str)
|
||||
|
||||
|
||||
if not hasattr(pytest, 'hookimpl'):
|
||||
raise AssertionError('Please upgrade pytest (the current version of pytest: %s is unsupported)' % (pytest.__version__,))
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
report = outcome.get_result()
|
||||
report.pydev_fspath_strpath = item.fspath.strpath
|
||||
report.pydev_captured_output, report.pydev_error_contents = get_curr_output()
|
||||
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_setup(item):
|
||||
'''
|
||||
Note: with xdist will be on a secondary process.
|
||||
'''
|
||||
# We have our own redirection: if xdist does its redirection, we'll have
|
||||
# nothing in our contents (which is OK), but if it does, we'll get nothing
|
||||
# from pytest but will get our own here.
|
||||
start_redirect()
|
||||
filename = item.fspath.strpath
|
||||
test = item.location[2]
|
||||
|
||||
pydev_runfiles_xml_rpc.notifyStartTest(filename, test)
|
||||
@@ -0,0 +1,150 @@
|
||||
import unittest as python_unittest
|
||||
from _pydev_runfiles import pydev_runfiles_xml_rpc
|
||||
import time
|
||||
from _pydevd_bundle import pydevd_io
|
||||
import traceback
|
||||
from _pydevd_bundle.pydevd_constants import * # @UnusedWildImport
|
||||
from io import StringIO
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# PydevTextTestRunner
|
||||
#=======================================================================================================================
|
||||
class PydevTextTestRunner(python_unittest.TextTestRunner):
|
||||
|
||||
def _makeResult(self):
|
||||
return PydevTestResult(self.stream, self.descriptions, self.verbosity)
|
||||
|
||||
|
||||
_PythonTextTestResult = python_unittest.TextTestRunner()._makeResult().__class__
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# PydevTestResult
|
||||
#=======================================================================================================================
|
||||
class PydevTestResult(_PythonTextTestResult):
|
||||
|
||||
def addSubTest(self, test, subtest, err):
|
||||
"""Called at the end of a subtest.
|
||||
'err' is None if the subtest ended successfully, otherwise it's a
|
||||
tuple of values as returned by sys.exc_info().
|
||||
"""
|
||||
_PythonTextTestResult.addSubTest(self, test, subtest, err)
|
||||
if err is not None:
|
||||
subdesc = subtest._subDescription()
|
||||
error = (test, self._exc_info_to_string(err, test))
|
||||
self._reportErrors([error], [], '', '%s %s' % (self.get_test_name(test), subdesc))
|
||||
|
||||
def startTest(self, test):
|
||||
_PythonTextTestResult.startTest(self, test)
|
||||
self.buf = pydevd_io.start_redirect(keep_original_redirection=True, std='both')
|
||||
self.start_time = time.time()
|
||||
self._current_errors_stack = []
|
||||
self._current_failures_stack = []
|
||||
|
||||
try:
|
||||
test_name = test.__class__.__name__ + "." + test._testMethodName
|
||||
except AttributeError:
|
||||
# Support for jython 2.1 (__testMethodName is pseudo-private in the test case)
|
||||
test_name = test.__class__.__name__ + "." + test._TestCase__testMethodName
|
||||
|
||||
pydev_runfiles_xml_rpc.notifyStartTest(
|
||||
test.__pydev_pyfile__, test_name)
|
||||
|
||||
def get_test_name(self, test):
|
||||
try:
|
||||
try:
|
||||
test_name = test.__class__.__name__ + "." + test._testMethodName
|
||||
except AttributeError:
|
||||
# Support for jython 2.1 (__testMethodName is pseudo-private in the test case)
|
||||
try:
|
||||
test_name = test.__class__.__name__ + "." + test._TestCase__testMethodName
|
||||
# Support for class/module exceptions (test is instance of _ErrorHolder)
|
||||
except:
|
||||
test_name = test.description.split()[1][1:-1] + ' <' + test.description.split()[0] + '>'
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return '<unable to get test name>'
|
||||
return test_name
|
||||
|
||||
def stopTest(self, test):
|
||||
end_time = time.time()
|
||||
pydevd_io.end_redirect(std='both')
|
||||
|
||||
_PythonTextTestResult.stopTest(self, test)
|
||||
|
||||
captured_output = self.buf.getvalue()
|
||||
del self.buf
|
||||
error_contents = ''
|
||||
test_name = self.get_test_name(test)
|
||||
|
||||
diff_time = '%.2f' % (end_time - self.start_time)
|
||||
|
||||
skipped = False
|
||||
outcome = getattr(test, '_outcome', None)
|
||||
if outcome is not None:
|
||||
skipped = bool(getattr(outcome, 'skipped', None))
|
||||
|
||||
if skipped:
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'skip', captured_output, error_contents, test.__pydev_pyfile__, test_name, diff_time)
|
||||
elif not self._current_errors_stack and not self._current_failures_stack:
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'ok', captured_output, error_contents, test.__pydev_pyfile__, test_name, diff_time)
|
||||
else:
|
||||
self._reportErrors(self._current_errors_stack, self._current_failures_stack, captured_output, test_name)
|
||||
|
||||
def _reportErrors(self, errors, failures, captured_output, test_name, diff_time=''):
|
||||
error_contents = []
|
||||
for test, s in errors + failures:
|
||||
if type(s) == type((1,)): # If it's a tuple (for jython 2.1)
|
||||
sio = StringIO()
|
||||
traceback.print_exception(s[0], s[1], s[2], file=sio)
|
||||
s = sio.getvalue()
|
||||
error_contents.append(s)
|
||||
|
||||
sep = '\n' + self.separator1
|
||||
error_contents = sep.join(error_contents)
|
||||
|
||||
if errors and not failures:
|
||||
try:
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'error', captured_output, error_contents, test.__pydev_pyfile__, test_name, diff_time)
|
||||
except:
|
||||
file_start = error_contents.find('File "')
|
||||
file_end = error_contents.find('", ', file_start)
|
||||
if file_start != -1 and file_end != -1:
|
||||
file = error_contents[file_start + 6:file_end]
|
||||
else:
|
||||
file = '<unable to get file>'
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'error', captured_output, error_contents, file, test_name, diff_time)
|
||||
|
||||
elif failures and not errors:
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'fail', captured_output, error_contents, test.__pydev_pyfile__, test_name, diff_time)
|
||||
|
||||
else: # Ok, we got both, errors and failures. Let's mark it as an error in the end.
|
||||
pydev_runfiles_xml_rpc.notifyTest(
|
||||
'error', captured_output, error_contents, test.__pydev_pyfile__, test_name, diff_time)
|
||||
|
||||
def addError(self, test, err):
|
||||
_PythonTextTestResult.addError(self, test, err)
|
||||
# Support for class/module exceptions (test is instance of _ErrorHolder)
|
||||
if not hasattr(self, '_current_errors_stack') or test.__class__.__name__ == '_ErrorHolder':
|
||||
# Not in start...end, so, report error now (i.e.: django pre/post-setup)
|
||||
self._reportErrors([self.errors[-1]], [], '', self.get_test_name(test))
|
||||
else:
|
||||
self._current_errors_stack.append(self.errors[-1])
|
||||
|
||||
def addFailure(self, test, err):
|
||||
_PythonTextTestResult.addFailure(self, test, err)
|
||||
if not hasattr(self, '_current_failures_stack'):
|
||||
# Not in start...end, so, report error now (i.e.: django pre/post-setup)
|
||||
self._reportErrors([], [self.failures[-1]], '', self.get_test_name(test))
|
||||
else:
|
||||
self._current_failures_stack.append(self.failures[-1])
|
||||
|
||||
|
||||
class PydevTestSuite(python_unittest.TestSuite):
|
||||
pass
|
||||
@@ -0,0 +1,257 @@
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from _pydev_bundle._pydev_filesystem_encoding import getfilesystemencoding
|
||||
from _pydev_bundle.pydev_imports import xmlrpclib, _queue
|
||||
from _pydevd_bundle.pydevd_constants import Null
|
||||
|
||||
Queue = _queue.Queue
|
||||
|
||||
# This may happen in IronPython (in Python it shouldn't happen as there are
|
||||
# 'fast' replacements that are used in xmlrpclib.py)
|
||||
warnings.filterwarnings(
|
||||
'ignore', 'The xmllib module is obsolete.*', DeprecationWarning)
|
||||
|
||||
file_system_encoding = getfilesystemencoding()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# _ServerHolder
|
||||
#=======================================================================================================================
|
||||
class _ServerHolder:
|
||||
'''
|
||||
Helper so that we don't have to use a global here.
|
||||
'''
|
||||
SERVER = None
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# set_server
|
||||
#=======================================================================================================================
|
||||
def set_server(server):
|
||||
_ServerHolder.SERVER = server
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ParallelNotification
|
||||
#=======================================================================================================================
|
||||
class ParallelNotification(object):
|
||||
|
||||
def __init__(self, method, args):
|
||||
self.method = method
|
||||
self.args = args
|
||||
|
||||
def to_tuple(self):
|
||||
return self.method, self.args
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# KillServer
|
||||
#=======================================================================================================================
|
||||
class KillServer(object):
|
||||
pass
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ServerFacade
|
||||
#=======================================================================================================================
|
||||
class ServerFacade(object):
|
||||
|
||||
def __init__(self, notifications_queue):
|
||||
self.notifications_queue = notifications_queue
|
||||
|
||||
def notifyTestsCollected(self, *args):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyTestsCollected', args))
|
||||
|
||||
def notifyConnected(self, *args):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyConnected', args))
|
||||
|
||||
def notifyTestRunFinished(self, *args):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyTestRunFinished', args))
|
||||
|
||||
def notifyStartTest(self, *args):
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyStartTest', args))
|
||||
|
||||
def notifyTest(self, *args):
|
||||
new_args = []
|
||||
for arg in args:
|
||||
new_args.append(_encode_if_needed(arg))
|
||||
args = tuple(new_args)
|
||||
self.notifications_queue.put_nowait(ParallelNotification('notifyTest', args))
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# ServerComm
|
||||
#=======================================================================================================================
|
||||
class ServerComm(threading.Thread):
|
||||
|
||||
def __init__(self, notifications_queue, port, daemon=False):
|
||||
threading.Thread.__init__(self)
|
||||
self.setDaemon(daemon) # If False, wait for all the notifications to be passed before exiting!
|
||||
self.finished = False
|
||||
self.notifications_queue = notifications_queue
|
||||
|
||||
from _pydev_bundle import pydev_localhost
|
||||
|
||||
# It is necessary to specify an encoding, that matches
|
||||
# the encoding of all bytes-strings passed into an
|
||||
# XMLRPC call: "All 8-bit strings in the data structure are assumed to use the
|
||||
# packet encoding. Unicode strings are automatically converted,
|
||||
# where necessary."
|
||||
# Byte strings most likely come from file names.
|
||||
encoding = file_system_encoding
|
||||
if encoding == "mbcs":
|
||||
# Windos symbolic name for the system encoding CP_ACP.
|
||||
# We need to convert it into a encoding that is recognized by Java.
|
||||
# Unfortunately this is not always possible. You could use
|
||||
# GetCPInfoEx and get a name similar to "windows-1251". Then
|
||||
# you need a table to translate on a best effort basis. Much to complicated.
|
||||
# ISO-8859-1 is good enough.
|
||||
encoding = "ISO-8859-1"
|
||||
|
||||
self.server = xmlrpclib.Server('http://%s:%s' % (pydev_localhost.get_localhost(), port),
|
||||
encoding=encoding)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
kill_found = False
|
||||
commands = []
|
||||
command = self.notifications_queue.get(block=True)
|
||||
if isinstance(command, KillServer):
|
||||
kill_found = True
|
||||
else:
|
||||
assert isinstance(command, ParallelNotification)
|
||||
commands.append(command.to_tuple())
|
||||
|
||||
try:
|
||||
while True:
|
||||
command = self.notifications_queue.get(block=False) # No block to create a batch.
|
||||
if isinstance(command, KillServer):
|
||||
kill_found = True
|
||||
else:
|
||||
assert isinstance(command, ParallelNotification)
|
||||
commands.append(command.to_tuple())
|
||||
except:
|
||||
pass # That's OK, we're getting it until it becomes empty so that we notify multiple at once.
|
||||
|
||||
if commands:
|
||||
try:
|
||||
self.server.notifyCommands(commands)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
if kill_found:
|
||||
self.finished = True
|
||||
return
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# initialize_server
|
||||
#=======================================================================================================================
|
||||
def initialize_server(port, daemon=False):
|
||||
if _ServerHolder.SERVER is None:
|
||||
if port is not None:
|
||||
notifications_queue = Queue()
|
||||
_ServerHolder.SERVER = ServerFacade(notifications_queue)
|
||||
_ServerHolder.SERVER_COMM = ServerComm(notifications_queue, port, daemon)
|
||||
_ServerHolder.SERVER_COMM.start()
|
||||
else:
|
||||
# Create a null server, so that we keep the interface even without any connection.
|
||||
_ServerHolder.SERVER = Null()
|
||||
_ServerHolder.SERVER_COMM = Null()
|
||||
|
||||
try:
|
||||
_ServerHolder.SERVER.notifyConnected()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# notifyTest
|
||||
#=======================================================================================================================
|
||||
def notifyTestsCollected(tests_count):
|
||||
assert tests_count is not None
|
||||
try:
|
||||
_ServerHolder.SERVER.notifyTestsCollected(tests_count)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# notifyStartTest
|
||||
#=======================================================================================================================
|
||||
def notifyStartTest(file, test):
|
||||
'''
|
||||
@param file: the tests file (c:/temp/test.py)
|
||||
@param test: the test ran (i.e.: TestCase.test1)
|
||||
'''
|
||||
assert file is not None
|
||||
if test is None:
|
||||
test = '' # Could happen if we have an import error importing module.
|
||||
|
||||
try:
|
||||
_ServerHolder.SERVER.notifyStartTest(file, test)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def _encode_if_needed(obj):
|
||||
# In the java side we expect strings to be ISO-8859-1 (org.python.pydev.debug.pyunit.PyUnitServer.initializeDispatches().new Dispatch() {...}.getAsStr(Object))
|
||||
if isinstance(obj, str): # Unicode in py3
|
||||
return xmlrpclib.Binary(obj.encode('ISO-8859-1', 'xmlcharrefreplace'))
|
||||
|
||||
elif isinstance(obj, bytes):
|
||||
try:
|
||||
return xmlrpclib.Binary(obj.decode(sys.stdin.encoding).encode('ISO-8859-1', 'xmlcharrefreplace'))
|
||||
except:
|
||||
return xmlrpclib.Binary(obj) # bytes already
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# notifyTest
|
||||
#=======================================================================================================================
|
||||
def notifyTest(cond, captured_output, error_contents, file, test, time):
|
||||
'''
|
||||
@param cond: ok, fail, error
|
||||
@param captured_output: output captured from stdout
|
||||
@param captured_output: output captured from stderr
|
||||
@param file: the tests file (c:/temp/test.py)
|
||||
@param test: the test ran (i.e.: TestCase.test1)
|
||||
@param time: float with the number of seconds elapsed
|
||||
'''
|
||||
assert cond is not None
|
||||
assert captured_output is not None
|
||||
assert error_contents is not None
|
||||
assert file is not None
|
||||
if test is None:
|
||||
test = '' # Could happen if we have an import error importing module.
|
||||
assert time is not None
|
||||
try:
|
||||
captured_output = _encode_if_needed(captured_output)
|
||||
error_contents = _encode_if_needed(error_contents)
|
||||
|
||||
_ServerHolder.SERVER.notifyTest(cond, captured_output, error_contents, file, test, time)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# notifyTestRunFinished
|
||||
#=======================================================================================================================
|
||||
def notifyTestRunFinished(total_time):
|
||||
assert total_time is not None
|
||||
try:
|
||||
_ServerHolder.SERVER.notifyTestRunFinished(total_time)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
#=======================================================================================================================
|
||||
# force_server_kill
|
||||
#=======================================================================================================================
|
||||
def force_server_kill():
|
||||
_ServerHolder.SERVER_COMM.notifications_queue.put_nowait(KillServer())
|
||||
Reference in New Issue
Block a user