201 lines
7.6 KiB
Python
201 lines
7.6 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",
|
|
"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)
|
|
self.assertFalse(any("stale" in name or "/build/" in name for name in zipped))
|
|
|
|
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()
|