Build Fix
ci / checks (push) Failing after 1h18m29s

This commit is contained in:
khodowany
2026-09-25 09:35:09 -05:00
parent 07559ad74c
commit 17326dbf56
34 changed files with 283 additions and 3723 deletions
+20 -3
View File
@@ -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", ""}) {
+38
View File
@@ -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')
+1 -3
View File
@@ -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())
+28 -6
View File
@@ -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"));
+5 -4
View File
@@ -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):
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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)
+38
View File
@@ -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')
+18 -4
View File
@@ -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: