@@ -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
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
@@ -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'):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')))
|
||||
|
||||
Reference in New Issue
Block a user