GeezerVison is showing github.com — Add a Python script that uses the PyTest API to run test discovery. (… · rtu/vscode-python@020bc56
File treeExpand file treeCollapse file tree Open diff view settings pythonFilestesting_tools/adapters
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: build/ci/templates/compile-and-validate.yml +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -156,6 +156,10 @@ jobs: | 156 | 156 | arguments: '-m pip install --upgrade -r ./build/test-requirements.txt' | 157 | 157 |
| 158 | 158 |
| | 159 | + - bash: 'python -m pythonFiles.tests' | | 160 | + displayName: 'run pythonFiles unit tests' | | 161 | + | | 162 | + | 159 | 163 | - task: CmdLine@1 | 160 | 164 | displayName: 'pip install python packages' | 161 | 165 | inputs: | |
Collapse file Copy file name to clipboard +1Lines changed: 1 addition & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +Add a Python script to run PyTest correctly for discovery. |
Collapse file Copy file name to clipboardExpand all lines: pythonFiles/testing_tools/adapter/__init__.py Collapse file Copy file name to clipboard +84Lines changed: 84 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +from __future__ import absolute_import | | 2 | + | | 3 | +import argparse | | 4 | +import sys | | 5 | + | | 6 | +from . import pytest, report | | 7 | +from .errors import UnsupportedToolError, UnsupportedCommandError | | 8 | + | | 9 | + | | 10 | +# Set this to True to pretty-print the output. | | 11 | +DEBUG=False | | 12 | +#DEBUG=True | | 13 | + | | 14 | +TOOLS = { | | 15 | + 'pytest': { | | 16 | + '_add_subparser': pytest.add_cli_subparser, | | 17 | + 'discover': pytest.discover, | | 18 | + }, | | 19 | + } | | 20 | +REPORTERS = { | | 21 | + 'discover': report.report_discovered, | | 22 | + } | | 23 | + | | 24 | + | | 25 | +def parse_args( | | 26 | + argv=sys.argv[1:], | | 27 | + prog=sys.argv[0], | | 28 | + ): | | 29 | + """ | | 30 | + Return the subcommand & tool to run, along with its args. | | 31 | +
| | 32 | + This defines the standard CLI for the different testing frameworks. | | 33 | + """ | | 34 | + parser = argparse.ArgumentParser( | | 35 | + description='Run Python testing operations.', | | 36 | + prog=prog, | | 37 | + ) | | 38 | + cmdsubs = parser.add_subparsers(dest='cmd') | | 39 | + | | 40 | + # Add "run" and "debug" subcommands when ready. | | 41 | + for cmdname in ['discover']: | | 42 | + sub = cmdsubs.add_parser(cmdname) | | 43 | + subsubs = sub.add_subparsers(dest='tool') | | 44 | + for toolname in sorted(TOOLS): | | 45 | + try: | | 46 | + add_subparser = TOOLS[toolname]['_add_subparser'] | | 47 | + except KeyError: | | 48 | + continue | | 49 | + add_subparser(cmdname, toolname, subsubs) | | 50 | + | | 51 | + # Parse the args! | | 52 | + args, toolargs = parser.parse_known_args(argv) | | 53 | + ns = vars(args) | | 54 | + | | 55 | + cmd = ns.pop('cmd') | | 56 | + if not cmd: | | 57 | + parser.error('missing command') | | 58 | + tool = ns.pop('tool') | | 59 | + if not tool: | | 60 | + parser.error('missing tool') | | 61 | + | | 62 | + return tool, cmd, ns, toolargs | | 63 | + | | 64 | + | | 65 | +def main(toolname, cmdname, subargs, toolargs, | | 66 | + _tools=TOOLS, _reporters=REPORTERS): | | 67 | + try: | | 68 | + tool = _tools[toolname] | | 69 | + except KeyError: | | 70 | + raise UnsupportedToolError(toolname) | | 71 | + | | 72 | + try: | | 73 | + run = tool[cmdname] | | 74 | + report_result = _reporters[cmdname] | | 75 | + except KeyError: | | 76 | + raise UnsupportedCommandError(cmdname) | | 77 | + | | 78 | + result = run(toolargs, **subargs) | | 79 | + report_result(result, debug=DEBUG) | | 80 | + | | 81 | + | | 82 | +if __name__ == '__main__': | | 83 | + tool, cmd, subargs, toolargs = parse_args() | | 84 | + main(tool, cmd, subargs, toolargs) |
Collapse file Copy file name to clipboard +11Lines changed: 11 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | + | | 2 | +class UnsupportedToolError(ValueError): | | 3 | + def __init__(self, tool): | | 4 | + super().__init__('unsupported tool {!r}'.format(tool)) | | 5 | + self.tool = tool | | 6 | + | | 7 | + | | 8 | +class UnsupportedCommandError(ValueError): | | 9 | + def __init__(self, cmd): | | 10 | + super().__init__('unsupported cmd {!r}'.format(cmd)) | | 11 | + self.cmd = cmd |
Collapse file Copy file name to clipboard +9Lines changed: 9 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +from collections import namedtuple | | 2 | + | | 3 | + | | 4 | +class TestPath(namedtuple('TestPath', 'root relfile func sub')): | | 5 | + """Where to find a single test.""" | | 6 | + | | 7 | + | | 8 | +class TestInfo(namedtuple('TestInfo', 'id name path lineno markers')): | | 9 | + """Info for a single test.""" |
Collapse file Copy file name to clipboard +150Lines changed: 150 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import os.path | | 2 | + | | 3 | +import pytest | | 4 | + | | 5 | +from .errors import UnsupportedCommandError | | 6 | +from .info import TestInfo, TestPath | | 7 | + | | 8 | + | | 9 | +def add_cli_subparser(cmd, name, parent): | | 10 | + """Add a new subparser to the given parent and add args to it.""" | | 11 | + parser = parent.add_parser(name) | | 12 | + if cmd == 'discover': | | 13 | + # For now we don't have any tool-specific CLI options to add. | | 14 | + pass | | 15 | + else: | | 16 | + raise UnsupportedCommandError(cmd) | | 17 | + return parser | | 18 | + | | 19 | + | | 20 | +def discover(pytestargs=None, | | 21 | + _pytest_main=pytest.main, _plugin=None): | | 22 | + """Return the results of test discovery.""" | | 23 | + if _plugin is None: | | 24 | + _plugin = TestCollector() | | 25 | + | | 26 | + pytestargs = _adjust_pytest_args(pytestargs) | | 27 | + ec = _pytest_main(pytestargs, [_plugin]) | | 28 | + if ec != 0: | | 29 | + raise Exception('pytest discovery failed (exit code {})'.format(ec)) | | 30 | + if _plugin.discovered is None: | | 31 | + raise Exception('pytest discovery did not start') | | 32 | + return _plugin.discovered | | 33 | + | | 34 | + | | 35 | +def _adjust_pytest_args(pytestargs): | | 36 | + pytestargs = list(pytestargs) if pytestargs else [] | | 37 | + # Duplicate entries should be okay. | | 38 | + pytestargs.insert(0, '--collect-only') | | 39 | + pytestargs.insert(0, '-pno:terminal') | | 40 | + # TODO: pull in code from: | | 41 | + # src/client/unittests/pytest/services/discoveryService.ts | | 42 | + # src/client/unittests/pytest/services/argsService.ts | | 43 | + return pytestargs | | 44 | + | | 45 | + | | 46 | +class TestCollector(object): | | 47 | + """This is a pytest plugin that collects the discovered tests.""" | | 48 | + | | 49 | + discovered = None | | 50 | + | | 51 | + # Relevant plugin hooks: | | 52 | + # https://docs.pytest.org/en/latest/reference.html#collection-hooks | | 53 | + | | 54 | + def pytest_collection_modifyitems(self, session, config, items): | | 55 | + self.discovered = [] | | 56 | + for item in items: | | 57 | + info = _parse_item(item) | | 58 | + self.discovered.append(info) | | 59 | + | | 60 | + # This hook is not specified in the docs, so we also provide | | 61 | + # the "modifyitems" hook just in case. | | 62 | + def pytest_collection_finish(self, session): | | 63 | + try: | | 64 | + items = session.items | | 65 | + except AttributeError: | | 66 | + # TODO: Is there an alternative? | | 67 | + return | | 68 | +# print(', '.join(k for k in dir(items[0]) if k[0].islower())) | | 69 | + self.discovered = [] | | 70 | + for item in items: | | 71 | +# print(' ', item.user_properties) | | 72 | +# print(' ', item.own_markers) | | 73 | +# print(' ', list(item.iter_markers())) | | 74 | +# print() | | 75 | + info = _parse_item(item) | | 76 | + self.discovered.append(info) | | 77 | + | | 78 | + | | 79 | +def _parse_item(item): | | 80 | + """ | | 81 | + (pytest.Collector) | | 82 | + pytest.Session | | 83 | + pytest.Package | | 84 | + pytest.Module | | 85 | + pytest.Class | | 86 | + (pytest.File) | | 87 | + (pytest.Item) | | 88 | + pytest.Function | | 89 | + """ | | 90 | + # Figure out the file. | | 91 | + filename, lineno, fullname = item.location | | 92 | + if not str(item.fspath).endswith(os.path.sep + filename): | | 93 | + raise NotImplementedError | | 94 | + testroot = str(item.fspath)[:-len(filename)].rstrip(os.path.sep) | | 95 | + if os.path.sep in filename: | | 96 | + relfile = filename | | 97 | + else: | | 98 | + relfile = os.path.join('.', filename) | | 99 | + | | 100 | + # Figure out the func (and subs). | | 101 | + funcname = item.function.__name__ | | 102 | + parts = item.nodeid.split('::') | | 103 | + if parts.pop(0) != filename: | | 104 | + # TODO: What to do? | | 105 | + raise NotImplementedError | | 106 | + suites = [] | | 107 | + while len(parts) > 1: | | 108 | + suites.append(parts.pop(0)) | | 109 | + parameterized = '' | | 110 | + if '[' in parts[0]: | | 111 | + _func, sep, parameterized = parts[0].partition('[') | | 112 | + parameterized = sep + parameterized | | 113 | + if _func != funcname: | | 114 | + # TODO: What to do? | | 115 | + raise NotImplementedError | | 116 | + if suites: | | 117 | + testfunc = '.'.join(suites) + '.' + funcname | | 118 | + else: | | 119 | + testfunc = funcname | | 120 | + if fullname != testfunc + parameterized: | | 121 | + # TODO: What to do? | | 122 | + raise NotImplementedError | | 123 | + | | 124 | + # Sort out markers. | | 125 | + # See: https://docs.pytest.org/en/latest/reference.html#marks | | 126 | + markers = set() | | 127 | + for marker in item.own_markers: | | 128 | + if marker.name == 'parameterize': | | 129 | + # We've already covered these. | | 130 | + continue | | 131 | + elif marker.name == 'skip': | | 132 | + markers.add('skip') | | 133 | + elif marker.name == 'skipif': | | 134 | + markers.add('skip-if') | | 135 | + elif marker.name == 'xfail': | | 136 | + markers.add('expected-failure') | | 137 | + # TODO: Support other markers? | | 138 | + | | 139 | + return TestInfo( | | 140 | + id=item.nodeid, | | 141 | + name=item.name, | | 142 | + path=TestPath( | | 143 | + root=testroot, | | 144 | + relfile=relfile, | | 145 | + func=testfunc, | | 146 | + sub=[parameterized] if parameterized else None, | | 147 | + ), | | 148 | + lineno=lineno, | | 149 | + markers=sorted(markers) if markers else None, | | 150 | + ) |
Collapse file Copy file name to clipboard +26Lines changed: 26 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import json | | 2 | + | | 3 | + | | 4 | +def report_discovered(tests, debug=False, | | 5 | + _send=print): | | 6 | + """Serialize the discovered tests and write to stdout.""" | | 7 | + data = [{ | | 8 | + 'id': test.id, | | 9 | + 'name': test.name, | | 10 | + 'testroot': test.path.root, | | 11 | + 'relfile': test.path.relfile, | | 12 | + 'lineno': test.lineno, | | 13 | + 'testfunc': test.path.func, | | 14 | + 'subtest': test.path.sub or None, | | 15 | + 'markers': test.markers or None, | | 16 | + } for test in tests] | | 17 | + kwargs = {} | | 18 | + if debug: | | 19 | + # human-formatted | | 20 | + kwargs = dict( | | 21 | + sort_keys=True, | | 22 | + indent=4, | | 23 | + separators=(',', ': '), | | 24 | + ) | | 25 | + serialized = json.dumps(data, **kwargs) | | 26 | + _send(serialized) |
Collapse file Copy file name to clipboard +13Lines changed: 13 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +# Replace the "." entry. | | 2 | +import os.path | | 3 | +import sys | | 4 | +sys.path[0] = os.path.dirname( | | 5 | + os.path.dirname( | | 6 | + os.path.abspath(__file__))) | | 7 | + | | 8 | +from testing_tools.adapter.__main__ import parse_args, main | | 9 | + | | 10 | + | | 11 | +if __name__ == '__main__': | | 12 | + tool, cmd, subargs, toolargs = parse_args() | | 13 | + main(tool, cmd, subargs, toolargs) |
Collapse file Copy file name to clipboardExpand all lines: pythonFiles/tests/__main__.py +3-1Lines changed: 3 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 7 | 7 | TEST_ROOT = os.path.dirname(__file__) | 8 | 8 | SRC_ROOT = os.path.dirname(TEST_ROOT) | 9 | 9 | DATASCIENCE_ROOT = os.path.join(SRC_ROOT, 'datascience') | | 10 | +TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, 'testing_tools') | 10 | 11 |
| 11 | 12 |
| 12 | 13 | if __name__ == '__main__': | 13 | 14 | sys.path.insert(1, DATASCIENCE_ROOT) | | 15 | + sys.path.insert(1, TESTING_TOOLS_ROOT) | 14 | 16 | ec = pytest.main([ | 15 | 17 | '--rootdir', SRC_ROOT, | 16 | 18 | TEST_ROOT, | 17 | | - ]) | | 19 | + ] + sys.argv[1:]) | 18 | 20 | sys.exit(ec) |
|