Skip to content

Commit 498279f

Browse files
author
Eric Snow
authored
Split up pytest.py. (microsoft#5638)
(for microsoft#5458) pythonFiles/testing_tools/adapter/pytest.py has gotten too big, with too many different things implemented there. This change addresses that by adding a "pytest" subpackage and splitting the code from pytest.py up into several modules under that subpackage. Note that this change is almost exclusively moving code around, adding missing docstrings, and removing superfluous comments.
1 parent 2312c3e commit 498279f

10 files changed

Lines changed: 645 additions & 517 deletions

File tree

pythonFiles/testing_tools/adapter/pytest.py

Lines changed: 0 additions & 462 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from __future__ import absolute_import
5+
6+
from ._cli import add_subparser as add_cli_subparser
7+
from ._discovery import discover
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from __future__ import absolute_import
5+
6+
from ..errors import UnsupportedCommandError
7+
8+
9+
def add_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
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from __future__ import absolute_import, print_function
5+
6+
import os.path
7+
import sys
8+
9+
import pytest
10+
11+
from .. import util
12+
from ..info import ParentInfo
13+
from ._pytest_item import parse_item
14+
15+
16+
def discover(pytestargs=None, hidestdio=False,
17+
_pytest_main=pytest.main, _plugin=None, **_ignored):
18+
"""Return the results of test discovery."""
19+
if _plugin is None:
20+
_plugin = TestCollector()
21+
22+
pytestargs = _adjust_pytest_args(pytestargs)
23+
# We use this helper rather than "-pno:terminal" due to possible
24+
# platform-dependent issues.
25+
with (util.hide_stdio() if hidestdio else util.noop_cm()) as stdio:
26+
ec = _pytest_main(pytestargs, [_plugin])
27+
# See: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes
28+
if ec == 5:
29+
# No tests were discovered.
30+
pass
31+
elif ec != 0:
32+
if hidestdio:
33+
print(stdio.getvalue(), file=sys.stderr)
34+
sys.stdout.flush()
35+
raise Exception('pytest discovery failed (exit code {})'.format(ec))
36+
if not _plugin._started:
37+
if hidestdio:
38+
print(stdio.getvalue(), file=sys.stderr)
39+
sys.stdout.flush()
40+
raise Exception('pytest discovery did not start')
41+
return (
42+
_plugin._tests.parents,
43+
list(_plugin._tests),
44+
)
45+
46+
47+
def _adjust_pytest_args(pytestargs):
48+
"""Return a corrected copy of the given pytest CLI args."""
49+
pytestargs = list(pytestargs) if pytestargs else []
50+
# Duplicate entries should be okay.
51+
pytestargs.insert(0, '--collect-only')
52+
# TODO: pull in code from:
53+
# src/client/testing/pytest/services/discoveryService.ts
54+
# src/client/testing/pytest/services/argsService.ts
55+
return pytestargs
56+
57+
58+
class TestCollector(object):
59+
"""This is a pytest plugin that collects the discovered tests."""
60+
61+
NORMCASE = staticmethod(os.path.normcase)
62+
PATHSEP = os.path.sep
63+
64+
def __init__(self, tests=None):
65+
if tests is None:
66+
tests = DiscoveredTests()
67+
self._tests = tests
68+
self._started = False
69+
70+
# Relevant plugin hooks:
71+
# https://docs.pytest.org/en/latest/reference.html#collection-hooks
72+
73+
def pytest_collection_modifyitems(self, session, config, items):
74+
self._started = True
75+
self._tests.reset()
76+
for item in items:
77+
test, suiteids = parse_item(item, self.NORMCASE, self.PATHSEP)
78+
self._tests.add_test(test, suiteids)
79+
80+
# This hook is not specified in the docs, so we also provide
81+
# the "modifyitems" hook just in case.
82+
def pytest_collection_finish(self, session):
83+
self._started = True
84+
try:
85+
items = session.items
86+
except AttributeError:
87+
# TODO: Is there an alternative?
88+
return
89+
self._tests.reset()
90+
for item in items:
91+
test, suiteids = parse_item(item, self.NORMCASE, self.PATHSEP)
92+
self._tests.add_test(test, suiteids)
93+
94+
95+
class DiscoveredTests(object):
96+
"""A container for the discovered tests and their parents."""
97+
98+
def __init__(self):
99+
self.reset()
100+
101+
def __len__(self):
102+
return len(self._tests)
103+
104+
def __getitem__(self, index):
105+
return self._tests[index]
106+
107+
@property
108+
def parents(self):
109+
return sorted(self._parents.values(), key=lambda v: (v.root or v.name, v.id))
110+
111+
def reset(self):
112+
"""Clear out any previously discovered tests."""
113+
self._parents = {}
114+
self._tests = []
115+
116+
def add_test(self, test, suiteids):
117+
"""Add the given test and its parents."""
118+
parentid = self._ensure_parent(test.path, test.parentid, suiteids)
119+
test = test._replace(parentid=parentid)
120+
if not test.id.startswith('.' + os.path.sep):
121+
test = test._replace(id=os.path.join('.', test.id))
122+
self._tests.append(test)
123+
124+
def _ensure_parent(self, path, parentid, suiteids):
125+
if not parentid.startswith('.' + os.path.sep):
126+
parentid = os.path.join('.', parentid)
127+
fileid = self._ensure_file(path.root, path.relfile)
128+
rootdir = path.root
129+
130+
if not path.func:
131+
return parentid
132+
133+
fullsuite, _, funcname = path.func.rpartition('.')
134+
suiteid = self._ensure_suites(fullsuite, rootdir, fileid, suiteids)
135+
parent = suiteid if suiteid else fileid
136+
137+
if path.sub:
138+
if (rootdir, parentid) not in self._parents:
139+
funcinfo = ParentInfo(parentid, 'function', funcname,
140+
rootdir, parent)
141+
self._parents[(rootdir, parentid)] = funcinfo
142+
elif parent != parentid:
143+
print(parent, parentid)
144+
# TODO: What to do?
145+
raise NotImplementedError
146+
return parentid
147+
148+
def _ensure_file(self, rootdir, relfile):
149+
if (rootdir, '.') not in self._parents:
150+
self._parents[(rootdir, '.')] = ParentInfo('.', 'folder', rootdir)
151+
if relfile.startswith('.' + os.path.sep):
152+
fileid = relfile
153+
else:
154+
fileid = relfile = os.path.join('.', relfile)
155+
156+
if (rootdir, fileid) not in self._parents:
157+
folderid, filebase = os.path.split(fileid)
158+
fileinfo = ParentInfo(fileid, 'file', filebase, rootdir, folderid)
159+
self._parents[(rootdir, fileid)] = fileinfo
160+
161+
while folderid != '.' and (rootdir, folderid) not in self._parents:
162+
parentid, name = os.path.split(folderid)
163+
folderinfo = ParentInfo(folderid, 'folder', name, rootdir, parentid)
164+
self._parents[(rootdir, folderid)] = folderinfo
165+
folderid = parentid
166+
return relfile
167+
168+
def _ensure_suites(self, fullsuite, rootdir, fileid, suiteids):
169+
if not fullsuite:
170+
if suiteids:
171+
print(suiteids)
172+
# TODO: What to do?
173+
raise NotImplementedError
174+
return None
175+
if len(suiteids) != fullsuite.count('.') + 1:
176+
print(suiteids)
177+
# TODO: What to do?
178+
raise NotImplementedError
179+
180+
suiteid = suiteids.pop()
181+
if not suiteid.startswith('.' + os.path.sep):
182+
suiteid = os.path.join('.', suiteid)
183+
final = suiteid
184+
while '.' in fullsuite and (rootdir, suiteid) not in self._parents:
185+
parentid = suiteids.pop()
186+
if not parentid.startswith('.' + os.path.sep):
187+
parentid = os.path.join('.', parentid)
188+
fullsuite, _, name = fullsuite.rpartition('.')
189+
suiteinfo = ParentInfo(suiteid, 'suite', name, rootdir, parentid)
190+
self._parents[(rootdir, suiteid)] = suiteinfo
191+
192+
suiteid = parentid
193+
else:
194+
name = fullsuite
195+
suiteinfo = ParentInfo(suiteid, 'suite', name, rootdir, fileid)
196+
if (rootdir, suiteid) not in self._parents:
197+
self._parents[(rootdir, suiteid)] = suiteinfo
198+
return final

0 commit comments

Comments
 (0)