|
|
|
@@ -1,6 +1,8 @@
|
|
|
|
|
import argparse
|
|
|
|
|
import contextlib
|
|
|
|
|
import copy
|
|
|
|
|
import hashlib
|
|
|
|
|
import io
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import tempfile
|
|
|
|
@@ -9,14 +11,16 @@ from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
from generator.__main__ import generate_assets
|
|
|
|
|
from generator.engraving import EngravingError, Fonts, Style, engrave_tree, layout_score
|
|
|
|
|
from generator.engraving.cache import renderer_fingerprint, restore_pages, score_fingerprint
|
|
|
|
|
from generator.engraving.cache import renderer_fingerprint, renderer_namespace, restore_pages, score_fingerprint
|
|
|
|
|
from generator.score import validate_generated_tree
|
|
|
|
|
from tests.test_musicxml import parse, xml_note, xml_score
|
|
|
|
|
|
|
|
|
|
class EngravingCacheTests(unittest.TestCase):
|
|
|
|
|
def test_ci_snapshot_tracks_all_generator_inputs_and_keeps_fallback(self):
|
|
|
|
|
workflow = Path('.gitea/workflows/ci.yml').read_text()
|
|
|
|
|
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'data/**/*.xml', 'data/*.xml')", workflow)
|
|
|
|
|
self.assertIn('restore-keys: product-vamp-pages-v1-${{ runner.os }}-', workflow)
|
|
|
|
|
self.assertIn('python3 -m generator --renderer-fingerprint', workflow)
|
|
|
|
|
self.assertIn('restore-keys: product-vamp-pages-v2-${{ steps.renderer.outputs.fingerprint }}-', workflow)
|
|
|
|
|
self.assertIn("if: steps.engraving-cache.outputs.cache-hit != 'true'", workflow)
|
|
|
|
|
self.assertIn('key: ${{ steps.engraving-cache.outputs.cache-primary-key }}', workflow)
|
|
|
|
|
def setUp(self):
|
|
|
|
@@ -36,7 +40,7 @@ class EngravingCacheTests(unittest.TestCase):
|
|
|
|
|
root = self.root / 'seed'
|
|
|
|
|
self.write_scores(root, [self.document])
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache)
|
|
|
|
|
return next(self.cache.iterdir())
|
|
|
|
|
return next(self.cache.rglob('pages.json')).parent
|
|
|
|
|
def test_cache_hit_preserves_bundle_bytes_across_output_directories(self):
|
|
|
|
|
cold, warm = self.root / 'cold', self.root / 'warm'
|
|
|
|
|
self.write_scores(cold, [self.document])
|
|
|
|
@@ -65,7 +69,7 @@ class EngravingCacheTests(unittest.TestCase):
|
|
|
|
|
with patch('generator.engraving.layout_score', wraps=layout_score) as render:
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache)
|
|
|
|
|
render.assert_called_once()
|
|
|
|
|
self.assertEqual(len(list(self.cache.iterdir())), 2)
|
|
|
|
|
self.assertEqual(len(list(self.cache.rglob('pages.json'))), 2)
|
|
|
|
|
def test_renderer_fingerprint_tracks_fonts_style_code_and_dependencies(self):
|
|
|
|
|
fonts, style = Fonts(self.fonts), Style()
|
|
|
|
|
expected = renderer_fingerprint(fonts, style)
|
|
|
|
@@ -75,6 +79,8 @@ class EngravingCacheTests(unittest.TestCase):
|
|
|
|
|
self.assertNotEqual(expected, renderer_fingerprint(fonts, style))
|
|
|
|
|
with patch('generator.engraving.cache.importlib.metadata.version', return_value='new-version'):
|
|
|
|
|
self.assertNotEqual(expected, renderer_fingerprint(fonts, style))
|
|
|
|
|
with patch('generator.engraving.cache.platform.python_version', return_value='0.0.0'):
|
|
|
|
|
self.assertNotEqual(expected, renderer_fingerprint(fonts, style))
|
|
|
|
|
read_bytes = Path.read_bytes
|
|
|
|
|
with patch.object(Path, 'read_bytes', lambda path: read_bytes(path) + (b'\n' if path.name == 'raster.py' else b'')):
|
|
|
|
|
self.assertNotEqual(expected, renderer_fingerprint(fonts, style))
|
|
|
|
@@ -129,8 +135,46 @@ class EngravingCacheTests(unittest.TestCase):
|
|
|
|
|
self.write_scores(root, documents)
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache, jobs=2)
|
|
|
|
|
self.assertEqual(self.files(serial), self.files(root))
|
|
|
|
|
self.assertEqual(len(list(self.cache.iterdir())), 2)
|
|
|
|
|
self.assertFalse(list(self.cache.glob('.engraving-*')))
|
|
|
|
|
self.assertEqual(len(list(self.cache.rglob('pages.json'))), 2)
|
|
|
|
|
self.assertFalse(list(self.cache.rglob('.engraving-*')))
|
|
|
|
|
def test_environment_change_gets_new_namespace_and_clear_progress(self):
|
|
|
|
|
old = self.seed_cache()
|
|
|
|
|
root = self.root / 'new-environment'
|
|
|
|
|
self.write_scores(root, [self.document])
|
|
|
|
|
progress = io.StringIO()
|
|
|
|
|
with patch('generator.engraving.cache.platform.python_version', return_value='0.0.0'), contextlib.redirect_stdout(progress):
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache, progress=True)
|
|
|
|
|
self.assertIn('cold namespace', progress.getvalue())
|
|
|
|
|
self.assertIn('0 reused, 1 missing, 0 rejected', progress.getvalue())
|
|
|
|
|
self.assertTrue((old / 'pages.json').is_file())
|
|
|
|
|
self.assertEqual(len(list((self.cache / 'v2').iterdir())), 2)
|
|
|
|
|
self.write_scores(root, [self.document])
|
|
|
|
|
progress = io.StringIO()
|
|
|
|
|
with patch('generator.engraving.cache.platform.python_version', return_value='0.0.0'), contextlib.redirect_stdout(progress), patch('generator.engraving.layout_score', side_effect=AssertionError('cache miss')):
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache, progress=True)
|
|
|
|
|
self.assertIn('1 reused, 0 missing, 0 rejected', progress.getvalue())
|
|
|
|
|
def test_namespace_symlink_is_rejected(self):
|
|
|
|
|
self.cache.mkdir()
|
|
|
|
|
(self.cache / 'v2').symlink_to(self.root, target_is_directory=True)
|
|
|
|
|
with self.assertRaisesRegex(ValueError, 'must not be symlinks'):
|
|
|
|
|
renderer_namespace(self.cache, 'renderer')
|
|
|
|
|
def test_only_changed_score_is_rendered_and_validation_reads_json_once(self):
|
|
|
|
|
root = self.root / 'scores'
|
|
|
|
|
self.write_scores(root, [self.document, self.document])
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache)
|
|
|
|
|
changed = parse(xml_score(xml_note('D')))
|
|
|
|
|
self.write_scores(root, [self.document, changed])
|
|
|
|
|
with patch('generator.engraving.layout_score', wraps=layout_score) as render:
|
|
|
|
|
engrave_tree(root, self.fonts, cache_dir=self.cache)
|
|
|
|
|
render.assert_called_once()
|
|
|
|
|
read_text = Path.read_text
|
|
|
|
|
reads = []
|
|
|
|
|
def read(path, *args, **kwargs):
|
|
|
|
|
reads.append(path)
|
|
|
|
|
return read_text(path, *args, **kwargs)
|
|
|
|
|
with patch.object(Path, 'read_text', read):
|
|
|
|
|
validate_generated_tree(root)
|
|
|
|
|
self.assertCountEqual(reads, sorted(root.glob('*.json')))
|
|
|
|
|
def test_invalid_worker_count_and_overlapping_cache_are_rejected(self):
|
|
|
|
|
root = self.root / 'assets'
|
|
|
|
|
self.write_scores(root, [self.document])
|
|
|
|
|