Files
mail/scripts/test-release.py
T

213 lines
9.9 KiB
Python

#!/usr/bin/env python3
"""Exercise release validation and publishing without contacting GitHub."""
import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parent
sys.dont_write_bytecode = True
def load_script(name):
spec = importlib.util.spec_from_file_location(name, SCRIPTS / f"{name}.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
metadata = load_script("release-metadata").metadata
release_assets = load_script("prepare-release")
prepare = release_assets.prepare
class ReleaseTests(unittest.TestCase):
def test_debian_prerelease_package(self):
# Exercise the actual shell packager and dpkg with a tiny existing ELF,
# so shell expansion bugs cannot hide behind Python-only asset tests.
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
paths = (
"scripts/build-deb.sh", "LICENSE", "THIRD_PARTY_NOTICES.md",
"resources/com.flectar.mail.desktop", "resources/com.flectar.mail.metainfo.xml",
"resources/app-icon/flectar-mail-masked-512.png",
"resources/app-icon/flectar-mail-masked.svg",
"resources/fonts/google-sans-flex/OFL.txt",
"resources/fonts/google-sans-flex/README.md",
"resources/debian/source-control.in", "resources/debian/control.in",
)
for name in paths:
destination = root / name
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(SCRIPTS.parent / name, destination)
shutil.copytree(SCRIPTS.parent / "LICENSES", root / "LICENSES")
(root / "Cargo.toml").write_text('[package]\nversion = "0.1.0-alpha.1"\n')
(root / "target/release").mkdir(parents=True)
shutil.copyfile("/bin/true", root / "target/release/flectar-mail")
(root / "bin").mkdir()
cargo = root / "bin/cargo"
cargo.write_text("#!/bin/sh\nexit 0\n")
cargo.chmod(0o755)
result = subprocess.run(
["bash", str(root / "scripts/build-deb.sh")], cwd=root,
env={**os.environ, "PATH": f"{root / 'bin'}:{os.environ['PATH']}"},
capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
package = root / "target/deb/flectar-mail_0.1.0~alpha.1_amd64.deb"
self.assertTrue(package.is_file())
version = subprocess.check_output(["dpkg-deb", "--field", str(package), "Version"], text=True)
self.assertEqual(version.strip(), "0.1.0~alpha.1")
def test_versions_and_tags(self):
for version in ("0.1.0", "1.2.3-alpha.1", "1.2.3-beta.2", "1.2.3-rc.3"):
with self.subTest(version=version):
result = metadata(version, f"v{version}")
self.assertEqual(result["prerelease"], str("-" in version).lower())
self.assertEqual(result["native_version"], version.split("-")[0])
for version, tag in (
("0.1.0", "v0.2.0"), ("0.1.0-beta.1", "v0.1.0"),
("0.1.0-beta.01", None), ("0.1.0-beta.0", None),
("01.2.3", None), ("1.2.3-nightly", None), ("1.2.3+build", None),
):
with self.subTest(version=version, tag=tag), self.assertRaises(ValueError):
metadata(version, tag)
def test_staging_and_checksums(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "assets"
source.mkdir()
names = (
"linux/appimage/flectar-mail.AppImage",
"linux/deb/flectar-mail_0.1.0~beta.1_amd64.deb",
"windows/flectar-mail-windows-x64.zip",
"windows/flectar-mail-windows-x64-setup.exe",
"macos/flectar-mail-macos-arm64.zip",
"macos/flectar-mail-macos-arm64.dmg",
"android/flectar-mail.apk",
)
for name in names:
path = source / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(name.encode())
(source / "benchmark.json").write_text("{}")
dist = root / "dist"
prepare(source, dist, "0.1.0-beta.1")
self.assertEqual(len(list(dist.iterdir())), 8)
self.assertTrue((dist / "flectar-mail-0.1.0-beta.1-android-arm64-test.apk").is_file())
result = subprocess.run(
["sha256sum", "--check", "SHA256SUMS"], cwd=dist,
capture_output=True, text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
missing = source / names[-1]
missing.unlink()
with self.assertRaises(ValueError):
prepare(source, root / "missing", "0.1.0-beta.1")
self.assertFalse((root / "missing").exists())
missing.write_bytes(b"")
with self.assertRaises(ValueError):
prepare(source, root / "empty", "0.1.0-beta.1")
missing.write_bytes(b"dmg")
(source / missing.name).write_bytes(b"duplicate")
with self.assertRaises(ValueError):
prepare(source, root / "duplicate", "0.1.0-beta.1")
# Stable releases keep the six desktop downloads, even if the
# input folder happens to contain test APKs from another step.
(source / "linux/deb/flectar-mail_0.1.0~beta.1_amd64.deb").rename(
source / "linux/deb/flectar-mail_0.1.0_amd64.deb"
)
stable_dist = root / "stable"
prepare(source, stable_dist, "0.1.0")
self.assertEqual(len(list(stable_dist.iterdir())), 7)
self.assertFalse(list(stable_dist.glob("*.apk")))
def test_notes_distinguish_android_preview(self):
self.assertIn("test-signed", release_assets.release_notes("0.1.0-beta.1"))
self.assertIn("prereleases only", release_assets.release_notes("0.1.0"))
def test_metadata_rejects_stale_android_lockfile(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "platform/android").mkdir(parents=True)
(root / "Cargo.toml").write_text('[package]\nversion = "0.2.0-beta.1"\n')
lock = '[[package]]\nname = "flectar-mail"\nversion = "0.2.0-beta.1"\n'
(root / "Cargo.lock").write_text(lock)
android_lock = root / "platform/android/Cargo.lock"
android_lock.write_text(lock.replace("0.2.0-beta.1", "0.1.0"))
command = [sys.executable, str(SCRIPTS / "release-metadata.py"), "--tag", "v0.2.0-beta.1"]
result = subprocess.run(command, cwd=root, capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn("platform/android/Cargo.lock", result.stderr)
android_lock.write_text(lock)
result = subprocess.run(command, cwd=root, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("prerelease=true", result.stdout)
def publish(self, *, existing="missing", prerelease="true", fail_upload=False):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "dist").mkdir()
(root / "dist" / "package.zip").write_bytes(b"package")
(root / "release-notes.md").write_text("Notes")
gh = root / "gh"
gh.write_text('''#!/usr/bin/env python3
import json, os, sys
with open("calls.jsonl", "a") as stream:
stream.write(json.dumps(sys.argv[1:]) + "\\n")
command = sys.argv[2]
if command == "view":
existing = os.environ["EXISTING"]
if existing == "missing":
sys.exit(1)
print(existing)
if command == "upload" and os.environ["FAIL_UPLOAD"] == "true":
sys.exit(1)
''')
gh.chmod(0o755)
result = subprocess.run(
["bash", str(SCRIPTS / "publish-release.sh")], cwd=root,
env={**os.environ, "PATH": f"{root}:{os.environ['PATH']}",
"GITHUB_REF_NAME": "v0.1.0-beta.1" if prerelease == "true" else "v0.1.0",
"RELEASE_PRERELEASE": prerelease, "EXISTING": existing,
"FAIL_UPLOAD": str(fail_upload).lower()},
capture_output=True, text=True,
)
calls = [json.loads(line) for line in (root / "calls.jsonl").read_text().splitlines()]
return result, calls
def test_preview_and_stable_publication(self):
for prerelease in ("true", "false"):
with self.subTest(prerelease=prerelease):
result, calls = self.publish(prerelease=prerelease)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual([c[1] for c in calls], ["view", "create", "upload", "edit"])
self.assertIn("--draft", calls[1])
self.assertIn(f"--prerelease={prerelease}", calls[-1])
self.assertIn("--latest=" + str(prerelease == "false").lower(), calls[-1])
def test_failed_upload_does_not_publish(self):
result, calls = self.publish(fail_upload=True)
self.assertNotEqual(result.returncode, 0)
self.assertNotIn("edit", [c[1] for c in calls])
def test_resume_draft_and_reject_published_replacement(self):
result, calls = self.publish(existing="true")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual([c[1] for c in calls], ["view", "upload", "edit"])
result, calls = self.publish(existing="false")
self.assertNotEqual(result.returncode, 0)
self.assertEqual([c[1] for c in calls], ["view"])
if __name__ == "__main__":
unittest.main()