blob: 936223f07f370685890d914dfa1d61887ad7f7c4 [file] [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 os
import shutil
import tempfile
import unittest
import unittest.mock as mock
from parameterized import parameterized
import args
import environment
class TestExecutionEnvironment(unittest.TestCase):
def _make_test_files(
self,
tmp: str,
real_out_dir: str | None = None,
out_dir_in_file: str | None = None,
) -> None:
out_dir = (
os.path.join(tmp, "out", "foo")
if not real_out_dir
else real_out_dir
)
os.makedirs(out_dir)
with open(os.path.join(tmp, ".fx-build-dir"), "w") as f:
f.write(out_dir if not out_dir_in_file else out_dir_in_file)
open(os.path.join(out_dir, "tests.json"), "a").close()
open(os.path.join(out_dir, "test-list.json"), "a").close()
open(os.path.join(out_dir, "package-repositories.json"), "a").close()
sdk_ctf_dir = os.path.join(tmp, "sdk", "ctf")
os.makedirs(sdk_ctf_dir)
open(os.path.join(sdk_ctf_dir, "disabled_tests.json"), "a").close()
def test_process_environment(self) -> None:
"""Test that we can load and use an environment."""
with tempfile.TemporaryDirectory() as tmp:
self._make_test_files(tmp)
out_dir = os.path.join(tmp, "out", "foo")
default_flags = args.parse_args([])
with mock.patch.dict(
os.environ,
{"FUCHSIA_DIR": tmp, "FUCHSIA_BUILD_DIR_FROM_FX": ""},
):
env = environment.ExecutionEnvironment.initialize_from_args(
default_flags
)
self.assertEqual(env.fuchsia_dir, tmp)
self.assertEqual(env.out_dir, out_dir)
self.assertTrue(
env.log_file and env.log_file.startswith(out_dir), str(env)
)
self.assertTrue(
env.log_file and "fxtest" in env.log_file, str(env)
)
self.assertEqual(
env.test_json_file, os.path.join(out_dir, "tests.json")
)
self.assertIsNone(env.test_list_file)
self.assertEqual(
env.relative_to_root(os.path.join(tmp, "foo", "bar")),
os.path.join("foo", "bar"),
)
def test_process_environment_with_fx_set_build_dir(self) -> None:
"""Test that we can load and use an environment with a build directory set by fx"""
with tempfile.TemporaryDirectory() as tmp:
out_dir = os.path.join(tmp, "out", "baz")
self._make_test_files(
tmp,
real_out_dir=out_dir,
out_dir_in_file=os.path.join(tmp, "out", "foo"),
)
default_flags = args.parse_args([])
for fx_build_dir in (out_dir, os.path.join("out", "baz")):
with self.subTest(fx_build_dir=fx_build_dir):
with mock.patch.dict(
os.environ,
{
"FUCHSIA_DIR": tmp,
"FUCHSIA_BUILD_DIR_FROM_FX": fx_build_dir,
},
):
env = environment.ExecutionEnvironment.initialize_from_args(
default_flags
)
self.assertEqual(env.fuchsia_dir, tmp)
self.assertEqual(env.out_dir, out_dir)
self.assertTrue(os.path.isabs(env.out_dir))
self.assertTrue(
env.log_file and env.log_file.startswith(out_dir),
str(env),
)
self.assertTrue(
env.log_file and "fxtest" in env.log_file, str(env)
)
self.assertEqual(
env.test_json_file,
os.path.join(out_dir, "tests.json"),
)
self.assertIsNone(env.test_list_file)
self.assertEqual(
env.relative_to_root(
os.path.join(tmp, "foo", "bar")
),
os.path.join("foo", "bar"),
)
def test_no_fuchsia_dir(self) -> None:
with mock.patch.dict(os.environ, {"FUCHSIA_DIR": ""}, clear=True):
default_flags = args.parse_args([])
self.assertRaisesRegex(
environment.EnvironmentError,
r"FUCHSIA_DIR",
lambda: environment.ExecutionEnvironment.initialize_from_args(
default_flags
),
)
def test_missing_build_dir_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self._make_test_files(tmp)
os.remove(os.path.join(tmp, ".fx-build-dir"))
with mock.patch.dict(os.environ, {"FUCHSIA_DIR": tmp}, clear=True):
default_flags = args.parse_args([])
self.assertRaisesRegex(
environment.EnvironmentError,
r".fx-build-dir",
lambda: environment.ExecutionEnvironment.initialize_from_args(
default_flags
),
)
def test_missing_build_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self._make_test_files(tmp)
shutil.rmtree(os.path.join(tmp, "out", "foo"))
with mock.patch.dict(os.environ, {"FUCHSIA_DIR": tmp}, clear=True):
default_flags = args.parse_args([])
self.assertRaisesRegex(
environment.EnvironmentError,
r"Expected directory at.*Ensure you have set up your build directory correctly using 'fx set'.",
lambda: environment.ExecutionEnvironment.initialize_from_args(
default_flags
),
)
def test_missing_tests_file(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self._make_test_files(tmp)
os.remove(os.path.join(tmp, "out", "foo", "tests.json"))
with mock.patch.dict(os.environ, {"FUCHSIA_DIR": tmp}, clear=True):
default_flags = args.parse_args([])
self.assertRaisesRegex(
environment.EnvironmentError,
r"tests.json",
lambda: environment.ExecutionEnvironment.initialize_from_args(
default_flags
),
)
@parameterized.expand(
[
(
"unset_ffx",
{},
["ffx", "target", "list"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"ffx",
"target",
"list",
],
),
(
"unset_serve",
{},
["serve"],
["fx", "--dir", "/fuchsia/out/default", "serve"],
),
(
"unset_build",
{},
["build", "test"],
["fx", "--dir", "/fuchsia/out/default", "build", "test"],
),
("unset_empty", {}, [], ["fx", "--dir", "/fuchsia/out/default"]),
(
"set_ffx",
{"FUCHSIA_NODENAME": "dev"},
["ffx", "target", "list"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"-t",
"dev",
"ffx",
"target",
"list",
],
),
(
"set_serve",
{"FUCHSIA_NODENAME": "dev"},
["serve", "-v"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"-t",
"dev",
"serve",
"-v",
],
),
(
"set_build",
{"FUCHSIA_NODENAME": "dev"},
["build", "test"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"-t",
"dev",
"build",
"test",
],
),
(
"has_short_flag",
{"FUCHSIA_NODENAME": "dev"},
["ffx", "-t", "other", "target", "list"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"ffx",
"-t",
"other",
"target",
"list",
],
),
(
"has_long_flag",
{"FUCHSIA_NODENAME": "dev"},
["ffx", "--target", "other", "target", "list"],
[
"fx",
"--dir",
"/fuchsia/out/default",
"ffx",
"--target",
"other",
"target",
"list",
],
),
]
)
def test_fx_cmd_line(
self,
_name: str,
env_dict: dict[str, str],
input_args: list[str],
expected: list[str],
) -> None:
env = environment.ExecutionEnvironment(
fuchsia_dir="/fuchsia",
out_dir="/fuchsia/out/default",
test_json_file="/fuchsia/out/default/tests.json",
disabled_ctf_tests_file="/fuchsia/sdk/ctf/disabled_tests.json",
)
with mock.patch.dict(os.environ, env_dict, clear=True):
self.assertEqual(env.fx_cmd_line(*input_args), expected)
def test_get_most_recent_log_stdout_option(self) -> None:
"""When log_file is '-' (stdout option), get_most_recent_log should search out_dir or raise EnvironmentError."""
with tempfile.TemporaryDirectory() as tmp:
env = environment.ExecutionEnvironment(
fuchsia_dir=tmp,
out_dir=tmp,
test_json_file=os.path.join(tmp, "tests.json"),
disabled_ctf_tests_file="",
log_file=args.LOG_TO_STDOUT_OPTION,
)
self.assertRaises(
environment.EnvironmentError,
lambda: env.get_most_recent_log(),
)
log_path = os.path.join(tmp, "fxtest-2026-01-01T00:00:00.json.gz")
with open(log_path, "w") as f:
f.write("test")
self.assertEqual(env.get_most_recent_log(), log_path)
def test_fx_cmd_line_direct_ffx_and_host_tools(self) -> None:
"""When host-tools binaries exist in out_dir, fx_cmd_line invokes them directly."""
with tempfile.TemporaryDirectory() as tmp:
out_dir = os.path.join(tmp, "out", "default")
host_tools_dir = os.path.join(out_dir, "host-tools")
os.makedirs(host_tools_dir)
ffx_bin = os.path.join(host_tools_dir, "ffx")
ffx_test_bin = os.path.join(host_tools_dir, "ffx-test")
dldist_bin = os.path.join(host_tools_dir, "dldist")
test_list_tool_bin = os.path.join(host_tools_dir, "test_list_tool")
for p in (ffx_bin, ffx_test_bin, dldist_bin, test_list_tool_bin):
with open(p, "w") as f:
f.write("#!/bin/sh\n")
os.chmod(p, 0o755)
# Write ffx_tools.json listing ffx-test (present) and ffx-repository (missing)
with open(os.path.join(out_dir, "ffx_tools.json"), "w") as f:
f.write('[{"name": "ffx-test"}, {"name": "ffx-repository"}]')
env = environment.ExecutionEnvironment(
fuchsia_dir=tmp,
out_dir=out_dir,
test_json_file=os.path.join(out_dir, "tests.json"),
disabled_ctf_tests_file=os.path.join(
tmp, "sdk/ctf/disabled_tests.json"
),
)
with mock.patch.dict(os.environ, {}, clear=True):
# Built-in subcommand (target) uses direct ffx
self.assertEqual(
env.fx_cmd_line(
"ffx", "--machine", "json", "target", "list"
),
[ffx_bin, "--machine", "json", "target", "list"],
)
# External subtool present on disk (ffx-test) uses direct ffx
self.assertEqual(
env.fx_cmd_line("ffx", "test", "run", "fuchsia-pkg://foo"),
[ffx_bin, "test", "run", "fuchsia-pkg://foo"],
)
# External subtool missing from disk but listed in ffx_tools.json falls back to fx
self.assertEqual(
env.fx_cmd_line("ffx", "repository", "publish"),
["fx", "--dir", out_dir, "ffx", "repository", "publish"],
)
# Direct host tools (dldist, test_list_tool)
self.assertEqual(
env.fx_cmd_line("dldist", "--needle", "foo"),
[dldist_bin, "--needle", "foo"],
)
self.assertEqual(
env.fx_cmd_line("test_list_tool", "--build-dir", out_dir),
[test_list_tool_bin, "--build-dir", out_dir],
)
# With FUCHSIA_NODENAME set, -t <target> is passed directly to ffx
with mock.patch.dict(
os.environ, {"FUCHSIA_NODENAME": "my-target"}, clear=True
):
self.assertEqual(
env.fx_cmd_line("ffx", "test", "run", "fuchsia-pkg://foo"),
[
ffx_bin,
"-t",
"my-target",
"test",
"run",
"fuchsia-pkg://foo",
],
)
# Explicit -t in args is not duplicated
self.assertEqual(
env.fx_cmd_line("ffx", "-t", "other", "target", "echo"),
[ffx_bin, "-t", "other", "target", "echo"],
)
# Direct host tools do not get -t injected
self.assertEqual(
env.fx_cmd_line("dldist", "--needle", "foo"),
[dldist_bin, "--needle", "foo"],
)
# Even if out_dir is relative, resolved tool paths are absolute so
# subprocesses invoked with a different CWD can still execute them.
rel_env = environment.ExecutionEnvironment(
fuchsia_dir=tmp,
out_dir=os.path.relpath(out_dir),
test_json_file=os.path.join(out_dir, "tests.json"),
disabled_ctf_tests_file=os.path.join(
tmp, "sdk/ctf/disabled_tests.json"
),
)
with mock.patch.dict(os.environ, {}, clear=True):
self.assertEqual(
rel_env.fx_cmd_line(
"ffx", "test", "run", "fuchsia-pkg://foo"
),
[ffx_bin, "test", "run", "fuchsia-pkg://foo"],
)
self.assertEqual(
rel_env.fx_cmd_line("dldist", "--needle", "foo"),
[dldist_bin, "--needle", "foo"],
)