@@ -1,4 +1,5 @@
|
||||
add_executable(product-vamp-test
|
||||
../src/vamp_pico/remote_display.cpp
|
||||
test.cpp
|
||||
)
|
||||
target_include_directories(product-vamp-test PRIVATE
|
||||
@@ -8,6 +9,9 @@ target_link_libraries(product-vamp-test PRIVATE
|
||||
vamp-zero-core
|
||||
)
|
||||
target_compile_features(product-vamp-test PRIVATE cxx_std_23)
|
||||
target_compile_definitions(product-vamp-test PRIVATE
|
||||
VAMP_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
add_test(NAME product-vamp COMMAND product-vamp-test)
|
||||
set_tests_properties(product-vamp PROPERTIES TIMEOUT 60)
|
||||
add_test(
|
||||
|
||||
+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);
|
||||
|
||||
@@ -3,10 +3,22 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from generator.arrange.generate import arrange_document
|
||||
from generator.arrange.fretboard import chord_pitch_classes, pitches_for_voicing
|
||||
from generator.transposition import mod12
|
||||
from generator.score import normalize_score_document
|
||||
|
||||
|
||||
class CompactScoreJsonTests(unittest.TestCase):
|
||||
def pitch_classes(self, pitches: list[dict]) -> set[int]:
|
||||
natural = {"c": 0, "d": 2, "e": 4, "f": 5, "g": 7, "a": 9, "b": 11}
|
||||
alterations = {"bb": -2, "b": -1, "n": 0, "": 0, "#": 1, "x": 2}
|
||||
result: set[int] = set()
|
||||
for pitch in pitches:
|
||||
letter = str(pitch.get("letter", "c")).lower()
|
||||
accidental = str(pitch.get("accidental", "n"))
|
||||
result.add(mod12(natural.get(letter, 0) + alterations.get(accidental, 0)))
|
||||
return result
|
||||
|
||||
def chord_symbols(self, measure: dict) -> list[str]:
|
||||
return [
|
||||
note["chord_symbol"]
|
||||
@@ -87,6 +99,33 @@ class CompactScoreJsonTests(unittest.TestCase):
|
||||
if part_type in {"acoustic", "electric"}:
|
||||
self.assertTrue(all(note["kind"] == "rest" for note in self.measure_notes(final_turnaround)))
|
||||
|
||||
def test_slash_arrangements_preserve_source_chord_markers(self) -> None:
|
||||
document = normalize_score_document({
|
||||
"title": "Slash Tune",
|
||||
"key": "C",
|
||||
"time": "4/4",
|
||||
"measures": [
|
||||
"[Cmaj7] C4:1",
|
||||
"[F7#11] F4:1",
|
||||
"[B-7b5] B4:1",
|
||||
"[E7b9] E4:1",
|
||||
],
|
||||
})
|
||||
|
||||
arranged = arrange_document(document, "acoustic")
|
||||
slash_measure = arranged["staves"][0]["measures"][12]
|
||||
|
||||
self.assertEqual(self.chord_symbols(slash_measure), ["Cmaj7"])
|
||||
self.assertTrue(all(note.get("notehead") == "slash" for note in self.measure_notes(slash_measure)))
|
||||
|
||||
def test_jazz_core_chords_include_color_tones(self) -> None:
|
||||
self.assertEqual(set(chord_pitch_classes("G7b9")), {7, 11, 5, 8})
|
||||
self.assertEqual(set(chord_pitch_classes("C13")), {0, 4, 10, 9})
|
||||
self.assertEqual(set(chord_pitch_classes("Fmaj7#11")), {5, 9, 4, 11})
|
||||
|
||||
self.assertTrue({11, 5, 8}.issubset(self.pitch_classes(pitches_for_voicing("G7b9"))))
|
||||
self.assertTrue({4, 10, 9}.issubset(self.pitch_classes(pitches_for_voicing("C13"))))
|
||||
|
||||
def test_compact_json_normalizes_pitch_groups_and_tuplets(self) -> None:
|
||||
document = normalize_score_document({
|
||||
"title": "Practice",
|
||||
|
||||
Reference in New Issue
Block a user