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
+4
View File
@@ -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
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);
+39
View File
@@ -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",