Data File Mappings
ci / checks (push) Successful in 2h3m59s

This commit is contained in:
khodowany
2026-09-16 23:55:09 -05:00
parent 079c024ca9
commit aa1273b07d
13 changed files with 291 additions and 271 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ target_compile_definitions(product-vamp-test PRIVATE
VAMP_TEST_ASSETS_DIR="${CMAKE_BINARY_DIR}/generated-assets"
)
add_test(NAME product-vamp COMMAND product-vamp-test)
set_tests_properties(product-vamp PROPERTIES TIMEOUT 60)
set_tests_properties(product-vamp PROPERTIES TIMEOUT 180)
add_test(
NAME product-vamp-generator
COMMAND ${Python3_EXECUTABLE} -m unittest discover -s tests -p test_*.py
+83 -3
View File
@@ -377,6 +377,13 @@ BOOST_AUTO_TEST_CASE(chord_and_melody_practice_share_key_navigation_and_remote_d
BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_keys) {
auto root = std::filesystem::temp_directory_path() / ("product-vamp-keys-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto config = test_database_config(root / "vamp.db");
auto expected_exercises = std::size_t{};
for (auto const &entry : std::filesystem::recursive_directory_iterator{std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "data/practice"}) {
if (entry.is_regular_file() && entry.path().extension() == ".xml") {
++expected_exercises;
}
}
BOOST_REQUIRE(expected_exercises > 0U);
BOOST_REQUIRE(run_ensure_database(config));
for (auto iteration = std::uint32_t{}; iteration < 2U; ++iteration) {
BOOST_REQUIRE(run_import(config, VAMP_TEST_ASSETS_DIR));
@@ -384,14 +391,15 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_keys) {
auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config), boost::asio::use_future);
io.run();
auto library = future.get();
BOOST_REQUIRE(library);
BOOST_TEST(library->exercises.size() == 4U);
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(vamp_zero::supports_key_selection(exercise));
BOOST_REQUIRE_EQUAL(exercise.variants.size(), 12U);
auto keys = vamp_zero::practice_key_names();
for (auto index = std::size_t{}; index < keys.size(); ++index) {
BOOST_TEST(exercise.variants[index].key_signature == keys[index]);
auto const &key = exercise.variants[index].key_signature;
BOOST_TEST(key.substr(0, key.find(' ')) == keys[index]);
BOOST_REQUIRE(vamp_zero::parse_score_document(exercise.variants[index].source_json, exercise.variants[index].source_path));
}
}
@@ -399,6 +407,78 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_keys) {
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_refresh_after_rename) {
auto root = std::filesystem::temp_directory_path() / ("product-vamp-paths-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto assets = root / "assets";
auto config = test_database_config(root / "vamp.db");
std::filesystem::create_directories(assets / "songs");
for (auto name : {"chord/001_lesson/010-cycles", "chord/001_lesson/002-cycles", "chord/002_lesson/001-cycles", "melody/001_lesson/127-cycles"}) {
write_text(assets / "practice" / name / "c.json", score_asset_json("Shared Title"));
}
auto library = vamp_zero::load_practice_library(assets.string());
BOOST_REQUIRE(library);
BOOST_REQUIRE_EQUAL(library->exercises.size(), 4U);
BOOST_REQUIRE_EQUAL(library->types.size(), 2U);
BOOST_TEST(library->types.front().id == "chord");
BOOST_REQUIRE_EQUAL(library->types.front().lesson_groups.size(), 2U);
auto const &lesson = library->types.front().lesson_groups.front();
BOOST_TEST(lesson.id == "001_lesson");
BOOST_REQUIRE_EQUAL(lesson.exercises.size(), 2U);
BOOST_TEST(lesson.exercises[0].id == "chord-001_lesson-002-cycles");
BOOST_TEST(lesson.exercises[1].id == "chord-001_lesson-010-cycles");
for (auto const &exercise : library->exercises) {
BOOST_TEST(exercise.title == "Shared Title");
}
BOOST_REQUIRE(run_ensure_database(config));
BOOST_REQUIRE(run_import(config, assets));
std::filesystem::rename(assets / "practice/chord/001_lesson", assets / "practice/chord/003_renamed_lesson");
std::filesystem::rename(assets / "practice/chord/003_renamed_lesson/002-cycles", assets / "practice/chord/003_renamed_lesson/099-renamed");
BOOST_REQUIRE(run_import(config, assets));
auto rows = query_database(root / "vamp.db", "SELECT id FROM practice_exercises ORDER BY id");
BOOST_REQUIRE(rows);
BOOST_REQUIRE_EQUAL(rows->rows.size(), 4U);
BOOST_TEST(!has_value(*rows, 0U, "chord-001_lesson-002-cycles-c"));
BOOST_TEST(!has_value(*rows, 0U, "chord-001_lesson-010-cycles-c"));
BOOST_TEST(has_value(*rows, 0U, "chord-003_renamed_lesson-099-renamed-c"));
BOOST_TEST(has_value(*rows, 0U, "chord-003_renamed_lesson-010-cycles-c"));
BOOST_TEST(has_value(*rows, 0U, "chord-002_lesson-001-cycles-c"));
BOOST_TEST(has_value(*rows, 0U, "melody-001_lesson-127-cycles-c"));
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(catalogue_queries_handle_empty_results_and_invalid_stored_scores) {
auto root = std::filesystem::temp_directory_path() / ("product-vamp-query-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto config = test_database_config(root / "vamp.db");
BOOST_REQUIRE(run_ensure_database(config));
for (auto invalid_rows : {false, true}) {
if (invalid_rows) {
for (auto table : {"songs", "practice_exercises"}) {
BOOST_REQUIRE(query_database(root / "vamp.db", std::string{"INSERT INTO "} + table + "(id, title, source_json, created_at_ms, updated_at_ms) VALUES('invalid', 'Invalid', '{}', 0, 0)"));
}
}
auto io = boost::asio::io_context{};
auto performance_future = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config), boost::asio::use_future);
auto practice_future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config), boost::asio::use_future);
io.run();
auto performance = performance_future.get();
auto practice = practice_future.get();
if (invalid_rows) {
BOOST_REQUIRE(!performance);
BOOST_REQUIRE(!practice);
auto invalid_score = vamp_zero::parse_score_document("{}", "");
BOOST_REQUIRE(!invalid_score);
BOOST_TEST(performance.error().message() == invalid_score.error().message());
BOOST_TEST(practice.error().message() == invalid_score.error().message());
} else {
BOOST_REQUIRE(performance);
BOOST_REQUIRE(practice);
BOOST_TEST(performance->parts.empty());
BOOST_TEST(practice->exercises.empty());
}
}
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(keyless_practice_variants_display_their_transposition_reference) {
auto source = boost::json::parse(score_asset_json("Keyless", "No key"));
auto &music = source.as_object()["music"].as_object();
+64
View File
@@ -0,0 +1,64 @@
import json
from pathlib import Path
import tempfile
import unittest
from generator.practice.generate import generate_practice_scores, key_file_name
from generator.score import slugify, validate_generated_tree
from generator.summary import validate_practice_variants
from generator.transposition import TARGET_KEYS
from tests.test_musicxml import xml_note, xml_score
class CatalogueTests(unittest.TestCase):
def test_every_practice_source_has_its_own_complete_output(self):
source = Path('data/practice')
sources = sorted(source.rglob('*.xml'))
self.assertTrue(sources)
expected = set()
for path in sources:
relative = path.relative_to(source)
self.assertEqual(len(relative.parts), 3, str(path))
self.assertIn(relative.parts[0], ('chord', 'melody'), str(path))
for key in TARGET_KEYS:
expected.add(relative.parent / slugify(path.stem) / key_file_name(key))
self.assertEqual(len(expected), len(sources) * len(TARGET_KEYS))
with tempfile.TemporaryDirectory() as temporary:
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_variants(output)
validate_generated_tree(output)
def test_numbered_paths_and_duplicate_titles_remain_distinct(self):
names = ('chord/001_lesson/001-cycles.xml', 'chord/001_lesson/010-cycles.xml', 'chord/002_lesson/001-cycles.xml', 'melody/001_lesson/127-cycles.xml')
with tempfile.TemporaryDirectory() as temporary:
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
for name in names:
path = source / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(xml_score(xml_note()), encoding='utf-8')
self.assertEqual(generate_practice_scores(source, output), len(names) * len(TARGET_KEYS))
for name in names:
directory = output / Path(name).with_suffix('')
self.assertEqual({p.name for p in directory.glob('*.json')}, {key_file_name(key) for key in TARGET_KEYS})
self.assertEqual(json.loads((directory / 'c.json').read_text())['metadata']['title'], 'Fixture')
def test_invalid_source_locations_fail_before_writing(self):
for name in ('chords/001_lesson/001-exercise.xml', 'chord/001-exercise.xml', 'melody/001_lesson/extra/001-exercise.xml'):
with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary:
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
for relative in ('chord/001_lesson/001-valid.xml', name):
path = source / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(xml_score(xml_note()), encoding='utf-8')
with self.assertRaisesRegex(ValueError, 'practice source must be stored'):
generate_practice_scores(source, output)
self.assertFalse(output.exists())
def test_normalized_filename_collisions_fail_before_writing(self):
with tempfile.TemporaryDirectory() as temporary:
source, output = Path(temporary) / 'source', Path(temporary) / 'output'
lesson = source / 'chord' / '001_lesson'
lesson.mkdir(parents=True)
for name in ('001-Cycles.xml', '001_cycles.xml'):
(lesson / name).write_text(xml_score(xml_note()), encoding='utf-8')
with self.assertRaisesRegex(ValueError, 'output path collides'):
generate_practice_scores(source, output)
self.assertFalse(output.exists())
+12 -5
View File
@@ -51,8 +51,11 @@ class GuitarPracticeTests(unittest.TestCase):
voicing = find_playable_voicing('C7')
self.assertEqual(len(voicing), 4)
self.assertTrue(all(p.fret == 1 for p in voicing))
def test_current_catalogue_all_keys_preserve_exact_voicings(self):
source = practice_document(load_musicxml(Path('data/practice/chord/01_three_note_shell_voicings/01-cycles.xml')))
def test_all_keys_preserve_exact_voicings_and_adjust_g_register(self):
harmony = '<harmony><root><root-step>G</root-step></root><kind>dominant</kind></harmony>'
content = xml_note('G', 2, 'half') + xml_note('B', 2, 'half', prefix='<chord/>') + xml_note('F', 2, 'half', prefix='<chord/>', octave=5)
content += xml_note('A', 2, 'half', alter=-1, octave=3) + xml_note('G', 2, 'half', alter=-1, prefix='<chord/>') + xml_note('C', 2, 'half', prefix='<chord/>', octave=5)
source = practice_document(parse(xml_score(harmony + content)))
original = copy.deepcopy(source)
for key in TARGET_KEYS:
with self.subTest(key=key):
@@ -209,11 +212,15 @@ class GuitarPracticeTests(unittest.TestCase):
self.assertEqual(list(output.iterdir()), [output / 'last-good'])
def test_all_practice_types_require_twelve_deterministic_variants(self):
with tempfile.TemporaryDirectory() as temp:
output = Path(temp)
self.assertEqual(generate_practice_scores(Path('data/practice'), output), 48)
source, output = Path(temp) / 'source', Path(temp) / 'output'
for practice_type in ('chord', 'melody'):
path = source / practice_type / '001_lesson' / '001-exercise.xml'
path.parent.mkdir(parents=True)
path.write_text(xml_score(xml_note()), encoding='utf-8')
self.assertEqual(generate_practice_scores(source, output), 24)
validate_practice_variants(output)
first = {p.relative_to(output): p.read_bytes() for p in output.rglob('*.json')}
generate_practice_scores(Path('data/practice'), output)
generate_practice_scores(source, output)
self.assertEqual(first, {p.relative_to(output): p.read_bytes() for p in output.rglob('*.json')})
next((output / 'chord').rglob('g.json')).unlink()
with self.assertRaisesRegex(ValueError, 'chord practice generator drift'):
+1 -1
View File
@@ -45,7 +45,7 @@ class MusicXMLTests(unittest.TestCase):
self.assertEqual(len(score['staves'][0]['octave_shifts']), 1)
def test_catalogue_imports(self):
sources = sorted(Path('data').rglob('*.xml'))
self.assertEqual(len(sources), 6)
self.assertTrue(sources)
for path in sources:
with self.subTest(path=path):
self.assertEqual(load_musicxml(path)['schema_version'], 3)
+8
View File
@@ -40,6 +40,14 @@ class ScoreBundleTests(unittest.TestCase):
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.write_text(xml_score(xml_note()))
renamed = lesson.with_name('002_renamed_lesson') / '127-renamed-exercise.xml'
renamed.parent.mkdir()
source.rename(renamed)
source = renamed
generate_assets(args)
self.assertFalse((output / 'practice/chord/lesson').exists())
self.assertEqual(len(list((output / 'practice/chord/002_renamed_lesson/127-renamed-exercise').glob('*.json'))), 12)
source.unlink()
generate_assets(args)
self.assertFalse(list(output.rglob('*.png')))