| # Copyright 2023 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 model and associated methods for tests.json files. | |
| tests.json is generated by the GN build for Fuchsia, and it contains | |
| descriptions of all test targets included in a build. | |
| It is typically stored at the root of the output directory for the | |
| Fuchsia build, and it is the main entrypoint to learn about what | |
| tests are available and how to rebuild them. | |
| """ | |
| from dataclasses import dataclass | |
| import json | |
| import typing | |
| from dataparse import dataparse | |
| @dataparse | |
| @dataclass | |
| class TestSection: | |
| """Provides details for a specific test in tests.json.""" | |
| # The name of the test. This is unique within the file. | |
| name: str | |
| # The build label for the test. | |
| label: str | |
| # The os configured for the test. | |
| os: str | |
| # The build label for the component being tested in this test. | |
| component_label: str | None = None | |
| # The build label for the package containing the test. | |
| package_label: str | None = None | |
| # If the test runs on the device, this is the URL of the test to execute. | |
| package_url: str | None = None | |
| # If the test runs on the host, this is the path of the binary to execute. | |
| path: str | None = None | |
| # Experimental host binary paths for test components | |
| new_path: str | None = None | |
| # If the test is a device tests, this is the argument to pass to | |
| # ffx test run --parallel | |
| parallel: int | None = None | |
| # Indicates that test_manager should create no exception channels as it would | |
| # otherwise do to detect panics. Some tests that create exception channels at the job | |
| # level will fail if test_manager creates its customary exception channels. | |
| create_no_exception_channel: bool | None = None | |
| # Indicates the path to the runtime_deps json file, which lists the files necessary to run the | |
| # test on a test runner successfully. | |
| runtime_deps: str | None = None | |
| # Timeout interval in seconds. This value is None unless a timeout is supplied via test_specs. | |
| timeout_secs: int | None = None | |
| # The argument to pass to the test binary to list test cases. | |
| list_cases_argument: str | None = None | |
| @dataparse | |
| @dataclass | |
| class DimensionsEntry: | |
| """A single entry for an environment's test dimensions.""" | |
| # The type of device this test is intended to run on. | |
| # Missing if the test is not for devices. | |
| device_type: str | None = None | |
| @dataparse | |
| @dataclass | |
| class EnvironmentEntry: | |
| """Provides details for a test environment in tests.json.""" | |
| # The dimensions for this environment. | |
| dimensions: DimensionsEntry | |
| class TestFileError(Exception): | |
| """There was an error processing the contents of the tests.json file.""" | |
| @dataparse | |
| @dataclass | |
| class TestEntry: | |
| """tests.json consists of a single list of TestEntry.""" | |
| # The "test" field for a specific entry in the file. | |
| test: TestSection | |
| # The "environments" field for a specific entry in the file. | |
| environments: list[EnvironmentEntry] | None = None | |
| # Optional field that is set for boot tests only. | |
| is_boot_test: bool | None = None | |
| # Indicates if the test expects an SSH connection to a device. | |
| expects_ssh: bool | None = None | |
| # Indicates if the test requires a running Fuchsia package server. | |
| needs_package_server: bool | None = None | |
| @classmethod | |
| def from_file( | |
| cls: typing.Type[typing.Self], file: str | |
| ) -> list[typing.Self]: | |
| """Parse the file at the given path into a list of TestEntry. | |
| This returns a list of entries because the ordering of the | |
| tests.json file is the default order in which tests are | |
| executed. | |
| Args: | |
| file (os.PathLike): Path to the file to parse. | |
| Raises: | |
| IOError: If reading the file fails. | |
| JSONDecodeError: If the file is not valid JSON. | |
| TestFileError: If the tests.json file was found to be invalid. | |
| Returns: | |
| list[TestEntry]: List of test entries contained in the file. | |
| """ | |
| with open(file, "r") as f: | |
| vals = json.load(f) | |
| if not isinstance(vals, list): | |
| raise TestFileError( | |
| "Expected a list at top-level of tests.json, found " | |
| + str(type(vals)) | |
| ) | |
| ret: list[typing.Self] = list( | |
| map(TestEntry.from_dict, vals) # type:ignore | |
| ) | |
| name_path_pairs: typing.Set[typing.Tuple[str, str | None]] = set() | |
| for v in ret: | |
| if (v.test.name, v.test.path) in name_path_pairs: | |
| raise TestFileError( | |
| f"Expected all names/path pairs to be unique in tests.json, but found {v.test.name} twice for the same path." | |
| ) | |
| name_path_pairs.add((v.test.name, v.test.path)) | |
| return ret |