| #!/usr/bin/env python3 |
| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| """Runs, i.e. walks through, the test points of a manual test plan. |
| |
| This script prints the descriptions of each test point in a manual test plan |
| one by one and prompts whether they pass or fail. A manual test plan passes if |
| all test points are interactively verified. |
| |
| Typical usage: |
| |
| # This script can be run either directly, or |
| util/run_manual_tests.py sw/device/silicon_creator/rom/data/rom_manual_testplan.hjson |
| # via the `manual_test()` bazel rule. |
| bazel test -t- //sw/device/silicon_creator/rom:manual |
| """ |
| |
| import os |
| import re |
| import subprocess |
| from pathlib import Path |
| from typing import Any, Dict, List, Tuple |
| |
| import hjson |
| import typer |
| from pluralizer import Pluralizer |
| from rich.console import Console |
| from rich.markdown import Markdown |
| from rich.prompt import Confirm |
| from rich.text import Text |
| |
| pluralize = Pluralizer().pluralize |
| |
| |
| def _get_tty(): |
| """Returns the terminal that we should use. |
| |
| When this script is run directly, the terminal should be the terminal of the process. |
| When this script is run by bazel, the terminal should be the terminal of the bazel |
| client. Note that we cannot simply traverse the process tree, since this script is |
| run by the bazel server, which could be started from a different terminal. |
| """ |
| term = subprocess.check_output( |
| ["/bin/ps", "-p", f"{os.getpid()}", "-o", "tty="]).decode().strip() |
| if term == "?": |
| res = subprocess.check_output(["/bin/ps", "-axo", |
| "tty=,cmd="]).decode() |
| m = re.search(r"^(?P<term>[\w/]*) *bazel.*test.*$", res, re.MULTILINE) |
| if not m: |
| raise RuntimeError("Could not find the terminal") |
| term = m["term"] |
| return f"/dev/{term}" |
| |
| |
| # Set the terminal manually since this test is interactive. |
| # This is really a hack since bazel does not support interactive commands. |
| stdin = open(_get_tty(), "r") |
| stdout = open(_get_tty(), "w") |
| console = Console(force_interactive=True, force_terminal=True, file=stdout) |
| |
| |
| def _run_manual_testpoint(testplan_name: str, |
| testpoint: Dict[str, Any]) -> Tuple[str, bool]: |
| """Runs a testpoint in a manual testplan.""" |
| name = f"{testplan_name}/{testpoint['name']}" |
| md = f"**{name}**: {testpoint['desc']}" |
| console.print(Markdown(md)) |
| console.print("") |
| prompt = Text.styled("\n> Was the test successful? [y/n]", style="bold") |
| return (name, |
| Confirm.ask(prompt, |
| show_choices=False, |
| console=console, |
| stream=stdin)) |
| |
| |
| def _print_results(testplan_name: str, results: List[Tuple[str, |
| bool]]) -> None: |
| """Prints the results for a testplan.""" |
| title = Text.assemble( |
| ("Results of testplan ", "bold"), |
| (f"{testplan_name}", "bold reverse"), |
| (":", "bold"), |
| ) |
| console.print("") |
| console.print(title) |
| console.print("") |
| |
| for testpoint, passed in results: |
| line = Text.assemble(f" {testpoint}: ", |
| ("PASSED", "bold green") if passed else |
| ("FAILED", "bold red")) |
| console.print(line) |
| console.print("") |
| |
| num_tests = len(results) |
| num_pass = len([res[1] for res in results if res[1]]) |
| num_fail = num_tests - num_pass |
| summary = Text.assemble(f"{num_pass} of {num_tests} tests passed. ", |
| ("All tests PASSED", |
| "bold green") if not num_fail else |
| (f"{pluralize('test', num_fail, True)} FAILED", |
| "bold white on red")) |
| console.print(summary) |
| raise typer.Exit(code=num_fail) |
| |
| |
| def main(testplan_path: Path): |
| """Run manual tests listed in a testplan hjson file.""" |
| testplan = hjson.load(testplan_path.open()) |
| results = [ |
| _run_manual_testpoint(testplan['name'], t) |
| for t in testplan['testpoints'] |
| ] |
| _print_results(testplan["name"], results) |
| |
| |
| if __name__ == "__main__": |
| typer.run(main) |