+26
-224
@@ -41,6 +41,8 @@
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
#include "catalogue_fixture.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
auto test_database_config(std::filesystem::path const &path) -> vamp_zero::database_config {
|
||||
@@ -49,20 +51,6 @@ auto test_database_config(std::filesystem::path const &path) -> vamp_zero::datab
|
||||
};
|
||||
}
|
||||
|
||||
auto run_ensure_database(vamp_zero::database_config config) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), std::move(config)), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto run_import(vamp_zero::database_config config, std::filesystem::path const &assets_path) -> std::expected<void, modulus::error> {
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_import_catalogs(io.get_executor(), std::move(config), assets_path.string()), boost::asio::use_future);
|
||||
io.run();
|
||||
return future.get();
|
||||
}
|
||||
|
||||
auto query_database(std::filesystem::path const &path,
|
||||
std::string sql,
|
||||
std::vector<noto::sqlite_value> params = {}) -> std::expected<noto::query_result, modulus::error> {
|
||||
@@ -98,11 +86,6 @@ auto text_value(noto::sqlite_value const &value) -> std::string {
|
||||
return text == nullptr ? std::string{} : *text;
|
||||
}
|
||||
|
||||
auto integer_value(noto::sqlite_value const &value) -> std::int64_t {
|
||||
auto integer = std::get_if<std::int64_t>(&value.value);
|
||||
return integer == nullptr ? 0 : *integer;
|
||||
}
|
||||
|
||||
auto has_value(noto::query_result const &result, std::size_t column, std::string_view expected) -> std::bool_t {
|
||||
return std::any_of(result.rows.begin(), result.rows.end(), [&](auto const &row) {
|
||||
return row.size() > column && text_value(row[column]) == expected;
|
||||
@@ -368,11 +351,10 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_keys) {
|
||||
}
|
||||
}
|
||||
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));
|
||||
config.path = (std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "catalogue.db").string();
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config), boost::asio::use_future);
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config, VAMP_TEST_ASSETS_DIR), boost::asio::use_future);
|
||||
io.run();
|
||||
auto library = future.get();
|
||||
BOOST_REQUIRE_MESSAGE(library.has_value(), (library ? "" : library.error().describe()));
|
||||
@@ -386,6 +368,21 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_keys) {
|
||||
BOOST_TEST(key.substr(0, key.find(' ')) == keys[index]);
|
||||
BOOST_TEST(exercise.variants[index].total_pages > 0U);
|
||||
BOOST_TEST(exercise.variants[index].measure_count > 0U);
|
||||
if (iteration == 0U) {
|
||||
auto const &variant = exercise.variants[index];
|
||||
io.restart();
|
||||
auto loaded = boost::asio::co_spawn(io, vamp_zero::async_load_score(io.get_executor(), config, {vamp_zero::score_category::PRACTICE, variant.id}, VAMP_TEST_ASSETS_DIR), boost::asio::use_future);
|
||||
io.run();
|
||||
auto score = loaded.get();
|
||||
BOOST_REQUIRE_MESSAGE(score, (score ? "" : score.error().describe()));
|
||||
BOOST_TEST(score->title == exercise.title);
|
||||
BOOST_TEST(score->composer == exercise.composer);
|
||||
BOOST_TEST(score->key_signature == variant.key_signature);
|
||||
BOOST_TEST(score->authored_key_signature == exercise.authored_key_signature);
|
||||
BOOST_TEST(score->measure_count == variant.measure_count);
|
||||
BOOST_TEST(score->pages.size() == variant.total_pages);
|
||||
BOOST_TEST(score->source_path == variant.source_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,7 +397,11 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
|
||||
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(vamp_test::build_catalogue(assets, config.path));
|
||||
auto io = boost::asio::io_context{};
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config, assets.string()), boost::asio::use_future);
|
||||
io.run();
|
||||
auto library = future.get();
|
||||
BOOST_REQUIRE(library);
|
||||
BOOST_REQUIRE_EQUAL(library->exercises.size(), 4U);
|
||||
BOOST_REQUIRE_EQUAL(library->types.size(), 2U);
|
||||
@@ -414,11 +415,10 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
|
||||
for (auto const &exercise : library->exercises) {
|
||||
BOOST_TEST(exercise.title == "Shared Title");
|
||||
}
|
||||
BOOST_REQUIRE(run_ensure_database(config));
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path));
|
||||
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));
|
||||
BOOST_REQUIRE(vamp_test::build_catalogue(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(), 4U);
|
||||
@@ -431,39 +431,6 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
|
||||
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();
|
||||
@@ -488,35 +455,6 @@ BOOST_AUTO_TEST_CASE(integer_parsing_checks_range_sign_and_entire_input) {
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(library_scans_report_filesystem_errors_instead_of_empty_success) {
|
||||
auto root = std::filesystem::temp_directory_path() /
|
||||
("product-vamp-scan-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
write_text(root / "songs", "not a directory");
|
||||
write_text(root / "practice", "not a directory");
|
||||
auto performance = vamp_zero::load_performance_library(root.string());
|
||||
auto practice = vamp_zero::load_practice_library(root.string());
|
||||
BOOST_REQUIRE(!performance.has_value());
|
||||
BOOST_REQUIRE(!practice.has_value());
|
||||
BOOST_TEST(performance.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::UNAVAILABLE));
|
||||
BOOST_TEST(practice.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::UNAVAILABLE));
|
||||
std::filesystem::remove(root / "songs");
|
||||
std::filesystem::remove(root / "practice");
|
||||
std::filesystem::create_directories(root / "songs");
|
||||
std::filesystem::create_directories(root / "practice");
|
||||
std::filesystem::create_symlink("loop.json", root / "songs" / "loop.json");
|
||||
std::filesystem::create_symlink("loop.json", root / "practice" / "loop.json");
|
||||
BOOST_CHECK_NO_THROW(performance = vamp_zero::load_performance_library(root.string()));
|
||||
BOOST_CHECK_NO_THROW(practice = vamp_zero::load_practice_library(root.string()));
|
||||
BOOST_REQUIRE(!performance.has_value());
|
||||
BOOST_REQUIRE(!practice.has_value());
|
||||
BOOST_TEST(performance.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::UNAVAILABLE));
|
||||
BOOST_TEST(practice.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::UNAVAILABLE));
|
||||
std::filesystem::remove_all(root);
|
||||
practice = vamp_zero::load_practice_library(root.string());
|
||||
BOOST_REQUIRE(practice.has_value());
|
||||
BOOST_TEST(practice->exercises.empty());
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_accepts_crlf_payloads) {
|
||||
auto state = vamp_pico::remote_display_state{};
|
||||
BOOST_REQUIRE(vamp_pico::parse_remote_display_body("active=1\r\ntitle=Autumn Leaves\r\npart=Vocal\r\nmode=score\r\npage=2\r\npages=11\r\n", state));
|
||||
@@ -545,142 +483,6 @@ BOOST_AUTO_TEST_CASE(pico_remote_display_parser_truncates_long_fields_safely) {
|
||||
BOOST_TEST(std::strcmp(state.mode, "performance-score") == 0);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(database_ensure_creates_current_schema_without_version_bookkeeping) {
|
||||
auto root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("product-vamp-db-schema-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
auto database_path = root / "vamp.db";
|
||||
std::filesystem::remove_all(root);
|
||||
auto config = test_database_config(database_path);
|
||||
BOOST_REQUIRE(run_ensure_database(config));
|
||||
BOOST_REQUIRE(run_ensure_database(config));
|
||||
auto tables = query_database(database_path, "SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name");
|
||||
BOOST_REQUIRE(tables);
|
||||
BOOST_TEST(has_value(*tables, 0U, "songs"));
|
||||
BOOST_TEST(has_value(*tables, 0U, "practice_exercises"));
|
||||
BOOST_TEST(has_value(*tables, 0U, "page_state"));
|
||||
BOOST_TEST(!has_value(*tables, 0U, "_noto_schema_migrations"));
|
||||
auto version = query_database(database_path, "PRAGMA user_version");
|
||||
BOOST_REQUIRE(version);
|
||||
BOOST_REQUIRE_EQUAL(version->rows.size(), 1U);
|
||||
BOOST_TEST(integer_value(version->rows[0][0]) == 0);
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(database_ensure_updates_legacy_schema_in_place) {
|
||||
auto root =
|
||||
std::filesystem::temp_directory_path() /
|
||||
("product-vamp-db-legacy-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
auto database_path = root / "vamp.db";
|
||||
std::filesystem::remove_all(root);
|
||||
std::filesystem::create_directories(root);
|
||||
BOOST_REQUIRE(query_database(database_path, R"sql(
|
||||
CREATE TABLE songs (
|
||||
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 120),
|
||||
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
|
||||
composer TEXT NOT NULL DEFAULT '',
|
||||
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
|
||||
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
|
||||
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
|
||||
) STRICT
|
||||
)sql"));
|
||||
BOOST_REQUIRE(query_database(database_path, R"sql(
|
||||
CREATE TABLE practice_exercises (
|
||||
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 160),
|
||||
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
|
||||
composer TEXT NOT NULL DEFAULT '',
|
||||
key_signature TEXT NOT NULL DEFAULT 'C',
|
||||
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
|
||||
source_path TEXT NOT NULL DEFAULT '',
|
||||
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
|
||||
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
|
||||
) STRICT
|
||||
)sql"));
|
||||
BOOST_REQUIRE(query_database(database_path, "CREATE TABLE _noto_schema_migrations(id TEXT PRIMARY KEY NOT NULL) STRICT"));
|
||||
BOOST_REQUIRE(query_database(database_path, "PRAGMA user_version = 5"));
|
||||
auto config = test_database_config(database_path);
|
||||
BOOST_REQUIRE(run_ensure_database(config));
|
||||
auto song_columns = query_database(database_path, "PRAGMA table_info(songs)");
|
||||
BOOST_REQUIRE(song_columns);
|
||||
BOOST_TEST(has_value(*song_columns, 1U, "part"));
|
||||
BOOST_TEST(has_value(*song_columns, 1U, "source_path"));
|
||||
BOOST_TEST(has_value(*song_columns, 1U, "key_signature"));
|
||||
BOOST_TEST(has_value(*song_columns, 1U, "catalog_managed"));
|
||||
auto practice_columns = query_database(database_path, "PRAGMA table_info(practice_exercises)");
|
||||
BOOST_REQUIRE(practice_columns);
|
||||
BOOST_TEST(has_value(*practice_columns, 1U, "practice_type_id"));
|
||||
BOOST_TEST(has_value(*practice_columns, 1U, "practice_type_title"));
|
||||
BOOST_TEST(has_value(*practice_columns, 1U, "lesson_group_id"));
|
||||
BOOST_TEST(has_value(*practice_columns, 1U, "lesson_group_title"));
|
||||
BOOST_TEST(has_value(*practice_columns, 1U, "catalog_managed"));
|
||||
auto tables = query_database(database_path, "SELECT name FROM sqlite_schema WHERE type = 'table'");
|
||||
BOOST_REQUIRE(tables);
|
||||
BOOST_TEST(!has_value(*tables, 0U, "_noto_schema_migrations"));
|
||||
auto version = query_database(database_path, "PRAGMA user_version");
|
||||
BOOST_REQUIRE(version);
|
||||
BOOST_REQUIRE_EQUAL(version->rows.size(), 1U);
|
||||
BOOST_TEST(integer_value(version->rows[0][0]) == 0);
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(database_import_upserts_catalog_rows_and_keeps_unmanaged_rows) {
|
||||
auto root = std::filesystem::temp_directory_path() /
|
||||
("product-vamp-db-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
auto assets = root / "assets";
|
||||
auto database_path = root / "vamp.db";
|
||||
auto song_path = assets / "songs" / "tune.json";
|
||||
std::filesystem::remove_all(root);
|
||||
write_text(song_path, score_asset_json("Catalog Tune"));
|
||||
auto config = test_database_config(database_path);
|
||||
BOOST_REQUIRE(run_ensure_database(config));
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto first = query_database(database_path, "SELECT title, created_at_ms, updated_at_ms, catalog_managed FROM songs WHERE id = 'tune'");
|
||||
BOOST_REQUIRE(first);
|
||||
BOOST_REQUIRE_EQUAL(first->rows.size(), 1U);
|
||||
auto created = integer_value(first->rows[0][1]);
|
||||
auto updated = integer_value(first->rows[0][2]);
|
||||
BOOST_TEST(text_value(first->rows[0][0]) == "Catalog Tune");
|
||||
BOOST_TEST(integer_value(first->rows[0][3]) == 1);
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto unchanged = query_database(database_path, "SELECT created_at_ms, updated_at_ms FROM songs WHERE id = 'tune'");
|
||||
BOOST_REQUIRE(unchanged);
|
||||
BOOST_REQUIRE_EQUAL(unchanged->rows.size(), 1U);
|
||||
BOOST_TEST(integer_value(unchanged->rows[0][0]) == created);
|
||||
BOOST_TEST(integer_value(unchanged->rows[0][1]) == updated);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds{2});
|
||||
write_text(song_path, score_asset_json("Catalog Tune Updated"));
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto changed =
|
||||
query_database(database_path, "SELECT title, created_at_ms, updated_at_ms FROM songs WHERE id = 'tune'");
|
||||
BOOST_REQUIRE(changed);
|
||||
BOOST_REQUIRE_EQUAL(changed->rows.size(), 1U);
|
||||
BOOST_TEST(text_value(changed->rows[0][0]) == "Catalog Tune Updated");
|
||||
BOOST_TEST(integer_value(changed->rows[0][1]) == created);
|
||||
BOOST_TEST(integer_value(changed->rows[0][2]) >= updated);
|
||||
auto inserted_custom = query_database(database_path,
|
||||
"INSERT INTO songs(id, title, composer, source_json, created_at_ms, updated_at_ms, part, source_path, "
|
||||
"key_signature, catalog_managed) VALUES('custom', 'Custom Tune', 'Composer', ?1, 1, 1, 'Lead', 'custom.json', 'C', "
|
||||
"0)",
|
||||
{noto::sqlite_value{score_asset_json("Custom Tune")}});
|
||||
BOOST_REQUIRE(inserted_custom);
|
||||
write_text(assets / "songs" / "custom.json", score_asset_json("Catalog Collision"));
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto custom = query_database(database_path, "SELECT title, catalog_managed FROM songs WHERE id = 'custom'");
|
||||
BOOST_REQUIRE(custom);
|
||||
BOOST_REQUIRE_EQUAL(custom->rows.size(), 1U);
|
||||
BOOST_TEST(text_value(custom->rows[0][0]) == "Custom Tune");
|
||||
BOOST_TEST(integer_value(custom->rows[0][1]) == 0);
|
||||
std::filesystem::remove(assets / "songs" / "custom.json");
|
||||
std::filesystem::remove(song_path);
|
||||
BOOST_REQUIRE(run_import(config, assets));
|
||||
auto counts = query_database(database_path, "SELECT sum(CASE WHEN id = 'tune' THEN 1 ELSE 0 END), sum(CASE WHEN id = 'custom' THEN 1 ELSE 0 END) FROM songs");
|
||||
BOOST_REQUIRE(counts);
|
||||
BOOST_REQUIRE_EQUAL(counts->rows.size(), 1U);
|
||||
BOOST_TEST(integer_value(counts->rows[0][0]) == 0);
|
||||
BOOST_TEST(integer_value(counts->rows[0][1]) == 1);
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_bundle_preserves_complete_music_without_runtime_flattening) {
|
||||
auto source = score_asset_json("Fixture");
|
||||
auto parsed = vamp_zero::parse_score_document(source, "fixture.json");
|
||||
|
||||
Reference in New Issue
Block a user