Skip to content

Commit cd065a8

Browse files
committed
Fix race conditions in test runner
1 parent cc850eb commit cd065a8

File tree

2 files changed

+58
-51
lines changed

2 files changed

+58
-51
lines changed

graalpython/com.oracle.graal.python.test/src/runner.py

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -204,39 +204,44 @@ class AbstractResult(unittest.TestResult):
204204
def __init__(self, test_suite: 'TestSuite'):
205205
super().__init__()
206206
self.test_suite = test_suite
207+
self.start_time = None
207208

208209
def test_id(self, test):
209210
return TestId.from_test_case(self.test_suite.test_file, test)
210211

212+
def startTest(self, test):
213+
self.start_time = time.time()
214+
211215
@abstractmethod
212216
def report_result(self, result: TestResult):
213217
pass
214218

219+
def make_result(self, test, status: TestStatus, **kwargs):
220+
return TestResult(status=status, test_id=self.test_id(test), duration=(time.time() - self.start_time), **kwargs)
221+
215222
def addSuccess(self, test):
216223
super().addSuccess(test)
217-
self.report_result(TestResult(status=TestStatus.SUCCESS, test_id=self.test_id(test)))
224+
self.report_result(self.make_result(test, status=TestStatus.SUCCESS))
218225

219226
def addFailure(self, test, err):
220227
super().addFailure(test, err)
221-
self.report_result(
222-
TestResult(status=TestStatus.FAILURE, test_id=self.test_id(test), param=format_exception(err)))
228+
self.report_result(self.make_result(test, status=TestStatus.FAILURE, param=format_exception(err)))
223229

224230
def addError(self, test, err):
225231
super().addError(test, err)
226-
self.report_result(TestResult(status=TestStatus.ERROR, test_id=self.test_id(test), param=format_exception(err)))
232+
self.report_result(self.make_result(test, status=TestStatus.ERROR, param=format_exception(err)))
227233

228234
def addSkip(self, test, reason):
229235
super().addSkip(test, reason)
230-
self.report_result(TestResult(status=TestStatus.SKIPPED, test_id=self.test_id(test), param=reason))
236+
self.report_result(self.make_result(test, status=TestStatus.SKIPPED, param=reason))
231237

232238
def addExpectedFailure(self, test, err):
233239
super().addExpectedFailure(test, err)
234-
self.report_result(
235-
TestResult(status=TestStatus.EXPECTED_FAILURE, test_id=self.test_id(test), param=format_exception(err)))
240+
self.report_result(self.make_result(test, status=TestStatus.EXPECTED_FAILURE, param=format_exception(err)))
236241

237242
def addUnexpectedSuccess(self, test):
238243
super().addUnexpectedSuccess(test)
239-
self.report_result(TestResult(status=TestStatus.UNEXPECTED_SUCCESS, test_id=self.test_id(test)))
244+
self.report_result(self.make_result(test, status=TestStatus.UNEXPECTED_SUCCESS))
240245

241246

242247
class DirectResult(AbstractResult):
@@ -286,21 +291,14 @@ class TestRunner:
286291
def __init__(self, *, failfast, report_durations):
287292
self.failfast = failfast
288293
self.report_durations = report_durations
289-
self.events = []
290294
self.results = []
291295
self.total_duration = 0.0
292-
self.last_started_test = None
293-
self.last_started_timestamp = None
294296

295-
def report_start(self, test_id: TestId):
296-
self.last_started_test = test_id
297-
self.last_started_timestamp = time.time()
297+
@staticmethod
298+
def report_start(test_id: TestId):
298299
log(f"{test_id} ... ", incomplete=True)
299300

300301
def report_result(self, result: TestResult):
301-
result.duration = time.time() - self.last_started_timestamp
302-
self.last_started_timestamp = None
303-
self.last_started_test = None
304302
self.results.append(result)
305303
message = f"{result.test_id} ... {result.status}"
306304
if result.status == TestStatus.SKIPPED and result.param:
@@ -398,6 +396,7 @@ def generate_mx_report(self, path: str):
398396
'duration': result.duration,
399397
})
400398
with open(path, 'w') as f:
399+
# noinspection PyTypeChecker
401400
json.dump(report_data, f)
402401

403402

@@ -422,7 +421,6 @@ def __init__(self, *, num_processes, subprocess_args, separate_workers, **kwargs
422421
self.separate_workers = separate_workers
423422
self.stop_event = threading.Event()
424423
self.crashes = []
425-
self.last_out_pos = 0
426424
self.default_test_timeout = 600
427425

428426
def report_result(self, result: TestResult):
@@ -484,42 +482,51 @@ def run_partitions_in_subprocesses(self, executor, partitions: list[list[TestId]
484482
print("Interrupted!")
485483
sys.exit(1)
486484

487-
def process_event(self, event, out_file):
488-
self.events.append(event)
489-
match event['event']:
490-
case 'testStarted':
491-
self.last_out_pos = event['out_pos']
492-
self.report_start(event['test'])
493-
case 'testResult':
494-
out_end = event['out_pos']
495-
test_output = ''
496-
if self.last_out_pos != out_end:
497-
out_file.seek(self.last_out_pos)
498-
test_output = out_file.read(out_end - self.last_out_pos)
499-
result = TestResult(
500-
test_id=event['test'],
501-
status=event['status'],
502-
param=event.get('param'),
503-
output=test_output,
504-
)
505-
self.report_result(result)
506-
self.last_out_pos = event['out_pos']
507-
508485
def run_in_subprocess_and_watch(self, tests: list[TestId]):
486+
remaining_tests = tests
487+
last_started_test: TestId | None = None
488+
last_started_time: float | None = None
489+
last_out_pos = 0
509490
conn, child_conn = multiprocessing.Pipe()
510491
with (
511492
tempfile.NamedTemporaryFile(prefix='graalpytest-in-', mode='w+') as tests_file,
512493
tempfile.NamedTemporaryFile(prefix='graalpytest-out-', mode='w+') as out_file,
513494
):
514495
env = os.environ.copy()
515496
env['IN_PROCESS'] = '1'
516-
remaining_tests = tests
497+
498+
def process_event(event):
499+
nonlocal remaining_tests, last_started_test, last_started_time, last_out_pos
500+
match event['event']:
501+
case 'testStarted':
502+
remaining_tests.remove(event['test'])
503+
self.report_start(event['test'])
504+
last_started_test = event['test']
505+
last_started_time = time.time()
506+
last_out_pos = event['out_pos']
507+
case 'testResult':
508+
out_end = event['out_pos']
509+
test_output = ''
510+
if last_out_pos != out_end:
511+
out_file.seek(last_out_pos)
512+
test_output = out_file.read(out_end - last_out_pos)
513+
result = TestResult(
514+
test_id=event['test'],
515+
status=event['status'],
516+
param=event.get('param'),
517+
output=test_output,
518+
)
519+
self.report_result(result)
520+
last_started_test = None
521+
last_started_time = None
522+
last_out_pos = event['out_pos']
523+
517524
while remaining_tests and not self.stop_event.is_set():
518-
self.last_out_pos = out_file.tell()
519-
python_args = ['-u', *self.subprocess_args]
525+
last_out_pos = out_file.tell()
520526
cmd = [
521527
sys.executable,
522-
*python_args,
528+
'-u',
529+
*self.subprocess_args,
523530
__file__,
524531
'--pipe-fd', str(child_conn.fileno()),
525532
'--tests-file', tests_file.name,
@@ -543,11 +550,11 @@ def run_in_subprocess_and_watch(self, tests: list[TestId]):
543550

544551
while process.poll() is None:
545552
while conn.poll(0.1):
546-
self.process_event(conn.recv(), out_file)
553+
process_event(conn.recv())
547554
if self.stop_event.is_set():
548555
interrupt_process(process)
549556
break
550-
if self.last_started_timestamp is not None and time.time() - self.last_started_timestamp >= self.default_test_timeout:
557+
if last_started_time is not None and time.time() - last_started_time >= self.default_test_timeout:
551558
interrupt_process(process)
552559
timed_out = True
553560
# Drain the pipe
@@ -558,11 +565,11 @@ def run_in_subprocess_and_watch(self, tests: list[TestId]):
558565
if self.stop_event.is_set():
559566
return
560567
while conn.poll(0.1):
561-
self.process_event(conn.recv(), out_file)
568+
process_event(conn.recv())
562569
if returncode != 0 or timed_out:
563-
out_file.seek(self.last_out_pos)
570+
out_file.seek(last_out_pos)
564571
output = out_file.read()
565-
if self.last_started_test:
572+
if last_started_test:
566573
if timed_out:
567574
message = "Timed out"
568575
elif returncode >= 0:
@@ -574,12 +581,11 @@ def run_in_subprocess_and_watch(self, tests: list[TestId]):
574581
signal_name = str(-returncode)
575582
message = f"Test process killed by signal {signal_name}"
576583
self.report_result(TestResult(
577-
test_id=self.last_started_test,
584+
test_id=last_started_test,
578585
status=TestStatus.ERROR,
579586
param=message,
580587
output=output,
581588
))
582-
remaining_tests = remaining_tests[remaining_tests.index(self.last_started_test) + 1:]
583589
continue
584590
else:
585591
# Crashed outside of tests, don't retry
@@ -893,9 +899,11 @@ def main():
893899

894900
if IS_GRAALPY:
895901
if get_bool_env('GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS'):
902+
# noinspection PyUnresolvedReferences
896903
if not __graalpython__.java_assert():
897904
sys.exit(
898905
"Java assertions are not enabled, refusing to run. Add --vm.ea to your invocation. Set GRAALPYTEST_ALLOW_NO_JAVA_ASSERTIONS=true to disable this check\n")
906+
# noinspection PyUnresolvedReferences
899907
if not hasattr(__graalpython__, 'tdebug'):
900908
sys.exit("Needs to be run with --experimental-options --python.EnableDebuggingBuiltins\n")
901909

graalpython/lib-python/3/test/conftest.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,4 @@ serial_tests = [
3030
]
3131
partial_splits_individual_tests = [
3232
'test_multiprocessing_spawn',
33-
'test_multiprocessing_graalpy',
3433
]

0 commit comments

Comments
 (0)