| # 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 | |
| from datetime import datetime | |
| import enum | |
| import os | |
| import pathlib | |
| import re | |
| import sys | |
| import typing | |
| import agents.agents as agents_lib | |
| import termout | |
| import selection_action | |
| LOG_TO_STDOUT_OPTION = "-" | |
| class PrevOption(enum.StrEnum): | |
| LOG = "log" | |
| PATH = "path" | |
| REPLAY = "replay" | |
| ARTIFACT_PATH = "artifact-path" | |
| FAILED_TESTS = "failed-tests" | |
| HELP = "help" | |
| STATS = "stats" | |
| STATS_ANALYTICS = "stats-analytics" | |
| def help(self) -> str: | |
| """Get the help string for this option. | |
| Raises: | |
| RuntimeError: If an invalid option is passed. | |
| Returns: | |
| str: Human-readable information about how this options is used. | |
| """ | |
| if self is PrevOption.LOG: | |
| return "Print all test logs from the previous run. Logs are grouped by test suite." | |
| elif self is PrevOption.PATH: | |
| return "Print the path of the log from the previous run." | |
| elif self is PrevOption.REPLAY: | |
| return "Replay the previous run, using new display options." | |
| elif self is PrevOption.ARTIFACT_PATH: | |
| return "Print the path where artifacts were stored in the previous run." | |
| elif self is PrevOption.FAILED_TESTS: | |
| return "Print the previously failed tests and how to run them." | |
| elif self is PrevOption.HELP: | |
| return "Print this help output." | |
| elif self is PrevOption.STATS: | |
| return "Print statistics from the previous run" | |
| elif self is PrevOption.STATS_ANALYTICS: | |
| return "Print statistics from the previous run formatted for analytics (minified JSON)." | |
| else: | |
| raise RuntimeError("BUG: Invalid prev option") | |
| @dataclass | |
| class Flags: | |
| """Command line flags for fx test. | |
| See `parse_args` for documentation. | |
| """ | |
| dry: bool | |
| show_affected_tests: bool | |
| run_affected_tests: bool | |
| affected_since: str | None | |
| list: bool | |
| list_runtime_deps: bool | |
| previous: PrevOption | None | |
| remote_suggestions: bool | |
| remote_suggestion_builder: typing.List[str] | |
| gemini_analysis: bool | |
| gemini_model: str | |
| build: bool | |
| updateifinbase: bool | |
| build_updates: bool | |
| host: bool | |
| device: bool | |
| exact: bool | |
| e2e: bool | |
| only_e2e: bool | |
| selection: typing.List[str] | |
| fuzzy: int | |
| allow_empty_selection: bool | |
| show_suggestions: bool | |
| suggestion_count: int | |
| parallel: int | |
| parallel_cases: int | |
| random: bool | |
| count: int | |
| limit: int | None | |
| offset: int | |
| min_severity_logs: typing.List[str] | |
| timeout: float | None | |
| timeout_grace_period: float | |
| test_filter: typing.List[str] | |
| fail: bool | |
| fail_by_group: bool | |
| use_package_hash: bool | |
| restrict_logs: bool | |
| also_run_disabled_tests: bool | |
| show_full_moniker_in_logs: bool | |
| capture_syslog: bool | |
| break_on_failure: bool | |
| breakpoints: typing.List[str] | |
| use_existing_debugger: bool | |
| enable_debug_adapter: bool | |
| debug_adapter_port: int | None | |
| agent_debugging_mode: bool | |
| use_test_pilot: bool | |
| extra_args: typing.List[str] | |
| env: typing.List[str] | |
| allow_temporary_package_server: bool | |
| allow_temporary_emulator: bool | |
| preflight: bool | |
| ffx_usb_socket_path: str | None | |
| output: bool | |
| simple: bool | |
| style: bool | |
| log: bool | |
| logpath: str | None | |
| json: bool | |
| status: bool | None | |
| verbose: bool | |
| status_lines: int | |
| status_delay: float | |
| timestamp_artifacts: bool | |
| artifact_output_directory: str | None | |
| slow: float | |
| quiet: bool | |
| replay_speed: float | |
| summary_json: str | None | |
| summary_to_stdout: bool | |
| agent_output: bool | |
| save_log_path_to_file: str | None = None | |
| def validate(self) -> None: | |
| """Validate incoming flags, raising an exception on failure. | |
| Raises: | |
| FlagError: If the flags are invalid. | |
| """ | |
| if self.agent_debugging_mode: | |
| if self.break_on_failure is False: | |
| raise FlagError( | |
| "--no-break-on-failure is incompatible with --agent-debugging-mode" | |
| ) | |
| if self.enable_debug_adapter is False: | |
| raise FlagError( | |
| "--no-enable-debug-adapter is incompatible with --agent-debugging-mode" | |
| ) | |
| self.break_on_failure = True | |
| self.enable_debug_adapter = True | |
| if self.break_on_failure is None: | |
| self.break_on_failure = False | |
| if self.enable_debug_adapter is None: | |
| self.enable_debug_adapter = False | |
| 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.agent_output and self.status: | |
| raise FlagError("--agent-output is incompatible with --status") | |
| if self.agent_output and self.style: | |
| raise FlagError("--agent-output 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 non-negative") | |
| if self.timeout_grace_period and self.timeout_grace_period <= 0: | |
| raise FlagError("--timeout-grace-period 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 ( | |
| self.artifact_output_directory is not None | |
| and pathlib.Path(self.artifact_output_directory).is_file() | |
| ): | |
| raise FlagError("--artifact-output-directory cannot be a file") | |
| if self.parallel <= 0: | |
| raise FlagError("--parallel must be a positive number") | |
| if self.parallel_cases < 0: | |
| raise FlagError("--parallel-cases must be non-negative") | |
| if self.debugger_will_attach() and self.host: | |
| raise FlagError( | |
| "--break-on-failure and --breakpoint flags are not supported with host tests." | |
| ) | |
| if self.use_existing_debugger and self.breakpoints: | |
| raise FlagError( | |
| "--breakpoint does not support --use-existing-debugger." | |
| ) | |
| if self.use_existing_debugger and self.enable_debug_adapter: | |
| raise FlagError( | |
| "--enable-debug-adapter and --use-existing-debugger are mutually exclusive." | |
| ) | |
| if self.use_existing_debugger and not self.break_on_failure: | |
| raise FlagError( | |
| "--break-on-failure must be set when passing --use-existing-debugger." | |
| ) | |
| if self.enable_debug_adapter and not self.debugger_will_attach(): | |
| raise FlagError( | |
| "--enable-debug-adapter must be used with either --breakpoint or --break-on-failure." | |
| ) | |
| if self.debug_adapter_port and not self.enable_debug_adapter: | |
| raise FlagError( | |
| "--enable-debug-adapter must be set when passing --debug-adapter-port." | |
| ) | |
| if self.replay_speed <= 0: | |
| raise FlagError("--replay-speed must be a positive number") | |
| if not termout.is_valid() and self.status: | |
| raise FlagError( | |
| "Refusing to output interactive status to a non-TTY." | |
| ) | |
| if ( | |
| self.artifact_output_directory is not None | |
| and self.timestamp_artifacts | |
| ): | |
| self.artifact_output_directory = os.path.join( | |
| self.artifact_output_directory, | |
| datetime.now().strftime("%Y-%m-%d-%H:%M:%S"), | |
| ) | |
| if self.remote_suggestion_builder and not self.remote_suggestions: | |
| raise FlagError( | |
| "--remote-suggestion-builder is only effective when --remote-suggestions is enabled" | |
| ) | |
| # Compute environment and check it is valid. | |
| self.computed_env() | |
| if self.json: | |
| self.logpath = LOG_TO_STDOUT_OPTION | |
| if self.only_e2e: | |
| self.e2e = True | |
| if self.simple or self.agent_output: | |
| 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() | |
| if self.agent_output: | |
| self.quiet = True | |
| def update_artifacts_directory_with_out_path(self, path: str) -> None: | |
| if ( | |
| self.artifact_output_directory is not None | |
| and "FUCHSIA_OUT" in self.artifact_output_directory | |
| ): | |
| self.artifact_output_directory = re.sub( | |
| r"(\$FUCHSIA_OUT|\$\{FUCHSIA_OUT\})", | |
| path, | |
| self.artifact_output_directory, | |
| ) | |
| def debugger_will_attach(self) -> bool: | |
| """Determine if this set of flags enables debugging features. | |
| This is distinct from `Flags.debugger_should_spawn`, which indicates that debugger should be | |
| spawned. | |
| Returns: | |
| bool: True if a debugger will be attached, False otherwise. | |
| """ | |
| return bool(self.break_on_failure or self.breakpoints) | |
| def debugger_should_spawn(self) -> bool: | |
| """Determine if this set of flags warrants the launch of a debugger. | |
| This is distinct from `Flags.debugger_will_attach`, which indicates that a debugger will be | |
| attached to the test upon execution. | |
| Returns: | |
| bool: True if a debugger needs to be spawned, False otherwise. | |
| """ | |
| return bool( | |
| self.debugger_will_attach() and not self.use_existing_debugger | |
| ) | |
| def is_replay(self) -> bool: | |
| """Determine if these flags specify that replay mode is active. | |
| Returns: | |
| bool: True if replay mode is active, False otherwise. | |
| """ | |
| return self.previous == PrevOption.REPLAY | |
| def computed_env(self) -> dict[str, str]: | |
| """Compute and return the environment denoted by --env flags. | |
| Warning: This method recomputes the environment on each call, if this | |
| operation is too expensive we will need to memoize the return value. | |
| Raises: | |
| FlagError: If an environment variable is not formatted correctly. | |
| Returns: | |
| dict[str, str]: Mapping from key to value of environment | |
| variables for tests executed by this invocation of the | |
| command. | |
| """ | |
| ret = {} | |
| for val in self.env: | |
| split = val.split("=", maxsplit=1) | |
| if len(split) != 2: | |
| raise FlagError( | |
| f'Environment variable "{val}" must be formatted as "name=value"' | |
| ) | |
| ret[split[0]] = split[1] | |
| return ret | |
| class FlagError(Exception): | |
| """Raised if there was a problem parsing command line flags.""" | |
| def parse_args( | |
| cli_args: typing.List[str] | None = None, defaults: Flags | None = None | |
| ) -> Flags: | |
| """Parse command line flags. | |
| Args: | |
| cli_args (List[str], optional): Arguments to parse. If | |
| unset, read arguments from actual command line. | |
| defaults (Flags, optional): Default set of flags. If set, | |
| overrides the defaults from the command line. | |
| Returns: | |
| Flags: Typed representation of the command line for this program. | |
| """ | |
| extra_args: typing.List[str] = [] | |
| cli_args = cli_args if cli_args is not None else sys.argv[1:] | |
| if cli_args is not None and "--" in cli_args: | |
| extra_index = cli_args.index("--") | |
| (cli_args, extra_args) = ( | |
| cli_args[:extra_index], | |
| cli_args[extra_index + 1 :], | |
| ) | |
| show_advanced_help = "--help-advanced" in cli_args | |
| def adv_help(help_text: str) -> str: | |
| """Hides an argument's help usage behind `--help-advanced`. | |
| Showing too many uncommon/irrelevant flags in `fx test --help` causes agents not to process | |
| arguments effectively -- their harnesses will truncate excessive command output. | |
| """ | |
| return help_text if show_advanced_help else argparse.SUPPRESS | |
| parser = argparse.ArgumentParser( | |
| "fx test", | |
| usage="fx test [options] [selection ...] [-- test_args ...]", | |
| description="Test Executor for Humans", | |
| epilog="Run 'fx test --help-advanced' to show all advanced and internal options.", | |
| exit_on_error=False, | |
| ) | |
| parser.add_argument( | |
| "--help-advanced", | |
| action="help", | |
| help="Show full help message including advanced and internal options.", | |
| ) | |
| utility = parser.add_argument_group("Utility Options") | |
| utility.add_argument( | |
| "--show-affected-tests", | |
| action="store_true", | |
| help="Do not actually run tests. Instead print affected tests spanning from the current dirty repository state.", | |
| ) | |
| utility.add_argument( | |
| "--run-affected-tests", | |
| action="store_true", | |
| help="Automatically add and run tests affected by the current dirty repository state.", | |
| ) | |
| utility.add_argument( | |
| "--affected-since", | |
| type=str, | |
| default=None, | |
| help=adv_help( | |
| "Get affected files by comparing against this commit/branch instead of checking uncommitted files in the working area." | |
| ), | |
| ) | |
| 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.", | |
| ) | |
| utility.add_argument( | |
| "--gemini-analysis", | |
| nargs="?", | |
| type=int, | |
| const=1, | |
| default=None, | |
| choices=range(1, 4), | |
| help=adv_help( | |
| """If specified, requests an AI-powered analysis of stack traces from test failures. | |
| Requires the GEMINI_API_KEY environment variable to be set. | |
| The Gemini model can be specified using the `--gemini-model` flag. | |
| An optional verbosity level from 1-3 can be provided (eg., --gemini-analysis=3). | |
| If no level is provided, it defaults to 1. | |
| Level 1: Key lines from the stack trace. | |
| Level 2: Key lines and a potential error from the git diff. | |
| Level 3: Full analysis with file contents and code snippets. | |
| """ | |
| ), | |
| dest="gemini_analysis", | |
| ) | |
| utility.add_argument( | |
| "--gemini-model", | |
| type=str, | |
| default="gemini-2.5-flash-lite-preview-09-2025", | |
| help=adv_help("The Gemini model to use for the analysis."), | |
| dest="gemini_model", | |
| ) | |
| utility.add_argument( | |
| "--list-runtime-deps", | |
| action="store_true", | |
| help=adv_help( | |
| "Do not actually run tests. Instead print out the contents of the `runtime_deps` for each test. This can be useful for debugging whether the correct artifacts are being uploaded to test runners" | |
| ), | |
| ) | |
| utility.add_argument( | |
| "--remote-suggestions", | |
| action=argparse.BooleanOptionalAction, | |
| default=False, | |
| help=adv_help( | |
| "Whether to use remote tests.json files for tests suggestions." | |
| ), | |
| ) | |
| utility.add_argument( | |
| "--remote-suggestion-builder", | |
| type=str, | |
| action="append", | |
| help=adv_help( | |
| "Add additional builder to query. May be specified multiple times." | |
| ), | |
| default=[], | |
| ) | |
| utility.add_argument( | |
| "-pr", | |
| "--prev", | |
| "--previous", | |
| dest="previous", | |
| type=PrevOption, | |
| choices=list(PrevOption), | |
| metavar="OPTION", | |
| help="Print information from the previous run (choices: log, path, replay, artifact-path, failed-tests, stats, help).", | |
| ) | |
| 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=adv_help( | |
| "Invoke `fx update-if-in-base` before running device tests (defaults to on)" | |
| ), | |
| default=True, | |
| ) | |
| build.add_argument( | |
| "--build-updates", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| "Build the updates package if there are 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( | |
| "--exact", | |
| action="store_true", | |
| default=False, | |
| help="Only match tests whose name exactly matches the selection (incompatible with --host/--device).", | |
| ) | |
| selection.add_argument( | |
| "--e2e", | |
| action=argparse.BooleanOptionalAction, | |
| help="Run selected end to end tests. Default is to not run e2e tests.", | |
| default=False, | |
| ) | |
| selection.add_argument( | |
| "--only-e2e", | |
| action="store_true", | |
| default=False, | |
| help=adv_help("Only run end to end tests. Implies --e2e."), | |
| ) | |
| selection.add_argument( | |
| "-p", | |
| "--package", | |
| action=selection_action.InvalidAction, | |
| nargs=0, | |
| dest="selection", | |
| help="Match tests against their Fuchsia package name", | |
| ) | |
| selection.add_argument( | |
| "-c", | |
| "--component", | |
| action=selection_action.InvalidAction, | |
| nargs=0, | |
| dest="selection", | |
| help="Match tests against their Fuchsia component name", | |
| ) | |
| selection.add_argument( | |
| "-a", | |
| "--and", | |
| action=selection_action.InvalidAction, | |
| nargs=0, | |
| dest="selection", | |
| help="Add requirements to the preceding filter", | |
| ) | |
| selection.add_argument( | |
| "selection", | |
| action=selection_action.SelectionAction, | |
| nargs="*", | |
| help="Test targets or patterns to select.", | |
| ) | |
| selection.add_argument( | |
| "--fuzzy", | |
| type=int, | |
| default=3, | |
| help=adv_help( | |
| "The Damerau-Levenshtein distance threshold for fuzzy matching tests" | |
| ), | |
| ) | |
| selection.add_argument( | |
| "--allow-empty-selection", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| "If True and multiple selections are provided, allow individual selections that match no tests as long as at least one test is selected. Default is True." | |
| ), | |
| default=True, | |
| ) | |
| selection.add_argument( | |
| "--show-suggestions", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_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=adv_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, | |
| help=adv_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( | |
| "--parallel-cases", | |
| type=int, | |
| help=adv_help( | |
| "Instruct on-device test runners to prefer running this number of cases in parallel." | |
| ), | |
| default=0, | |
| ) | |
| execution.add_argument( | |
| "-r", | |
| "--random", | |
| action="store_true", | |
| help=adv_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 (0 disables timeout).", | |
| ) | |
| execution.add_argument( | |
| "--timeout-grace-period", | |
| type=float, | |
| help=adv_help( | |
| "Number of seconds following timeout after which the test process will be killed. Default 15 seconds." | |
| ), | |
| default=15.0, | |
| ) | |
| execution.add_argument( | |
| "--test-filter", | |
| type=str, | |
| action="append", | |
| default=[], | |
| help="Run specific test cases in a test suite. Can be specified multiple times.", | |
| ) | |
| execution.add_argument( | |
| "--count", | |
| type=int, | |
| help="Execute each test this many times. Stops repeating if an iteration times out.", | |
| default=1, | |
| ) | |
| execution.add_argument( | |
| "--limit", | |
| type=int, | |
| help="Stop execution after this many tests", | |
| default=None, | |
| ) | |
| execution.add_argument( | |
| "--offset", | |
| type=int, | |
| help=adv_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.", | |
| default=False, | |
| ) | |
| execution.add_argument( | |
| "--fail-by-group", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| "When repeating tests with --count, stop repeating if any execution fails. If --no-fail-by-group is sets, continue repeating test executions despite failures to identify flakes." | |
| ), | |
| default=True, | |
| ) | |
| 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="*", | |
| metavar="LEVEL", | |
| help="Minimum log severity (<selector>#<level> or <level>: 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 (components only).", | |
| default=False, | |
| ) | |
| execution.add_argument( | |
| "--show-full-moniker-in-logs", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| """If set, show the full moniker in log output for on-device tests. | |
| Otherwise only the last segment of the moniker is displayed. | |
| Default is False.""" | |
| ), | |
| default=False, | |
| ) | |
| execution.add_argument( | |
| "-e", | |
| "--env", | |
| action="append", | |
| type=str, | |
| help="Add an environment variable (NAME=VALUE) to each test invocation. May be specified multiple times.", | |
| default=[], | |
| ) | |
| execution.add_argument( | |
| "--break-on-failure", | |
| action=argparse.BooleanOptionalAction, | |
| help="Stop test execution and launch/attach zxdb on any test case failure.", | |
| default=None, | |
| ) | |
| execution.add_argument( | |
| "--breakpoint", | |
| metavar="BREAKPOINT", # This is to make the help text singular. | |
| dest="breakpoints", | |
| action="append", | |
| help="Run test with zxdb attached and set breakpoint (e.g. `--breakpoint file.cc:37`). May be repeated.", | |
| default=[], | |
| ) | |
| execution.add_argument( | |
| "--use-existing-debugger", | |
| action="store_true", | |
| help=adv_help( | |
| """If set, suppresses the automatic launch and attach of zxdb when `--break-on-failure` | |
| is set. Incompatible with `--breakpoint`.""" | |
| ), | |
| default=False, | |
| ) | |
| execution.add_argument( | |
| "--enable-debug-adapter", | |
| help=adv_help( | |
| """If set, spawns zxdb in Debug Adapter mode. This will not spawn a zxdb console | |
| session. Instead, zxdb will be acting as a Debug Adapter server. This option is incompatible | |
| with --use-existing-debugger.""" | |
| ), | |
| action=argparse.BooleanOptionalAction, | |
| default=None, | |
| ) | |
| execution.add_argument( | |
| "--debug-adapter-port", | |
| type=int, | |
| help=adv_help( | |
| """Specifies the PORT to use for the Debug Adapter Server. Must be used in conjunction | |
| with --enable-debug-adapter. An open port is randomly assigned if not specified.""" | |
| ), | |
| metavar="PORT", | |
| default=None, | |
| ) | |
| execution.add_argument( | |
| "--agent-debugging-mode", | |
| action="store_true", | |
| help=adv_help( | |
| """If set, automatically begins an fx debug cli session in the background, which should | |
| be used by Agents for test debugging workflows. Implies --break-on-failure and | |
| --enable-debug-adapter.""" | |
| ), | |
| default=False, | |
| ) | |
| execution.add_argument( | |
| "--allow-temporary-package-server", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| "Allow this script to start a temporary package server if one is not already running. Default is True." | |
| ), | |
| default=True, | |
| ) | |
| execution.add_argument( | |
| "--ffx-usb-socket-path", | |
| help=adv_help( | |
| """If set, explicitly sets the path to the socket used to communicate with the FFX USB | |
| driver. Otherwise the default is used from your ffx config.""" | |
| ), | |
| default=None, | |
| ) | |
| execution.add_argument( | |
| "--allow-temporary-emulator", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| "Allow this script to start a temporary emulator if no active device is detected. Default is True." | |
| ), | |
| default=True, | |
| ) | |
| execution.add_argument( | |
| "--preflight", | |
| action=argparse.BooleanOptionalAction, | |
| help="Perform preflight device reachability and emulator checks before running tests. Use --no-preflight to skip these checks for faster startup when the device is already running. Default is True.", | |
| default=True, | |
| ) | |
| execution.add_argument( | |
| "--use-test-pilot", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| """Run test components using test-pilot. Note: this flag is experimental""" | |
| ), | |
| default=False, | |
| ) | |
| output = parser.add_argument_group("Output Options") | |
| output.add_argument( | |
| "-o", | |
| "--output", | |
| action=argparse.BooleanOptionalAction, | |
| help="Display the output from passing tests.", | |
| ) | |
| output.add_argument( | |
| "--simple", | |
| action=argparse.BooleanOptionalAction, | |
| default=True if agents_lib.is_invoked_by_agent() else False, | |
| help=adv_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=adv_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=adv_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, customizes the destination of the target log.", | |
| default=None, | |
| ) | |
| output.add_argument( | |
| "--save-log-path-to-file", | |
| help=adv_help( | |
| "Write the path of the log file to this file. Used for scripting so a script will know where the log path is without needing to intercept output." | |
| ), | |
| default=None, | |
| ) | |
| output.add_argument( | |
| "--json", | |
| action="store_true", | |
| help=adv_help("Format output as JSON logs, alias for --logpath -"), | |
| default=False, | |
| ) | |
| 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: enabled on TTY).", | |
| ) | |
| output.add_argument( | |
| "--status-lines", | |
| default=8, | |
| type=int, | |
| help=adv_help("Number of lines used to display status output."), | |
| ) | |
| output.add_argument( | |
| "--status-delay", | |
| default=0.033, | |
| type=float, | |
| help=adv_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." | |
| ), | |
| ) | |
| output.add_argument( | |
| "--timestamp-artifacts", | |
| default=False, | |
| action=argparse.BooleanOptionalAction, | |
| help="Output artifacts in a timestamped subdirectory under --outdir.", | |
| ) | |
| output.add_argument( | |
| "--outdir", | |
| "--ffx-output-directory", # For compatibility | |
| "--artifact-output-directory", | |
| default=None, | |
| dest="artifact_output_directory", | |
| metavar="DIR", | |
| help="Write test artifact output to this directory.", | |
| ) | |
| output.add_argument( | |
| "-s", | |
| "--slow", | |
| type=float, | |
| default=0, | |
| help=adv_help( | |
| "If non-zero, automatically show output for tests taking longer than this many seconds." | |
| ), | |
| ) | |
| output.add_argument( | |
| "-q", | |
| "--quiet", | |
| action=argparse.BooleanOptionalAction, | |
| default=False, | |
| help="Silence INFO and INSTRUCTION messages from the tool", | |
| ) | |
| output.add_argument( | |
| "--replay-speed", | |
| type=float, | |
| default=1, | |
| help=adv_help( | |
| "Speed up replays by this amount. Can be less than 1 for slow motion." | |
| ), | |
| ) | |
| output.add_argument( | |
| "--capture-syslog", | |
| action=argparse.BooleanOptionalAction, | |
| help=adv_help( | |
| """If set, include captured syslog in output. Set by default.""" | |
| ), | |
| default=True, | |
| ) | |
| output.add_argument( | |
| "--summary-json", | |
| default=None, | |
| help=adv_help( | |
| "If set, write a structured JSON summary of the test run to this path." | |
| ), | |
| ) | |
| output.add_argument( | |
| "--summary-to-stdout", | |
| action=argparse.BooleanOptionalAction, | |
| default=False, | |
| help=adv_help( | |
| "If set, print the structured JSON summary to stdout on completion." | |
| ), | |
| ) | |
| output.add_argument( | |
| "--agent-output", | |
| action=argparse.BooleanOptionalAction, | |
| default=True if agents_lib.is_invoked_by_agent() else False, | |
| help=adv_help( | |
| "Format output for AI agents with isolated logs and JSON summary output." | |
| ), | |
| ) | |
| if defaults is not None: | |
| actions = parser._actions.copy() | |
| groups_to_process: typing.List[ | |
| argparse._ArgumentGroup | |
| ] = parser._action_groups.copy() | |
| # Recursively find all actions. | |
| while groups_to_process: | |
| group = groups_to_process.pop() | |
| actions.extend(group._actions) | |
| groups_to_process.extend(group._action_groups) | |
| # Apply defaults for all identified actions from the given defaults. | |
| for action in actions: | |
| if hasattr(defaults, action.dest): | |
| action.default = getattr(defaults, action.dest) | |
| cli_args = selection_action.SelectionAction.preprocess_args(cli_args) | |
| namespace = parser.parse_intermixed_args(cli_args) | |
| flags: Flags = Flags(**vars(namespace), extra_args=extra_args) | |
| return flags |