"""Changelog exports and remote publication boundaries."""
import copy
import importlib.util
import json
import re
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
def module(name, file):
spec = importlib.util.spec_from_file_location(name, ROOT/'tools'/file)
result = importlib.util.module_from_spec(spec)
spec.loader.exec_module(result)
return result
export = module('export', 'export-changelog.py')
sync = module('sync', 'sync-forgejo-changelog.py')
class ChangelogTests(unittest.TestCase):
def setUp(self):
self.source, self.languages = export.load_source(ROOT/'changelog.json')
self.tag = 'android-2.7-preview.14'
def test_all_outputs_reuse_source_entries(self):
with tempfile.TemporaryDirectory() as d:
root = Path(d)
files = export.generate(ROOT/'changelog.json', root, root/'site.json', root/'releases')
data = json.loads(files[root/'site.json'])
self.assertEqual(data['languages'], self.languages)
for lang, name in [('fr', 'CHANGELOG.md'), ('en', 'CHANGELOG.en.md')]:
for entry in self.source['entries']:
self.assertIn(entry['text'][lang], files[root/name])
def test_range_and_summary(self):
documents = export.release_documents(self.source, self.tag)
self.assertTrue(documents['release-body.md'].startswith(self.source['releases'][self.tag]['summary']))
for lang in export.LANGUAGES:
text = documents[f'CHANGELOG.{lang}.md']
self.assertIn('## 30/09/2026 : 2.7-preview.14', text)
self.assertNotIn('## 2.7-156.0.1', text)
future = copy.deepcopy(self.source['entries'][0])
future['id'] = 'future'
self.source['entries'].insert(0, future)
self.assertEqual(documents, export.release_documents(self.source, self.tag))
def test_scope_and_fixed_ids(self):
self.assertTrue(any(g['platforms'] == ['pc', 'models']
for entry in self.languages['fr'] for g in entry['groups']))
self.assertEqual(len(self.source['entries']), len({e['id'] for e in self.source['entries']}))
def test_invalid_source_rejected_before_output(self):
with tempfile.TemporaryDirectory() as d:
p = Path(d)/'source.json'
del self.source['entries'][0]['text']['en']
p.write_text(json.dumps(self.source))
with self.assertRaisesRegex(ValueError, 'Missing translation'):
export.generate(p, Path(d), Path(d)/'site.json', Path(d)/'releases')
self.assertFalse((Path(d)/'site.json').exists())
def test_grouped_entries_keep_explicit_platforms_in_both_languages(self):
for language in export.LANGUAGES:
for source, rendered in zip(self.source['entries'], self.languages[language]):
self.assertEqual(rendered['platforms'], source['metadata']['platforms'])
self.assertEqual(rendered['models'], source['metadata']['models'])
self.assertEqual(rendered['status'], source['metadata']['status'])
self.assertEqual({p for group in rendered['groups'] for p in group['platforms']},
set(rendered['platforms']))
def test_date_first_heading_and_platform_badges(self):
for language in export.LANGUAGES:
entry = self.languages[language][0]
self.assertEqual(entry['id'], '2.7-preview.20')
self.assertEqual(entry['date'], '2026-10-13')
common = next(g for g in entry['groups'] if g['common'])
self.assertEqual(common['platforms'], ['pc', 'apk', 'models'])
web = next(g for g in entry['groups'] if g['name'] == 'Web')
self.assertEqual(web['platforms'], ['web'])
self.assertIn('>Web', web['nameHtml'])
self.assertEqual(entry['notes'], '')
self.assertEqual(export.group_scope('')[1], ['pc', 'models'])
with self.assertRaisesRegex(ValueError, 'Unknown platform'):
export.group_scope('Unknown')
def test_tags_alphabetical_in_every_platform_group(self):
for entry in self.source['entries']:
for lang in export.LANGUAGES:
for group in re.split(r'^### ', entry['text'][lang], flags=re.M)[1:]:
tags = re.findall(r'^- !\[([^\]]+)\]', group, re.M)
self.assertTrue(tags, (entry['id'], lang))
self.assertEqual(tags, sorted(tags), (entry['id'], lang))
self.assertLessEqual(set(tags), {'Add', 'Change', 'Fix', 'Note', 'Test'})
def test_release_summary_follows_the_entry_without_copying_text(self):
tag = 'v2.7-preview.20'
self.source['entries'][0]['text']['fr'] += '\nA new correction.\n'
docs = export.release_documents(self.source, tag)
self.assertIn('A new correction.', docs['release-body.md'])
def test_badges_are_images_with_escaped_attributes(self):
rendered = export.inline(' **Camera**')
self.assertIn('
Camera', rendered)
self.assertNotIn('Fix')
self.assertNotIn('