Merge branch 'main' into fix/feedback-terminal-links-20260923
This commit is contained in:
26 files changed
+1643
-136
No files matched your search
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Plan the native CI matrices before GitHub creates runner jobs.
|
||||
|
||||
The output file is the path normally supplied as ``GITHUB_OUTPUT``. Planning
|
||||
is deliberately separate from workflow steps: a draft pull request therefore
|
||||
does not materialize jobs for the scarce platforms.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Platform:
|
||||
runner: str
|
||||
draft: bool
|
||||
release_check: bool = False
|
||||
|
||||
|
||||
# One platform catalog owns both draft eligibility and release compilation.
|
||||
PLATFORMS = (
|
||||
Platform("ubuntu-24.04", draft=True),
|
||||
Platform("windows-2022", draft=True),
|
||||
Platform("macos-26", draft=True, release_check=True),
|
||||
Platform("windows-11-arm", draft=False),
|
||||
Platform("macos-26-intel", draft=False, release_check=True),
|
||||
)
|
||||
|
||||
FULL_EVENTS = frozenset(
|
||||
{"push", "merge_group", "workflow_dispatch", "workflow_call", "schedule"}
|
||||
)
|
||||
|
||||
|
||||
class PlanError(ValueError):
|
||||
"""An event cannot be converted into a safe CI plan."""
|
||||
|
||||
|
||||
def _matrices(draft: bool) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||
selected = [platform for platform in PLATFORMS if not draft or platform.draft]
|
||||
return (
|
||||
[{"os": platform.runner} for platform in selected],
|
||||
[{"os": platform.runner} for platform in selected if platform.release_check],
|
||||
)
|
||||
|
||||
|
||||
def _reject_json_constant(value: str) -> None:
|
||||
raise ValueError(f"non-standard JSON constant {value}")
|
||||
|
||||
|
||||
def _read_event(path: Path) -> dict[str, Any]:
|
||||
try:
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise PlanError(f"cannot read event payload {path}: {exc}") from exc
|
||||
|
||||
try:
|
||||
payload = json.loads(raw, parse_constant=_reject_json_constant)
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
raise PlanError(f"invalid event JSON in {path}: {exc}") from exc
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise PlanError("event payload must be a JSON object")
|
||||
return payload
|
||||
|
||||
|
||||
def plan_matrices(event_name: str, payload: dict[str, Any]) -> tuple[list[dict[str, str]], list[dict[str, str]]]:
|
||||
"""Return native and release matrices for one validated event."""
|
||||
|
||||
if event_name == "pull_request":
|
||||
pull_request = payload.get("pull_request")
|
||||
if not isinstance(pull_request, dict):
|
||||
raise PlanError("pull_request event must contain a pull_request object")
|
||||
draft = pull_request.get("draft")
|
||||
if not isinstance(draft, bool):
|
||||
raise PlanError("pull_request.draft must be a boolean")
|
||||
return _matrices(draft)
|
||||
|
||||
if event_name in FULL_EVENTS:
|
||||
return _matrices(draft=False)
|
||||
|
||||
supported = sorted(FULL_EVENTS | {"pull_request"})
|
||||
raise PlanError(f"unsupported event name {event_name!r}; expected one of {', '.join(supported)}")
|
||||
|
||||
|
||||
def _compact_output(name: str, value: list[dict[str, str]]) -> str:
|
||||
return f"{name}={json.dumps(value, separators=(',', ':'), ensure_ascii=True)}"
|
||||
|
||||
|
||||
def write_outputs(path: Path, native_matrix: list[dict[str, str]], release_matrix: list[dict[str, str]]) -> None:
|
||||
"""Append both outputs in one write after planning has fully succeeded."""
|
||||
|
||||
content = "\n".join(
|
||||
(
|
||||
_compact_output("native_matrix", native_matrix),
|
||||
_compact_output("release_matrix", release_matrix),
|
||||
)
|
||||
) + "\n"
|
||||
try:
|
||||
with path.open("a", encoding="utf-8", newline="\n") as output:
|
||||
output.write(content)
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise PlanError(f"cannot append CI plan to {path}: {exc}") from exc
|
||||
|
||||
|
||||
def _arguments(argv: Sequence[str] | None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--event-name", required=True)
|
||||
parser.add_argument("--event-path", required=True, type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
arguments = _arguments(argv)
|
||||
try:
|
||||
payload = _read_event(arguments.event_path)
|
||||
native_matrix, release_matrix = plan_matrices(arguments.event_name, payload)
|
||||
write_outputs(arguments.output, native_matrix, release_matrix)
|
||||
except PlanError as exc:
|
||||
print(f"ci_plan: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+5
-1
@@ -17,7 +17,11 @@ class UpdateProcess {
|
||||
if (!File.Exists(Path.Combine(directory, "parent-exited"))) return 3;
|
||||
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_FAIL_INSTALL") == "1") return 17;
|
||||
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_NOOP_INSTALL") == "1") return 0;
|
||||
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
|
||||
try {
|
||||
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
|
||||
Directory.CreateDirectory(Path.Combine(target, "runtime"));
|
||||
File.WriteAllText(Path.Combine(target, "runtime", "pebrel-hook.exe"), "repaired helper fixture");
|
||||
} catch (IOException) { return 5; }
|
||||
return 0;
|
||||
#else
|
||||
if (args.Length > 0 && args[0] == "--version") {
|
||||
|
||||
@@ -11,6 +11,48 @@ from scripts.ci_native_tests import main, native_commands
|
||||
|
||||
|
||||
class NativeSuiteTests(unittest.TestCase):
|
||||
def test_required_lint_plans_runners_before_native_jobs(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
|
||||
lint = workflow.split("\n lint:\n", 1)[1].split("\n native-tests:\n", 1)[0]
|
||||
self.assertIn("name: lint", lint)
|
||||
self.assertIn("python scripts/ci_plan.py", lint)
|
||||
self.assertIn('--event-path "$GITHUB_EVENT_PATH"', lint)
|
||||
self.assertLess(lint.index("cargo fmt"), lint.index("python scripts/ci_plan.py"))
|
||||
for job, output in (("native-tests", "native_matrix"),
|
||||
("macos-release-check", "release_matrix")):
|
||||
body = workflow.split(f"\n {job}:\n", 1)[1]
|
||||
body = re.split(r"\n [a-z][a-z-]*:\n", body, maxsplit=1)[0]
|
||||
self.assertIn("needs: lint", body)
|
||||
self.assertIn(f"fromJSON(needs.lint.outputs.{output})", body)
|
||||
self.assertNotIn("pull_request.draft", body)
|
||||
self.assertNotIn("matrix.tier", body)
|
||||
self.assertIn("cargo check --locked --workspace --release", workflow)
|
||||
# Native validation never needs to retain checkout credentials.
|
||||
checkouts = re.findall(r"- uses: actions/checkout@[^\n]+\n(.*?)(?= - |\Z)",
|
||||
workflow, re.S)
|
||||
self.assertTrue(checkouts)
|
||||
for checkout in checkouts:
|
||||
self.assertIn("persist-credentials: false", checkout)
|
||||
|
||||
def test_packages_run_after_merge_or_manual_dispatch_not_for_prs(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/preview-packages.yml").read_text()
|
||||
events = workflow.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
|
||||
triggers = set(re.findall(r"^ ([a-z_]+):", events, re.M))
|
||||
self.assertEqual(triggers, {"push", "workflow_dispatch"})
|
||||
push = re.search(r"^ push:(.*?)(?=^ [a-z_]+:|\Z)", events, re.M | re.S)
|
||||
self.assertIsNotNone(push)
|
||||
self.assertIn("branches: [main]", push.group(1))
|
||||
self.assertIn("paths:", push.group(1))
|
||||
# Packaging still needs explicit dispatch to create a public release.
|
||||
self.assertIn("github.event_name == 'workflow_dispatch' && inputs.publish == true", workflow)
|
||||
stable = (root / ".github/workflows/release.yml").read_text()
|
||||
stable_events = stable.split("\non:\n", 1)[1].split("\nconcurrency:", 1)[0]
|
||||
self.assertEqual(set(re.findall(r"^ ([a-z_]+):", stable_events, re.M)),
|
||||
{"push", "workflow_dispatch"})
|
||||
self.assertIn('tags: ["v*.*.*"]', stable_events)
|
||||
|
||||
def test_every_pr_and_merge_group_runs_without_path_exclusions(self):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
workflow = (root / ".github/workflows/linux-lua.yml").read_text()
|
||||
@@ -20,7 +62,12 @@ class NativeSuiteTests(unittest.TestCase):
|
||||
self.assertIsNotNone(declaration)
|
||||
self.assertNotIn("paths", declaration.group(1))
|
||||
self.assertNotIn("branches", declaration.group(1))
|
||||
self.assertNotIn("types", declaration.group(1))
|
||||
if event == "pull_request":
|
||||
# ready_for_review starts the scarce-runner jobs that draft
|
||||
# pushes skip; the default activity types must stay listed.
|
||||
self.assertIn("types: [opened, synchronize, reopened, ready_for_review]", declaration.group(1))
|
||||
else:
|
||||
self.assertNotIn("types", declaration.group(1))
|
||||
self.assertIn("branches: [main]", events)
|
||||
self.assertNotIn("branches-ignore", events)
|
||||
self.assertNotIn("pull_request_target", workflow)
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "ci_plan.py"
|
||||
|
||||
|
||||
class CiPlanCliTests(unittest.TestCase):
|
||||
def run_cli(self, event_name: str, payload: object, output: Path) -> subprocess.CompletedProcess[str]:
|
||||
event_path = output.parent / "event.json"
|
||||
event_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
return subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--event-name",
|
||||
event_name,
|
||||
"--event-path",
|
||||
str(event_path),
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def outputs(path: Path) -> dict[str, object]:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
return {name: json.loads(value) for name, value in (line.split("=", 1) for line in lines)}
|
||||
|
||||
def test_draft_pr_does_not_materialize_scarce_runner_rows(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "github-output"
|
||||
result = self.run_cli("pull_request", {"pull_request": {"draft": True}}, output)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
output.read_text(encoding="utf-8"),
|
||||
'native_matrix=[{"os":"ubuntu-24.04"},{"os":"windows-2022"},{"os":"macos-26"}]\n'
|
||||
'release_matrix=[{"os":"macos-26"}]\n',
|
||||
)
|
||||
plan = self.outputs(output)
|
||||
self.assertNotIn({"os": "windows-11-arm"}, plan["native_matrix"])
|
||||
self.assertNotIn({"os": "macos-26-intel"}, plan["native_matrix"])
|
||||
|
||||
def test_ready_pr_has_all_native_and_release_checks(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "github-output"
|
||||
result = self.run_cli("pull_request", {"pull_request": {"draft": False}}, output)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
plan = self.outputs(output)
|
||||
self.assertEqual(
|
||||
[row["os"] for row in plan["native_matrix"]],
|
||||
["ubuntu-24.04", "windows-2022", "macos-26", "windows-11-arm", "macos-26-intel"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[row["os"] for row in plan["release_matrix"]],
|
||||
["macos-26", "macos-26-intel"],
|
||||
)
|
||||
|
||||
def test_success_appends_outputs_without_replacing_existing_lines(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
output = Path(directory) / "github-output"
|
||||
output.write_text("existing=preserved\n", encoding="utf-8")
|
||||
result = self.run_cli("push", {}, output)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
lines = output.read_text(encoding="utf-8").splitlines()
|
||||
self.assertEqual(lines[0], "existing=preserved")
|
||||
self.assertTrue(lines[1].startswith("native_matrix=[{"))
|
||||
self.assertTrue(lines[2].startswith("release_matrix=[{"))
|
||||
|
||||
def test_merge_group_and_non_pr_events_have_full_coverage(self) -> None:
|
||||
events = ("merge_group", "push", "workflow_dispatch", "workflow_call", "schedule")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for event_name in events:
|
||||
with self.subTest(event_name=event_name):
|
||||
output = Path(directory) / f"{event_name}.out"
|
||||
result = self.run_cli(event_name, {}, output)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
plan = self.outputs(output)
|
||||
self.assertEqual(
|
||||
[row["os"] for row in plan["native_matrix"]],
|
||||
["ubuntu-24.04", "windows-2022", "macos-26", "windows-11-arm", "macos-26-intel"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[row["os"] for row in plan["release_matrix"]],
|
||||
["macos-26", "macos-26-intel"],
|
||||
)
|
||||
|
||||
def test_invalid_events_fail_without_matrix_outputs(self) -> None:
|
||||
cases = (
|
||||
("pull_request", {}, "missing pull request"),
|
||||
("pull_request", {"pull_request": []}, "wrong pull request type"),
|
||||
("pull_request", {"pull_request": {}}, "missing draft"),
|
||||
("pull_request", {"pull_request": {"draft": "false"}}, "wrong draft type"),
|
||||
("pull_request", {"pull_request": {"draft": 0}}, "integer draft type"),
|
||||
("unknown", {}, "unknown event"),
|
||||
("push", [], "non-object payload"),
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
for event_name, payload, label in cases:
|
||||
with self.subTest(label=label):
|
||||
output = Path(directory) / f"{label.replace(' ', '-')}.out"
|
||||
output.write_text("existing=preserved\n", encoding="utf-8")
|
||||
result = self.run_cli(event_name, payload, output)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("ci_plan:", result.stderr)
|
||||
self.assertEqual(output.read_text(encoding="utf-8"), "existing=preserved\n")
|
||||
|
||||
def test_malformed_json_fails_without_creating_output(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
event_path = root / "broken.json"
|
||||
output = root / "github-output"
|
||||
event_path.write_text('{"pull_request":', encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--event-name",
|
||||
"pull_request",
|
||||
"--event-path",
|
||||
str(event_path),
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(output.exists())
|
||||
|
||||
def test_non_standard_json_constant_is_rejected(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
event_path = root / "non-standard.json"
|
||||
output = root / "github-output"
|
||||
event_path.write_text('{"value":NaN}', encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(SCRIPT),
|
||||
"--event-name",
|
||||
"push",
|
||||
"--event-path",
|
||||
str(event_path),
|
||||
"--output",
|
||||
str(output),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertFalse(output.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -16,6 +16,7 @@ from scripts.stable_release import (
|
||||
)
|
||||
from scripts.preview_release import sha256
|
||||
from scripts.preview_release import MIN_ASSET_SIZE
|
||||
from scripts.ci_plan import plan_matrices
|
||||
|
||||
|
||||
VERSION = "1.6.0"
|
||||
@@ -78,8 +79,21 @@ class StableReleaseTests(unittest.TestCase):
|
||||
self.assertNotIn(" native-tests:\n", workflow)
|
||||
self.assertNotIn("uses: ./.github/workflows/linux-lua.yml", workflow)
|
||||
shared = (root / ".github/workflows/linux-lua.yml").read_text(encoding="utf-8")
|
||||
for platform in ("ubuntu-24.04", "windows-2022", "windows-11-arm", "macos-26", "macos-26-intel"):
|
||||
self.assertIn(platform, shared)
|
||||
# Platform coverage comes from the event plan, not literal runner names
|
||||
# in YAML. Verify both the consumer wiring and the full caller matrices.
|
||||
for output in ("native_matrix", "release_matrix"):
|
||||
self.assertIn(f"fromJSON(needs.lint.outputs.{output})", shared)
|
||||
self.assertIn("runs-on: ${{ matrix.os }}", shared)
|
||||
for event in ("push", "merge_group", "workflow_call", "workflow_dispatch"):
|
||||
with self.subTest(event=event):
|
||||
native, release = plan_matrices(event, {})
|
||||
self.assertCountEqual(
|
||||
[row["os"] for row in native],
|
||||
["ubuntu-24.04", "windows-2022", "windows-11-arm", "macos-26", "macos-26-intel"],
|
||||
)
|
||||
self.assertCountEqual(
|
||||
[row["os"] for row in release], ["macos-26", "macos-26-intel"],
|
||||
)
|
||||
for trigger in ("pull_request:", "merge_group:", "branches: [main]"):
|
||||
self.assertIn(trigger, shared)
|
||||
self.assertIn("run: python scripts/ci_native_tests.py", shared)
|
||||
|
||||
@@ -36,7 +36,7 @@ function Write-Json([string]$Path, $Value) {
|
||||
}
|
||||
|
||||
$results = @()
|
||||
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'reinstall-noop')) {
|
||||
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
|
||||
$directory = Join-Path $OutputRoot $scenario
|
||||
$installation = Join-Path $directory 'installed app'
|
||||
$transaction = Join-Path $directory 'transaction'
|
||||
@@ -44,17 +44,25 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
$null = New-Item -ItemType Directory -Path $installation, $transaction, $config -Force
|
||||
$executable = Join-Path $installation 'pebrel.exe'
|
||||
Copy-Item -LiteralPath $old -Destination $executable
|
||||
$payload = if ($scenario -eq 'reinstall') { $reinstall } else { $candidate }
|
||||
$payload = if ($scenario -eq 'reinstall') { $reinstall } elseif ($scenario -eq 'repair-identical') { $old } else { $candidate }
|
||||
Copy-Item -LiteralPath $payload -Destination (Join-Path $directory 'candidate.exe')
|
||||
[System.IO.File]::WriteAllText((Join-Path $installation 'unins000.exe'), 'fixture marker')
|
||||
$env:PEBREL_HANDOFF_FIXTURE = $directory
|
||||
$env:PEBREL_HANDOFF_FAIL_INSTALL = if ($scenario -eq 'installer-failure') { '1' } else { '0' }
|
||||
$env:PEBREL_HANDOFF_NOOP_INSTALL = if ($scenario -eq 'reinstall-noop') { '1' } else { '0' }
|
||||
$env:PEBREL_HANDOFF_NOOP_INSTALL = if ($scenario -eq 'upgrade-noop') { '1' } else { '0' }
|
||||
$parent = Start-Process -FilePath $executable -ArgumentList 'wait' -PassThru
|
||||
$null = $parent.Handle
|
||||
$runner = $null
|
||||
$other = $null
|
||||
$helperLock = $null
|
||||
try {
|
||||
if ($scenario -in @('helper-unlocked', 'helper-held')) {
|
||||
$null = New-Item -ItemType Directory -Path (Join-Path $installation 'runtime') -Force
|
||||
$helperPath = Join-Path $installation 'runtime\pebrel-hook.exe'
|
||||
[System.IO.File]::WriteAllText($helperPath, 'old helper fixture')
|
||||
$helperLock = [System.IO.FileStream]::new($helperPath, [System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
|
||||
}
|
||||
$plan = @{
|
||||
schema = 1; transaction = $scenario; executable = $executable; installation = $installation
|
||||
config_directory = $config; installer = $installer
|
||||
@@ -64,7 +72,7 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
participants = @(@{ pid = $parent.Id; created = $parent.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() })
|
||||
}
|
||||
if ($scenario -eq 'checksum') { $plan.sha256 = '0' * 64 }
|
||||
if ($scenario -in @('reinstall', 'reinstall-noop')) { $plan.version = '1.8.0' }
|
||||
if ($scenario -in @('reinstall', 'repair-identical')) { $plan.version = '1.8.0' }
|
||||
if ($scenario -eq 'creation-time') { $plan.participants[0].created = '1' }
|
||||
if ($scenario -eq 'other-process') {
|
||||
$other = Start-Process -FilePath $executable -ArgumentList 'wait-other' -PassThru
|
||||
@@ -104,8 +112,14 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran while the old process was alive'
|
||||
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
|
||||
Assert ($parent.WaitForExit(5000)) 'Fixture parent did not exit'
|
||||
if ($scenario -eq 'helper-unlocked') {
|
||||
Start-Sleep -Milliseconds 350
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup started with the helper still locked'
|
||||
$helperLock.Dispose()
|
||||
$helperLock = $null
|
||||
}
|
||||
Wait-File $resultPath
|
||||
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'reinstall-noop')) {
|
||||
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
|
||||
Wait-File (Join-Path $directory 'new-launch')
|
||||
$launch = [System.IO.File]::ReadAllLines((Join-Path $directory 'new-launch'))
|
||||
Assert ($launch[0] -eq $executable) 'Relaunch selected another installation'
|
||||
@@ -117,24 +131,31 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
}
|
||||
Assert ($runner.WaitForExit(5000)) 'Helper did not finish'
|
||||
$result = Get-Content -LiteralPath $resultPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Assert ($result.success -eq ($scenario -in @('success', 'reinstall'))) 'Unexpected helper outcome'
|
||||
Assert ($result.success -eq ($scenario -in @('success', 'reinstall', 'repair-identical', 'helper-unlocked'))) 'Unexpected helper outcome'
|
||||
if ($result.success) {
|
||||
$expected = (Get-FileHash -LiteralPath $payload -Algorithm SHA256).Hash
|
||||
Assert ($result.executable_sha256 -eq $expected) 'Installed binary differs from candidate'
|
||||
Assert (Test-Path -LiteralPath (Join-Path $installation 'runtime\pebrel-hook.exe')) 'Successful setup did not repair the helper'
|
||||
Assert ([System.IO.File]::ReadAllText((Join-Path $installation 'runtime\pebrel-hook.exe')) -eq 'repaired helper fixture') 'Helper still contains the original payload'
|
||||
}
|
||||
if ($scenario -in @('other-process', 'late-process')) {
|
||||
Assert (-not $other.HasExited) 'Update stopped an unprepared process'
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran with an unprepared process'
|
||||
}
|
||||
if ($scenario -in @('installer-failure', 'reinstall-noop')) {
|
||||
if ($scenario -in @('installer-failure', 'upgrade-noop', 'helper-held')) {
|
||||
Assert $result.recovered_original 'Untouched old executable was not recovered'
|
||||
}
|
||||
if ($scenario -eq 'helper-held') {
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'A blocked helper allowed a partial installation'
|
||||
Assert ($result.error -match 'pebrel-hook.exe') 'The blocked file is missing from the failure details'
|
||||
}
|
||||
$probe = [System.IO.FileStream]::new($plan.guard_path, [System.IO.FileMode]::OpenOrCreate,
|
||||
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
||||
$probe.Dispose()
|
||||
$results += @{ scenario = $scenario; passed = $true }
|
||||
Write-Output "$scenario : passed"
|
||||
} finally {
|
||||
if ($helperLock) { $helperLock.Dispose() }
|
||||
if ($runner -and -not $runner.HasExited) { $runner.Kill(); $runner.WaitForExit() }
|
||||
if (-not $parent.HasExited) {
|
||||
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
|
||||
|
||||
Reference in new issue
Block a user