| # Copyright 2026 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 contextlib | |
| import io | |
| import json | |
| import os | |
| import subprocess | |
| import tempfile | |
| import unittest | |
| from pathlib import Path | |
| from typing import Any | |
| from unittest import mock | |
| import locator | |
| class LocatorTests(unittest.TestCase): | |
| def _run_locator( | |
| self, args: list[str], fuchsia_dir: Path | |
| ) -> subprocess.CompletedProcess[str]: | |
| """Run locator.main in-process and capture stdout, stderr, and exit code.""" | |
| out_buf, err_buf = io.StringIO(), io.StringIO() | |
| full_args = ["--fuchsia-dir", str(fuchsia_dir)] + args | |
| env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")} | |
| with ( | |
| mock.patch.dict(os.environ, env, clear=True), | |
| contextlib.redirect_stdout(out_buf), | |
| contextlib.redirect_stderr(err_buf), | |
| ): | |
| try: | |
| rc = locator.main(full_args) | |
| except SystemExit as exc: | |
| rc = exc.code if isinstance(exc.code, int) else 1 | |
| return subprocess.CompletedProcess( | |
| full_args, rc, out_buf.getvalue(), err_buf.getvalue() | |
| ) | |
| def _write_exec( | |
| self, path: Path, body: str = "#!/bin/sh\necho ok\n" | |
| ) -> Path: | |
| """Create an executable script file at path.""" | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(body, encoding="utf-8") | |
| path.chmod(0o755) | |
| return path | |
| def _write_registry(self, path: Path, entries: dict[str, Any]) -> Path: | |
| """Write an fx_host_tools.json file at path.""" | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text( | |
| json.dumps(entries, indent=4, sort_keys=True) + "\n", | |
| encoding="utf-8", | |
| ) | |
| return path | |
| def test_compute_isolated_path(self) -> None: | |
| """Verify PATH isolation strips prebuilt/runtime dirs unless inject_prebuilt_paths=1.""" | |
| current_path = "/parent.runtime_dir/.bin:/parent.runtime_dir/bin:/prebuilt/python3/bin:/usr/bin:/bin" | |
| prebuilts = "/prebuilt/python3/bin:/prebuilt/clang/bin" | |
| # Default (inject=0): strip parent runtime_dir bins and all prebuilt dirs. | |
| isolated = locator.compute_isolated_path( | |
| inject_prebuilt_paths=0, | |
| current_path=current_path, | |
| prebuilt_all_paths=prebuilts, | |
| saved_prebuilt_all_paths=prebuilts, | |
| ) | |
| self.assertEqual(isolated, "/usr/bin:/bin") | |
| # Opt-in (inject=1): prepend prebuilt dirs ahead of ambient PATH segments. | |
| restored = locator.compute_isolated_path( | |
| inject_prebuilt_paths=1, | |
| current_path=isolated, | |
| prebuilt_all_paths=prebuilts, | |
| saved_prebuilt_all_paths=prebuilts, | |
| ) | |
| self.assertEqual( | |
| restored, "/prebuilt/python3/bin:/prebuilt/clang/bin:/usr/bin:/bin" | |
| ) | |
| def test_resolve_4_level_vendor_first_precedence(self) -> None: | |
| """Verify 5-level vendor-first command resolution precedence and multi-vendor collision handling.""" | |
| with tempfile.TemporaryDirectory() as td: | |
| fuchsia_dir = Path(td) | |
| build_dir = fuchsia_dir / "out" / "default" | |
| # Level 5: Unregistered binary in <build_dir>/host-tools resolves last. | |
| host_only = self._write_exec(build_dir / "host-tools" / "host-only") | |
| res5 = locator.resolve_command( | |
| fuchsia_dir, "host-only", build_dir=build_dir | |
| ) | |
| self.assertEqual(res5.kind, locator.CommandKind.LEGACY_FX_HOST_TOOL) | |
| self.assertEqual(res5.script, str(host_only)) | |
| # Level 4: Core legacy script in //tools/devshell. | |
| core_legacy = self._write_exec( | |
| fuchsia_dir / "tools" / "devshell" / "foo" | |
| ) | |
| res4 = locator.resolve_command(fuchsia_dir, "foo") | |
| self.assertEqual(res4.kind, locator.CommandKind.LEGACY_SCRIPT) | |
| self.assertEqual(res4.exec_path, str(core_legacy)) | |
| # Level 3: Core registered tool overrides core legacy script. | |
| self._write_registry( | |
| fuchsia_dir / "tools" / "devshell" / "fx_host_tools.json", | |
| { | |
| "foo": { | |
| "category": "Build", | |
| "summary": "core registered foo", | |
| "script": "//tools/devshell/foo_script.sh", | |
| } | |
| }, | |
| ) | |
| res3 = locator.resolve_command(fuchsia_dir, "foo") | |
| self.assertEqual(res3.kind, locator.CommandKind.REGISTERED_SCRIPT) | |
| self.assertEqual( | |
| res3.exec_path, | |
| str(fuchsia_dir / "tools/devshell/foo_script.sh"), | |
| ) | |
| # Level 2: Vendor legacy script overrides core registered tool. | |
| vendor_legacy = self._write_exec( | |
| fuchsia_dir | |
| / "vendor" | |
| / "google" | |
| / "scripts" | |
| / "devshell" | |
| / "foo" | |
| ) | |
| res2 = locator.resolve_command(fuchsia_dir, "foo") | |
| self.assertEqual(res2.kind, locator.CommandKind.LEGACY_SCRIPT) | |
| self.assertEqual(res2.exec_path, str(vendor_legacy)) | |
| # Level 1: Vendor registered tool overrides vendor legacy script. | |
| self._write_registry( | |
| fuchsia_dir | |
| / "vendor" | |
| / "google" | |
| / "scripts" | |
| / "devshell" | |
| / "fx_host_tools.json", | |
| { | |
| "foo": { | |
| "category": "Build", | |
| "summary": "vendor registered foo", | |
| "script": "//vendor/google/scripts/devshell/vendor_foo.sh", | |
| } | |
| }, | |
| ) | |
| res1 = locator.resolve_command(fuchsia_dir, "foo") | |
| self.assertEqual(res1.kind, locator.CommandKind.REGISTERED_SCRIPT) | |
| self.assertEqual( | |
| res1.exec_path, | |
| str( | |
| fuchsia_dir / "vendor/google/scripts/devshell/vendor_foo.sh" | |
| ), | |
| ) | |
| # Explicit --vendor restriction scopes lookup to that vendor only. | |
| res_v = locator.resolve_command( | |
| fuchsia_dir, "foo", vendor_name="google" | |
| ) | |
| self.assertEqual(res_v.kind, locator.CommandKind.REGISTERED_SCRIPT) | |
| res_other_v = locator.resolve_command( | |
| fuchsia_dir, "foo", vendor_name="other" | |
| ) | |
| self.assertEqual(res_other_v.kind, locator.CommandKind.NOT_FOUND) | |
| # Multi-vendor collision: >=2 vendor registries defining 'foo' exits with code 2. | |
| self._write_registry( | |
| fuchsia_dir | |
| / "vendor" | |
| / "acme" | |
| / "scripts" | |
| / "devshell" | |
| / "fx_host_tools.json", | |
| { | |
| "foo": { | |
| "category": "Build", | |
| "summary": "acme registered foo", | |
| "script": "//vendor/acme/scripts/devshell/acme_foo.sh", | |
| } | |
| }, | |
| ) | |
| proc_collision = self._run_locator(["resolve", "foo"], fuchsia_dir) | |
| self.assertEqual(proc_collision.returncode, 2) | |
| self.assertIn("multiple vendor repositories", proc_collision.stderr) | |
| self.assertIn("acme", proc_collision.stderr) | |
| self.assertIn("google", proc_collision.stderr) | |
| # Explicit vendor disambiguation resolves cleanly. | |
| proc_acme = self._run_locator( | |
| ["resolve", "foo", "--vendor", "acme"], fuchsia_dir | |
| ) | |
| self.assertEqual(proc_acme.returncode, 0, msg=proc_acme.stderr) | |
| self.assertIn("KIND=registered_script", proc_acme.stdout) | |
| self.assertIn("VENDOR=acme", proc_acme.stdout) | |
| def test_help_command(self) -> None: | |
| """Verify help-command executes --help when present on disk and falls back to metadata otherwise.""" | |
| with tempfile.TemporaryDirectory() as td: | |
| fuchsia_dir = Path(td) | |
| self._write_registry( | |
| fuchsia_dir / "tools" / "devshell" / "fx_host_tools.json", | |
| { | |
| "live-script": { | |
| "category": "Testing", | |
| "summary": "Script with on-disk executable", | |
| "script": "//tools/devshell/live_runner.sh", | |
| }, | |
| "my-cmd": { | |
| "category": "Testing", | |
| "summary": "Run custom test suite", | |
| "script": "//tools/devshell/test_runner.sh", | |
| }, | |
| }, | |
| ) | |
| self._write_exec( | |
| fuchsia_dir / "tools" / "devshell" / "live_runner.sh", | |
| "#!/bin/sh\necho 'usage: fx live-script [--flag]'\n", | |
| ) | |
| # Script missing on disk falls back to printing registry metadata. | |
| proc = self._run_locator(["help-command", "my-cmd"], fuchsia_dir) | |
| self.assertEqual(proc.returncode, 0, msg=proc.stderr) | |
| self.assertIn("Run custom test suite", proc.stdout) | |
| self.assertIn("Category: Testing", proc.stdout) | |
| self.assertIn( | |
| "Registry: script //tools/devshell/test_runner.sh", proc.stdout | |
| ) | |
| # Script present on disk runs `--help` directly. | |
| proc_live = self._run_locator( | |
| ["help-command", "live-script"], fuchsia_dir | |
| ) | |
| self.assertEqual(proc_live.returncode, 0, msg=proc_live.stderr) | |
| self.assertIn("usage: fx live-script [--flag]", proc_live.stdout) | |
| # Unregistered command returns exit code 1. | |
| proc_not_found = self._run_locator( | |
| ["help-command", "unknown-cmd"], fuchsia_dir | |
| ) | |
| self.assertEqual(proc_not_found.returncode, 1) | |
| def test_ensure_target_cl1_stub(self) -> None: | |
| """Verify ensure-target fails cleanly with an informative error before fx_host_tool support lands.""" | |
| with tempfile.TemporaryDirectory() as td: | |
| fuchsia_dir = Path(td) | |
| self._write_registry( | |
| fuchsia_dir / "tools" / "devshell" / "fx_host_tools.json", | |
| { | |
| "target-cmd": { | |
| "category": "Build", | |
| "summary": "target tool", | |
| "target": "//tools/devshell/host_tools:target-cmd", | |
| } | |
| }, | |
| ) | |
| proc = self._run_locator( | |
| ["ensure-target", "target-cmd"], fuchsia_dir | |
| ) | |
| self.assertNotEqual(proc.returncode, 0) | |
| self.assertIn("requires fx_host_tool support", proc.stderr) | |
| if __name__ == "__main__": | |
| unittest.main() |