+38
-16
@@ -1,17 +1,17 @@
|
||||
import copy
|
||||
import argparse
|
||||
from fractions import Fraction
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from generator.arrange.generate import arrange_document
|
||||
from generator.engraving import EngravingError, check_page, engrave_tree, qualify
|
||||
from generator.engraving.audit import audit_roundtrip
|
||||
from generator.__main__ import generate_assets
|
||||
from generator.engraving import ENGINE_NAME, ENGINE_VERSION, EngravingError, Fonts, Style, check_page, engrave_tree, layout_score
|
||||
from tests.musicxml_roundtrip import audit_roundtrip
|
||||
from generator.model import fraction, pitch_number, rational, validate_document
|
||||
from generator.musicxml import load_musicxml
|
||||
from generator.musicxml_writer import write_musicxml
|
||||
@@ -21,6 +21,29 @@ from generator.transposition import transpose_document
|
||||
from tests.test_musicxml import notes, parse, xml_note, xml_score
|
||||
|
||||
class ScoreBundleTests(unittest.TestCase):
|
||||
def test_real_asset_regeneration_removes_pages_and_rolls_back_font_failure(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
data, output = root / 'data', root / 'output'
|
||||
lesson = data / 'practice' / 'chord' / 'lesson'
|
||||
lesson.mkdir(parents=True)
|
||||
(data / 'songs').mkdir()
|
||||
source = lesson / 'fixture.xml'
|
||||
source.write_text(xml_score(xml_note()))
|
||||
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), clean=False, check=False)
|
||||
generate_assets(args)
|
||||
original = {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()}
|
||||
self.assertTrue(list(output.rglob('*.png')))
|
||||
generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||
source.write_text(xml_score(xml_note(extra='<accidental smufl="unavailableGlyph">other</accidental>')))
|
||||
with self.assertRaisesRegex(EngravingError, 'unavailableGlyph'):
|
||||
generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||
source.unlink()
|
||||
generate_assets(args)
|
||||
self.assertFalse(list(output.rglob('*.png')))
|
||||
self.assertFalse(list(output.rglob('*.json')))
|
||||
def round_trip(self, document):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'roundtrip.xml'
|
||||
@@ -48,8 +71,8 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
self.assertEqual(notes(restored)[0]['pitches'], notes(transposed)[0]['pitches'])
|
||||
unqualified = parse(xml_score(xml_note(alter='0.1')))
|
||||
self.assertEqual(fraction(notes(unqualified)[0]['pitches'][0]['alter']), Fraction(1, 10))
|
||||
with self.assertRaisesRegex(EngravingError, 'microtonal'):
|
||||
qualify(unqualified)
|
||||
layout = layout_score(unqualified, Style(), Fonts(Path('package/assets/fonts/petaluma')))
|
||||
self.assertTrue(any(p.role == 'accidental' and p.value == '(+1/10 st)' for page in layout.pages for p in page.primitives))
|
||||
def test_octave_shift_stop_at_a_bar_boundary_is_emitted_once(self):
|
||||
tree = ElementTree.fromstring(xml_score(xml_note()))
|
||||
ElementTree.SubElement(tree.find('part'), 'measure', number='2').append(ElementTree.fromstring(xml_note()))
|
||||
@@ -61,8 +84,9 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
source = parse(xml_score(xml_note() + xml_note('E', 2, 'half', prefix='<chord/>')))
|
||||
restored = self.round_trip(source)
|
||||
self.assertEqual(notes(restored)[0]['pitches'], notes(source)[0]['pitches'])
|
||||
with self.assertRaisesRegex(EngravingError, 'independently sustained'):
|
||||
qualify(source)
|
||||
layout = layout_score(source, Style(), Fonts(Path('package/assets/fonts/petaluma')))
|
||||
heads = [p.value for page in layout.pages for p in page.primitives if p.role == 'head']
|
||||
self.assertEqual(heads, ['noteheadWhole', 'noteheadHalf'])
|
||||
def test_overlapping_notes_within_one_voice_are_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, 'overlapping'):
|
||||
parse(xml_score(xml_note() + '<backup><duration>4</duration></backup>' + xml_note('E')))
|
||||
@@ -113,15 +137,14 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
self.assertEqual(original['duration'], result['duration'])
|
||||
for v, w in zip(original['voices'], result['voices']):
|
||||
self.assertEqual(v['notes'], w['notes'])
|
||||
def test_wrong_engine_version_does_not_produce_a_bundle(self):
|
||||
def test_missing_font_does_not_produce_a_bundle(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
source = parse(xml_score(xml_note()))
|
||||
path = root / 'fixture.json'
|
||||
path.write_text(json.dumps(source))
|
||||
with patch.dict(os.environ, {'VAMP_MUSESCORE': '/engine'}), patch('generator.engraving.run_engine', return_value='MuseScore 3.6'):
|
||||
with self.assertRaisesRegex(EngravingError, 'expected MuseScore'):
|
||||
engrave_tree(root, Path('package/assets/fonts/petaluma'))
|
||||
with self.assertRaisesRegex(EngravingError, 'cannot load bundled'):
|
||||
engrave_tree(root, root / 'missing-fonts')
|
||||
self.assertEqual(json.loads(path.read_text()), source)
|
||||
def test_blank_and_wrong_sized_pages_are_rejected(self):
|
||||
from PIL import Image
|
||||
@@ -176,7 +199,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
page = assets / 'page.png'
|
||||
page.write_bytes(data)
|
||||
document = load_musicxml(Path('tests/fixtures/engraving.xml'))
|
||||
bundle = {'schema_version': 3, 'music': {k: v for k, v in document.items() if k != 'schema_version'}, 'presentation': {'engine': 'MuseScore Studio', 'engine_version': '4.7.4', 'width': 976, 'height': 484, 'pages': [{'path': 'page.png', 'sha256': hashlib.sha256(data).hexdigest()}]}}
|
||||
bundle = {'schema_version': 3, 'music': {k: v for k, v in document.items() if k != 'schema_version'}, 'presentation': {'engine': ENGINE_NAME, 'engine_version': ENGINE_VERSION, 'width': 976, 'height': 484, 'pages': [{'path': 'page.png', 'sha256': hashlib.sha256(data).hexdigest()}]}}
|
||||
path = assets / 'score.json'
|
||||
path.write_text(json.dumps(bundle))
|
||||
self.assertEqual(load_json(path), document)
|
||||
@@ -189,8 +212,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
page.symlink_to(outside)
|
||||
with self.assertRaisesRegex(ValueError, 'escapes'):
|
||||
load_json(path)
|
||||
@unittest.skipUnless(os.environ.get('VAMP_MUSESCORE'), 'pinned engraver integration runs in the managed build image')
|
||||
def test_pinned_engine_visual_golden_and_determinism(self):
|
||||
def test_python_engraver_visual_golden_and_determinism(self):
|
||||
from PIL import Image, ImageChops
|
||||
from generator.score import write_json
|
||||
document = load_musicxml(Path('tests/fixtures/engraving.xml'))
|
||||
@@ -204,7 +226,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
self.assertEqual(bundle['music'], {k: v for k, v in document.items() if k != 'schema_version'})
|
||||
self.assertEqual(len(bundle['presentation']['pages']), 1)
|
||||
page = root / bundle['presentation']['pages'][0]['path']
|
||||
with Image.open(page) as actual, Image.open('tests/fixtures/engraving.png') as expected:
|
||||
with Image.open(page) as actual, Image.open('tests/fixtures/engraving-python.png') as expected:
|
||||
self.assertIsNone(ImageChops.difference(actual.convert('RGB'), expected.convert('RGB')).getbbox())
|
||||
if before is not None:
|
||||
self.assertEqual(bundle, before)
|
||||
|
||||
Reference in New Issue
Block a user