This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
platform-ci/tests/test_execution.py

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))