Improve Catalog
ci / checks (push) Successful in 46m42s

This commit is contained in:
khodowany
2026-09-27 11:03:43 -05:00
parent a416a80dd9
commit 142d843fac
418 changed files with 440 additions and 324488 deletions
+62 -158
View File
@@ -37,8 +37,7 @@ template <typename T>
concept retains_score_json = requires(T value) { value.source_json; };
static_assert(!retains_score_json<vamp_zero::library_part>);
static_assert(!retains_score_json<vamp_zero::practice_exercise>);
static_assert(!retains_score_json<vamp_zero::library_part>);
static_assert(!retains_score_json<vamp_zero::catalog>);
struct catalog_directory {
std::filesystem::path path;
@@ -127,60 +126,41 @@ struct catalog_database {
auto build() const -> std::bool_t {
return vamp_test::build_catalog(assets, config.path);
}
auto performance() const -> std::expected<vamp_zero::library_state, modulus::error> {
return run_catalog_operation(vamp_zero::async_load_performance_library, config, assets.string());
auto performance() const -> std::expected<vamp_zero::catalog, modulus::error> {
return run_catalog_operation(vamp_zero::async_load_catalog, config, assets.string());
}
auto practice() const -> std::expected<vamp_zero::practice_library, modulus::error> {
return run_catalog_operation(vamp_zero::async_load_practice_library, config, assets.string());
}
auto score(vamp_zero::score_category category, std::string id) const -> std::expected<vamp_zero::score_document, modulus::error> {
return run_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string());
auto score(std::string id) const -> std::expected<vamp_zero::score_document, modulus::error> {
return run_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{std::move(id)}, assets.string());
}
};
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",
.composer = "Composer",
};
for (auto part_name : vamp_zero::practice_part_names()) {
result.parts.push_back({.id = result.id + "-" + std::string{part_name}, .key_signature = authored, .part = std::string{part_name}, .measure_count = 3, .total_pages = 2});
}
return result;
}
}
BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_metadata) {
auto performance = vamp_zero::group_library_parts({
auto performance = vamp_zero::group_ordered_parts({
{.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", "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 catalog = std::make_shared<vamp_zero::catalog const>(std::move(performance));
auto original = vamp_zero::app_state{
.catalog = catalog,
.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU,
.selected_song_index = 2,
.selected_part_index = 1,
.selected_practice_exercise_index = 2,
};
auto control = vamp_zero::control_state{{}, original};
auto observed = std::vector<vamp_zero::app_state>{};
control.set_observer([&](vamp_zero::app_state state) { observed.push_back(std::move(state)); });
auto before = control.snapshot();
BOOST_TEST(before.catalog.get() == catalog.get());
BOOST_TEST(vamp_zero::selected_part(before) == &catalog->performance.parts[1]);
BOOST_TEST(vamp_zero::selected_practice_exercise(before) == &catalog->practice.exercises[1]);
BOOST_TEST(vamp_zero::selected_part(before) == &catalog->parts[1]);
auto heartbeat = control.heartbeat();
BOOST_TEST(heartbeat.catalog.get() == catalog.get());
BOOST_TEST(heartbeat.heartbeat_count == 1U);
BOOST_TEST(heartbeat.pico_connected);
auto playing = control.apply(vamp_protocol::page_command::SELECT);
BOOST_TEST(vamp_zero::to_string(playing.mode) == "performance_score");
BOOST_TEST(vamp_zero::active_part(playing) == &catalog->performance.parts[1]);
BOOST_TEST(vamp_zero::active_part(playing) == &catalog->parts[1]);
BOOST_TEST(control.apply(vamp_protocol::page_command::NEXT).page_index == 1U);
BOOST_TEST(control.apply(vamp_protocol::page_command::PREVIOUS).page_index == 0U);
control.apply(vamp_protocol::page_command::SELECT);
@@ -193,29 +173,14 @@ BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_
for (auto const &snapshot : observed) {
BOOST_TEST(snapshot.catalog.get() == catalog.get());
}
original.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
auto practice_control = vamp_zero::control_state{{}, original};
auto parts = practice_control.apply(vamp_protocol::page_command::SELECT);
BOOST_TEST(vamp_zero::to_string(parts.mode) == "practice_part_menu");
auto selected = practice_control.apply(vamp_protocol::page_command::NEXT);
BOOST_TEST(selected.catalog.get() == catalog.get());
BOOST_TEST(vamp_zero::selected_practice_part(selected, catalog->practice.exercises[1]) == &catalog->practice.exercises[1].parts[0]);
auto exercise = practice_control.apply(vamp_protocol::page_command::SELECT);
BOOST_TEST(vamp_zero::to_string(exercise.mode) == "practice_score");
BOOST_TEST(exercise.catalog.get() == catalog.get());
BOOST_TEST(vamp_zero::active_practice(exercise) == &catalog->practice.exercises[1]);
BOOST_TEST(practice_control.apply(vamp_protocol::page_command::NEXT).page_index == 1U);
BOOST_TEST(practice_control.heartbeat().catalog.get() == catalog.get());
BOOST_TEST(parts.active_practice_id.empty());
BOOST_TEST(parts.selected_practice_part_index == 0U);
}
BOOST_AUTO_TEST_CASE(catalog_performance_groups_use_title_and_composer_with_part_indices) {
auto library = vamp_zero::group_library_parts({
{.id = "other-composer", .title = "Shared Title", .composer = "Bea", .part = "Lead"},
{.id = "vocal", .title = "Shared Title", .composer = "Ada", .part = "Vocal"},
auto library = vamp_zero::group_ordered_parts({
{.id = "other-title", .title = "Other Title", .composer = "Ada", .part = "Lead"},
{.id = "guitar", .title = "Shared Title", .composer = "Ada", .part = "Guitar"},
{.id = "vocal", .title = "Shared Title", .composer = "Ada", .part = "Vocal"},
{.id = "other-composer", .title = "Shared Title", .composer = "Bea", .part = "Lead"},
});
BOOST_REQUIRE_EQUAL(library.parts.size(), 4U);
BOOST_REQUIRE_EQUAL(library.groups.size(), 3U);
@@ -241,79 +206,56 @@ BOOST_AUTO_TEST_CASE(catalog_performance_groups_use_title_and_composer_with_part
BOOST_TEST(library.parts[library.groups[1].parts[1]].id == "vocal");
}
BOOST_AUTO_TEST_CASE(catalog_practice_requires_complete_ordered_parts_and_key_agreement) {
for (auto authored : {"Eb", "C dorian", "No key", ""}) {
auto first = catalog_exercise(authored);
std::reverse(first.parts.begin(), first.parts.end());
auto last = first.parts.back();
first.parts.pop_back();
auto fragment = catalog_exercise(authored);
fragment.parts = {last};
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 == "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) {
BOOST_TEST(exercise.parts[index].part == vamp_zero::practice_part_names()[index]);
}
}
for (auto conflict : {"F", ""}) {
auto first = catalog_exercise();
auto second = catalog_exercise(conflict);
second.parts = {second.parts.back()};
first.parts.pop_back();
auto grouped = vamp_zero::group_practice_exercises({first, second});
BOOST_REQUIRE(!grouped);
BOOST_TEST(grouped.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
BOOST_TEST(grouped.error().message().find("disagree on key") != std::string_view::npos);
}
auto missing = catalog_exercise();
missing.parts.pop_back();
BOOST_TEST(!vamp_zero::group_practice_exercises({missing}));
auto duplicate = catalog_exercise();
duplicate.parts.back().part = "Acoustic";
BOOST_TEST(!vamp_zero::group_practice_exercises({duplicate}));
}
BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_missing_or_invalid) {
BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_is_parameterized_and_reports_missing_or_invalid) {
auto db = catalog_database{};
auto song = catalog_bundle("Song", "F");
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/0001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part));
BOOST_REQUIRE(db.build());
std::filesystem::remove_all(db.assets);
auto document = db.score("shared");
BOOST_REQUIRE(document);
BOOST_TEST(document->id == "shared");
BOOST_TEST(document->title == "Song");
BOOST_TEST(document->key_signature == "F");
BOOST_TEST(document->source_json == song);
BOOST_TEST(document->source_path == (db.assets / "songs/shared.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");
for (auto id : {"missing", "shared' OR 1=1 --"}) {
auto missing = db.score(id);
BOOST_REQUIRE(!missing);
BOOST_TEST(missing.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::NOT_FOUND));
}
db.query("UPDATE songs SET source_json = '{}' WHERE id = 'shared'");
auto invalid = db.score("shared");
BOOST_REQUIRE(!invalid);
BOOST_TEST(invalid.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
}
BOOST_AUTO_TEST_CASE(catalog_query_orders_and_groups_by_exact_song_identity) {
auto db = catalog_database{};
for (auto const &id : {"upper-bea", "upper-vocal", "upper-acoustic", "lower", "upper-acoustic-second"}) {
db.write(std::filesystem::path{"songs"} / (std::string{id} + ".json"), catalog_bundle("Same"));
}
BOOST_REQUIRE(db.build());
db.query("UPDATE practice_exercises SET id = 'shared' WHERE part = 'Electric'");
std::filesystem::remove_all(db.assets);
for (auto category : {vamp_zero::score_category::PERFORMANCE, vamp_zero::score_category::PRACTICE}) {
auto is_song = category == vamp_zero::score_category::PERFORMANCE;
auto document = db.score(category, "shared");
BOOST_REQUIRE(document);
BOOST_TEST(document->id == "shared");
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/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");
for (auto id : {"missing", "shared' OR 1=1 --"}) {
auto missing = db.score(category, id);
BOOST_REQUIRE(!missing);
BOOST_TEST(missing.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::NOT_FOUND));
}
auto table = std::string{is_song ? "songs" : "practice_exercises"};
db.query("UPDATE " + table + " SET source_json = '{}' WHERE id = 'shared'");
auto invalid = db.score(category, "shared");
BOOST_REQUIRE(!invalid);
BOOST_TEST(invalid.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
BOOST_REQUIRE(db.score(is_song ? vamp_zero::score_category::PRACTICE : vamp_zero::score_category::PERFORMANCE, "shared"));
db.query("UPDATE " + table + " SET source_json = ?1 WHERE id = 'shared'", {noto::sqlite_value{is_song ? song : exercise}});
db.query("UPDATE songs SET composer = 'Ada', part = 'Acoustic'");
db.query("UPDATE songs SET composer = 'Bea' WHERE id = 'upper-bea'");
db.query("UPDATE songs SET part = 'Vocal' WHERE id = 'upper-vocal'");
db.query("UPDATE songs SET title = 'same' WHERE id = 'lower'");
auto result = db.performance();
BOOST_REQUIRE(result);
BOOST_REQUIRE_EQUAL(result->groups.size(), 3U);
auto ids = std::vector<std::string>{};
for (auto const &part : result->parts) {
ids.push_back(part.id);
}
auto expected = std::vector<std::string>{"upper-acoustic", "upper-acoustic-second", "upper-vocal", "upper-bea", "lower"};
BOOST_TEST(ids == expected, boost::test_tools::per_element());
BOOST_TEST(result->groups[0].composer == "Ada");
BOOST_TEST(result->groups[0].parts.size() == 3U);
BOOST_TEST(result->groups[1].composer == "Bea");
BOOST_TEST(result->groups[2].title == "same");
}
BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded) {
@@ -322,7 +264,7 @@ BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded)
BOOST_REQUIRE(db.build());
auto repeated_loads = [](boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets)
-> boost::asio::awaitable<std::expected<std::uint32_t, modulus::error>> {
auto reference = vamp_zero::score_reference{.category = vamp_zero::score_category::PERFORMANCE, .id = "repeated"};
auto reference = vamp_zero::score_reference{.id = "repeated"};
for (auto count = std::uint32_t{}; count < 4096U; ++count) {
auto score = co_await vamp_zero::async_load_score(executor, config, reference, assets);
if (!score) {
@@ -343,48 +285,20 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
db.query("INSERT INTO songs(id, title, composer, source_json, part, source_path, "
"key_signature, measure_count, total_pages, title_image) "
"VALUES('tune', 'Metadata Song', 'Composer', '{}', 'Lead', 'songs/tune.json', 'F', 3, 2, '0000000000000000000000000000000000000000000000000000000000000000')");
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, title_image) "
"VALUES(?1, '0001', 'Metadata Exercise', 'Composer', 'Eb', '{}', ?3, 3, 2, ?2, '0000000000000000000000000000000000000000000000000000000000000000')",
{noto::sqlite_value{std::string{"practice_part-"} + std::string{part_name}}, noto::sqlite_value{std::string{part_name}},
noto::sqlite_value{std::string{"practice/0001/"} + std::string{part_name} + ".json"}});
}
auto songs = db.rows("songs");
auto exercises = db.rows("practice_exercises");
for (auto attempt = std::uint32_t{}; attempt < 2U; ++attempt) {
auto performance = db.performance();
auto practice = db.practice();
BOOST_REQUIRE(performance);
BOOST_REQUIRE(practice);
BOOST_REQUIRE_EQUAL(performance->parts.size(), 1U);
BOOST_TEST(performance->parts[0].title == "Metadata Song");
BOOST_TEST(performance->parts[0].key_signature == "F");
BOOST_TEST(performance->parts[0].measure_count == 3U);
BOOST_TEST(performance->parts[0].total_pages == 2U);
BOOST_REQUIRE_EQUAL(practice->exercises.size(), 1U);
BOOST_TEST(practice->exercises[0].title == "Metadata Exercise");
BOOST_TEST(practice->exercises[0].parts.front().key_signature == "Eb");
BOOST_REQUIRE_EQUAL(practice->exercises[0].parts.size(), 3U);
BOOST_TEST(practice->exercises[0].parts[0].part == "Acoustic");
BOOST_TEST(practice->exercises[0].parts[2].part == "Vocal");
for (auto const &practice_part : practice->exercises[0].parts) {
BOOST_TEST(practice_part.measure_count == 3U);
BOOST_TEST(practice_part.total_pages == 2U);
}
}
BOOST_TEST(db.rows("songs") == songs, boost::test_tools::per_element());
BOOST_TEST(db.rows("practice_exercises") == exercises, boost::test_tools::per_element());
auto song = db.score(vamp_zero::score_category::PERFORMANCE, "tune");
auto exercise = db.score(vamp_zero::score_category::PRACTICE, "practice_part-Vocal");
auto song = db.score("tune");
BOOST_REQUIRE(!song);
BOOST_REQUIRE(!exercise);
BOOST_TEST(song.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
BOOST_TEST(exercise.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
db.query("UPDATE practice_exercises SET key_signature = 'F' WHERE id = 'practice_part-Vocal'");
auto disagreement = db.practice();
BOOST_REQUIRE(!disagreement);
BOOST_TEST(disagreement.error().message().find("disagree on key") != std::string_view::npos);
}
BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_incompatible_and_corrupt_files_without_repair) {
@@ -395,12 +309,9 @@ BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_incompatible_and_corrupt
BOOST_REQUIRE(db.build());
BOOST_REQUIRE(validate());
auto songs = db.performance();
auto practice = db.practice();
BOOST_REQUIRE(songs);
BOOST_REQUIRE(practice);
BOOST_TEST(songs->parts.empty());
BOOST_TEST(practice->exercises.empty());
for (auto sql : {"ALTER TABLE songs RENAME COLUMN part TO obsolete_part", "ALTER TABLE practice_exercises RENAME COLUMN exercise_id TO obsolete_id", "DROP TABLE songs", "DROP TABLE practice_exercises"}) {
for (auto sql : {"ALTER TABLE songs RENAME COLUMN part TO obsolete_part", "DROP TABLE songs"}) {
db.query(sql);
auto snapshot_sql = "SELECT json_group_array(sql) FROM (SELECT sql FROM sqlite_schema ORDER BY name)";
auto before = std::get<std::string>(db.query(snapshot_sql).rows[0][0].value);
@@ -444,12 +355,8 @@ BOOST_AUTO_TEST_CASE(catalog_validation_checks_column_types_constraints_and_tabl
BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_relocated_assets) {
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/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");
std::filesystem::remove_all(db.assets / "practice");
auto moved = db.directory.path / "relocated";
std::filesystem::rename(db.assets, moved);
db.assets = moved;
@@ -457,14 +364,11 @@ BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_r
std::filesystem::permissions(db.config.path, std::filesystem::perms::owner_read);
BOOST_REQUIRE(run_catalog_operation(vamp_zero::async_validate_database, db.config, true));
auto songs = db.performance();
auto practice = db.practice();
auto score = db.score(vamp_zero::score_category::PERFORMANCE, "tune");
auto score = db.score("tune");
BOOST_REQUIRE(songs);
BOOST_REQUIRE(practice);
BOOST_REQUIRE(score);
BOOST_TEST(score->source_path == (moved / "songs/tune.json").string());
BOOST_TEST(songs->parts[0].source_path == score->source_path);
BOOST_TEST(practice->exercises[0].parts.front().key_signature == "Eb");
BOOST_TEST((std::filesystem::last_write_time(db.config.path) == timestamp));
BOOST_TEST(!std::filesystem::exists(db.config.path + "-wal"));
BOOST_TEST(!std::filesystem::exists(db.config.path + "-shm"));
@@ -481,10 +385,10 @@ BOOST_AUTO_TEST_CASE(catalog_rejects_paths_outside_the_asset_root) {
}
db.query("UPDATE songs SET source_path = ?1", {noto::sqlite_value{std::string{path}}});
BOOST_TEST(!db.performance());
BOOST_TEST(!db.score(vamp_zero::score_category::PERFORMANCE, "tune"));
BOOST_TEST(!db.score("tune"));
}
std::filesystem::create_directory_symlink(db.directory.path, db.assets / "escape");
db.query("UPDATE songs SET source_path = 'escape/tune.json'");
BOOST_TEST(!db.performance());
BOOST_TEST(!db.score(vamp_zero::score_category::PERFORMANCE, "tune"));
BOOST_TEST(!db.score("tune"));
}