Skip to content

Commit 3e6e2bb

Browse files
- improved performance by first reducing by module;
- added reduce test for same-module polluter; - small cleanups;
1 parent f23c367 commit 3e6e2bb

File tree

2 files changed

+75
-33
lines changed

2 files changed

+75
-33
lines changed

src/pytest_cleanslate/reduce.py

+45-33
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ def get_tests(self) -> T.List[str]:
130130
def get_first_failed(self) -> T.Union[None, str]:
131131
return next(iter(o['id'] for o in self._results['collect'] + self._results['run'] if o['outcome'] == 'failed'), None)
132132

133-
def get_module(self, testid: str) -> str:
134-
return testid.split('::')[0]
135133

136-
def is_module(self, testid: str) -> bool:
137-
return '::' not in testid
134+
def _get_module(testid: str) -> str:
135+
return testid.split('::')[0]
136+
137+
138+
def _is_module(testid: str) -> bool:
139+
return '::' not in testid
138140

139141

140142
def _run_pytest(tests_path: Path, extra_args=(), *,
@@ -183,6 +185,7 @@ def _bisect_items(items: T.List[str], failing: str, fails: T.Callable[[T.List[st
183185
while len(items) > 1:
184186
middle = len(items) // 2
185187

188+
bar.refresh() # for when using --trace
186189
bar.set_postfix({"remaining": len(items)})
187190
bar.update()
188191

@@ -200,32 +203,45 @@ def _bisect_items(items: T.List[str], failing: str, fails: T.Callable[[T.List[st
200203
if len(items) == 1 and fails([failing]):
201204
items = []
202205

206+
bar.refresh() # for when using --trace
203207
bar.set_postfix({"remaining": len(items)})
204208
bar.update()
205209

206210
return items
207211

208212

209-
def _reduce_tests(tests_path: Path, tests: T.List[str], failing_test: str,
213+
def _reduce_tests(tests_path: Path, tests: T.List[str], failing_test: str, modules: T.List[str],
210214
*, trace: bool = False, pytest_args: T.List[str] = ()) -> T.List[str]:
211215
def fails(test_set: T.List[str]):
212216
trial = _run_pytest(tests_path, (*pytest_args, '--continue-on-collection-errors'),
213-
tests=test_set, trace=trace)
217+
tests=test_set, modules=modules, trace=trace)
214218
return trial.get_outcome(failing_test) == 'failed'
215219

216-
with tqdm.tqdm(desc="Trying to reduce tests.....", total=math.ceil(math.log(len(tests), 2))) as bar:
220+
module_set = {*modules}
221+
tests = [t for t in tests if t != failing_test and _get_module(t) in module_set]
222+
if not tests:
223+
return tests
224+
225+
steps=math.ceil(math.log(len(tests), 2))
226+
with tqdm.tqdm(desc="Trying to reduce tests.....", total=steps) as bar:
217227
return _bisect_items(tests, failing_test, fails, bar=bar)
218228

219229

220-
def _reduce_modules(tests_path: Path, tests: T.List[str], failing_test: str,
230+
def _reduce_modules(tests_path: Path, tests: T.List[str], failing_id: str,
221231
modules: T.List[str], failing_module: str,
222232
*, trace: bool = False, pytest_args: T.List[str] = ()) -> T.List[str]:
233+
223234
def fails(module_set: T.List[str]):
224235
trial = _run_pytest(tests_path, (*pytest_args, '--continue-on-collection-errors',),
225236
tests=tests, modules=module_set, trace=trace)
226-
return trial.get_outcome(failing_test) == 'failed'
237+
return trial.get_outcome(failing_id) == 'failed'
227238

228-
with tqdm.tqdm(desc="Trying to reduce modules...", total=math.ceil(math.log(len(modules), 2))) as bar:
239+
modules = [m for m in modules if m != failing_module]
240+
if not modules:
241+
return modules
242+
243+
steps = math.ceil(math.log(len(modules), 2))
244+
with tqdm.tqdm(desc="Trying to reduce modules...", total=steps) as bar:
229245
return _bisect_items(modules, failing_module, fails, bar=bar)
230246

231247

@@ -252,53 +268,49 @@ def main():
252268
print("Running tests...", flush=True)
253269
results = _run_pytest(args.tests_path, (*pytest_args, '-x'), trace=args.trace)
254270

255-
failed = results.get_first_failed()
256-
if failed is None:
271+
failed_id = results.get_first_failed()
272+
if failed_id is None:
257273
print("No tests failed!", flush=True)
258274
if args.save_to:
259275
with args.save_to.open("w") as f:
260276
json.dump({
261-
'failed': failed,
277+
'failed': failed_id,
262278
'error': 'No tests failed',
263279
}, f)
264280
return 1
265281

266-
is_module = results.is_module(failed)
267-
268-
if is_module:
282+
failed_is_module = _is_module(failed_id)
283+
if failed_is_module:
269284
if args.trace: print()
270-
print(f"Module \"{failed}\"'s collection failed; trying it by itself...", flush=True)
271-
failed_module = failed
285+
print(f"Module \"{failed_id}\"'s collection failed; trying it by itself...", flush=True)
286+
failed_module = failed_id
272287
tests = None
273288
else:
274289
if args.trace: print()
275-
print(f"Test \"{failed}\" failed; trying it by itself...", flush=True)
276-
failed_module = results.get_module(failed)
277-
tests = [failed]
290+
print(f"Test \"{failed_id}\" failed; trying it by itself...", flush=True)
291+
failed_module = _get_module(failed_id)
292+
tests = [failed_id]
278293

279294
solo = _run_pytest(args.tests_path, pytest_args, modules=[failed_module], tests=tests, trace=args.trace)
280-
if solo.get_outcome(failed) != 'passed':
295+
if solo.get_outcome(failed_id) != 'passed':
281296
print("That also fails by itself!", flush=True)
282297
if args.save_to:
283298
with args.save_to.open("w") as f:
284299
json.dump({
285-
'failed': failed,
286-
'error': f'{"Module" if is_module else "Test"} also fails by itself',
300+
'failed': failed_id,
301+
'error': f'{"Module" if failed_is_module else "Test"} also fails by itself',
287302
}, f)
288303
return 1
289304

290305
tests = results.get_tests()
291-
if not is_module:
292-
assert tests[-1] == failed
293-
tests = tests[:-1]
294306

295-
if args.trace: print()
296-
tests = _reduce_tests(args.tests_path, tests, failed, trace=args.trace, pytest_args=pytest_args)
307+
if args.trace: print()
308+
modules = _reduce_modules(args.tests_path, tests, failed_id, results.get_modules(), failed_module,
309+
trace=args.trace, pytest_args=pytest_args)
297310

298311
if args.trace: print()
299-
modules = [m for m in results.get_modules() if m != failed_module]
300-
modules = _reduce_modules(args.tests_path, tests if is_module else tests + [failed], failed,
301-
modules, failed_module, trace=args.trace, pytest_args=pytest_args)
312+
tests = _reduce_tests(args.tests_path, tests, failed_id, [*modules, failed_module],
313+
trace=args.trace, pytest_args=pytest_args)
302314

303315
if args.trace: print()
304316
print("Reduced failure set:")
@@ -309,7 +321,7 @@ def main():
309321
if args.save_to:
310322
with args.save_to.open("w") as f:
311323
json.dump({
312-
'failed': failed,
324+
'failed': failed_id,
313325
'modules': modules,
314326
'tests': tests,
315327
}, f)

tests/test_reduce.py

+30
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,33 @@ def test_reduce_pytest_args(tests_dir, pollute_in_collect, fail_collect):
241241
assert reduction['failed'] == failing
242242
assert reduction['modules'] == [get_test_module(polluter)]
243243
assert reduction['tests'] == [] if pollute_in_collect else [polluter]
244+
245+
246+
def test_reduce_polluter_test_in_single_module(tests_dir):
247+
test = seq2p(tests_dir, 0)
248+
test.write_text(dedent("""\
249+
import sys
250+
251+
def test_polluter():
252+
sys.needs_this = True
253+
assert True
254+
255+
def test_nothing():
256+
assert True
257+
258+
def test_failing():
259+
assert not hasattr(sys, 'needs_this')
260+
"""))
261+
262+
reduction_file = tests_dir.parent / "reduction.json"
263+
264+
p = subprocess.run([sys.executable, '-m', 'pytest_cleanslate.reduce',
265+
'--save-to', reduction_file, '--trace', tests_dir], check=False)
266+
assert p.returncode == 0
267+
268+
with reduction_file.open("r") as f:
269+
reduction = json.load(f)
270+
271+
assert reduction['failed'] == f"{str(test)}::test_failing"
272+
assert reduction['modules'] == []
273+
assert reduction['tests'] == [f"{str(test)}::test_polluter"]

0 commit comments

Comments
 (0)