blob: e46f463c45b8f94fe6c4eadd5711491e78d68a35 [file] [log] [blame] [edit]
# 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.
import argparse
from dataclasses import dataclass
import typing
import termout
import util.arg_option as arg_option
@dataclass
class Flags:
"""Command line flags for fx test.
See `parse_args` for documentation.
"""
dry: bool
list: bool
build: bool
updateifinbase: bool
host: bool
device: bool
selection: typing.List[str]
fuzzy: int
show_suggestions: bool
suggestion_count: int
parallel: int
random: bool
count: int
limit: int | None
offset: int
min_severity_logs: typing.List[str]
timeout: float | None
test_filter: typing.List[str]
fail: bool
use_package_hash: bool
restrict_logs: bool
also_run_disabled_tests: bool
output: bool
simple: bool
style: bool
log: bool
logpath: str | None
status: bool | None
verbose: bool
status_lines: int
status_delay: float
def validate(self):
"""Validate incoming flags, raising an exception on failure.
Raises:
FlagError: If the flags are invalid.
"""
if self.simple and self.status:
raise FlagError("--simple is incompatible with --status")
if self.simple and self.style:
raise FlagError("--simple is incompatible with --style")
if self.device and self.host:
raise FlagError("--device is incompatible with --host")
if self.status_delay < 0.005:
raise FlagError("--status-delay must be at least 0.005 (5ms)")
if self.timeout and self.timeout <= 0:
raise FlagError("--timeout must be greater than 0")
if self.count < 1:
raise FlagError("--count must be a positive number")
if self.suggestion_count < 0:
raise FlagError("--suggestion-count must be non-negative")
if not termout.is_valid() and self.status:
raise FlagError(
"Refusing to output interactive status to a non-TTY."
)
if self.simple:
self.style = False
self.status = False
else:
if self.style is None:
self.style = termout.is_valid()
if self.status is None:
self.status = termout.is_valid()
class FlagError(Exception):
"""Raised if there was a problem parsing command line flags."""
def parse_args(cli_args: typing.List[str] | None = None) -> Flags:
"""Parse command line flags.
Returns:
Flags: Typed representation of the command line for this program.
"""
parser = argparse.ArgumentParser(
"fx test",
description="Test Executor for Humans",
)
utility = parser.add_argument_group("Utility Options")
utility.add_argument(
"--dry",
action="store_true",
help="Do not actually run tests. Instead print out the tests that would have been run.",
)
utility.add_argument(
"--list",
action="store_true",
help="Do not actually run tests. Instead print out the list of test cases each test contains.",
)
build = parser.add_argument_group("Build Options")
build.add_argument(
"--build",
action=argparse.BooleanOptionalAction,
help="Invoke `fx build` before running the test suite (defaults to on)",
default=True,
)
build.add_argument(
"--updateifinbase",
action=argparse.BooleanOptionalAction,
help="Invoke `fx update-if-in-base` before running device tests (defaults to on)",
default=True,
)
selection = parser.add_argument_group("Test Selection Options")
selection.add_argument(
"--host",
action="store_true",
default=False,
help="Only run host tests. The opposite of `--device`",
)
selection.add_argument(
"-d",
"--device",
action="store_true",
default=False,
help="Only run device tests. The opposite of `--host`",
)
selection.add_argument(
"-p",
"--package",
action=arg_option.SelectionAction,
dest="selection",
nargs="*",
help="Match tests against their Fuchsia package name",
)
selection.add_argument(
"-c",
"--component",
action=arg_option.SelectionAction,
dest="selection",
nargs="*",
help="Match tests against their Fuchsia component name",
)
selection.add_argument(
"-a",
"--and",
action=arg_option.SelectionAction,
dest="selection",
nargs="*",
help="Add requirements to the preceding filter",
)
selection.add_argument(
"selection", action=arg_option.SelectionAction, nargs="*"
)
selection.add_argument(
"--fuzzy",
type=int,
default=3,
help="The Damerau–Levenshtein distance threshold for fuzzy matching tests",
)
selection.add_argument(
"--show-suggestions",
action=argparse.BooleanOptionalAction,
type=bool,
help="If True and no tests match, suggest matching tests from the build directory. Default is True.",
default=True,
)
selection.add_argument(
"--suggestion-count",
type=int,
help="Show this number of suggestions if no tests match. Default is 6.",
default=6,
)
execution = parser.add_argument_group("Execution Options")
execution.add_argument(
"--use-package-hash",
action=argparse.BooleanOptionalAction,
type=bool,
help="Use the package Merkle root hash from the build artifacts to ensure you are running the most recently built device test code.",
default=True,
)
execution.add_argument(
"--parallel",
type=int,
help="Maximum number of test suites to run in parallel. Does not affect per-suite parallelism.",
default=4,
)
execution.add_argument(
"-r",
"--random",
action="store_true",
help="Randomize test execution order",
default=False,
)
execution.add_argument(
"--timeout",
type=float,
help="Terminate tests that take longer than this number of seconds to complete. Default is no timeout.",
)
execution.add_argument(
"--test-filter",
type=str,
default=[],
nargs="*",
help="Run specific test cases in a test suite. Can be specified multiple times to pass in multiple patterns.",
)
execution.add_argument(
"--count",
type=int,
help="Execute each test this many times. If any iteration of a test times out, no further iterations will be executed",
default=1,
)
execution.add_argument(
"--limit",
type=int,
help="Stop execution after this many tests",
default=None,
)
execution.add_argument(
"--offset",
type=int,
help="Skip this many tests at the beginning of the test list. Combine with --limit to deterministically select a subrange of tests.",
default=0,
)
execution.add_argument(
"-f",
"--fail",
action="store_true",
help="Stop running tests after the first failed test suite. This will abort all tests in progress and end with a failure code.",
default=False,
)
execution.add_argument(
"--restrict-logs",
action=argparse.BooleanOptionalAction,
help="If False, do not limit maximum log severity regardless of the test's configuration. Default is True.",
default=True,
)
execution.add_argument(
"--min-severity-logs",
nargs="*",
help="""Modifies the minimum log severity level emitted by components during the test execution.
Specify using the format <component-selector>#<log-level>, or just <log-level> (in which
case the severity will apply to all components under the test, including the test component
itself) with level as one of FATAL|ERROR|WARN|INFO|DEBUG|TRACE.""",
default=[],
)
execution.add_argument(
"--also-run-disabled-tests",
action="store_true",
help="If True, also run tests that are disabled by the test author. This only affects test components. Default is False.",
default=False,
)
output = parser.add_argument_group("Output Options")
output.add_argument(
"-o",
"--output",
action="store_true",
help="Display the output from passing tests. Some test arguments may be needed.",
)
output.add_argument(
"--simple",
action="store_true",
help="Remove any color or decoration from output. Disable pretty status printing. Implies --no-style",
)
output.add_argument(
"--style",
action=argparse.BooleanOptionalAction,
default=None,
help="Remove color and decoration from output. Does not disable pretty status printing. Default is to only style for TTY output.",
)
output.add_argument(
"--log",
action=argparse.BooleanOptionalAction,
help="Emit command events to a file. Turned on when running real tests unless `--no-log` is passed.",
default=True,
)
output.add_argument(
"--logpath",
help="If passed and --log is enabled, custimizes the destination of the target log.",
default=None,
)
output.add_argument(
"-v",
"--verbose",
action="store_true",
default=False,
help="Print verbose logs to the console",
)
output.add_argument(
"--status",
action=argparse.BooleanOptionalAction,
default=None,
help="Toggle interactive status printing to console. Default is to vary behavior depending on if output is to a TTY. Setting to True on a non-TTY is an error.",
)
output.add_argument(
"--status-lines",
default=8,
type=int,
help="Number of lines used to display status output.",
)
output.add_argument(
"--status-delay",
default=0.033,
type=float,
help="Control how frequently the status output is updated. Default is every 0.033s, but you can increase the number for calmer output on slower connections.",
)
flags: Flags = Flags(**vars(parser.parse_args(cli_args)))
return flags