You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/tests/siso_test.py

1732 lines
56 KiB
Python

#!/usr/bin/env vpython3
# Copyright (c) 2024 The Chromium 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 io
import os
import shlex
import sys
import json
import pytest
import subprocess
import itertools
from unittest.mock import DEFAULT
from pathlib import Path
from typing import Any, Dict, List, Tuple, Generator, Optional
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT_DIR)
import siso
import build_telemetry
import gclient_paths
# These are required for fixtures to work.
# pylint: disable=redefined-outer-name,unused-argument
# The functions are annotated with lru and the state is preserved between tests.
@pytest.fixture(autouse=True)
def clear_gclient_paths_caches():
gclient_paths.FindGclientRoot.cache_clear()
gclient_paths._GetPrimarySolutionPathInternal.cache_clear()
gclient_paths._GetBuildtoolsPathInternal.cache_clear()
gclient_paths._GetGClientSolutions.cache_clear()
def _get_siso_config_dir(tmp_path: Path) -> Path:
return tmp_path / "src" / "build" / "config" / "siso"
def _get_sisoenv_path(tmp_path: Path) -> Path:
return _get_siso_config_dir(tmp_path) / ".sisoenv"
def _get_siso_bin_dir(tmp_path: Path) -> Path:
return tmp_path / "src" / "third_party" / "siso" / "cipd"
def _get_siso_bin_path(tmp_path: Path) -> Path:
return _get_siso_bin_dir(tmp_path) / ('siso' + gclient_paths.GetExeSuffix())
def create_telemetry_cfg(tmp_path: Path,
mocker: Any,
enabled: bool = True) -> build_telemetry.Config:
config_path = tmp_path / "build_telemetry.cfg"
status = "opt-in" if enabled else "opt-out"
config_data = {
"user": "test@google.com",
"status": status,
"countdown": 0,
"version": 1
}
config_path.write_text(json.dumps(config_data))
mocker.patch("build_telemetry.shutil.which", return_value="/bin/gcert")
config = build_telemetry.Config(str(config_path), 0)
mocker.patch("build_telemetry.load_config", return_value=config)
return config
@pytest.fixture
def siso_test_fixture(tmp_path: Path,
mocker: Any) -> Generator[None, None, None]:
# Replace trial dir functionality with tmp_parth.
previous_dir = os.getcwd()
(tmp_path / "src").mkdir(parents=True, exist_ok=True)
os.chdir(tmp_path / "src")
mocker.patch("siso.getpass.getuser", return_value="testuser")
yield
os.chdir(previous_dir)
@pytest.fixture
def siso_project_setup(siso_test_fixture: None, tmp_path: Path,
mocker: Any) -> None:
sisoenv_dir = _get_siso_config_dir(tmp_path)
sisoenv_dir.mkdir(parents=True, exist_ok=True)
(sisoenv_dir / ".sisoenv").write_text("SISO_PROJECT=test-project\n")
siso_bin_dir = _get_siso_bin_dir(tmp_path)
siso_bin_dir.mkdir(parents=True, exist_ok=True)
siso_bin_path = _get_siso_bin_path(tmp_path)
siso_bin_path.write_text("")
os.chmod(siso_bin_path, 0o755)
(tmp_path / ".gclient").write_text("")
(tmp_path / ".gclient_entries").write_text("entries = {'src': '...'}")
(tmp_path / "src" / "out" / "Default").mkdir(parents=True, exist_ok=True)
mocker.patch("siso._get_siso_subcmds",
return_value={"ninja", "query", "other"})
mocker.patch("siso._handle_collector",
side_effect=lambda _p, _a, e, subcmd="": e)
# Create enabled telemetry config by default
create_telemetry_cfg(tmp_path, mocker, enabled=True)
def test_load_sisorc_no_file(siso_test_fixture: Any) -> None:
global_flags, subcmd_flags = siso.load_sisorc(
os.path.join("build", "config", "siso", ".sisorc"))
assert global_flags == []
assert subcmd_flags == {}
def test_load_sisorc(siso_test_fixture: Any) -> None:
sisorc = os.path.join("build", "config", "siso", ".sisorc")
os.makedirs(os.path.dirname(sisorc))
with open(sisorc, "w") as f:
f.write("""
# comment
-credential_helper=gcloud
ninja --failure_verbose=false -k=0
""")
global_flags, subcmd_flags = siso.load_sisorc(sisorc)
assert global_flags == ["-credential_helper=gcloud"]
assert subcmd_flags == {"ninja": ["--failure_verbose=false", "-k=0"]}
@pytest.mark.parametrize(
"args, subcmds, want",
[
pytest.param(
[
"-mutexprofile", "siso_mutex.prof", "ninja", "-project",
"rbe-chrome-untrusted", "--enable_cloud_logging", "-C",
"out/Default"
],
{"ninja", "query"},
(["-mutexprofile", "siso_mutex.prof"], "ninja", [
"-project", "rbe-chrome-untrusted", "--enable_cloud_logging",
"-C", "out/Default"
]),
id="complex_global_flags",
),
pytest.param(
["ninja", "-C", "out/Default"],
{"ninja", "query"},
([], "ninja", ["-C", "out/Default"]),
id="simple_ninja",
),
pytest.param(
["-v", "1", "ninja"],
{"ninja", "query"},
(["-v", "1"], "ninja", []),
id="flag_with_value_before_subcmd",
),
pytest.param(
["--version"],
{"ninja", "query"},
(["--version"], "", []),
id="no_subcmd",
),
],
)
def test_split_args(args: List[str], subcmds: set[str],
want: Tuple[List[str], str,
List[str]], mocker: Any) -> None:
mocker.patch("siso._get_siso_subcmds", return_value=subcmds)
got = siso.split_args(args, "siso")
assert got == want
@pytest.mark.parametrize(
"args, want",
[
pytest.param(
["-C", "out/Debug", "ninja"],
"out/Debug",
id="C_before_subcmd",
),
pytest.param(
["-Cout/Release", "ninja"],
"out/Release",
id="C_compact_before_subcmd",
),
pytest.param(
["ninja", "-C", "out/Default"],
"out/Default",
id="simple_ninja",
),
pytest.param(
["ninja", "-Cout/Release"],
"out/Release",
id="C_compact_after_subcmd",
),
pytest.param(
["--version"],
".",
id="no_outdir",
),
],
)
def test_fetch_out_dir(args: List[str], want: str) -> None:
got = siso.fetch_out_dir(args)
assert got == want
def test_get_siso_subcmds(mocker: Any) -> None:
mock_run = mocker.patch("siso.subprocess.run")
mock_run.return_value = mocker.Mock(
returncode=0,
stdout="""Usage: siso <flags> <subcommand> <subcommand args>
Subcommands:
collector OTEL collector daemon
ninja build the requests targets as ninja
Subcommands for auth:
auth-check prints current auth status
login login to siso system
logout logout from siso system
""")
got = siso._get_siso_subcmds("siso")
assert got == {"collector", "ninja", "auth-check", "login", "logout"}
@pytest.mark.parametrize(
"subcmd_args, env, want",
[
pytest.param(
["-C", "out/Default"],
{},
[
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
],
id="no_env_flags",
),
pytest.param(
[
"-C",
"out/Default",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
],
{},
[
"-C",
"out/Default",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
],
id="some_already_applied_no_env_flags",
),
pytest.param(
["-C", "out/Default", "--metrics_project", "some_project"],
{},
[
"-C",
"out/Default",
"--metrics_project",
"some_project",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
],
id="metrics_project_set",
),
pytest.param(
["-C", "out/Default"],
{"RBE_metrics_project": "some_project"},
[
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
],
id="metrics_project_set_thru_env",
),
pytest.param(
["-C", "out/Default", "--project", "some_project"],
{},
[
"-C",
"out/Default",
"--project",
"some_project",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=some_project",
],
id="cloud_project_set",
),
pytest.param(
["-C", "out/Default"],
{"SISO_PROJECT": "some_project"},
[
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=some_project",
],
id="cloud_project_set_thru_env",
),
pytest.param(
["-C", "out/Default", "--enable_cloud_profiler=false"],
{"SISO_PROJECT": "some_project"},
[
"-C",
"out/Default",
"--enable_cloud_profiler=false",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=some_project",
],
id="respects_set_flags",
),
pytest.param(
["--help"],
{},
[
"--help",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
],
id="help_flag",
),
pytest.param(
["-h"],
{},
[
"-h",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
],
id="short_help_flag",
),
pytest.param(
["-C", "out/Default", "--metrics_labels=foo=bar"],
{},
["-C", "out/Default", "--metrics_labels=foo=bar"],
id="labels_exist",
),
],
)
def test_apply_telemetry_flags(subcmd_args: List[str], env: Dict[str, str],
want: List[str]) -> None:
got = siso.apply_telemetry_flags(subcmd_args, env)
assert got == want
def test_apply_telemetry_flags_sets_expected_env_var(mocker: Any) -> None:
mocker.patch.dict("os.environ", {})
subcmd_args = [
"-C",
"out/Default",
]
env = {}
_ = siso.apply_telemetry_flags(subcmd_args, env)
assert env.get("GOOGLE_API_USE_CLIENT_CERTIFICATE") == "false"
@pytest.mark.parametrize(
"subcmd_args, env, want",
[
pytest.param(
["--metrics_project", "proj1"],
{},
"proj1",
id="metrics_project_arg",
),
pytest.param(["--project", "proj2"], {}, "proj2", id="project_arg"),
pytest.param(
["--metrics_project", "proj1", "--project", "proj2"],
{},
"proj1",
id="metrics_project_and_project_args",
),
pytest.param(
[],
{"RBE_metrics_project": "proj3"},
"proj3",
id="rbe_metrics_project_env",
),
pytest.param(
[], {"SISO_PROJECT": "proj4"}, "proj4", id="siso_project_env"),
pytest.param(
[],
{
"RBE_metrics_project": "proj3",
"SISO_PROJECT": "proj4"
},
"proj3",
id="rbe_and_siso_project_env",
),
pytest.param(
["--project", "proj2"],
{"RBE_metrics_project": "proj3"},
"proj2",
id="project_arg_and_rbe_env",
),
pytest.param(
["--metrics_project", "proj1"],
{"RBE_metrics_project": "proj3"},
"proj1",
id="metrics_project_arg_and_rbe_env",
),
pytest.param([], {}, "", id="no_project"),
pytest.param(
["-metrics_project", "proj1"],
{},
"proj1",
id="short_metrics_project_arg",
),
pytest.param(["-project", "proj2"], {}, "proj2",
id="short_project_arg"),
],
)
def test_fetch_metrics_project(subcmd_args: List[str], env: Dict[str, str],
want: str) -> None:
got = siso._fetch_metrics_project(subcmd_args, env)
assert got == want
@pytest.mark.parametrize(
"platform, env_vars, want_path_template",
[
(
"Linux",
{
"XDG_RUNTIME_DIR": os.path.join("{root_dir}", "run", "user",
"1000")
},
os.path.join("{root_dir}", "run", "user", "1000", "{user}", "siso"),
),
("Linux", {}, os.path.join("/tmp", "{user}", "siso")),
(
"Darwin",
{
"TMPDIR":
os.path.join("{root_dir}", "var", "folders", "12", "345..."),
},
os.path.join("{root_dir}", "var", "folders", "12", "345...",
"{user}", "siso"),
),
("Darwin", {}, os.path.join("/tmp", "{user}", "siso")),
(
"Linux",
{
"XDG_RUNTIME_DIR": "a" * 100
},
os.path.join("/tmp", "{user}", "siso"),
),
],
)
def test_resolve_sockets_folder(
siso_test_fixture: Any,
tmp_path: Path,
platform: str,
env_vars: Dict[str, str],
want_path_template: str,
mocker: Any,
) -> None:
user = "testuser"
# Replace placeholders in paths
for key, value in env_vars.items():
env_vars[key] = value.format(root_dir=str(tmp_path))
want_path = want_path_template.format(root_dir=str(tmp_path), user=user)
mocker.patch("sys.platform", new=platform.lower())
path, length = siso._resolve_sockets_folder(env_vars)
# If the desired path is too long, the function will fall back to /tmp/<user>/siso
if (104 - len(want_path) - 6) < 1:
expected_path = os.path.join("/tmp", user, "siso")
else:
expected_path = want_path
expected_len = 104 - len(expected_path) - 6
# Windows.
assert path == expected_path
assert length == expected_len
assert os.path.isdir(path)
@pytest.mark.parametrize(
"args, subcmd, project_val, telemetry_enabled",
[
(["-h"], "ninja", "test-project", True),
(["--help"], "ninja", "test-project", True),
(["-help"], "ninja", "test-project", True),
([], "other", "test-project", True),
([], "ninja", "", True),
([], "ninja", "test-project", False),
],
)
def test_main_handle_collector_skipped(
siso_project_setup: None,
tmp_path: Path,
mocker: Any,
args: List[str],
subcmd: str,
project_val: str,
telemetry_enabled: bool,
) -> None:
_get_sisoenv_path(tmp_path).write_text("")
siso_bin_path = _get_siso_bin_path(tmp_path)
mocker.patch("siso._fetch_metrics_project", return_value=project_val)
runner = mocker.Mock(return_value=0)
telemetry_cfg = create_telemetry_cfg(tmp_path,
mocker,
enabled=telemetry_enabled)
siso.main(["siso.py"] + args + ([subcmd] if subcmd else []),
telemetry_cfg=telemetry_cfg,
env={"SISO_PATH": str(siso_bin_path)},
runner=runner)
# Verify that the runner was NOT passed a collector address in its env
_, called_kwargs = runner.call_args
called_env = called_kwargs.get("env", {})
assert "SISO_COLLECTOR_ADDRESS" not in called_env
@pytest.fixture
def start_collector_mocks(mocker: Any) -> Dict[str, Any]:
mocks = {
"subprocess_run":
mocker.patch("siso.subprocess.run"),
"kill_collector":
mocker.patch("siso._kill_collector"),
"time_sleep":
mocker.patch("siso.time.sleep"),
"time_time":
mocker.patch("siso.time.time",
side_effect=(1000 + i * 0.1 for i in range(100))),
"http_connection":
mocker.patch("siso.http.client.HTTPConnection"),
"subprocess_popen":
mocker.patch("siso.subprocess.Popen"),
"os_path_exists":
mocker.patch("os.path.exists", return_value=True),
"os_remove":
mocker.patch("os.remove"),
}
mock_conn = mocker.Mock()
mocks["http_connection"].return_value = mock_conn
mocks["mock_conn"] = mock_conn
return mocks
def _configure_http_responses(
mocker: Any,
mock_conn: Any,
status_responses: List[Tuple[int, Any]],
config_responses: Optional[List[Tuple[int, Any]]] = None,
) -> None:
if config_responses is None:
config_responses = []
request_path_history = []
def request_side_effect(method, path):
request_path_history.append(path)
def getresponse_side_effect():
path = request_path_history[-1]
if path == "/health/status":
if not status_responses:
return mocker.Mock(status=404,
read=mocker.Mock(return_value=b""))
status_code, _ = status_responses.pop(0)
return mocker.Mock(status=status_code,
read=mocker.Mock(return_value=b""))
if path == "/health/config":
if not config_responses:
return mocker.Mock(status=200,
read=mocker.Mock(return_value=b"{}"))
status_code, _ = config_responses.pop(0)
return mocker.Mock(status=status_code,
read=mocker.Mock(return_value=b""))
return mocker.Mock(status=404)
mock_conn.request.side_effect = request_side_effect
mock_conn.getresponse.side_effect = getresponse_side_effect
def test_handle_collector_removes_existing_socket_file(
siso_test_fixture: Any, start_collector_mocks: Dict[str, Any],
mocker: Any) -> None:
mocker.patch("sys.platform", new="linux")
mock_os_path_exists = mocker.patch("os.path.exists", return_value=True)
mock_os_remove = mocker.patch("os.remove")
mocker.patch("siso._fetch_metrics_project", return_value="test-project")
siso_path = "siso_path"
sockets_file = os.path.join("/tmp", "testuser", "siso", "test-project.sock")
siso._handle_collector(siso_path, ["ninja"], {})
mock_os_path_exists.assert_called_with(sockets_file)
mock_os_remove.assert_called_with(sockets_file)
def test_handle_collector_remove_socket_file_fails(siso_test_fixture: Any,
start_collector_mocks: Dict[
str, Any],
mocker: Any) -> None:
mocker.patch("sys.platform", new="linux")
mock_os_path_exists = mocker.patch("os.path.exists", return_value=True)
mock_os_remove = mocker.patch("os.remove",
side_effect=OSError("Permission denied"))
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
mocker.patch("siso._fetch_metrics_project", return_value="test-project")
siso_path = "siso_path"
sockets_file = os.path.join("/tmp", "testuser", "siso", "test-project.sock")
siso._handle_collector(siso_path, ["ninja"], {})
mock_os_path_exists.assert_called_with(sockets_file)
mock_os_remove.assert_called_with(sockets_file)
assert f"Failed to remove {sockets_file}" in mock_stderr.getvalue()
@pytest.mark.parametrize(
"global_flags, subcmd_flags, args, should_collect_logs, env, want, want_stderr",
[
pytest.param(
[],
{},
["other", "-C", "out/Default"],
True,
{},
["other", "-C", "out/Default"],
"",
id="no_ninja",
),
pytest.param(
[],
{},
["ninja", "-C", "out/Default"],
False,
{},
[
"ninja",
"-C",
"out/Default",
],
"",
id="ninja_no_logs",
),
pytest.param(
[],
{},
["ninja", "-C", "out/Default"],
True,
{},
[
"ninja",
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
],
"depot_tools/siso.py: %s\n" % shlex.join([
"ninja", "-C", "out/Default", "--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}"
]),
id="ninja_with_logs_no_project",
),
pytest.param(
[],
{},
["ninja", "-C", "out/Default", "--project=test-project"],
True,
{},
[
"ninja",
"-C",
"out/Default",
"--project=test-project",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=test-project",
],
"depot_tools/siso.py: %s\n" % shlex.join([
"ninja", "-C", "out/Default", "--project=test-project",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring", "--enable_cloud_profiler",
"--enable_cloud_trace", "--enable_cloud_logging",
"--metrics_project=test-project"
]),
id="ninja_with_logs_with_project_in_args",
),
pytest.param(
[],
{},
["ninja", "-C", "out/Default"],
True,
{"SISO_PROJECT": "test-project"},
[
"ninja",
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=test-project",
],
"depot_tools/siso.py: %s\n" % shlex.join([
"ninja", "-C", "out/Default", "--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring", "--enable_cloud_profiler",
"--enable_cloud_trace", "--enable_cloud_logging",
"--metrics_project=test-project"
]),
id="ninja_with_logs_with_project_in_env",
),
pytest.param(
["-gflag"],
{"ninja": ["-sflag"]},
["ninja", "-C", "out/Default"],
False,
{},
[
"-gflag",
"ninja",
"-sflag",
"-C",
"out/Default",
],
"depot_tools/siso.py: %s\n" %
shlex.join(["-gflag", "ninja", "-sflag", "-C", "out/Default"]),
id="with_sisorc",
),
pytest.param(
["-gflag_only"],
{},
["ninja", "-C", "out/Default"],
False,
{},
[
"-gflag_only",
"ninja",
"-C",
"out/Default",
],
"depot_tools/siso.py: %s\n" %
shlex.join(["-gflag_only", "ninja", "-C", "out/Default"]),
id="with_sisorc_global_flags_only",
),
pytest.param(
[],
{"ninja": ["-sflag_only"]},
["ninja", "-C", "out/Default"],
False,
{},
[
"ninja",
"-sflag_only",
"-C",
"out/Default",
],
"depot_tools/siso.py: %s\n" %
shlex.join(["ninja", "-sflag_only", "-C", "out/Default"]),
id="with_sisorc_subcmd_flags_only",
),
pytest.param(
["-gflag_tel"],
{"ninja": ["-sflag_tel"]},
["ninja", "-C", "out/Default"],
True,
{"SISO_PROJECT": "telemetry-project"},
[
"-gflag_tel",
"ninja",
"-sflag_tel",
"-C",
"out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring",
"--enable_cloud_profiler",
"--enable_cloud_trace",
"--enable_cloud_logging",
"--metrics_project=telemetry-project",
],
"depot_tools/siso.py: %s\n" % shlex.join([
"-gflag_tel", "ninja", "-sflag_tel", "-C", "out/Default",
"--metrics_labels",
f"type=developer,tool=siso,host_os={siso._SYSTEM_DICT.get(sys.platform, sys.platform)}",
"--enable_cloud_monitoring", "--enable_cloud_profiler",
"--enable_cloud_trace", "--enable_cloud_logging",
"--metrics_project=telemetry-project"
]),
id="with_sisorc_global_and_subcmd_flags_and_telemetry",
),
pytest.param(
["-gflag_non_ninja"],
{"query": ["-sflag_non_ninja"]},
["query", "-C", "out/Default"],
True,
{"SISO_PROJECT": "telemetry-project"},
[
"-gflag_non_ninja",
"query",
"-sflag_non_ninja",
"-C",
"out/Default",
],
"depot_tools/siso.py: %s\n" % shlex.join([
"-gflag_non_ninja",
"query",
"-sflag_non_ninja",
"-C",
"out/Default",
]),
id="with_sisorc_non_ninja_subcmd",
),
],
)
def test_main_process_args(
global_flags: List[str],
subcmd_flags: Dict[str, List[str]],
args: List[str],
should_collect_logs: bool,
env: Dict[str, str],
want: List[str],
want_stderr: str,
siso_project_setup: None,
tmp_path: Path,
mocker: Any,
) -> None:
# Clear .sisoenv to avoid interference with test cases that set environment variables
_get_sisoenv_path(tmp_path).write_text("")
# Setup dummy project structure using real files to avoid mocking 'open'
siso_bin_path = _get_siso_bin_path(tmp_path)
# Create .sisorc if flags are provided
sisorc_path = _get_siso_config_dir(tmp_path) / ".sisorc"
if sisorc_path.exists():
sisorc_path.unlink()
if global_flags or subcmd_flags:
with open(sisorc_path, "w") as f:
for flag in global_flags:
f.write(f"{flag}\n")
for subcmd, flags in subcmd_flags.items():
f.write(f"{subcmd} {' '.join(flags)}\n")
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
runner = mocker.Mock(return_value=0)
telemetry_cfg = create_telemetry_cfg(tmp_path,
mocker,
enabled=should_collect_logs)
# Provide a SISO_PATH to point to our dummy binary
env_with_path = env.copy()
env_with_path["SISO_PATH"] = str(siso_bin_path)
siso.main(["siso.py"] + args,
telemetry_cfg=telemetry_cfg,
env=env_with_path,
runner=runner)
# Verify runner was called with the expected arguments
assert runner.call_count == 1
called_args = runner.call_args[0][0]
# The first argument is the siso path, the rest are the processed args
assert called_args[0] == str(siso_bin_path)
assert called_args[1:] == want
actual_stderr = mock_stderr.getvalue()
expected_full_stderr = f"depot_tools/siso.py: Using Siso binary from SISO_PATH: {siso_bin_path}.\n"
expected_full_stderr += want_stderr
assert actual_stderr == expected_full_stderr
# Else it won"t even compile on Windows.
if sys.platform != "win32":
SIGKILL = siso.signal.SIGKILL # pylint: disable=no-member
else:
SIGKILL = None
@pytest.mark.skipif(sys.platform == "win32", reason="Not applicable on Windows")
@pytest.mark.parametrize(
"stdout, stderr, returncode, kill_side_effect, expected_result, expected_kill_args",
[
pytest.param(
b"123\n", b"", 0, None, True,
(123, SIGKILL), id="found_and_killed"),
pytest.param(
b"",
b"lsof: no process found\n",
1,
None,
False,
None,
id="process_not_found",
),
pytest.param(
b"123\n",
b"",
0,
OSError("Operation not permitted"),
False,
(123, SIGKILL),
id="kill_fails",
),
pytest.param(b"\n", b"", 0, None, False, None, id="no_pids_found"),
pytest.param(
b"0\n123\n456\n",
b"",
0,
None,
True,
(123, SIGKILL),
id="multiple_pids_found",
),
],
)
def test_kill_collector_posix(
stdout: bytes,
stderr: bytes,
returncode: int,
kill_side_effect: Optional[OSError],
expected_result: bool,
expected_kill_args: Optional[Tuple[int, Any]],
mocker: Any,
) -> None:
mocker.patch("sys.platform", new="linux")
mock_os_kill = mocker.patch("siso.os.kill")
mock_subprocess_run = mocker.patch("siso.subprocess.run")
mock_subprocess_run.return_value = mocker.Mock(stdout=stdout,
stderr=stderr,
returncode=returncode)
mock_os_kill.side_effect = kill_side_effect
result = siso._kill_collector()
assert result == expected_result
mock_subprocess_run.assert_called_once_with(
["lsof", "-t", f"-i:{siso._OTLP_HEALTH_PORT}"], capture_output=True)
if expected_kill_args:
mock_os_kill.assert_called_once_with(*expected_kill_args)
else:
mock_os_kill.assert_not_called()
@pytest.mark.skipif(sys.platform != "win32", reason="Only for Windows")
@pytest.mark.parametrize(
"run_effects, expected_result, expected_calls",
[
pytest.param(
[
(
f" TCP 127.0.0.1:{siso._OTLP_HEALTH_PORT} [::]:0 LISTENING 1234\r\n"
.encode("utf-8"),
b"",
0,
),
(b"", b"", 0),
],
True,
[
["netstat", "-aon"],
["taskkill", "/F", "/T", "/PID", "1234"],
],
id="found_and_killed",
),
pytest.param(
[
(
b" TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 868\r\n",
b"",
0,
),
],
False,
[["netstat", "-aon"]],
id="process_not_found",
),
pytest.param(
[
(
f" TCP 127.0.0.1:{siso._OTLP_HEALTH_PORT} [::]:0 LISTENING 1234\r\n TCP 127.0.0.1:{siso._OTLP_HEALTH_PORT} [::]:0 LISTENING 5678\r\n"
.encode("utf-8"),
b"",
0,
),
(b"", b"", 0),
],
True,
[
["netstat", "-aon"],
["taskkill", "/F", "/T", "/PID", "1234"],
],
id="multiple_pids_found",
),
pytest.param(
[
(b"", b"netstat error\n", 1),
],
False,
[["netstat", "-aon"]],
id="netstat_fails",
),
pytest.param(
[
(
f" TCP 127.0.0.1:{siso._OTLP_HEALTH_PORT} [::]:0 LISTENING 1234\r\n"
.encode("utf-8"),
b"",
0,
),
(b"", b"ERROR: Cannot terminate process.", 1),
],
False,
[
["netstat", "-aon"],
["taskkill", "/F", "/T", "/PID", "1234"],
],
id="taskkill_fails",
),
],
)
def test_kill_collector_windows(
run_effects: List[Tuple[bytes, bytes, int]],
expected_result: bool,
expected_calls: List[List[str]],
mocker: Any,
) -> None:
mock_subprocess_run = mocker.patch("siso.subprocess.run")
mock_subprocess_run.side_effect = [
mocker.Mock(stdout=stdout, stderr=stderr, returncode=returncode)
for stdout, stderr, returncode in run_effects
]
result = siso._kill_collector()
assert result == expected_result
calls = [mocker.call(c, capture_output=True) for c in expected_calls]
mock_subprocess_run.assert_has_calls(calls)
assert mock_subprocess_run.call_count == len(calls)
@pytest.mark.parametrize(
"platform, creationflags",
[
("linux", 0),
("win32", 512), # subprocess.CREATE_NEW_PROCESS_GROUP
],
)
def test_handle_collector_dead_then_healthy(
siso_test_fixture: Any,
platform: str,
creationflags: int,
start_collector_mocks: Dict[str, Any],
mocker: Any,
) -> None:
mocker.patch("sys.platform", new=platform)
mocker.patch("subprocess.CREATE_NEW_PROCESS_GROUP",
creationflags,
create=True)
mock_json_loads = mocker.patch("siso.json.loads")
m = start_collector_mocks
siso_path = "siso_path"
project = "test-project"
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=[(404, None), (200, None)],
config_responses=[(200, None)],
)
status_healthy = {"healthy": True, "status": "StatusOK"}
if platform == "linux":
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
else:
endpoint = siso._OTLP_DEFAULT_TCP_ENDPOINT
config = {
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
}
}
mock_json_loads.side_effect = [status_healthy, config]
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
assert res_env.get("SISO_COLLECTOR_ADDRESS")
if platform == "linux":
assert res_env["SISO_COLLECTOR_ADDRESS"] == f"unix://{endpoint}"
else:
assert res_env["SISO_COLLECTOR_ADDRESS"] == endpoint
m["subprocess_popen"].assert_called_once_with(
[siso_path, "collector", "--project", project],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
env=res_env,
creationflags=creationflags,
)
m["kill_collector"].assert_not_called()
def test_handle_collector_unhealthy_then_healthy(siso_test_fixture: Any,
start_collector_mocks: Dict[
str, Any],
mocker: Any) -> None:
mocker.patch("sys.platform", new="linux")
mock_json_loads = mocker.patch("siso.json.loads")
m = start_collector_mocks
siso_path = "siso_path"
project = "test-project"
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=[(200, None), (200, None)],
config_responses=[(200, None), (200, None)],
)
status_unhealthy = {"healthy": False, "status": "NotOK"}
status_healthy = {"healthy": True, "status": "StatusOK"}
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
config_project_full = {
"exporters": {
"googlecloud": {
"project": project
}
},
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
},
}
mock_json_loads.side_effect = [
status_unhealthy,
status_healthy,
config_project_full,
config_project_full,
]
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
assert res_env.get("SISO_COLLECTOR_ADDRESS") == f"unix://{endpoint}"
m["subprocess_popen"].assert_called_once_with(
[siso_path, "collector", "--project", project],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
env=res_env,
creationflags=0,
)
m["kill_collector"].assert_called_once()
def test_handle_collector_already_healthy(siso_test_fixture: Any,
start_collector_mocks: Dict[str, Any],
mocker: Any) -> None:
mocker.patch("sys.platform", new="linux")
mock_json_loads = mocker.patch("siso.json.loads")
m = start_collector_mocks
siso_path = "siso_path"
project = "test-project"
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=[(200, None)],
config_responses=[(200, None), (200, None)],
)
status_healthy = {"healthy": True, "status": "StatusOK"}
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
config_project_full = {
"exporters": {
"googlecloud": {
"project": project
}
},
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
},
}
mock_json_loads.side_effect = [
status_healthy,
config_project_full,
config_project_full,
]
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
assert res_env.get("SISO_COLLECTOR_ADDRESS") == f"unix://{endpoint}"
m["subprocess_popen"].assert_not_called()
m["kill_collector"].assert_not_called()
def test_handle_collector_never_healthy(siso_test_fixture: Any,
start_collector_mocks: Dict[str, Any],
mocker: Any) -> None:
mocker.patch("sys.platform", new="linux")
m = start_collector_mocks
captured_env = {}
def popen_side_effect(*args, **kwargs):
nonlocal captured_env
if "env" in kwargs:
captured_env = kwargs["env"].copy()
return mocker.Mock()
m["subprocess_popen"].side_effect = popen_side_effect
siso_path = "siso_path"
project = "test-project"
_configure_http_responses(mocker,
m["mock_conn"],
status_responses=[(404, None)])
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
# If never healthy, handle_collector removes the address from env
assert "SISO_COLLECTOR_ADDRESS" not in res_env
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
expected_env = env.copy()
expected_env["SISO_COLLECTOR_ADDRESS"] = f"unix://{endpoint}"
assert captured_env == expected_env
m["subprocess_popen"].assert_called_once()
m["kill_collector"].assert_not_called()
@pytest.mark.parametrize(
"expected_result, http_status_responses, json_loads_side_effect_values",
[
(
True,
[(404, None), (200, None)],
["status_healthy", "config_with_socket"],
),
(
True,
[(404, None)] + [(404, None)] * 5 + [(200, None)],
["status_healthy", "config_with_socket"],
),
(False, [(404, None)] * 30, []),
],
ids=["healthy_immediately", "healthy_later", "never_healthy"],
)
def test_handle_collector_lifecycle(
siso_test_fixture: Any,
start_collector_mocks: Dict[str, Any],
mocker: Any,
expected_result: bool,
http_status_responses: List[Tuple[int, Any]],
json_loads_side_effect_values: List[str],
) -> None:
mocker.patch("sys.platform", new="linux")
mock_json_loads = mocker.patch("siso.json.loads")
siso_path = "siso_path"
project = "test-project"
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
status_healthy = {"healthy": True, "status": "StatusOK"}
config_with_socket = {
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
}
}
json_loads_map = {
"status_healthy": status_healthy,
"config_with_socket": config_with_socket,
}
json_loads_side_effect = [
json_loads_map[v] for v in json_loads_side_effect_values
]
m = start_collector_mocks
mock_json_loads.side_effect = json_loads_side_effect
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=list(http_status_responses),
config_responses=[(200, None)] * 20,
)
captured_env = {}
def popen_side_effect(*args, **kwargs):
nonlocal captured_env
if "env" in kwargs:
captured_env = kwargs["env"].copy()
return mocker.Mock()
m["subprocess_popen"].side_effect = popen_side_effect
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
if expected_result:
assert res_env.get("SISO_COLLECTOR_ADDRESS") == f"unix://{endpoint}"
else:
assert "SISO_COLLECTOR_ADDRESS" not in res_env
expected_env = env.copy()
expected_env["SISO_COLLECTOR_ADDRESS"] = f"unix://{endpoint}"
m["subprocess_popen"].assert_called_once()
assert captured_env == expected_env
if not expected_result:
m["kill_collector"].assert_not_called()
@pytest.mark.skipif(sys.platform == "win32", reason="Not applicable on Windows")
def test_handle_collector_missing_sockets_file_appears_later(
siso_test_fixture: Any,
start_collector_mocks: Dict[str, Any],
mocker: Any,
) -> None:
mocker.patch("sys.platform", new="linux")
socket_exists_vals = iter([False, False, True])
def socket_file_sideeff(path: str) -> bool:
if path.endswith(".sock"):
return next(socket_exists_vals)
return DEFAULT
mocker.patch("os.path.exists", side_effect=socket_file_sideeff)
m = start_collector_mocks
siso_path = "siso_path"
project = "test-project"
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
# Status: DEAD -> (Start) -> Loop 1 (200) -> Loop 2 (200)
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=[(404, None), (200, None), (200, None)],
config_responses=[(200, None), (200, None)],
)
status_healthy = {"healthy": True, "status": "StatusOK"}
config_with_socket = {
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
}
}
mock_json_loads = mocker.patch("siso.json.loads")
mock_json_loads.side_effect = [
status_healthy, config_with_socket, status_healthy, config_with_socket
]
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
assert res_env.get("SISO_COLLECTOR_ADDRESS") == f"unix://{endpoint}"
@pytest.mark.skipif(sys.platform == "win32", reason="Not applicable on Windows")
def test_handle_collector_missing_sockets_file_never_appears(
siso_test_fixture: Any,
start_collector_mocks: Dict[str, Any],
mocker: Any,
) -> None:
mocker.patch("sys.platform", new="linux")
def socket_file_sideeff(path: str) -> bool:
if path.endswith(".sock"):
return False
return DEFAULT
mocker.patch("os.path.exists", side_effect=socket_file_sideeff)
m = start_collector_mocks
siso_path = "siso_path"
project = "test-project"
endpoint = os.path.join("/tmp", "testuser", "siso", f"{project}.sock")
# Status: DEAD -> (Start) -> Loop 1..N (200)
status_responses = [(404, None)] + [(200, None)] * 20
config_responses = [(200, None)] * 20
_configure_http_responses(
mocker,
m["mock_conn"],
status_responses=status_responses,
config_responses=config_responses,
)
status_healthy = {"healthy": True, "status": "StatusOK"}
config_with_socket = {
"receivers": {
"otlp": {
"protocols": {
"grpc": {
"endpoint": endpoint
}
}
}
}
}
mock_json_loads = mocker.patch("siso.json.loads")
mock_json_loads.side_effect = itertools.cycle(
[status_healthy, config_with_socket])
env = {}
args = ["ninja", "--project", project]
res_env = siso._handle_collector(siso_path, args, env)
# Should fail to find socket file, so no address in env.
assert "SISO_COLLECTOR_ADDRESS" not in res_env
@pytest.mark.parametrize(
"file_exists, expected_exit",
[
(True, True),
(False, False),
],
)
def test_check_outdir(tmp_path: Path, mocker: Any, file_exists: bool,
expected_exit: bool) -> None:
out_dir = tmp_path / "out" / "Default"
out_dir.mkdir(parents=True)
if file_exists:
(out_dir / ".ninja_deps").touch()
mock_exit = mocker.patch("sys.exit")
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
siso.check_outdir(str(out_dir))
if expected_exit:
mock_exit.assert_called_once_with(1)
assert "contains Ninja state file" in mock_stderr.getvalue()
else:
mock_exit.assert_not_called()
@pytest.mark.parametrize(
"is_corp, expected_msg_part",
[
(True, "gclient custom vars is correct"),
(False, "backend_config/README.md"),
],
)
def test_main_backend_star_missing(siso_project_setup: None, tmp_path: Path,
mocker: Any, is_corp: bool,
expected_msg_part: str) -> None:
# Setup project structure
backend_config_dir = _get_siso_config_dir(tmp_path) / "backend_config"
backend_config_dir.mkdir()
# Ensure backend.star does NOT exist
mocker.patch("siso._is_google_corp_machine", return_value=is_corp)
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
exit_code = siso.main(["siso.py", "ninja", "-C", "out/Default"])
assert exit_code == 1
assert expected_msg_part in mock_stderr.getvalue()
def test_main_siso_binary_missing(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
# Remove created siso.
siso_bin_path = _get_siso_bin_path(tmp_path)
siso_bin_path.unlink()
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
exit_code = siso.main(["siso.py", "ninja", "-C", "out/Default"])
assert exit_code == 1
assert "Could not find siso in third_party/siso" in mock_stderr.getvalue()
def test_main_siso_override_path_missing(siso_project_setup: None,
tmp_path: Path, mocker: Any) -> None:
# Setup project structure (minimal needed to reach the check)
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
# Set SISO_PATH to a non-existent file
env = {"SISO_PATH": str(tmp_path / "non_existent_siso")}
exit_code = siso.main(["siso.py", "ninja", "-C", "out/Default"], env=env)
assert exit_code == 1
assert "Could not find Siso at provided SISO_PATH" in mock_stderr.getvalue()
def test_main_sisoenv_missing(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
# Setup project structure
# Do NOT create .sisoenv
sisoenv_file = _get_sisoenv_path(tmp_path)
sisoenv_file.unlink()
mock_stderr = mocker.patch("sys.stderr", new_callable=io.StringIO)
exit_code = siso.main(["siso.py", "ninja", "-C", "out/Default"])
assert exit_code == 1
assert "Could not find .sisoenv" in mock_stderr.getvalue()
@pytest.mark.parametrize(
"env, expected_val",
[
({}, "1"),
({
"PYTHONDONTWRITEBYTECODE": "0"
}, "0"),
({
"PYTHONPYCACHEPREFIX": "/tmp/pycache"
}, None),
({
"PYTHONPYCACHEPREFIX": "/tmp/pycache",
"PYTHONDONTWRITEBYTECODE": "0"
}, "0"),
],
)
def test_env_python_dont_write_bytecode(siso_project_setup: None, mocker: Any,
env: Dict[str, str],
expected_val: Optional[str]) -> None:
runner = mocker.Mock(return_value=0)
siso.main(["siso.py", "ninja"], env=env, runner=runner)
assert runner.called
call_env = runner.call_args[1]["env"]
if expected_val is None:
assert "PYTHONDONTWRITEBYTECODE" not in call_env
else:
assert call_env.get("PYTHONDONTWRITEBYTECODE") == expected_val
def test_main_fallback_to_siso_path(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
# Setup: valid solution path but NO .sisoenv
sisoenv_file = _get_sisoenv_path(tmp_path)
sisoenv_file.unlink()
# Setup valid SISO_PATH
siso_bin = tmp_path / "custom_siso"
siso_bin.touch()
os.chmod(siso_bin, 0o755)
runner = mocker.Mock(return_value=0)
env = {"SISO_PATH": str(siso_bin)}
exit_code = siso.main(["siso.py", "ninja", "-C", "out/Default"],
env=env,
runner=runner)
assert exit_code == 0
# Verify runner called with override path
cmd = runner.call_args[0][0]
assert cmd[0] == str(siso_bin)
@pytest.mark.skipif(sys.platform != "win32", reason="Only for Windows")
def test_main_windows_arg_splitting(mocker: Any) -> None:
mocker.patch("siso.signal.signal")
# Mock internals to bypass file checks
mocker.patch("os.path.isfile", return_value=True) # For SISO_PATH check
runner = mocker.Mock(return_value=0)
env = {"SISO_PATH": "C:\\siso.exe"}
# Pass a single string argument simulating siso.bat behavior
# "siso.py" is args[0], "ninja -C out/Default" is args[1]
args = ["siso.py", "ninja -C out/Default"]
# As the env is incomplete on windows send a mocked false telemetry.
telemetry_cfg = mocker.Mock()
telemetry_cfg.enabled.return_value = False
siso.main(args, env=env, runner=runner, telemetry_cfg=telemetry_cfg)
# Verify args were split
cmd = runner.call_args[0][0]
assert cmd == ["C:\\siso.exe", "ninja", "-C", "out/Default"]
def test_main_e2e(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
# siso binary, already set up.
siso_bin = _get_siso_bin_path(tmp_path)
runner = mocker.Mock(return_value=0)
args = ["siso.py", "ninja", "-C", str(tmp_path)]
# run
exit_code = siso.main(args, runner=runner)
assert exit_code == 0
# Verify runner was called with siso binary and processed args.
called_args, _ = runner.call_args
cmd = called_args[0]
assert cmd[0] == str(siso_bin)
assert "ninja" in cmd
assert "-C" in cmd
assert str(tmp_path) in cmd
assert any(arg.startswith("--metrics_labels") for arg in cmd)
def test_main_dynamic_subcmds_e2e(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
siso_bin = _get_siso_bin_path(tmp_path)
runner = mocker.Mock(return_value=0)
# Mock _get_siso_subcmds to return dynamic subcmds including a custom one
mocker.patch("siso._get_siso_subcmds",
return_value={"custom_subcmd", "ninja"})
args = ["siso.py", "-mutexprofile", "prof", "custom_subcmd", "-flag"]
exit_code = siso.main(args, runner=runner)
assert exit_code == 0
called_args, _ = runner.call_args
cmd = called_args[0]
assert cmd[0] == str(siso_bin)
# Check that custom_subcmd was correctly identified
assert "custom_subcmd" in cmd
custom_idx = cmd.index("custom_subcmd")
# -mutexprofile and prof should be before custom_subcmd
assert cmd[custom_idx - 2] == "-mutexprofile"
assert cmd[custom_idx - 1] == "prof"
assert cmd[custom_idx + 1] == "-flag"
def test_main_complex_args_e2e(siso_project_setup: None, tmp_path: Path,
mocker: Any) -> None:
siso_bin = _get_siso_bin_path(tmp_path)
runner = mocker.Mock(return_value=0)
# Complex command similar to the reported issue.
args = [
"siso.py", "-mutexprofile", "siso_mutex.prof", "ninja", "-project",
"rbe-chrome-untrusted", "--enable_cloud_logging", "-C",
str(tmp_path / "src" / "out" / "Default")
]
exit_code = siso.main(args, runner=runner)
assert exit_code == 0
called_args, _ = runner.call_args
cmd = called_args[0]
assert cmd[0] == str(siso_bin)
# Subcommand should be "ninja" at index 3 (after -mutexprofile prof)
assert cmd[3] == "ninja"
# Pre-subcommand args should be preserved
assert cmd[1] == "-mutexprofile"
assert cmd[2] == "siso_mutex.prof"
# Telemetry flags should be AFTER ninja
ninja_idx = cmd.index("ninja")
assert any(arg.startswith("--metrics_labels") for arg in cmd[ninja_idx:])
assert any(
arg.startswith("--metrics_project=rbe-chrome-untrusted")
for arg in cmd[ninja_idx:])
# Stanza to have pytest be executed.
if __name__ == "__main__":
sys.exit(pytest.main([__file__] + sys.argv[1:]))