Files
product-vamp/tests/test.cpp
T
khodowany da5640291a
ci / checks (push) Successful in 1h15m45s
Cleanupf
2026-09-23 23:54:19 -05:00

596 lines
28 KiB
C++

#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 <boost/json/object.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/serialize.hpp>
#include <boost/json/array.hpp>
#include <noto.hpp>
#include <vamp_protocol/page_control.hpp>
#include <vamp_zero/config.hpp>
#include <vamp_pico/http_client.hpp>
#include <vamp_zero/database.hpp>
#include <vamp_zero/library.hpp>
#include <vamp_zero/navigation.hpp>
#include <vamp_zero/parse.hpp>
#include <vamp_zero/page_control.hpp>
#include <vamp_zero/portal.hpp>
#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 {
return {
.path = path.string(),
};
}
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 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;
});
}
auto score_asset_json(std::string title, std::string key = "C", std::string part = "Lead") -> std::string {
auto value = boost::json::parse(R"json({
"schema_version":3,
"music":{"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"},
"staves":[{"id":"staff-lead","name":"Lead","measures":[{"id":"measure-1","voices":[]}]}]},
"presentation":{"width":976,"height":484,"pages":[{"path":"fixture.pages/0001.png",
"sha256":"0000000000000000000000000000000000000000000000000000000000000000"}]}
})json");
auto &metadata = value.as_object()["music"].as_object()["metadata"].as_object();
metadata["title"] = std::move(title);
metadata["key_signature"] = std::move(key);
value.at("music").at("staves").as_array().front().as_object()["name"] = std::move(part);
return boost::json::serialize(value);
}
auto navigation_state() -> vamp_zero::app_state {
auto part = vamp_zero::library_part{
.id = "part-lead",
.title = "Navigation Tune",
.composer = "Composer",
.key_signature = "C",
.part = "Lead",
.source_path = "navigation.json",
.measure_count = 1,
.total_pages = 2,
};
auto practice_part = vamp_zero::library_part{
.key_signature = "C",
.source_path = "exercise.json",
.measure_count = 1,
.total_pages = 1,
};
auto exercise = vamp_zero::practice_exercise{
.id = "exercise-1",
.title = "C Major",
.composer = "Composer",
};
for (auto part_name : vamp_zero::practice_part_names()) {
practice_part.id = "exercise-" + std::string{part_name};
practice_part.part = part_name;
exercise.parts.push_back(practice_part);
}
auto practice = vamp_zero::group_practice_exercises({exercise});
BOOST_REQUIRE(practice);
return vamp_zero::app_state{
.catalog = std::make_shared<vamp_zero::catalogue const>(vamp_zero::catalogue{
.performance = vamp_zero::group_library_parts({part}),
.practice = std::move(*practice),
}),
};
}
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(runtime_config_uses_headless_and_ignores_unknown_fields) {
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":true,"obsolete_setting":"ignored"},"audio":{"enabled":false}})json"));
BOOST_REQUIRE(parsed);
BOOST_TEST(parsed->runtime.headless);
BOOST_TEST(!parsed->audio.enabled);
auto invalid = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":"yes"}})json"));
BOOST_TEST(!invalid);
}
BOOST_AUTO_TEST_CASE(product_vamp_audio_config_parses_defaults_and_values) {
auto defaults = vamp_zero::parse_config(boost::json::parse(R"json({})json"));
BOOST_REQUIRE(defaults);
BOOST_TEST(defaults->audio.enabled);
BOOST_TEST(!defaults->audio.playback_enabled);
BOOST_TEST(!defaults->audio.metronome_enabled);
BOOST_TEST(defaults->audio.metronome_bpm == 120U);
BOOST_TEST(defaults->audio.metronome_min_bpm == 40U);
BOOST_TEST(defaults->audio.metronome_max_bpm == 240U);
BOOST_TEST(defaults->audio.metronome_step == 5U);
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({
"audio": {
"enabled": false,
"playback_enabled": true,
"metronome_enabled": true,
"metronome_bpm": 90,
"metronome_min_bpm": 50,
"metronome_max_bpm": 180,
"metronome_step": 10,
"beats_per_bar": 3,
"master_volume": 0.5,
"metronome_volume": 0.25
}
})json"));
BOOST_REQUIRE(parsed);
BOOST_TEST(!parsed->audio.enabled);
BOOST_TEST(parsed->audio.playback_enabled);
BOOST_TEST(parsed->audio.metronome_enabled);
BOOST_TEST(parsed->audio.metronome_bpm == 90U);
BOOST_TEST(parsed->audio.metronome_min_bpm == 50U);
BOOST_TEST(parsed->audio.metronome_max_bpm == 180U);
BOOST_TEST(parsed->audio.metronome_step == 10U);
BOOST_TEST(parsed->audio.beats_per_bar == 3U);
BOOST_TEST(parsed->audio.master_volume == 0.5F);
BOOST_TEST(parsed->audio.metronome_volume == 0.25F);
}
BOOST_AUTO_TEST_CASE(product_vamp_audio_config_rejects_invalid_tempo_range) {
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({
"audio": {
"metronome_bpm": 120,
"metronome_min_bpm": 160,
"metronome_max_bpm": 80
}
})json"));
BOOST_TEST(!parsed);
}
BOOST_AUTO_TEST_CASE(main_menu_toggles_audio_settings_and_edits_tempo) {
auto state = navigation_state();
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.selected_main_menu_index == 2U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(state.audio.playback_enabled);
BOOST_TEST(static_cast<std::int32_t>(state.mode) == static_cast<std::int32_t>(vamp_zero::app_mode::MAIN_MENU));
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.selected_main_menu_index == 3U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(state.audio.metronome_enabled);
BOOST_TEST(static_cast<std::int32_t>(state.mode) == static_cast<std::int32_t>(vamp_zero::app_mode::MAIN_MENU));
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.selected_main_menu_index == 4U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(state.audio.tempo_editing);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.audio.metronome_bpm == 125U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
BOOST_TEST(state.audio.metronome_bpm == 120U);
for (auto index = std::int32_t{}; index < 40; ++index) {
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
}
BOOST_TEST(state.audio.metronome_bpm == 40U);
for (auto index = std::int32_t{}; index < 80; ++index) {
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
}
BOOST_TEST(state.audio.metronome_bpm == 240U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(!state.audio.tempo_editing);
}
BOOST_AUTO_TEST_CASE(audio_status_json_reports_menu_settings) {
auto state = navigation_state();
state.audio.playback_enabled = true;
state.audio.metronome_enabled = true;
state.audio.metronome_bpm = 135;
state.audio.tempo_editing = true;
auto status = vamp_zero::app_status_json(state).as_object();
auto const &audio = status.at("audio").as_object();
BOOST_TEST(audio.at("playback_enabled").as_bool());
BOOST_TEST(audio.at("metronome_enabled").as_bool());
BOOST_TEST(audio.at("metronome_bpm").as_uint64() == 135U);
BOOST_TEST(audio.at("metronome_min_bpm").as_uint64() == 40U);
BOOST_TEST(audio.at("metronome_max_bpm").as_uint64() == 240U);
BOOST_TEST(audio.at("metronome_step").as_uint64() == 5U);
BOOST_TEST(audio.at("tempo_editing").as_bool());
}
BOOST_AUTO_TEST_CASE(main_menu_metronome_setting_survives_score_selection) {
auto state = navigation_state();
state.audio.metronome_enabled = true;
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(static_cast<std::int32_t>(state.mode) == static_cast<std::int32_t>(vamp_zero::app_mode::PERFORMANCE_SONG_MENU));
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(static_cast<std::int32_t>(state.mode) == static_cast<std::int32_t>(vamp_zero::app_mode::PERFORMANCE_PART_MENU));
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_TEST(static_cast<std::int32_t>(state.mode) == static_cast<std::int32_t>(vamp_zero::app_mode::PERFORMANCE_SCORE));
BOOST_TEST(state.audio.metronome_enabled);
BOOST_TEST(!state.audio.tempo_editing);
}
BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) {
auto count = std::size_t{};
for (auto const &entry : std::filesystem::recursive_directory_iterator{VAMP_TEST_ASSETS_DIR}) {
if (entry.path().extension() != ".json") {
continue;
}
auto input = std::ifstream{entry.path()};
auto json = std::string{std::istreambuf_iterator<std::char_t>{input}, std::istreambuf_iterator<std::char_t>{}};
auto parsed = vamp_zero::parse_score_document(json, entry.path().string());
BOOST_TEST_CONTEXT(entry.path().string()) {
BOOST_REQUIRE(parsed.has_value());
BOOST_TEST(vamp_zero::score_page_count(*parsed) > 0U);
}
++count;
}
BOOST_TEST(count > 0U);
}
BOOST_AUTO_TEST_CASE(practice_part_navigation_and_remote_display) {
auto state = navigation_state();
auto catalog = std::make_shared<vamp_zero::catalogue>(*state.catalog);
auto &practice_part = catalog->practice.exercises.front().parts[2];
practice_part.source_path = "exercise-vocal.json";
practice_part.total_pages = 3U;
state.catalog = catalog;
state.selected_main_menu_index = 1U;
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
state.audio.metronome_enabled = true;
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU);
BOOST_TEST(vamp_zero::practice_part_row_count(state) == 4U);
for (auto index = std::uint32_t{}; index < 3U; ++index) {
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
}
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_SCORE);
BOOST_TEST(state.audio.metronome_enabled);
auto selected = vamp_zero::selected_practice_part(state, state.catalog->practice.exercises.front());
BOOST_REQUIRE(selected != nullptr);
BOOST_TEST(selected->id == "exercise-Vocal");
BOOST_TEST(selected->source_path == "exercise-vocal.json");
BOOST_TEST(vamp_zero::active_pages(state) == 3U);
auto remote = vamp_pico::remote_display_state{};
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote));
BOOST_TEST(remote.active);
BOOST_TEST(std::string{remote.part} == "Vocal");
BOOST_TEST(remote.pages == 3U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.page_index == 1U);
auto playing = state;
playing.audio.playback_enabled = true;
auto control = vamp_zero::control_state{{}, playing};
control.finish_playback(playing.score_session);
BOOST_CHECK(control.snapshot().mode == vamp_zero::app_mode::PRACTICE_PART_MENU);
BOOST_TEST(control.snapshot().selected_practice_part_index == 3U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU);
BOOST_TEST(state.selected_practice_part_index == 3U);
BOOST_TEST(state.active_practice_id.empty());
for (auto index = std::uint32_t{}; index < 3U; ++index) {
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
}
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU);
BOOST_TEST(state.selected_practice_exercise_index == 1U);
auto status = vamp_zero::app_status_json(state).as_object().at("practice").as_object();
BOOST_TEST(!status.contains("types"));
BOOST_TEST(status.at("exercises").as_array().size() == 1U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::MAIN_MENU);
BOOST_REQUIRE(vamp_zero::validate_app_state(state));
}
BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalogue_returns_to_main) {
auto state = navigation_state();
auto catalog = std::make_shared<vamp_zero::catalogue>(*state.catalog);
auto first = catalog->practice.exercises.front();
for (auto index = std::uint32_t{2}; index <= 366U; ++index) {
auto exercise = first;
exercise.id = std::to_string(index);
catalog->practice.exercises.push_back(std::move(exercise));
}
state.catalog = catalog;
state.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
BOOST_TEST(state.selected_practice_exercise_index == 366U);
BOOST_TEST(vamp_zero::selected_practice_exercise(state)->id == "366");
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_TEST(state.selected_practice_exercise_index == 0U);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_PART_MENU);
state.catalog = vamp_zero::empty_catalogue();
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::PRACTICE_EXERCISE_MENU);
BOOST_TEST(state.selected_practice_exercise_index == 0U);
BOOST_REQUIRE(vamp_zero::validate_app_state(state));
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
BOOST_CHECK(state.mode == vamp_zero::app_mode::MAIN_MENU);
}
BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_parts) {
auto root = std::filesystem::temp_directory_path() / ("product-vamp-parts-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::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);
for (auto iteration = std::uint32_t{}; iteration < 2U; ++iteration) {
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, VAMP_TEST_ASSETS_DIR), boost::asio::use_future);
io.run();
auto library = future.get();
BOOST_REQUIRE_MESSAGE(library.has_value(), (library ? "" : library.error().describe()));
BOOST_TEST(library->exercises.size() == expected_exercises);
for (auto const &exercise : library->exercises) {
BOOST_REQUIRE_EQUAL(exercise.parts.size(), 3U);
auto parts = vamp_zero::practice_part_names();
for (auto index = std::size_t{}; index < parts.size(); ++index) {
BOOST_TEST(exercise.parts[index].part == parts[index]);
BOOST_TEST(exercise.parts[index].total_pages > 0U);
BOOST_TEST(exercise.parts[index].measure_count > 0U);
if (iteration == 0U) {
auto const &practice_part = exercise.parts[index];
io.restart();
auto loaded = boost::asio::co_spawn(io, vamp_zero::async_load_score(io.get_executor(), config, {vamp_zero::score_category::PRACTICE, practice_part.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 == practice_part.key_signature);
BOOST_TEST(score->part == practice_part.part);
BOOST_TEST(score->measure_count == practice_part.measure_count);
BOOST_TEST(score->pages.size() == practice_part.total_pages);
BOOST_TEST(score->source_path == practice_part.source_path);
}
}
}
}
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 : {"010", "002", "001", "127"}) {
for (auto part_name : vamp_zero::practice_part_names()) {
write_text(assets / "practice" / name / vamp_test::part_filename(part_name), score_asset_json("Shared Title", "C", std::string{part_name}));
}
}
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_TEST(library->exercises[0].id == "001");
BOOST_TEST(library->exercises[1].id == "002");
BOOST_TEST(library->exercises[2].id == "010");
BOOST_TEST(library->exercises[3].id == "127");
for (auto const &exercise : library->exercises) {
BOOST_TEST(exercise.title == "Shared Title");
}
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path));
std::filesystem::rename(assets / "practice/002", assets / "practice/099");
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(), 12U);
BOOST_TEST(!has_value(*rows, 0U, "002-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "099-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "010-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "001-acoustic"));
BOOST_TEST(has_value(*rows, 0U, "127-acoustic"));
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(keyless_scores_keep_their_authored_key) {
auto source = score_asset_json("Keyless", "No key", "Vocal");
auto document = vamp_zero::parse_score_document(source, "vocal.json");
BOOST_REQUIRE(document);
BOOST_TEST(document->key_signature == "No key");
BOOST_TEST(document->part == "Vocal");
}
BOOST_AUTO_TEST_CASE(integer_parsing_checks_range_sign_and_entire_input) {
BOOST_TEST(vamp_zero::parse_integer<std::uint16_t>("65535", "port").value_or(0) == 65535U);
BOOST_TEST(vamp_zero::parse_integer<std::int32_t>("-2147483648", "octave").value_or(0) == std::numeric_limits<std::int32_t>::min());
BOOST_TEST(vamp_zero::parse_integer<std::uint64_t>("18446744073709551615", "page").value_or(0) == std::numeric_limits<std::uint64_t>::max());
for (auto text : {"", "-1", "+1", " 1", "1 ", "123x", "1.5", "65536", "18446744073709551616"}) {
auto parsed = vamp_zero::parse_integer<std::uint16_t>(text, "port");
BOOST_REQUIRE(!parsed.has_value());
BOOST_TEST(parsed.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
BOOST_TEST(parsed.error().message().starts_with("port "));
}
}
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));
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_bad_numbers_and_preserves_state) {
auto state = vamp_pico::remote_display_state{};
std::snprintf(state.title, sizeof(state.title), "Keep");
BOOST_TEST(!vamp_pico::parse_remote_display_body("page=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{"title="} + 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(score_bundle_preserves_complete_music_without_runtime_flattening) {
auto source = score_asset_json("Fixture");
auto parsed = vamp_zero::parse_score_document(source, "fixture.json");
BOOST_REQUIRE(parsed);
BOOST_TEST(parsed->title == "Fixture");
BOOST_TEST(parsed->measure_count == 1U);
BOOST_TEST(parsed->source_json == source);
BOOST_TEST(vamp_zero::score_page_count(*parsed) == 1U);
}
BOOST_AUTO_TEST_CASE(score_bundle_rejects_legacy_schema_malformed_manifest_and_unsafe_paths) {
for (auto text : {"", "null", "[]", "{}", "{", R"({"schema_version":2})"}) {
BOOST_CHECK_NO_THROW(BOOST_TEST(!vamp_zero::parse_score_document(text)));
}
for (auto path : {"../page.png", "/page.png", "pages/../../page.png", "pages/./page.png", "page.jpg", ""}) {
auto value = boost::json::parse(score_asset_json("Fixture"));
value.at_pointer("/presentation/pages/0").as_object()["path"] = path;
BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(value)));
}
for (auto pointer : {"/music", "/music/metadata", "/music/staves", "/presentation", "/presentation/pages",
"/presentation/width", "/presentation/pages/0/path", "/presentation/pages/0/sha256"}) {
auto value = boost::json::parse(score_asset_json("Fixture"));
value.at_pointer(pointer) = nullptr;
BOOST_CHECK_NO_THROW(BOOST_TEST(!vamp_zero::parse_score_document(boost::json::serialize(value))));
}
}
BOOST_AUTO_TEST_CASE(score_page_validation_reports_missing_assets) {
auto parsed = vamp_zero::parse_score_document(score_asset_json("Fixture"), "/nonexistent/fixture.json");
BOOST_REQUIRE(parsed);
BOOST_TEST(!vamp_zero::validated_page_path(*parsed, 0));
BOOST_TEST(!vamp_zero::validated_page_path(*parsed, 1));
}
BOOST_AUTO_TEST_CASE(score_page_validation_checks_integrity_and_symlink_boundaries) {
auto root = std::filesystem::temp_directory_path() / ("vamp-page-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto assets = root / "assets";
std::filesystem::create_directories(assets);
auto page = assets / "page.png";
std::filesystem::copy_file(std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "tests/fixtures/engraving.png", page);
auto parsed = vamp_zero::parse_score_document(score_asset_json("Fixture"), (assets / "score.json").string());
BOOST_REQUIRE(parsed);
parsed->pages[0] = {.path = "page.png", .sha256 = "3bb4d7bd173db4517afed83f512b636f56f053a60e2b08db09106e0698f40284"};
BOOST_REQUIRE(vamp_zero::validated_page_path(*parsed, 0));
{
auto output = std::ofstream{page, std::ios::binary | std::ios::app};
output << "corrupt";
}
BOOST_TEST(!vamp_zero::validated_page_path(*parsed, 0));
std::filesystem::rename(page, root / "outside.png");
std::filesystem::create_symlink(root / "outside.png", page);
BOOST_TEST(!vamp_zero::validated_page_path(*parsed, 0));
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(score_page_cache_retains_at_most_two_textures) {
class page_texture final : public luma::texture {
public:
auto info() const -> luma::texture_info override {
return {.width = 976, .height = 484};
}
};
auto calls = std::size_t{};
auto weak = std::vector<std::weak_ptr<luma::texture>>{};
auto cache = vamp_zero::score_page_cache{[&](std::string const &) -> std::expected<std::shared_ptr<luma::texture>, modulus::error> {
++calls;
auto texture = std::make_shared<page_texture>();
weak.push_back(texture);
return texture;
}};
BOOST_REQUIRE(cache.get("one"));
BOOST_REQUIRE(cache.get("two"));
BOOST_REQUIRE(cache.get("one"));
BOOST_TEST(calls == 2U);
BOOST_REQUIRE(cache.get("three"));
BOOST_TEST(cache.size() == 2U);
BOOST_TEST(weak[1].expired());
BOOST_TEST(!weak[0].expired());
cache.clear();
BOOST_TEST(weak[0].expired());
BOOST_TEST(weak[2].expired());
}