Files
.pouch/tests/test_builder_version.py
T
laily e60755f280 fix(builder): 版本时间格式改回 YYYYMMDDTHHMM
按要求保留字母分隔符:20260918T1722。分钟精度不变,同一分支同日多次
构建仍能正确排序。

记录切换边界:Debian 里字母排在标点之前,YYYYMMDDT… 小于旧格式的
YYYYMMDD.…,切换当天同日的旧格式包会压住新包,需要在仓库里把那些旧包
移除或停用;次日新格式始终更大。回归测试显式断言这行边界。
2026-09-19 13:32:16 +08:00

399 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = REPO_ROOT / "skills" / "builder" / "scripts"
VERSION_SH = SCRIPTS / "version.sh"
PUBLISH_DOCKER = SCRIPTS / "publish_docker.sh"
VERSION_MK = SCRIPTS / "version.mk"
# Frozen build instant so test versions are deterministic.
SOURCE_DATE_EPOCH = "1704067200" # 2024-01-01T00:00:00Z
BUILD_STAMP = "20240101T0000"
def git(cwd: Path, *args: str, env: dict[str, str] | None = None) -> str:
merged = os.environ.copy()
if env:
merged.update(env)
merged.setdefault("GIT_AUTHOR_NAME", "Test")
merged.setdefault("GIT_AUTHOR_EMAIL", "test@example.com")
merged.setdefault("GIT_COMMITTER_NAME", "Test")
merged.setdefault("GIT_COMMITTER_EMAIL", "test@example.com")
result = subprocess.run(
["git", *args],
cwd=cwd,
env=merged,
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AssertionError(f"git {args} failed: {result.stderr}")
return result.stdout.strip()
def commit(cwd: Path, message: str) -> str:
git(cwd, "add", "-A")
git(cwd, "commit", "-m", message)
return git(cwd, "rev-parse", "HEAD")
def sha7(cwd: Path) -> str:
return git(cwd, "rev-parse", "HEAD")[:7]
def version_sh(*args: str, cwd: Path, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
merged = os.environ.copy()
merged["SOURCE_DATE_EPOCH"] = SOURCE_DATE_EPOCH
if env:
merged.update(env)
return subprocess.run(
["bash", str(VERSION_SH), *args],
cwd=cwd,
env=merged,
text=True,
capture_output=True,
check=False,
)
def init_repo(path: Path) -> None:
git(path, "init", "-b", "main")
git(path, "config", "user.email", "test@example.com")
git(path, "config", "user.name", "Test")
class BuilderVersionTests(unittest.TestCase):
def test_version_sh_is_executable(self) -> None:
mode = VERSION_SH.stat().st_mode
self.assertTrue(mode & stat.S_IXUSR, "version.sh must be executable")
def test_official_on_stable_tag(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "-a", "v1.4.2", "-m", "1.4.2")
result = version_sh(cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "1.4.2\n")
docker = version_sh("--docker", cwd=repo)
self.assertEqual(docker.returncode, 0, docker.stderr)
self.assertEqual(docker.stdout, "1.4.2\n")
def test_post_release_commit_bumps_patch(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "release")
git(repo, "tag", "-a", "v1.2.0", "-m", "1.2.0")
(repo / "b").write_text("fix\n", encoding="utf-8")
commit(repo, "bugfix")
sha = sha7(repo)
result = version_sh(cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
result.stdout.strip(),
f"1.2.1~{BUILD_STAMP}.main.1+g{sha}",
)
docker = version_sh("--docker", cwd=repo)
self.assertEqual(
docker.stdout.strip(),
f"1.2.1-{BUILD_STAMP}.main.1.g{sha}",
)
def test_test_version_uses_ancestor_not_global_max(self) -> None:
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
repo = root / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "base")
git(repo, "tag", "v1.4.2")
git(repo, "checkout", "-b", "feat/login-v2")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "feature")
(repo / "c").write_text("3\n", encoding="utf-8")
commit(repo, "feature 2")
feature_sha = sha7(repo)
git(repo, "checkout", "main")
(repo / "d").write_text("4\n", encoding="utf-8")
commit(repo, "release")
git(repo, "tag", "v1.4.3")
git(repo, "checkout", "feat/login-v2")
result = version_sh(cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
result.stdout.strip(),
f"1.4.3~{BUILD_STAMP}.feat-login-v2.2+g{feature_sha}",
)
docker = version_sh("--docker", cwd=repo)
self.assertEqual(
docker.stdout.strip(),
f"1.4.3-{BUILD_STAMP}.feat-login-v2.2.g{feature_sha}",
)
def test_ignores_prerelease_and_app_tags(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v1.0.0")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "next")
git(repo, "tag", "v1.1.0-rc.1")
git(repo, "tag", "v1.1.0-app-1")
sha = sha7(repo)
result = version_sh(cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), f"1.0.1~{BUILD_STAMP}.main.1+g{sha}")
def test_no_stable_tag(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
sha = sha7(repo)
result = version_sh(cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), f"0.0.0~{BUILD_STAMP}.main+g{sha}")
def test_same_day_rebuild_sorts_higher(self) -> None:
"""同一天重建必须比上一包大,否则 apt 永远拿不到最新构建。
只写到日期时同分支同日的版本前缀完全相同,只剩 +g<sha> 可区分,
Debian 版本比较把哈希当普通文本,新包可能被判成比旧包小。
已知边界(故意断言):切换格式当天,旧格式同日包 `YYYYMMDD.…` 仍
大于新格式 `YYYYMMDDT…`,因为 Debian 把字母排在标点之前。那天需要
把旧格式包在仓库里移除或停用,或者等次日(次日新格式一定更大)。
"""
if shutil.which("dpkg") is None:
self.skipTest("dpkg not available")
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
early = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1704067200"}) # 00:00Z
later = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1704103200"}) # 10:00Z
self.assertEqual(early.returncode, 0, early.stderr)
self.assertEqual(later.returncode, 0, later.stderr)
first, second = early.stdout.strip(), later.stdout.strip()
self.assertEqual(
second,
f"0.0.0~20240101T1000.main+g{sha7(repo)}",
"构建时间精确到分钟,形如 YYYYMMDDTHHMM",
)
for label, a, b in (
("同日重建", second, first),
("旧格式前一日包", second, "0.0.0~20231231.main+gaaaaaaa"),
("次日新格式包", "0.0.0~20240102T1000.main+gccccccc", second),
("下一正式版", "0.1.0", second),
):
compared = subprocess.run(
["dpkg", "--compare-versions", a, "gt", b],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(compared.returncode, 0, f"{label}:{a} 应大于 {b}")
compared = subprocess.run(
["dpkg", "--compare-versions", "0.0.0~20240101.main+gaaaaaaa", "gt", second],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(
compared.returncode,
0,
"已知边界:旧格式同日包大于 YYYYMMDDT…,切换当天需要移除旧包或等次日",
)
def test_detached_uses_env_branch(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v0.1.0")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "next")
sha = sha7(repo)
git(repo, "checkout", "--detach", "HEAD")
result = version_sh(cwd=repo, env={"BUILD_BRANCH": "ci_job"})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), f"0.1.1~{BUILD_STAMP}.ci-job.1+g{sha}")
def test_source_date_epoch_changes_test_date(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v1.0.0")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "next")
sha = sha7(repo)
later = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"})
self.assertEqual(later.returncode, 0, later.stderr)
self.assertEqual(later.stdout.strip(), f"1.0.1~20240901T0000.main.1+g{sha}")
def test_official_version_ignores_build_date(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "-a", "v1.4.2", "-m", "1.4.2")
result = version_sh(cwd=repo, env={"SOURCE_DATE_EPOCH": "1725148800"})
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "1.4.2")
def test_override_official_rejected_off_tag(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v1.4.2")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "next")
result = version_sh("--version", "1.4.3", cwd=repo)
self.assertEqual(result.returncode, 2)
self.assertIn("looks official", result.stderr)
def test_override_official_ok_on_tag(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v1.4.2")
result = version_sh("--version", "v1.4.2", cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "1.4.2")
def test_override_test_shaped_accepted(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
result = version_sh("--version", "1.4.2~feat.1+gabc1234", "--docker", cwd=repo)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "1.4.2-feat.1.gabc1234")
def test_version_mk_sets_make_version(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "a").write_text("1\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v2.0.0")
makefile = repo / "Makefile"
makefile.write_text(
"include {mk}\n"
".PHONY: version\n"
"version:\n"
"\t@echo \"$(VERSION)\"\n".format(mk=VERSION_MK),
encoding="utf-8",
)
result = subprocess.run(
["make", "--no-print-directory", "-C", str(repo), "version"],
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
self.assertEqual(result.stdout.strip(), "2.0.0")
def test_publish_docker_dry_run_uses_derived_tag(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v3.1.4")
result = subprocess.run(
[
"bash",
str(PUBLISH_DOCKER),
"--registry",
"registry.example.com",
"--repository",
"ns/app",
"--dry-run",
],
cwd=repo,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr + result.stdout)
self.assertIn("Image: registry.example.com/ns/app:3.1.4", result.stdout)
def test_publish_docker_rejects_official_tag_off_head(self) -> None:
with tempfile.TemporaryDirectory() as temp:
repo = Path(temp) / "repo"
repo.mkdir()
init_repo(repo)
(repo / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
commit(repo, "init")
git(repo, "tag", "v1.0.0")
(repo / "b").write_text("2\n", encoding="utf-8")
commit(repo, "next")
result = subprocess.run(
[
"bash",
str(PUBLISH_DOCKER),
"--registry",
"registry.example.com",
"--repository",
"ns/app",
"--tag",
"1.0.1",
"--dry-run",
],
cwd=repo,
text=True,
capture_output=True,
check=False,
)
self.assertEqual(result.returncode, 2)
self.assertIn("looks official", result.stderr)
if __name__ == "__main__":
unittest.main()