+286
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user