Archived
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
98 lines
4.7 KiB
Python
98 lines
4.7 KiB
Python
"""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))
|