import json from pathlib import Path import tempfile import unittest from generator.practice.generate import generate_practice_scores from generator.arrange.generate import generate_performance_scores from generator.score import validate_generated_tree from generator.summary import validate_practice_parts from generator.parts import PART_TYPES from unittest.mock import patch from tests.test_musicxml import xml_note, xml_score class CatalogueTests(unittest.TestCase): def test_every_numbered_source_is_wired_to_three_part_paths(self): source = Path('data/practice') sources = sorted(source.glob('*.xml')) self.assertEqual([p.name for p in sources], [f'{i:03}.xml' for i in range(1, 367)]) expected = {Path(path.stem) / (part + '.json') for path in sources for part in PART_TYPES} self.assertEqual(len(expected), 1098) with tempfile.TemporaryDirectory() as temporary, patch('generator.practice.generate.practice_document', side_effect=lambda document, part: document): output = Path(temporary) self.assertEqual(generate_practice_scores(source, output), len(expected)) self.assertEqual({p.relative_to(output) for p in output.rglob('*.json')}, expected) validate_practice_parts(output) validate_generated_tree(output) def test_numbers_and_duplicate_titles_remain_distinct_and_songs_are_excluded(self): with tempfile.TemporaryDirectory() as temporary: source, output = Path(temporary) / 'source', Path(temporary) / 'output' source.mkdir(parents=True) for name in ('000.xml', '001.xml', '010.xml', '366.xml'): (source / name).write_text(xml_score(xml_note()), encoding='utf-8') self.assertEqual(generate_practice_scores(source, output), 12) self.assertEqual(sorted(p.name for p in output.iterdir()), ['000', '001', '010', '366']) for directory in output.iterdir(): self.assertEqual({p.name for p in directory.glob('*.json')}, {(part + '.json') for part in PART_TYPES}) self.assertEqual(json.loads((directory / 'vocal.json').read_text())['metadata']['title'], 'Fixture') def test_invalid_names_and_legacy_nesting_preserve_output(self): for name in ('1.xml', '001-copy.xml', '001_copy.xml', 'chord/001.xml', 'practice/melody/lesson/001.xml'): with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary: source, output = Path(temporary) / 'source', Path(temporary) / 'output' for relative in ('002.xml', name): path = source / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_text(xml_score(xml_note()), encoding='utf-8') output.mkdir() sentinel = output / 'existing' sentinel.write_text('unchanged') with self.assertRaisesRegex(ValueError, 'practice sources'): generate_practice_scores(source, output) self.assertEqual(list(output.iterdir()), [sentinel]) self.assertEqual(sentinel.read_text(), 'unchanged') def test_empty_part_directory_fails(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) (root / '001').mkdir() with self.assertRaisesRegex(ValueError, 'practice generator drift'): validate_practice_parts(root) def test_songs_generate_only_authored_key_parts(self): with tempfile.TemporaryDirectory() as temporary: source, output = Path(temporary) / 'songs', Path(temporary) / 'output' source.mkdir() (source / 'song.xml').write_text(xml_score(xml_note('E', alter=-1), '-3')) self.assertEqual(generate_performance_scores(source, output), 3) self.assertEqual({p.name for p in output.iterdir()}, {'song-acoustic.json', 'song-electric.json', 'song-vocal.json'}) for path in output.iterdir(): metadata = json.loads(path.read_text())['metadata'] self.assertEqual(metadata['key_signature'], 'Eb') self.assertNotIn('variant_tonic', metadata)