@@ -0,0 +1,19 @@
|
||||
import unittest.mock
|
||||
|
||||
import generator.arrange.form
|
||||
import generator.arrange.generate
|
||||
import generator.parts
|
||||
|
||||
def section_document(document, part_type, *, rhythms, plan=None):
|
||||
"""Exercise the real part pipeline with a single section instead of a full song."""
|
||||
prepared = generator.parts.prepare_document(document)
|
||||
role = {'acoustic': 'accompaniment', 'electric': 'melody', 'vocal': 'vocal'}[part_type]
|
||||
section = generator.arrange.form.ArrangementSection('', 0, len(prepared['staves'][0]['measures']), 0, role)
|
||||
with unittest.mock.patch('generator.arrange.form.full_arrangement_sections', return_value=[section]):
|
||||
if plan is not None:
|
||||
plans = {0: plan}
|
||||
elif part_type == 'acoustic':
|
||||
plans = generator.arrange.generate.plan_arrangement(prepared, rhythms)
|
||||
else:
|
||||
plans = {}
|
||||
return generator.arrange.generate._arrange_document(prepared, part_type, plans)
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <cerrno>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
@@ -15,12 +14,6 @@
|
||||
|
||||
namespace vamp_test {
|
||||
|
||||
inline auto part_filename(std::string_view part) -> std::string {
|
||||
auto result = std::string{part};
|
||||
result.front() = static_cast<std::char_t>(result.front() - 'A' + 'a');
|
||||
return result + ".json";
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
+74
-227
@@ -16,7 +16,6 @@
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
@@ -27,8 +26,6 @@
|
||||
#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>
|
||||
@@ -42,7 +39,6 @@
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
#include "catalog_fixture.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -52,47 +48,6 @@ auto test_database_config(std::filesystem::path const &path) -> vamp_zero::datab
|
||||
};
|
||||
}
|
||||
|
||||
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({
|
||||
"music":{"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"},
|
||||
@@ -118,38 +73,11 @@ auto navigation_state() -> vamp_zero::app_state {
|
||||
.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::catalog const>(vamp_zero::catalog{
|
||||
.performance = vamp_zero::group_library_parts({part}),
|
||||
.practice = std::move(*practice),
|
||||
}),
|
||||
.catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::group_ordered_parts({part})),
|
||||
};
|
||||
}
|
||||
|
||||
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(build_identification_uses_git_without_an_application_release_label) {
|
||||
@@ -230,18 +158,17 @@ BOOST_AUTO_TEST_CASE(product_vamp_audio_config_rejects_invalid_tempo_range) {
|
||||
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);
|
||||
BOOST_TEST(state.selected_main_menu_index == 1U);
|
||||
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);
|
||||
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.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);
|
||||
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.tempo_editing);
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
@@ -310,177 +237,97 @@ BOOST_AUTO_TEST_CASE(score_parser_loads_the_generated_catalog) {
|
||||
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::catalog>(*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);
|
||||
BOOST_AUTO_TEST_CASE(empty_catalog_navigation_and_main_menu_wrap) {
|
||||
auto state = vamp_zero::app_state{};
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::PREVIOUS);
|
||||
BOOST_TEST(state.selected_main_menu_index == 3U);
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
BOOST_TEST(state.selected_main_menu_index == 0U);
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||
BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_SONG_MENU);
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
BOOST_TEST(state.selected_song_index == 0U);
|
||||
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_TEST(!vamp_zero::app_status_json(state).as_object().contains("practice"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(song_part_navigation_preserves_selection_and_remote_display) {
|
||||
auto parts = std::vector<vamp_zero::library_part>{};
|
||||
for (auto name : {"Acoustic", "Electric", "Vocal"}) {
|
||||
parts.push_back({.id = name, .title = "Song", .composer = "Composer", .part = name, .total_pages = 3});
|
||||
}
|
||||
auto state = vamp_zero::app_state{
|
||||
.catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::group_ordered_parts(std::move(parts))),
|
||||
};
|
||||
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);
|
||||
for (auto name : {"Acoustic", "Electric", "Vocal"}) {
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
auto selection = state.selected_part_index;
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||
BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_SCORE);
|
||||
BOOST_TEST(state.active_part_id == name);
|
||||
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} == name);
|
||||
BOOST_TEST(remote.pages == 3U);
|
||||
for (auto index = std::uint32_t{}; index < 4U; ++index) {
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::NEXT);
|
||||
}
|
||||
BOOST_TEST(state.page_index == 2U);
|
||||
state = vamp_zero::apply_app_command(std::move(state), vamp_protocol::page_command::SELECT);
|
||||
BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_PART_MENU);
|
||||
BOOST_TEST(state.selected_part_index == selection);
|
||||
BOOST_TEST(state.active_part_id.empty());
|
||||
BOOST_TEST(state.audio.metronome_enabled);
|
||||
}
|
||||
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);
|
||||
BOOST_TEST(state.selected_part_index == 0U);
|
||||
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);
|
||||
BOOST_CHECK(state.mode == vamp_zero::app_mode::PERFORMANCE_SONG_MENU);
|
||||
BOOST_TEST(state.selected_song_index == 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_catalog_returns_to_main) {
|
||||
auto state = navigation_state();
|
||||
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;
|
||||
exercise.id = std::to_string(index);
|
||||
exercise.id.insert(0U, 4U - exercise.id.size(), '0');
|
||||
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 == "0366");
|
||||
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_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);
|
||||
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"}) {
|
||||
BOOST_AUTO_TEST_CASE(database_reload_preserves_all_generated_song_parts) {
|
||||
auto config = test_database_config(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "catalog.db");
|
||||
auto expected = std::size_t{};
|
||||
for (auto const &entry : std::filesystem::directory_iterator{std::filesystem::path{VAMP_TEST_SOURCE_DIR} / "data/songs"}) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".xml") {
|
||||
++expected_exercises;
|
||||
expected += 3U;
|
||||
}
|
||||
}
|
||||
BOOST_REQUIRE(expected_exercises > 0U);
|
||||
BOOST_REQUIRE(expected > 0U);
|
||||
for (auto iteration = std::uint32_t{}; iteration < 2U; ++iteration) {
|
||||
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);
|
||||
auto future = boost::asio::co_spawn(io, vamp_zero::async_load_catalog(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_TEST(exercise.id.size() == 4U);
|
||||
BOOST_TEST(exercise.id.find_first_not_of("0123456789") == std::string::npos);
|
||||
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);
|
||||
}
|
||||
}
|
||||
auto catalog = future.get();
|
||||
BOOST_REQUIRE_MESSAGE(catalog, (catalog ? "" : catalog.error().describe()));
|
||||
BOOST_REQUIRE_EQUAL(catalog->parts.size(), expected);
|
||||
for (auto const &part : catalog->parts) {
|
||||
io.restart();
|
||||
auto loaded = boost::asio::co_spawn(io, vamp_zero::async_load_score(io.get_executor(), config, {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 == part.title);
|
||||
BOOST_TEST(score->composer == part.composer);
|
||||
BOOST_TEST(score->key_signature == part.key_signature);
|
||||
BOOST_TEST(score->part == part.part);
|
||||
BOOST_TEST(score->measure_count == part.measure_count);
|
||||
BOOST_TEST(score->pages.size() == part.total_pages);
|
||||
BOOST_TEST(score->source_path == 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 : {"0010", "0002", "0001", "0127"}) {
|
||||
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_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();
|
||||
auto library = future.get();
|
||||
BOOST_REQUIRE(library);
|
||||
BOOST_REQUIRE_EQUAL(library->exercises.size(), 4U);
|
||||
BOOST_TEST(library->exercises[0].id == "0001");
|
||||
BOOST_TEST(library->exercises[1].id == "0002");
|
||||
BOOST_TEST(library->exercises[2].id == "0010");
|
||||
BOOST_TEST(library->exercises[3].id == "0127");
|
||||
for (auto const &exercise : library->exercises) {
|
||||
BOOST_TEST(exercise.title == "Shared Title");
|
||||
}
|
||||
BOOST_REQUIRE(vamp_test::build_catalog(assets, config.path));
|
||||
std::filesystem::rename(assets / "practice/0002", assets / "practice/0099");
|
||||
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);
|
||||
BOOST_TEST(!has_value(*rows, 0U, "0002-acoustic"));
|
||||
BOOST_TEST(has_value(*rows, 0U, "0099-acoustic"));
|
||||
BOOST_TEST(has_value(*rows, 0U, "0010-acoustic"));
|
||||
BOOST_TEST(has_value(*rows, 0U, "0001-acoustic"));
|
||||
BOOST_TEST(has_value(*rows, 0U, "0127-acoustic"));
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(keyless_scores_keep_their_authored_key) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import generator.accompaniment
|
||||
import generator.model
|
||||
import generator.parts
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import generator.rhythm
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
@@ -337,8 +337,8 @@ class DetachedAccompanimentTests(unittest.TestCase):
|
||||
before = copy.deepcopy(source)
|
||||
bars = tied_library(2).for_meter('4/4')[0].bars
|
||||
plan = generator.accompaniment.Plan(bars, bars, ())
|
||||
prepared = generator.parts.prepare_document(source, practice=True)
|
||||
result = generator.practice.generate._practice_document(prepared, 'acoustic', plan)
|
||||
prepared = generator.parts.prepare_document(source)
|
||||
result = tests.arrangement_fixtures.section_document(prepared, 'acoustic', rhythms=tests.rhythm_fixtures.library(), plan=plan)
|
||||
generator.model.validate_document(result)
|
||||
self.assertEqual(timing(tests.test_musicxml.notes(result)), [(0, 4, 'note')])
|
||||
self.assertEqual(timing(tests.test_musicxml.notes(result, 1)), [(0, 4, 'rest')])
|
||||
|
||||
@@ -11,7 +11,7 @@ import generator.engraving.layout
|
||||
import generator.engraving.types
|
||||
import generator.model
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
import tests.test_musicxml
|
||||
@@ -123,10 +123,10 @@ class AcousticTests(unittest.TestCase):
|
||||
generator.acoustic.prepare(document)
|
||||
generator.acoustic.combine(document)
|
||||
self.assertEqual(document, before)
|
||||
def test_song_and_practice_integration_preserve_playback(self):
|
||||
def test_song_and_section_integration_preserve_playback(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
rhythms = tests.rhythm_fixtures.library()
|
||||
for builder in (generator.arrange.generate.arrange_document, generator.practice.generate.practice_document):
|
||||
for builder in (generator.arrange.generate.arrange_document, tests.arrangement_fixtures.section_document):
|
||||
with unittest.mock.patch('generator.acoustic.combine'):
|
||||
separate = builder(source, 'acoustic', rhythms=rhythms)
|
||||
combined = builder(source, 'acoustic', rhythms=rhythms)
|
||||
|
||||
@@ -2,61 +2,12 @@ import json
|
||||
import pathlib
|
||||
import tempfile
|
||||
import unittest
|
||||
import unittest.mock
|
||||
|
||||
import generator.arrange.generate
|
||||
import generator.parts
|
||||
import generator.practice.generate
|
||||
import generator.score
|
||||
import generator.summary
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
|
||||
class CatalogTests(unittest.TestCase):
|
||||
def test_every_numbered_source_is_wired_to_three_part_paths(self):
|
||||
source = pathlib.Path('data/practice')
|
||||
sources = sorted(source.glob('*.xml'))
|
||||
self.assertEqual([p.name for p in sources], [f'{i:04}.xml' for i in range(1, 367)])
|
||||
expected = {pathlib.Path(path.stem) / (part + '.json') for path in sources for part in generator.parts.PART_TYPES}
|
||||
self.assertEqual(len(expected), 1098)
|
||||
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch('generator.practice.generate._practice_document', side_effect=lambda document, part, plan: document):
|
||||
output = pathlib.Path(temporary)
|
||||
self.assertEqual(generator.practice.generate.generate_practice_scores(source, output, rhythms=tests.rhythm_fixtures.library()), len(expected))
|
||||
self.assertEqual({p.relative_to(output) for p in output.rglob('*.json')}, expected)
|
||||
generator.summary.validate_practice_parts(output)
|
||||
generator.score.validate_generated_tree(output)
|
||||
def test_numbers_and_duplicate_titles_remain_distinct_and_songs_are_excluded(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
source, output = pathlib.Path(temporary) / 'source', pathlib.Path(temporary) / 'output'
|
||||
source.mkdir(parents=True)
|
||||
for name in ('1000.xml', '0000.xml', '0001.xml', '0999.xml', '0010.xml', '0366.xml'):
|
||||
(source / name).write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()), encoding='utf-8')
|
||||
self.assertEqual(generator.practice.generate.generate_practice_scores(source, output, rhythms=tests.rhythm_fixtures.library()), 18)
|
||||
self.assertEqual(sorted(p.name for p in output.iterdir()), ['0000', '0001', '0010', '0366', '0999', '1000'])
|
||||
for directory in output.iterdir():
|
||||
self.assertEqual({p.name for p in directory.glob('*.json')}, {(part + '.json') for part in generator.parts.PART_TYPES})
|
||||
self.assertEqual(json.loads((directory / 'vocal.json').read_text())['metadata']['title'], 'Fixture')
|
||||
def test_invalid_names_and_legacy_nesting_preserve_output(self):
|
||||
for name in ('1.xml', '001.xml', '00001.xml', '\u0660\u0660\u0660\u0661.xml', '0001-copy.xml', '0001_copy.xml', 'chord/0001.xml', 'practice/melody/lesson/0001.xml'):
|
||||
with self.subTest(name=name), tempfile.TemporaryDirectory() as temporary:
|
||||
source, output = pathlib.Path(temporary) / 'source', pathlib.Path(temporary) / 'output'
|
||||
for relative in ('0002.xml', name):
|
||||
path = source / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()), encoding='utf-8')
|
||||
output.mkdir()
|
||||
sentinel = output / 'existing'
|
||||
sentinel.write_text('unchanged')
|
||||
with self.assertRaisesRegex(ValueError, 'practice sources'):
|
||||
generator.practice.generate.generate_practice_scores(source, output, rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(list(output.iterdir()), [sentinel])
|
||||
self.assertEqual(sentinel.read_text(), 'unchanged')
|
||||
def test_empty_part_directory_fails(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
(root / '0001').mkdir()
|
||||
with self.assertRaisesRegex(ValueError, 'practice generator drift'):
|
||||
generator.summary.validate_practice_parts(root)
|
||||
def test_songs_generate_only_authored_key_parts(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
source, output = pathlib.Path(temporary) / 'songs', pathlib.Path(temporary) / 'output'
|
||||
|
||||
@@ -35,8 +35,6 @@ class CatalogTests(unittest.TestCase):
|
||||
document = bundle('Song', 'F')
|
||||
document['presentation']['title_image'] = '0123456789abcdef' * 4
|
||||
write_score(self.root, 'songs/tune.json', document)
|
||||
for part in ('Acoustic', 'Electric', 'Vocal'):
|
||||
write_score(self.root, f'practice/0001/{part.lower()}.json', bundle('Exercise', 'Eb', part))
|
||||
database = generator.catalog.build_catalog(self.root)
|
||||
self.assertEqual(database, self.root / 'catalog.db')
|
||||
song = self.rows('songs')[0]
|
||||
@@ -44,22 +42,12 @@ class CatalogTests(unittest.TestCase):
|
||||
self.assertEqual((song['id'], song['part'], song['source_path']), ('tune', 'Lead', 'songs/tune.json'))
|
||||
self.assertEqual((song['measure_count'], song['total_pages']), (1, 1))
|
||||
self.assertEqual(song['title_image'], document['presentation']['title_image'])
|
||||
practice = self.rows('practice_exercises')
|
||||
self.assertEqual([row['id'] for row in practice], ['0001-acoustic', '0001-electric', '0001-vocal'])
|
||||
self.assertEqual(practice[0]['exercise_id'], '0001')
|
||||
self.assertNotIn('practice_type_id', practice[0])
|
||||
self.assertNotIn('lesson_group_id', practice[0])
|
||||
self.assertEqual(practice[0]['key_signature'], 'Eb')
|
||||
self.assertTrue(all(row['title_image'] == '0' * 64 for row in practice))
|
||||
self.assertTrue(all(json.loads(row['source_json'])['presentation']['title_image'] == row['title_image'] for row in practice))
|
||||
self.assertNotIn('authored_key_signature', practice[0])
|
||||
with contextlib.closing(sqlite3.connect(database)) as connection:
|
||||
self.assertEqual(connection.execute('PRAGMA application_id').fetchone()[0], generator.catalog.APPLICATION_ID)
|
||||
self.assertEqual(connection.execute('PRAGMA integrity_check').fetchall(), [('ok',)])
|
||||
self.assertEqual(connection.execute("SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name").fetchall(), [('practice_exercises',), ('songs',)])
|
||||
for table in ('songs', 'practice_exercises'):
|
||||
columns = {row[1]: row for row in connection.execute(f'PRAGMA table_info({table})')}
|
||||
self.assertEqual((columns['title_image'][2], columns['title_image'][3]), ('TEXT', 1))
|
||||
self.assertEqual(connection.execute("SELECT name FROM sqlite_schema WHERE type='table' ORDER BY name").fetchall(), [('songs',)])
|
||||
columns = {row[1]: row for row in connection.execute('PRAGMA table_info(songs)')}
|
||||
self.assertEqual((columns['title_image'][2], columns['title_image'][3]), ('TEXT', 1))
|
||||
self.assertFalse(pathlib.Path(str(database) + '-wal').exists())
|
||||
self.assertFalse(pathlib.Path(str(database) + '-journal').exists())
|
||||
def test_rebuild_is_deterministic_relocatable_and_replaces_deleted_rows(self):
|
||||
@@ -88,14 +76,6 @@ class CatalogTests(unittest.TestCase):
|
||||
generator.catalog.build_catalog(self.root)
|
||||
self.assertEqual(database.read_bytes(), previous)
|
||||
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/0001/{part.lower()}.json', bundle(part=part))
|
||||
with self.assertRaisesRegex(ValueError, 'all three parts'):
|
||||
generator.catalog.build_catalog(self.root)
|
||||
write_score(self.root, 'practice/0001/vocal.json', bundle(key='F', part='Vocal'))
|
||||
with self.assertRaisesRegex(ValueError, 'disagree'):
|
||||
generator.catalog.build_catalog(self.root)
|
||||
def test_missing_or_invalid_title_image_preserves_existing_database(self):
|
||||
score = write_score(self.root, 'songs/tune.json')
|
||||
database = generator.catalog.build_catalog(self.root)
|
||||
@@ -113,9 +93,9 @@ class CatalogTests(unittest.TestCase):
|
||||
self.assertFalse(list(self.root.glob('.vamp-catalog-*')))
|
||||
def test_keyless_parts_preserve_their_original_key(self):
|
||||
for part in ('Acoustic', 'Electric', 'Vocal'):
|
||||
write_score(self.root, f'practice/0001/{part.lower()}.json', bundle(key='No key', part=part))
|
||||
write_score(self.root, f'songs/{part.lower()}.json', bundle(key='No key', part=part))
|
||||
generator.catalog.build_catalog(self.root)
|
||||
self.assertTrue(all(row['key_signature'] == 'No key' for row in self.rows('practice_exercises')))
|
||||
self.assertTrue(all(row['key_signature'] == 'No key' for row in self.rows('songs')))
|
||||
def test_invalid_layout_and_escaping_source_fail(self):
|
||||
with self.assertRaisesRegex(ValueError, 'directory not found'):
|
||||
generator.catalog.build_catalog(self.root / 'missing')
|
||||
@@ -140,3 +120,24 @@ class CatalogTests(unittest.TestCase):
|
||||
self.assertEqual(pathlib.Path(config['database']['path']).name, 'catalog.db')
|
||||
self.assertTrue((destination / 'songs/tune.png').exists())
|
||||
self.assertFalse(list(destination.rglob('*.json')))
|
||||
def test_menu_query_uses_index_and_preserves_binary_identity_order(self):
|
||||
for name, title, composer, part in (('z', 'Same', 'Bea', 'Acoustic'), ('b', 'Same', 'Ada', 'Vocal'), ('a', 'Same', 'Ada', 'Acoustic'), ('c', 'same', 'Ada', 'Electric'), ('d', 'Same', 'Ada', 'Acoustic')):
|
||||
document = bundle(title=title, part=part)
|
||||
document['music']['metadata']['composer'] = composer
|
||||
write_score(self.root, f'songs/{name}.json', document)
|
||||
database = generator.catalog.build_catalog(self.root)
|
||||
query = 'SELECT id, title, composer, part, source_path, measure_count, key_signature, total_pages, title_image FROM songs ORDER BY title, composer, part, id'
|
||||
with contextlib.closing(sqlite3.connect(database)) as connection:
|
||||
self.assertEqual([row[0] for row in connection.execute(query)], ['a', 'd', 'b', 'z', 'c'])
|
||||
plan = ' '.join(row[3] for row in connection.execute('EXPLAIN QUERY PLAN ' + query))
|
||||
self.assertIn('songs_menu', plan)
|
||||
self.assertNotIn('TEMP B-TREE', plan)
|
||||
def test_failed_population_rolls_back_schema_and_rows(self):
|
||||
write_score(self.root, 'songs/a.json')
|
||||
write_score(self.root, 'songs/b.json', bundle(title=''))
|
||||
with contextlib.closing(sqlite3.connect(':memory:')) as connection:
|
||||
with self.assertRaises(ValueError):
|
||||
generator.catalog.populate(connection, self.root)
|
||||
self.assertFalse(connection.in_transaction)
|
||||
self.assertEqual(connection.execute("SELECT name FROM sqlite_schema").fetchall(), [])
|
||||
self.assertEqual(connection.execute('PRAGMA application_id').fetchone()[0], 0)
|
||||
|
||||
+62
-158
@@ -37,8 +37,7 @@ template <typename T>
|
||||
concept retains_score_json = requires(T value) { value.source_json; };
|
||||
|
||||
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>);
|
||||
static_assert(!retains_score_json<vamp_zero::catalog>);
|
||||
|
||||
struct catalog_directory {
|
||||
std::filesystem::path path;
|
||||
@@ -127,60 +126,41 @@ struct catalog_database {
|
||||
auto build() const -> std::bool_t {
|
||||
return vamp_test::build_catalog(assets, config.path);
|
||||
}
|
||||
auto performance() const -> std::expected<vamp_zero::library_state, modulus::error> {
|
||||
return run_catalog_operation(vamp_zero::async_load_performance_library, config, assets.string());
|
||||
auto performance() const -> std::expected<vamp_zero::catalog, modulus::error> {
|
||||
return run_catalog_operation(vamp_zero::async_load_catalog, config, assets.string());
|
||||
}
|
||||
auto practice() const -> std::expected<vamp_zero::practice_library, modulus::error> {
|
||||
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_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{category, std::move(id)}, assets.string());
|
||||
auto score(std::string id) const -> std::expected<vamp_zero::score_document, modulus::error> {
|
||||
return run_catalog_operation(vamp_zero::async_load_score, config, vamp_zero::score_reference{std::move(id)}, assets.string());
|
||||
}
|
||||
};
|
||||
|
||||
auto catalog_exercise(std::string authored = "Eb", std::string id = "0001") -> vamp_zero::practice_exercise {
|
||||
auto result = vamp_zero::practice_exercise{
|
||||
.id = std::move(id),
|
||||
.title = "Exercise",
|
||||
.composer = "Composer",
|
||||
};
|
||||
for (auto part_name : vamp_zero::practice_part_names()) {
|
||||
result.parts.push_back({.id = result.id + "-" + std::string{part_name}, .key_signature = authored, .part = std::string{part_name}, .measure_count = 3, .total_pages = 2});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_metadata) {
|
||||
auto performance = vamp_zero::group_library_parts({
|
||||
auto performance = vamp_zero::group_ordered_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({catalog_exercise("Eb", "0002"), catalog_exercise("Eb", "0001")});
|
||||
BOOST_REQUIRE(practice);
|
||||
auto catalog = std::make_shared<vamp_zero::catalog const>(vamp_zero::catalog{std::move(performance), std::move(*practice)});
|
||||
auto catalog = std::make_shared<vamp_zero::catalog const>(std::move(performance));
|
||||
auto original = vamp_zero::app_state{
|
||||
.catalog = catalog,
|
||||
.mode = vamp_zero::app_mode::PERFORMANCE_PART_MENU,
|
||||
.selected_song_index = 2,
|
||||
.selected_part_index = 1,
|
||||
.selected_practice_exercise_index = 2,
|
||||
};
|
||||
auto control = vamp_zero::control_state{{}, original};
|
||||
auto observed = std::vector<vamp_zero::app_state>{};
|
||||
control.set_observer([&](vamp_zero::app_state state) { observed.push_back(std::move(state)); });
|
||||
auto before = control.snapshot();
|
||||
BOOST_TEST(before.catalog.get() == catalog.get());
|
||||
BOOST_TEST(vamp_zero::selected_part(before) == &catalog->performance.parts[1]);
|
||||
BOOST_TEST(vamp_zero::selected_practice_exercise(before) == &catalog->practice.exercises[1]);
|
||||
BOOST_TEST(vamp_zero::selected_part(before) == &catalog->parts[1]);
|
||||
auto heartbeat = control.heartbeat();
|
||||
BOOST_TEST(heartbeat.catalog.get() == catalog.get());
|
||||
BOOST_TEST(heartbeat.heartbeat_count == 1U);
|
||||
BOOST_TEST(heartbeat.pico_connected);
|
||||
auto playing = control.apply(vamp_protocol::page_command::SELECT);
|
||||
BOOST_TEST(vamp_zero::to_string(playing.mode) == "performance_score");
|
||||
BOOST_TEST(vamp_zero::active_part(playing) == &catalog->performance.parts[1]);
|
||||
BOOST_TEST(vamp_zero::active_part(playing) == &catalog->parts[1]);
|
||||
BOOST_TEST(control.apply(vamp_protocol::page_command::NEXT).page_index == 1U);
|
||||
BOOST_TEST(control.apply(vamp_protocol::page_command::PREVIOUS).page_index == 0U);
|
||||
control.apply(vamp_protocol::page_command::SELECT);
|
||||
@@ -193,29 +173,14 @@ BOOST_AUTO_TEST_CASE(catalog_snapshots_heartbeat_and_navigation_share_immutable_
|
||||
for (auto const &snapshot : observed) {
|
||||
BOOST_TEST(snapshot.catalog.get() == catalog.get());
|
||||
}
|
||||
original.mode = vamp_zero::app_mode::PRACTICE_EXERCISE_MENU;
|
||||
auto practice_control = vamp_zero::control_state{{}, original};
|
||||
auto parts = practice_control.apply(vamp_protocol::page_command::SELECT);
|
||||
BOOST_TEST(vamp_zero::to_string(parts.mode) == "practice_part_menu");
|
||||
auto selected = practice_control.apply(vamp_protocol::page_command::NEXT);
|
||||
BOOST_TEST(selected.catalog.get() == catalog.get());
|
||||
BOOST_TEST(vamp_zero::selected_practice_part(selected, catalog->practice.exercises[1]) == &catalog->practice.exercises[1].parts[0]);
|
||||
auto exercise = practice_control.apply(vamp_protocol::page_command::SELECT);
|
||||
BOOST_TEST(vamp_zero::to_string(exercise.mode) == "practice_score");
|
||||
BOOST_TEST(exercise.catalog.get() == catalog.get());
|
||||
BOOST_TEST(vamp_zero::active_practice(exercise) == &catalog->practice.exercises[1]);
|
||||
BOOST_TEST(practice_control.apply(vamp_protocol::page_command::NEXT).page_index == 1U);
|
||||
BOOST_TEST(practice_control.heartbeat().catalog.get() == catalog.get());
|
||||
BOOST_TEST(parts.active_practice_id.empty());
|
||||
BOOST_TEST(parts.selected_practice_part_index == 0U);
|
||||
}
|
||||
|
||||
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"},
|
||||
auto library = vamp_zero::group_ordered_parts({
|
||||
{.id = "other-title", .title = "Other Title", .composer = "Ada", .part = "Lead"},
|
||||
{.id = "guitar", .title = "Shared Title", .composer = "Ada", .part = "Guitar"},
|
||||
{.id = "vocal", .title = "Shared Title", .composer = "Ada", .part = "Vocal"},
|
||||
{.id = "other-composer", .title = "Shared Title", .composer = "Bea", .part = "Lead"},
|
||||
});
|
||||
BOOST_REQUIRE_EQUAL(library.parts.size(), 4U);
|
||||
BOOST_REQUIRE_EQUAL(library.groups.size(), 3U);
|
||||
@@ -241,79 +206,56 @@ BOOST_AUTO_TEST_CASE(catalog_performance_groups_use_title_and_composer_with_part
|
||||
BOOST_TEST(library.parts[library.groups[1].parts[1]].id == "vocal");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_practice_requires_complete_ordered_parts_and_key_agreement) {
|
||||
for (auto authored : {"Eb", "C dorian", "No key", ""}) {
|
||||
auto first = catalog_exercise(authored);
|
||||
std::reverse(first.parts.begin(), first.parts.end());
|
||||
auto last = first.parts.back();
|
||||
first.parts.pop_back();
|
||||
auto fragment = catalog_exercise(authored);
|
||||
fragment.parts = {last};
|
||||
auto grouped = vamp_zero::group_practice_exercises({catalog_exercise("F", "0002"), first, fragment});
|
||||
BOOST_REQUIRE(grouped);
|
||||
BOOST_REQUIRE_EQUAL(grouped->exercises.size(), 2U);
|
||||
auto const &exercise = grouped->exercises[0];
|
||||
BOOST_TEST(exercise.id == "0001");
|
||||
BOOST_TEST(exercise.parts.front().key_signature == authored);
|
||||
BOOST_REQUIRE_EQUAL(exercise.parts.size(), 3U);
|
||||
for (auto index = std::size_t{}; index < exercise.parts.size(); ++index) {
|
||||
BOOST_TEST(exercise.parts[index].part == vamp_zero::practice_part_names()[index]);
|
||||
}
|
||||
}
|
||||
for (auto conflict : {"F", ""}) {
|
||||
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});
|
||||
BOOST_REQUIRE(!grouped);
|
||||
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 = catalog_exercise();
|
||||
missing.parts.pop_back();
|
||||
BOOST_TEST(!vamp_zero::group_practice_exercises({missing}));
|
||||
auto duplicate = catalog_exercise();
|
||||
duplicate.parts.back().part = "Acoustic";
|
||||
BOOST_TEST(!vamp_zero::group_practice_exercises({duplicate}));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_uses_category_and_reports_missing_or_invalid) {
|
||||
BOOST_AUTO_TEST_CASE(catalog_individual_score_lookup_is_parameterized_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/0001"} / vamp_test::part_filename(part), catalog_bundle("Exercise", "D", part));
|
||||
BOOST_REQUIRE(db.build());
|
||||
std::filesystem::remove_all(db.assets);
|
||||
auto document = db.score("shared");
|
||||
BOOST_REQUIRE(document);
|
||||
BOOST_TEST(document->id == "shared");
|
||||
BOOST_TEST(document->title == "Song");
|
||||
BOOST_TEST(document->key_signature == "F");
|
||||
BOOST_TEST(document->source_json == song);
|
||||
BOOST_TEST(document->source_path == (db.assets / "songs/shared.json").string());
|
||||
BOOST_TEST(document->measure_count == 3U);
|
||||
BOOST_REQUIRE_EQUAL(document->pages.size(), 2U);
|
||||
BOOST_TEST(document->pages[1].path == "fixture.pages/2.png");
|
||||
for (auto id : {"missing", "shared' OR 1=1 --"}) {
|
||||
auto missing = db.score(id);
|
||||
BOOST_REQUIRE(!missing);
|
||||
BOOST_TEST(missing.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::NOT_FOUND));
|
||||
}
|
||||
db.query("UPDATE songs SET source_json = '{}' WHERE id = 'shared'");
|
||||
auto invalid = db.score("shared");
|
||||
BOOST_REQUIRE(!invalid);
|
||||
BOOST_TEST(invalid.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_query_orders_and_groups_by_exact_song_identity) {
|
||||
auto db = catalog_database{};
|
||||
for (auto const &id : {"upper-bea", "upper-vocal", "upper-acoustic", "lower", "upper-acoustic-second"}) {
|
||||
db.write(std::filesystem::path{"songs"} / (std::string{id} + ".json"), catalog_bundle("Same"));
|
||||
}
|
||||
BOOST_REQUIRE(db.build());
|
||||
db.query("UPDATE practice_exercises SET id = 'shared' WHERE part = 'Electric'");
|
||||
std::filesystem::remove_all(db.assets);
|
||||
for (auto category : {vamp_zero::score_category::PERFORMANCE, vamp_zero::score_category::PRACTICE}) {
|
||||
auto is_song = category == vamp_zero::score_category::PERFORMANCE;
|
||||
auto document = db.score(category, "shared");
|
||||
BOOST_REQUIRE(document);
|
||||
BOOST_TEST(document->id == "shared");
|
||||
BOOST_TEST(document->title == (is_song ? "Song" : "Exercise"));
|
||||
BOOST_TEST(document->key_signature == (is_song ? "F" : "D"));
|
||||
BOOST_TEST(document->source_json == (is_song ? song : exercise));
|
||||
BOOST_TEST(document->source_path == (db.assets / (is_song ? "songs/shared.json" : "practice/0001/electric.json")).string());
|
||||
BOOST_TEST(document->measure_count == 3U);
|
||||
BOOST_REQUIRE_EQUAL(document->pages.size(), 2U);
|
||||
BOOST_TEST(document->pages[1].path == "fixture.pages/2.png");
|
||||
for (auto id : {"missing", "shared' OR 1=1 --"}) {
|
||||
auto missing = db.score(category, id);
|
||||
BOOST_REQUIRE(!missing);
|
||||
BOOST_TEST(missing.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::NOT_FOUND));
|
||||
}
|
||||
auto table = std::string{is_song ? "songs" : "practice_exercises"};
|
||||
db.query("UPDATE " + table + " SET source_json = '{}' WHERE id = 'shared'");
|
||||
auto invalid = db.score(category, "shared");
|
||||
BOOST_REQUIRE(!invalid);
|
||||
BOOST_TEST(invalid.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
|
||||
BOOST_REQUIRE(db.score(is_song ? vamp_zero::score_category::PRACTICE : vamp_zero::score_category::PERFORMANCE, "shared"));
|
||||
db.query("UPDATE " + table + " SET source_json = ?1 WHERE id = 'shared'", {noto::sqlite_value{is_song ? song : exercise}});
|
||||
db.query("UPDATE songs SET composer = 'Ada', part = 'Acoustic'");
|
||||
db.query("UPDATE songs SET composer = 'Bea' WHERE id = 'upper-bea'");
|
||||
db.query("UPDATE songs SET part = 'Vocal' WHERE id = 'upper-vocal'");
|
||||
db.query("UPDATE songs SET title = 'same' WHERE id = 'lower'");
|
||||
auto result = db.performance();
|
||||
BOOST_REQUIRE(result);
|
||||
BOOST_REQUIRE_EQUAL(result->groups.size(), 3U);
|
||||
auto ids = std::vector<std::string>{};
|
||||
for (auto const &part : result->parts) {
|
||||
ids.push_back(part.id);
|
||||
}
|
||||
auto expected = std::vector<std::string>{"upper-acoustic", "upper-acoustic-second", "upper-vocal", "upper-bea", "lower"};
|
||||
BOOST_TEST(ids == expected, boost::test_tools::per_element());
|
||||
BOOST_TEST(result->groups[0].composer == "Ada");
|
||||
BOOST_TEST(result->groups[0].parts.size() == 3U);
|
||||
BOOST_TEST(result->groups[1].composer == "Bea");
|
||||
BOOST_TEST(result->groups[2].title == "same");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded) {
|
||||
@@ -322,7 +264,7 @@ BOOST_AUTO_TEST_CASE(catalog_sequential_score_loads_keep_executor_stack_bounded)
|
||||
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>> {
|
||||
auto reference = vamp_zero::score_reference{.category = vamp_zero::score_category::PERFORMANCE, .id = "repeated"};
|
||||
auto reference = vamp_zero::score_reference{.id = "repeated"};
|
||||
for (auto count = std::uint32_t{}; count < 4096U; ++count) {
|
||||
auto score = co_await vamp_zero::async_load_score(executor, config, reference, assets);
|
||||
if (!score) {
|
||||
@@ -343,48 +285,20 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
|
||||
db.query("INSERT INTO songs(id, title, composer, source_json, part, source_path, "
|
||||
"key_signature, measure_count, total_pages, title_image) "
|
||||
"VALUES('tune', 'Metadata Song', 'Composer', '{}', 'Lead', 'songs/tune.json', 'F', 3, 2, '0000000000000000000000000000000000000000000000000000000000000000')");
|
||||
for (auto part_name : vamp_zero::practice_part_names()) {
|
||||
db.query("INSERT INTO practice_exercises(id, exercise_id, "
|
||||
"title, composer, key_signature, source_json, source_path, measure_count, total_pages, part, title_image) "
|
||||
"VALUES(?1, '0001', 'Metadata Exercise', 'Composer', 'Eb', '{}', ?3, 3, 2, ?2, '0000000000000000000000000000000000000000000000000000000000000000')",
|
||||
{noto::sqlite_value{std::string{"practice_part-"} + std::string{part_name}}, noto::sqlite_value{std::string{part_name}},
|
||||
noto::sqlite_value{std::string{"practice/0001/"} + std::string{part_name} + ".json"}});
|
||||
}
|
||||
auto songs = db.rows("songs");
|
||||
auto exercises = db.rows("practice_exercises");
|
||||
for (auto attempt = std::uint32_t{}; attempt < 2U; ++attempt) {
|
||||
auto performance = db.performance();
|
||||
auto practice = db.practice();
|
||||
BOOST_REQUIRE(performance);
|
||||
BOOST_REQUIRE(practice);
|
||||
BOOST_REQUIRE_EQUAL(performance->parts.size(), 1U);
|
||||
BOOST_TEST(performance->parts[0].title == "Metadata Song");
|
||||
BOOST_TEST(performance->parts[0].key_signature == "F");
|
||||
BOOST_TEST(performance->parts[0].measure_count == 3U);
|
||||
BOOST_TEST(performance->parts[0].total_pages == 2U);
|
||||
BOOST_REQUIRE_EQUAL(practice->exercises.size(), 1U);
|
||||
BOOST_TEST(practice->exercises[0].title == "Metadata Exercise");
|
||||
BOOST_TEST(practice->exercises[0].parts.front().key_signature == "Eb");
|
||||
BOOST_REQUIRE_EQUAL(practice->exercises[0].parts.size(), 3U);
|
||||
BOOST_TEST(practice->exercises[0].parts[0].part == "Acoustic");
|
||||
BOOST_TEST(practice->exercises[0].parts[2].part == "Vocal");
|
||||
for (auto const &practice_part : practice->exercises[0].parts) {
|
||||
BOOST_TEST(practice_part.measure_count == 3U);
|
||||
BOOST_TEST(practice_part.total_pages == 2U);
|
||||
}
|
||||
}
|
||||
BOOST_TEST(db.rows("songs") == songs, boost::test_tools::per_element());
|
||||
BOOST_TEST(db.rows("practice_exercises") == exercises, boost::test_tools::per_element());
|
||||
auto song = db.score(vamp_zero::score_category::PERFORMANCE, "tune");
|
||||
auto exercise = db.score(vamp_zero::score_category::PRACTICE, "practice_part-Vocal");
|
||||
auto song = db.score("tune");
|
||||
BOOST_REQUIRE(!song);
|
||||
BOOST_REQUIRE(!exercise);
|
||||
BOOST_TEST(song.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
|
||||
BOOST_TEST(exercise.error().code() == static_cast<std::int32_t>(vamp_zero::error_code::INVALID_ARGUMENT));
|
||||
db.query("UPDATE practice_exercises SET key_signature = 'F' WHERE id = 'practice_part-Vocal'");
|
||||
auto disagreement = db.practice();
|
||||
BOOST_REQUIRE(!disagreement);
|
||||
BOOST_TEST(disagreement.error().message().find("disagree on key") != std::string_view::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_incompatible_and_corrupt_files_without_repair) {
|
||||
@@ -395,12 +309,9 @@ BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_incompatible_and_corrupt
|
||||
BOOST_REQUIRE(db.build());
|
||||
BOOST_REQUIRE(validate());
|
||||
auto songs = db.performance();
|
||||
auto practice = db.practice();
|
||||
BOOST_REQUIRE(songs);
|
||||
BOOST_REQUIRE(practice);
|
||||
BOOST_TEST(songs->parts.empty());
|
||||
BOOST_TEST(practice->exercises.empty());
|
||||
for (auto sql : {"ALTER TABLE songs RENAME COLUMN part TO obsolete_part", "ALTER TABLE practice_exercises RENAME COLUMN exercise_id TO obsolete_id", "DROP TABLE songs", "DROP TABLE practice_exercises"}) {
|
||||
for (auto sql : {"ALTER TABLE songs RENAME COLUMN part TO obsolete_part", "DROP TABLE songs"}) {
|
||||
db.query(sql);
|
||||
auto snapshot_sql = "SELECT json_group_array(sql) FROM (SELECT sql FROM sqlite_schema ORDER BY name)";
|
||||
auto before = std::get<std::string>(db.query(snapshot_sql).rows[0][0].value);
|
||||
@@ -444,12 +355,8 @@ BOOST_AUTO_TEST_CASE(catalog_validation_checks_column_types_constraints_and_tabl
|
||||
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/0001"} / vamp_test::part_filename(part_name), catalog_bundle("Exercise", "Eb", part_name));
|
||||
}
|
||||
BOOST_REQUIRE(db.build());
|
||||
std::filesystem::remove(db.assets / "songs/tune.json");
|
||||
std::filesystem::remove_all(db.assets / "practice");
|
||||
auto moved = db.directory.path / "relocated";
|
||||
std::filesystem::rename(db.assets, moved);
|
||||
db.assets = moved;
|
||||
@@ -457,14 +364,11 @@ BOOST_AUTO_TEST_CASE(catalog_reads_readonly_database_without_json_and_resolves_r
|
||||
std::filesystem::permissions(db.config.path, std::filesystem::perms::owner_read);
|
||||
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");
|
||||
auto score = db.score("tune");
|
||||
BOOST_REQUIRE(songs);
|
||||
BOOST_REQUIRE(practice);
|
||||
BOOST_REQUIRE(score);
|
||||
BOOST_TEST(score->source_path == (moved / "songs/tune.json").string());
|
||||
BOOST_TEST(songs->parts[0].source_path == score->source_path);
|
||||
BOOST_TEST(practice->exercises[0].parts.front().key_signature == "Eb");
|
||||
BOOST_TEST((std::filesystem::last_write_time(db.config.path) == timestamp));
|
||||
BOOST_TEST(!std::filesystem::exists(db.config.path + "-wal"));
|
||||
BOOST_TEST(!std::filesystem::exists(db.config.path + "-shm"));
|
||||
@@ -481,10 +385,10 @@ BOOST_AUTO_TEST_CASE(catalog_rejects_paths_outside_the_asset_root) {
|
||||
}
|
||||
db.query("UPDATE songs SET source_path = ?1", {noto::sqlite_value{std::string{path}}});
|
||||
BOOST_TEST(!db.performance());
|
||||
BOOST_TEST(!db.score(vamp_zero::score_category::PERFORMANCE, "tune"));
|
||||
BOOST_TEST(!db.score("tune"));
|
||||
}
|
||||
std::filesystem::create_directory_symlink(db.directory.path, db.assets / "escape");
|
||||
db.query("UPDATE songs SET source_path = 'escape/tune.json'");
|
||||
BOOST_TEST(!db.performance());
|
||||
BOOST_TEST(!db.score(vamp_zero::score_category::PERFORMANCE, "tune"));
|
||||
BOOST_TEST(!db.score("tune"));
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import generator.guitar
|
||||
import generator.model
|
||||
import generator.musicxml
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
import tests.test_musicxml
|
||||
@@ -40,7 +40,7 @@ class EngravingTests(unittest.TestCase):
|
||||
def test_tab_omits_rests_but_keeps_standard_notation(self):
|
||||
content = tests.test_musicxml.xml_note('E', 2, 'half', octave=5) + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
document = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(document)
|
||||
self.assertEqual(len(self.primitives(layout, 'rest')), 1)
|
||||
self.assertEqual(self.primitives(layout, 'tab-rest'), [])
|
||||
@@ -55,7 +55,7 @@ class EngravingTests(unittest.TestCase):
|
||||
def test_guitar_pages_always_pair_systems_including_tall_passages(self):
|
||||
content = tests.test_musicxml.xml_note('E', octave=5)
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 32))
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
document = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(document)
|
||||
self.assert_paired_pages(layout)
|
||||
self.assertEqual(layout.style.space, 10)
|
||||
@@ -108,7 +108,7 @@ class EngravingTests(unittest.TestCase):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(dense, divisions=16))
|
||||
documents = [generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/notation.xml')),
|
||||
tests.test_musicxml.parse(tests.test_musicxml.xml_score(tied)),
|
||||
generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())]
|
||||
tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())]
|
||||
checked = 0
|
||||
for sampling in (1, 2, 3, 4):
|
||||
for document in documents:
|
||||
@@ -280,11 +280,11 @@ class EngravingTests(unittest.TestCase):
|
||||
percussion = '<note><unpitched><display-step>C</display-step><display-octave>5</display-octave></unpitched><duration>4</duration><type>whole</type><notehead>x</notehead></note>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(percussion, attributes='<clef><sign>percussion</sign></clef>'))
|
||||
self.assertEqual(self.primitives(self.layout(source), 'head')[0].value, 'noteheadXWhole')
|
||||
tab = generator.practice.generate.practice_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=7))), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
tab = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('C', octave=6))), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(self.primitives(self.layout(tab), 'tab')[0].value, '20')
|
||||
def test_generated_tab_shares_columns_and_systems_without_duplicating_music(self):
|
||||
content = ''.join(tests.test_musicxml.xml_note('G', 1, '64th') for _ in range(64))
|
||||
source = generator.practice.generate.practice_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=16)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
source = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=16)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(source)
|
||||
systems = [s for page in layout.pages for s in page.systems]
|
||||
self.assertGreater(len(systems), 1)
|
||||
@@ -300,7 +300,7 @@ class EngravingTests(unittest.TestCase):
|
||||
self.assertEqual(len(generator.playback.compile_timeline(source)['events']), 64)
|
||||
def test_generated_tab_ties_and_barline_styles(self):
|
||||
content = tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="stop"/>') + '<barline location="right"><bar-style>dashed</bar-style></barline>'
|
||||
source = generator.practice.generate.practice_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
source = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
layout = self.layout(source)
|
||||
self.assertEqual(len(self.primitives(layout, 'tab-tie')), 1)
|
||||
self.assertEqual(len(self.primitives(layout, 'barline')), len(self.primitives(layout, 'tab-barline')))
|
||||
|
||||
@@ -29,7 +29,7 @@ import tests.test_musicxml
|
||||
class EngravingCacheTests(unittest.TestCase):
|
||||
def test_ci_snapshot_tracks_all_generator_inputs_and_keeps_fallback(self):
|
||||
workflow = pathlib.Path('.gitea/workflows/ci.yml').read_text()
|
||||
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'data/practice/*.xml', 'data/songs/*.xml', 'data/rhythms/**/*.xml')", workflow)
|
||||
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'data/songs/*.xml', 'data/rhythms/**/*.xml')", workflow)
|
||||
self.assertIn('python3 -m generator --renderer-fingerprint', workflow)
|
||||
self.assertIn('restore-keys: product-vamp-pages-${{ steps.renderer.outputs.fingerprint }}-', workflow)
|
||||
self.assertIn('path: build/build_common/engraving-cache/${{ steps.renderer.outputs.fingerprint }}', workflow)
|
||||
@@ -318,10 +318,10 @@ class EngravingCacheTests(unittest.TestCase):
|
||||
generator.engraving.cache.renderer_namespace(self.cache, 'renderer')
|
||||
def test_only_changed_score_is_rendered_and_validation_reads_json_once(self):
|
||||
root = self.root / 'scores'
|
||||
self.write_scores(root / 'practice', [self.document, self.document])
|
||||
self.write_scores(root / 'songs', [self.document, self.document])
|
||||
generator.engraving.pipeline.engrave_tree(root, self.fonts, cache_dir=self.cache)
|
||||
changed = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('D')))
|
||||
self.write_scores(root / 'practice', [self.document, changed])
|
||||
self.write_scores(root / 'songs', [self.document, changed])
|
||||
with unittest.mock.patch('generator.engraving.layout.layout_systems', wraps=generator.engraving.layout.layout_systems) as render:
|
||||
generator.engraving.pipeline.engrave_tree(root, self.fonts, cache_dir=self.cache)
|
||||
render.assert_called_once()
|
||||
@@ -347,8 +347,7 @@ class EngravingCacheTests(unittest.TestCase):
|
||||
self.enterContext(unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()))
|
||||
data, output = self.root / 'data', self.root / 'output'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
(data / 'practice').mkdir()
|
||||
source = data / 'practice/0001.xml'
|
||||
source = data / 'songs/fixture.xml'
|
||||
source.write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False, engraving_cache=self.cache, jobs=2)
|
||||
generator.__main__.generate_assets(args)
|
||||
|
||||
+19
-27
@@ -15,8 +15,7 @@ import generator.model
|
||||
import generator.parts
|
||||
import generator.pitch
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import generator.summary
|
||||
import tests.arrangement_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
|
||||
@@ -65,25 +64,25 @@ class GuitarTests(unittest.TestCase):
|
||||
def test_melody_placement_preserves_notes_timing_and_sound(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', 2, 'half', octave=4) + tests.test_musicxml.xml_note('F', 2, 'half', octave=4)))
|
||||
before = copy.deepcopy(source)
|
||||
electric = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
vocal = generator.practice.generate.practice_document(source, 'vocal', rhythms=tests.rhythm_fixtures.library())
|
||||
electric = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
vocal = tests.arrangement_fixtures.section_document(source, 'vocal', rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(electric)
|
||||
self.assertEqual(source, before)
|
||||
self.assertEqual(generator.playback.compile_timeline(electric)['events'], generator.playback.compile_timeline(vocal)['events'])
|
||||
self.assertEqual([(e['pitch'], e['onset'], e['duration']) for e in generator.playback.compile_timeline(electric)['events']], [(generator.model.rational(generator.model.fraction(e['pitch']) - 12), e['onset'], e['duration']) for e in generator.playback.compile_timeline(source)['events']])
|
||||
self.assertEqual([(e['onset'], e['duration'], generator.model.fraction(e['pitch']) - 12) for e in generator.playback.compile_timeline(electric)['events']], [(e['onset'], e['duration'], generator.model.fraction(e['pitch'])) for e in generator.playback.compile_timeline(vocal)['events']])
|
||||
self.assertEqual([(e['pitch'], e['onset'], e['duration']) for e in generator.playback.compile_timeline(electric)['events']], [(e['pitch'], e['onset'], e['duration']) for e in generator.playback.compile_timeline(source)['events']])
|
||||
for original, generated in zip(tests.test_musicxml.notes(source), tests.test_musicxml.notes(electric)):
|
||||
self.assertEqual(generator.model.pitch_number(generated['pitches'][0]), generator.model.pitch_number(original['pitches'][0]))
|
||||
self.assertEqual(generator.model.pitch_number(generated['pitches'][0]), generator.model.pitch_number(original['pitches'][0]) + 12)
|
||||
self.assertIn('tablature', generated['pitches'][0])
|
||||
def test_instrument_transposition_and_octave_clef_applied_once(self):
|
||||
for attributes, expected in (('', 48), ('<transpose><chromatic>-12</chromatic><diatonic>-7</diatonic></transpose>', 48), ('<transpose><chromatic>0</chromatic></transpose>', 60), ('<clef><sign>G</sign><line>2</line><clef-octave-change>-1</clef-octave-change></clef>', 60), ('<clef><sign>F</sign><line>4</line></clef>', 48)):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(), attributes))
|
||||
for part in ('electric', 'vocal'):
|
||||
document = generator.practice.generate.practice_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(generator.model.fraction(generator.playback.compile_timeline(document)['events'][0]['pitch']), expected)
|
||||
document = tests.arrangement_fixtures.section_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(generator.model.fraction(generator.playback.compile_timeline(document)['events'][0]['pitch']), expected + (12 if part == 'electric' else 0))
|
||||
def test_written_middle_c_and_c_sharp_six_have_matching_tab_and_audio(self):
|
||||
for octave, alter, expected, tab in ((4, 0, 48, {'string': 5, 'fret': 3}), (6, 1, 73, {'string': 1, 'fret': 9}), (7, 0, 84, {'string': 1, 'fret': 20})):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(octave=octave, alter=alter)))
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(octave=octave - 1, alter=alter)))
|
||||
document = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
tone = tests.test_musicxml.notes(document)[0]['pitches'][0]
|
||||
self.assertEqual(generator.model.pitch_number(tone), 12 * (octave + 1) + alter)
|
||||
self.assertEqual(tone['tablature'], tab)
|
||||
@@ -96,12 +95,12 @@ class GuitarTests(unittest.TestCase):
|
||||
concert = generator.pitch.concert_document(source, octave_transposing=True)
|
||||
self.assertEqual(generator.pitch.concert_document(concert, octave_transposing=True), concert)
|
||||
self.assertEqual(source, before)
|
||||
document = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual([generator.model.fraction(e['pitch']) for e in generator.playback.compile_timeline(document)['events']], [48, 60])
|
||||
def test_all_parts_share_written_octave_convention_in_songs_and_practice(self):
|
||||
document = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual([generator.model.fraction(e['pitch']) for e in generator.playback.compile_timeline(document)['events']], [60, 72])
|
||||
def test_all_parts_share_written_octave_convention(self):
|
||||
chord = '<harmony><root><root-step>C</root-step></root><kind>major</kind></harmony>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord + tests.test_musicxml.xml_note()))
|
||||
for generate in (generator.practice.generate.practice_document, generator.arrange.generate.arrange_document):
|
||||
for generate in (tests.arrangement_fixtures.section_document, generator.arrange.generate.arrange_document):
|
||||
for part in generator.parts.PART_TYPES:
|
||||
with self.subTest(generator=generate.__name__, part=part):
|
||||
document = generate(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
@@ -163,12 +162,10 @@ class GuitarTests(unittest.TestCase):
|
||||
self.assertEqual(before, document)
|
||||
def test_source_contract_and_unplayable_music_fail(self):
|
||||
cases = [(tests.test_musicxml.xml_note(octave=7, alter=1), '', '0-20'), (tests.test_musicxml.xml_note(alter='0.5'), '', '0-20'),
|
||||
(tests.test_musicxml.xml_note() + tests.test_musicxml.xml_note('E', prefix='<chord/>'), '', 'monophonic'),
|
||||
(tests.test_musicxml.xml_note() + '<backup><duration>4</duration></backup>' + tests.test_musicxml.xml_note('E', extra='<voice>2</voice>'), '', 'one monophonic voice'),
|
||||
(tests.test_musicxml.xml_note(), '<staff-details><capo>1</capo></staff-details>', 'capo')]
|
||||
for content, attributes, error in cases:
|
||||
with self.subTest(error=error), self.assertRaisesRegex(ValueError, error):
|
||||
generator.practice.generate.practice_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, attributes)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, attributes)), 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
for content, attributes in [(tests.test_musicxml.xml_note(), '<clef><sign>TAB</sign></clef>'), (tests.test_musicxml.xml_note(extra='<notations><technical><fingering>1</fingering></technical></notations>'), '')]:
|
||||
with self.assertRaisesRegex(ValueError, 'authored TAB'):
|
||||
tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, attributes))
|
||||
@@ -177,20 +174,15 @@ class GuitarTests(unittest.TestCase):
|
||||
source, output = pathlib.Path(temporary) / 'source', pathlib.Path(temporary) / 'output'
|
||||
source.mkdir()
|
||||
(source / '0000.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
|
||||
self.assertEqual(generator.practice.generate.generate_practice_scores(source, output, rhythms=tests.rhythm_fixtures.library()), 3)
|
||||
generator.summary.validate_practice_parts(output)
|
||||
self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output, rhythms=tests.rhythm_fixtures.library()), 3)
|
||||
first = {p.name: p.read_bytes() for p in output.rglob('*.json')}
|
||||
generator.practice.generate.generate_practice_scores(source, output, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.arrange.generate.generate_performance_scores(source, output, rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(first, {p.name: p.read_bytes() for p in output.rglob('*.json')})
|
||||
(output / '0000/vocal.json').unlink()
|
||||
with self.assertRaisesRegex(ValueError, 'practice generator drift'):
|
||||
generator.summary.validate_practice_parts(output)
|
||||
def test_unplayable_generation_preserves_previous_assets(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
(root / 'data/songs').mkdir(parents=True)
|
||||
(root / 'data/practice').mkdir()
|
||||
(root / 'data/practice/0001.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(octave=7, alter=1)))
|
||||
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(octave=7, alter=1)))
|
||||
output = root / 'output'
|
||||
output.mkdir()
|
||||
(output / 'last-good').write_text('preserved')
|
||||
@@ -200,7 +192,7 @@ class GuitarTests(unittest.TestCase):
|
||||
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
|
||||
self.assertEqual((output / 'last-good').read_text(), 'preserved')
|
||||
harmony = '<harmony><root><root-step>C</root-step></root><kind>dominant</kind><degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree><degree><degree-value>11</degree-value><degree-alter>1</degree-alter><degree-type>add</degree-type></degree></harmony>'
|
||||
(root / 'data/practice/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
|
||||
(root / 'data/songs/0001.xml').write_text(tests.test_musicxml.xml_score(harmony + tests.test_musicxml.xml_note()))
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0001.xml, part acoustic:.*measure 1.*no acoustic grip.*0-20'):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual([p.name for p in output.iterdir()], ['last-good'])
|
||||
|
||||
+24
-24
@@ -9,7 +9,7 @@ import generator.harmony
|
||||
import generator.model
|
||||
import generator.parts
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import generator.rhythm
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_accompaniment
|
||||
@@ -17,9 +17,9 @@ import tests.test_musicxml
|
||||
import tests.test_parts
|
||||
|
||||
class GuitarPartTests(unittest.TestCase):
|
||||
def practice(self, content, part='electric', divisions=1):
|
||||
def section(self, content, part='electric', divisions=1):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content, divisions=divisions))
|
||||
result = generator.practice.generate.practice_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
result = tests.arrangement_fixtures.section_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
return result
|
||||
def test_profile_shapes_cover_defining_chord_tones(self):
|
||||
@@ -40,7 +40,7 @@ class GuitarPartTests(unittest.TestCase):
|
||||
candidates = generator.guitar.chord_positions(*generator.guitar.chord_requirements(chord, 'electric'), profile='electric')
|
||||
self.assertTrue(any(all(p.midi % 12 != 0 for p in c) for c in candidates))
|
||||
def test_acoustic_combines_independent_rhythms_with_a_fixed_grip(self):
|
||||
result = self.practice(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
|
||||
result = self.section(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note(), 'acoustic')
|
||||
measure = result['staves'][0]['measures'][0]
|
||||
self.assertEqual(len(measure['voices']), 1)
|
||||
notes = measure['voices'][0]['notes']
|
||||
@@ -70,15 +70,15 @@ class GuitarPartTests(unittest.TestCase):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
chord = generator.harmony.parse_symbol(symbol)
|
||||
source['staves'][0]['measures'][0]['harmonies'] = [{**chord, 'id': 'h1', 'onset': generator.model.rational(0)}]
|
||||
result = generator.practice.generate.practice_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
result = tests.arrangement_fixtures.section_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual({generator.model.pitch_number(p) % 12 for n in notes for p in n['pitches'] if p['tablature']['string'] >= 5}, {expected})
|
||||
def test_acoustic_bass_and_upper_chord_can_attack_together(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
prepared = generator.parts.prepare_document(source, practice=True)
|
||||
prepared = generator.parts.prepare_document(source)
|
||||
bar = tests.test_accompaniment.quarter_bar(4, {0, 2})
|
||||
plan = generator.accompaniment.Plan((bar,), (bar,), ())
|
||||
result = generator.practice.generate._practice_document(prepared, 'acoustic', plan)
|
||||
result = tests.arrangement_fixtures.section_document(prepared, 'acoustic', rhythms=tests.rhythm_fixtures.library(), plan=plan)
|
||||
generator.model.validate_document(result)
|
||||
self.assertEqual(len(result['staves'][0]['measures'][0]['voices']), 1)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
@@ -89,15 +89,15 @@ class GuitarPartTests(unittest.TestCase):
|
||||
self.assertIn(tuple(sorted((p.string_number for p in positions), reverse=True)), generator.guitar.ACOUSTIC_STRINGS)
|
||||
self.assertTrue(generator.guitar.within_fret_span(positions))
|
||||
def test_long_melody_chords_and_short_notes(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 2, 'half', octave=5) + tests.test_musicxml.xml_note('G', 1, 'quarter', octave=5) + tests.test_musicxml.xml_note('C', 1, 'quarter', octave=6)
|
||||
notes = tests.test_musicxml.notes(self.practice(content))
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 2, 'half', octave=4) + tests.test_musicxml.xml_note('G', 1, 'quarter', octave=4) + tests.test_musicxml.xml_note('C', 1, 'quarter', octave=5)
|
||||
notes = tests.test_musicxml.notes(self.section(content))
|
||||
self.assertIn(len(notes[0]['pitches']), (3, 4))
|
||||
self.assertEqual([len(n['pitches']) for n in notes[1:]], [1, 1])
|
||||
self.assertEqual(generator.model.pitch_number(notes[0]['pitches'][0]), 76)
|
||||
self.assertIn(sorted(p['tablature']['string'] for p in notes[0]['pitches']), ([1, 2, 3], [1, 2, 3, 4]))
|
||||
def test_tied_quarters_qualify_and_keep_all_strings(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=5, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=5, extra='<tie type="stop"/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
result = self.practice(content)
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=4, extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 1, 'quarter', octave=4, extra='<tie type="stop"/>') + '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
result = self.section(content)
|
||||
first, second = tests.test_musicxml.notes(result)[:2]
|
||||
self.assertIn(len(first['pitches']), (3, 4))
|
||||
self.assertEqual(len(first['pitches']), len(second['pitches']))
|
||||
@@ -106,8 +106,8 @@ class GuitarPartTests(unittest.TestCase):
|
||||
self.assertEqual(after['tablature'], before['tablature'])
|
||||
self.assertEqual({generator.model.fraction(e['duration']) for e in generator.playback.compile_timeline(result)['events']}, {2})
|
||||
def test_chord_change_reattacks_even_for_final_eighth(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F', offset=7) + tests.test_musicxml.xml_note('G', 8, 'whole', octave=5)
|
||||
result = self.practice(content, divisions=2)
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony('F', offset=7) + tests.test_musicxml.xml_note('G', 8, 'whole', octave=4)
|
||||
result = self.section(content, divisions=2)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual([generator.model.fraction(n['duration']) for n in notes], [fractions.Fraction(7, 2), fractions.Fraction(1, 2)])
|
||||
for note in notes:
|
||||
@@ -118,10 +118,10 @@ class GuitarPartTests(unittest.TestCase):
|
||||
def test_cross_measure_ties_continue_only_without_a_chord_change(self):
|
||||
for root in ('C', 'F'):
|
||||
with self.subTest(root=root):
|
||||
first = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', octave=5, extra='<tie type="start"/>')
|
||||
second = tests.test_parts.xml_harmony(root) + tests.test_musicxml.xml_note('E', octave=5, extra='<tie type="stop"/>')
|
||||
first = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', octave=4, extra='<tie type="start"/>')
|
||||
second = tests.test_parts.xml_harmony(root) + tests.test_musicxml.xml_note('E', octave=4, extra='<tie type="stop"/>')
|
||||
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([first, second]))
|
||||
result = generator.practice.generate.practice_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
result = tests.arrangement_fixtures.section_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes']]
|
||||
self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes))
|
||||
@@ -133,16 +133,16 @@ class GuitarPartTests(unittest.TestCase):
|
||||
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes><normal-type>half</normal-type></time-modification>'
|
||||
if index != 1:
|
||||
extra += '<notations><tuplet type="' + ('start' if index == 0 else 'stop') + '"/></notations>'
|
||||
content += tests.test_musicxml.xml_note('E', 4, 'half', octave=5, extra=extra)
|
||||
result = self.practice(content, divisions=3)
|
||||
content += tests.test_musicxml.xml_note('E', 4, 'half', octave=4, extra=extra)
|
||||
result = self.section(content, divisions=3)
|
||||
notes = tests.test_musicxml.notes(result)
|
||||
self.assertEqual([generator.model.fraction(n['duration']) for n in notes], [fractions.Fraction(4, 3)] * 2 + [fractions.Fraction(2, 3)] * 2)
|
||||
self.assertEqual(result['staves'][0]['tuplets'][0]['end'], notes[-1]['id'])
|
||||
self.assertTrue(all(len(n['pitches']) in (3, 4) for n in notes))
|
||||
self.assertFalse(notes[-1]['pitches'][0].get('tie_from'))
|
||||
def test_no_chord_releases_harmony_without_reattacking_melody(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony(kind='none', offset=2) + tests.test_musicxml.xml_note('E', octave=5)
|
||||
result = self.practice(content)
|
||||
content = tests.test_parts.xml_harmony() + tests.test_parts.xml_harmony(kind='none', offset=2) + tests.test_musicxml.xml_note('E', octave=4)
|
||||
result = self.section(content)
|
||||
first, second = tests.test_musicxml.notes(result)
|
||||
self.assertIn(len(first['pitches']), (3, 4))
|
||||
self.assertEqual(len(second['pitches']), 1)
|
||||
@@ -151,20 +151,20 @@ class GuitarPartTests(unittest.TestCase):
|
||||
self.assertEqual(sum(generator.model.fraction(e['duration']) == 4 for e in timeline), 1)
|
||||
self.assertTrue(all(generator.model.fraction(e['onset']) == 0 for e in timeline))
|
||||
def test_unplayable_harmonization_preserves_low_melody(self):
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('C', octave=4)
|
||||
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('C', octave=3)
|
||||
with self.assertLogs('generator.guitar', level='WARNING'):
|
||||
result = self.practice(content)
|
||||
result = self.section(content)
|
||||
note = tests.test_musicxml.notes(result)[0]
|
||||
self.assertEqual(len(note['pitches']), 1)
|
||||
self.assertEqual(note['pitches'][0]['tablature'], {'string': 5, 'fret': 3})
|
||||
def test_song_melody_only_is_raised_an_octave(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=4)))
|
||||
before = copy.deepcopy(source)
|
||||
for builder in (generator.practice.generate.practice_document, generator.arrange.generate.arrange_document):
|
||||
for builder in (tests.arrangement_fixtures.section_document, generator.arrange.generate.arrange_document):
|
||||
for part in ('electric', 'vocal'):
|
||||
result = builder(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
notes = [n for m in result['staves'][0]['measures'] for v in m['voices'] for n in v['notes'] if n['pitches']]
|
||||
expected = 76 if part == 'electric' and builder is generator.arrange.generate.arrange_document else 64
|
||||
expected = 76 if part == 'electric' else 64
|
||||
self.assertEqual({generator.model.pitch_number(n['pitches'][0]) for n in notes}, {expected})
|
||||
self.assertEqual(source, before)
|
||||
def test_electric_stabs_are_capped_and_do_not_continue(self):
|
||||
|
||||
+5
-12
@@ -10,7 +10,7 @@ import generator.model
|
||||
import generator.musicxml
|
||||
import generator.musicxml_writer
|
||||
import generator.pitch
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
|
||||
@@ -58,9 +58,9 @@ class HarmonyTests(unittest.TestCase):
|
||||
self.assertEqual(chord['symbol'], label)
|
||||
self.assertTrue(chord['chord_optional'])
|
||||
self.assertTrue(chord['parenthesized'])
|
||||
practice = generator.practice.generate.practice_document(source, 'vocal', rhythms=tests.rhythm_fixtures.library())['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertNotIn('chord_optional', practice)
|
||||
self.assertEqual(generator.harmony.symbol(practice), label)
|
||||
arranged = tests.arrangement_fixtures.section_document(source, 'vocal', rhythms=tests.rhythm_fixtures.library())['staves'][0]['measures'][0]['harmonies'][0]
|
||||
self.assertNotIn('chord_optional', arranged)
|
||||
self.assertEqual(generator.harmony.symbol(arranged), label)
|
||||
source['staves'][0]['measures'][0]['transpose'] = {'diatonic': 1, 'chromatic': 2, 'octave_change': 0}
|
||||
shifted = generator.pitch.concert_document(source)
|
||||
result = shifted['staves'][0]['measures'][0]['harmonies'][0]
|
||||
@@ -90,14 +90,7 @@ class HarmonyTests(unittest.TestCase):
|
||||
self.assertEqual(generator.harmony.symbol(chord), 'Cm7')
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(chord_xml('7', 'dominant', extra='<degree><degree-value>9</degree-value><degree-alter>-1</degree-alter><degree-type>add</degree-type></degree>') + tests.test_musicxml.xml_note()))
|
||||
self.assertEqual(source['staves'][0]['measures'][0]['harmonies'][0]['symbol'], 'C7b9')
|
||||
def test_current_exercise_and_song_exports_import_without_edits(self):
|
||||
exercise = generator.musicxml.load_musicxml(pathlib.Path('data/practice/0001.xml'))
|
||||
measures = exercise['staves'][0]['measures']
|
||||
self.assertEqual(len(measures), 20)
|
||||
self.assertEqual(sum(len(n['pitches']) for m in measures for v in m['voices'] for n in v['notes']), 70)
|
||||
self.assertEqual(measures[1]['harmonies'][0]['symbol'], 'Bm9')
|
||||
self.assertEqual(measures[3]['harmonies'][0]['symbol'], 'DmM7')
|
||||
self.assertEqual(measures[0]['voices'][0]['notes'][0]['duration'], generator.model.rational(1))
|
||||
def test_current_song_exports_import_without_edits(self):
|
||||
for name, count, key in (('autumn-leaves', 33, 'F'), ('it-could-happen-to-you', 32, 'Eb')):
|
||||
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs') / f'{name}.xml')
|
||||
self.assertEqual(len(source['staves'][0]['measures']), count)
|
||||
|
||||
+3
-13
@@ -189,11 +189,6 @@ auto make_assets(vamp_zero::config const &config) -> void {
|
||||
auto root = std::filesystem::path{config.runtime.assets_path};
|
||||
write_score(root / "songs" / "first.json", root);
|
||||
write_score(root / "songs" / "second.json", root);
|
||||
for (auto part_name : vamp_zero::practice_part_names()) {
|
||||
auto name = std::string{part_name};
|
||||
name.front() = static_cast<std::char_t>(name.front() - 'A' + 'a');
|
||||
write_score(root / "practice" / "0001" / (name + ".json"), root, std::string{part_name});
|
||||
}
|
||||
BOOST_REQUIRE(vamp_test::build_catalog(root, config.database.path));
|
||||
}
|
||||
|
||||
@@ -359,16 +354,11 @@ BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartb
|
||||
BOOST_TEST(!std::filesystem::exists(config.runtime.heartbeat_path));
|
||||
require_no_probe(config);
|
||||
auto io = boost::asio::io_context{};
|
||||
auto performance = boost::asio::co_spawn(io, vamp_zero::async_load_performance_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||
auto practice = boost::asio::co_spawn(io, vamp_zero::async_load_practice_library(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||
auto performance = boost::asio::co_spawn(io, vamp_zero::async_load_catalog(io.get_executor(), config.database, config.runtime.assets_path), boost::asio::use_future);
|
||||
io.run();
|
||||
auto songs = performance.get();
|
||||
auto exercises = practice.get();
|
||||
BOOST_REQUIRE(songs);
|
||||
BOOST_REQUIRE(exercises);
|
||||
BOOST_TEST(songs->parts.size() == 2U);
|
||||
BOOST_REQUIRE_EQUAL(exercises->exercises.size(), 1U);
|
||||
BOOST_TEST(exercises->exercises.front().parts.size() == 3U);
|
||||
write_file(config.runtime.heartbeat_path, "existing heartbeat");
|
||||
auto heartbeat_time = std::filesystem::last_write_time(config.runtime.heartbeat_path);
|
||||
config.runtime.exit_after_startup = true;
|
||||
@@ -380,7 +370,7 @@ BOOST_AUTO_TEST_CASE(readiness_reads_packaged_catalog_without_hardware_or_heartb
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(readiness_validates_later_pages_and_parts) {
|
||||
for (std::char_t const *relative : {"songs/first.pages/2.png", "songs/second.pages/2.png", "practice/0001/electric.pages/2.png"}) {
|
||||
for (std::char_t const *relative : {"songs/first.pages/2.png", "songs/second.pages/2.png"}) {
|
||||
auto root = temporary_directory{};
|
||||
auto config = fixture_config(root.path);
|
||||
make_assets(config);
|
||||
@@ -422,7 +412,7 @@ BOOST_AUTO_TEST_CASE(readiness_validates_generated_audio_using_configured_asset_
|
||||
auto assets = std::filesystem::path{config.runtime.assets_path};
|
||||
auto fixture = std::filesystem::path{VAMP_TEST_PLAYBACK_ASSETS_DIR};
|
||||
std::filesystem::copy(fixture / "audio", assets / "audio", std::filesystem::copy_options::recursive);
|
||||
auto path = assets / "practice/0001/acoustic.json";
|
||||
auto path = assets / "songs/acoustic.json";
|
||||
write_score(path, assets, "Acoustic");
|
||||
auto score = boost::json::parse(read_file(path));
|
||||
score.as_object()["playback"] = boost::json::parse(read_file(fixture / "score.json")).at("playback");
|
||||
|
||||
@@ -91,7 +91,7 @@ class InstallZeroTests(unittest.TestCase):
|
||||
def stale_state(self):
|
||||
for name in ('etc/product-vamp/vamp.json', 'etc/product-vamp/obsolete', 'var/lib/product-vamp/vamp.db',
|
||||
'var/lib/product-vamp/vamp.db-wal', 'var/lib/product-vamp/vamp.db-shm', 'run/product-vamp/heartbeat.json',
|
||||
'usr/share/product-vamp/songs/removed.json', 'etc/systemd/system/product-vamp-zero.service.d/override.conf',
|
||||
'usr/share/product-vamp/songs/removed.json', 'usr/share/product-vamp/practice/0001/electric.json', 'etc/systemd/system/product-vamp-zero.service.d/override.conf',
|
||||
'run/systemd/system/product-vamp-ap.service.d/override.conf'):
|
||||
path = self.root / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -59,7 +59,7 @@ class MusicModelTests(unittest.TestCase):
|
||||
change = '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>'
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=1, kind='quarter') + change + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')))
|
||||
measure = source['staves'][0]['measures'][0]
|
||||
events = generator.parts.generated_voice(measure, 'slash', None)
|
||||
events = generator.parts.generated_voice(measure, 'slash')
|
||||
self.assertEqual([generator.model.fraction(n['onset']) for n in events], [0, 1, fractions.Fraction(5, 2)])
|
||||
self.assertEqual([generator.model.fraction(n['duration']) for n in events], [1, fractions.Fraction(3, 2), fractions.Fraction(3, 2)])
|
||||
def test_harmony_carry_uses_last_event_and_never_future_event(self):
|
||||
@@ -107,7 +107,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
root = pathlib.Path(temp)
|
||||
data = root / 'data'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
(data / 'practice').mkdir()
|
||||
(data / 'songs' / 'bad.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=5)), encoding='utf-8')
|
||||
output = root / 'output'
|
||||
output.mkdir()
|
||||
@@ -132,7 +131,6 @@ class MusicModelTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
data, output = pathlib.Path(temp) / 'data', pathlib.Path(temp) / 'output'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
(data / 'practice').mkdir()
|
||||
source = data / 'songs' / 'fixture.xml'
|
||||
source.write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()), encoding='utf-8')
|
||||
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False)
|
||||
|
||||
@@ -62,7 +62,7 @@ class MusicXMLTests(unittest.TestCase):
|
||||
self.assertEqual(len(score['staves'][0]['measures']), 4)
|
||||
self.assertEqual(len(score['staves'][0]['octave_shifts']), 1)
|
||||
def test_catalog_imports(self):
|
||||
sources = sorted(path for category in ('practice', 'songs') for path in (pathlib.Path('data') / category).glob('*.xml'))
|
||||
sources = sorted(pathlib.Path('data/songs').glob('*.xml'))
|
||||
self.assertTrue(sources)
|
||||
for path in sources:
|
||||
with self.subTest(path=path):
|
||||
|
||||
+33
-32
@@ -16,7 +16,7 @@ import generator.musicxml
|
||||
import generator.parts
|
||||
import generator.pitch
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import tests.rhythm_fixtures
|
||||
import tests.test_musicxml
|
||||
|
||||
@@ -33,7 +33,7 @@ class PartGenerationTests(unittest.TestCase):
|
||||
self.assertEqual(cursor, generator.model.fraction(measure['duration']))
|
||||
def test_dense_melody_keeps_rhythmic_accompaniment_without_sustaining_across_changes(self):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + xml_harmony('F', offset=3) + ''.join(tests.test_musicxml.xml_note(duration=1, kind='eighth') for _ in range(8)), divisions=2))
|
||||
for result in (generator.practice.generate.practice_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library()), generator.arrange.generate.arrange_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())):
|
||||
for result in (tests.arrangement_fixtures.section_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library()), generator.arrange.generate.arrange_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())):
|
||||
generator.model.validate_document(result)
|
||||
for measure in result['staves'][0]['measures']:
|
||||
events = measure['voices'][0]['notes']
|
||||
@@ -52,7 +52,7 @@ class PartGenerationTests(unittest.TestCase):
|
||||
def test_harmony_carry_and_explicit_silence(self):
|
||||
content = xml_harmony(offset=1) + xml_harmony(kind='none', offset=2) + xml_harmony('F', offset=3) + tests.test_musicxml.xml_note()
|
||||
source = tests.test_musicxml.xml_score(content).replace('</measure></part>', '</measure><measure number="2">' + tests.test_musicxml.xml_note() + '</measure><measure number="3">' + xml_harmony(kind='none') + tests.test_musicxml.xml_note() + '</measure><measure number="4">' + tests.test_musicxml.xml_note() + '</measure></part>')
|
||||
result = generator.practice.generate.practice_document(tests.test_musicxml.parse(source), 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
result = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(source), 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
for measure in result['staves'][0]['measures']:
|
||||
self.assert_complete_measure(measure)
|
||||
sounding = [n for n in tests.test_musicxml.notes(result) if n['pitches']]
|
||||
@@ -80,9 +80,11 @@ class PartGenerationTests(unittest.TestCase):
|
||||
self.assertTrue(all(n['_harmony']['root']['letter'] == 'f' for n in sounding))
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + tests.test_musicxml.xml_note(duration=1, kind='quarter') + '<attributes><time><beats>6</beats><beat-type>8</beat-type></time></attributes>' + tests.test_musicxml.xml_note(duration=3, kind='half', extra='<dot/>')))
|
||||
with self.assertRaisesRegex(ValueError, 'mid-measure meter'):
|
||||
generator.practice.generate.practice_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
def test_exercise_006_has_accompaniment_in_every_measure(self):
|
||||
result = generator.practice.generate.practice_document(generator.musicxml.load_musicxml(pathlib.Path('data/practice/0006.xml')), 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
tests.arrangement_fixtures.section_document(source, 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
def test_dense_melody_has_accompaniment_in_every_measure(self):
|
||||
bar = xml_harmony() + ''.join(tests.test_musicxml.xml_note(duration=1, kind='quarter') for _ in range(4))
|
||||
xml = tests.test_musicxml.xml_score(bar).replace('</part>', ''.join(f'<measure number="{i}">{bar}</measure>' for i in range(2, 13)) + '</part>')
|
||||
result = tests.arrangement_fixtures.section_document(tests.test_musicxml.parse(xml), 'acoustic', rhythms=tests.rhythm_fixtures.library())
|
||||
self.assertEqual(len(result['staves'][0]['measures']), 12)
|
||||
for measure in result['staves'][0]['measures']:
|
||||
self.assertTrue(any(n['pitches'] for v in measure['voices'] for n in v['notes']), measure['id'])
|
||||
@@ -97,22 +99,21 @@ class PartGenerationTests(unittest.TestCase):
|
||||
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
source['staves'][0]['octave_shifts'] = [{'id': 'octave', 'start': generator.model.rational(0), 'end': generator.model.rational(4), 'octaves': -1}]
|
||||
original = copy.deepcopy(source)
|
||||
for builder, wrapper, practice in ((generator.practice.generate._practice_document, generator.practice.generate.practice_document, True), (generator.arrange.generate._arrange_document, generator.arrange.generate.arrange_document, False)):
|
||||
prepared = generator.parts.prepare_document(source, practice=practice)
|
||||
before = copy.deepcopy(prepared)
|
||||
plan = generator.accompaniment.plan_section(prepared['staves'][0]['measures'], tests.rhythm_fixtures.library(), electric=False) if practice else generator.arrange.generate.plan_arrangement(prepared, tests.rhythm_fixtures.library())
|
||||
plan_before = copy.deepcopy(plan)
|
||||
parts = [builder(prepared, part, plan) for part in generator.parts.PART_TYPES]
|
||||
self.assertEqual(prepared, before)
|
||||
self.assertEqual(plan, plan_before)
|
||||
self.assertEqual(parts, [wrapper(source, part, rhythms=tests.rhythm_fixtures.library()) for part in generator.parts.PART_TYPES])
|
||||
vocal = copy.deepcopy(parts[2])
|
||||
parts[1]['metadata']['title'] = 'changed'
|
||||
tests.test_musicxml.notes(parts[1])[0]['pitches'].clear()
|
||||
if parts[1]['staves'][0]['octave_shifts']:
|
||||
parts[1]['staves'][0]['octave_shifts'][0]['octaves'] = 9
|
||||
self.assertEqual(parts[2], vocal)
|
||||
self.assertEqual(prepared, before)
|
||||
prepared = generator.parts.prepare_document(source)
|
||||
before = copy.deepcopy(prepared)
|
||||
plan = generator.arrange.generate.plan_arrangement(prepared, tests.rhythm_fixtures.library())
|
||||
plan_before = copy.deepcopy(plan)
|
||||
parts = [generator.arrange.generate._arrange_document(prepared, part, plan) for part in generator.parts.PART_TYPES]
|
||||
self.assertEqual(prepared, before)
|
||||
self.assertEqual(plan, plan_before)
|
||||
self.assertEqual(parts, [generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library()) for part in generator.parts.PART_TYPES])
|
||||
vocal = copy.deepcopy(parts[2])
|
||||
parts[1]['metadata']['title'] = 'changed'
|
||||
tests.test_musicxml.notes(parts[1])[0]['pitches'].clear()
|
||||
if parts[1]['staves'][0]['octave_shifts']:
|
||||
parts[1]['staves'][0]['octave_shifts'][0]['octaves'] = 9
|
||||
self.assertEqual(parts[2], vocal)
|
||||
self.assertEqual(prepared, before)
|
||||
self.assertEqual(source, original)
|
||||
def test_melody_written_pitch_rhythm_rests_and_cross_bar_ties_are_preserved(self):
|
||||
content = ''
|
||||
@@ -126,13 +127,13 @@ class PartGenerationTests(unittest.TestCase):
|
||||
source = tests.test_musicxml.parse(xml.etree.ElementTree.tostring(root, encoding='unicode'))
|
||||
before = copy.deepcopy(source)
|
||||
expected = [n for m in source['staves'][0]['measures'] for n in m['voices'][0]['notes']]
|
||||
for builder in (generator.practice.generate.practice_document, generator.arrange.generate.arrange_document):
|
||||
for builder in (tests.arrangement_fixtures.section_document, generator.arrange.generate.arrange_document):
|
||||
for part in ('electric', 'vocal'):
|
||||
with self.subTest(builder=builder.__module__, part=part):
|
||||
result = builder(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
generator.model.validate_document(result)
|
||||
measures = result['staves'][0]['measures']
|
||||
starts = [0] if builder is generator.practice.generate.practice_document else [i for i, m in enumerate(measures) if m.get('section_label') == ('Guitar Melody' if part == 'electric' else 'Vocal Melody')]
|
||||
starts = [0] if builder is tests.arrangement_fixtures.section_document else [i for i, m in enumerate(measures) if m.get('section_label') == ('Guitar Melody' if part == 'electric' else 'Vocal Melody')]
|
||||
self.assertTrue(starts)
|
||||
for start in starts:
|
||||
notes = [n for m in measures[start:start + 2] for n in m['voices'][0]['notes']]
|
||||
@@ -142,16 +143,16 @@ class PartGenerationTests(unittest.TestCase):
|
||||
self.assertEqual(generated[key], original[key])
|
||||
self.assertEqual(len(generated['pitches']), len(original['pitches']))
|
||||
for pitch, tone in zip(original['pitches'], generated['pitches']):
|
||||
octave = 12 if part == 'electric' and builder is generator.arrange.generate.arrange_document else 0
|
||||
octave = 12 if part == 'electric' else 0
|
||||
self.assertEqual(generator.model.pitch_number(tone), generator.model.pitch_number(pitch) + octave)
|
||||
self.assertEqual(tone['written'], pitch['written'])
|
||||
for key in ('tie_from', 'tie_to'):
|
||||
self.assertEqual(key in tone, key in pitch)
|
||||
self.assertEqual(notes[0]['pitches'][0]['tie_to'], notes[1]['pitches'][0]['id'])
|
||||
self.assertEqual(notes[1]['pitches'][0]['tie_to'], notes[2]['pitches'][0]['id'])
|
||||
if builder is generator.practice.generate.practice_document:
|
||||
if builder is tests.arrangement_fixtures.section_document:
|
||||
events = generator.playback.compile_timeline(result)['events']
|
||||
self.assertEqual([(generator.model.fraction(e['onset']), generator.model.fraction(e['duration']), generator.model.fraction(e['pitch'])) for e in events], [(0, 6, 49), (fractions.Fraction(13, 2), fractions.Fraction(3, 2), 51)])
|
||||
self.assertEqual([(generator.model.fraction(e['onset']), generator.model.fraction(e['duration']), generator.model.fraction(e['pitch'])) for e in events], [(0, 6, 61 if part == 'electric' else 49), (fractions.Fraction(13, 2), fractions.Fraction(3, 2), 63 if part == 'electric' else 51)])
|
||||
self.assertEqual(source, before)
|
||||
def test_song_slug_collisions_fail_before_writes(self):
|
||||
for names in (('one-two', 'one_two'), ('Song', 'song'), ('!!!', '???')):
|
||||
@@ -171,8 +172,8 @@ class PartGenerationTests(unittest.TestCase):
|
||||
def test_pipeline_parses_and_normalizes_each_source_once(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
for category, name in (('songs', 'song'), ('practice', '0001')):
|
||||
(root / 'data' / category).mkdir(parents=True)
|
||||
for category, name in (('songs', 'song'), ('songs', 'other')):
|
||||
(root / 'data' / category).mkdir(parents=True, exist_ok=True)
|
||||
(root / 'data' / category / (name + '.xml')).write_text(tests.test_musicxml.xml_score(xml_harmony() + tests.test_musicxml.xml_note()))
|
||||
args = argparse.Namespace(data_dir=root / 'data', output_dir=root / 'output', static_assets_dir=pathlib.Path('package/assets'), check=False)
|
||||
with (
|
||||
@@ -186,11 +187,11 @@ class PartGenerationTests(unittest.TestCase):
|
||||
):
|
||||
generator.__main__.generate_assets(args)
|
||||
load_library.assert_called_once_with(args.data_dir.resolve() / 'rhythms')
|
||||
self.assertEqual(load.call_args_list, [unittest.mock.call(args.data_dir.resolve() / 'songs/song.xml'), unittest.mock.call(args.data_dir.resolve() / 'practice/0001.xml')])
|
||||
self.assertEqual(load.call_args_list, [unittest.mock.call(args.data_dir.resolve() / 'songs/other.xml'), unittest.mock.call(args.data_dir.resolve() / 'songs/song.xml')])
|
||||
self.assertEqual(normalize.call_count, 2)
|
||||
original = {p.relative_to(args.output_dir): p.read_bytes() for p in args.output_dir.rglob('*') if p.is_file()}
|
||||
(root / 'data/practice/0002.xml').write_text('<broken')
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, '0002.xml'):
|
||||
(root / 'data/songs/broken.xml').write_text('<broken')
|
||||
with unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()), self.assertRaisesRegex(ValueError, 'broken.xml'):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(args.output_dir): p.read_bytes() for p in args.output_dir.rglob('*') if p.is_file()})
|
||||
self.assertEqual(len(list(args.output_dir.rglob('*.json'))), 6)
|
||||
|
||||
@@ -23,8 +23,8 @@ auto playback_score() -> vamp_zero::score_document {
|
||||
|
||||
auto performance_state() -> vamp_zero::app_state {
|
||||
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}});
|
||||
catalog->parts.push_back({.id = "part", .title = "Song", .part = "Vocal", .total_pages = 2U});
|
||||
catalog->groups.push_back({.title = "Song", .parts = {0U}});
|
||||
auto state = vamp_zero::app_state{};
|
||||
state.catalog = std::move(catalog);
|
||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
|
||||
|
||||
@@ -52,7 +52,7 @@ struct artwork_fixture {
|
||||
}
|
||||
auto state() const -> vamp_zero::app_state {
|
||||
auto catalog = std::make_shared<vamp_zero::catalog>();
|
||||
catalog->performance.parts.push_back({.id = "part", .title = "Title", .part = "Acoustic", .total_pages = 10000U, .title_image = identifier});
|
||||
catalog->parts.push_back({.id = "part", .title = "Title", .part = "Acoustic", .total_pages = 10000U, .title_image = identifier});
|
||||
auto value = vamp_zero::app_state{};
|
||||
value.catalog = catalog;
|
||||
value.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import copy
|
||||
import fractions
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
import generator.engraving.fonts
|
||||
import generator.engraving.layout
|
||||
import generator.engraving.notation
|
||||
import generator.engraving.types
|
||||
import generator.model
|
||||
import generator.playback
|
||||
import generator.scat
|
||||
import tests.test_musicxml
|
||||
|
||||
class ScatTests(unittest.TestCase):
|
||||
def test_syllables_use_duration_pulse_phrase_and_previous_consonant(self):
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(2), fractions.Fraction(1), False, True, ''), 'doo')
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(1, 2), fractions.Fraction(1), True, False, ''), 'ba')
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(1, 2), fractions.Fraction(1), False, False, ''), 'dit')
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(1, 2), fractions.Fraction(1), False, False, 'dit'), 'da')
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(1, 2), fractions.Fraction(1), True, True, ''), 'dat')
|
||||
self.assertEqual(generator.scat.syllable(fractions.Fraction(1, 2), None, False, False, ''), 'ba')
|
||||
def test_compound_additive_meter_and_pickups(self):
|
||||
measure = {'meter': {'groups': [{'beats': [6], 'unit': 8}]}, 'duration': generator.model.rational(3)}
|
||||
self.assertEqual(generator.scat.pulse(measure, fractions.Fraction(3, 2)), (fractions.Fraction(3, 2), True))
|
||||
measure['meter']['groups'][0]['beats'] = [3, 2]
|
||||
self.assertEqual(generator.scat.pulse(measure, fractions.Fraction(3, 2)), (fractions.Fraction(1), True))
|
||||
measure.update(implicit=True, duration=generator.model.rational(fractions.Fraction(1, 2)))
|
||||
self.assertEqual(generator.scat.pulse(measure, fractions.Fraction()), (fractions.Fraction(1), False))
|
||||
def test_ties_sustain_one_syllable_and_stop_before_rests(self):
|
||||
content = tests.test_musicxml.xml_note(duration=1, kind='quarter', extra='<tie type="start"/>') + tests.test_musicxml.xml_note(duration=1, kind='quarter', extra='<tie type="stop"/>')
|
||||
content += '<note><rest/><duration>2</duration><type>half</type></note>'
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
|
||||
original = copy.deepcopy(document)
|
||||
before = generator.playback.compile_timeline(document)
|
||||
generator.scat.add_scat(document)
|
||||
self.assertEqual(generator.playback.compile_timeline(document), before)
|
||||
self.assertEqual(tests.test_musicxml.notes(document)[0]['lyric'], 'doo')
|
||||
self.assertEqual(tests.test_musicxml.notes(document)[0]['lyrics'][0]['extend_to'], generator.model.rational(2))
|
||||
self.assertNotIn('lyric', tests.test_musicxml.notes(document)[1])
|
||||
self.assertNotIn('lyrics', tests.test_musicxml.notes(document)[2])
|
||||
again = copy.deepcopy(original)
|
||||
generator.scat.add_scat(again)
|
||||
self.assertEqual(document, again)
|
||||
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
|
||||
layout = generator.engraving.layout.layout_score(document, generator.engraving.types.Style(), fonts)
|
||||
system = layout.pages[0].systems[0]
|
||||
rest = next(a.x for a in system.anchors if a.fragment.kind == 'rest')
|
||||
self.assertTrue(all(p.box.right < rest for p in system.primitives if p.role == 'lyric-extension'))
|
||||
def test_cross_bar_tie_has_one_lyric_and_page_continuations(self):
|
||||
text = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(extra='<tie type="start"/>'))
|
||||
text = text.replace('</measure></part>', '</measure><measure number="2">' + tests.test_musicxml.xml_note(extra='<tie type="stop"/>') + '</measure></part>')
|
||||
document = tests.test_musicxml.parse(text)
|
||||
generator.scat.add_scat(document)
|
||||
self.assertEqual(sum(bool(n.get('lyric')) for n in tests.test_musicxml.notes(document)), 1)
|
||||
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
|
||||
for start, end in ((0, 4), (4, 8)):
|
||||
system = generator.engraving.layout.make_system(generator.engraving.notation.ScoreIndex(document), start, end, fonts, generator.engraving.types.Style())
|
||||
self.assertTrue(any(p.role == 'lyric-extension' for p in system.primitives))
|
||||
def test_long_tie_chain_draws_one_extender_per_system(self):
|
||||
text = tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(extra='<tie type="start"/>'))
|
||||
text = text.replace('</measure></part>', '</measure><measure number="2">' + tests.test_musicxml.xml_note(extra='<tie type="stop"/><tie type="start"/>') + '</measure><measure number="3">' + tests.test_musicxml.xml_note(extra='<tie type="stop"/>') + '</measure></part>')
|
||||
document = tests.test_musicxml.parse(text)
|
||||
generator.scat.add_scat(document)
|
||||
fonts = generator.engraving.fonts.Fonts(pathlib.Path('package/assets/fonts/petaluma'))
|
||||
index = generator.engraving.notation.ScoreIndex(document)
|
||||
for start, end in ((0, 4), (4, 8), (8, 12), (0, 12)):
|
||||
system = generator.engraving.layout.make_system(index, start, end, fonts, generator.engraving.types.Style())
|
||||
self.assertEqual(sum(p.role == 'lyric-extension' for p in system.primitives), 1)
|
||||
def test_straight_offbeat_attacks_and_cues(self):
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(''.join(tests.test_musicxml.xml_note(duration=1, kind='eighth') for _ in range(8)), divisions=2))
|
||||
before = generator.playback.compile_timeline(document)
|
||||
generator.scat.add_scat(document)
|
||||
self.assertEqual([n['lyric'] for n in tests.test_musicxml.notes(document)], ['ba', 'dit', 'ba', 'dit', 'ba', 'dit', 'ba', 'dat'])
|
||||
self.assertEqual(generator.playback.compile_timeline(document), before)
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(prefix='<cue/>')))
|
||||
generator.scat.add_scat(document)
|
||||
self.assertNotIn('lyrics', tests.test_musicxml.notes(document)[0])
|
||||
+11
-18
@@ -26,7 +26,7 @@ import generator.musicxml_writer
|
||||
import generator.pico_artwork
|
||||
import generator.pitch
|
||||
import generator.playback
|
||||
import generator.practice.generate
|
||||
import tests.arrangement_fixtures
|
||||
import generator.score
|
||||
import tests.musicxml_roundtrip
|
||||
import tests.rhythm_fixtures
|
||||
@@ -39,19 +39,12 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
root = pathlib.Path(temporary)
|
||||
data, output = root / 'data', root / 'output'
|
||||
(data / 'songs').mkdir(parents=True)
|
||||
(data / 'practice').mkdir()
|
||||
source = data / 'practice/0001.xml'
|
||||
source = data / 'songs/fixture.xml'
|
||||
source.write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
args = argparse.Namespace(data_dir=data, output_dir=output, static_assets_dir=pathlib.Path('package/assets'), check=False)
|
||||
generator.__main__.generate_assets(args)
|
||||
original = {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()}
|
||||
self.assertTrue(list(output.rglob('*.png')))
|
||||
legacy_source = source.with_name('001.xml')
|
||||
source.rename(legacy_source)
|
||||
with self.assertRaisesRegex(ValueError, 'four-digit'):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||
legacy_source.rename(source)
|
||||
(data / 'rhythms' / 'drafts').mkdir(parents=True)
|
||||
for path in ('rhythms/0001.xml', 'rhythms/drafts/0002.xml'):
|
||||
(data / path).write_text('<unfinished')
|
||||
@@ -75,16 +68,16 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||
source.write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
renamed = data / 'practice/0127.xml'
|
||||
renamed = data / 'songs/renamed.xml'
|
||||
source.rename(renamed)
|
||||
source = renamed
|
||||
legacy_output = output / 'practice/001'
|
||||
legacy_output = output / 'obsolete'
|
||||
legacy_output.mkdir()
|
||||
(legacy_output / 'electric.json').write_text('{}')
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertFalse(legacy_output.exists())
|
||||
self.assertFalse((output / 'practice/0001').exists())
|
||||
self.assertEqual(len(list((output / 'practice/0127').glob('*.json'))), 3)
|
||||
self.assertFalse(list((output / 'songs').glob('fixture*')))
|
||||
self.assertEqual(len(list((output / 'songs').glob('renamed-*.json'))), 3)
|
||||
source.unlink()
|
||||
generator.__main__.generate_assets(args)
|
||||
self.assertFalse(list(output.rglob('*.png')))
|
||||
@@ -170,7 +163,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(), attributes='<clef><sign>TAB</sign></clef>'))
|
||||
for part in ('vocal', 'acoustic', 'electric'):
|
||||
with self.assertRaisesRegex(ValueError, 'pitched'):
|
||||
generator.practice.generate.practice_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
tests.arrangement_fixtures.section_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
with self.assertRaisesRegex(ValueError, 'pitched'):
|
||||
generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library())
|
||||
def test_percussion_instrument_definitions_round_trip_and_are_audited(self):
|
||||
@@ -192,7 +185,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'unknown score instrument'):
|
||||
generator.model.validate_document(source)
|
||||
def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self):
|
||||
for path in sorted(path for category in ('practice', 'songs') for path in (pathlib.Path('data') / category).glob('*.xml')):
|
||||
for path in sorted(pathlib.Path('data/songs').glob('*.xml')):
|
||||
with self.subTest(path=path):
|
||||
source = generator.musicxml.load_musicxml(path)
|
||||
restored = self.round_trip(source)
|
||||
@@ -281,12 +274,12 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
root = pathlib.Path(temporary)
|
||||
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
|
||||
for index in range(3):
|
||||
generator.score.write_json(root / 'practice' / f'{index}.json', document)
|
||||
generator.score.write_json(root / 'songs' / f'{index}.json', document)
|
||||
trusted = generator.engraving.pipeline.engrave_tree(root, pathlib.Path('package/assets/fonts/petaluma'))
|
||||
generator.pico_artwork.generate_titles(root, pathlib.Path('package/assets/fonts/petaluma'))
|
||||
with unittest.mock.patch('generator.engraving.validation.check_content', side_effect=AssertionError('already validated content')):
|
||||
generator.score.validate_generated_tree(root, trusted)
|
||||
page = root / 'practice/2.pages/0001.png'
|
||||
page = root / 'songs/2.pages/0001.png'
|
||||
original = page.read_bytes()
|
||||
page.write_bytes(original + b'changed')
|
||||
with self.assertRaisesRegex(ValueError, 'checksum'):
|
||||
@@ -296,7 +289,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
generator.score.validate_generated_tree(root)
|
||||
decode.assert_called_once()
|
||||
PIL.Image.new('RGB', (976, 484), 'white').save(page)
|
||||
path = root / 'practice/2.json'
|
||||
path = root / 'songs/2.json'
|
||||
bundle = json.loads(path.read_text())
|
||||
bundle['presentation']['pages'][0]['sha256'] = hashlib.sha256(page.read_bytes()).hexdigest()
|
||||
path.write_text(json.dumps(bundle))
|
||||
|
||||
@@ -70,7 +70,7 @@ class PicoArtworkTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
paths = []
|
||||
for category, part in (('songs', 'Acoustic'), ('songs', 'Vocal'), ('practice', 'Electric')):
|
||||
for category, part in (('songs', 'Acoustic'), ('songs', 'Vocal'), ('songs', 'Electric')):
|
||||
path = root / category / f'{part}.json'
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps({'music': {'metadata': {'title': 'Evening Song'}, 'staves': [{'name': part}]},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
namespace {
|
||||
|
||||
auto performance(std::string id) -> vamp_zero::score_reference {
|
||||
return {.category = vamp_zero::score_category::PERFORMANCE, .id = std::move(id)};
|
||||
return {.id = std::move(id)};
|
||||
}
|
||||
|
||||
auto drain(boost::asio::io_context &io) -> void {
|
||||
@@ -147,7 +147,7 @@ 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_catalog_identity_and_category_invalidate_selection) {
|
||||
BOOST_AUTO_TEST_CASE(score_loader_catalog_identity_invalidates_same_score_selection) {
|
||||
auto fixture = loader_fixture{};
|
||||
fixture.loader.select(fixture.catalog, performance("same-id"));
|
||||
drain(fixture.io);
|
||||
@@ -160,13 +160,6 @@ BOOST_AUTO_TEST_CASE(score_loader_catalog_identity_and_category_invalidate_selec
|
||||
fixture.scores->complete(1);
|
||||
drain(fixture.io);
|
||||
BOOST_REQUIRE(fixture.loader.snapshot().document);
|
||||
fixture.loader.select(fixture.catalog, vamp_zero::score_reference{.category = vamp_zero::score_category::PRACTICE, .id = "same-id"});
|
||||
BOOST_CHECK(!fixture.loader.snapshot().document);
|
||||
drain(fixture.io);
|
||||
BOOST_REQUIRE_EQUAL(fixture.scores->requests.size(), 3U);
|
||||
BOOST_CHECK(fixture.scores->requests.back().category == vamp_zero::score_category::PRACTICE);
|
||||
fixture.scores->complete(2);
|
||||
drain(fixture.io);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_loader_reuses_active_score_and_releases_on_menu_exit) {
|
||||
@@ -316,10 +309,9 @@ BOOST_AUTO_TEST_CASE(score_loader_completion_can_overlap_stop_and_consumer_destr
|
||||
BOOST_CHECK(entered_status == std::future_status::ready);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(selected_score_reference_uses_active_part_and_practice_part) {
|
||||
BOOST_AUTO_TEST_CASE(selected_score_reference_uses_active_part) {
|
||||
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"}}}};
|
||||
catalog->parts = {{.id = "part-a"}, {.id = "part-b"}};
|
||||
auto state = vamp_zero::app_state{.catalog = catalog};
|
||||
BOOST_CHECK(!vamp_zero::selected_score_reference(state));
|
||||
state.mode = vamp_zero::app_mode::PERFORMANCE_SCORE;
|
||||
@@ -327,15 +319,7 @@ BOOST_AUTO_TEST_CASE(selected_score_reference_uses_active_part_and_practice_part
|
||||
auto reference = vamp_zero::selected_score_reference(state);
|
||||
BOOST_REQUIRE(reference);
|
||||
BOOST_CHECK_EQUAL(reference->id, "part-b");
|
||||
BOOST_CHECK(reference->category == vamp_zero::score_category::PERFORMANCE);
|
||||
state.mode = vamp_zero::app_mode::PRACTICE_SCORE;
|
||||
state.active_practice_id = "scale";
|
||||
state.selected_practice_part_index = 2;
|
||||
reference = vamp_zero::selected_score_reference(state);
|
||||
BOOST_REQUIRE(reference);
|
||||
BOOST_CHECK_EQUAL(reference->id, "scale-d");
|
||||
BOOST_CHECK(reference->category == vamp_zero::score_category::PRACTICE);
|
||||
state.selected_practice_part_index = 0;
|
||||
state.active_part_id = "missing";
|
||||
BOOST_CHECK(!vamp_zero::selected_score_reference(state));
|
||||
state.catalog.reset();
|
||||
BOOST_CHECK(!vamp_zero::selected_score_reference(state));
|
||||
|
||||
Reference in New Issue
Block a user