Update
ci / checks (push) Successful in 1h15m13s

This commit is contained in:
khodowany
2026-09-24 06:20:37 -05:00
parent da5640291a
commit 1be98e14aa
35 changed files with 191 additions and 184 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ add_executable(product-vamp-test
../src/vamp_pico/input.cpp
../src/vamp_pico/remote_display.cpp
test.cpp
test_catalogue_runtime.cpp
test_catalog_runtime.cpp
test_health.cpp
test_pico.cpp
test_playback.cpp
@@ -21,13 +21,13 @@ inline auto part_filename(std::string_view part) -> std::string {
return result + ".json";
}
inline auto build_catalogue(std::filesystem::path const &assets, std::filesystem::path const &database) -> std::bool_t {
inline auto build_catalog(std::filesystem::path const &assets, std::filesystem::path const &database) -> std::bool_t {
auto source = assets.string();
auto destination = database.string();
auto child = ::fork();
BOOST_REQUIRE(child >= 0);
if (child == 0) {
::execl(VAMP_TEST_PYTHON, VAMP_TEST_PYTHON, VAMP_TEST_SOURCE_DIR "/generator/catalogue.py", source.c_str(), destination.c_str(), nullptr);
::execl(VAMP_TEST_PYTHON, VAMP_TEST_PYTHON, VAMP_TEST_SOURCE_DIR "/generator/catalog.py", source.c_str(), destination.c_str(), nullptr);
::_exit(127);
}
auto status = std::int32_t{};
+12 -11
View File
@@ -41,7 +41,7 @@
#include <vamp_zero/score.hpp>
#include <vamp_zero/types.hpp>
#include "catalogue_fixture.hpp"
#include "catalog_fixture.hpp"
namespace {
@@ -137,7 +137,7 @@ auto navigation_state() -> vamp_zero::app_state {
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{
.catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::catalog{
.performance = vamp_zero::group_library_parts({part}),
.practice = std::move(*practice),
}),
@@ -157,6 +157,7 @@ BOOST_AUTO_TEST_CASE(runtime_config_uses_headless_and_ignores_unknown_fields) {
BOOST_REQUIRE(parsed);
BOOST_TEST(parsed->runtime.headless);
BOOST_TEST(!parsed->audio.enabled);
BOOST_TEST(parsed->database.path == "/usr/share/product-vamp/catalog.db");
auto invalid = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":"yes"}})json"));
BOOST_TEST(!invalid);
}
@@ -274,7 +275,7 @@ BOOST_AUTO_TEST_CASE(main_menu_metronome_setting_survives_score_selection) {
BOOST_TEST(!state.audio.tempo_editing);
}
BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) {
BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalog) {
auto count = std::size_t{};
for (auto const &entry : std::filesystem::recursive_directory_iterator{VAMP_TEST_ASSETS_DIR}) {
if (entry.path().extension() != ".json") {
@@ -294,7 +295,7 @@ BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalogue) {
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 catalog = std::make_shared<vamp_zero::catalog>(*state.catalog);
auto &practice_part = catalog->practice.exercises.front().parts[2];
practice_part.source_path = "exercise-vocal.json";
practice_part.total_pages = 3U;
@@ -350,9 +351,9 @@ BOOST_AUTO_TEST_CASE(practice_part_navigation_and_remote_display) {
BOOST_REQUIRE(vamp_zero::validate_app_state(state));
}
BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalogue_returns_to_main) {
BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalog_returns_to_main) {
auto state = navigation_state();
auto catalog = std::make_shared<vamp_zero::catalogue>(*state.catalog);
auto catalog = std::make_shared<vamp_zero::catalog>(*state.catalog);
auto first = catalog->practice.exercises.front();
for (auto index = std::uint32_t{2}; index <= 366U; ++index) {
auto exercise = first;
@@ -369,7 +370,7 @@ BOOST_AUTO_TEST_CASE(flat_practice_wraps_and_empty_catalogue_returns_to_main) {
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.catalog = vamp_zero::empty_catalog();
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);
@@ -389,7 +390,7 @@ BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_practice_parts) {
}
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();
config.path = (std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "catalog.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();
@@ -434,7 +435,7 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
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));
BOOST_REQUIRE(vamp_test::build_catalog(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();
@@ -448,9 +449,9 @@ BOOST_AUTO_TEST_CASE(numbered_practice_paths_keep_duplicate_titles_separate_and_
for (auto const &exercise : library->exercises) {
BOOST_TEST(exercise.title == "Shared Title");
}
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path));
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path));
std::filesystem::rename(assets / "practice/002", assets / "practice/099");
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.path));
BOOST_REQUIRE(vamp_test::build_catalog(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);
@@ -11,7 +11,7 @@ from generator.parts import PART_TYPES
from unittest.mock import patch
from tests.test_musicxml import xml_note, xml_score
class CatalogueTests(unittest.TestCase):
class CatalogTests(unittest.TestCase):
def test_every_numbered_source_is_wired_to_three_part_paths(self):
source = Path('data/practice')
sources = sorted(source.glob('*.xml'))
@@ -5,7 +5,7 @@ import tempfile
import unittest
from pathlib import Path
from generator.catalogue import APPLICATION_ID, SCHEMA_VERSION, build_catalogue
from generator.catalog import APPLICATION_ID, SCHEMA_VERSION, build_catalog
def bundle(title='Fixture', key='C', part='Lead'):
return {
@@ -21,14 +21,14 @@ def write_score(root, relative, document=None):
path.write_text(json.dumps(document or bundle()), encoding='utf-8')
return path
class CatalogueTests(unittest.TestCase):
class CatalogTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = Path(self.temporary.name)
(self.root / 'songs').mkdir()
def rows(self, table):
with closing(sqlite3.connect(self.root / 'catalogue.db')) as connection:
with closing(sqlite3.connect(self.root / 'catalog.db')) as connection:
connection.row_factory = sqlite3.Row
return [dict(row) for row in connection.execute(f'SELECT * FROM {table} ORDER BY id')]
def test_metadata_relative_paths_and_complete_documents(self):
@@ -36,7 +36,8 @@ class CatalogueTests(unittest.TestCase):
write_score(self.root, 'songs/tune.json', document)
for part in ('Acoustic', 'Electric', 'Vocal'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle('Exercise', 'Eb', part))
database = build_catalogue(self.root)
database = build_catalog(self.root)
self.assertEqual(database, self.root / 'catalog.db')
song = self.rows('songs')[0]
self.assertEqual(json.loads(song['source_json']), document)
self.assertEqual((song['id'], song['part'], song['source_path']), ('tune', 'Lead', 'songs/tune.json'))
@@ -57,62 +58,65 @@ class CatalogueTests(unittest.TestCase):
self.assertFalse(Path(str(database) + '-journal').exists())
def test_rebuild_is_deterministic_relocatable_and_replaces_deleted_rows(self):
score = write_score(self.root, 'songs/tune.json')
database = build_catalogue(self.root)
database = build_catalog(self.root)
previous = database.read_bytes()
build_catalogue(self.root)
build_catalog(self.root)
self.assertEqual(database.read_bytes(), previous)
with tempfile.TemporaryDirectory() as other:
root = Path(other)
write_score(root, 'songs/tune.json')
self.assertEqual(build_catalogue(root).read_bytes(), previous)
self.assertEqual(build_catalog(root).read_bytes(), previous)
score.rename(score.with_name('renamed.json'))
build_catalogue(self.root)
build_catalog(self.root)
self.assertEqual([row['id'] for row in self.rows('songs')], ['renamed'])
score.with_name('renamed.json').unlink()
build_catalogue(self.root)
build_catalog(self.root)
self.assertEqual(self.rows('songs'), [])
def test_invalid_input_preserves_existing_database(self):
score = write_score(self.root, 'songs/tune.json')
database = build_catalogue(self.root)
database = build_catalog(self.root)
previous = database.read_bytes()
for document in ({}, bundle(title=''), bundle(key=None)):
score.write_text(json.dumps(document))
with self.assertRaises(ValueError):
build_catalogue(self.root)
build_catalog(self.root)
self.assertEqual(database.read_bytes(), previous)
self.assertFalse(list(self.root.glob('.vamp-catalogue-*')))
self.assertFalse(list(self.root.glob('.vamp-catalog-*')))
def test_missing_parts_and_metadata_disagreement_fail(self):
for part in ('Acoustic', 'Electric'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle(part=part))
with self.assertRaisesRegex(ValueError, 'all three parts'):
build_catalogue(self.root)
build_catalog(self.root)
write_score(self.root, 'practice/001/vocal.json', bundle(key='F', part='Vocal'))
with self.assertRaisesRegex(ValueError, 'disagree'):
build_catalogue(self.root)
build_catalog(self.root)
def test_keyless_parts_preserve_their_original_key(self):
for part in ('Acoustic', 'Electric', 'Vocal'):
write_score(self.root, f'practice/001/{part.lower()}.json', bundle(key='No key', part=part))
build_catalogue(self.root)
build_catalog(self.root)
self.assertTrue(all(row['key_signature'] == 'No key' for row in self.rows('practice_exercises')))
def test_invalid_layout_and_escaping_source_fail(self):
with self.assertRaisesRegex(ValueError, 'directory not found'):
build_catalogue(self.root / 'missing')
build_catalog(self.root / 'missing')
self.assertFalse((self.root / 'missing').exists())
score = write_score(self.root, 'songs/nested/tune.json')
with self.assertRaisesRegex(ValueError, 'unexpected songs'):
build_catalogue(self.root)
build_catalog(self.root)
score.unlink()
(self.root / 'songs/escape.json').symlink_to('/etc/passwd')
with self.assertRaises(ValueError):
build_catalogue(self.root)
build_catalog(self.root)
def test_runtime_asset_packaging_excludes_score_json(self):
import subprocess
write_score(self.root, 'songs/tune.json')
build_catalogue(self.root)
build_catalog(self.root)
(self.root / 'songs/tune.png').write_bytes(b'page')
with tempfile.TemporaryDirectory() as temporary:
destination = Path(temporary)
subprocess.run(['cmake', f'-DSOURCE={self.root}', f'-DDESTINATION={destination}', '-P', 'cmake/runtime_assets.cmake'], check=True, capture_output=True)
self.assertTrue((destination / 'catalogue.db').exists())
self.assertTrue((destination / 'catalog.db').exists())
for name in ('vamp.example.json', 'vamp.development.json'):
config = json.loads((Path('package/config') / name).read_text())
self.assertEqual(Path(config['database']['path']).name, 'catalog.db')
self.assertTrue((destination / 'songs/tune.png').exists())
self.assertFalse(list(destination.rglob('*.json')))
@@ -29,7 +29,7 @@
#include <vamp_zero/navigation.hpp>
#include <vamp_zero/portal.hpp>
#include "catalogue_fixture.hpp"
#include "catalog_fixture.hpp"
namespace {
@@ -40,30 +40,30 @@ static_assert(!retains_score_json<vamp_zero::library_part>);
static_assert(!retains_score_json<vamp_zero::practice_exercise>);
static_assert(!retains_score_json<vamp_zero::library_part>);
struct catalogue_directory {
struct catalog_directory {
std::filesystem::path path;
catalogue_directory() {
auto pattern = (std::filesystem::temp_directory_path() / "vamp-catalogue-XXXXXX").string();
catalog_directory() {
auto pattern = (std::filesystem::temp_directory_path() / "vamp-catalog-XXXXXX").string();
BOOST_REQUIRE(::mkdtemp(pattern.data()) != nullptr);
path = pattern;
}
~catalogue_directory() {
~catalog_directory() {
auto ignored = std::error_code{};
std::filesystem::remove_all(path, ignored);
}
};
template <typename Function, typename... Arguments>
auto run_catalogue_operation(Function function, Arguments... arguments) {
auto run_catalog_operation(Function function, Arguments... arguments) {
auto io = boost::asio::io_context{};
auto future = boost::asio::co_spawn(io, function(io.get_executor(), std::move(arguments)...), boost::asio::use_future);
io.run();
return future.get();
}
auto catalogue_query(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string sql, std::vector<noto::sqlite_value> params)
auto catalog_query(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string sql, std::vector<noto::sqlite_value> params)
-> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp-catalogue-test")};
auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp-catalog-test")};
auto connected = co_await connection.async_connect();
if (!connected) {
co_return std::unexpected(connected.error());
@@ -79,7 +79,7 @@ auto catalogue_query(boost::asio::any_io_executor executor, vamp_zero::database_
co_return result;
}
auto catalogue_bundle(std::string_view title, std::string_view key = "C", std::string_view part = "Lead") -> std::string {
auto catalog_bundle(std::string_view title, std::string_view key = "C", std::string_view 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"},
@@ -95,15 +95,15 @@ auto catalogue_bundle(std::string_view title, std::string_view key = "C", std::s
return boost::json::serialize(value);
}
struct catalogue_database {
catalogue_directory directory;
struct catalog_database {
catalog_directory directory;
std::filesystem::path assets{directory.path / "assets"};
vamp_zero::database_config config{.path = (directory.path / "catalogue.db").string()};
catalogue_database() {
vamp_zero::database_config config{.path = (directory.path / "catalog.db").string()};
catalog_database() {
std::filesystem::create_directories(assets / "songs");
}
auto query(std::string sql, std::vector<noto::sqlite_value> params = {}) const -> noto::query_result {
auto result = run_catalogue_operation(catalogue_query, config, std::move(sql), std::move(params));
auto result = run_catalog_operation(catalog_query, config, std::move(sql), std::move(params));
BOOST_REQUIRE_MESSAGE(result, (result ? "" : result.error().describe()));
return std::move(*result);
}
@@ -126,20 +126,20 @@ struct catalogue_database {
BOOST_REQUIRE(output.good());
}
auto build() const -> std::bool_t {
return vamp_test::build_catalogue(assets, config.path);
return vamp_test::build_catalog(assets, config.path);
}
auto performance() const -> std::expected<vamp_zero::library_state, modulus::error> {
return run_catalogue_operation(vamp_zero::async_load_performance_library, config, assets.string());
return run_catalog_operation(vamp_zero::async_load_performance_library, config, assets.string());
}
auto practice() const -> std::expected<vamp_zero::practice_library, modulus::error> {
return run_catalogue_operation(vamp_zero::async_load_practice_library, config, assets.string());
return run_catalog_operation(vamp_zero::async_load_practice_library, config, assets.string());
}
auto score(vamp_zero::score_category category, std::string id) const -> std::expected<vamp_zero::score_document, modulus::error> {
return run_catalogue_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string());
return run_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string());
}
};
auto catalogue_exercise(std::string authored = "Eb", std::string id = "001") -> vamp_zero::practice_exercise {
auto catalog_exercise(std::string authored = "Eb", std::string id = "001") -> vamp_zero::practice_exercise {
auto result = vamp_zero::practice_exercise{
.id = std::move(id),
.title = "Exercise",
@@ -153,14 +153,14 @@ auto catalogue_exercise(std::string authored = "Eb", std::string id = "001") ->
}
BOOST_AUTO_TEST_CASE(catalogue_snapshots_heartbeat_and_navigation_share_immutable_metadata) {
BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_metadata) {
auto performance = vamp_zero::group_library_parts({
{.id = "other", .title = "A Tune", .composer = "Composer", .part = "Lead", .total_pages = 1},
{.id = "chosen", .title = "B Tune", .composer = "Composer", .part = "Lead", .total_pages = 2},
});
auto practice = vamp_zero::group_practice_exercises({catalogue_exercise("Eb", "002"), catalogue_exercise("Eb", "001")});
auto practice = vamp_zero::group_practice_exercises({catalog_exercise("Eb", "002"), catalog_exercise("Eb", "001")});
BOOST_REQUIRE(practice);
auto catalog = std::make_shared<vamp_zero::catalogue const>(vamp_zero::catalogue{std::move(performance), std::move(*practice)});
auto catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::catalog{std::move(performance), std::move(*practice)});
auto original = vamp_zero::app_state{
.catalog = catalog,
.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU,
@@ -211,7 +211,7 @@ BOOST_AUTO_TEST_CASE(catalogue_snapshots_heartbeat_and_navigation_share_immutabl
BOOST_TEST(parts.selected_practice_part_index == 0U);
}
BOOST_AUTO_TEST_CASE(catalogue_performance_groups_use_title_and_composer_with_part_indices) {
BOOST_AUTO_TEST_CASE(catalog_performance_groups_use_title_and_composer_with_part_indices) {
auto library = vamp_zero::group_library_parts({
{.id = "other-composer", .title = "Shared Title", .composer = "Bea", .part = "Lead"},
{.id = "vocal", .title = "Shared Title", .composer = "Ada", .part = "Vocal"},
@@ -242,15 +242,15 @@ BOOST_AUTO_TEST_CASE(catalogue_performance_groups_use_title_and_composer_with_pa
BOOST_TEST(library.parts[library.groups[1].parts[1]].id == "vocal");
}
BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_agreement) {
BOOST_AUTO_TEST_CASE(catalog_practice_requires_complete_ordered_parts_and_key_agreement) {
for (auto authored : {"Eb", "C dorian", "No key", ""}) {
auto first = catalogue_exercise(authored);
auto first = catalog_exercise(authored);
std::reverse(first.parts.begin(), first.parts.end());
auto last = first.parts.back();
first.parts.pop_back();
auto fragment = catalogue_exercise(authored);
auto fragment = catalog_exercise(authored);
fragment.parts = {last};
auto grouped = vamp_zero::group_practice_exercises({catalogue_exercise("F", "002"), first, fragment});
auto grouped = vamp_zero::group_practice_exercises({catalog_exercise("F", "002"), first, fragment});
BOOST_REQUIRE(grouped);
BOOST_REQUIRE_EQUAL(grouped->exercises.size(), 2U);
auto const &exercise = grouped->exercises[0];
@@ -262,8 +262,8 @@ BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_
}
}
for (auto conflict : {"F", ""}) {
auto first = catalogue_exercise();
auto second = catalogue_exercise(conflict);
auto first = catalog_exercise();
auto second = catalog_exercise(conflict);
second.parts = {second.parts.back()};
first.parts.pop_back();
auto grouped = vamp_zero::group_practice_exercises({first, second});
@@ -271,21 +271,21 @@ BOOST_AUTO_TEST_CASE(catalogue_practice_requires_complete_ordered_parts_and_key_
BOOST_TEST(grouped.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
BOOST_TEST(grouped.error().message().find("disagree on key") != std::string_view::npos);
}
auto missing = catalogue_exercise();
auto missing = catalog_exercise();
missing.parts.pop_back();
BOOST_TEST(!vamp_zero::group_practice_exercises({missing}));
auto duplicate = catalogue_exercise();
auto duplicate = catalog_exercise();
duplicate.parts.back().part = "Acoustic";
BOOST_TEST(!vamp_zero::group_practice_exercises({duplicate}));
}
BOOST_AUTO_TEST_CASE(catalogue_individual_score_lookup_uses_category_and_reports_missing_or_invalid) {
auto db = catalogue_database{};
auto song = catalogue_bundle("Song", "F");
auto exercise = catalogue_bundle("Exercise", "D", "Electric");
BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_missing_or_invalid) {
auto db = catalog_database{};
auto song = catalog_bundle("Song", "F");
auto exercise = catalog_bundle("Exercise", "D", "Electric");
db.write("songs/shared.json", song);
for (auto part : vamp_zero::practice_part_names()) {
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part), catalogue_bundle("Exercise", "D", part));
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part));
}
BOOST_REQUIRE(db.build());
db.query("UPDATE practice_exercises SET id = 'shared' WHERE part = 'Electric'");
@@ -317,9 +317,9 @@ BOOST_AUTO_TEST_CASE(catalogue_individual_score_lookup_uses_category_and_reports
}
}
BOOST_AUTO_TEST_CASE(catalogue_sequential_score_loads_keep_executor_stack_bounded) {
auto db = catalogue_database{};
db.write("songs/repeated.json", catalogue_bundle("Repeated"));
BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded) {
auto db = catalog_database{};
db.write("songs/repeated.json", catalog_bundle("Repeated"));
BOOST_REQUIRE(db.build());
auto repeated_loads = [](boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string assets)
-> boost::asio::awaitable<std::expected<std::uint32_t, modulus::error>> {
@@ -332,13 +332,13 @@ BOOST_AUTO_TEST_CASE(catalogue_sequential_score_loads_keep_executor_stack_bounde
}
co_return 4096U;
};
auto result = run_catalogue_operation(repeated_loads, db.config, db.assets.string());
auto result = run_catalog_operation(repeated_loads, db.config, db.assets.string());
BOOST_REQUIRE_MESSAGE(result, (result ? "" : result.error().describe()));
BOOST_TEST(*result == 4096U);
}
BOOST_AUTO_TEST_CASE(catalogue_reload_uses_cached_metadata_without_parsing_score_json) {
auto db = catalogue_database{};
BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_json) {
auto db = catalog_database{};
BOOST_REQUIRE(db.build());
// Complete cached metadata must not require parsing or retaining the score body.
db.query("INSERT INTO songs(id, title, composer, source_json, part, source_path, "
@@ -388,9 +388,9 @@ BOOST_AUTO_TEST_CASE(catalogue_reload_uses_cached_metadata_without_parsing_score
BOOST_TEST(disagreement.error().message().find("disagree on key") != std::string_view::npos);
}
BOOST_AUTO_TEST_CASE(catalogue_validation_rejects_missing_legacy_and_corrupt_files_without_repair) {
auto db = catalogue_database{};
auto validate = [&] { return run_catalogue_operation(vamp_zero::async_validate_database, db.config, true); };
BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_legacy_and_corrupt_files_without_repair) {
auto db = catalog_database{};
auto validate = [&] { return run_catalog_operation(vamp_zero::async_validate_database, db.config, true); };
BOOST_TEST(!validate());
BOOST_TEST(!std::filesystem::exists(db.config.path));
BOOST_REQUIRE(db.build());
@@ -419,11 +419,11 @@ BOOST_AUTO_TEST_CASE(catalogue_validation_rejects_missing_legacy_and_corrupt_fil
BOOST_TEST(!std::filesystem::exists(db.config.path + "-journal"));
}
BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves_relocated_assets) {
auto db = catalogue_database{};
db.write("songs/tune.json", catalogue_bundle("Relocated"));
BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_relocated_assets) {
auto db = catalog_database{};
db.write("songs/tune.json", catalog_bundle("Relocated"));
for (auto part_name : vamp_zero::practice_part_names()) {
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part_name), catalogue_bundle("Exercise", "Eb", part_name));
db.write(std::filesystem::path{"practice/001"} / vamp_test::part_filename(part_name), catalog_bundle("Exercise", "Eb", part_name));
}
BOOST_REQUIRE(db.build());
std::filesystem::remove(db.assets / "songs/tune.json");
@@ -433,7 +433,7 @@ BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves
db.assets = moved;
auto timestamp = std::filesystem::last_write_time(db.config.path);
std::filesystem::permissions(db.config.path, std::filesystem::perms::owner_read);
BOOST_REQUIRE(run_catalogue_operation(vamp_zero::async_validate_database, db.config, true));
BOOST_REQUIRE(run_catalog_operation(vamp_zero::async_validate_database, db.config, true));
auto songs = db.performance();
auto practice = db.practice();
auto score = db.score(vamp_zero::score_category::PERFORMANCE, "tune");
@@ -449,9 +449,9 @@ BOOST_AUTO_TEST_CASE(catalogue_reads_readonly_database_without_json_and_resolves
BOOST_TEST(!std::filesystem::exists(db.config.path + "-journal"));
}
BOOST_AUTO_TEST_CASE(catalogue_rejects_paths_outside_the_asset_root) {
auto db = catalogue_database{};
db.write("songs/tune.json", catalogue_bundle("Fixture"));
BOOST_AUTO_TEST_CASE(catalog_rejects_paths_outside_the_asset_root) {
auto db = catalog_database{};
db.write("songs/tune.json", catalog_bundle("Fixture"));
BOOST_REQUIRE(db.build());
for (auto path : {"/tmp/tune.json", "../tune.json", "songs/../../tune.json", "songs/./tune.json", "songs\\tune.json", ""}) {
if (std::string_view{path}.empty()) {
+7 -7
View File
@@ -29,7 +29,7 @@
#include <vamp_zero/health.hpp>
#include <vamp_zero/portal.hpp>
#include "catalogue_fixture.hpp"
#include "catalog_fixture.hpp"
namespace {
@@ -189,7 +189,7 @@ auto make_assets(vamp_zero::config const &config) -> void {
name.front() = static_cast<std::char_t>(name.front() - 'A' + 'a');
write_score(root / "practice" / "001" / (name + ".json"), std::string{part_name});
}
BOOST_REQUIRE(vamp_test::build_catalogue(root, config.database.path));
BOOST_REQUIRE(vamp_test::build_catalog(root, config.database.path));
}
auto run_once(std::filesystem::path const &root, vamp_zero::config const &config) -> std::expected<void, modulus::error> {
@@ -336,7 +336,7 @@ BOOST_AUTO_TEST_CASE(portal_remote_commands_refresh_connection_without_backgroun
}
}
BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalogue_without_hardware_or_heartbeat) {
BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartbeat) {
auto root = temporary_directory{};
auto config = fixture_config(root.path);
make_assets(config);
@@ -399,7 +399,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_
auto score = boost::json::parse(read_file(path));
score.as_object()["playback"] = boost::json::parse(read_file(fixture / "score.json")).at("playback");
write_file(path, boost::json::serialize(score));
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.database.path));
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path));
auto ready = run_once(root.path, config);
BOOST_REQUIRE_MESSAGE(ready, (ready ? "" : ready.error().describe()));
auto &sample = score.at("playback").at("samples").as_array().front().as_object();
@@ -409,7 +409,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_
sample["sha256"] = checksum(corrupt);
write_file(audio, corrupt);
write_file(path, boost::json::serialize(score));
BOOST_REQUIRE(vamp_test::build_catalogue(assets, config.database.path));
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.database.path));
ready = run_once(root.path, config);
BOOST_REQUIRE(!ready);
BOOST_TEST(std::string{ready.error().message()}.find("PCM16 WAV") != std::string::npos);
@@ -437,7 +437,7 @@ BOOST_AUTO_TEST_CASE(readiness_rejects_corrupt_checksums_and_escaping_pages) {
}
}
BOOST_AUTO_TEST_CASE(readiness_rejects_missing_or_malformed_catalogues) {
BOOST_AUTO_TEST_CASE(readiness_rejects_missing_or_malformed_catalogs) {
auto root = temporary_directory{};
auto config = fixture_config(root.path);
BOOST_TEST(!run_once(root.path, config));
@@ -479,7 +479,7 @@ BOOST_AUTO_TEST_CASE(readiness_decodes_pages_even_when_checksums_match) {
write_file(score_path.parent_path() / std::string{page.at("path").as_string()}, bytes);
page["sha256"] = checksum(bytes);
write_file(score_path, boost::json::serialize(score));
BOOST_REQUIRE(vamp_test::build_catalogue(config.runtime.assets_path, config.database.path));
BOOST_REQUIRE(vamp_test::build_catalog(config.runtime.assets_path, config.database.path));
auto ready = run_once(root.path, config);
BOOST_REQUIRE_MESSAGE(!ready, kind);
BOOST_TEST(ready.error().describe().find("first.pages/1.png") != std::string::npos);
+5 -5
View File
@@ -22,9 +22,9 @@ class MusicModelTests(unittest.TestCase):
engraving = patch('generator.__main__.engrave_tree')
engraving.start()
self.addCleanup(engraving.stop)
catalogue = patch('generator.__main__.build_catalogue')
catalogue.start()
self.addCleanup(catalogue.stop)
catalog = patch('generator.__main__.build_catalog')
catalog.start()
self.addCleanup(catalog.stop)
def test_chord_semantics_are_not_voicing_heuristics(self):
cases = {'C': {0, 4, 7}, 'Cdim': {0, 3, 6}, 'Cdim7': {0, 3, 6, 9}, 'Csus2': {0, 2, 7},
'C7b5': {0, 4, 6, 10}, 'G7b9': {7, 11, 2, 5, 8}, 'Fmaj7#11': {5, 9, 0, 4, 11}}
@@ -35,7 +35,7 @@ class MusicModelTests(unittest.TestCase):
pitches = chord_pitches(definition, chord_positions(*chord_requirements(definition))[0])
self.assertTrue(essentials.issubset({pitch_number(p) % 12 for p in pitches}), label)
with self.assertRaises(ValueError):
harmony.parse_symbol('Cunrecognised')
harmony.parse_symbol('Cunrecognized')
def test_transposition_preserves_spelling_octaves_modes(self):
for p in ({'letter': 'b', 'alter': 1, 'octave': 4}, {'letter': 'c', 'alter': -1, 'octave': 4}, {'letter': 'f', 'alter': 2, 'octave': 4}):
p['alter'] = rational(p['alter'])
@@ -101,7 +101,7 @@ class MusicModelTests(unittest.TestCase):
for text in ('Med.', 'Med. Swing', 'Medium', 'Medium Swing'):
self.assertEqual(canonical_style(text), 'Medium Swing')
with self.assertRaises(ValueError):
canonical_style('Unrecognised style')
canonical_style('Unrecognized style')
def test_failure_preserves_generated_assets(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
+1 -1
View File
@@ -43,7 +43,7 @@ class MusicXMLTests(unittest.TestCase):
score = load_musicxml(Path('tests/fixtures/notation.xml'))
self.assertEqual(len(score['staves'][0]['measures']), 4)
self.assertEqual(len(score['staves'][0]['octave_shifts']), 1)
def test_catalogue_imports(self):
def test_catalog_imports(self):
sources = sorted(Path('data').rglob('*.xml'))
self.assertTrue(sources)
for path in sources:
+1 -1
View File
@@ -100,7 +100,7 @@ class PartGenerationTests(unittest.TestCase):
(root / 'data' / category).mkdir(parents=True)
(root / 'data' / category / (name + '.xml')).write_text(xml_score(xml_harmony() + xml_note()))
args = argparse.Namespace(data_dir=root / 'data', output_dir=root / 'output', static_assets_dir=Path('package/assets'), check=False)
with patch('generator.arrange.generate.load_musicxml', wraps=load_musicxml) as song_load, patch('generator.practice.generate.load_musicxml', wraps=load_musicxml) as practice_load, patch('generator.parts.concert_document', wraps=concert_document) as normalize, patch('generator.__main__.engrave_tree'), patch('generator.__main__.build_catalogue'), patch('generator.__main__.compile_tree'):
with patch('generator.arrange.generate.load_musicxml', wraps=load_musicxml) as song_load, patch('generator.practice.generate.load_musicxml', wraps=load_musicxml) as practice_load, patch('generator.parts.concert_document', wraps=concert_document) as normalize, patch('generator.__main__.engrave_tree'), patch('generator.__main__.build_catalog'), patch('generator.__main__.compile_tree'):
generate_assets(args)
song_load.assert_called_once()
practice_load.assert_called_once()
+3 -3
View File
@@ -22,7 +22,7 @@ auto playback_score() -> vamp_zero::score_document {
}
auto performance_state() -> vamp_zero::app_state {
auto catalog = std::make_shared<vamp_zero::catalogue>();
auto catalog = std::make_shared<vamp_zero::catalog>();
catalog->performance.parts.push_back({.id = "part", .title = "Song", .part = "Vocal", .total_pages = 2U});
catalog->performance.groups.push_back({.title = "Song", .parts = {0U}});
auto state = vamp_zero::app_state{};
@@ -71,7 +71,7 @@ BOOST_AUTO_TEST_CASE(playback_completion_is_session_guarded) {
auto state = vamp_zero::control_state{{}, performance_state()};
state.finish_playback(41U);
BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE);
state.finish_playback(42U, {}, std::make_shared<vamp_zero::catalogue const>());
state.finish_playback(42U, {}, std::make_shared<vamp_zero::catalog const>());
BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE);
state.finish_playback(42U, "sample missing");
BOOST_CHECK(state.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE);
@@ -91,7 +91,7 @@ BOOST_AUTO_TEST_CASE(playback_completion_is_session_guarded) {
BOOST_CHECK(quiet.snapshot().mode == vamp_zero::app_mode::PERFORMANCE_SCORE);
}
BOOST_AUTO_TEST_CASE(playback_worker_discards_cancelled_preparation_and_reports_errors_once) {
BOOST_AUTO_TEST_CASE(playback_worker_discards_canceled_preparation_and_reports_errors_once) {
auto engine = sonus::engine::create({.no_device = true});
BOOST_REQUIRE(engine);
auto audio = vamp_zero::audio_controller{{}, std::move(*engine)};
+1 -1
View File
@@ -158,7 +158,7 @@ class ScoreBundleTests(unittest.TestCase):
source['staves'][0]['instruments'] = []
with self.assertRaisesRegex(ValueError, 'unknown score instrument'):
validate_document(source)
def test_catalogue_roundtrip_preserves_written_pitch_and_rhythm(self):
def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self):
for path in Path('data').rglob('*.xml'):
with self.subTest(path=path):
source = load_musicxml(path)
+6 -6
View File
@@ -64,7 +64,7 @@ struct controlled_scores {
struct loader_fixture {
boost::asio::io_context io{};
std::shared_ptr<controlled_scores> scores{std::make_shared<controlled_scores>(io)};
std::shared_ptr<vamp_zero::catalogue const> catalog{std::make_shared<vamp_zero::catalogue const>()};
std::shared_ptr<vamp_zero::catalog const> catalog{std::make_shared<vamp_zero::catalog const>()};
std::shared_ptr<std::size_t> completions{std::make_shared<std::size_t>()};
vamp_zero::score_loader loader{io.get_executor(),
[scores = scores](vamp_zero::score_reference reference) { return scores->load(std::move(reference)); },
@@ -147,11 +147,11 @@ BOOST_AUTO_TEST_CASE(score_loader_stale_error_does_not_replace_pending_selection
BOOST_CHECK_EQUAL(fixture.loader.snapshot().document->id, "available");
}
BOOST_AUTO_TEST_CASE(score_loader_catalogue_identity_and_category_invalidate_selection) {
BOOST_AUTO_TEST_CASE(score_loader_catalog_identity_and_category_invalidate_selection) {
auto fixture = loader_fixture{};
fixture.loader.select(fixture.catalog, performance("same-id"));
drain(fixture.io);
fixture.catalog = std::make_shared<vamp_zero::catalogue const>();
fixture.catalog = std::make_shared<vamp_zero::catalog const>();
fixture.loader.select(fixture.catalog, performance("same-id"));
fixture.scores->complete(0);
drain(fixture.io);
@@ -279,7 +279,7 @@ BOOST_AUTO_TEST_CASE(score_loader_destruction_during_load_does_not_call_consumer
auto loader = vamp_zero::score_loader{io.get_executor(),
[scores](vamp_zero::score_reference reference) { return scores->load(std::move(reference)); },
[&] { ++completions; }};
loader.select(std::make_shared<vamp_zero::catalogue const>(), performance("a"));
loader.select(std::make_shared<vamp_zero::catalog const>(), performance("a"));
drain(io);
BOOST_REQUIRE_EQUAL(scores->requests.size(), 1U);
}
@@ -301,7 +301,7 @@ BOOST_AUTO_TEST_CASE(score_loader_completion_can_overlap_stop_and_consumer_destr
if (auto target = weak.lock()) {
++*target;
} });
loader->select(std::make_shared<vamp_zero::catalogue const>(), performance("a"));
loader->select(std::make_shared<vamp_zero::catalog const>(), performance("a"));
auto worker = std::thread{[&] { io.run(); }};
auto entered_status = entered_future.wait_for(std::chrono::seconds{5});
if (entered_status == std::future_status::ready) {
@@ -317,7 +317,7 @@ BOOST_AUTO_TEST_CASE(score_loader_completion_can_overlap_stop_and_consumer_destr
}
BOOST_AUTO_TEST_CASE(selected_score_reference_uses_active_part_and_practice_part) {
auto catalog = std::make_shared<vamp_zero::catalogue>();
auto catalog = std::make_shared<vamp_zero::catalog>();
catalog->performance.parts = {{.id = "part-a"}, {.id = "part-b"}};
catalog->practice.exercises = {{.id = "scale", .parts = {{.id = "scale-c"}, {.id = "scale-d"}}}};
auto state = vamp_zero::app_state{.catalog = catalog};