| # Copyright 2025 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| """ Data structures holding test suites and test cases. | |
| TestCase: A single named test case name with disabled flag and bug annotation. | |
| TestSuite: A single named test suite with a list of `TestCase`s. | |
| Tests: Holds a list of TestSuites. This is the top data structure for the | |
| program. Can be initialized with a list of TestSuites or a | |
| `_DictStructuredTests`. | |
| load_and_merge(enumerated_tests: Tests) -> Tests: Starts with a set of tests | |
| produced by the enumeration mechanism (in command_runner.TestEnumerator). | |
| Loads the saved test info, and uses that to update/overwrite the enumerated | |
| list. Returns the combined list. Prints the name of any tests found in the | |
| saved file but not the enumerated tests - this is possible if a test has | |
| been dropped from the current test set but is still in the file. | |
| """ | |
| import re | |
| from dataclasses import dataclass | |
| from typing import Any | |
| import util | |
| @dataclass | |
| class TestCase: | |
| """A named test case, with bug annotation and disabled status.""" | |
| name: str | |
| disabled: bool = False | |
| bug: str = "" | |
| @dataclass | |
| class TestSuite: | |
| """Contains a named test suite and its `TestCase`s. | |
| Can be initialized from a list of test cases or an internal | |
| _DictTestSuite.""" | |
| name: str | |
| cases: list[TestCase] | |
| compact_name: str = "" | |
| def __init__( | |
| self, | |
| name: str = "", | |
| cases: list[TestCase] | None = None, | |
| dict_suite: "_DictTestSuite | None" = None, | |
| ) -> None: | |
| assert cases is None or dict_suite is None | |
| if dict_suite: | |
| self.name = dict_suite.name | |
| self.cases = [c for c in dict_suite.cases.values()] | |
| else: | |
| self.name = name | |
| self.cases = cases or [] | |
| self.compact_name = re.sub( | |
| "^fuchsia-pkg://fuchsia.com/", "fx/", self.name | |
| ) | |
| def _to_dict_suite(self) -> "_DictTestSuite": | |
| return _DictTestSuite( | |
| name=self.name, cases={case.name: case for case in self.cases} | |
| ) | |
| # For merging two sets of tests, dicts are easier than lists. | |
| @dataclass | |
| class _DictTestSuite: | |
| """TestSuite, but with a dict to find `TestCase`s by their name. | |
| Used for merging two sets of tests.""" | |
| name: str | |
| cases: dict[str, TestCase] | |
| @dataclass | |
| class _DictStructuredTests: | |
| """A dict to find `TestSuite`s by their name. | |
| Used for merging two sets of tests.""" | |
| suites: dict[str, _DictTestSuite] | |
| class Tests: | |
| """Public-facing top-level data structure of tests. | |
| Field `tests` is public, a list of `TestSuite`. | |
| Method: save() writes to a fixed-location file. | |
| save() can ignore a parameter for interop with GUI events.""" | |
| def __init__( | |
| self, | |
| test_dict: _DictStructuredTests | None = None, | |
| test_list: list[TestSuite] | None = None, | |
| ) -> None: | |
| self.tests: list[TestSuite] = [] | |
| if test_dict: | |
| self.tests.extend( | |
| [ | |
| TestSuite(dict_suite=test) | |
| for test in test_dict.suites.values() | |
| ] | |
| ) | |
| if test_list: | |
| self.tests.extend(test_list) | |
| def save(self, _: Any = None) -> None: | |
| """Save all test cases that are disabled and/or have bug info.""" | |
| save_list: list[TestSuite] = [] | |
| for suite in self.tests: | |
| save_suite = TestSuite(suite.name) | |
| for case in suite.cases: | |
| if case.bug or case.disabled: | |
| save_suite.cases.append(case) | |
| if save_suite.cases: | |
| save_list.append(save_suite) | |
| util.save_program_state(save_list) | |
| def __str__(self) -> str: | |
| return f"Tests {self.tests}" | |
| def _to_dict_tests(self) -> _DictStructuredTests: | |
| suites = {} | |
| for t in self.tests: | |
| cases_dict = {c.name: c for c in t.cases} | |
| suites[t.name] = _DictTestSuite(name=t.name, cases=cases_dict) | |
| return _DictStructuredTests(suites) | |
| def _saved_tests() -> "Tests": | |
| """Loads the program state file and parses the JSON.""" | |
| raw_tests = util.load_program_state() | |
| suites = [] | |
| for test in raw_tests: | |
| suite_name = test["name"] | |
| cases = [] | |
| for case in test["cases"]: | |
| name: str = case["name"] | |
| disabled: bool = case["disabled"] | |
| bug: str = case["bug"] | |
| cases.append(TestCase(name=name, disabled=disabled, bug=bug)) | |
| suites.append(TestSuite(name=suite_name, cases=cases)) | |
| return Tests(test_list=suites) | |
| def load_and_merge(enumerated_tests: "Tests") -> "Tests": | |
| """Combines the saved test state with the enumerated tests.""" | |
| test_dict = enumerated_tests._to_dict_tests() | |
| test_accumulator = test_dict.suites | |
| tests_from_file = _saved_tests() | |
| for suite in tests_from_file.tests: | |
| if suite.name not in test_accumulator: | |
| print(f"Test {suite.name} not found in enumerated tests.") | |
| test_accumulator[suite.name] = suite._to_dict_suite() | |
| else: | |
| t = test_accumulator[suite.name] | |
| for case in suite.cases: | |
| if case.name not in t.cases: | |
| print( | |
| f"Test case {suite.name} : {case.name} not found in enumerated tests." | |
| ) | |
| test_accumulator[suite.name].cases[case.name] = case | |
| return Tests(test_dict=test_dict) |