Files
pebrel/scripts/tests/test_preview_release.py
T

251 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
import json
import os
import tempfile
import unittest
from scripts.preview_release import (
MIN_ASSET_SIZE,
ManifestError,
asset_version,
expected_asset_names,
preview_notes,
read_cargo_package_version,
reviewed_notes,
validate_assets,
validate_evidence,
verify_binary_freshness,
verify_release,
sha256,
RUNTIME_REPORTS,
)
def write_fake_asset(path: Path) -> None:
if path.name.endswith(".AppImage"):
header = b"\x7fELF\x02\x01\x01\x00"
elif path.suffix == ".deb":
header = b"!<arch>\n"
elif path.name.endswith(".tar.gz"):
header = b"\x1f\x8b\x08\x00\x00\x00\x00\x00"
else:
header = b"\x00" * 8
with path.open("wb") as stream:
stream.write(header)
stream.truncate(MIN_ASSET_SIZE)
if path.suffix == ".dmg":
stream.seek(-512, 2)
stream.write(b"koly")
class PreviewReleaseTests(unittest.TestCase):
def write_reviewed_notes(self, root: Path) -> Path:
changes = "## English\n\n### Added\n\n- Preview packages.\n\n## 中文\n\n### 新增\n\n- 预览安装包。"
source = root / "docs/release-notes/v1.5.0-preview.42.md"
source.parent.mkdir(parents=True)
source.write_text(
"# Pebrel 1.5.0 Preview 42\n\n" + changes +
"\n\n## Contributors\n\n- Contributors\n\n"
"macOS signing / macOS 签名: `adhoc`\n\n---\n\n## SHA256\n\n<!-- PREVIEW_SHA256 -->\n",
encoding="utf-8",
)
changelog_changes = "\n".join("#" + line if line.startswith("#") else line for line in changes.splitlines())
(root / "CHANGELOG.md").write_text(
"# Changelog\n\n## Unreleased\n\nNone.\n\n## 1.5.0-preview.42 - 2026-09-05\n\n" +
changelog_changes + "\n\n## 1.5.0 - 2026-09-01\n\nOld notes.\n", encoding="utf-8",
)
return source
def test_public_preview_requires_committed_synchronized_notes(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.write_reviewed_notes(root)
self.assertEqual(reviewed_notes(root, "1.5.0", "42", "adhoc"), source.read_text(encoding="utf-8"))
changelog = root / "CHANGELOG.md"
changelog.write_text(changelog.read_text(encoding="utf-8").replace("Preview packages.", "Other changes."), encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "different changes"):
reviewed_notes(root, "1.5.0", "42", "adhoc")
def test_public_preview_rejects_missing_notes_or_wrong_signing_mode(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with self.assertRaises(OSError):
reviewed_notes(root, "1.5.0", "42", "adhoc")
self.write_reviewed_notes(root)
with self.assertRaisesRegex(ManifestError, "signing mode"):
reviewed_notes(root, "1.5.0", "42", "developer-id")
with self.assertRaises(OSError):
reviewed_notes(root, "1.5.0", "43", "adhoc")
def test_public_preview_rejects_stale_checksums(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.write_reviewed_notes(root)
source.write_text(source.read_text(encoding="utf-8").replace("<!-- PREVIEW_SHA256 -->", "old-checksum"), encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "stale checksums"):
reviewed_notes(root, "1.5.0", "42", "adhoc")
def test_public_preview_requires_matching_bilingual_categories(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = self.write_reviewed_notes(root)
source.write_text(source.read_text(encoding="utf-8").replace("### 新增", "### 修复"), encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "bilingual"):
reviewed_notes(root, "1.5.0", "42", "adhoc")
def test_packaging_rejects_stale_binary(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "nebula_app/res/shell/zshrc"
source.parent.mkdir(parents=True)
source.write_text("updated script", encoding="utf-8")
binary = root / "nebula"
binary.write_bytes(b"binary")
os.utime(binary, (10, 10))
os.utime(source, (20, 20))
with self.assertRaisesRegex(ManifestError, "rebuild GPUI"):
verify_binary_freshness(binary, root)
os.utime(binary, (30, 30))
verify_binary_freshness(binary, root)
def test_reads_only_top_level_cargo_package_version(self) -> None:
with tempfile.TemporaryDirectory() as raw_directory:
manifest = Path(raw_directory) / "Cargo.toml"
manifest.write_text(
'[package]\nname = "nebula"\nversion = "1.5.0"\n\n'
'[dependencies]\nversion = "99.0.0"\n',
encoding="utf-8",
)
self.assertEqual(read_cargo_package_version(manifest), "1.5.0")
def test_asset_version_rejects_path_like_preview_id(self) -> None:
self.assertEqual(asset_version("1.5.0", "42.1"), "1.5.0-preview.42.1")
with self.assertRaises(ManifestError):
asset_version("1.5.0", "../../release")
def test_complete_manifest_passes(self) -> None:
with tempfile.TemporaryDirectory() as raw_directory:
directory = Path(raw_directory)
for name in expected_asset_names("1.5.0", "42"):
write_fake_asset(directory / name)
assets = validate_assets(directory, "1.5.0", "42")
self.assertEqual(len(assets), 5)
def test_unexpected_asset_fails_closed(self) -> None:
with tempfile.TemporaryDirectory() as raw_directory:
directory = Path(raw_directory)
for name in expected_asset_names("1.5.0", "42"):
write_fake_asset(directory / name)
(directory / "old-build.zip").write_bytes(b"old")
with self.assertRaisesRegex(ManifestError, "unexpected: old-build.zip"):
validate_assets(directory, "1.5.0", "42")
def test_bad_magic_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as raw_directory:
directory = Path(raw_directory)
names = expected_asset_names("1.5.0", "42")
for name in names:
write_fake_asset(directory / name)
(directory / names[0]).write_bytes(b"not an appimage" + b"\0" * MIN_ASSET_SIZE)
with self.assertRaisesRegex(ManifestError, "not an ELF"):
validate_assets(directory, "1.5.0", "42")
def test_notes_are_bilingual_and_disclose_preview_limits(self) -> None:
notes = preview_notes(
"1.5.0",
"42",
"Kuddev/nebula",
"a" * 40,
"deadbeef package\n",
)
self.assertIn("## English", notes)
self.assertIn("## 中文", notes)
self.assertIn("ad-hoc", notes)
self.assertIn("不是稳定版本", notes)
self.assertIn("deadbeef package", notes)
def test_signed_notes_never_claim_adhoc(self) -> None:
notes = preview_notes("1.5.0", "42", "Kuddev/nebula", "a" * 40, "hash file", "developer-id")
self.assertIn("Developer ID", notes)
self.assertIn("公证票据", notes)
self.assertNotIn("ad-hoc", notes)
def test_missing_native_evidence_cannot_publish(self) -> None:
with tempfile.TemporaryDirectory() as directory:
with self.assertRaises(OSError):
validate_evidence(Path(directory), "a" * 40)
def test_complete_evidence_requires_same_commit_and_installed_launch(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
common_path = Path(__file__).resolve().parents[1] / "conformance/golden/common.json"
required = json.loads(common_path.read_text(encoding="utf-8"))["required"]
report = {"schema_version": 1, "build": {"commit": "a" * 40}}
for path, value in required.items():
cursor = report
parts = path.split(".")
for part in parts[:-1]:
cursor = cursor.setdefault(part, {})
cursor[parts[-1]] = value
report["cases"]["ssh_loop"] = {"status": "skipped", "reason": "no_interactive_ssh_automation"}
report["summary"].update(passed=9, skipped=1)
for filename, platform in RUNTIME_REPORTS.items():
report["platform"] = platform
(root / filename).write_text(json.dumps(report), encoding="utf-8")
for architecture in ("aarch64", "x86_64"):
(root / f"macos-{architecture}-launch.json").write_text(json.dumps({
"status": "passed", "commit": "a" * 40, "architecture": architecture,
"launch_method": "launchservices", "utf8_locale": True, "home_cwd": True,
"screenshot": "captured", "rendered_text": True,
}), encoding="utf-8")
validate_evidence(root, "a" * 40)
with self.assertRaises(FileNotFoundError):
validate_evidence(root, "a" * 40, windows_arm64=True)
arm_report = root / "windows-arm64-report.json"
report["platform"] = "windows-aarch64"
arm_report.write_text(json.dumps(report), encoding="utf-8")
validate_evidence(root, "a" * 40, windows_arm64=True)
report["platform"] = "windows-x86_64"
arm_report.write_text(json.dumps(report), encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "wrong platform"):
validate_evidence(root, "a" * 40, windows_arm64=True)
with self.assertRaisesRegex(ManifestError, "source commit"):
validate_evidence(root, "b" * 40)
launch_path = root / "macos-aarch64-launch.json"
launch = json.loads(launch_path.read_text(encoding="utf-8"))
for field, value in (("rendered_text", False), ("screenshot", "unavailable")):
with self.subTest(field=field):
launch_path.write_text(json.dumps({**launch, field: value}), encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "LaunchServices"):
validate_evidence(root, "a" * 40)
(root / "macos-aarch64-launch.json").write_text('{}', encoding="utf-8")
with self.assertRaisesRegex(ManifestError, "LaunchServices"):
validate_evidence(root, "a" * 40)
def test_remote_release_checks_assets_notes_labels_and_tag_commit(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
names = [*expected_asset_names("1.5.0", "42"), "SHA256SUMS"]
(root / "PREVIEW_NOTES.md").write_text("verified notes", encoding="utf-8")
for name in names:
(root / name).write_bytes(b"fixture")
metadata = {
"name": "Pebrel 1.5.0 Preview 42", "tag_name": "preview-v1.5.0-42",
"prerelease": True, "draft": False, "body": "verified notes",
"assets": [{"name": name, "label": "", "state": "uploaded", "size": 7,
"digest": f"sha256:{sha256(root / name)}"} for name in names],
}
verify_release(metadata, root, "1.5.0", "42", "a" * 40, "a" * 40)
with self.assertRaisesRegex(ManifestError, "commit"):
verify_release(metadata, root, "1.5.0", "42", "a" * 40, "b" * 40)
metadata["assets"][0]["label"] = "wrong label"
with self.assertRaisesRegex(ManifestError, "label"):
verify_release(metadata, root, "1.5.0", "42", "a" * 40, "a" * 40)
if __name__ == "__main__":
unittest.main()