Files
sharedinbox/scripts/test_generate_build_history.py
Thomas SharedInboxandClaude Sonnet 4.6 cf277064cc feat(builds): populate builds page with Linux and Android history (#94)
The builds page at /builds/ was empty because generate-build-history
only ran inside deploy-playstore; if that job failed early (e.g. Play
Store secrets not configured) the website was never updated, and the
build-linux job never triggered a website update at all.

Changes:
- generate_build_history.py: extend to cover Linux tarballs in addition
  to Android APKs, capped at MAX_BUILDS_PER_PLATFORM (30) each
- Taskfile: add website-publish task (generate-build-history +
  website-deploy), exclude *.tar.gz from rsync, update descriptions
- .forgejo/workflows/ci.yml: add publish-website job that waits for
  both build-linux and deploy-playstore (using always() so it runs
  even when deploy-playstore fails), then removes the duplicate
  generate/deploy steps from deploy-playstore
- .github/workflows/ci.yml: add deploy job that deploys Linux build,
  generates build history, builds Hugo site, and rsyncs to server
- .gitignore: ignore website/content/builds/_index.md (generated),
  Python __pycache__, and widget test failure screenshots
- stalwart-dev/integration_ui_test.sh: use ${USER:-$(id -un)} for
  robustness in environments where USER is unset
- scripts/test_generate_build_history.py: unit tests for parse_builds
  and render_entries covering both platforms

Generated content (builds/_index.md and per-day pages) is not tracked
in git; it is produced at CI time and rsynced to the server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 19:08:55 +02:00

114 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Tests for pure functions in generate_build_history.py."""
import re
import unittest
from unittest.mock import patch
from generate_build_history import MAX_BUILDS_PER_PLATFORM, parse_builds, render_entries
LINUX_RE = re.compile(
r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-linux-amd64-(.+)\.tar\.gz)$"
)
APK_RE = re.compile(
r"public_html/builds/(\d{4})/(\d{2})/(\d{2})/(sharedinbox-mua-(.+)\.apk)$"
)
def _fake_commit_info(hash_val: str):
return (f"feat: {hash_val}", "2025-05-10T12:00:00Z")
class TestParseBuilds(unittest.TestCase):
def setUp(self):
patcher = patch("generate_build_history.get_commit_info", side_effect=_fake_commit_info)
self.mock_commit = patcher.start()
self.addCleanup(patcher.stop)
def test_linux_path_parsed(self):
paths = ["public_html/builds/2025/05/10/sharedinbox-linux-amd64-abc1234.tar.gz"]
result = parse_builds(paths, LINUX_RE)
self.assertIn("2025/05/10", result)
entry = result["2025/05/10"][0]
self.assertEqual(entry[0], "abc1234")
self.assertIn("sharedinbox-linux-amd64-abc1234.tar.gz", entry[1])
def test_apk_path_parsed(self):
paths = ["public_html/builds/2025/05/11/sharedinbox-mua-def5678.apk"]
result = parse_builds(paths, APK_RE)
self.assertIn("2025/05/11", result)
entry = result["2025/05/11"][0]
self.assertEqual(entry[0], "def5678")
self.assertIn("sharedinbox-mua-def5678.apk", entry[1])
def test_unexpected_path_skipped(self):
paths = [
"public_html/builds/2025/05/10/sharedinbox-linux-amd64-abc1234.tar.gz",
"public_html/builds/bad-path/other.tar.gz",
]
result = parse_builds(paths, LINUX_RE)
self.assertEqual(len(result), 1)
def test_multiple_builds_same_day(self):
paths = [
"public_html/builds/2025/05/10/sharedinbox-linux-amd64-aaa0001.tar.gz",
"public_html/builds/2025/05/10/sharedinbox-linux-amd64-bbb0002.tar.gz",
]
result = parse_builds(paths, LINUX_RE)
self.assertEqual(len(result["2025/05/10"]), 2)
def test_limited_to_max_builds(self):
paths = [
f"public_html/builds/2025/05/{i:02d}/sharedinbox-linux-amd64-hash{i:03d}.tar.gz"
for i in range(1, MAX_BUILDS_PER_PLATFORM + 5)
]
result = parse_builds(paths, LINUX_RE)
total = sum(len(v) for v in result.values())
self.assertEqual(total, MAX_BUILDS_PER_PLATFORM)
def test_download_url_contains_date_and_filename(self):
paths = ["public_html/builds/2025/03/15/sharedinbox-linux-amd64-cafebabe.tar.gz"]
result = parse_builds(paths, LINUX_RE)
url = result["2025/03/15"][0][1]
self.assertIn("/2025/03/15/", url)
self.assertIn("sharedinbox-linux-amd64-cafebabe.tar.gz", url)
self.assertTrue(url.startswith("https://"))
class TestRenderEntries(unittest.TestCase):
def _make_entry(self, hash_val="abc1234", url="https://example.com/file.apk",
title="feat: something", dt="2025-05-10T12:00:00Z"):
return (hash_val, url, title, dt)
def test_output_contains_title_and_link(self):
entry = self._make_entry()
out = render_entries([entry], "Download APK")
self.assertIn("feat: something", out)
self.assertIn("Download APK", out)
self.assertIn("abc1234", out)
def test_commit_url_uses_hash(self):
entry = self._make_entry(hash_val="deadbeef")
out = render_entries([entry], "Download")
self.assertIn("deadbeef", out)
self.assertIn("codeberg.org", out)
def test_datetime_shown_when_present(self):
entry = self._make_entry(dt="2025-05-10T12:00:00Z")
out = render_entries([entry], "Download")
self.assertIn("2025-05-10T12:00:00Z", out)
def test_datetime_omitted_when_empty(self):
entry = self._make_entry(dt="")
out = render_entries([entry], "Download")
self.assertNotIn(" · ", out)
def test_multiple_entries_all_rendered(self):
entries = [self._make_entry(hash_val=f"hash{i}", title=f"commit {i}") for i in range(3)]
out = render_entries(entries, "Download")
for i in range(3):
self.assertIn(f"commit {i}", out)
if __name__ == "__main__":
unittest.main()