Styles
ci / checks (push) Successful in 39m31s

This commit is contained in:
khodowany
2026-09-08 22:07:31 -05:00
parent 6e7db6fd97
commit fa7485d404
28 changed files with 522 additions and 292 deletions
+100 -12
View File
@@ -209,13 +209,12 @@ void expect_down_stem_connected(vamp_zero::score_layout_item const& head, vamp_z
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 run_ensure_database(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);
auto future = boost::asio::co_spawn(io, vamp_zero::async_ensure_database(io.get_executor(), std::move(config)), boost::asio::use_future);
io.run();
return future.get();
}
@@ -264,9 +263,14 @@ auto integer_value(noto::sqlite_value const& value) -> std::int64_t {
return integer == nullptr ? 0 : *integer;
}
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 {
return std::string{R"json({
"version": 1,
"metadata": {
"title": ")json"} + title + R"json(",
"composer": "Composer",
@@ -318,7 +322,6 @@ void write_text(std::filesystem::path const& path, std::string const& text) {
BOOST_AUTO_TEST_CASE(score_parser_preserves_notation_preamble) {
auto parsed = vamp_zero::parse_score_document(R"json(
{
"version": 1,
"metadata": {
"title": "Standard Tune",
"composer": "Composer",
@@ -467,7 +470,7 @@ BOOST_AUTO_TEST_CASE(tied_rests_hidden_and_slash_notes_do_not_emit_accidentals)
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_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);
@@ -477,19 +480,17 @@ BOOST_AUTO_TEST_CASE(pico_remote_display_parser_accepts_crlf_payloads) {
BOOST_TEST(state.pages == 11U);
}
BOOST_AUTO_TEST_CASE(pico_remote_display_parser_rejects_missing_version_and_bad_numbers) {
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("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(!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{"version=1\ntitle="} + std::string(140U, 'A') + "\npart=" + std::string(80U, 'B') + "\nmode=performance-score";
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);
@@ -497,6 +498,93 @@ BOOST_AUTO_TEST_CASE(pico_remote_display_parser_truncates_long_fields_safely) {
BOOST_TEST(std::strcmp(state.mode, "performance-score") == 0);
}
BOOST_AUTO_TEST_CASE(database_ensure_creates_current_schema_without_version_bookkeeping) {
auto root = std::filesystem::temp_directory_path()
/ ("product-vamp-db-schema-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto database_path = root / "vamp.db";
std::filesystem::remove_all(root);
auto config = test_database_config(database_path);
BOOST_REQUIRE(run_ensure_database(config));
BOOST_REQUIRE(run_ensure_database(config));
auto tables = query_database(database_path, "SELECT name FROM sqlite_schema WHERE type = 'table' ORDER BY name");
BOOST_REQUIRE(tables);
BOOST_TEST(has_value(*tables, 0U, "songs"));
BOOST_TEST(has_value(*tables, 0U, "practice_exercises"));
BOOST_TEST(has_value(*tables, 0U, "page_state"));
BOOST_TEST(!has_value(*tables, 0U, "_noto_schema_migrations"));
auto version = query_database(database_path, "PRAGMA user_version");
BOOST_REQUIRE(version);
BOOST_REQUIRE_EQUAL(version->rows.size(), 1U);
BOOST_TEST(integer_value(version->rows[0][0]) == 0);
std::filesystem::remove_all(root);
}
BOOST_AUTO_TEST_CASE(database_ensure_updates_legacy_schema_in_place) {
auto root = std::filesystem::temp_directory_path()
/ ("product-vamp-db-legacy-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
auto database_path = root / "vamp.db";
std::filesystem::remove_all(root);
std::filesystem::create_directories(root);
BOOST_REQUIRE(query_database(database_path, R"sql(
CREATE TABLE songs (
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 120),
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
composer TEXT NOT NULL DEFAULT '',
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
) STRICT
)sql"));
BOOST_REQUIRE(query_database(database_path, R"sql(
CREATE TABLE practice_exercises (
id TEXT PRIMARY KEY NOT NULL CHECK(length(trim(id)) BETWEEN 1 AND 160),
title TEXT NOT NULL CHECK(length(trim(title)) BETWEEN 1 AND 200),
composer TEXT NOT NULL DEFAULT '',
key_signature TEXT NOT NULL DEFAULT 'C',
source_json TEXT NOT NULL CHECK(json_valid(source_json)),
source_path TEXT NOT NULL DEFAULT '',
created_at_ms INTEGER NOT NULL CHECK(created_at_ms >= 0),
updated_at_ms INTEGER NOT NULL CHECK(updated_at_ms >= created_at_ms)
) STRICT
)sql"));
BOOST_REQUIRE(query_database(database_path, "CREATE TABLE _noto_schema_migrations(id TEXT PRIMARY KEY NOT NULL) STRICT"));
BOOST_REQUIRE(query_database(database_path, "PRAGMA user_version = 5"));
auto config = test_database_config(database_path);
BOOST_REQUIRE(run_ensure_database(config));
auto song_columns = query_database(database_path, "PRAGMA table_info(songs)");
BOOST_REQUIRE(song_columns);
BOOST_TEST(has_value(*song_columns, 1U, "part"));
BOOST_TEST(has_value(*song_columns, 1U, "source_path"));
BOOST_TEST(has_value(*song_columns, 1U, "key_signature"));
BOOST_TEST(has_value(*song_columns, 1U, "catalog_managed"));
auto practice_columns = query_database(database_path, "PRAGMA table_info(practice_exercises)");
BOOST_REQUIRE(practice_columns);
BOOST_TEST(has_value(*practice_columns, 1U, "practice_type_id"));
BOOST_TEST(has_value(*practice_columns, 1U, "practice_type_title"));
BOOST_TEST(has_value(*practice_columns, 1U, "lesson_group_id"));
BOOST_TEST(has_value(*practice_columns, 1U, "lesson_group_title"));
BOOST_TEST(has_value(*practice_columns, 1U, "catalog_managed"));
auto tables = query_database(database_path, "SELECT name FROM sqlite_schema WHERE type = 'table'");
BOOST_REQUIRE(tables);
BOOST_TEST(!has_value(*tables, 0U, "_noto_schema_migrations"));
auto version = query_database(database_path, "PRAGMA user_version");
BOOST_REQUIRE(version);
BOOST_REQUIRE_EQUAL(version->rows.size(), 1U);
BOOST_TEST(integer_value(version->rows[0][0]) == 0);
std::filesystem::remove_all(root);
}
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()));
@@ -507,7 +595,7 @@ BOOST_AUTO_TEST_CASE(database_import_upserts_catalog_rows_and_keeps_unmanaged_ro
write_text(song_path, score_asset_json("Catalog Tune"));
auto config = test_database_config(database_path);
BOOST_REQUIRE(run_migrate(config));
BOOST_REQUIRE(run_ensure_database(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'");
+62
View File
@@ -4,6 +4,7 @@ import unittest
from generator.arrange.generate import arrange_document
from generator.arrange.fretboard import chord_pitch_classes, pitches_for_voicing
from generator.styles import BUILT_IN_STYLES
from generator.transposition import mod12
from generator.score import normalize_score_document
@@ -48,7 +49,9 @@ class CompactScoreJsonTests(unittest.TestCase):
first_note = staff["measures"][0]["voices"][0]["notes"][0]
second_measure_first_note = staff["measures"][1]["voices"][0]["notes"][0]
self.assertNotIn("version", document)
self.assertEqual(document["metadata"]["key_signature"], "F")
self.assertEqual(document["metadata"]["style"], "Medium Swing")
self.assertEqual(staff["name"], "Vocal")
self.assertEqual(first_note["duration_ticks"], 16384)
self.assertEqual(first_note["chord_symbol"], "G-7")
@@ -56,6 +59,64 @@ class CompactScoreJsonTests(unittest.TestCase):
self.assertTrue(first_note["tie_to_next"])
self.assertTrue(second_measure_first_note["tie_from_previous"])
def test_compact_json_canonicalizes_player_styles(self) -> None:
self.assertEqual(len(BUILT_IN_STYLES), 50)
for style in ("Med.", "Med. Swing", "Medium", "Medium Swing"):
document = normalize_score_document({
"title": "Style Tune",
"style": style,
"measures": ["C4:1"],
})
self.assertEqual(document["metadata"]["style"], "Medium Swing")
with self.assertRaisesRegex(ValueError, "supported player style"):
normalize_score_document({
"title": "Bad Style Tune",
"style": "Medium Bossa That Does Not Exist",
"measures": ["C4:1"],
})
def test_compact_json_preserves_musicxml_like_measure_metadata(self) -> None:
document = normalize_score_document({
"title": "Metadata Tune",
"measures": [
{
"notes": "C4:1",
"attributes": {"divisions": 4},
"directions": [{"type": "dynamics", "value": "mf"}],
"harmony": [{"root": "C", "kind": "major-seventh"}],
"barlines": {"right": "light-heavy"},
},
],
})
measure = document["staves"][0]["measures"][0]
self.assertEqual(measure["attributes"], {"divisions": 4})
self.assertEqual(measure["directions"], [{"type": "dynamics", "value": "mf"}])
self.assertEqual(measure["harmony"], [{"root": "C", "kind": "major-seventh"}])
self.assertEqual(measure["barlines"], {"right": "light-heavy"})
def test_arrangements_preserve_measure_notation_metadata(self) -> None:
document = normalize_score_document({
"title": "Directions Tune",
"key": "C",
"time": "4/4",
"measures": [
{
"notes": "[Cmaj7] C4:1",
"directions": [{"type": "rehearsal", "value": "A"}],
"barlines": {"left": "heavy-light"},
},
],
})
arranged = arrange_document(document, "vocal")
measure = arranged["staves"][0]["measures"][0]
self.assertEqual(measure["directions"], [{"type": "rehearsal", "value": "A"}])
self.assertEqual(measure["barlines"], {"left": "heavy-light"})
def test_compact_json_marks_optional_chords(self) -> None:
document = normalize_score_document({
"title": "Optional Turnaround",
@@ -92,6 +153,7 @@ class CompactScoreJsonTests(unittest.TestCase):
non_final_turnaround = measures[7]
final_turnaround = measures[-1]
self.assertNotIn("version", arranged)
self.assertEqual(self.chord_symbols(non_final_turnaround), ["F-7", "Bb7"])
self.assertEqual(self.chord_symbols(final_turnaround), [])
self.assertFalse(any("chord_optional" in note for measure in measures for note in self.measure_notes(measure)))