Skip to content

Commit e1c563a

Browse files
committed
WIP: Updated test priority.
1 parent 74dc4ba commit e1c563a

File tree

6 files changed

+172
-9
lines changed

6 files changed

+172
-9
lines changed

vunit/dependency_graph.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ def get_direct_dependencies(self, node: T) -> Set[T]:
126126
"""
127127
return self._backward.get(node, set())
128128

129+
def get_direct_dependents(self, node: T) -> Set[T]:
130+
"""
131+
Get the direct dependents of node.
132+
"""
133+
return self._forward.get(node, set())
134+
135+
def get_independents(self) -> Set[T]:
136+
"""
137+
Get nodes without dependencies.
138+
"""
139+
return {node for node in self._nodes if not self._backward.get(node, False)}
140+
129141

130142
class CircularDependencyException(Exception):
131143
"""

vunit/project.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ def _handle_circular_dependency(exception):
473473
" ->\n".join(source_file.name for source_file in exception.path),
474474
)
475475

476-
def _get_compile_timestamps(self, files):
476+
def get_compile_timestamps(self, files):
477477
"""
478478
Return a dictionary of mapping file to the timestamp when it
479479
was compiled or None if it was not compiled
@@ -509,7 +509,7 @@ def _get_files_to_recompile(self, files, dependency_graph, incremental):
509509
param: files: a list of type SourceFile
510510
param: dependency_graph: The DependencyGraph object to be used
511511
"""
512-
timestamps = self._get_compile_timestamps(files)
512+
timestamps = self.get_compile_timestamps(files)
513513
result_list = []
514514
for source_file in files:
515515
if (not incremental) or self._needs_recompile(dependency_graph, source_file, timestamps):

vunit/test/list.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ def keep_matches(self, test_filter):
3535
"""
3636
self._test_suites = [test for test in self._test_suites if test.keep_matches(test_filter)]
3737

38+
def sort(self, key):
39+
self._test_suites.sort(key=key)
40+
3841
@property
3942
def num_tests(self):
4043
"""

vunit/test/report.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,9 @@ def to_junit_xml_str(self, xunit_xml_format="jenkins"):
241241
xml = ElementTree.tostring(root, encoding="unicode")
242242
return xml
243243

244+
def __iter__(self):
245+
return iter(self._test_results.values())
246+
244247

245248
class TestStatus(object):
246249
"""
@@ -271,12 +274,14 @@ class TestResult(object):
271274
Represents the result of a single test case
272275
"""
273276

274-
def __init__(self, name, status, time, output_file_name):
277+
def __init__(self, name, status, time, output_file_name, test_suite_name, start_time):
275278
assert status in (PASSED, FAILED, SKIPPED)
276279
self.name = name
277280
self._status = status
278281
self.time = time
279282
self._output_file_name = output_file_name
283+
self.test_suite_name = test_suite_name
284+
self.start_time = start_time
280285

281286
@property
282287
def output(self):

vunit/test/runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def _add_results(
327327

328328
for test_name in test_suite.test_names:
329329
status = results[test_name]
330-
self._report.add_result(test_name, status, time_per_test, output_file_name)
330+
self._report.add_result(test_name, status, time_per_test, output_file_name, test_suite.name, start_time)
331331
self._report.print_latest_status(total_tests=num_tests)
332332
print()
333333

vunit/ui/__init__.py

Lines changed: 148 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)