Data Labels
ci / checks (push) Failing after 3h12m18s

This commit is contained in:
khodowany
2026-09-20 02:07:26 -05:00
parent 5fc2103810
commit 7bb1ed2b13
24 changed files with 2334 additions and 2563 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

+1 -1
View File
@@ -79,7 +79,7 @@ class GuitarPracticeTests(unittest.TestCase):
source = parse(xml_score(harmony + xml_note('E', alter=-1), '<key><fifths>-3</fifths><mode>major</mode></key>'))
result = transpose_guitar_practice(source, 'D')
self.assertEqual(pitch_number(notes(result)[0]['pitches'][0]), 62)
self.assertEqual(result['staves'][0]['measures'][0]['harmonies'][0]['symbol'], 'D/F#')
self.assertEqual(result['staves'][0]['measures'][0]['harmonies'][0]['symbol'], 'DM/F#')
for mode, fifths in [('minor', -3), ('dorian', -2)]:
source = parse(xml_score(xml_note(), f'<key><fifths>{fifths}</fifths><mode>{mode}</mode></key>'))
result = transpose_guitar_practice(source, 'D')
+100
View File
@@ -0,0 +1,100 @@
from pathlib import Path
import tempfile
import unittest
from generator import harmony
from generator.model import rational
from generator.musicxml import MusicXMLError, load_musicxml
from generator.musicxml_writer import write_musicxml
from generator.practice.generate import practice_document
from generator.transposition import transpose_document
from tests.test_musicxml import parse, xml_note, xml_score
def chord_xml(text, kind='other', root='C', extra=''):
return f'<harmony><root><root-step>{root}</root-step></root><kind text="{text}">{kind}</kind>{extra}</harmony>'
class HarmonyTests(unittest.TestCase):
def test_quality_spellings_are_canonical(self):
cases = {'C': 'CM', 'Cmaj': 'CM', 'Cmin': 'Cm', 'C-': 'Cm', 'C+': 'Caug', 'Co': 'Cdim', 'Csus4': 'Csus', 'Csus2': 'Csus2',
'Cmaj7': 'CM7', 'C-7': 'Cm7', 'C+7': 'Caug7', 'C+maj7': 'CaugM7', 'Co7': 'Cdim7', 'C-7b5': 'Cm7b5',
'C-maj7': 'CmM7', 'CmM': 'CmM7', 'C6': 'CM6', 'C-6': 'Cm6', 'Fmaj7#11/A': 'FM7#11/A', 'G7b9': 'G7b9'}
for spelling, canonical in cases.items():
with self.subTest(spelling=spelling):
chord = harmony.parse_symbol(spelling)
self.assertEqual(harmony.symbol(chord), canonical)
self.assertEqual(harmony.degrees(harmony.parse_symbol(canonical)), harmony.degrees(chord))
def test_extended_chords_include_sevenths_but_added_ninths_do_not(self):
for prefix, seventh in (('', -1), ('m', -1), ('M', 0), ('mM', 0)):
for highest in (9, 11, 13):
with self.subTest(prefix=prefix, highest=highest):
chord = harmony.parse_symbol(f'C{prefix}{highest}')
values = harmony.degrees(chord)
self.assertEqual(values[7], seventh)
self.assertTrue(all(values[degree] == 0 for degree in range(9, highest + 1, 2)))
self.assertEqual(harmony.symbol(chord), f'C{prefix}{highest}')
self.assertEqual(harmony.degrees(harmony.parse_symbol('Cmadd9')), {1: 0, 3: -1, 5: 0, 9: 0})
self.assertEqual(harmony.degrees(harmony.parse_symbol('C7no5')), {1: 0, 3: 0, 7: -1})
self.assertEqual(harmony.degrees(harmony.parse_symbol('C7sus')), {1: 0, 4: 0, 5: 0, 7: -1})
def test_soundslice_text_only_chords_become_structured(self):
for text, kind, values in (('mM7', 'major-minor', {1: 0, 3: -1, 5: 0, 7: 0}), ('m7b5', 'half-diminished', {1: 0, 3: -1, 5: -1, 7: -1}), ('7b9', 'dominant', {1: 0, 3: 0, 5: 0, 7: -1, 9: -1})):
with self.subTest(text=text):
source = parse(xml_score(chord_xml(text) + xml_note()))
chord = source['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(chord['kind'], kind)
self.assertEqual(harmony.degrees(chord), values)
self.assertEqual(chord['symbol'], 'C' + text)
self.assertNotIn('kind_text', chord)
source = parse(xml_score(chord_xml('m9', 'minor-ninth') + xml_note()))
self.assertEqual(harmony.degrees(source['staves'][0]['measures'][0]['harmonies'][0])[7], -1)
def test_parenthesized_full_symbols_transpose_and_roundtrip(self):
for label, transposed in (('(Fm7)', '(Gm7)'), ('(Bb7)', '(C7)'), ('(Fm7/Ab)', '(Gm7/Bb)')):
with self.subTest(label=label):
source = parse(xml_score(chord_xml(label) + xml_note()))
chord = source['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(chord['symbol'], label)
self.assertTrue(chord['chord_optional'])
self.assertTrue(chord['parenthesized'])
practice = practice_document(source)['staves'][0]['measures'][0]['harmonies'][0]
self.assertNotIn('chord_optional', practice)
self.assertEqual(harmony.symbol(practice), label)
shifted = transpose_document(source, 'D')
result = shifted['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(result['symbol'], transposed)
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / 'score.xml'
write_musicxml(path, shifted)
restored = load_musicxml(path)['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(restored, result)
def test_requested_labels_are_engraved_without_overlaps(self):
from generator.engraving import Fonts, Style, layout_score
labels = ('CM', 'Cm7', 'CM13#11', 'CmM7', 'CaugM7', 'Cdim7', 'Csus', 'Csus2', 'C7sus4', '(Fm7/Ab)')
for label in labels:
with self.subTest(label=label):
source = parse(xml_score(chord_xml(label) + xml_note()))
layout = layout_score(source, Style(), Fonts(Path('package/assets/fonts/petaluma')))
rendered = [p.value for page in layout.pages for p in page.primitives if p.role == 'harmony']
self.assertEqual(rendered, [harmony.symbol(harmony.parse_symbol(label))])
def test_unknown_or_contradictory_symbols_are_rejected(self):
for label in ('Cunknown', 'Cm79', 'C7b9#9', 'C7add7', 'C7no3no3', 'C7no#5', '(Cm7', 'Cm7)', 'C7/G/Ab'):
with self.subTest(label=label), self.assertRaises(ValueError):
harmony.parse_symbol(label)
for content in (chord_xml('m7', 'major-seventh'), chord_xml('(Fm7)', 'minor-seventh'), chord_xml('(Fm7)', root='D'), chord_xml('m7/G', extra='<bass><bass-step>A</bass-step></bass>')):
with self.subTest(content=content), self.assertRaisesRegex(MusicXMLError, 'contradictory'):
parse(xml_score(content + xml_note()))
def test_kind_text_cannot_override_canonical_output(self):
chord = harmony.parse_symbol('Cm7') | {'kind_text': '-7'}
self.assertEqual(harmony.symbol(chord), 'Cm7')
source = parse(xml_score(chord_xml('7', 'dominant', extra='<degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree>') + xml_note()))
self.assertEqual(source['staves'][0]['measures'][0]['harmonies'][0]['symbol'], 'C7b9')
def test_current_exercise_and_song_exports_import_without_edits(self):
exercise = load_musicxml(Path('data/001.xml'))
measures = exercise['staves'][0]['measures']
self.assertEqual(len(measures), 20)
self.assertEqual(sum(len(n['pitches']) for m in measures for v in m['voices'] for n in v['notes']), 70)
self.assertEqual(measures[1]['harmonies'][0]['symbol'], 'Bm9')
self.assertEqual(measures[3]['harmonies'][0]['symbol'], 'DmM7')
self.assertEqual(measures[0]['voices'][0]['notes'][0]['duration'], rational(1))
for name, count, key in (('autumn-leaves', 33, 'F'), ('it-could-happen-to-you', 32, 'Eb')):
source = load_musicxml(Path('data/songs') / f'{name}.xml')
self.assertEqual(len(source['staves'][0]['measures']), count)
self.assertEqual(source['metadata']['key_signature'], key)
+14
View File
@@ -80,6 +80,20 @@ class MusicModelTests(unittest.TestCase):
self.assertFalse(measures[-1]['harmonies'])
self.assertTrue(any(h for m in measures[:-1] for h in m['harmonies']))
self.assertFalse(any('chord_optional' in h for m in measures for h in m['harmonies']))
labels = [h['symbol'] for m in measures[:-1] for h in m['harmonies']]
self.assertIn('(Fm7)', labels)
self.assertIn('(Bb7)', labels)
def test_legacy_repeat_model_fields_are_rejected(self):
for field in ('repeats', 'endings', 'initial_attributes'):
source = parse(xml_score(xml_note()))
source['staves'][0]['measures'][0][field] = {}
with self.assertRaisesRegex(ValueError, 'repeat notation'):
validate_document(source)
for field in ('sound_ties', 'printed_ties'):
source = parse(xml_score(xml_note()))
notes(source)[0]['pitches'][0][field] = []
with self.assertRaisesRegex(ValueError, 'pass-specific tie'):
validate_document(source)
def test_player_styles_preserved(self):
self.assertEqual(len(BUILT_IN_STYLES), 50)
for text in ('Med.', 'Med. Swing', 'Medium', 'Medium Swing'):
+23 -7
View File
@@ -91,18 +91,18 @@ class MusicXMLTests(unittest.TestCase):
self.assertEqual(fraction(result[0]['pitches'][1]['duration']), 1)
self.assertEqual(result[0]['pitches'][0]['tie_to'], result[1]['pitches'][0]['id'])
self.assertNotIn('tie_to', result[0]['pitches'][1])
def test_standard_minor_harmonies_preserve_display_and_pitches(self):
def test_standard_minor_harmonies_normalize_display_and_preserve_pitches(self):
from generator.harmony import pitch_classes
for text, kind, expected in (('-7', 'minor-seventh', (0, 3, 7, 10)), ('-7b5', 'half-diminished', (0, 3, 6, 10)), ('-maj7', 'major-minor', (0, 3, 7, 11)), ('-6', 'minor-sixth', (0, 3, 7, 9))):
for text, kind, label, expected in (('-7', 'minor-seventh', 'm7', (0, 3, 7, 10)), ('-7b5', 'half-diminished', 'm7b5', (0, 3, 6, 10)), ('-maj7', 'major-minor', 'mM7', (0, 3, 7, 11)), ('-6', 'minor-sixth', 'm6', (0, 3, 7, 9))):
with self.subTest(text=text):
content = f'<harmony><root><root-step>C</root-step></root><kind text="{text}">{kind}</kind></harmony>'
score = parse(xml_score(content + xml_note()))
chord = score['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(chord['symbol'], 'C' + text)
self.assertEqual(chord['symbol'], 'C' + label)
self.assertEqual(pitch_classes(chord), expected)
def test_other_harmony_requires_explicit_degrees(self):
for kind in ('<kind>other</kind>', '<kind text="">other</kind>', '<kind text="-7">other</kind>', '<kind text="unknown">other</kind>'):
with self.subTest(kind=kind), self.assertRaisesRegex(MusicXMLError, r"fixture.xml: part P1, measure 1: harmony kind 'other' requires explicit added degrees"):
def test_other_harmony_requires_degrees_or_recognized_text(self):
for kind in ('<kind>other</kind>', '<kind text="">other</kind>', '<kind text="unknown">other</kind>'):
with self.subTest(kind=kind), self.assertRaisesRegex(MusicXMLError, r'fixture.xml: part P1, measure 1:'):
parse(xml_score('<harmony><root><root-step>C</root-step></root>' + kind + '</harmony>' + xml_note()))
def test_other_harmony_with_explicit_degrees_remains_supported(self):
from generator.harmony import pitch_classes
@@ -111,7 +111,7 @@ class MusicXMLTests(unittest.TestCase):
score = parse(xml_score(content + xml_note()))
chord = score['staves'][0]['measures'][0]['harmonies'][0]
self.assertEqual(chord['kind'], 'other')
self.assertEqual(chord['symbol'], 'C-7')
self.assertEqual(chord['symbol'], 'Cm7')
self.assertEqual(pitch_classes(chord), (0, 3, 7, 10))
with self.assertRaisesRegex(MusicXMLError, 'contradictory harmony text'):
parse(xml_score(content.replace('text="-7"', 'text="maj7"') + xml_note()))
@@ -162,6 +162,22 @@ class MusicXMLTests(unittest.TestCase):
for content in (marker + xml_note(), marker + marker + h + xml_note(), marker.replace('optional-chord', 'unknown') + xml_note()):
with self.assertRaisesRegex(MusicXMLError, 'measure 1'):
parse(xml_score(content))
def test_repeat_navigation_and_pass_specific_ties_are_rejected(self):
rejected = [
'<barline location="left"><repeat direction="forward"/></barline>',
'<barline><repeat direction="backward" times="3"/></barline>',
'<barline location="left"><ending number="1" type="start"/></barline>',
'<barline><ending number="2" type="discontinue" print-object="no"/></barline>',
'<direction><direction-type><segno/></direction-type></direction>',
'<direction><direction-type><coda/></direction-type></direction>',
'<sound dacapo="yes"/>', '<sound dalsegno="segno"/>', '<sound tocoda="coda"/>',
]
for content in rejected:
with self.subTest(content=content), self.assertRaisesRegex(MusicXMLError, 'measure 1'):
parse(xml_score(content + xml_note()))
for passes in ('1', ''):
with self.assertRaisesRegex(MusicXMLError, 'pass-specific ties'):
parse(xml_score(xml_note(extra=f'<tie type="start" time-only="{passes}"/>')))
def test_rejects_musical_content_outside_contract(self):
extras = ['<notations><slur type="start"/></notations>', '<notations><articulations><accent/></articulations></notations>',
'<voice>2</voice><staff>2</staff>', '<pitch><unpitched/></pitch>',
-139
View File
@@ -1,139 +0,0 @@
import argparse
import json
import unittest
from pathlib import Path
import tempfile
from unittest.mock import patch
from generator.model import fraction, validate_document
from generator.musicxml import MusicXMLError
from generator.playback import compile_timeline
from generator.repeats import expand_document, performed_order
from tests.test_musicxml import parse, xml_note, xml_score
def score_bars(bars):
return xml_score(bars[0]).replace('</measure></part>', '</measure>' + ''.join(f'<measure number="{i + 2}">{bar}</measure>' for i, bar in enumerate(bars[1:])) + '</part>')
def boundary(direction, times=None):
side = 'left' if direction == 'forward' else 'right'
count = f' times="{times}"' if times else ''
return f'<barline location="{side}"><repeat direction="{direction}"{count}/></barline>'
def ending(numbers, action, repeat=False):
side = 'left' if action == 'start' else 'right'
return f'<barline location="{side}"><ending number="{numbers}" type="{action}"/>' + ('<repeat direction="backward"/>' if repeat else '') + '</barline>'
class RepeatTests(unittest.TestCase):
def test_repeat_count_and_implicit_start(self):
source = parse(score_bars([xml_note(), xml_note('D') + boundary('backward', 3)]))
self.assertEqual([o.measure for o in performed_order(source['staves'][0]['measures'])], [0, 1] * 3)
expanded = expand_document(source)
validate_document(expanded)
self.assertEqual(len(compile_timeline(source)['events']), 6)
def test_combined_endings(self):
source = parse(score_bars([boundary('forward') + xml_note(), ending('1,2', 'start') + xml_note('D') + ending('1,2', 'stop', True), ending('3', 'start') + xml_note('E') + ending('3', 'discontinue')]))
self.assertEqual([o.measure for o in performed_order(source['staves'][0]['measures'])], [0, 1, 0, 1, 0, 2])
self.assertIn('endings', source['staves'][0]['measures'][1])
self.assertNotIn('endings', expand_document(source)['staves'][0]['measures'][1])
def test_nested_repeat(self):
source = parse(score_bars([boundary('forward') + xml_note(), boundary('forward') + xml_note('D') + boundary('backward'), xml_note('E') + boundary('backward')]))
self.assertEqual([o.measure for o in performed_order(source['staves'][0]['measures'])], [0, 1, 1, 2] * 2)
def test_sequential_and_expansion_limit(self):
source = parse(score_bars([xml_note() + boundary('backward'), boundary('forward') + xml_note('D') + boundary('backward', 3)]))
self.assertEqual([o.measure for o in performed_order(source['staves'][0]['measures'])], [0, 0, 1, 1, 1])
with self.assertRaisesRegex(MusicXMLError, '10000'):
parse(score_bars([boundary('forward') + xml_note(), xml_note() + boundary('backward', 10000)]))
def test_bad_endings_and_open_repeat(self):
for content in (boundary('forward') + xml_note(), ending('1', 'start') + xml_note(), ending('1', 'start') + xml_note() + ending('1', 'stop')):
with self.assertRaises(MusicXMLError):
parse(xml_score(content))
def test_ties_reidentify_each_pass(self):
source = parse(score_bars([boundary('forward') + xml_note(extra='<tie type="start"/>'), xml_note(extra='<tie type="stop"/>') + boundary('backward')]))
validate_document(expand_document(source))
self.assertEqual(len(compile_timeline(source)['events']), 2)
def test_ties_across_repeat_jump_and_unreachable_pass(self):
first = xml_note(extra='<tie type="stop" time-only="2"/>')
last = xml_note(extra='<tie type="start" time-only="1"/>')
source = parse(score_bars([boundary('forward') + first, last + boundary('backward')]))
self.assertEqual([fraction(e['duration']) for e in compile_timeline(source)['events']], [4, 8, 4])
for bad in (first.replace('time-only="2"', ''), first.replace('time-only="2"', 'time-only="3"')):
with self.assertRaisesRegex(MusicXMLError, 'tie'):
parse(score_bars([boundary('forward') + bad, last + boundary('backward')]))
def test_printed_tie_does_not_create_sound_tie(self):
source = parse(score_bars([boundary('forward') + xml_note(extra='<notations><tied type="start"/></notations>'), xml_note(extra='<notations><tied type="stop"/></notations>') + boundary('backward')]))
self.assertEqual(len(compile_timeline(source)['events']), 4)
def test_ties_into_both_endings(self):
source = parse(score_bars([boundary('forward') + xml_note(extra='<tie type="start"/>'), ending('1', 'start') + xml_note(extra='<tie type="stop"/>') + ending('1', 'stop', True), ending('2', 'start') + xml_note(extra='<tie type="stop"/>') + ending('2', 'discontinue')]))
self.assertEqual([fraction(e['duration']) for e in compile_timeline(source)['events']], [8, 8])
def test_cross_measure_tuplets_and_tremolos_remap(self):
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
bars = [boundary('forward'), '']
for index in range(3):
bars[0] += xml_note(duration=4, kind='half', extra=extra + ('<notations><tuplet type="start"/></notations>' if index == 0 else ''))
bars[1] += xml_note(duration=4, kind='half', extra=extra + ('<notations><tuplet type="stop"/></notations>' if index == 2 else ''))
bars[1] += boundary('backward')
source = parse(score_bars(bars).replace('<divisions>1</divisions>', '<divisions>3</divisions>'))
expanded = expand_document(source)
validate_document(expanded)
self.assertEqual(len(expanded['staves'][0]['tuplets']), 2)
modulation = '<time-modification><actual-notes>2</actual-notes><normal-notes>1</normal-notes></time-modification>'
bars = [xml_note(step, kind='breve', extra=modulation + f'<notations><ornaments><tremolo type="{action}">3</tremolo></ornaments></notations>') for step, action in [('C', 'start'), ('G', 'stop')]]
source = parse(score_bars([boundary('forward') + bars[0], bars[1] + boundary('backward')]))
validate_document(expand_document(source))
self.assertEqual(len(compile_timeline(source)['events']), 128)
def test_no_repeat_is_unchanged(self):
source = parse(xml_score(xml_note()))
self.assertEqual(source, expand_document(source))
def test_roundtrip_and_engraving(self):
from generator.musicxml_writer import write_musicxml
from generator.musicxml import load_musicxml
from generator.engraving import Fonts, Style, layout_score
source = parse(score_bars([boundary('forward') + xml_note(), ending('1', 'start') + xml_note('D') + ending('1', 'stop', True), ending('2', 'start') + xml_note('E') + ending('2', 'discontinue')]))
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / 'repeat.xml'
write_musicxml(path, source)
restored = load_musicxml(path)
self.assertEqual(performed_order(source['staves'][0]['measures']), performed_order(restored['staves'][0]['measures']))
layout = layout_score(source, Style(), Fonts(Path('package/assets/fonts/petaluma')))
items = [p for page in layout.pages for p in page.primitives]
self.assertEqual(sum(p.role == 'repeat-dot' for p in items), 4)
self.assertEqual([p.value for p in items if p.role == 'ending-label'], ['1.', '2.'])
def test_printed_tie_continues_across_system_boundary(self):
from generator.engraving import Fonts, Style
from generator.engraving.layout import make_system
from generator.engraving.notation import ScoreIndex
source = parse(score_bars([boundary('forward') + xml_note(extra='<notations><tied type="start"/></notations>'), xml_note(extra='<notations><tied type="stop"/></notations>') + boundary('backward')]))
index = ScoreIndex(source)
fonts = Fonts(Path('package/assets/fonts/petaluma'))
for start, end in [(0, 4), (4, 8)]:
system = make_system(index, start, end, fonts, Style())
self.assertTrue(any(p.role == 'tie' and len(p.sources) == 2 for p in system.primitives))
def test_build_pipeline_keeps_compact_practice_and_compiles_performed_order(self):
from generator.__main__ import generate_assets
from generator.arrange.generate import arrange_document
text = score_bars([boundary('forward') + xml_note(), xml_note('D') + boundary('backward')])
source = parse(text)
self.assertEqual(arrange_document(source, 'vocal'), arrange_document(expand_document(source), 'vocal'))
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
data = root / 'data'
(data / 'songs').mkdir(parents=True)
(data / '001.xml').write_text(text)
output = root / 'assets'
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), check=False)
generate_assets(args)
bundle = json.loads((output / 'practice/001/c.json').read_text())
self.assertEqual(len(bundle['music']['staves'][0]['measures']), 2)
self.assertEqual(len(bundle['playback']['events']), 4)
self.assertEqual(fraction(bundle['playback']['duration']), 16)
before = {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()}
generate_assets(args)
self.assertEqual(before, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
with patch('generator.__main__.compile_tree', side_effect=ValueError('invalid generated audio')):
with self.assertRaisesRegex(ValueError, 'audio'):
generate_assets(args)
self.assertEqual(before, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
def test_alternate_ending_does_not_inherit_skipped_key_change(self):
source = parse(score_bars([boundary('forward') + xml_note(), ending('1', 'start') + '<attributes><key><fifths>2</fifths></key></attributes>' + xml_note('D') + ending('1', 'stop', True), ending('2', 'start') + xml_note('E') + ending('2', 'discontinue')]))
expanded = expand_document(source)
self.assertEqual([m['key']['fifths'] for m in expanded['staves'][0]['measures']], [0, 2, 0, 0])
+25
View File
@@ -6,6 +6,7 @@ import json
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
@@ -34,6 +35,14 @@ class ScoreBundleTests(unittest.TestCase):
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()})
with patch('generator.__main__.compile_tree', side_effect=ValueError('invalid generated audio')):
with self.assertRaisesRegex(ValueError, 'audio'):
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() + '<barline><repeat direction="backward"/></barline>'))
with self.assertRaisesRegex(ValueError, 'write the music out'):
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)
@@ -68,6 +77,22 @@ class ScoreBundleTests(unittest.TestCase):
restored = self.round_trip(document)
validate_document(restored)
self.assertTrue(any(len(m['voices']) == 2 for m in restored['staves'][0]['measures']))
def test_contiguous_ties_continue_across_systems_without_repeat_state(self):
from generator.engraving.layout import make_system
from generator.engraving.notation import ScoreIndex
from generator.playback import compile_timeline
text = xml_score(xml_note(extra='<tie type="start"/>'))
text = text.replace('</measure></part>', '</measure><measure number="2">' + xml_note(extra='<tie type="stop"/>') + '<barline><bar-style>light-heavy</bar-style></barline></measure></part>')
source = parse(text)
index = ScoreIndex(source)
fonts = Fonts(Path('package/assets/fonts/petaluma'))
for start, end in ((0, 4), (4, 8)):
system = make_system(index, start, end, fonts, Style())
self.assertTrue(any(p.role == 'tie' and len(p.sources) == 2 for p in system.primitives))
self.assertEqual([fraction(e['duration']) for e in compile_timeline(source)['events']], [8])
restored = self.round_trip(source)
self.assertEqual(notes(restored)[0]['pitches'], notes(source)[0]['pitches'])
self.assertEqual(restored['staves'][0]['measures'][1]['barlines'], {'right': 'light-heavy'})
def test_microtones_remain_exact_after_transposition_and_export(self):
source = parse(xml_score(xml_note(alter='0.5', extra='<accidental>quarter-sharp</accidental>')))
transposed = transpose_document(source, 'D')