Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions cli/python/base_pr_policy/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@
from base_setup.github_manifest import GithubPrConfig, GithubPrRequiredSectionsConfig


def test_explicit_manifest_populates_history_project_metadata(tmp_path) -> None:
def test_explicit_manifest_populates_history_project_metadata(tmp_path, manifest_factory) -> None:
project_root = tmp_path / "demo"
project_root.mkdir()
manifest_path = project_root / "base_manifest.yaml"
manifest_path.write_text(
"project:\n name: demo\ngithub:\n pr:\n required_sections:\n default: [Summary]\n",
encoding="utf-8",
)
manifest_path = manifest_factory.write_pr_policy(project_root)
outside = tmp_path / "outside"
outside.mkdir()
captured = []
Expand Down
99 changes: 22 additions & 77 deletions cli/python/base_release/tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from contextlib import redirect_stderr
from contextlib import redirect_stdout
from pathlib import Path
from typing import Any
from unittest import mock

from base_cli_adapters.history import build_finished_record
Expand Down Expand Up @@ -65,63 +66,6 @@ def isatty(self) -> bool:
return True


def write_release_project(
root: Path,
*,
version_file_content: str = "1.2.3\n",
changelog: str | None = None,
homebrew: bool = True,
) -> Path:
changelog_content = changelog or "\n".join(
[
"# Changelog",
"",
"## [Unreleased]",
"",
"## [1.2.3] - 2026-06-09",
"",
"- Added the release assistant.",
"",
"## [1.2.2] - 2026-06-01",
"",
"- Previous release.",
]
)
root.joinpath("VERSION").write_text(version_file_content, encoding="utf-8")
root.joinpath("CHANGELOG.md").write_text(changelog_content, encoding="utf-8")
manifest_lines = [
"project:",
" name: demo",
"",
"release:",
" version_file: VERSION",
" changelog: CHANGELOG.md",
" tag_prefix: v",
" github:",
" repository: codeforester/demo",
" release_title: \"Demo v{version}\"",
]
if homebrew:
manifest_lines.extend(
[
" homebrew:",
" required: true",
" tap_repository: codeforester/homebrew-demo",
" formula_path: Formula/demo.rb",
" package: codeforester/demo/demo",
]
)
manifest_lines.extend(["", "artifacts: []"])
manifest_path = root / "base_manifest.yaml"
manifest_path.write_text("\n".join(manifest_lines), encoding="utf-8")
subprocess.run(["git", "init"], cwd=root, check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "config", "user.email", "base@example.com"], cwd=root, check=True)
subprocess.run(["git", "config", "user.name", "Base Tests"], cwd=root, check=True)
subprocess.run(["git", "add", "."], cwd=root, check=True)
subprocess.run(["git", "commit", "-m", "initial"], cwd=root, check=True, stdout=subprocess.DEVNULL)
return manifest_path


def add_origin(root: Path) -> None:
remote_path = root.parent / "remote.git"
subprocess.run(["git", "init", "--bare", str(remote_path)], check=True, stdout=subprocess.DEVNULL)
Expand Down Expand Up @@ -199,13 +143,14 @@ def test_delegated_missing_required_option_usage_uses_basectl_release(self) -> N


class ReleaseEngineTests(unittest.TestCase): # pylint: disable=too-many-public-methods
manifest_factory: Any

def test_explicit_manifest_populates_history_project_metadata(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
project_root = root / "demo"
project_root.mkdir()
manifest_path = write_release_project(project_root)
manifest_path = self.manifest_factory.write_release(project_root)
outside = root / "outside"
outside.mkdir()
captured: list[tuple[object, ...]] = []
Expand Down Expand Up @@ -233,7 +178,7 @@ def test_explicit_manifest_populates_history_project_metadata(self) -> None:
def test_check_json_reports_ready_findings_with_stable_envelope(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch("base_release.engine.release_findings", return_value=READY_FINDINGS):
status, stdout, stderr = run_engine(
Expand Down Expand Up @@ -271,7 +216,7 @@ def test_check_json_finding_is_error_with_null_execution_error(self) -> None:
)
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch("base_release.engine.release_findings", return_value=findings):
status, stdout, stderr = run_engine(
Expand All @@ -289,7 +234,7 @@ def test_check_json_finding_is_error_with_null_execution_error(self) -> None:
def test_check_json_warning_and_empty_findings_preserve_success_exit(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch(
"base_release.engine.release_findings",
Expand Down Expand Up @@ -334,7 +279,7 @@ def test_check_json_controlled_manifest_failure_has_error_object(self) -> None:
def test_notes_prints_changelog_section_for_version(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

status, stdout, stderr = run_engine(
["notes", "--version", "1.2.3", "--manifest", str(manifest_path)],
Expand All @@ -349,7 +294,7 @@ def test_notes_prints_changelog_section_for_version(self) -> None:
def test_plan_prints_github_and_homebrew_handoff(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

status, stdout, stderr = run_engine(
["plan", "--version", "1.2.3", "--manifest", str(manifest_path)],
Expand Down Expand Up @@ -387,7 +332,7 @@ def test_plan_prints_1_0_homebrew_upgrade_reminder_without_issue_number(self) ->
"- Stable release.",
]
)
manifest_path = write_release_project(
manifest_path = self.manifest_factory.write_release(
root,
version_file_content="1.0.0\n",
changelog=changelog,
Expand All @@ -406,7 +351,7 @@ def test_plan_prints_1_0_homebrew_upgrade_reminder_without_issue_number(self) ->
def test_plan_prints_no_homebrew_handoff_for_github_only_project(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root, homebrew=False)
manifest_path = self.manifest_factory.write_release(root, homebrew=False)

status, stdout, stderr = run_engine(
["plan", "--version", "1.2.3", "--manifest", str(manifest_path)],
Expand All @@ -421,7 +366,7 @@ def test_plan_prints_no_homebrew_handoff_for_github_only_project(self) -> None:
def test_publish_dry_run_prints_planned_actions_without_running_commands(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch("base_release.engine.release_findings", return_value=READY_FINDINGS), mock.patch(
"base_release.engine.github_release_finding",
Expand All @@ -444,7 +389,7 @@ def test_publish_dry_run_prints_planned_actions_without_running_commands(self) -
def test_publish_requires_yes_when_stdin_is_not_interactive(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch("base_release.engine.release_findings", return_value=READY_FINDINGS), mock.patch(
"base_release.engine.github_release_finding",
Expand All @@ -464,7 +409,7 @@ def test_publish_requires_yes_when_stdin_is_not_interactive(self) -> None:
def test_publish_yes_creates_annotated_tag_pushes_and_creates_github_release(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
commands: list[tuple[list[str], Path | None]] = []

def fake_run_release_step(command: list[str], *, cwd: Path | None = None) -> None:
Expand Down Expand Up @@ -508,7 +453,7 @@ def fake_run_release_step(command: list[str], *, cwd: Path | None = None) -> Non
def test_publish_yes_reports_recovery_when_github_release_create_fails_after_tag_push(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
commands: list[tuple[list[str], Path | None]] = []

def fake_run_release_step(command: list[str], *, cwd: Path | None = None) -> None:
Expand Down Expand Up @@ -549,7 +494,7 @@ def fake_run_release_step(command: list[str], *, cwd: Path | None = None) -> Non
def test_publish_fails_when_readiness_has_errors(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch(
"base_release.engine.release_findings",
Expand All @@ -570,7 +515,7 @@ def test_publish_fails_when_readiness_has_errors(self) -> None:
def test_publish_fails_when_github_release_already_exists(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)

with mock.patch("base_release.engine.release_findings", return_value=READY_FINDINGS), mock.patch(
"base_release.engine.github_release_finding",
Expand All @@ -596,7 +541,7 @@ def test_publish_fails_when_github_release_already_exists(self) -> None:
def test_check_fails_when_version_file_does_not_match(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(root, version_file_content="1.2.2\n")
manifest_path = self.manifest_factory.write_release(root, version_file_content="1.2.2\n")

status, stdout, stderr = run_engine(
["check", "--version", "1.2.3", "--manifest", str(manifest_path)],
Expand All @@ -611,7 +556,7 @@ def test_check_fails_when_version_file_does_not_match(self) -> None:
def test_check_fails_when_changelog_section_is_missing(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
manifest_path = write_release_project(
manifest_path = self.manifest_factory.write_release(
root,
changelog="# Changelog\n\n## [1.2.2] - 2026-06-01\n\n- Previous release.\n",
)
Expand All @@ -630,7 +575,7 @@ def test_check_passes_for_clean_release_ready_project(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "project"
root.mkdir()
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
add_origin(root)

with mock.patch(
Expand All @@ -653,7 +598,7 @@ def test_check_fails_when_worktree_is_dirty(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "project"
root.mkdir()
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
add_origin(root)
root.joinpath("scratch.txt").write_text("dirty\n", encoding="utf-8")

Expand All @@ -675,7 +620,7 @@ def test_check_fails_when_local_tag_already_exists(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "project"
root.mkdir()
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
add_origin(root)
subprocess.run(["git", "tag", "v1.2.3"], cwd=root, check=True)

Expand All @@ -697,7 +642,7 @@ def test_check_fails_when_remote_tag_already_exists(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir) / "project"
root.mkdir()
manifest_path = write_release_project(root)
manifest_path = self.manifest_factory.write_release(root)
add_origin_with_remote_tag(root, "v1.2.3")

with mock.patch(
Expand Down
96 changes: 96 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import subprocess
from pathlib import Path

import pytest
Expand Down Expand Up @@ -90,6 +91,101 @@ def write_command_surfaces(self, root: Path, name: str = "demo") -> Path:
],
)

def write_pr_policy(self, root: Path, name: str = "demo") -> Path:
"""Write a manifest with the default pull-request policy sections."""

return self._write(
root,
[
"project:",
f" name: {name}",
"github:",
" pr:",
" required_sections:",
" default: [Summary]",
],
)

def write_release(
self,
root: Path,
*,
version_file_content: str = "1.2.3\n",
changelog: str | None = None,
homebrew: bool = True,
) -> Path:
"""Write a release-ready project with an initial Git commit."""

changelog_content = changelog or "\n".join(
[
"# Changelog",
"",
"## [Unreleased]",
"",
"## [1.2.3] - 2026-06-09",
"",
"- Added the release assistant.",
"",
"## [1.2.2] - 2026-06-01",
"",
"- Previous release.",
]
)
root.mkdir(parents=True, exist_ok=True)
root.joinpath("VERSION").write_text(version_file_content, encoding="utf-8")
root.joinpath("CHANGELOG.md").write_text(changelog_content, encoding="utf-8")
manifest_lines = [
"project:",
" name: demo",
"",
"release:",
" version_file: VERSION",
" changelog: CHANGELOG.md",
" tag_prefix: v",
" github:",
" repository: codeforester/demo",
" release_title: \"Demo v{version}\"",
]
if homebrew:
manifest_lines.extend(
[
" homebrew:",
" required: true",
" tap_repository: codeforester/homebrew-demo",
" formula_path: Formula/demo.rb",
" package: codeforester/demo/demo",
]
)
manifest_path = self._write(root, manifest_lines + ["", "artifacts: []"])
self._initialize_git(root)
return manifest_path

@staticmethod
def _initialize_git(root: Path) -> None:
subprocess.run(["git", "init"], cwd=root, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(
["git", "config", "user.email", "base@example.com"],
cwd=root,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(
["git", "config", "user.name", "Base Tests"],
cwd=root,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.run(["git", "add", "."], cwd=root, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(
["git", "commit", "-m", "initial"],
cwd=root,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

@staticmethod
def write_ready_python_bin(python_bin: Path) -> None:
python_bin.parent.mkdir(parents=True, exist_ok=True)
Expand Down
Loading
Loading