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_source_lifecycle.py
forge-recoveryandClaude Sonnet 5.5 a36f17b857
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 6s
feat(ci): advance shared pipeline capabilities
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 10:30:25 +00:00

429 lines
20 KiB
Python

"""The source-lifecycle write phase: the captured revision rule, the confined writer, the staged version write and the evidence it waits for.
NOT YET RUN: written while test execution was suspended. Forge and Git are fakes of the captured helper's calls; nothing contacts a forge, signs or pushes.
"""
import base64
import hashlib
import io
import json
import unittest
import urllib.error
from dataclasses import dataclass
from email.message import Message
from types import SimpleNamespace
from typing import Any, override
from unittest.mock import patch
from platform_ci.configuration import WriteBackPolicy
from platform_ci.delivery import completion_receipt
from platform_ci.delivery.maven_reactor import build_reactor
from platform_ci.delivery.source_lifecycle import (
Fatal,
NotYetPublished,
PushFailure,
SourceLifecycleError,
SourceWriter,
VersionGit,
VersionPlan,
WriteContext,
build_plan,
git_blob_sha,
persist_version,
postpublish,
revision_only_change,
rewrite_revision,
verify_bump_commit,
verify_completion,
verify_publication,
)
SRC = "a" * 40
CHILD = "b" * 40
OLD = "<project><properties><revision>1.2.0-SNAPSHOT</revision></properties></project>\n"
NEW = OLD.replace("1.2.0-SNAPSHOT", "1.2.0-RC.3")
CTX = WriteContext("O/r", SRC, "release/1.2", 41, 1)
class RevisionRuleTest(unittest.TestCase):
"""The captured `revisionOnlyChange`: only the one root `properties/revision` value may differ, byte for byte."""
def test_the_one_value_change_is_accepted_and_reported(self) -> None:
self.assertEqual(revision_only_change(OLD, NEW), ("1.2.0-SNAPSHOT", "1.2.0-RC.3"))
self.assertEqual(rewrite_revision(OLD, "1.2.0-RC.3"), NEW)
self.assertEqual(revision_only_change(OLD, OLD), ("1.2.0-SNAPSHOT", "1.2.0-SNAPSHOT"))
def test_everything_else_is_refused(self) -> None:
two = OLD.replace("</properties>", "<revision>x</revision></properties>")
managed = "<project><dependencyManagement><revision>1</revision></dependencyManagement><properties></properties></project>"
cases = {
"two revisions": (two, two),
"revision outside root properties": (managed, managed),
"another edit": (OLD, NEW.replace("<project>", "<project >")),
"unsafe value": (OLD, NEW.replace("RC.3", "RC 3")),
"doctype": ("<!DOCTYPE x>" + OLD, "<!DOCTYPE x>" + NEW),
"none": ("<project/>", "<project/>"),
}
for name, (old, new) in cases.items():
with self.subTest(name), self.assertRaises(SourceLifecycleError):
revision_only_change(old, new)
def test_rewrite_changes_only_the_value(self) -> None:
with self.assertRaises(SourceLifecycleError):
rewrite_revision(OLD, "bad value")
self.assertEqual(git_blob_sha(b"x"), hashlib.sha1(b"blob 1\0x").hexdigest()) # noqa: S324
class PlanTest(unittest.TestCase):
"""The plan is bound to the source and to the version the verified bundle recorded."""
policy = WriteBackPolicy()
def test_a_changed_version_plans_and_an_unchanged_one_does_not(self) -> None:
plan = build_plan(CTX, self.policy, old=OLD.encode(), new=NEW.encode(), verified_version="1.2.0-RC.3")
assert plan is not None
self.assertEqual((plan.declared, plan.effective, plan.source_blob, plan.source_commit), ("1.2.0-SNAPSHOT", "1.2.0-RC.3", git_blob_sha(OLD.encode()), SRC))
self.assertIsNone(build_plan(CTX, self.policy, old=OLD.encode(), new=OLD.encode(), verified_version="1.2.0-SNAPSHOT"))
def test_relabelling_oversize_and_an_unconfigured_tag_run_are_refused(self) -> None:
with self.assertRaises(Fatal):
build_plan(CTX, self.policy, old=OLD.encode(), new=NEW.encode(), verified_version="1.2.0-RC.4")
with self.assertRaises(Fatal):
build_plan(CTX, self.policy, old=OLD.encode() + b" " * 600000, new=NEW.encode(), verified_version="1.2.0-RC.3")
with self.assertRaises(Fatal):
build_plan(WriteContext("O/r", SRC, "main", 41, 1, tag="v1.2.0"), self.policy, old=OLD.encode(), new=NEW.encode(), verified_version="1.2.0-RC.3")
@dataclass(frozen=True)
class Faults:
"""The ways the captured checks can fail; the defaults are a clean forge."""
verified: bool = True
extra_file: bool = False
message: str | None = None
moved: bool = False
staged: bytes | None = None
parents: list[str] | None = None
class FakeForge:
"""The forge calls `persist_version` makes, with switches for each way the captured checks can fail."""
def __init__(self, faults: Faults | None = None) -> None:
self.faults = faults or Faults()
self.branches = {"release/1.2": "c" * 40 if self.faults.moved else SRC}
self.calls: list[tuple[str, str]] = []
def __call__(self, method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
self.calls.append((method, path))
tail = path.split("/repos/O/r", 1)[1]
answered = self._branch_or_write(method, tail, body)
if answered is not None:
return answered
return self._read(method, path, tail)
def _branch_or_write(self, method: str, tail: str, body: dict[str, Any] | None) -> tuple[int, Any] | None:
"""Answer the branch reads, the staging-branch creation, the staged write and the deletion; None for anything else."""
if method == "GET" and tail.startswith("/branches/"):
name = tail.removeprefix("/branches/").replace("%2F", "/")
return (200, {"commit": {"id": self.branches[name]}}) if name in self.branches else (404, None)
if method == "POST" and tail == "/branches":
assert body is not None
self.branches[body["new_branch_name"]] = body["old_ref_name"]
return 201, {}
if method == "PUT":
assert body is not None
self.branches[body["branch"]] = CHILD
return 200, {"commit": {"sha": CHILD}}
if method == "DELETE":
self.branches.pop(tail.removeprefix("/branches/").replace("%2F", "/"), None)
return 204, None
return None
def _read(self, method: str, path: str, tail: str) -> tuple[int, Any]:
"""Answer the reads of the staged commit, its server verification and its file bytes."""
if tail == f"/commits/{CHILD}":
return 200, {"files": [{"filename": "pom.xml"}] + ([{"filename": "other"}] if self.faults.extra_file else []), "parents": [{"sha": p} for p in (self.faults.parents or [SRC])]}
if tail == f"/git/commits/{CHILD}":
return 200, {"commit": {"verification": {"verified": self.faults.verified}, "message": self.faults.message or "Bump version to 1.2.0-RC.3 [skip ci]\n"}}
if tail.startswith("/contents/pom.xml?ref="):
return 200, {"content": base64.b64encode(self.faults.staged if self.faults.staged is not None else NEW.encode()).decode(), "sha": "x"}
raise AssertionError((method, path))
class FakeGit(VersionGit):
"""Pushes the verified child to the branch, or fails the way Git can; it never runs Git."""
def __init__(self, forge: FakeForge, *, reject: bool = False, silent: bool = False) -> None:
super().__init__("https://forge.invalid", "O/r", "unused-token")
self.forge, self.reject, self.silent = forge, reject, silent
self.pushed: list[tuple[str, str]] = []
@override
def push_verified(self, stage: str, child: str, source: str, branch: str) -> None:
if self.reject:
raise PushFailure("the version push was rejected")
self.pushed.append((child, branch))
if not self.silent:
self.forge.branches[branch] = child
def plan() -> VersionPlan:
built = build_plan(CTX, WriteBackPolicy(), old=OLD.encode(), new=NEW.encode(), verified_version="1.2.0-RC.3")
assert built is not None
return built
class PersistVersionTest(unittest.TestCase):
"""Staged, server-verified, normal fast-forward; any doubt is refused and only the exact owned staging heads are deleted."""
def test_the_happy_path_pushes_the_verified_child_and_removes_the_staging_branch(self) -> None:
forge = FakeForge()
git = FakeGit(forge)
self.assertEqual(persist_version(CTX, plan(), forge, git), CHILD)
self.assertEqual((forge.branches, git.pushed), ({"release/1.2": CHILD}, [(CHILD, "release/1.2")]))
self.assertIn(("DELETE", "/repos/O/r/branches/ci%2Fversion-stage-41-1"), forge.calls)
def test_untrustworthy_staging_is_fatal_and_still_cleaned_up(self) -> None:
for name, forge in {
"not server verified": FakeForge(Faults(verified=False)),
"more than one file": FakeForge(Faults(extra_file=True)),
"wrong message": FakeForge(Faults(message="Bump version to 9 [skip ci]")),
"wrong bytes": FakeForge(Faults(staged=b"other")),
"wrong parent": FakeForge(Faults(parents=["d" * 40])),
}.items():
with self.subTest(name), self.assertRaises(Fatal):
persist_version(CTX, plan(), forge, FakeGit(forge))
self.assertEqual(forge.branches, {"release/1.2": SRC}, name)
def test_races_rejections_and_an_unacknowledged_push_are_push_failures_that_leave_the_branch_alone(self) -> None:
moved = FakeForge(Faults(moved=True))
with self.assertRaises(PushFailure):
persist_version(CTX, plan(), moved, FakeGit(moved))
self.assertFalse(any(method == "POST" for method, _ in moved.calls), "no staging branch is created for a moved branch")
rejected = FakeForge()
with self.assertRaises(PushFailure):
persist_version(CTX, plan(), rejected, FakeGit(rejected, reject=True))
self.assertEqual(rejected.branches, {"release/1.2": SRC})
silent = FakeForge()
with self.assertRaises(PushFailure):
persist_version(CTX, plan(), silent, FakeGit(silent, silent=True))
def test_a_foreign_staging_head_is_never_deleted(self) -> None:
forge = FakeForge()
class Interfering(FakeGit):
@override
def push_verified(self, stage: str, child: str, source: str, branch: str) -> None:
forge.branches[stage] = "e" * 40 # someone else moved the staging branch
raise PushFailure("rejected")
with self.assertRaises(PushFailure):
persist_version(CTX, plan(), forge, Interfering(forge))
self.assertIn("ci/version-stage-41-1", forge.branches)
def test_a_tag_run_requires_the_tag_to_still_name_the_source(self) -> None:
forge = FakeForge()
tagged = WriteContext("O/r", SRC, "release/1.2", 41, 1, tag="v1.2.0")
def with_tags(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
return (200, {"commit": {"sha": "f" * 40}}) if "/tags/" in path else forge(method, path, body)
with self.assertRaises(Fatal):
persist_version(tagged, plan(), with_tags, FakeGit(forge))
def test_verify_bump_commit_refuses_an_unverified_commit(self) -> None:
verify_bump_commit(CTX, plan(), CHILD, FakeForge())
with self.assertRaises(Fatal):
verify_bump_commit(CTX, plan(), CHILD, FakeForge(Faults(verified=False)))
class SourceWriterTest(unittest.TestCase):
"""The client refuses everything but the planned write, before any request."""
def make(self) -> tuple[SourceWriter, list[tuple[str, str, dict[str, str], bytes | None]]]:
sent: list[tuple[str, str, dict[str, str], bytes | None]] = []
def send(method: str, url: str, headers: Any, data: bytes | None) -> tuple[int, bytes]:
sent.append((method, url, dict(headers), data))
return 200, b"{}"
return SourceWriter("tok", CTX, send=send), sent
def test_planned_requests_go_to_the_trusted_origin_with_the_token_only_in_a_header(self) -> None:
writer, sent = self.make()
stage = "ci/version-stage-41-1"
writer("GET", "/repos/O/r")
writer("GET", f"/repos/O/r/contents/pom.xml?ref={SRC}")
writer("POST", "/repos/O/r/branches", {"new_branch_name": stage, "old_ref_name": SRC})
writer("PUT", "/repos/O/r/contents/pom.xml", {"branch": stage, "sha": "s", "content": "c", "message": "m", "force_push": False})
writer("DELETE", "/repos/O/r/branches/ci%2Fversion-stage-41-1")
self.assertTrue(all(url.startswith("https://git.kof22.com/api/v1/repos/O/r") and "tok" not in url and headers["Authorization"] == "token tok" for _, url, headers, _ in sent))
self.assertEqual(len(sent), 5)
def test_everything_else_is_refused_before_any_request(self) -> None:
writer, sent = self.make()
stage = "ci/version-stage-41-1"
for method, path, body in (
("GET", "/repos/other/r", None),
("GET", "/repos/O/r/contents/other.xml?ref=" + SRC, None),
("GET", "/repos/O/r/contents/pom.xml?ref=main", None),
("POST", "/repos/O/r/branches", {"new_branch_name": "evil", "old_ref_name": SRC}),
("PUT", "/repos/O/r/contents/pom.xml", {"branch": "main", "sha": "s", "content": "c", "message": "m", "force_push": False}),
("PUT", "/repos/O/r/contents/pom.xml", {"branch": stage, "sha": "s", "content": "c", "message": "m", "force_push": True}),
("PUT", "/repos/O/r/contents/other.xml", {"branch": stage, "sha": "s", "content": "c", "message": "m", "force_push": False}),
("DELETE", "/repos/O/r/branches/main", None),
("POST", "/repos/O/r/merges", {}),
):
with self.subTest(f"{method} {path}"), self.assertRaises(SourceLifecycleError):
writer(method, path, body)
self.assertEqual(sent, [])
def artifact() -> Any:
"""The parts of a verified artifact the evidence readers use."""
return SimpleNamespace(build_sha=SRC, run=SimpleNamespace(run_id="41", created_at="2026-11-05T01:02:03Z"))
MODULES = [{"groupId": "com.example", "artifactId": "lib", "version": "1.2.0-RC.3", "packaging": "jar", "files": [{"classifier": None, "extension": "jar", "data": b"jar"}, {"classifier": None, "extension": "pom", "data": b"<lib/>"}]}]
class Registry:
"""An in-memory registry reader: 404 as the client raises it."""
def __init__(self, files: dict[str, bytes] | None = None) -> None:
self.files = files or {}
def __call__(self, method: str, path: str) -> tuple[int, bytes]:
if path not in self.files:
raise urllib.error.HTTPError(path, 404, "nf", Message(), io.BytesIO(b""))
return 200, self.files[path]
class EvidenceTest(unittest.TestCase):
"""Nothing is written until the registry and Central evidence are complete and exact."""
def published(self) -> dict[str, bytes]:
base = "/api/packages/O/maven/com/example/lib/1.2.0-RC.3"
return {f"{base}/lib-1.2.0-RC.3.jar": b"jar", f"{base}/lib-1.2.0-RC.3.pom": b"<lib/>"}
def test_publication_readback_is_pending_when_absent_and_fatal_when_different(self) -> None:
with patch("platform_ci.delivery.source_lifecycle.verified_modules", return_value=MODULES):
verify_publication(artifact(), (), "1.2.0-RC.3", "O", Registry(self.published()))
with self.assertRaises(NotYetPublished):
verify_publication(artifact(), (), "1.2.0-RC.3", "O", Registry({}))
changed = self.published()
changed[next(iter(changed))] = b"other"
with self.assertRaises(Fatal):
verify_publication(artifact(), (), "1.2.0-RC.3", "O", Registry(changed))
def test_the_completion_receipt_must_be_terminal_and_describe_this_reactor(self) -> None:
reactor = build_reactor(SRC, "1.2.0-RC.3", [{key: value for key, value in MODULES[0].items() if key != "version"}])
receipt = {
"schema": "qrun-publication-completion/1",
"repository": "O/r",
"repository_id": 7,
"run_id": 41,
"source_commit": SRC,
"version": "1.2.0-RC.3",
"archive_sha256": reactor.archive_sha256,
"manifest_sha256": reactor.manifest_sha256,
"gav": ["com.example:lib:1.2.0-RC.3"],
"target": "central-publisher",
"state": "PUBLISHED",
"deployment_id": "dep-1",
"files": [],
"purls": ["pkg:maven/com.example/lib@1.2.0-RC.3"],
}
location = completion_receipt.location("O", 7, 41, SRC)
def run(files: dict[str, bytes]) -> dict[str, Any]:
with patch("platform_ci.delivery.source_lifecycle.verified_modules", return_value=MODULES), patch("platform_ci.delivery.source_lifecycle.reactor_for", return_value=reactor):
return verify_completion(artifact(), (), "1.2.0-RC.3", repository="O/r", repository_id=7, owner="O", reader=Registry(files))
self.assertEqual(run({location: completion_receipt.encode("O", receipt)})["deployment_id"], "dep-1")
with self.assertRaises(NotYetPublished):
run({})
for changes in ({"manifest_sha256": "0" * 64}, {"state": "PENDING"}, {"run_id": 42}):
with self.subTest(changes), self.assertRaises(Fatal):
run({location: json.dumps(receipt | changes).encode()})
@dataclass(frozen=True)
class Run:
"""What one `postpublish` run varies; the defaults are a Central release with the default policy and one verified artifact."""
policy: WriteBackPolicy | None = None
git: FakeGit | None = None
version: str = "1.2.0-RC.3"
central: bool = True
extra: int = 0
def serve_source_pom(*_arguments: object, **_options: object) -> tuple[int, Any]:
"""Answer every read with the source POM at its blob id."""
return 200, {"content": base64.b64encode(OLD.encode()).decode(), "sha": git_blob_sha(OLD.encode())}
class PostpublishTest(unittest.TestCase):
"""The captured order: source POM, registry, completion, then the write; outcomes as the helper reported them."""
def run_it(self, forge: FakeForge, options: Run | None = None) -> dict[str, str]:
chosen = options or Run()
def forge_with_source(method: str, path: str, body: dict[str, Any] | None = None) -> tuple[int, Any]:
if method == "GET" and path == f"/repos/O/r/contents/pom.xml?ref={SRC}":
return 200, {"content": base64.b64encode(OLD.encode()).decode(), "sha": git_blob_sha(OLD.encode())}
return forge(method, path, body)
with patch("platform_ci.delivery.source_lifecycle.verify_publication") as publication, patch("platform_ci.delivery.source_lifecycle.verify_completion") as completion:
self.publication, self.completion = publication, completion
return postpublish(
CTX,
chosen.policy or WriteBackPolicy(),
artifact=artifact(),
files=(),
version=chosen.version,
owner="O",
repository_id=7,
central_required=chosen.central,
writer=forge_with_source,
registry_reader=Registry(),
completion_reader=Registry(),
git=chosen.git or FakeGit(forge),
extra_evidence=tuple((artifact(), ()) for _ in range(chosen.extra)),
)
def test_a_changed_version_is_written_only_after_the_evidence_was_read(self) -> None:
forge = FakeForge()
result = self.run_it(forge)
self.assertEqual((result["status"], result["pushed_sha"]), ("pushed", CHILD))
self.assertEqual((self.publication.call_count, self.completion.call_count), (1, 1))
def test_every_publication_of_the_release_is_verified_before_the_one_write(self) -> None:
forge = FakeForge()
self.run_it(forge, Run(extra=2))
self.assertEqual((self.publication.call_count, self.completion.call_count), (3, 3))
def test_missing_evidence_writes_nothing(self) -> None:
forge = FakeForge()
with patch("platform_ci.delivery.source_lifecycle.verify_publication", side_effect=NotYetPublished("later")), self.assertRaises(NotYetPublished):
postpublish(CTX, WriteBackPolicy(), artifact=artifact(), files=(), version="1.2.0-RC.3", owner="O", repository_id=7, central_required=False, writer=serve_source_pom, registry_reader=Registry(), completion_reader=Registry(), git=FakeGit(forge))
self.assertEqual(forge.calls, [])
def test_an_unchanged_version_still_waits_for_the_evidence_and_ends_skipped(self) -> None:
forge = FakeForge()
result = self.run_it(forge, Run(version="1.2.0-SNAPSHOT"))
self.assertEqual(result["status"], "skipped")
self.assertEqual(self.publication.call_count, 1)
def test_a_push_failure_is_a_warning_unless_the_push_is_required(self) -> None:
forge = FakeForge()
self.assertEqual(self.run_it(forge, Run(git=FakeGit(forge, reject=True)))["status"], "push-failed-warning")
with self.assertRaises(Fatal):
self.run_it(FakeForge(), Run(policy=WriteBackPolicy(require_push=True), git=FakeGit(FakeForge(), reject=True)))
if __name__ == "__main__":
unittest.main()