Skip to content

Commit 3d5af66

Browse files
Change the output format of the pytest adapter. (#4732)
(for #4035) We are moving to a relatively flat format that captures parent "nodes", in addition to tests. The new format looks like this: ```json [{ "rootid": ".", "root": "/x/y/z", "parents": [{ "id": "./test_spam.py", "kind": "file", "name": "test_spam.py", "parentid": "." }, { "id": "./test_spam.py::SpamTests", "kind": "suite", "name": "SpamTests", "parentid": "./test_spam.py" }, "tests" [{ "id": "./test_spam.py::test_all", "name": "test_all", "source": "test_spam.py:11", "markers": ["skip", "expected-failure"], "parentid": "./test_spam.py" }, { "id": "./test_spam.py::SpamTests::test_spam1", "name": "test_spam1", "source": "test_spam.py:23", "markers": ["skip"], "parentid": "./test_spam.py::SpamTests" }] ``` This also fixes a couple of bugs that I found while working on the change.
1 parent 8a8653d commit 3d5af66

File tree

13 files changed

+2238
-154
lines changed

13 files changed

+2238
-154
lines changed

pythonFiles/testing_tools/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.

pythonFiles/testing_tools/adapter/__main__.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
14
from __future__ import absolute_import
25

36
import argparse
@@ -7,10 +10,6 @@
710
from .errors import UnsupportedToolError, UnsupportedCommandError
811

912

10-
# Set this to True to pretty-print the output.
11-
DEBUG=False
12-
#DEBUG=True
13-
1413
TOOLS = {
1514
'pytest': {
1615
'_add_subparser': pytest.add_cli_subparser,
@@ -22,6 +21,7 @@
2221
}
2322

2423

24+
2525
def parse_args(
2626
argv=sys.argv[1:],
2727
prog=sys.argv[0],
@@ -40,6 +40,9 @@ def parse_args(
4040
# Add "run" and "debug" subcommands when ready.
4141
for cmdname in ['discover']:
4242
sub = cmdsubs.add_parser(cmdname)
43+
if cmdname == 'discover':
44+
sub.add_argument('--simple', action='store_true')
45+
sub.add_argument('--show-pytest', action='store_true')
4346
subsubs = sub.add_subparsers(dest='tool')
4447
for toolname in sorted(TOOLS):
4548
try:
@@ -55,6 +58,14 @@ def parse_args(
5558
cmd = ns.pop('cmd')
5659
if not cmd:
5760
parser.error('missing command')
61+
if cmd == 'discover':
62+
if '--simple' in toolargs:
63+
toolargs.remove('--simple')
64+
ns['simple'] = True
65+
if '--show-pytest' in toolargs:
66+
toolargs.remove('--show-pytest')
67+
ns['show_pytest'] = True
68+
5869
tool = ns.pop('tool')
5970
if not tool:
6071
parser.error('missing tool')
@@ -75,8 +86,11 @@ def main(toolname, cmdname, subargs, toolargs,
7586
except KeyError:
7687
raise UnsupportedCommandError(cmdname)
7788

78-
result = run(toolargs, **subargs)
79-
report_result(result, debug=DEBUG)
89+
parents, result = run(toolargs, **subargs)
90+
report_result(result, parents,
91+
debug=('-v' in toolargs or '--verbose' in toolargs),
92+
**subargs
93+
)
8094

8195

8296
if __name__ == '__main__':

pythonFiles/testing_tools/adapter/errors.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
14

25
class UnsupportedToolError(ValueError):
36
def __init__(self, tool):
+106-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,114 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
14
from collections import namedtuple
25

36

47
class TestPath(namedtuple('TestPath', 'root relfile func sub')):
58
"""Where to find a single test."""
69

10+
def __new__(cls, root, relfile, func, sub=None):
11+
self = super(TestPath, cls).__new__(
12+
cls,
13+
str(root) if root else None,
14+
str(relfile) if relfile else None,
15+
str(func) if func else None,
16+
[str(s) for s in sub] if sub else None,
17+
)
18+
return self
19+
20+
def __init__(self, *args, **kwargs):
21+
if self.root is None:
22+
raise TypeError('missing id')
23+
if self.relfile is None:
24+
raise TypeError('missing kind')
25+
# self.func may be None (e.g. for doctests).
26+
# self.sub may be None.
27+
28+
29+
class ParentInfo(namedtuple('ParentInfo', 'id kind name root parentid')):
30+
31+
KINDS = ('folder', 'file', 'suite', 'function', 'subtest')
32+
33+
def __new__(cls, id, kind, name, root=None, parentid=None):
34+
self = super(ParentInfo, cls).__new__(
35+
cls,
36+
str(id) if id else None,
37+
str(kind) if kind else None,
38+
str(name) if name else None,
39+
str(root) if root else None,
40+
str(parentid) if parentid else None,
41+
)
42+
return self
43+
44+
def __init__(self, *args, **kwargs):
45+
if self.id is None:
46+
raise TypeError('missing id')
47+
if self.kind is None:
48+
raise TypeError('missing kind')
49+
if self.kind not in self.KINDS:
50+
raise ValueError('unsupported kind {!r}'.format(self.kind))
51+
if self.name is None:
52+
raise TypeError('missing name')
53+
if self.root is None:
54+
if self.parentid is not None or self.kind != 'folder':
55+
raise TypeError('missing root')
56+
elif self.parentid is None:
57+
raise TypeError('missing parentid')
58+
759

8-
class TestInfo(namedtuple('TestInfo', 'id name path lineno markers')):
60+
class TestInfo(namedtuple('TestInfo', 'id name path source markers parentid kind')):
961
"""Info for a single test."""
62+
63+
MARKERS = ('skip', 'skip-if', 'expected-failure')
64+
KINDS = ('function', 'doctest')
65+
66+
def __new__(cls, id, name, path, source, markers, parentid, kind='function'):
67+
self = super(TestInfo, cls).__new__(
68+
cls,
69+
str(id) if id else None,
70+
str(name) if name else None,
71+
path or None,
72+
str(source) if source else None,
73+
[str(marker) for marker in markers or ()],
74+
str(parentid) if parentid else None,
75+
str(kind) if kind else None,
76+
)
77+
return self
78+
79+
def __init__(self, *args, **kwargs):
80+
if self.id is None:
81+
raise TypeError('missing id')
82+
if self.name is None:
83+
raise TypeError('missing name')
84+
if self.path is None:
85+
raise TypeError('missing path')
86+
if self.source is None:
87+
raise TypeError('missing source')
88+
else:
89+
srcfile, _, lineno = self.source.rpartition(':')
90+
if not srcfile or not lineno or int(lineno) < 0:
91+
raise ValueError('bad source {!r}'.format(self.source))
92+
if self.markers:
93+
badmarkers = [m for m in self.markers if m not in self.MARKERS]
94+
if badmarkers:
95+
raise ValueError('unsupported markers {!r}'.format(badmarkers))
96+
if self.parentid is None:
97+
raise TypeError('missing parentid')
98+
if self.kind is None:
99+
raise TypeError('missing kind')
100+
elif self.kind not in self.KINDS:
101+
raise ValueError('unsupported kind {!r}'.format(self.kind))
102+
103+
104+
@property
105+
def root(self):
106+
return self.path.root
107+
108+
@property
109+
def srcfile(self):
110+
return self.source.rpartition(':')[0]
111+
112+
@property
113+
def lineno(self):
114+
return int(self.source.rpartition(':')[-1])

0 commit comments

Comments
 (0)