+20
-3
@@ -38,6 +38,7 @@
|
||||
#include <vamp_zero/parse.hpp>
|
||||
#include <vamp_zero/page_control.hpp>
|
||||
#include <vamp_zero/portal.hpp>
|
||||
#include <vamp_zero/app.hpp>
|
||||
#include <vamp_zero/score.hpp>
|
||||
#include <vamp_zero/types.hpp>
|
||||
|
||||
@@ -94,7 +95,6 @@ auto has_value(noto::query_result const &result, std::size_t column, std::string
|
||||
|
||||
auto score_asset_json(std::string title, std::string key = "C", std::string part = "Lead") -> std::string {
|
||||
auto value = boost::json::parse(R"json({
|
||||
"schema_version":3,
|
||||
"music":{"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"},
|
||||
"staves":[{"id":"staff-lead","name":"Lead","measures":[{"id":"measure-1","voices":[]}]}]},
|
||||
"presentation":{"width":976,"height":484,"pages":[{"path":"fixture.pages/0001.png",
|
||||
@@ -152,6 +152,23 @@ void write_text(std::filesystem::path const &path, std::string const &text) {
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(build_identification_uses_git_without_an_application_release_label) {
|
||||
auto program = std::string{"vamp-zero"};
|
||||
auto option = std::string{"--build-info"};
|
||||
auto args = std::vector<std::char_t *>{program.data(), option.data()};
|
||||
auto command = vamp_zero::parse_command_line(2, args.data());
|
||||
BOOST_REQUIRE(command);
|
||||
BOOST_TEST(command->print_build_info);
|
||||
command->config_path = "/nonexistent/product-vamp.json";
|
||||
BOOST_TEST(vamp_zero::run(*command).has_value());
|
||||
auto once = std::string{"--once"};
|
||||
args.push_back(once.data());
|
||||
BOOST_TEST(!vamp_zero::parse_command_line(3, args.data()));
|
||||
auto status = vamp_zero::portal_status_json({}, {}).as_object();
|
||||
BOOST_TEST(status.at("git_sha").as_string() == VAMP_GIT_SHA);
|
||||
BOOST_TEST(!status.contains("version"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(runtime_config_uses_headless_and_ignores_unknown_fields) {
|
||||
auto parsed = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":true,"obsolete_setting":"ignored"},"audio":{"enabled":false}})json"));
|
||||
BOOST_REQUIRE(parsed);
|
||||
@@ -521,8 +538,8 @@ BOOST_AUTO_TEST_CASE(score_bundle_preserves_complete_music_without_runtime_flatt
|
||||
BOOST_TEST(vamp_zero::score_page_count(*parsed) == 1U);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(score_bundle_rejects_legacy_schema_malformed_manifest_and_unsafe_paths) {
|
||||
for (auto text : {"", "null", "[]", "{}", "{", R"({"schema_version":2})"}) {
|
||||
BOOST_AUTO_TEST_CASE(score_bundle_rejects_malformed_structure_manifest_and_unsafe_paths) {
|
||||
for (auto text : {"", "null", "[]", "{}", "{", R"({"music":{}})", R"({"presentation":{}})"}) {
|
||||
BOOST_CHECK_NO_THROW(BOOST_TEST(!vamp_zero::parse_score_document(text)));
|
||||
}
|
||||
for (auto path : {"../page.png", "/page.png", "pages/../../page.png", "pages/./page.png", "page.jpg", ""}) {
|
||||
|
||||
@@ -1,12 +1,50 @@
|
||||
import json
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
class BuildBootstrapTests(unittest.TestCase):
|
||||
def test_arm_profile_selects_versioned_compilers_available_in_ci_and_docker(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
profile = (root / 'conan/profiles/arm_64_gcc12_release').read_text()
|
||||
self.assertIn('tools.build:compiler_executables={"c": "aarch64-linux-gnu-gcc-12", "cpp": "aarch64-linux-gnu-g++-12"}', profile)
|
||||
for path in ('.gitea/workflows/ci.yml', 'package/Dockerfile.build'):
|
||||
source = (root / path).read_text()
|
||||
for compiler in ('gcc', 'g++'):
|
||||
self.assertIn(f'{compiler}-12-aarch64-linux-gnu', source)
|
||||
self.assertNotIn(f'{compiler}-aarch64-linux-gnu', source)
|
||||
@unittest.skipUnless(shutil.which('conan'), 'Conan is required for package identity checks')
|
||||
def test_arm_package_ids_exclude_unverified_internal_binaries_only(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
if not (root / 'build/build_tools/conan/profiles/arm_64_gcc12_release').is_file():
|
||||
self.skipTest('Configure first to export the shared Conan profiles')
|
||||
profile = root / 'conan/profiles/arm_64_gcc12_release'
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
directory = Path(temporary)
|
||||
environment = dict(os.environ, CONAN_HOME=str(directory / 'conan'), CONAN_NON_INTERACTIVE='1')
|
||||
command = ['conan', 'graph', 'info', str(directory), f'--profile:host={profile}', f'--profile:build={profile}', '--no-remote', '--format=json']
|
||||
def package_id(*extra):
|
||||
result = subprocess.run([*command, *extra], env=environment, text=True, capture_output=True, timeout=30)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return json.loads(result.stdout)['graph']['nodes']['0']['package_id']
|
||||
for name in ('modulus', 'ligo', 'noto', 'luma', 'sonus', 'raylib'):
|
||||
with self.subTest(package=name):
|
||||
(directory / 'conanfile.py').write_text(f'from conan import ConanFile\nclass Fixture(ConanFile):\n name = "{name}"\n version = "latest"\n settings = "os", "arch", "compiler", "build_type"\n')
|
||||
current = package_id()
|
||||
legacy = package_id('-c:h', f'{name}/*:tools.info.package_id:confs=[]')
|
||||
different_compiler = package_id('-c:h', 'tools.build:compiler_executables={"c": "aarch64-linux-gnu-gcc-13", "cpp": "aarch64-linux-gnu-g++-13"}')
|
||||
if name == 'raylib':
|
||||
self.assertEqual(current, different_compiler)
|
||||
else:
|
||||
self.assertNotEqual(current, legacy)
|
||||
self.assertNotEqual(current, different_compiler)
|
||||
def test_build_recipe_is_declared_and_editor_commands_do_not_bootstrap(self):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
spec = importlib.util.spec_from_file_location('vamp_bootstrap', root / 'modulus.py')
|
||||
|
||||
@@ -5,11 +5,10 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from generator.catalog import APPLICATION_ID, SCHEMA_VERSION, build_catalog
|
||||
from generator.catalog import APPLICATION_ID, build_catalog
|
||||
|
||||
def bundle(title='Fixture', key='C', part='Lead'):
|
||||
return {
|
||||
'schema_version': 3,
|
||||
'music': {'metadata': {'title': title, 'composer': 'Composer', 'key_signature': key, 'time_signature': '4/4'},
|
||||
'staves': [{'name': part, 'arrangement_part_type': part.lower(), 'measures': [{'voices': []}]}]},
|
||||
'presentation': {'width': 976, 'height': 484, 'pages': [{'path': 'fixture.pages/1.png', 'sha256': '0' * 64}]},
|
||||
@@ -51,7 +50,6 @@ class CatalogTests(unittest.TestCase):
|
||||
self.assertNotIn('authored_key_signature', practice[0])
|
||||
with closing(sqlite3.connect(database)) as connection:
|
||||
self.assertEqual(connection.execute('PRAGMA application_id').fetchone()[0], APPLICATION_ID)
|
||||
self.assertEqual(connection.execute('PRAGMA user_version').fetchone()[0], SCHEMA_VERSION)
|
||||
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',)])
|
||||
self.assertFalse(Path(str(database) + '-wal').exists())
|
||||
|
||||
@@ -81,7 +81,6 @@ auto catalog_query(boost::asio::any_io_executor executor, vamp_zero::database_co
|
||||
|
||||
auto catalog_bundle(std::string_view title, std::string_view key = "C", std::string_view part = "Lead") -> std::string {
|
||||
auto value = boost::json::parse(R"json({
|
||||
"schema_version":3,
|
||||
"music":{"metadata":{"title":"Fixture","composer":"Composer","key_signature":"C","time_signature":"4/4"},
|
||||
"staves":[{"name":"Lead","measures":[{"voices":[]},{"voices":[]},{"voices":[]}]}]},
|
||||
"presentation":{"width":976,"height":484,"pages":[
|
||||
@@ -388,7 +387,7 @@ BOOST_AUTO_TEST_CASE(catalog_reload_uses_cached_metadata_without_parsing_score_j
|
||||
BOOST_TEST(disagreement.error().message().find("disagree on key") != std::string_view::npos);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_legacy_and_corrupt_files_without_repair) {
|
||||
BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_incompatible_and_corrupt_files_without_repair) {
|
||||
auto db = catalog_database{};
|
||||
auto validate = [&] { return run_catalog_operation(vamp_zero::async_validate_database, db.config, true); };
|
||||
BOOST_TEST(!validate());
|
||||
@@ -401,14 +400,16 @@ BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_legacy_and_corrupt_files
|
||||
BOOST_REQUIRE(practice);
|
||||
BOOST_TEST(songs->parts.empty());
|
||||
BOOST_TEST(practice->exercises.empty());
|
||||
for (auto version : {0, 1, 2}) {
|
||||
db.query("PRAGMA user_version = " + std::to_string(version));
|
||||
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"}) {
|
||||
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);
|
||||
auto result = validate();
|
||||
BOOST_REQUIRE(!result);
|
||||
BOOST_TEST(result.error().message().find("reinstall") != std::string_view::npos);
|
||||
BOOST_TEST(std::get<std::int64_t>(db.query("PRAGMA user_version").rows[0][0].value) == version);
|
||||
BOOST_TEST(std::get<std::string>(db.query(snapshot_sql).rows[0][0].value) == before);
|
||||
BOOST_REQUIRE(db.build());
|
||||
}
|
||||
db.query("PRAGMA user_version = 3");
|
||||
db.query("PRAGMA application_id = 0");
|
||||
BOOST_TEST(!validate());
|
||||
auto output = std::ofstream{db.config.path, std::ios::trunc};
|
||||
@@ -419,6 +420,27 @@ BOOST_AUTO_TEST_CASE(catalog_validation_rejects_missing_legacy_and_corrupt_files
|
||||
BOOST_TEST(!std::filesystem::exists(db.config.path + "-journal"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(catalog_validation_checks_column_types_constraints_and_table_kind) {
|
||||
auto db = catalog_database{};
|
||||
auto validate = [&] { return run_catalog_operation(vamp_zero::async_validate_database, db.config, false); };
|
||||
for (auto change : {std::pair{"title TEXT NOT NULL", "title BLOB NOT NULL"}, {"title TEXT NOT NULL", "title TEXT"}, {"PRIMARY KEY", ""}, {" STRICT", ""}}) {
|
||||
BOOST_REQUIRE(db.build());
|
||||
auto sql = std::get<std::string>(db.query("SELECT sql FROM sqlite_schema WHERE name = 'songs'").rows[0][0].value);
|
||||
auto position = sql.find(change.first);
|
||||
BOOST_REQUIRE(position != std::string::npos);
|
||||
sql.replace(position, std::string_view{change.first}.size(), change.second);
|
||||
db.query("DROP TABLE songs");
|
||||
db.query(sql);
|
||||
auto timestamp = std::filesystem::last_write_time(db.config.path);
|
||||
BOOST_TEST(!validate());
|
||||
BOOST_TEST((std::filesystem::last_write_time(db.config.path) == timestamp));
|
||||
}
|
||||
BOOST_REQUIRE(db.build());
|
||||
db.query("ALTER TABLE songs RENAME TO underlying_songs");
|
||||
db.query("CREATE VIEW songs AS SELECT * FROM underlying_songs");
|
||||
BOOST_TEST(!validate());
|
||||
}
|
||||
|
||||
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"));
|
||||
|
||||
@@ -18,9 +18,10 @@ from tests.test_musicxml import parse, xml_note, xml_score
|
||||
class EngravingCacheTests(unittest.TestCase):
|
||||
def test_ci_snapshot_tracks_all_generator_inputs_and_keeps_fallback(self):
|
||||
workflow = Path('.gitea/workflows/ci.yml').read_text()
|
||||
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'data/**/*.xml', 'data/*.xml')", workflow)
|
||||
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'data/practice/*.xml', 'data/songs/*.xml')", workflow)
|
||||
self.assertIn('python3 -m generator --renderer-fingerprint', workflow)
|
||||
self.assertIn('restore-keys: product-vamp-pages-v2-${{ steps.renderer.outputs.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)
|
||||
self.assertIn("if: steps.engraving-cache.outputs.cache-hit != 'true'", workflow)
|
||||
self.assertIn('key: ${{ steps.engraving-cache.outputs.cache-primary-key }}', workflow)
|
||||
def setUp(self):
|
||||
@@ -147,7 +148,7 @@ class EngravingCacheTests(unittest.TestCase):
|
||||
self.assertIn('cold namespace', progress.getvalue())
|
||||
self.assertIn('0 reused, 1 missing, 0 rejected', progress.getvalue())
|
||||
self.assertTrue((old / 'pages.json').is_file())
|
||||
self.assertEqual(len(list((self.cache / 'v2').iterdir())), 2)
|
||||
self.assertEqual(len(list(self.cache.iterdir())), 2)
|
||||
self.write_scores(root, [self.document])
|
||||
progress = io.StringIO()
|
||||
with patch('generator.engraving.cache.platform.python_version', return_value='0.0.0'), contextlib.redirect_stdout(progress), patch('generator.engraving.layout_score', side_effect=AssertionError('cache miss')):
|
||||
@@ -155,7 +156,7 @@ class EngravingCacheTests(unittest.TestCase):
|
||||
self.assertIn('1 reused, 0 missing, 0 rejected', progress.getvalue())
|
||||
def test_namespace_symlink_is_rejected(self):
|
||||
self.cache.mkdir()
|
||||
(self.cache / 'v2').symlink_to(self.root, target_is_directory=True)
|
||||
(self.cache / 'renderer').symlink_to(self.root, target_is_directory=True)
|
||||
with self.assertRaisesRegex(ValueError, 'must not be symlinks'):
|
||||
renderer_namespace(self.cache, 'renderer')
|
||||
def test_only_changed_score_is_rendered_and_validation_reads_json_once(self):
|
||||
|
||||
@@ -166,7 +166,7 @@ auto checksum(std::string const &bytes) -> std::string {
|
||||
}
|
||||
|
||||
auto write_score(std::filesystem::path const &path, std::string part = "Lead") -> void {
|
||||
auto score = boost::json::parse(R"({"schema_version":3,"music":{"metadata":{"title":"Fixture","composer":"Composer",
|
||||
auto score = boost::json::parse(R"({"music":{"metadata":{"title":"Fixture","composer":"Composer",
|
||||
"key_signature":"C","time_signature":"4/4"},"staves":[{"name":"Lead","measures":[{"voices":[]}]}]},
|
||||
"presentation":{"width":976,"height":484,"pages":[]}})");
|
||||
score.at("music").at("staves").as_array().front().as_object()["name"] = part;
|
||||
|
||||
@@ -44,11 +44,11 @@ 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('data').rglob('*.xml'))
|
||||
sources = sorted(path for category in ('practice', 'songs') for path in (Path('data') / category).glob('*.xml'))
|
||||
self.assertTrue(sources)
|
||||
for path in sources:
|
||||
with self.subTest(path=path):
|
||||
self.assertEqual(load_musicxml(path)['schema_version'], 3)
|
||||
self.assertEqual(set(load_musicxml(path)), {'metadata', 'staves'})
|
||||
def test_all_written_values_and_dots(self):
|
||||
for kind in NOTE_TYPES:
|
||||
for dots in (0, 1, 2, 3):
|
||||
@@ -71,7 +71,7 @@ class MusicXMLTests(unittest.TestCase):
|
||||
def test_doctype_allowed_but_entities_rejected(self):
|
||||
source = xml_score(xml_note())
|
||||
source = source.replace('<score-partwise', '<!DOCTYPE score-partwise SYSTEM "https://invalid.test/partwise.dtd"><score-partwise', 1)
|
||||
self.assertEqual(parse(source)['schema_version'], 3)
|
||||
self.assertEqual(parse(source), parse(xml_score(xml_note())))
|
||||
malicious = xml_score(xml_note()).replace('<score-partwise', '<!DOCTYPE score-partwise [<!ENTITY secret SYSTEM "file:///etc/passwd">]><score-partwise', 1)
|
||||
with self.assertRaises(MusicXMLError):
|
||||
parse(malicious)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from package.scripts import publish_latest_generic_package
|
||||
|
||||
class PackagePublishingTests(unittest.TestCase):
|
||||
def test_only_configured_target_archives_and_checksums_are_uploaded(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
archive = root / 'product-vamp-arm_64_gcc12_release.tar.gz'
|
||||
checksum = archive.with_name(archive.name + '.sha256')
|
||||
for path in (archive, checksum, root / 'product-vamp-obsolete-arm_64_gcc12_release.tar.gz'):
|
||||
path.write_bytes(b'fixture')
|
||||
self.assertEqual(publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release']), [archive, checksum])
|
||||
checksum.unlink()
|
||||
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
|
||||
publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release'])
|
||||
archive.unlink()
|
||||
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
|
||||
publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release'])
|
||||
def test_missing_artifacts_do_not_touch_the_registry(self):
|
||||
with tempfile.TemporaryDirectory() as temporary, patch.object(publish_latest_generic_package, 'request') as request:
|
||||
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
|
||||
publish_latest_generic_package.main(['--dist-dir', temporary, '--user', 'user', '--token', 'test-token'])
|
||||
request.assert_not_called()
|
||||
def test_publication_replaces_the_single_registry_slot(self):
|
||||
archive = Path('product-vamp-arm_64_gcc12_release.tar.gz')
|
||||
with patch.object(publish_latest_generic_package, 'discover_artifacts', return_value=[archive]), \
|
||||
patch.object(publish_latest_generic_package, 'list_versions', return_value=['latest', 'obsolete']), \
|
||||
patch.object(publish_latest_generic_package, 'delete_version') as delete, \
|
||||
patch.object(publish_latest_generic_package, 'upload_file') as upload:
|
||||
self.assertEqual(publish_latest_generic_package.main(['--user', 'user', '--token', 'test-token']), 0)
|
||||
self.assertEqual([call.args[-2] for call in delete.call_args_list], ['latest', 'obsolete'])
|
||||
self.assertEqual(upload.call_args.args[2], archive)
|
||||
url = publish_latest_generic_package.file_upload_url('https://example.test/generic', 'product-vamp', archive)
|
||||
self.assertEqual(url, 'https://example.test/generic/product-vamp/latest/product-vamp-arm_64_gcc12_release.tar.gz')
|
||||
@@ -11,7 +11,7 @@ from xml.etree import ElementTree
|
||||
|
||||
from generator.arrange.generate import arrange_document
|
||||
from generator.__main__ import generate_assets
|
||||
from generator.engraving import ENGINE_NAME, ENGINE_VERSION, EngravingError, Fonts, Style, check_page, engrave_tree, layout_score
|
||||
from generator.engraving import ENGINE_NAME, EngravingError, Fonts, Style, check_page, engrave_tree, layout_score
|
||||
from tests.musicxml_roundtrip import audit_roundtrip
|
||||
from generator.model import fraction, pitch_number, rational, validate_document
|
||||
from generator.musicxml import load_musicxml
|
||||
@@ -34,8 +34,16 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
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')))
|
||||
(data / 'rhythms' / 'drafts').mkdir(parents=True)
|
||||
for path in ('rhythms/001.xml', 'rhythms/drafts/002.xml'):
|
||||
(data / path).write_text('<unfinished')
|
||||
generate_assets(args)
|
||||
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
|
||||
misplaced = data / '001.xml'
|
||||
misplaced.write_text(xml_score(xml_note()))
|
||||
with self.assertRaisesRegex(ValueError, 'belong directly under'):
|
||||
generate_assets(args)
|
||||
misplaced.unlink()
|
||||
with patch('generator.__main__.compile_tree', side_effect=ValueError('invalid generated audio')):
|
||||
with self.assertRaisesRegex(ValueError, 'audio'):
|
||||
generate_assets(args)
|
||||
@@ -64,6 +72,12 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
path = Path(temporary) / 'roundtrip.xml'
|
||||
write_musicxml(path, document)
|
||||
return load_musicxml(path)
|
||||
def test_score_structure_is_validated_without_schema_labels(self):
|
||||
document = parse(xml_score(xml_note()))
|
||||
self.assertEqual(set(document), {'metadata', 'staves'})
|
||||
for malformed in (None, [], {}, {'metadata': {}, 'staves': []}, {**document, 'staves': None}, {**document, 'staves': [{}]}):
|
||||
with self.subTest(document=malformed), self.assertRaises(ValueError):
|
||||
validate_document(malformed)
|
||||
def test_multiple_voices_keep_independent_ties_and_transformations(self):
|
||||
def half(action):
|
||||
return xml_note(duration=2, kind='half', extra=f'<voice>1</voice><tie type="{action}"/>')
|
||||
@@ -159,7 +173,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'unknown score instrument'):
|
||||
validate_document(source)
|
||||
def test_catalog_roundtrip_preserves_written_pitch_and_rhythm(self):
|
||||
for path in Path('data').rglob('*.xml'):
|
||||
for path in sorted(path for category in ('practice', 'songs') for path in (Path('data') / category).glob('*.xml')):
|
||||
with self.subTest(path=path):
|
||||
source = load_musicxml(path)
|
||||
restored = self.round_trip(source)
|
||||
@@ -231,7 +245,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
page = assets / 'page.png'
|
||||
page.write_bytes(data)
|
||||
document = load_musicxml(Path('tests/fixtures/engraving.xml'))
|
||||
bundle = {'schema_version': 3, 'music': {k: v for k, v in document.items() if k != 'schema_version'}, 'presentation': {'engine': ENGINE_NAME, 'engine_version': ENGINE_VERSION, 'width': 976, 'height': 484, 'pages': [{'path': 'page.png', 'sha256': hashlib.sha256(data).hexdigest()}]}}
|
||||
bundle = {'music': document, 'presentation': {'engine': ENGINE_NAME, 'width': 976, 'height': 484, 'pages': [{'path': 'page.png', 'sha256': hashlib.sha256(data).hexdigest()}]}}
|
||||
path = assets / 'score.json'
|
||||
path.write_text(json.dumps(bundle))
|
||||
self.assertEqual(load_json(path), document)
|
||||
@@ -255,7 +269,7 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
write_json(root / 'fixture.json', document)
|
||||
engrave_tree(root, Path('package/assets/fonts/petaluma'))
|
||||
bundle = json.loads((root / 'fixture.json').read_text())
|
||||
self.assertEqual(bundle['music'], {k: v for k, v in document.items() if k != 'schema_version'})
|
||||
self.assertEqual(bundle['music'], document)
|
||||
self.assertEqual(len(bundle['presentation']['pages']), 1)
|
||||
page = root / bundle['presentation']['pages'][0]['path']
|
||||
with Image.open(page) as actual, Image.open('tests/fixtures/engraving-python.png') as expected:
|
||||
|
||||
Reference in New Issue
Block a user