1313import pytest
1414
1515from cvise .passes .abstract import AbstractPass , PassResult # noqa: E402
16- from cvise .passes .hint_based import HintBasedPass # noqa: E402
16+ from cvise .passes .hint_based import HintBasedPass , HintState # noqa: E402
1717from cvise .utils import sigmonitor , statistics , testing # noqa: E402
1818from cvise .utils .fileutil import filter_files_by_patterns
1919from cvise .utils .hint import Hint , HintBundle , Patch
2323baz
2424"""
2525
26- PARALLEL_TESTS = 10
26+ DEFAULT_PARALLEL_TESTS = 10
2727
2828
2929class StubPass (AbstractPass ):
@@ -291,7 +291,23 @@ def with_colordiff(fp, with_tty) -> None:
291291
292292
293293@pytest .fixture
294- def manager (tmp_path : Path , input_path : Path , interestingness_script : str , job_timeout : int , print_diff : bool ):
294+ def parallel_tests () -> int :
295+ """The default parallel_tests parameter.
296+
297+ Can be overridden in particular tests.
298+ """
299+ return DEFAULT_PARALLEL_TESTS
300+
301+
302+ @pytest .fixture
303+ def manager (
304+ tmp_path : Path ,
305+ input_path : Path ,
306+ interestingness_script : str ,
307+ job_timeout : int ,
308+ print_diff : bool ,
309+ parallel_tests : int ,
310+ ):
295311 SAVE_TEMPS = False
296312 NO_CACHE = False
297313 SKIP_KEY_OFF = True # tests shouldn't listen to keyboard
@@ -317,7 +333,7 @@ def manager(tmp_path: Path, input_path: Path, interestingness_script: str, job_t
317333 job_timeout ,
318334 SAVE_TEMPS ,
319335 [input_path ],
320- PARALLEL_TESTS ,
336+ parallel_tests ,
321337 NO_CACHE ,
322338 SKIP_KEY_OFF ,
323339 SHADDAP ,
@@ -412,7 +428,7 @@ def test_give_up_on_repeating_timeouts(input_path: Path, manager):
412428 manager .run_passes ([p ], interleaving = False )
413429 assert extra_dir_count () >= manager .MAX_TIMEOUTS
414430 # we should've stopped soon after MAX_TIMEOUTS, at worst a batch of jobs later.
415- assert extra_dir_count () <= 2 * max (manager .MAX_TIMEOUTS , PARALLEL_TESTS )
431+ assert extra_dir_count () <= 2 * max (manager .MAX_TIMEOUTS , DEFAULT_PARALLEL_TESTS )
416432
417433
418434def test_interleaving_letter_removals (input_path : Path , manager ):
@@ -429,7 +445,7 @@ def test_interleaving_letter_removals(input_path: Path, manager):
429445
430446
431447@pytest .mark .skipif (os .name != 'posix' , reason = 'requires POSIX for command-line tools' )
432- @pytest .mark .parametrize ('input_contents' , ['ababacac' * PARALLEL_TESTS ])
448+ @pytest .mark .parametrize ('input_contents' , ['ababacac' * DEFAULT_PARALLEL_TESTS ])
433449@pytest .mark .parametrize ('interestingness_script' , [r"grep a {test_case} && ! grep '\(.\)\1' {test_case}" ])
434450def test_interleaving_letter_removals_large (input_path : Path , manager ):
435451 """Test that multiple passes executed in interleaving way can delete all but one character.
@@ -452,20 +468,22 @@ def test_interleaving_letter_removals_large(input_path: Path, manager):
452468@pytest .mark .parametrize ('interestingness_script' , [r'false {test_case}' ])
453469def test_interleaving_round_robin_transforms (manager : testing .TestManager ):
454470 tracing_queue = multiprocessing .Manager ().Queue ()
455- passes = [TracingHintPass (tracing_queue , letters_to_remove = chr (ord ('a' ) + i )) for i in range (PARALLEL_TESTS )]
471+ passes = [
472+ TracingHintPass (tracing_queue , letters_to_remove = chr (ord ('a' ) + i )) for i in range (DEFAULT_PARALLEL_TESTS )
473+ ]
456474 manager .run_passes (passes , interleaving = True )
457475
458476 transform_calls = []
459477 while not tracing_queue .empty ():
460478 transform_calls .append (tracing_queue .get ())
461479
462480 # all passes should've gotten equal number of jobs
463- execs_per_pass = [transform_calls .count (str (i )) for i in range (PARALLEL_TESTS )]
481+ execs_per_pass = [transform_calls .count (str (i )) for i in range (DEFAULT_PARALLEL_TESTS )]
464482 assert min (execs_per_pass ) == max (execs_per_pass )
465483 # we cannot assert the ideal round-robin order (like 123..N123..) because concurrent writes to the queue are racy,
466484 # but at least it's almost guaranteed that no pass should be recorded N times in a row.
467- for i in range (len (transform_calls ) - PARALLEL_TESTS + 1 ):
468- slice = transform_calls [i : i + PARALLEL_TESTS ]
485+ for i in range (len (transform_calls ) - DEFAULT_PARALLEL_TESTS + 1 ):
486+ slice = transform_calls [i : i + DEFAULT_PARALLEL_TESTS ]
469487 assert min (slice ) != max (slice )
470488
471489
@@ -637,3 +655,39 @@ def _assert_stats_validity(pass_statistic: statistics.PassStatistic, start_time:
637655 assert stat .worked + stat .failed <= stat .totally_executed
638656 elapsed = time .monotonic () - start_time
639657 assert sum (stat .total_seconds for stat in stats ) <= elapsed
658+
659+
660+ class MockHintState (HintState ):
661+ def subset_of (self , other : HintState ) -> bool :
662+ # We only want to skip state 2 if state 1 has already succeeded
663+ return self .ptr == 2 and other .ptr == 1
664+
665+ def real_chunk (self ) -> int :
666+ return 1
667+
668+
669+ class StateSkippingPass (AbstractPass ):
670+ """A pass that triggers the subset skipping logic, causing its state to exhaust unexpectedly."""
671+
672+ def new (self , test_case : Path , * args , ** kwargs ) -> MockHintState :
673+ return MockHintState (tmp_dir = Path (), per_type_states = (), ptr = 1 , special_hints = ())
674+
675+ def advance (self , test_case : Path , state : MockHintState ) -> MockHintState | None :
676+ if state .ptr == 1 :
677+ return MockHintState (tmp_dir = Path (), per_type_states = (), ptr = 2 , special_hints = ())
678+ return None
679+
680+ def advance_on_success (self , test_case : Path , state : MockHintState , * args , ** kwargs ) -> MockHintState | None :
681+ return self .advance (test_case , state )
682+
683+ def transform (self , test_case : Path , state : MockHintState , * args , ** kwargs ) -> tuple [PassResult , MockHintState ]:
684+ with open (test_case , 'a' ) as f :
685+ f .write (f'modification { state .ptr } \n ' )
686+ return (PassResult .OK , state )
687+
688+
689+ @pytest .mark .parametrize ('parallel_tests' , [1 ])
690+ def test_scheduler_deadlock_on_empty_jobs (input_path : Path , manager : testing .TestManager ):
691+ """Verifies that the orchestrator avoids deadlock when subset-skipping exhausts remaining states."""
692+
693+ manager .run_passes ([StateSkippingPass ()], interleaving = True )
0 commit comments