1+ import platform
2+ from mutmut .custom_process_pool import Task
3+ from mutmut .custom_process_pool import CustomProcessPool
14import ast
25import fnmatch
36import gc
47import inspect
58import itertools
69import json
7- from multiprocessing import Pool , Process , set_start_method
10+ from multiprocessing import JoinableQueue , Pool , Process , Queue , set_start_method
811import multiprocessing
912import multiprocessing .connection
1013import os
4750)
4851from typing import (
4952 Dict ,
53+ Generic ,
5054 List ,
55+ TypeVar ,
5156 Union ,
5257)
5358
@@ -373,6 +378,8 @@ def new_tests(self):
373378 return self .ids - collected_test_names ()
374379
375380
381+ _pytest_initialized = False
382+
376383class PytestRunner (TestRunner ):
377384 # noinspection PyMethodMayBeStatic
378385 def execute_pytest (self , params : list [str ], ** kwargs ):
@@ -386,6 +393,10 @@ def execute_pytest(self, params: list[str], **kwargs):
386393 print (' exit code' , exit_code )
387394 if exit_code == 4 :
388395 raise BadTestExecutionCommandsException (params )
396+
397+ global _pytest_initialized
398+ _pytest_initialized = True
399+
389400 return exit_code
390401
391402 def run_stats (self , * , tests ):
@@ -889,7 +900,10 @@ def inner_timout_checker():
889900@click .option ('--max-children' , type = int )
890901@click .argument ('mutant_names' , required = False , nargs = - 1 )
891902def run (mutant_names , * , max_children ):
892- set_start_method ('spawn' )
903+ if platform .system () == 'Windows' :
904+ set_start_method ('spawn' )
905+ else :
906+ set_start_method ('fork' )
893907
894908 assert isinstance (mutant_names , (tuple , list )), mutant_names
895909 _run (mutant_names , max_children )
@@ -956,23 +970,6 @@ def _run(mutant_names: Union[tuple, list], max_children: Union[None, int]):
956970
957971 running_processes : set [Process ] = set ()
958972
959- def handle_finished_processes () -> int :
960- nonlocal running_processes
961- sentinels = [p .sentinel for p in running_processes ]
962- multiprocessing .connection .wait (sentinels )
963-
964- finished_processes = {p for p in running_processes if not p .is_alive ()}
965- running_processes -= finished_processes
966-
967- for p in finished_processes :
968- if mutmut .config .debug :
969- print (' worker exit code' , p .exitcode )
970- source_file_mutation_data_by_pid [p .pid ].register_result (pid = p .pid , exit_code = p .exitcode )
971-
972- p .close ()
973-
974- return len (finished_processes )
975-
976973 source_file_mutation_data_by_pid : dict [int , SourceFileMutationData ] = {} # many pids map to one MutationData
977974 running_children = 0
978975 count_tried = 0
@@ -996,6 +993,8 @@ def handle_finished_processes() -> int:
996993 # TODO: implement timeout for windows + unix
997994 # Thread(target=timeout_checker(mutants), daemon=True).start()
998995
996+ args : list [tuple [TestRunner , SourceFileMutationData , str , list [str ], Config ]] = []
997+
999998 # Now do mutation
1000999 for m , mutant_name , result in mutants :
10011000 print_stats (source_file_mutation_data_by_path )
@@ -1015,37 +1014,32 @@ def handle_finished_processes() -> int:
10151014 m .save ()
10161015 continue
10171016
1018- p = Process (target = _test_mutation , args = (runner , m , mutant_name , tests , mutmut .config ))
1019- running_processes .add (p )
1020- p .start ()
1021- pid = p .pid
1022- # in the parent
1023- source_file_mutation_data_by_pid [pid ] = m
1024- m .register_pid (pid = pid , key = mutant_name , estimated_time_of_tests = estimated_time_of_tests )
1025- running_children += 1
1026-
1027- if running_children >= max_children :
1028- count_finished = handle_finished_processes ()
1029- count_tried += count_finished
1030- running_children -= count_finished
1031-
1032- try :
1033- while running_children :
1034- print_stats (source_file_mutation_data_by_path )
1035- count_finished = handle_finished_processes ()
1036- count_tried += count_finished
1037- running_children -= count_finished
1038- except ChildProcessError :
1039- pass
1017+ args .append ((runner , m , mutant_name , tests , mutmut .config ))
1018+ source_file_mutation_data_by_pid [mutant_name ] = m
1019+ m .register_pid (pid = mutant_name , key = mutant_name , estimated_time_of_tests = estimated_time_of_tests )
1020+
1021+ tasks : list [Task ] = []
1022+ for arg in args :
1023+ tasks .append (Task (id = arg [2 ], args = arg , timeout_seconds = 1000 ))
1024+ pool = CustomProcessPool (tasks , _test_mutation , max_children )
1025+ done = 0
1026+ for finished_task in pool .run ():
1027+ done += 1
1028+ # print(f'Finished {done} tasks')
1029+ if finished_task .error :
1030+ print (finished_task )
1031+ # print(finished_task)
1032+ source_file_mutation_data_by_pid [finished_task .id ].register_result (pid = finished_task .id , exit_code = finished_task .result )
1033+ print_stats (source_file_mutation_data_by_path )
10401034 except KeyboardInterrupt :
1035+ pool .shutdown ()
10411036 print ('Stopping...' )
1042- stop_all_children (mutants )
10431037
10441038 t = datetime .now () - start
10451039
10461040 print_stats (source_file_mutation_data_by_path , force_output = True )
10471041 print ()
1048- print (f'{ count_tried / t .total_seconds ():.2f} mutations/second' )
1042+ print (f'{ len ( tasks ) / t .total_seconds ():.2f} mutations/second' )
10491043
10501044 if mutant_names :
10511045 print ()
@@ -1062,12 +1056,18 @@ def handle_finished_processes() -> int:
10621056 print ()
10631057
10641058
1065- def _test_mutation (runner : TestRunner , m : SourceFileMutationData , mutant_name : str , tests , config ):
1059+ def _test_mutation (task : Task ):
1060+ args : tuple [TestRunner , SourceFileMutationData , str , list [str ], Config ] = task .args
1061+ runner , m , mutant_name , tests , config = args
10661062 try :
10671063 mutmut .config = config
10681064
1069- with CatchOutput ():
1070- runner .list_all_tests ()
1065+ # ensure that we imported all files at least once per process
1066+ # before we set MUTANT_UNDER_TEST (so everything that runs at import
1067+ # time is not mutated)
1068+ if not _pytest_initialized :
1069+ with CatchOutput ():
1070+ runner .list_all_tests ()
10711071
10721072 os .environ ['MUTANT_UNDER_TEST' ] = mutant_name
10731073 setproctitle (f'mutmut: { mutant_name } ' )
@@ -1082,13 +1082,14 @@ def _test_mutation(runner: TestRunner, m: SourceFileMutationData, mutant_name: s
10821082
10831083 with CatchOutput ():
10841084 result = runner .run_tests (mutant_name = mutant_name , tests = tests )
1085- os ._exit (result )
1085+
1086+ return result
1087+ # os._exit(result)
10861088 except Exception as e :
10871089 with open (f'error.{ mutant_name } .log' , 'w' ) as log :
10881090 log .write (str (e ))
1089- os ._exit (- 1 )
1090-
1091-
1091+ log .flush ()
1092+ return - 24
10921093
10931094def tests_for_mutant_names (mutant_names ):
10941095 tests = set ()
0 commit comments