"""Command execution with subprocess stubs: verification runs once per event, tokens stay tokens, secrets stay out.""" import json import os import sys import tempfile import unittest from collections.abc import Mapping, Sequence from pathlib import Path from typing import override from platform_ci.execution import ExecutionError, run_process, run_target_commands from platform_ci.plan import EventContext, Target, build_matrix from tests.support import event, example_configuration, plan_for RUNNER_HOME = str(Path(tempfile.gettempdir()) / "runner-home") RUNNER_ENVIRONMENT = { "PATH": "/usr/bin", "HOME": RUNNER_HOME, "ACTIONS_RUNTIME_TOKEN": "runner-secret", "GITEA_TOKEN": "release-secret", } class RecordingRunner: """A command stub that records every invocation and returns scripted exit codes.""" def __init__(self, exit_codes: Sequence[int] = ()) -> None: """Create a stub; commands beyond the scripted exit codes succeed.""" self.exit_codes = list(exit_codes) self.commands: list[list[str]] = [] self.environments: list[Mapping[str, str]] = [] def __call__(self, command: Sequence[str], working_directory: Path, environment: Mapping[str, str]) -> int: """Record the invocation and return the next scripted exit code.""" self.commands.append(list(command)) self.environments.append(environment) return self.exit_codes.pop(0) if self.exit_codes else 0 class CommandExecutionTest(unittest.TestCase): """Planned commands run through the stub exactly as planned.""" @override def setUp(self) -> None: """Create an empty source checkout.""" directory = tempfile.TemporaryDirectory() self.addCleanup(directory.cleanup) self.source = Path(directory.name) def _run_pipeline(self, example: str, context_event: EventContext) -> list[list[str]]: """Simulate what the workflow does for one event: verify every matrix entry once; nothing else builds.""" plan = plan_for(example_configuration(example), context_event) runner = RecordingRunner() for entry in build_matrix(plan)["include"]: target: Target = json.loads(entry["target"]) self.assertTrue(run_target_commands(target, self.source, runner, RUNNER_ENVIRONMENT)) return runner.commands def test_verification_runs_exactly_once_for_pull_request_push_and_release_eligible_push(self) -> None: """The native build command appears once per event, including a push that is release-eligible.""" for description, context in { "pull request": event("pull_request", "refs/pull/7/merge"), "default branch push": event("push", "refs/heads/main"), "release-eligible push": event("push", "refs/heads/develop"), "feature branch push": event("push", "refs/heads/feature-x"), "tag push": event("push", "refs/tags/v1.0.0"), }.items(): with self.subTest(description): self.assertEqual(self._run_pipeline("maven.ci.yaml", context), [["mvn", "--batch-mode", "verify"]]) def test_node_runs_install_then_declared_command_once(self) -> None: """Node runs `npm ci` and the declared script, each once.""" commands = self._run_pipeline("node.ci.yaml", event("push", "refs/heads/main")) self.assertEqual(commands, [["npm", "ci"], ["npm", "run", "verify"]]) def test_first_failure_stops_the_target(self) -> None: """A failed install means the build command never runs.""" target = plan_for(example_configuration("node.ci.yaml"), event())["targets"][0] runner = RecordingRunner([1]) self.assertFalse(run_target_commands(target, self.source, runner, RUNNER_ENVIRONMENT)) self.assertEqual(runner.commands, [["npm", "ci"]]) def test_arguments_stay_separate_tokens_and_credentials_are_removed(self) -> None: """Shell metacharacters are not interpreted, and runner or release tokens never reach build code.""" target = plan_for(example_configuration("maven.ci.yaml"), event())["targets"][0] target["commands"] = [["mvn", "verify; echo owned", "$(id)"]] runner = RecordingRunner() run_target_commands(target, self.source, runner, RUNNER_ENVIRONMENT) self.assertEqual(runner.commands, [["mvn", "verify; echo owned", "$(id)"]]) self.assertEqual(runner.environments[0], {"PATH": "/usr/bin", "HOME": RUNNER_HOME}) def test_real_process_exit_code_and_missing_executable(self) -> None: """The default runner reports a child's exit code and fails visibly when a tool is absent.""" self.assertEqual(run_process([sys.executable, "-c", "raise SystemExit(3)"], self.source, dict(os.environ)), 3) with self.assertRaises(ExecutionError): run_process(["definitely-not-an-installed-build-tool"], self.source, dict(os.environ))