Rhythm Time Signatures

This commit is contained in:
khodowany
2026-09-26 01:44:04 -05:00
parent 4485eb7413
commit 4d6348d354
3099 changed files with 203023 additions and 57 deletions
+15 -12
View File
@@ -375,13 +375,14 @@ BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalog_returns_to_main) {
for (auto index = std::uint32_t{2}; index <= 366U; ++index) {
auto exercise = first;
exercise.id = std::to_string(index);
exercise.id.insert(0U, 4U - exercise.id.size(), '0');
catalog->practice.exercises.push_back(std::move(exercise));
}
state.catalog = catalog;
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
BOOST_TEST(state.selected_practice_exercise_index == 366U);
BOOST_TEST(vamp_zero::selected_practice_exercise(state)->id == "366");
BOOST_TEST(vamp_zero::selected_practice_exercise(state)->id == "0366");
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.selected_practice_exercise_index == 0U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
@@ -415,6 +416,8 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_parts) {
BOOST_REQUIRE_MESSAGE(library.has_value(), (library ? "" : library.error().describe()));
BOOST_TEST(library->exercises.size() == expected_exercises);
for (auto const &exercise : library->exercises) {
BOOST_TEST(exercise.id.size() == 4U);
BOOST_TEST(exercise.id.find_first_not_of("0123456789") == std::string::npos);
BOOST_REQUIRE_EQUAL(exercise.parts.size(), 3U);
auto parts = vamp_zero::practice_part_names();
for (auto index = std::size_t{}; index < parts.size(); ++index) {
@@ -447,7 +450,7 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
auto assets = root / "assets";
auto config = test_database_config(root / "vamp.db");
std::filesystem::create_directories(assets / "songs");
for (auto name : {"010", "002", "001", "127"}) {
for (auto name : {"0010", "0002", "0001", "0127"}) {
for (auto part_name : vamp_zero::practice_part_names()) {
write_text(assets / "practice" / name / vamp_test::part_filename(part_name), score_asset_json("Shared Title", "C", std::string{part_name}));
}
@@ -459,24 +462,24 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
auto library = future.get();
BOOST_REQUIRE(library);
BOOST_REQUIRE_EQUAL(library->exercises.size(), 4U);
BOOST_TEST(library->exercises[0].id == "001");
BOOST_TEST(library->exercises[1].id == "002");
BOOST_TEST(library->exercises[2].id == "010");
BOOST_TEST(library->exercises[3].id == "127");
BOOST_TEST(library->exercises[0].id == "0001");
BOOST_TEST(library->exercises[1].id == "0002");
BOOST_TEST(library->exercises[2].id == "0010");
BOOST_TEST(library->exercises[3].id == "0127");
for (auto const &exercise : library->exercises) {
BOOST_TEST(exercise.title == "Shared Title");
}
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path));
std::filesystem::rename(assets / "practice/002", assets / "practice/099");
std::filesystem::rename(assets / "practice/0002", assets / "practice/0099");
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path));
auto rows = query_database(root / "vamp.db", "SELECT id FROM practice_exercises ORDER BY id");
BOOST_REQUIRE(rows);
BOOST_REQUIRE_EQUAL(rows->rows.size(), 12U);
BOOST_TEST(!has_value(*rows, 0U, "002-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "099-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "010-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "001-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "127-acoustic"));
BOOST_TEST(!has_value(*rows, 0U, "0002-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "0099-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "0010-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "0001-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "0127-acoustic"));
std::filesystem::remove_all(root);
}
+7 -7
View File
@@ -15,7 +15,7 @@ class CatalogTests(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)])
self.assertEqual([p.name for p in sources], [f'{i:04}.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):
@@ -28,18 +28,18 @@ class CatalogTests(unittest.TestCase):
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'):
for name in ('1000.xml', '0000.xml', '0001.xml', '0999.xml', '0010.xml', '0366.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'])
self.assertEqual(generate_practice_scores(source, output), 18)
self.assertEqual(sorted(p.name for p in output.iterdir()), ['0000', '0001', '0010', '0366', '0999', '1000'])
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'):
for name in ('1.xml', '001.xml', '00001.xml', '\u0660\u0660\u0660\u0661.xml', '0001-copy.xml', '0001_copy.xml', 'chord/0001.xml', 'practice/melody/lesson/0001.xml'):
with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary:
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
for relative in ('002.xml', name):
for relative in ('0002.xml', name):
path = source / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(xml_score(xml_note()), encoding='utf-8')
@@ -53,7 +53,7 @@ class CatalogTests(unittest.TestCase):
def test_empty_part_directory_fails(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / '001').mkdir()
(root / '0001').mkdir()
with self.assertRaisesRegex(ValueError, 'practice generator drift'):
validate_practice_parts(root)
def test_songs_generate_only_authored_key_parts(self):
+6 -6
View File
@@ -34,7 +34,7 @@ class CatalogTests(unittest.TestCase):
document = bundle('Song', 'F')
write_score(self.root, 'songs/tune.json', document)
for part in ('Acoustic', 'Electric', 'Vocal'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle('Exercise', 'Eb', part))
write_score(self.root, f'practice/0001/{part.lower()}.json', bundle('Exercise', 'Eb', part))
database = build_catalog(self.root)
self.assertEqual(database, self.root / 'catalog.db')
song = self.rows('songs')[0]
@@ -42,8 +42,8 @@ class CatalogTests(unittest.TestCase):
self.assertEqual((song['id'], song['part'], song['source_path']), ('tune', 'Lead', 'songs/tune.json'))
self.assertEqual((song['measure_count'], song['total_pages']), (1, 1))
practice = self.rows('practice_exercises')
self.assertEqual([row['id'] for row in practice], ['001-acoustic', '001-electric', '001-vocal'])
self.assertEqual(practice[0]['exercise_id'], '001')
self.assertEqual([row['id'] for row in practice], ['0001-acoustic', '0001-electric', '0001-vocal'])
self.assertEqual(practice[0]['exercise_id'], '0001')
self.assertNotIn('practice_type_id', practice[0])
self.assertNotIn('lesson_group_id', practice[0])
self.assertEqual(practice[0]['key_signature'], 'Eb')
@@ -82,15 +82,15 @@ class CatalogTests(unittest.TestCase):
self.assertFalse(list(self.root.glob('.vamp-catalog-*')))
def test_missing_parts_and_metadata_disagreement_fail(self):
for part in ('Acoustic', 'Electric'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle(part=part))
write_score(self.root, f'practice/0001/{part.lower()}.json', bundle(part=part))
with self.assertRaisesRegex(ValueError, 'all three parts'):
build_catalog(self.root)
write_score(self.root, 'practice/001/vocal.json', bundle(key='F', part='Vocal'))
write_score(self.root, 'practice/0001/vocal.json', bundle(key='F', part='Vocal'))
with self.assertRaisesRegex(ValueError, 'disagree'):
build_catalog(self.root)
def test_keyless_parts_preserve_their_original_key(self):
for part in ('Acoustic', 'Electric', 'Vocal'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle(key='No key', part=part))
write_score(self.root, f'practice/0001/{part.lower()}.json', bundle(key='No key', part=part))
build_catalog(self.root)
self.assertTrue(all(row['key_signature'] == 'No key' for row in self.rows('practice_exercises')))
def test_invalid_layout_and_escaping_source_fail(self):
+9 -9
View File
@@ -138,7 +138,7 @@ struct catalog_database {
}
};
auto catalog_exercise(std::string authored = "Eb", std::string id = "001") -> vamp_zero::practice_exercise {
auto catalog_exercise(std::string authored = "Eb", std::string id = "0001") -> vamp_zero::practice_exercise {
auto result = vamp_zero::practice_exercise{
.id = std::move(id),
.title = "Exercise",
@@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_
{.id = "other", .title = "A Tune", .composer = "Composer", .part = "Lead", .total_pages = 1},
{.id = "chosen", .title = "B Tune", .composer = "Composer", .part = "Lead", .total_pages = 2},
});
auto practice = vamp_zero::group_practice_exercises({catalog_exercise("Eb", "002"), catalog_exercise("Eb", "001")});
auto practice = vamp_zero::group_practice_exercises({catalog_exercise("Eb", "0002"), catalog_exercise("Eb", "0001")});
BOOST_REQUIRE(practice);
auto catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::catalog{std::move(performance), std::move(*practice)});
auto original = vamp_zero::app_state{
@@ -249,11 +249,11 @@ BOOST_AUTO_TEST_CASE(catalog_practice_requires_complete_ordered_parts_and_key_ag
first.parts.pop_back();
auto fragment = catalog_exercise(authored);
fragment.parts = {last};
auto grouped = vamp_zero::group_practice_exercises({catalog_exercise("F", "002"), first, fragment});
auto grouped = vamp_zero::group_practice_exercises({catalog_exercise("F", "0002"), first, fragment});
BOOST_REQUIRE(grouped);
BOOST_REQUIRE_EQUAL(grouped->exercises.size(), 2U);
auto const &exercise = grouped->exercises[0];
BOOST_TEST(exercise.id == "001");
BOOST_TEST(exercise.id == "0001");
BOOST_TEST(exercise.parts.front().key_signature == authored);
BOOST_REQUIRE_EQUAL(exercise.parts.size(), 3U);
for (auto index = std::size_t{}; index < exercise.parts.size(); ++index) {
@@ -284,7 +284,7 @@ BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_m
auto exercise = catalog_bundle("Exercise", "D", "Electric");
db.write("songs/shared.json", song);
for (auto part : vamp_zero::practice_part_names()) {
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part));
db.write(std::filesystem::path{"practice/0001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part));
}
BOOST_REQUIRE(db.build());
db.query("UPDATE practice_exercises SET id = 'shared' WHERE part = 'Electric'");
@@ -297,7 +297,7 @@ BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_m
BOOST_TEST(document->title == (is_song ? "Song" : "Exercise"));
BOOST_TEST(document->key_signature == (is_song ? "F" : "D"));
BOOST_TEST(document->source_json == (is_song ? song : exercise));
BOOST_TEST(document->source_path == (db.assets / (is_song ? "songs/shared.json" : "practice/001/electric.json")).string());
BOOST_TEST(document->source_path == (db.assets / (is_song ? "songs/shared.json" : "practice/0001/electric.json")).string());
BOOST_TEST(document->measure_count == 3U);
BOOST_REQUIRE_EQUAL(document->pages.size(), 2U);
BOOST_TEST(document->pages[1].path == "fixture.pages/2.png");
@@ -346,9 +346,9 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
for (auto part_name : vamp_zero::practice_part_names()) {
db.query("INSERT INTO practice_exercises(id, exercise_id, "
"title, composer, key_signature, source_json, source_path, measure_count, total_pages, part) "
"VALUES(?1, '001', 'Metadata Exercise', 'Composer', 'Eb', '{}', ?3, 3, 2, ?2)",
"VALUES(?1, '0001', 'Metadata Exercise', 'Composer', 'Eb', '{}', ?3, 3, 2, ?2)",
{noto::sqlite_value{std::string{"practice_part-"} + std::string{part_name}}, noto::sqlite_value{std::string{part_name}},
noto::sqlite_value{std::string{"practice/001/"} + std::string{part_name} + ".json"}});
noto::sqlite_value{std::string{"practice/0001/"} + std::string{part_name} + ".json"}});
}
auto songs = db.rows("songs");
auto exercises = db.rows("practice_exercises");
@@ -445,7 +445,7 @@ BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_r
auto db = catalog_database{};
db.write("songs/tune.json", catalog_bundle("Relocated"));
for (auto part_name : vamp_zero::practice_part_names()) {
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part_name), catalog_bundle("Exercise", "Eb", part_name));
db.write(std::filesystem::path{"practice/0001"} / vamp_test::part_filename(part_name), catalog_bundle("Exercise", "Eb", part_name));
}
BOOST_REQUIRE(db.build());
std::filesystem::remove(db.assets / "songs/tune.json");
+1 -1
View File
@@ -189,7 +189,7 @@ class EngravingCacheTests(unittest.TestCase):
data, output = self.root / 'data', self.root / 'output'
(data / 'songs').mkdir(parents=True)
(data / 'practice').mkdir()
source = data / 'practice/001.xml'
source = data / 'practice/0001.xml'
source.write_text(xml_score(xml_note()))
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), check=False, engraving_cache=self.cache, jobs=2)
generate_assets(args)
+4 -4
View File
@@ -160,13 +160,13 @@ class GuitarTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as temporary:
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
source.mkdir()
(source / '000.xml').write_text(xml_score(xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
(source / '0000.xml').write_text(xml_score(xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
self.assertEqual(generate_practice_scores(source, output), 3)
validate_practice_parts(output)
first = {p.name: p.read_bytes() for p in output.rglob('*.json')}
generate_practice_scores(source, output)
self.assertEqual(first, {p.name: p.read_bytes() for p in output.rglob('*.json')})
(output / '000/vocal.json').unlink()
(output / '0000/vocal.json').unlink()
with self.assertRaisesRegex(ValueError, 'practice generator drift'):
validate_practice_parts(output)
def test_unplayable_generation_preserves_previous_assets(self):
@@ -174,12 +174,12 @@ class GuitarTests(unittest.TestCase):
root = Path(temporary)
(root / 'data/songs').mkdir(parents=True)
(root / 'data/practice').mkdir()
(root / 'data/practice/001.xml').write_text(xml_score(xml_note(octave=7, alter=1)))
(root / 'data/practice/0001.xml').write_text(xml_score(xml_note(octave=7, alter=1)))
output = root / 'output'
output.mkdir()
(output / 'last-good').write_text('preserved')
args = argparse.Namespace(data_dir=root / 'data', output_dir=output, static_assets_dir=Path('package/assets'), check=False)
with self.assertRaisesRegex(ValueError, '001.xml, part electric:.*0-20'):
with self.assertRaisesRegex(ValueError, '0001.xml, part electric:.*0-20'):
generate_assets(args)
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
self.assertEqual((output / 'last-good').read_text(), 'preserved')
+1 -1
View File
@@ -88,7 +88,7 @@ class HarmonyTests(unittest.TestCase):
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/practice/001.xml'))
exercise = load_musicxml(Path('data/practice/0001.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)
+3 -3
View File
@@ -187,7 +187,7 @@ auto make_assets(vamp_zero::config const &config) -> void {
for (auto part_name : vamp_zero::practice_part_names()) {
auto name = std::string{part_name};
name.front() = static_cast<std::char_t>(name.front() - 'A' + 'a');
write_score(root / "practice" / "001" / (name + ".json"), std::string{part_name});
write_score(root / "practice" / "0001" / (name + ".json"), std::string{part_name});
}
BOOST_REQUIRE(vamp_test::build_catalog(root, config.database.path));
}
@@ -375,7 +375,7 @@ BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartb
}
BOOST_AUTO_TEST_CASE(readiness_validates_later_pages_and_parts) {
for (std::char_t const *relative : {"songs/first.pages/2.png", "songs/second.pages/2.png", "practice/001/electric.pages/2.png"}) {
for (std::char_t const *relative : {"songs/first.pages/2.png", "songs/second.pages/2.png", "practice/0001/electric.pages/2.png"}) {
auto root = temporary_directory{};
auto config = fixture_config(root.path);
make_assets(config);
@@ -394,7 +394,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_
auto assets = std::filesystem::path{config.runtime.assets_path};
auto fixture = std::filesystem::path{VAMP_TEST_PLAYBACK_ASSETS_DIR};
std::filesystem::copy(fixture / "audio", assets / "audio", std::filesystem::copy_options::recursive);
auto path = assets / "practice/001/acoustic.json";
auto path = assets / "practice/0001/acoustic.json";
write_score(path, "Acoustic");
auto score = boost::json::parse(read_file(path));
score.as_object()["playback"] = boost::json::parse(read_file(fixture / "score.json")).at("playback");
+3 -3
View File
@@ -54,7 +54,7 @@ class PartGenerationTests(unittest.TestCase):
source = parse(xml_score(xml_harmony() + xml_note(duration=1, kind='quarter') + '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>' + xml_note(duration=3, kind='half', extra='<dot/>')))
self.assertEqual(len(generated_voice(source['staves'][0]['measures'][0], 'accompaniment', None)), 1)
def test_exercise_006_has_accompaniment_in_every_measure(self):
result = practice_document(load_musicxml(Path('data/practice/006.xml')), 'acoustic')
result = practice_document(load_musicxml(Path('data/practice/0006.xml')), 'acoustic')
self.assertEqual(len(result['staves'][0]['measures']), 12)
for measure in result['staves'][0]['measures']:
self.assertTrue(any(n['pitches'] for v in measure['voices'] for n in v['notes']), measure['id'])
@@ -96,7 +96,7 @@ class PartGenerationTests(unittest.TestCase):
def test_pipeline_parses_and_normalizes_each_source_once(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for category, name in (('songs', 'song'), ('practice', '001')):
for category, name in (('songs', 'song'), ('practice', '0001')):
(root / 'data' / category).mkdir(parents=True)
(root / 'data' / category / (name + '.xml')).write_text(xml_score(xml_harmony() + xml_note()))
args = argparse.Namespace(data_dir=root / 'data', output_dir=root / 'output', static_assets_dir=Path('package/assets'), check=False)
@@ -106,7 +106,7 @@ class PartGenerationTests(unittest.TestCase):
practice_load.assert_called_once()
self.assertEqual(normalize.call_count, 2)
original = {p.relative_to(args.output_dir): p.read_bytes() for p in args.output_dir.rglob('*') if p.is_file()}
(root / 'data/practice/002.xml').write_text('<broken')
(root / 'data/practice/0002.xml').write_text('<broken')
with self.assertRaises(ValueError):
generate_assets(args)
self.assertEqual(original, {p.relative_to(args.output_dir): p.read_bytes() for p in args.output_dir.rglob('*') if p.is_file()})
+16 -6
View File
@@ -28,18 +28,24 @@ class ScoreBundleTests(unittest.TestCase):
data, output = root / 'data', root / 'output'
(data / 'songs').mkdir(parents=True)
(data / 'practice').mkdir()
source = data / 'practice/001.xml'
source = data / 'practice/0001.xml'
source.write_text(xml_score(xml_note()))
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=Path('package/assets'), 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')))
legacy_source = source.with_name('001.xml')
source.rename(legacy_source)
with self.assertRaisesRegex(ValueError, 'four-digit'):
generate_assets(args)
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
legacy_source.rename(source)
(data / 'rhythms' / 'drafts').mkdir(parents=True)
for path in ('rhythms/001.xml', 'rhythms/drafts/002.xml'):
for path in ('rhythms/0001.xml', 'rhythms/drafts/0002.xml'):
(data / path).write_text('<unfinished')
generate_assets(args)
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
misplaced = data / '001.xml'
misplaced = data / '0001.xml'
misplaced.write_text(xml_score(xml_note()))
with self.assertRaisesRegex(ValueError, 'belong directly under'):
generate_assets(args)
@@ -57,12 +63,16 @@ class ScoreBundleTests(unittest.TestCase):
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()))
renamed = data / 'practice/127.xml'
renamed = data / 'practice/0127.xml'
source.rename(renamed)
source = renamed
legacy_output = output / 'practice/001'
legacy_output.mkdir()
(legacy_output / 'electric.json').write_text('{}')
generate_assets(args)
self.assertFalse((output / 'practice/001').exists())
self.assertEqual(len(list((output / 'practice/127').glob('*.json'))), 3)
self.assertFalse(legacy_output.exists())
self.assertFalse((output / 'practice/0001').exists())
self.assertEqual(len(list((output / 'practice/0127').glob('*.json'))), 3)
source.unlink()
generate_assets(args)
self.assertFalse(list(output.rglob('*.png')))