Cleanup
ci / checks (push) Successful in 38m56s

This commit is contained in:
khodowany
2026-09-08 08:53:41 -05:00
parent 62abb2d3f6
commit d7919f148d
21 changed files with 1322 additions and 479 deletions
+286
View File
@@ -1,16 +1,32 @@
#include <test.hpp>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <future>
#include <initializer_list>
#include <limits>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <variant>
#include <vector>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/use_future.hpp>
#include <noto.hpp>
#include <vamp_pico/http_client.hpp>
#include <vamp_zero/database.hpp>
#include <vamp_zero/library.hpp>
#include <vamp_zero/score.hpp>
#include <vamp_zero/types.hpp>
@@ -166,6 +182,113 @@ auto near(std::float32_t left, std::float32_t right) -> std::bool_t {
return std::abs(left - right) < 0.02F;
}
auto test_database_config(std::filesystem::path const& path) -> vamp_zero::database_config {
return {
.path = path.string(),
.migrations_path = (std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "package" / "migrations").string(),
};
}
auto run_migrate(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_migrate_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> {
auto io = boost::asio::io_context{};
auto future = boost::asio::co_spawn(
io,
[executor = io.get_executor(), path = path.string(), sql = std::move(sql), params = std::move(params)]() mutable -> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
auto config = noto::file_connection_config(path, "product-vamp-test");
config.foreign_keys = true;
auto connection = noto::connection{executor, std::move(config)};
auto connected = co_await connection.async_connect();
if (!connected) {
co_return std::unexpected(connected.error());
}
auto selected = co_await connection.async_execute(std::move(sql), std::move(params));
auto closed = co_await connection.async_close();
if (!selected) {
co_return std::unexpected(selected.error());
}
if (!closed) {
co_return std::unexpected(closed.error());
}
co_return selected;
},
boost::asio::use_future);
io.run();
return future.get();
}
auto text_value(noto::sqlite_value const& value) -> std::string {
auto text = std::get_if<std::string>(&value.value);
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 score_asset_json(std::string title, std::string key = "C") -> std::string {
return std::string{R"json({
"version": 1,
"metadata": {
"title": ")json"} + title + R"json(",
"composer": "Composer",
"key_signature": ")json" + key + R"json(",
"time_signature": "4/4"
},
"staves": [
{
"id": "staff-lead",
"name": "Lead",
"clef": "treble",
"role": "lead",
"measures": [
{
"id": "measure-1",
"time_signature": "4/4",
"key_signature": ")json" + key + R"json(",
"voices": [
{
"id": "voice-1",
"notes": [
{
"id": "note-1",
"kind": "note",
"duration_ticks": 4096,
"pitches": [
{"letter": "c", "octave": 4, "accidental": "n"}
]
}
]
}
]
}
]
}
]
}
)json";
}
void write_text(std::filesystem::path const& path, std::string const& text) {
std::filesystem::create_directories(path.parent_path());
auto output = std::ofstream{path, std::ios::binary | std::ios::trunc};
output << text;
}
}
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
@@ -318,6 +441,93 @@ BOOST_AUTO_TEST_CASE(tied_rests_hidden_and_slash_notes_do_not_emit_accidentals)
expect_marks(vamp_zero::notated_accidentals_for_measure(measure, "C"), {"none", "flat"});
}
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("version=1\r\nactive=1\r\ntitle=Autumn Leaves\r\npart=Vocal\r\nmode=score\r\npage=2\r\npages=11\r\n", state));
BOOST_TEST(state.active);
BOOST_TEST(std::strcmp(state.title, "Autumn Leaves") == 0);
BOOST_TEST(std::strcmp(state.part, "Vocal") == 0);
BOOST_TEST(std::strcmp(state.mode, "score") == 0);
BOOST_TEST(state.page == 2U);
BOOST_TEST(state.pages == 11U);
}
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_rejects_missing_version_and_bad_numbers) {
auto state = vamp_pico::remote_display_state{};
std::snprintf(state.title, sizeof(state.title), "Keep");
BOOST_TEST(!vamp_pico::parse_remote_display_body("title=Autumn Leaves\npage=1\npages=2\n", state));
BOOST_TEST(std::strcmp(state.title, "Keep") == 0);
BOOST_TEST(!vamp_pico::parse_remote_display_body("version=1\npage=1x\npages=2\n", state));
BOOST_TEST(std::strcmp(state.title, "Keep") == 0);
}
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_truncates_long_fields_safely) {
auto state = vamp_pico::remote_display_state{};
auto body = std::string{"version=1\ntitle="} + std::string(140U, 'A') + "\npart=" + std::string(80U, 'B') + "\nmode=performance-score";
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(body, state));
BOOST_TEST(std::strlen(state.title) == sizeof(state.title) - 1U);
BOOST_TEST(std::strlen(state.part) == sizeof(state.part) - 1U);
BOOST_TEST(std::strcmp(state.mode, "performance-score") == 0);
}
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_migrate(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);
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_layout_orders_preamble_on_staff_baselines) {
auto score = score_with_notes({note({pitch('e', "n")})}, "F", "4/4", "treble");
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
@@ -340,6 +550,82 @@ BOOST_AUTO_TEST_CASE(score_layout_orders_preamble_on_staff_baselines) {
BOOST_TEST(near(times[1]->origin.y, staff_lines.front()->from.y + layout.staff_spacing * 3.5F));
}
BOOST_AUTO_TEST_CASE(score_system_ranges_force_breaks_before_key_changes) {
auto score = vamp_zero::score_document{
.title = "Key Change",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = {
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('c', "n")})}},
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('d', "n")})}},
{.time_signature = "4/4", .key_signature = "G", .notes = {note({pitch('f', "#")})}},
{.time_signature = "4/4", .key_signature = "G", .notes = {note({pitch('g', "n")})}},
},
};
auto ranges = vamp_zero::score_system_ranges(score);
BOOST_REQUIRE_EQUAL(ranges.size(), 2U);
BOOST_TEST(ranges[0].first_measure == 0U);
BOOST_TEST(ranges[0].measure_count == 2U);
BOOST_TEST(ranges[1].first_measure == 2U);
BOOST_TEST(ranges[1].measure_count == 2U);
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
BOOST_REQUIRE(!layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 1U, 2U, 0U).empty());
BOOST_TEST(!layout_items(layout, vamp_zero::score_layout_item_role::KEY_SIGNATURE, 1U).empty());
BOOST_TEST(layout_items(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, 1U).empty());
}
BOOST_AUTO_TEST_CASE(score_system_ranges_force_breaks_before_time_changes) {
auto score = vamp_zero::score_document{
.title = "Time Change",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = {
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('c', "n")})}},
{.time_signature = "4/4", .key_signature = "C", .notes = {note({pitch('d', "n")})}},
{.time_signature = "3/4", .key_signature = "C", .notes = {note({pitch('e', "n")})}},
},
};
auto ranges = vamp_zero::score_system_ranges(score);
BOOST_REQUIRE_EQUAL(ranges.size(), 2U);
BOOST_TEST(ranges[1].first_measure == 2U);
BOOST_TEST(ranges[1].measure_count == 1U);
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);
BOOST_REQUIRE(!layout_items(layout, vamp_zero::score_layout_item_role::NOTEHEAD, 1U, 2U, 0U).empty());
BOOST_TEST(!layout_items(layout, vamp_zero::score_layout_item_role::TIME_SIGNATURE, 1U).empty());
}
BOOST_AUTO_TEST_CASE(score_page_count_uses_forced_notation_breaks) {
auto measures = std::vector<vamp_zero::score_measure>{};
for (auto index = std::size_t{}; index < 10U; ++index) {
measures.push_back({
.time_signature = "4/4",
.key_signature = (index / 2U) % 2U == 0U ? "C" : "G",
.notes = {note({pitch('c', "n")})},
});
}
auto score = vamp_zero::score_document{
.title = "Many Breaks",
.composer = "Composer",
.key_signature = "C",
.time_signature = "4/4",
.clef = "treble",
.part = "Lead",
.measures = std::move(measures),
};
BOOST_TEST(vamp_zero::score_page_count(score) == 2U);
}
BOOST_AUTO_TEST_CASE(score_layout_attaches_stems_to_notehead_anchors) {
auto score = score_with_notes({note({pitch('d', "n")})});
auto layout = vamp_zero::layout_score_page({.x = 0.0F, .y = 0.0F, .width = 976.0F, .height = 484.0F}, score, 0U);