@@ -159,16 +159,17 @@ def __init__(
159159
160160 self ._create_output_path (args .clean )
161161
162- database = self ._create_database ()
162+ self . _database = self ._create_database ()
163163 self ._project = Project (
164- database = database ,
164+ database = self . _database ,
165165 depend_on_package_body = simulator_class .package_users_depend_on_bodies ,
166166 )
167167
168- self ._test_bench_list = TestBenchList (database = database )
168+ self ._test_bench_list = TestBenchList (database = self . _database )
169169
170170 self ._builtins = Builtins (self , self ._vhdl_standard , simulator_class )
171171
172+ self ._dependency_graph = None
172173 self ._include_in_test_pattern : Optional [List [Union [str , Path ]]] = []
173174 self ._exclude_from_test_pattern : Optional [List [Union [str , Path ]]] = []
174175
@@ -773,6 +774,7 @@ def _update_test_filter(self, include_dependencies=None, exclude_dependencies=No
773774
774775 project = self ._project
775776 project_source_files = project .get_source_files_in_order ()
777+ self ._dependency_graph = project .create_dependency_graph (True )
776778
777779 def get_dependent_files (dependencies ):
778780 "Return all project files dependent on project files matching any of the dependencies."
@@ -792,8 +794,7 @@ def get_dependent_files(dependencies):
792794 dependency_files .add (source_file )
793795
794796 # Get dependent files, non-testbench files included
795- dependency_graph = project .create_dependency_graph (True )
796- dependent_files = set (project .get_affected_files (dependency_files , dependency_graph .get_dependent ))
797+ dependent_files = set (project .get_affected_files (dependency_files , self ._dependency_graph .get_dependent ))
797798
798799 return dependent_files
799800
@@ -832,13 +833,154 @@ def _create_simulator_if(self):
832833
833834 return self ._simulator_class .from_args (args = self ._args , output_path = self ._simulator_output_path )
834835
836+ def _get_test_suite_history (self ):
837+ key = "test_suite_history" .encode ()
838+ if key in self ._database :
839+ return self ._database [key ]
840+ else :
841+ return {}
842+
843+ def _get_latest_dependency_updates (self ):
844+ """
845+ Return the timestamp for the latest updated dependency of each file.
846+ """
847+ if not self ._dependency_graph :
848+ self ._dependency_graph = self ._project .create_dependency_graph (True )
849+
850+ # Start with leaf nodes in the dependency graph which has no dependencies
851+ # other then to themselves
852+ timestamps = {}
853+ source_files = self ._dependency_graph .get_independents ()
854+
855+ # Move towards the graph roots and record the maximum of the timestamps for each file and
856+ # its dependencies
857+ while source_files :
858+ source_file_timestamps = self ._project .get_compile_timestamps (source_files )
859+ updated_timestamps = {}
860+ for source_file , timestamp in source_file_timestamps .items ():
861+ dependency_timestamps = self ._project .get_compile_timestamps (
862+ self ._dependency_graph .get_direct_dependencies (source_file )
863+ )
864+ dependency_timestamps = [timestamp for timestamp in dependency_timestamps .values () if timestamp ]
865+ if dependency_timestamps :
866+ updated_timestamps [source_file .name ] = max (timestamp , * dependency_timestamps )
867+ elif timestamp is None :
868+ # If there is no timestamp, we set a timestamp well into the future to make
869+ # sure that all dependent tests consider it to be updated after the last test run.
870+ updated_timestamps [source_file .name ] = 1e10
871+ else :
872+ updated_timestamps [source_file .name ] = timestamp
873+
874+ timestamps = {** timestamps , ** updated_timestamps }
875+ source_files = set .union (
876+ * (self ._dependency_graph .get_direct_dependents (source_file ) for source_file in source_files )
877+ )
878+
879+ return timestamps
880+
881+ def _sort_test_list (self , test_list , simulator_if ):
882+ """
883+ Sort the test list.
884+ """
885+ latest_dependency_updates = self ._get_latest_dependency_updates ()
886+ test_suite_history = self ._get_test_suite_history ()
887+
888+ # Prune removed tests from history
889+ full_test_list = self ._test_bench_list .create_tests (simulator_if , self ._args .seed , self ._args .elaborate )
890+ full_test_list = {test_suite .name : test_suite for test_suite in full_test_list ._test_suites }
891+
892+ pruned_test_suite_history = {
893+ test_suite_name : test_suite_data
894+ for test_suite_name , test_suite_data in test_suite_history .items ()
895+ if test_suite_name in full_test_list
896+ }
897+ for test_suite_name , test_suite_data in pruned_test_suite_history .items ():
898+ pruned_test_suite_data = {
899+ test_name : test_data
900+ for test_name , test_data in test_suite_data .items ()
901+ if test_name in full_test_list [test_suite_name ].test_names
902+ }
903+ pruned_test_suite_history [test_suite_name ] = pruned_test_suite_data
904+
905+ test_suite_history = pruned_test_suite_history
906+
907+ def test_priority (test_suite ):
908+ # Priority is set to fail fast:
909+ # 1. Tests that failed before and for which no updates have been made. Expected to fail again. Shortest # test first (STF).
910+ # 2. Tests that failed before but updates have been made that can change that. STF.
911+ # 3. Tests without a history. New tests are more likely to fail and should be run early. Without
912+ # any execution time order, there is no further sorting within this group
913+ # 4. Tests that passed before but depends on updates. There is a risk that we've introduced new bugs. STF.
914+ # 5. Tests that passed before and for which there are no updates. They are expected to pass again.
915+ # Longest test first to optimize completion time when running with multiple threads
916+ #
917+ # Each test is given a priority number where the integer part is according to the priority above and
918+ # the decimal part seperates the tests within the group based on execution time.
919+
920+ test_suite_data = test_suite_history .get (test_suite .name , False )
921+ if not test_suite_data :
922+ return 3
923+
924+ highest_priority = None
925+ for test_name in test_suite .test_names :
926+ test_data = test_suite_data .get (test_name , False )
927+ if not test_data :
928+ priority = 3
929+ else :
930+ updated_dependency = latest_dependency_updates [test_suite .file_name ] > test_data ["start_time" ]
931+ if test_data ["failed" ]:
932+ if not updated_dependency :
933+ priority = 1 + test_data ["total_time" ] / 1e9
934+ else :
935+ priority = 2 + test_data ["total_time" ] / 1e9
936+ elif test_data ["skipped" ]:
937+ priority = 3
938+ elif updated_dependency :
939+ priority = 4 + test_data ["total_time" ] / 1e9
940+ else :
941+ priority = 6 - test_data ["total_time" ] / 1e9
942+
943+ highest_priority = priority if not highest_priority else min (highest_priority , priority )
944+
945+ return highest_priority
946+
947+ test_list .sort (test_priority )
948+
949+ def _update_test_suite_history (self , report ):
950+ # TODO: It is a problem if a test suite with several test cases (run_all_in_same_sim) is run partially.
951+ # Then previous history for the test cases not run is overwritten
952+ # Need to to know the test cases in the suite or we may save data for a deleted test case "forever"
953+ test_suite_data = {}
954+ all_tests = self ._test_bench_list .get_test_benches ()
955+ for test_result in report :
956+ if not test_result .test_suite_name in test_suite_data :
957+ test_suite_data [test_result .test_suite_name ] = dict ()
958+
959+ if not test_result .name in test_suite_data [test_result .test_suite_name ]:
960+ test_suite_data [test_result .test_suite_name ][test_result .name ] = dict ()
961+
962+ test_suite_data [test_result .test_suite_name ][test_result .name ]["total_time" ] = test_result .time
963+ test_suite_data [test_result .test_suite_name ][test_result .name ]["passed" ] = test_result .passed
964+ test_suite_data [test_result .test_suite_name ][test_result .name ]["skipped" ] = test_result .skipped
965+ test_suite_data [test_result .test_suite_name ][test_result .name ]["failed" ] = test_result .failed
966+ test_suite_data [test_result .test_suite_name ][test_result .name ]["start_time" ] = test_result .start_time
967+
968+ test_suite_history = self ._get_test_suite_history ()
969+
970+ for test_suite_name , data in test_suite_data .items ():
971+ test_suite_history [test_suite_name ] = data
972+
973+ self ._database ["test_suite_history" .encode ()] = test_suite_history
974+
835975 def _main_run (self , post_run ):
836976 """
837977 Main with running tests
838978 """
839979 simulator_if = self ._create_simulator_if ()
840980 test_list = self ._create_tests (simulator_if )
841981 self ._compile (simulator_if )
982+ # Sort after compilation has determined changed files
983+ self ._sort_test_list (test_list , simulator_if )
842984 print ()
843985
844986 start_time = ostools .get_time ()
@@ -853,6 +995,7 @@ def _main_run(self, post_run):
853995 del test_list
854996
855997 report .set_real_total_time (ostools .get_time () - start_time )
998+ self ._update_test_suite_history (report )
856999 report .print_str ()
8571000
8581001 if post_run is not None :
0 commit comments