diff --git a/docs/news.d/1240.bugfix.rst b/docs/news.d/1240.bugfix.rst new file mode 100644 index 000000000..112e034db --- /dev/null +++ b/docs/news.d/1240.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a ``KeyError`` during test prioritization on subsequent runs when +``scan_tests_from_file()`` was given a relative path, a ``pathlib.Path``, or a +forward-slash path on Windows instead of the project's exact source filename. diff --git a/tests/unit/test_test_scheduler.py b/tests/unit/test_test_scheduler.py index 13659d72c..ca49f8eba 100644 --- a/tests/unit/test_test_scheduler.py +++ b/tests/unit/test_test_scheduler.py @@ -9,6 +9,7 @@ """ import unittest +from pathlib import Path from unittest import mock from vunit.test.runner import TestScheduler @@ -63,6 +64,16 @@ def test_that_new_test_suites_go_to_set_2(self): self.assertEqual(result[set_idx]["total_exec_time"], 0) self.assertEqual(len(result[set_idx]["test_suites"]), 0) + def test_that_scanned_file_with_different_path_spelling_and_history_goes_to_set_3(self): + test_suite = self._create_test_suite("lib1.tb1.test1", ["lib1.tb1.test1"], "tests.vhd") + self._add_test_history(test_suite.name, test_suite.name, "passed", 1, 10) + latest_dependency_updates = {str(Path("tests.vhd").resolve()): 0} + + scheduler = TestScheduler([test_suite], 1, latest_dependency_updates, self._test_history) + result = scheduler._create_test_suite_sets([test_suite]) + + self.assertEqual(result[3]["test_suites"], [{"test_suite": test_suite, "exec_time": 10}]) + def test_that_skipped_test_suites_go_to_set_2(self): latest_dependency_updates = dict(file1=0) test_suites = [self._create_test_suite("lib1.tb1.test1", ["lib1.tb1.test1"], "file1")] diff --git a/vunit/test/runner.py b/vunit/test/runner.py index 0cc986159..4d909e554 100644 --- a/vunit/test/runner.py +++ b/vunit/test/runner.py @@ -458,9 +458,12 @@ def _create_test_suite_sets(self, test_suites): if test_data: if test_data["total_time"] is not None: exec_time += test_data["total_time"] - updated_dependency = self._latest_dependency_updates[test_suite.file_name] > test_data.get( - "start_time", 0 - ) + # Tests may be scanned from a file which is not a project source file + # (scan_tests_from_file). Without dependency information, treat it as + # updated like _get_latest_dependency_updates does for changed files. + updated_dependency = self._latest_dependency_updates.get( + test_suite.file_name, 10e9 + ) > test_data.get("start_time", 0) if test_data["skipped"]: set_idx = 2 diff --git a/vunit/ui/__init__.py b/vunit/ui/__init__.py index 17503a6c8..52b75baf8 100644 --- a/vunit/ui/__init__.py +++ b/vunit/ui/__init__.py @@ -1032,7 +1032,8 @@ def _get_test_list_depending_on_change(self, test_list): self._test_history = self._get_test_history(simulator_if=None) def depending_on_change(test_suite): - latest_dependency_update = self._latest_dependency_updates[test_suite.file_name] + # Unknown files (tests scanned from a non-project file) count as changed + latest_dependency_update = self._latest_dependency_updates.get(test_suite.file_name, 10e9) test_suite_history = self._test_history.get(test_suite.name, None) if test_suite_history: test_start_times = [test_data["start_time"] for test_data in test_suite_history.values()] diff --git a/vunit/ui/testbench.py b/vunit/ui/testbench.py index db9a0a788..342c54d7e 100644 --- a/vunit/ui/testbench.py +++ b/vunit/ui/testbench.py @@ -9,6 +9,7 @@ """ from fnmatch import fnmatch +from pathlib import Path from .common import lower_generics from .test import Test @@ -240,4 +241,21 @@ def scan_tests_from_file(self, file_name): the ``NESTED_TEST_SUITE`` macro should be used instead of the ``TEST_SUITE`` macro. """ - self._test_bench.scan_tests_from_file(file_name) + self._test_bench.scan_tests_from_file(self._get_project_file_name(file_name)) + + def _get_project_file_name(self, file_name): + """ + Return the name under which the project knows file_name. + + Test suites are associated with the file their tests are scanned from, and that + name is used to look up dependency information (e.g. for test prioritization). + The project knows a source file by its resolved path, or by the path of its + preprocessed copy if preprocessing is enabled. Map any spelling of the path + (relative, pathlib.Path, posix style on Windows, original instead of preprocessed + file) to that name. Files which are not part of the project are returned resolved. + """ + path = Path(file_name).resolve() + for source_file in self._library._project.get_source_files_in_order(): # pylint: disable=protected-access + if Path(source_file.original_name).resolve() == path: + return source_file.name + return str(path)