blob: 55ede4e138d270f3a46bc131ae43faf124823a8b [file]
#!/usr/bin/env fuchsia-vendored-python
# 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.
"""Unit tests for tools/devshell/lib/shac_runner.py."""
import io
import os
import pathlib
import subprocess
import sys
import tempfile
import unittest
from unittest import mock
import scm
import shac_runner
class ShacRunnerTest(unittest.TestCase):
def setUp(self) -> None:
super().setUp()
temp_dir = self.enterContext(tempfile.TemporaryDirectory())
self.fuchsia_dir = pathlib.Path(temp_dir)
self.build_dir = self.fuchsia_dir / "out" / "default"
self.build_dir.mkdir(parents=True)
self.shac_binary = self.fuchsia_dir / "shac"
self.shac_binary.touch()
self.enterContext(
mock.patch.dict(
os.environ, {"PREBUILT_SHAC": str(self.shac_binary)}
)
)
def test_run_shac_validates_explicit_files(self) -> None:
existing = self.fuchsia_dir / "foo.cc"
existing.touch()
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("foo.cc",),
),
)
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
) as mock_get_groups,
mock.patch.object(
subprocess,
"run",
autospec=True,
return_value=subprocess.CompletedProcess(args=[], returncode=0),
),
mock.patch.object(
sys, "stderr", new_callable=io.StringIO
) as mock_stderr,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
explicit_files=("//foo.cc", "//missing.cc"),
)
mock_get_groups.assert_called_once_with(
self.fuchsia_dir,
self.fuchsia_dir,
explicit_files=("//foo.cc",),
)
self.assertIn("ERROR: no such file:", mock_stderr.getvalue())
def test_run_shac_returns_early_when_all_explicit_files_missing(
self,
) -> None:
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True
) as mock_get_groups,
mock.patch.object(
sys, "stderr", new_callable=io.StringIO
) as mock_stderr,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
explicit_files=("//missing.cc", " "),
)
mock_get_groups.assert_not_called()
self.assertIn("ERROR: no such file:", mock_stderr.getvalue())
def test_run_shac_raises_when_shac_binary_missing(self) -> None:
missing_shac = self.fuchsia_dir / "nonexistent_shac"
with (
mock.patch.dict(os.environ, {"PREBUILT_SHAC": str(missing_shac)}),
self.assertRaises(FileNotFoundError),
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
)
def test_run_shac_uses_prebuilt_shac_env(self) -> None:
custom_shac = self.fuchsia_dir / "custom_shac"
custom_shac.touch()
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("foo.cc",),
),
)
with (
mock.patch.dict(os.environ, {"PREBUILT_SHAC": str(custom_shac)}),
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
),
mock.patch.object(
subprocess,
"run",
autospec=True,
return_value=subprocess.CompletedProcess(args=[], returncode=0),
) as mock_run,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
)
command = mock_run.call_args.args[0]
self.assertEqual(command[0], str(custom_shac))
def test_run_shac_command_construction(self) -> None:
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("foo.cc",),
),
)
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
),
mock.patch.object(
subprocess,
"run",
autospec=True,
return_value=subprocess.CompletedProcess(args=[], returncode=0),
) as mock_run,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
all_files=True,
only_checks=("clang_format", "commit_msg"),
skip_checks=("rustfmt",),
extra_args=("--no-color",),
)
mock_run.assert_called_once()
command = mock_run.call_args.args[0]
self.assertSequenceEqual(
command,
(
str(self.shac_binary),
"check",
"-C",
str(self.fuchsia_dir),
"--var=fuchsia_dir=.",
"--var=fuchsia_build_dir=out/default",
"--all",
"--only=clang_format,commit_msg",
"--skip=rustfmt",
"--no-color",
"foo.cc",
),
)
def test_run_shac_dry_run_skips_execution(self) -> None:
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("foo.cc",),
),
)
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
),
mock.patch.object(subprocess, "run", autospec=True) as mock_run,
mock.patch.object(
sys, "stdout", new_callable=io.StringIO
) as mock_stdout,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
dry_run=True,
)
mock_run.assert_not_called()
self.assertIn("[dry-run] Would execute:", mock_stdout.getvalue())
def test_run_shac_propagates_subprocess_error(self) -> None:
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("foo.cc",),
),
)
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
),
mock.patch.object(
subprocess,
"run",
autospec=True,
side_effect=subprocess.CalledProcessError(
returncode=1, cmd=["shac", "check"]
),
),
self.assertRaises(subprocess.CalledProcessError),
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
)
def test_run_shac_executes_across_multiple_repos(self) -> None:
subrepo = self.fuchsia_dir / "vendor" / "google"
subrepo.mkdir(parents=True)
repo_groups = (
scm.RepoGroup(
repo_root=self.fuchsia_dir,
has_shac=True,
files=("src/main.cc",),
),
scm.RepoGroup(
repo_root=subrepo,
has_shac=True,
files=("foo/bar.cc",),
),
)
with (
mock.patch.object(
scm, "get_repo_groups", autospec=True, return_value=repo_groups
),
mock.patch.object(
subprocess,
"run",
autospec=True,
return_value=subprocess.CompletedProcess(args=[], returncode=0),
) as mock_run,
):
shac_runner.run_shac(
"check",
self.fuchsia_dir,
self.build_dir,
self.fuchsia_dir,
)
self.assertEqual(mock_run.call_count, 2)
first_command = mock_run.call_args_list[0].args[0]
second_command = mock_run.call_args_list[1].args[0]
self.assertSequenceEqual(
first_command,
(
str(self.shac_binary),
"check",
"-C",
str(self.fuchsia_dir),
"--var=fuchsia_dir=.",
"--var=fuchsia_build_dir=out/default",
"src/main.cc",
),
)
self.assertSequenceEqual(
second_command,
(
str(self.shac_binary),
"check",
"-C",
str(subrepo),
"--var=fuchsia_dir=../..",
"--var=fuchsia_build_dir=out/default",
"foo/bar.cc",
),
)
def test_split_comma_separated(self) -> None:
self.assertSequenceEqual(
shac_runner.split_comma_separated(()),
(),
)
self.assertSequenceEqual(
shac_runner.split_comma_separated(
("clang_format, rustfmt ", "", "commit_msg , ")
),
("clang_format", "rustfmt", "commit_msg"),
)
def test_resolve_workspace_paths_defaults(self) -> None:
env_without_dirs = {
key: value
for key, value in os.environ.items()
if key not in ("FUCHSIA_DIR", "FUCHSIA_BUILD_DIR")
}
with mock.patch.dict(os.environ, env_without_dirs, clear=True):
fuchsia_dir, build_dir, _ = shac_runner.resolve_workspace_paths(
self.fuchsia_dir
)
self.assertEqual(fuchsia_dir, self.fuchsia_dir.resolve())
self.assertEqual(
build_dir, (self.fuchsia_dir / "out" / "default").resolve()
)
def test_resolve_workspace_paths_from_env(self) -> None:
custom_build_dir = self.fuchsia_dir / "out" / "custom"
with mock.patch.dict(
os.environ,
{
"FUCHSIA_DIR": str(self.fuchsia_dir),
"FUCHSIA_BUILD_DIR": str(custom_build_dir),
},
):
fuchsia_dir, build_dir, _ = shac_runner.resolve_workspace_paths(
pathlib.Path("/unused/fallback")
)
self.assertEqual(fuchsia_dir, self.fuchsia_dir.resolve())
self.assertEqual(build_dir, custom_build_dir.resolve())
if __name__ == "__main__":
unittest.main()