| #!/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() |