Files
GiteaMailTemplates/.github/scripts/test_package_release.py
T
2026-10-11 11:05:48 +08:00

211 lines
8.1 KiB
Python

"""Offline packaging tests using a minimal, independently generated repository."""
import importlib.util
import json
import tarfile
import tempfile
import unittest
import zipfile
from pathlib import Path
SCRIPT_PATH = Path(__file__).with_name("package_release.py")
SPEC = importlib.util.spec_from_file_location("package_release", SCRIPT_PATH)
PACKER = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PACKER)
class PackagingTests(unittest.TestCase):
VERSION = "v28.0.0"
COMMIT = "a" * 40
THEME = "demo"
LANGUAGE = "en-US"
ROOT_DOCUMENTS = (
"LICENSE",
"AGENTS.md",
"README.md",
"CONTRIBUTING.md",
"COMPATIBILITY.md",
"THIRD_PARTY_NOTICES.md",
"docs/README.zh-CN.md",
"docs/CONTRIBUTING.zh-CN.md",
"preview/index.html",
"preview/lib/html2canvas/html2canvas.min.js",
"preview/lib/html2canvas/LICENSE",
"preview/lib/html2canvas/README.md",
"docs/images/README.md",
)
def setUp(self):
temporary_directory = tempfile.TemporaryDirectory()
self.addCleanup(temporary_directory.cleanup)
self.root = Path(temporary_directory.name)
self.output = self.root / "artifacts"
self.create_theme()
self.create_snapshot()
self.create_preview()
for name in self.ROOT_DOCUMENTS:
self.write(name, b"test")
# Fixture helpers keep test bodies focused on the behavior being checked.
def write(self, name, data):
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
def write_json(self, name, data):
self.write(name, json.dumps(data).encode("utf-8"))
def create_theme(self):
theme_data = json.dumps({"name": self.THEME, "mode": "shared"}).encode("utf-8")
css_data = b"a { color:blue; }"
self.write("themes/demo/theme.json", theme_data)
self.write("themes/demo/theme.css", css_data)
generated_files = {
"mail/base/head.tmpl": b"<title>test</title>",
"mail/base/footer.tmpl": b"<p>footer</p>",
}
metadata = {
"theme": self.THEME,
"mode": "shared",
"gitea_tag": self.VERSION,
"gitea_commit": self.COMMIT,
"sources": {
"theme.json": PACKER.digest(theme_data),
"theme.css": PACKER.digest(css_data),
},
"files": {
path: PACKER.digest(data)
for path, data in generated_files.items()
},
}
for path, data in generated_files.items():
self.write("build/themes/demo/" + path, data)
self.write_json("build/themes/demo/build.json", metadata)
def create_snapshot(self):
self.write_json("gitea.lock.json", {
"tag": self.VERSION,
"commit": self.COMMIT,
"files": {
"LICENSE": PACKER.digest(b"MIT"),
"options/locale/locale_en-US.json": PACKER.digest(b"{}"),
},
})
self.write("build/upstream/LICENSE", b"MIT")
def create_preview(self):
manifest = {
"upstream": self.VERSION,
"upstream_commit": self.COMMIT,
"themes": [self.THEME],
"languages": [self.LANGUAGE],
"registry": {"activate": {}},
}
bundle = {self.THEME: {"activate": "<html></html>"}}
self.write("preview/rendered.js", (
"window.__MAIL_PREVIEW__ = " + json.dumps(manifest) + ";"
).encode("utf-8"))
self.write("preview/rendered/en-US.js", (
'window.__MAIL_PREVIEW_DATA__["en-US"] = ' + json.dumps(bundle) + ";"
).encode("utf-8"))
# Archives: both formats contain the verified bytes, never stale build files.
def test_archives_contain_only_verified_outputs(self):
self.write("build/themes/stale/mail/stale.tmpl", b"stale")
expected_files = PACKER.collect(self.root, self.VERSION)
prefix = f"gitea-mail-templates-{self.VERSION}/"
expected_members = {
prefix + name: data for name, data in expected_files.items()
}
PACKER.package(self.root, self.VERSION, self.output)
with zipfile.ZipFile(self.output / f"gitea-mail-templates-{self.VERSION}.zip") as archive:
zipped = {name: archive.read(name) for name in archive.namelist()}
with tarfile.open(self.output / f"gitea-mail-templates-{self.VERSION}.tar.gz") as archive:
tarred = {
member.name: archive.extractfile(member).read()
for member in archive.getmembers()
}
self.assertTrue(all(member.mode == 0o644 for member in archive.getmembers()))
self.assertEqual(expected_members, zipped)
self.assertEqual(expected_members, tarred)
self.assertIn(prefix + "themes/demo/mail/base/head.tmpl", zipped)
for name in ("html2canvas.min.js", "LICENSE", "README.md"):
self.assertIn(prefix + "preview/lib/html2canvas/" + name, zipped)
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
def test_missing_screenshot_library_is_rejected(self):
(self.root / "preview/lib/html2canvas/html2canvas.min.js").unlink()
with self.assertRaisesRegex(ValueError, "Expected regular file"):
PACKER.collect(self.root, self.VERSION)
def test_existing_archives_are_not_overwritten(self):
PACKER.package(self.root, self.VERSION, self.output)
original_archives = {
path.name: path.read_bytes() for path in self.output.iterdir()
}
with self.assertRaisesRegex(ValueError, "Refusing to overwrite"):
PACKER.package(self.root, self.VERSION, self.output)
self.assertEqual(original_archives, {
path.name: path.read_bytes() for path in self.output.iterdir()
})
# Validation: release identity, source checksums and generated checksums.
def test_wrong_version_is_rejected(self):
with self.assertRaisesRegex(ValueError, "must match the locked Gitea tag"):
PACKER.collect(self.root, "v28.1.0")
def test_changed_source_is_rejected(self):
self.write("themes/demo/theme.css", b"changed")
with self.assertRaisesRegex(ValueError, "Source changed"):
PACKER.collect(self.root, self.VERSION)
def test_missing_language_bundle_is_rejected(self):
(self.root / "preview/rendered/en-US.js").unlink()
with self.assertRaisesRegex(ValueError, "Expected regular file"):
PACKER.collect(self.root, self.VERSION)
def test_modified_generated_files_are_rejected(self):
self.write("build/themes/demo/mail/base/head.tmpl", b"modified")
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
PACKER.collect(self.root, self.VERSION)
# Documentation: recursive inclusion and optional root files.
def test_nested_documentation_and_release_notes_are_included(self):
documents = {
"docs/INDEX.zh-CN.md": "中文索引".encode("utf-8"),
"docs/images/README.zh-CN.md": "截图指南".encode("utf-8"),
"docs/release-notes/v28.0.0.zh-CN.md": "发布说明".encode("utf-8"),
".github/release-notes/v28.0.0.md": b"English release notes",
}
for name, data in documents.items():
self.write(name, data)
self.write("docs/private.tmp", b"not documentation")
files = PACKER.collect(self.root, self.VERSION)
for name, data in documents.items():
with self.subTest(document=name):
self.assertEqual(data, files[name])
self.assertIn("AGENTS.md", files)
self.assertNotIn("docs/private.tmp", files)
def test_optional_root_documents_may_be_absent(self):
for name in ("AGENTS.md", "THIRD_PARTY_NOTICES.md"):
(self.root / name).unlink()
files = PACKER.collect(self.root, self.VERSION)
self.assertNotIn("AGENTS.md", files)
self.assertNotIn("THIRD_PARTY_NOTICES.md", files)
self.assertIn("GITEA-LICENSE", files)
if __name__ == "__main__":
unittest.main()