Package
ci / checks (push) Successful in 48m34s

This commit is contained in:
khodowany
2026-09-30 19:46:37 -05:00
parent 2a1aa2c67a
commit 09656a9d60
6621 changed files with 698 additions and 774 deletions
+24 -24
View File
@@ -1,4 +1,4 @@
add_executable(product-vamp-test
add_executable(product_vamp_test
../src/vamp_pico/http_client.cpp
../src/vamp_pico/input.cpp
../src/vamp_pico/remote_display.cpp
@@ -14,57 +14,57 @@ add_executable(product-vamp-test
test_touch_input.cpp
test_ui.cpp
)
target_include_directories(product-vamp-test PRIVATE
target_include_directories(product_vamp_test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/fixtures/pico_sdk
)
target_link_libraries(product-vamp-test PRIVATE
vamp-zero-core
target_link_libraries(product_vamp_test PRIVATE
vamp_zero_core
modulus::raylib
)
target_compile_features(product-vamp-test PRIVATE cxx_std_23)
target_compile_definitions(product-vamp-test PRIVATE
target_compile_features(product_vamp_test PRIVATE cxx_std_23)
target_compile_definitions(product_vamp_test PRIVATE
VAMP_ZERO_HOST="192.168.42.1"
VAMP_ZERO_PORT=8080
VAMP_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
VAMP_TEST_PYTHON="${Python3_EXECUTABLE}"
VAMP_TEST_ASSETS_DIR="${CMAKE_BINARY_DIR}/generated-assets"
VAMP_TEST_PLAYBACK_ASSETS_DIR="${CMAKE_CURRENT_BINARY_DIR}/playback-assets"
VAMP_TEST_UI_DIR="${CMAKE_CURRENT_BINARY_DIR}/ui-previews"
VAMP_TEST_ASSETS_DIR="${CMAKE_BINARY_DIR}/generated_assets"
VAMP_TEST_PLAYBACK_ASSETS_DIR="${CMAKE_CURRENT_BINARY_DIR}/playback_assets"
VAMP_TEST_UI_DIR="${CMAKE_CURRENT_BINARY_DIR}/ui_previews"
)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/ui-previews/static.p4
COMMAND ${Python3_EXECUTABLE} -m generator.pico_artwork --static-output ${CMAKE_CURRENT_BINARY_DIR}/ui-previews/static.p4
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/ui_previews/static.p4
COMMAND ${VAMP_PYTHON_COMMAND} -m generator.pico_artwork --static-output ${CMAKE_CURRENT_BINARY_DIR}/ui_previews/static.p4
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
DEPENDS ${CMAKE_SOURCE_DIR}/generator/pico_artwork.py ${CMAKE_SOURCE_DIR}/package/assets/fonts/petaluma/PetalumaScript.otf
VERBATIM
)
add_custom_target(vamp-ui-test-assets DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/ui-previews/static.p4)
add_dependencies(product-vamp-test vamp-ui-test-assets)
add_custom_target(vamp_ui_test_assets DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/ui_previews/static.p4)
add_dependencies(product_vamp_test vamp_ui_test_assets)
file(GLOB_RECURSE VAMP_PLAYBACK_TEST_INPUTS CONFIGURE_DEPENDS ${CMAKE_SOURCE_DIR}/generator/*.py)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/playback-assets/score.json
COMMAND ${Python3_EXECUTABLE} -m tests.test_playback --fixture ${CMAKE_CURRENT_BINARY_DIR}/playback-assets
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/playback_assets/score.json
COMMAND ${VAMP_PYTHON_COMMAND} -m tests.test_playback --fixture ${CMAKE_CURRENT_BINARY_DIR}/playback_assets
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
DEPENDS test_playback.py test_musicxml.py ${VAMP_PLAYBACK_TEST_INPUTS}
VERBATIM
)
add_custom_target(vamp-playback-test-assets DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/playback-assets/score.json)
add_dependencies(product-vamp-test vamp-playback-test-assets)
add_test(NAME product-vamp COMMAND product-vamp-test)
set_tests_properties(product-vamp PROPERTIES TIMEOUT 180)
add_custom_target(vamp_playback_test_assets DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/playback_assets/score.json)
add_dependencies(product_vamp_test vamp_playback_test_assets)
add_test(NAME product_vamp COMMAND product_vamp_test)
set_tests_properties(product_vamp PROPERTIES TIMEOUT 180)
add_test(
NAME product-vamp-generator
COMMAND ${Python3_EXECUTABLE} -m unittest discover -s tests -p test_*.py
NAME product_vamp_generator
COMMAND ${VAMP_PYTHON_COMMAND} -m unittest discover -s tests -p test_*.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
set_tests_properties(product-vamp-generator PROPERTIES TIMEOUT 180)
set_tests_properties(product_vamp_generator PROPERTIES TIMEOUT 180)
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir ${CMAKE_BINARY_DIR}
-R ^product-vamp
-R ^product_vamp
--output-on-failure
--timeout 60
USES_TERMINAL
)
add_dependencies(check product-vamp-test)
add_dependencies(check product_vamp_test)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+3 -3
View File
@@ -12,9 +12,9 @@ def library() -> generator.rhythm.Library:
events = tuple(generator.rhythm.Event(fractions.Fraction(i), fractions.Fraction(1), bool(mask & (1 << i)), 'quarter') for i in range(beats))
bar = generator.rhythm.Bar(events, fractions.Fraction(beats))
bars.append(bar)
snippets.append(generator.rhythm.Snippet(f'{beats}-4/quarters/{mask}', f'{beats}/4', (bar,)))
snippets.append(generator.rhythm.Snippet(f'{beats}_4/quarters/{mask}', f'{beats}/4', (bar,)))
for phase in (0, 1):
events = tuple(generator.rhythm.Event(fractions.Fraction(i, 2), fractions.Fraction(1, 2), i % 2 == phase, 'eighth') for i in range(beats * 2))
snippets.append(generator.rhythm.Snippet(f'{beats}-4/eighths/{phase}', f'{beats}/4', (generator.rhythm.Bar(events, fractions.Fraction(beats)),)))
snippets.append(generator.rhythm.Snippet(f'{beats}-4/pair', f'{beats}/4', (bars[1], bars[-1])))
snippets.append(generator.rhythm.Snippet(f'{beats}_4/eighths/{phase}', f'{beats}/4', (generator.rhythm.Bar(events, fractions.Fraction(beats)),)))
snippets.append(generator.rhythm.Snippet(f'{beats}_4/pair', f'{beats}/4', (bars[1], bars[-1])))
return generator.rhythm.Library(tuple(snippets))
+3 -3
View File
@@ -81,13 +81,13 @@ auto navigation_state() -> vamp_zero::app_state {
}
BOOST_AUTO_TEST_CASE(build_identification_uses_git_without_an_application_release_label) {
auto program = std::string{"vamp-zero"};
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";
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());
@@ -99,7 +99,7 @@ BOOST_AUTO_TEST_CASE(runtime_config_uses_headless_and_ignores_unknown_fields) {
BOOST_REQUIRE(parsed);
BOOST_TEST(parsed->runtime.headless);
BOOST_TEST(!parsed->audio.enabled);
BOOST_TEST(parsed->database.path == "/usr/share/product-vamp/catalog.db");
BOOST_TEST(parsed->database.path == "/usr/share/product_vamp/catalog.db");
auto invalid = vamp_zero::parse_config(boost::json::parse(R"json({"runtime":{"headless":"yes"}})json"));
BOOST_TEST(!invalid);
}
+14 -14
View File
@@ -40,7 +40,7 @@ def tied_library(continuation_duration):
second += '<note><rest/><duration>2</duration><type>half</type></note>'
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
path = root / '4-4/2/tied.xml'
path = root / '4_4/2/tied.xml'
path.parent.mkdir(parents=True)
path.write_text(xml_bars([first, second]), encoding='utf-8')
return generator.rhythm.load_library(root)
@@ -57,7 +57,7 @@ class RhythmLibraryTests(unittest.TestCase):
extra = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
source = xml_bars([tests.test_musicxml.xml_note(duration=2, kind='quarter', extra=extra) * 6], divisions=3)
with self.assertRaisesRegex(ValueError, 'time modifications'):
self.load({'4-4/1/ratio.xml': source})
self.load({'4_4/1/ratio.xml': source})
def load(self, sources):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.Path(temporary)
@@ -71,9 +71,9 @@ class RhythmLibraryTests(unittest.TestCase):
content += tests.test_musicxml.xml_note(duration=1, kind='16th', extra='<beam number="1">end</beam><beam number="2">backward hook</beam>')
content += '<note><rest/><duration>4</duration><type>quarter</type></note>'
content += tests.test_musicxml.xml_note(duration=8, kind='half')
library = self.load({'4-4/1/dotted.xml': xml_bars([content], divisions=4)})
library = self.load({'4_4/1/dotted.xml': xml_bars([content], divisions=4)})
snippet = library.for_meter('4/4')[0]
self.assertEqual((snippet.id, snippet.meter), ('4-4/1/dotted.xml', '4/4'))
self.assertEqual((snippet.id, snippet.meter), ('4_4/1/dotted.xml', '4/4'))
self.assertEqual(snippet.bars, (generator.rhythm.Bar((
generator.rhythm.Event(fractions.Fraction(0), fractions.Fraction(3, 4), True, 'eighth', 1, ((1, 'begin'),)),
generator.rhythm.Event(fractions.Fraction(3, 4), fractions.Fraction(1, 4), True, '16th', 0, ((1, 'end'), (2, 'backward hook'))),
@@ -84,7 +84,7 @@ class RhythmLibraryTests(unittest.TestCase):
self.assertTrue(all(type(e.onset) is fractions.Fraction and type(e.duration) is fractions.Fraction for e in snippet.bars[0].events))
def test_two_bar_ties_are_continuations_without_new_attacks(self):
content = xml_bars([tests.test_musicxml.xml_note(extra='<tie type="start"/>'), tests.test_musicxml.xml_note(extra='<tie type="stop"/>')])
snippet = self.load({'4-4/2/tied.xml': content}).for_meter('4/4')[0]
snippet = self.load({'4_4/2/tied.xml': content}).for_meter('4/4')[0]
self.assertEqual(len(snippet.bars), 2)
self.assertEqual([bar.attacks() for bar in snippet.bars], [(0,), ()])
self.assertEqual([bar.events[0].continuation for bar in snippet.bars], [False, True])
@@ -96,13 +96,13 @@ class RhythmLibraryTests(unittest.TestCase):
content = tests.test_musicxml.xml_note(duration=duration, kind=kind, extra='<dot/>' if dots else '') if sounding else f'<note><rest/><duration>{duration}</duration><type>{kind}</type>{"<dot/>" if dots else ""}</note>'
if beats == 5:
content += tests.test_musicxml.xml_note(duration=1, kind='quarter')
snippet = self.load({f'{beats}-4/3/0001.xml': xml_bars([content], beats=beats)}).for_meter(f'{beats}/4')[0]
snippet = self.load({f'{beats}_4/3/0001.xml': xml_bars([content], beats=beats)}).for_meter(f'{beats}/4')[0]
self.assertEqual(snippet.bars[0].events[0], generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(duration), sounding, kind, dots))
self.assertEqual(snippet.bars[0].length, beats)
def test_all_three_series_participate_in_deterministic_long_note_selection(self):
silence = '<note><rest/><duration>4</duration><type>whole</type></note>'
halves = tests.test_musicxml.xml_note(duration=2, kind='half') * 2
library = self.load({'4-4/1/0001.xml': xml_bars([silence] * 2), '4-4/2/0001.xml': xml_bars([silence]), '4-4/3/0001.xml': xml_bars([halves])})
library = self.load({'4_4/1/0001.xml': xml_bars([silence] * 2), '4_4/2/0001.xml': xml_bars([silence]), '4_4/3/0001.xml': xml_bars([halves])})
self.assertEqual(len(library.for_meter('4/4')), 3)
measures = melody_measures(8)
plan = generator.accompaniment.plan_section(measures, library)
@@ -134,10 +134,10 @@ class RhythmLibraryTests(unittest.TestCase):
for beats in (2, 3, 4, 5):
content = xml_bars([''.join(tests.test_musicxml.xml_note(duration=1, kind='quarter') for _ in range(beats))], beats=beats)
for name in ('z', 'a'):
sources[f'{beats}-4/1/{name}.xml'] = content
sources[f'{beats}_4/1/{name}.xml'] = content
library = self.load(sources)
for beats in (2, 3, 4, 5):
self.assertEqual([s.id for s in library.for_meter(f'{beats}/4')], [f'{beats}-4/1/a.xml', f'{beats}-4/1/z.xml'])
self.assertEqual([s.id for s in library.for_meter(f'{beats}/4')], [f'{beats}_4/1/a.xml', f'{beats}_4/1/z.xml'])
self.assertTrue(all(s.bars[0].length == beats for s in library.for_meter(f'{beats}/4')))
def test_missing_empty_unsupported_and_misfiled_libraries_fail(self):
with tempfile.TemporaryDirectory() as temporary, self.assertRaisesRegex(ValueError, 'not found'):
@@ -145,12 +145,12 @@ class RhythmLibraryTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'empty'):
self.load({})
source = xml_bars([tests.test_musicxml.xml_note()])
for path in ('snippet.xml', '4-4/snippet.xml', '4-4/4/snippet.xml', '4-4/1/nested/snippet.xml', '6-8/1/snippet.xml'):
for path in ('snippet.xml', '4_4/snippet.xml', '4_4/4/snippet.xml', '4_4/1/nested/snippet.xml', '6-8/1/snippet.xml'):
with self.subTest(path=path), self.assertRaisesRegex(ValueError, 'meter/series'):
self.load({path: source})
with self.assertRaisesRegex(ValueError, 'meter disagrees'):
self.load({'3-4/1/snippet.xml': source})
library = self.load({'4-4/1/snippet.xml': source})
self.load({'3_4/1/snippet.xml': source})
library = self.load({'4_4/1/snippet.xml': source})
for meter in ('2/4', '6/8', '7/4', '4/8', 'invalid'):
with self.subTest(meter=meter), self.assertRaisesRegex(ValueError, 'rhythm snippets'):
library.for_meter(meter)
@@ -175,7 +175,7 @@ class RhythmLibraryTests(unittest.TestCase):
}
for name, content in cases.items():
with self.subTest(source=name), self.assertRaises(ValueError):
self.load({'4-4/1/invalid.xml': content})
self.load({'4_4/1/invalid.xml': content})
class AccompanimentPlanningTests(unittest.TestCase):
def test_pickup_coverage_excludes_notes_that_ended_before_the_excerpt(self):
@@ -251,7 +251,7 @@ class AccompanimentPlanningTests(unittest.TestCase):
self.assertTrue(attacks <= melody)
self.assertTrue(any(bar.attacks() for bar in plan.electric))
def test_section_and_local_meter_changes_restart_the_motif(self):
library = generator.rhythm.Library(tuple(generator.rhythm.Snippet(f'{beats}-4/pair', f'{beats}/4', (quarter_bar(beats, {0}), quarter_bar(beats, {1}))) for beats in (3, 4)))
library = generator.rhythm.Library(tuple(generator.rhythm.Snippet(f'{beats}_4/pair', f'{beats}/4', (quarter_bar(beats, {0}), quarter_bar(beats, {1}))) for beats in (3, 4)))
segments = [melody_measures(3), melody_measures(2, beats=3), melody_measures(3), melody_measures(4)]
segments[-1][0]['section_label'] = 'Bridge'
measures = [m for segment in segments for m in segment]
+2 -2
View File
@@ -131,7 +131,7 @@ class AcousticTests(unittest.TestCase):
self.assertEqual(document, before)
document['staves'][0]['arrangement_part_type'] = 'acoustic'
measure = document['staves'][0]['measures'][0]
measure['voices'] = [{'id': 'voice-arranged', 'notes': [generator.model.make_event('slash', fractions.Fraction(), fractions.Fraction(4), 'note') | {'voice': 'voice-arranged', 'notehead': 'slash'}]}]
measure['voices'] = [{'id': 'voice_arranged', 'notes': [generator.model.make_event('slash', fractions.Fraction(), fractions.Fraction(4), 'note') | {'voice': 'voice_arranged', 'notehead': 'slash'}]}]
before = copy.deepcopy(document)
generator.acoustic.prepare(document)
generator.acoustic.combine(document)
@@ -155,4 +155,4 @@ class AcousticTests(unittest.TestCase):
self.assertFalse([p for p in primitives if p.role in ('rest', 'tab-rest')])
for note in document['staves'][0]['measures'][0]['voices'][0]['notes']:
self.assertEqual(len([p for p in primitives if p.role == 'stem' and note['id'] in p.sources]), 1)
self.assertFalse([p for p in primitives if p.role in ('tie', 'tab-tie')])
self.assertFalse([p for p in primitives if p.role in ('tie', 'tab_tie')])
+4 -4
View File
@@ -25,7 +25,7 @@ def planned_document(harmonies, attacks=None):
measure['harmonies'] = []
for onset, symbol in changes:
harmony = {'kind': 'none', 'degrees': []} if symbol == 'N.C.' else generator.harmony.parse_symbol(symbol) if isinstance(symbol, str) else copy.deepcopy(symbol)
measure['harmonies'].append(harmony | {'id': f'{measure["id"]}-h{len(measure["harmonies"])}', 'onset': generator.model.rational(onset)})
measure['harmonies'].append(harmony | {'id': f'{measure["id"]}_h{len(measure["harmonies"])}', 'onset': generator.model.rational(onset)})
for voice in measure['voices']:
for note in voice['notes']:
if not note['pitches']:
@@ -145,8 +145,8 @@ class AcousticVoicingTests(unittest.TestCase):
def test_non_accompaniment_passage_ends_the_span(self):
document = planned_document([[(0, 'C7')], [], []])
measure = document['staves'][0]['measures'][1]
slash = generator.model.make_event('slash', fractions.Fraction(), generator.model.fraction(measure['duration']), 'note') | {'voice': 'voice-arranged', 'notehead': 'slash'}
measure['voices'] = [{'id': 'voice-arranged', 'notes': [slash]}]
slash = generator.model.make_event('slash', fractions.Fraction(), generator.model.fraction(measure['duration']), 'note') | {'voice': 'voice_arranged', 'notehead': 'slash'}
measure['voices'] = [{'id': 'voice_arranged', 'notes': [slash]}]
self.assertEqual(len(self.place(document)), 2)
def test_carried_harmony_can_begin_an_arrangement_excerpt(self):
document = planned_document([[(0, 'C7')], []])
@@ -268,7 +268,7 @@ class AcousticVoicingTests(unittest.TestCase):
upper = sorted(p['tablature']['string'] for p in note['pitches'] if p['tablature']['string'] <= 4)
self.assertIn(upper, ([], [2, 3, 4]))
self.assertTrue(all('tie_from' not in p and 'tie_to' not in p for p in note['pitches']))
if path.stem == 'autumn-leaves':
if path.stem == 'autumn_leaves':
for index, expected in ((0, voicing((3, 2, 3, 3), 6)), (1, voicing((3, 2, 3, 2), 6)), (4, voicing((3, 2, 3, 3), 6)), (5, voicing((3, 2, 3, 2), 6))):
measure = result['staves'][0]['measures'][index]
actual = {p for v in measure['voices'] for n in v['notes'] for p in shape(n)}
+1 -1
View File
@@ -36,7 +36,7 @@ class ArrangementPickupTests(unittest.TestCase):
self.assertEqual(len(measures), 176)
self.assertEqual(sum(generator.model.fraction(m['duration']) for m in measures), 704)
timelines.append([m['duration'] for m in measures])
if path.stem == 'autumn-leaves' and part in ('vocal', 'electric'):
if path.stem == 'autumn_leaves' and part in ('vocal', 'electric'):
for boundary in ([8, 136] if part == 'vocal' else [40]):
notes = [n for n in generator.score.measure_notes(measures[boundary - 1]) if generator.model.fraction(n['onset']) >= 1 and n['pitches']]
self.assertEqual([generator.model.fraction(n['onset']) for n in notes], [1, 2, 3])
+3 -2
View File
@@ -52,8 +52,9 @@ class BuildBootstrapTests(unittest.TestCase):
spec.loader.exec_module(bootstrap)
docker = json.loads((root / 'modulus.json').read_text())['tooling']['docker']
self.assertEqual(docker['socket_commands'], ['build', 'package'])
self.assertIn('generator/requirements.txt', docker['inputs'])
self.assertIn('COPY generator/requirements.txt /tmp/vamp-build-requirements.txt', (root / docker['dockerfile']).read_text())
self.assertEqual(json.loads((root / 'modulus.json').read_text())['tooling']['python']['groups'], ['assets'])
self.assertFalse((root / 'generator/requirements.txt').exists())
self.assertNotIn('generator/requirements.txt', (root / docker['dockerfile']).read_text())
package = types.ModuleType('modulus')
cli = types.ModuleType('modulus.cli')
package.cli = cli
+1 -1
View File
@@ -14,7 +14,7 @@ class CatalogTests(unittest.TestCase):
source.mkdir()
(source / 'song.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', alter=-1), '<key><fifths>-3</fifths></key>'))
self.assertEqual(generator.arrange.generate.generate_performance_scores(source, output, rhythms=tests.rhythm_fixtures.library()), 3)
self.assertEqual({p.name for p in output.iterdir()}, {'song-acoustic.json', 'song-electric.json', 'song-vocal.json'})
self.assertEqual({p.name for p in output.iterdir()}, {'song_acoustic.json', 'song_electric.json', 'song_vocal.json'})
for path in output.iterdir():
metadata = json.loads(path.read_text())['metadata']
self.assertEqual(metadata['key_signature'], 'Eb')
+2 -2
View File
@@ -75,7 +75,7 @@ class CatalogTests(unittest.TestCase):
with self.assertRaises(ValueError):
generator.catalog.build_catalog(self.root)
self.assertEqual(database.read_bytes(), previous)
self.assertFalse(list(self.root.glob('.vamp-catalog-*')))
self.assertFalse(list(self.root.glob('.vamp_catalog-*')))
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)
@@ -90,7 +90,7 @@ class CatalogTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'title image'):
generator.catalog.build_catalog(self.root)
self.assertEqual(database.read_bytes(), previous)
self.assertFalse(list(self.root.glob('.vamp-catalog-*')))
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'songs/{part.lower()}.json', bundle(key='No key', part=part))
+2 -2
View File
@@ -42,7 +42,7 @@ static_assert(!retains_score_json<vamp_zero::catalog>);
struct catalog_directory {
std::filesystem::path path;
catalog_directory() {
auto pattern = (std::filesystem::temp_directory_path() / "vamp-catalog-XXXXXX").string();
auto pattern = (std::filesystem::temp_directory_path() / "vamp_catalog-XXXXXX").string();
BOOST_REQUIRE(::mkdtemp(pattern.data()) != nullptr);
path = pattern;
}
@@ -62,7 +62,7 @@ auto run_catalog_operation(Function function, Arguments... arguments) {
auto catalog_query(boost::asio::any_io_executor executor, vamp_zero::database_config config, std::string sql, std::vector<noto::sqlite_value> params)
-> boost::asio::awaitable<std::expected<noto::query_result, modulus::error>> {
auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp-catalog-test")};
auto connection = noto::connection{executor, noto::file_connection_config(config.path, "vamp_catalog-test")};
auto connected = co_await connection.async_connect();
if (!connected) {
co_return std::unexpected(connected.error());
+2 -2
View File
@@ -73,7 +73,7 @@ class ElectricVoicingTests(unittest.TestCase):
self.assertEqual(reference, before)
self.assertEqual(shape(reference), shape(target))
self.assertTrue(all('tie_to' not in p and 'tie_from' not in p for p in target['pitches']))
self.assertEqual({p['id'] for p in target['pitches']}, {f'stab-s{i}' for i in range(1, 5)})
self.assertEqual({p['id'] for p in target['pitches']}, {f'stab_s{i}' for i in range(1, 5)})
target['pitches'][0]['tablature']['fret'] = 20
self.assertEqual(reference, before)
def test_unharmonized_passage_does_not_borrow_from_another_occurrence(self):
@@ -107,7 +107,7 @@ class ElectricVoicingTests(unittest.TestCase):
generator.electric.reuse_voicings(links)
self.assertEqual(target, before)
def test_real_arrangement_reuses_requested_grips_without_changing_melody(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn-leaves.xml'))
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
before = copy.deepcopy(source)
rhythms = tests.rhythm_fixtures.library()
with unittest.mock.patch('generator.electric.reuse_voicings'):
+13 -13
View File
@@ -53,7 +53,7 @@ class EngravingTests(unittest.TestCase):
for page in layout.pages:
self.assertEqual(len({system.scale for system in page.systems}), 1)
for system in page.systems:
for role in ('staff', 'tab-staff'):
for role in ('staff', 'tab_staff'):
lines = [p for p in system.primitives if p.role == role]
if lines:
self.assertAlmostEqual(min(x for p in lines for x, _ in p.points), layout.style.margin_x + 1)
@@ -136,7 +136,7 @@ class EngravingTests(unittest.TestCase):
layout = self.layout(generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/notation.xml')))
self.assertEqual(len(layout.pages), 1)
actual = generator.engraving.raster.rasterize_page(layout.pages[0], self.fonts)
with PIL.Image.open('tests/fixtures/notation-python.png') as expected:
with PIL.Image.open('tests/fixtures/notation_python.png') as expected:
self.assertIsNone(PIL.ImageChops.difference(actual, expected.convert('RGB')).getbbox())
def test_aligned_bands_match_full_pages_with_spans_tab_and_sampling_phases(self):
tied = tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="start"/>') + tests.test_musicxml.xml_note('E', 2, 'half', extra='<tie type="stop"/>')
@@ -342,7 +342,7 @@ class EngravingTests(unittest.TestCase):
held = tone['id']
system = generator.engraving.layout.make_system(index, fractions.Fraction(start), fractions.Fraction(end), self.fonts, style)
number = next(p for p in system.primitives if p.role == 'tab' and held in p.sources)
ties = [p for p in system.primitives if p.role == 'tab-tie' and {held, tone.get('tie_from')} & set(p.sources)]
ties = [p for p in system.primitives if p.role == 'tab_tie' and {held, tone.get('tie_from')} & set(p.sources)]
self.assertEqual(sum(p.points[-1][0] < number.box.left for p in ties), int(start > 0))
self.assertEqual(sum(p.points[0][0] > number.box.right for p in ties), int(end < 12))
def test_dense_measure_fails_contextually_without_modifying_music(self):
@@ -373,7 +373,7 @@ class EngravingTests(unittest.TestCase):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(grace + tests.test_musicxml.xml_note(duration=4, kind='whole', prefix='<cue/>')))
layout = self.layout(source)
self.assertEqual(len(self.primitives(layout, 'head')), 2)
self.assertEqual(len(self.primitives(layout, 'grace-slash')), 1)
self.assertEqual(len(self.primitives(layout, 'grace_slash')), 1)
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=2, kind='half', extra='<notations><ornaments><tremolo type="single">8</tremolo></ornaments></notations>'), implicit=True))
self.assertEqual(len(self.primitives(self.layout(source), 'tremolo')), 8)
def test_percussion_and_tab_use_explicit_display_positions(self):
@@ -393,7 +393,7 @@ class EngravingTests(unittest.TestCase):
heads = [p for p in system.primitives if p.role == 'head']
tabs = [p for p in system.primitives if p.role == 'tab']
self.assertEqual(len(heads), len(tabs))
self.assertEqual(len({p.points[0][1] for p in system.primitives if p.role == 'tab-staff'}), 6)
self.assertEqual(len({p.points[0][1] for p in system.primitives if p.role == 'tab_staff'}), 6)
self.assertLess(max(p.box.bottom for p in system.primitives if not p.role.startswith('tab')), min(p.box.top for p in system.primitives if p.role.startswith('tab')))
self.assertEqual({p.sources[-1] for p in heads}, {p.sources[-1] for p in tabs})
self.assertTrue(all(any(abs(head.points[0][0] - tab.points[0][0]) < 0.01 for head in heads if head.sources[-1] == tab.sources[-1]) for tab in tabs))
@@ -402,16 +402,16 @@ class EngravingTests(unittest.TestCase):
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 = 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')))
self.assertEqual(len(self.primitives(layout, 'tab_tie')), 1)
self.assertEqual(len(self.primitives(layout, 'barline')), len(self.primitives(layout, 'tab_barline')))
for system in (s for page in layout.pages for s in page.systems):
baselines = {role: max(p.points[0][1] for p in system.primitives if p.role == role) for role in ('staff', 'tab-staff')}
baselines = {role: max(p.points[0][1] for p in system.primitives if p.role == role) for role in ('staff', 'tab_staff')}
notation = [p for p in system.primitives if p.role == 'barline']
tabs = [p for p in system.primitives if p.role == 'tab-barline']
tabs = [p for p in system.primitives if p.role == 'tab_barline']
for bar, tab in zip(notation, tabs):
for (x1, y1), (x2, y2) in zip(bar.points, tab.points):
self.assertAlmostEqual(x1, x2)
self.assertAlmostEqual((y1 - baselines['staff']) * 5 * layout.style.tab_space / (4 * layout.style.space), y2 - baselines['tab-staff'])
self.assertAlmostEqual((y1 - baselines['staff']) * 5 * layout.style.tab_space / (4 * layout.style.space), y2 - baselines['tab_staff'])
def test_adjacent_string_tab_numerals_do_not_overlap(self):
content = ''.join(tests.test_musicxml.xml_note(step, octave=octave, prefix='<chord/>' if index else '') for index, (step, octave) in enumerate((('G', 3), ('C', 4), ('E', 4), ('G', 4), ('C', 5), ('E', 5))))
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(content))
@@ -427,8 +427,8 @@ class EngravingTests(unittest.TestCase):
end = '<lyric number="1"><syllabic>end</syllabic><text>lo</text><extend type="start"/></lyric>'
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(duration=1, kind='quarter', extra=begin) + tests.test_musicxml.xml_note(duration=1, kind='quarter', extra=end) + tests.test_musicxml.xml_note(duration=2, kind='half')))
layout = self.layout(source)
self.assertTrue(self.primitives(layout, 'lyric-hyphen'))
self.assertTrue(self.primitives(layout, 'lyric-extension'))
self.assertTrue(self.primitives(layout, 'lyric_hyphen'))
self.assertTrue(self.primitives(layout, 'lyric_extension'))
def test_nested_and_cross_bar_tuplets_keep_span_ownership(self):
modulation = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
content = ''.join(tests.test_musicxml.xml_note(duration=2, kind='quarter', extra=modulation) for _ in range(6))
@@ -469,7 +469,7 @@ class EngravingTests(unittest.TestCase):
layout = self.layout(source)
systems = [s for p in layout.pages for s in p.systems]
self.assertGreater(len(systems), 1)
self.assertTrue(all(any(p.role == 'lyric-extension' for p in s.primitives) for s in systems))
self.assertTrue(all(any(p.role == 'lyric_extension' for p in s.primitives) for s in systems))
def test_audit_catches_removed_pitch_and_accidental_primitives(self):
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note(alter=1)))
layout = self.layout(source)
+10 -8
View File
@@ -30,12 +30,12 @@ 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/songs/*.xml', 'data/rhythms/**/*.xml', 'data/scales/**/*.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)
self.assertIn("if: steps.engraving-cache.outputs.cache-hit != 'true'", workflow)
self.assertIn('key: ${{ steps.engraving-cache.outputs.cache-primary-key }}', workflow)
self.assertIn("hashFiles('generator/**/*.py', 'generator/*.py', 'generator/palette.json', 'data/songs/*.xml', 'data/rhythms/**/*.xml', 'data/scales/**/*.xml')", workflow)
self.assertIn('python3 modulus.py python -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)
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):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
@@ -231,6 +231,8 @@ class EngravingCacheTests(unittest.TestCase):
read_bytes = pathlib.Path.read_bytes
with unittest.mock.patch.object(pathlib.Path, 'read_bytes', lambda path: read_bytes(path) + (b'\n' if path.name == 'raster.py' else b'')):
self.assertNotEqual(expected, generator.engraving.cache.renderer_fingerprint(fonts, style))
with unittest.mock.patch.object(pathlib.Path, 'read_bytes', lambda path: read_bytes(path) + (b'\n' if path.name == 'palette.json' else b'')):
self.assertNotEqual(expected, generator.engraving.cache.renderer_fingerprint(fonts, style))
def test_corrupt_or_missing_pages_are_regenerated_and_repaired(self):
entry = self.seed_cache()
page = entry / '0001.png'
@@ -295,7 +297,7 @@ class EngravingCacheTests(unittest.TestCase):
self.assertIn('fullscore 8, bodyhits 0, bodies 0, renderedpages 0', progress.getvalue())
self.assertEqual(len(list(self.cache.rglob('pages.json'))), 8)
self.assertEqual(len(list(self.cache.rglob('band.json'))), 2)
self.assertFalse(list(self.cache.rglob('.engraving-*')))
self.assertFalse(list(self.cache.rglob('.engraving_*')))
def test_environment_change_gets_new_namespace_and_clear_progress(self):
old = self.seed_cache()
root = self.root / 'new-environment'
@@ -360,4 +362,4 @@ class EngravingCacheTests(unittest.TestCase):
with self.assertRaisesRegex(generator.engraving.types.EngravingError, 'unavailableGlyph'):
generator.__main__.generate_assets(args)
self.assertEqual(original, self.files(output))
self.assertFalse(list(output.parent.glob('.vamp-assets-*')))
self.assertFalse(list(output.parent.glob('.vamp_assets_*')))
+1 -1
View File
@@ -49,7 +49,7 @@ class GuitarTests(unittest.TestCase):
def test_low_open_e_fails_without_octave_fitting_or_mutation(self):
document = tests.test_musicxml.parse(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note('E', octave=2)))
before = copy.deepcopy(document)
with self.assertRaisesRegex(ValueError, 'measure-1, event .*pitch 40.*1-20'):
with self.assertRaisesRegex(ValueError, 'measure_1, event .*pitch 40.*1-20'):
generator.guitar.place_guitar(document)
self.assertEqual(document, before)
def test_open_string_pitch_uses_a_fretted_alternative(self):
+1 -1
View File
@@ -182,7 +182,7 @@ class GuitarPartTests(unittest.TestCase):
content = tests.test_parts.xml_harmony() + tests.test_musicxml.xml_note('E', octave=4)
source = tests.test_musicxml.parse(tests.test_accompaniment.xml_bars([content] * 8))
bar = generator.rhythm.Bar((generator.rhythm.Event(fractions.Fraction(), fractions.Fraction(4), True, 'whole'),), fractions.Fraction(4))
library = generator.rhythm.Library((generator.rhythm.Snippet('4-4/3/whole', '4/4', (bar,)),))
library = generator.rhythm.Library((generator.rhythm.Snippet('4_4/3/whole', '4/4', (bar,)),))
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=library)
generator.model.validate_document(result)
durations = {}
+1 -1
View File
@@ -100,7 +100,7 @@ class HarmonyTests(unittest.TestCase):
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_song_exports_import_without_edits(self):
for name, count, key in (('autumn-leaves', 33, 'D minor'), ('it-could-happen-to-you', 32, 'Eb')):
for name, count, key in (('autumn_leaves', 33, 'D minor'), ('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)
self.assertEqual(source['metadata']['key_signature'], key)
+2 -2
View File
@@ -62,7 +62,7 @@ auto read_file(std::filesystem::path const &path) -> std::string {
}
auto healthy_payload() -> boost::json::value {
return boost::json::object{{"device", "zero"}, {"status", "ready"}, {"product", "product-vamp"}};
return boost::json::object{{"device", "zero"}, {"status", "ready"}, {"product", "product_vamp"}};
}
auto health_router(boost::json::value payload) -> ligo::service_router {
@@ -247,7 +247,7 @@ BOOST_AUTO_TEST_CASE(health_rejects_wrong_device_product_status_and_payload_type
BOOST_AUTO_TEST_CASE(health_rejects_malformed_or_failed_envelopes_and_missing_routes) {
for (std::char_t const *body : {"not JSON", "{}", "[]", "{\"ok\":true}", "{\"ok\":false}",
"{\"device\":\"zero\",\"product\":\"product-vamp\",\"status\":\"ready\"}"}) {
"{\"device\":\"zero\",\"product\":\"product_vamp\",\"status\":\"ready\"}"}) {
auto router = ligo::service_router{};
router.add_response_route("GET", "/api/health", [body](ligo::request_context, boost::json::value) -> boost::asio::awaitable<std::expected<ligo::service_response, modulus::error>> {
co_return ligo::text_response("application/json", body);
+68 -44
View File
@@ -42,6 +42,7 @@ elif command == 'docker':
elif command == 'systemctl' and args[0] == 'cat' and os.environ.get('FIRST_INSTALL'):
sys.exit(1)
elif command == 'nmcli' and args[:4] == ['-t', '-f', 'NAME', 'connection']:
print('product_vamp_control')
print('product-vamp-control')
print('unrelated-network')
'''
@@ -56,17 +57,17 @@ class InstallZeroTests(unittest.TestCase):
self.zero.mkdir(parents=True)
(self.zero / 'config').mkdir()
(self.zero / 'systemd').mkdir()
self.image_tag = 'product-vamp-zero:dev'
self.image_tag = 'product_vamp_zero:dev'
self.write_archive(self.image_tag)
shutil.copyfile(ROOT / 'package/config/vamp.example.json', self.zero / 'config/vamp.example.json')
for name in ('start-zero.sh', 'start-ap.sh'):
for name in ('start_zero.sh', 'start_ap.sh'):
shutil.copyfile(ROOT / 'package/zero' / name, self.zero / name)
for name in ('product-vamp-zero.service', 'product-vamp-ap.service'):
for name in ('product_vamp_zero.service', 'product_vamp_ap.service'):
shutil.copyfile(ROOT / 'package/zero' / name, self.zero / 'systemd' / name)
script = (ROOT / 'package/zero/install-zero.sh').read_text()
script = (ROOT / 'package/zero/install_zero.sh').read_text()
for prefix in ('/tmp/', '/etc/', '/var/', '/run/', '/usr/local/', '/usr/share/'):
script = script.replace(prefix, str(self.root) + prefix)
(self.zero / 'install-zero.sh').write_text(script)
(self.zero / 'install_zero.sh').write_text(script)
for path in ('etc/systemd/system', 'usr/local/bin', 'tmp', 'bin'):
(self.root / path).mkdir(parents=True, exist_ok=True)
for command in ('id', 'dpkg-query', 'apt-get', 'systemctl', 'docker', 'nmcli', 'sleep', 'chvt', 'setterm'):
@@ -78,21 +79,21 @@ class InstallZeroTests(unittest.TestCase):
configuration = b'{"architecture":"arm64","os":"linux"}'
self.image_id = 'sha256:' + hashlib.sha256(configuration).hexdigest()
manifest = json.dumps([{'Config': 'image.json', 'RepoTags': [tag]}]).encode()
with tarfile.open(self.zero / 'product-vamp-zero-linux-arm64.docker.tar', 'w') as archive:
with tarfile.open(self.zero / 'product_vamp_zero_linux_arm64.docker.tar', 'w') as archive:
for name, data in (('manifest.json', manifest), ('image.json', configuration)):
info = tarfile.TarInfo(name)
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
(self.bundle / 'manifest.json').write_text(json.dumps({'zero': {'image_tag': tag}}))
def install(self, **environment):
return subprocess.run(['bash', str(self.zero / 'install-zero.sh')], env={**self.environment, **environment}, text=True, capture_output=True)
return subprocess.run(['bash', str(self.zero / 'install_zero.sh')], env={**self.environment, **environment}, text=True, capture_output=True)
def commands(self):
return [json.loads(line) for line in (self.root / 'commands').read_text().splitlines()]
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', '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'):
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', '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)
path.write_text('previous installation')
@@ -105,79 +106,102 @@ class InstallZeroTests(unittest.TestCase):
self.stale_state()
second = self.install()
self.assertEqual(second.returncode, 0, second.stderr)
self.assertEqual((self.root / 'etc/product-vamp/vamp.json').read_bytes(), (self.zero / 'config/vamp.example.json').read_bytes())
self.assertEqual((self.root / 'etc/product-vamp/image-id').read_text().strip(), self.image_id)
self.assertFalse((self.root / 'etc/product-vamp/obsolete').exists())
self.assertFalse((self.root / 'var/lib/product-vamp').exists())
self.assertFalse((self.root / 'usr/share/product-vamp').exists())
self.assertFalse((self.root / 'etc/systemd/system/product-vamp-zero.service.d').exists())
self.assertFalse((self.root / 'run/systemd/system/product-vamp-ap.service.d').exists())
self.assertEqual(list((self.root / 'run/product-vamp').iterdir()), [])
self.assertEqual((self.root / 'etc/product_vamp/vamp.json').read_bytes(), (self.zero / 'config/vamp.example.json').read_bytes())
self.assertEqual((self.root / 'etc/product_vamp/image_id').read_text().strip(), self.image_id)
self.assertFalse((self.root / 'etc/product_vamp/obsolete').exists())
self.assertFalse((self.root / 'var/lib/product_vamp').exists())
self.assertFalse((self.root / 'usr/share/product_vamp').exists())
self.assertFalse((self.root / 'etc/systemd/system/product_vamp_zero.service.d').exists())
self.assertFalse((self.root / 'run/systemd/system/product_vamp_ap.service.d').exists())
self.assertEqual(list((self.root / 'run/product_vamp').iterdir()), [])
self.assertEqual((self.root / 'var/lib/unrelated/keep').read_text(), 'keep')
self.assertEqual(list((self.root / 'tmp').iterdir()), [])
commands = self.commands()
self.assertIn(['nmcli', 'connection', 'delete', 'product-vamp-control'], commands)
self.assertIn(['nmcli', 'connection', 'delete', 'product_vamp_control'], commands)
self.assertNotIn(['nmcli', 'connection', 'delete', 'unrelated-network'], commands)
self.assertFalse(any('prune' in command for command in commands))
probe = next(index for index, command in enumerate(commands) if command[:2] == ['docker', 'run'])
stop = commands.index(['systemctl', 'stop', 'product-vamp-zero.service'])
stop = commands.index(['systemctl', 'stop', 'product_vamp_zero.service'])
self.assertLess(probe, stop)
def test_failed_preflight_keeps_old_state_and_restores_image_tag(self):
self.stale_state()
result = self.install(FAIL_PREFLIGHT='1')
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.root / 'var/lib/product-vamp/vamp.db').read_text(), 'previous installation')
self.assertEqual((self.root / 'etc/product-vamp/vamp.json').read_text(), 'previous installation')
self.assertNotIn(['systemctl', 'stop', 'product-vamp-zero.service'], self.commands())
self.assertIn(['docker', 'tag', 'sha256:old', 'product-vamp-zero:dev'], self.commands())
def test_failed_health_leaves_services_stopped_without_success(self):
result = self.install(FAIL_HEALTH='1')
self.assertNotEqual(result.returncode, 0)
self.assertNotIn('Zero installed.', result.stdout)
self.assertIn(['systemctl', 'stop', 'product-vamp-zero.service', 'product-vamp-ap.service'], self.commands())
self.assertIn(['docker', 'rm', '-f', 'product-vamp-zero'], self.commands())
def test_symlink_target_aborts_without_deleting_external_data(self):
self.assertEqual((self.root / 'var/lib/product_vamp/vamp.db').read_text(), 'previous installation')
self.assertEqual((self.root / 'etc/product_vamp/vamp.json').read_text(), 'previous installation')
self.assertNotIn(['systemctl', 'stop', 'product_vamp_zero.service'], self.commands())
self.assertIn(['docker', 'tag', 'sha256:old', 'product_vamp_zero:dev'], self.commands())
def test_legacy_paths_are_removed_only_after_successful_preflight(self):
legacy = ('etc/product-vamp/vamp.json', 'var/lib/product-vamp/vamp.db', 'run/product-vamp/heartbeat.json', 'usr/share/product-vamp/catalog.db', 'usr/local/bin/product-vamp-start-zero', 'etc/systemd/system/product-vamp-zero.service', 'etc/systemd/system/product-vamp-zero.service.d/override.conf')
for name in legacy:
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text('previous installation')
failed = self.install(FAIL_PREFLIGHT='1')
self.assertNotEqual(failed.returncode, 0)
self.assertTrue(all((self.root / name).is_file() for name in legacy))
installed = self.install()
self.assertEqual(installed.returncode, 0, installed.stderr)
self.assertFalse(any((self.root / name).exists() for name in legacy))
self.assertIn(['systemctl', 'disable', 'product-vamp-zero.service'], self.commands())
self.assertIn(['nmcli', 'connection', 'delete', 'product-vamp-control'], self.commands())
def test_legacy_symlink_is_rejected_before_cleanup(self):
external = self.root / 'external'
external.mkdir()
(external / 'keep').write_text('keep')
(self.root / 'etc/product-vamp').symlink_to(external, target_is_directory=True)
result = self.install()
self.assertNotEqual(result.returncode, 0)
self.assertEqual((external / 'keep').read_text(), 'keep')
self.assertFalse(any(command[:2] == ['docker', 'load'] for command in self.commands()))
def test_failed_health_leaves_services_stopped_without_success(self):
result = self.install(FAIL_HEALTH='1')
self.assertNotEqual(result.returncode, 0)
self.assertNotIn('Zero installed.', result.stdout)
self.assertIn(['systemctl', 'stop', 'product_vamp_zero.service', 'product_vamp_ap.service'], self.commands())
self.assertIn(['docker', 'rm', '-f', 'product_vamp_zero'], self.commands())
def test_symlink_target_aborts_without_deleting_external_data(self):
external = self.root / 'external'
external.mkdir()
(external / 'keep').write_text('keep')
(self.root / 'etc/product_vamp').symlink_to(external, target_is_directory=True)
result = self.install()
self.assertNotEqual(result.returncode, 0)
self.assertIn('unsafe installation target', result.stderr)
self.assertEqual((external / 'keep').read_text(), 'keep')
self.assertNotIn(['systemctl', 'stop', 'product-vamp-zero.service'], self.commands())
self.assertNotIn(['systemctl', 'stop', 'product_vamp_zero.service'], self.commands())
def test_image_in_use_by_another_container_is_not_removed(self):
result = self.install(SHARED_IMAGE='ancestor=sha256:old')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn(['docker', 'image', 'rm', 'sha256:old'], self.commands())
self.assertIn(['docker', 'image', 'rm', 'sha256:older'], self.commands())
def test_custom_bundle_tag_selects_and_pins_the_incoming_image(self):
self.write_archive('product-vamp-zero:release-test')
self.write_archive('product_vamp_zero:release-test')
result = self.install()
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(['docker', 'image', 'inspect', '--format', '{{.Id}}', 'product-vamp-zero:release-test'], self.commands())
self.assertEqual((self.root / 'etc/product-vamp/image-id').read_text().strip(), self.image_id)
self.assertIn(['docker', 'image', 'inspect', '--format', '{{.Id}}', 'product_vamp_zero:release-test'], self.commands())
self.assertEqual((self.root / 'etc/product_vamp/image_id').read_text().strip(), self.image_id)
def test_wrong_archive_never_resets_the_old_installation(self):
self.stale_state()
(self.bundle / 'manifest.json').write_text(json.dumps({'zero': {'image_tag': 'product-vamp-zero:missing'}}))
(self.bundle / 'manifest.json').write_text(json.dumps({'zero': {'image_tag': 'product_vamp_zero:missing'}}))
result = self.install()
self.assertNotEqual(result.returncode, 0)
self.assertEqual((self.root / 'var/lib/product-vamp/vamp.db').read_text(), 'previous installation')
self.assertEqual((self.root / 'var/lib/product_vamp/vamp.db').read_text(), 'previous installation')
self.assertFalse(any(command[:2] == ['docker', 'load'] for command in self.commands()))
def test_loaded_image_must_match_archive_digest(self):
self.stale_state()
result = self.install(FIXTURE_IMAGE='sha256:unexpected')
self.assertNotEqual(result.returncode, 0)
self.assertIn('does not match', result.stderr)
self.assertEqual((self.root / 'var/lib/product-vamp/vamp.db').read_text(), 'previous installation')
self.assertIn(['docker', 'tag', 'sha256:old', 'product-vamp-zero:dev'], self.commands())
self.assertEqual((self.root / 'var/lib/product_vamp/vamp.db').read_text(), 'previous installation')
self.assertIn(['docker', 'tag', 'sha256:old', 'product_vamp_zero:dev'], self.commands())
def test_startup_uses_pinned_image_without_mounting_legacy_database(self):
result = self.install()
self.assertEqual(result.returncode, 0, result.stderr)
environment = {**self.environment, 'PRODUCT_VAMP_CONFIG_DIR': str(self.root / 'etc/product-vamp'),
'PRODUCT_VAMP_RUN_DIR': str(self.root / 'run/product-vamp'), 'PRODUCT_VAMP_DISPLAY_TTY': str(self.root / 'tty')}
started = subprocess.run(['bash', str(ROOT / 'package/zero/start-zero.sh')], env=environment, text=True, capture_output=True)
environment = {**self.environment, 'PRODUCT_VAMP_CONFIG_DIR': str(self.root / 'etc/product_vamp'),
'PRODUCT_VAMP_RUN_DIR': str(self.root / 'run/product_vamp'), 'PRODUCT_VAMP_DISPLAY_TTY': str(self.root / 'tty')}
started = subprocess.run(['bash', str(ROOT / 'package/zero/start_zero.sh')], env=environment, text=True, capture_output=True)
self.assertEqual(started.returncode, 0, started.stderr)
command = [item for item in self.commands() if item[:2] == ['docker', 'run']][-1]
self.assertIn(self.image_id, command)
self.assertFalse(any('/var/lib/product-vamp' in argument for argument in command))
self.assertFalse(any('/var/lib/product_vamp' in argument for argument in command))
+1 -1
View File
@@ -63,7 +63,7 @@ class KeyCancellationTests(unittest.TestCase):
self.assertEqual(naturals[0].points[0][1], natural_y)
self.assertEqual([p.value for p in glyphs[1:]], ['accidentalSharp' if expected_new > 0 else 'accidentalFlat'] * abs(expected_new))
layout = generator.engraving.layout.layout_score(result, generator.engraving.types.Style(), self.fonts)
reference = 'measure-1:attributes:0' if local else 'measure-2:initial'
reference = 'measure_1:attributes:0' if local else 'measure_2:initial'
drawn = [p for page in layout.pages for p in page.primitives if p.role == 'accidental' and p.sources == (reference,)]
self.assertEqual([p.value for p in drawn], [p.value for p in glyphs])
self.assertEqual([p.points[0][1] - drawn[0].points[0][1] for p in drawn], [p.points[0][1] - glyphs[0].points[0][1] for p in glyphs])
+1 -1
View File
@@ -82,7 +82,7 @@ class MusicModelTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, 'reduced'):
generator.model.validate_document(document)
def test_optional_turnaround_and_form(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/it-could-happen-to-you.xml'))
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/it_could_happen_to_you.xml'))
for part in ('acoustic', 'electric', 'vocal'):
result = generator.arrange.generate.arrange_document(source, part, rhythms=tests.rhythm_fixtures.library())
generator.model.validate_document(result)
+1 -1
View File
@@ -84,7 +84,7 @@ class MusicXMLTests(unittest.TestCase):
def test_voiced_forward_has_its_own_event_id(self):
content = '<forward><duration>1</duration><voice>1</voice></forward>' + xml_note(duration=3, kind='half', extra='<dot/>')
events = notes(parse(xml_score(content)))
self.assertEqual([(n['id'], n['kind']) for n in events], [('spacer-1', 'spacer'), ('note-2', 'note')])
self.assertEqual([(n['id'], n['kind']) for n in events], [('spacer_1', 'spacer'), ('note_2', 'note')])
self.assertEqual([generator.model.fraction(n['duration']) for n in events], [1, 3])
def test_doctype_allowed_but_entities_rejected(self):
source = xml_score(xml_note())
+1 -1
View File
@@ -197,4 +197,4 @@ class PartGenerationTests(unittest.TestCase):
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)
self.assertEqual(json.loads((args.output_dir / 'songs/song-vocal.json').read_text())['metadata']['title'], 'Fixture')
self.assertEqual(json.loads((args.output_dir / 'songs/song_vocal.json').read_text())['metadata']['title'], 'Fixture')
+9 -62
View File
@@ -1,27 +1,19 @@
import hashlib
import pathlib
import subprocess
import sys
import tarfile
import tempfile
import unittest
import unittest.mock
import package.scripts.publish_latest_generic_package
import modulus.artifacts
import modulus.cli
import modulus.config
import modulus.python
class PackagePublishingTests(unittest.TestCase):
def test_configured_packager_matches_publisher(self):
root = pathlib.Path(__file__).resolve().parents[1]
runtime = root / 'build/build_common/modulus'
if not (runtime / 'modulus/cli.py').is_file():
self.skipTest('Configure first to check the exported Modulus packager')
with unittest.mock.patch.object(sys, 'path', [str(runtime), *sys.path]):
import modulus.cli
import modulus.config
with tempfile.TemporaryDirectory() as temporary:
directory = pathlib.Path(temporary)
context = modulus.cli.build_context(directory, modulus.config.load_repo_config(root))
name, registry, targets = package.scripts.publish_latest_generic_package.read_config(root / 'modulus.json')
def fake_build(context, command, **kwargs):
if command[:2] == ['cmake', '--install']:
stage = pathlib.Path(command[2]).parent / 'stage'
@@ -29,54 +21,9 @@ class PackagePublishingTests(unittest.TestCase):
return 0
with unittest.mock.patch.object(modulus.cli, 'in_docker', return_value=True), \
unittest.mock.patch.object(modulus.cli, 'run', side_effect=fake_build), \
unittest.mock.patch.object(modulus.cli, 'run_capture', return_value=subprocess.CompletedProcess([], 0, stdout='fixture\n', stderr='')), \
unittest.mock.patch.object(modulus.cli, 'upload_generic_artifact') as upload:
unittest.mock.patch.object(modulus.python, 'interpreter', return_value=pathlib.Path('/usr/bin/python3')), \
unittest.mock.patch.object(modulus.artifacts, 'main') as publish:
self.assertEqual(modulus.cli.package_artifacts(context, ['--no-remote']), 0)
upload.assert_not_called()
try:
artifacts = package.scripts.publish_latest_generic_package.discover_artifacts(directory / 'build/packages', name, targets)
except SystemExit as error:
self.fail(str(error))
for archive, checksum in zip(artifacts[::2], artifacts[1::2]):
self.assertEqual(checksum.read_text(), f'{hashlib.sha256(archive.read_bytes()).hexdigest()} {archive.name}\n')
with tarfile.open(archive) as contents:
self.assertIn('manifest.json', contents.getnames())
def test_only_configured_target_archives_and_checksums_are_uploaded(self):
with tempfile.TemporaryDirectory() as temporary:
root = pathlib.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(package.scripts.publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release']), [archive, checksum])
checksum.unlink()
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
package.scripts.publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release'])
archive.unlink()
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
package.scripts.publish_latest_generic_package.discover_artifacts(root, 'product-vamp', ['arm_64_gcc12_release'])
def test_mismatched_filenames_report_actual_artifacts_without_uploading(self):
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'request') as request:
archive = pathlib.Path(temporary) / 'product-vamp-abc123-arm_64_gcc12_release.tar.gz'
archive.write_bytes(b'fixture')
with self.assertRaises(SystemExit) as error:
package.scripts.publish_latest_generic_package.main(['--dist-dir', temporary, '--user', 'user', '--token', 'test-token'])
self.assertIn(f'Found: {archive.name}', str(error.exception))
self.assertIn('Publish the current Modulus tooling', str(error.exception))
request.assert_not_called()
def test_missing_artifacts_do_not_touch_the_registry(self):
with tempfile.TemporaryDirectory() as temporary, unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'request') as request:
with self.assertRaisesRegex(SystemExit, 'missing artifact files'):
package.scripts.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 = pathlib.Path('product-vamp-arm_64_gcc12_release.tar.gz')
with unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'discover_artifacts', return_value=[archive]), \
unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'list_versions', return_value=['latest', 'obsolete']), \
unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'delete_version') as delete, \
unittest.mock.patch.object(package.scripts.publish_latest_generic_package, 'upload_file') as upload:
self.assertEqual(package.scripts.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 = package.scripts.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')
publish.assert_not_called()
artifacts = modulus.artifacts.discover_artifacts(directory / 'build/packages', context.package['name'], context.artifact['targets'])
self.assertEqual([p.name for p in artifacts], ['product_vamp_arm_64_gcc12_release.tar.gz', 'product_vamp_arm_64_gcc12_release.tar.gz.sha256'])
+4 -4
View File
@@ -50,15 +50,15 @@ class ScaleLibraryTests(unittest.TestCase):
self.assertEqual(len(scale.pitches), 7)
self.assertEqual(scale.notes[0], root)
self.assertEqual(library.chord_mode(scale, root), (1, scale.label()))
self.assertEqual(library.scale('harmonic-minor', 3, 'C').notes, ('C', 'D', 'E', 'F', 'G#', 'A', 'B'))
self.assertEqual(library.scale('harmonic-minor', 7, 'C').notes, ('C', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bbb'))
self.assertEqual(library.scale('harmonic_minor', 3, 'C').notes, ('C', 'D', 'E', 'F', 'G#', 'A', 'B'))
self.assertEqual(library.scale('harmonic_minor', 7, 'C').notes, ('C', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bbb'))
self.assertEqual(library.scale('major', 1, 'C#').pitches, library.scale('major', 1, 'Db').pitches)
self.assertNotEqual(library.scale('major', 1, 'C#').notes, library.scale('major', 1, 'Db').notes)
def test_incomplete_unexpected_and_inconsistent_xml_patterns_are_rejected(self):
with tempfile.TemporaryDirectory() as directory:
root = pathlib.Path(directory) / 'scales'
shutil.copytree('data/scales', root)
path = root / 'harmonic-minor-scale-modes/0003.xml'
path = root / 'harmonic_minor_scale_modes/0003.xml'
original = path.read_text()
path.unlink()
with self.assertRaisesRegex(ValueError, '21 scale patterns'):
@@ -89,7 +89,7 @@ class ScaleSelectionTests(unittest.TestCase):
self.assertEqual(library.chord_mode(ending[1].scale, 'A'), (5, 'A Phrygian Dominant'))
self.assertEqual(library.chord_mode(ending[4].scale, 'C'), (2, 'C Dorian'))
def test_actual_song_uses_minor_tonic_triads_without_forcing_sevenths(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn-leaves.xml'))
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
spans = generator.scales.timeline.read(source['staves'][0])
choices = generator.scales.selection.select(spans, generator.scales.timeline.Solo(4, spans[-1].end, 'Solo'), tests.scale_fixtures.library())
self.assertTrue(all(c.scale for c in choices))
+14 -13
View File
@@ -29,6 +29,7 @@ import generator.pitch
import generator.playback
import tests.arrangement_fixtures
import generator.score
import generator.theme
import tests.musicxml_roundtrip
import tests.rhythm_fixtures
import tests.test_musicxml
@@ -48,13 +49,13 @@ class ScoreBundleTests(unittest.TestCase):
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)
(data / 'sequences' / '3-4').mkdir(parents=True)
(data / 'sequences/3-4/0001.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
for path in ('rhythms/0001.xml', 'rhythms/drafts/0002.xml', 'sequences/0001.xml', 'sequences/3-4/0002.xml'):
(data / 'sequences' / '3_4').mkdir(parents=True)
(data / 'sequences/3_4/0001.xml').write_text(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
for path in ('rhythms/0001.xml', 'rhythms/drafts/0002.xml', 'sequences/0001.xml', 'sequences/3_4/0002.xml'):
(data / path).write_text('<unfinished')
generator.__main__.generate_assets(args)
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
scale = data / 'scales/major-scale-modes/0002.xml'
scale = data / 'scales/major_scale_modes/0002.xml'
previous = scale.read_bytes()
scale.write_text('<unfinished')
with self.assertRaisesRegex(ValueError, '0002.xml'):
@@ -76,7 +77,7 @@ class ScoreBundleTests(unittest.TestCase):
self.assertEqual(original, {p.relative_to(output): p.read_bytes() for p in output.rglob('*') if p.is_file()})
dense = ''.join(tests.test_musicxml.xml_note('G', 1, '64th') for _ in range(64))
source.write_text(tests.test_musicxml.xml_score(dense, divisions=16))
with self.assertRaisesRegex(generator.engraving.types.EngravingError, r'songs/fixture-electric.json: measure 1 .*complete measure exceeds'):
with self.assertRaisesRegex(generator.engraving.types.EngravingError, r'songs/fixture_electric.json: measure 1 .*complete measure exceeds'):
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(extra='<accidental smufl="unavailableGlyph">other</accidental>')))
@@ -93,7 +94,7 @@ class ScoreBundleTests(unittest.TestCase):
generator.__main__.generate_assets(args)
self.assertFalse(legacy_output.exists())
self.assertFalse(list((output / 'songs').glob('fixture*')))
self.assertEqual(len(list((output / 'songs').glob('renamed-*.json'))), 3)
self.assertEqual(len(list((output / 'songs').glob('renamed_*.json'))), 3)
source.unlink()
generator.__main__.generate_assets(args)
self.assertFalse(list(output.rglob('*.png')))
@@ -151,7 +152,7 @@ class ScoreBundleTests(unittest.TestCase):
tree = xml.etree.ElementTree.fromstring(tests.test_musicxml.xml_score(tests.test_musicxml.xml_note()))
xml.etree.ElementTree.SubElement(tree.find('part'), 'measure', number='2').append(xml.etree.ElementTree.fromstring(tests.test_musicxml.xml_note()))
source = tests.test_musicxml.parse(xml.etree.ElementTree.tostring(tree, encoding='unicode'))
source['staves'][0]['octave_shifts'] = [{'id': 'octave-1', 'start': generator.model.rational(0), 'end': generator.model.rational(4), 'octaves': 1}]
source['staves'][0]['octave_shifts'] = [{'id': 'octave_1', 'start': generator.model.rational(0), 'end': generator.model.rational(4), 'octaves': 1}]
restored = self.round_trip(source)
self.assertEqual(source['staves'][0]['octave_shifts'], restored['staves'][0]['octave_shifts'])
def test_unequal_chord_tones_are_retained_but_never_silently_flattened(self):
@@ -249,14 +250,14 @@ class ScoreBundleTests(unittest.TestCase):
def test_audit_rejects_relocated_tuplet_brackets(self):
modulation = '<time-modification><actual-notes>3</actual-notes><normal-notes>2</normal-notes></time-modification>'
source = tests.test_musicxml.parse(tests.test_musicxml.xml_score(''.join(tests.test_musicxml.xml_note(duration=2, kind='quarter', extra=modulation) for _ in range(6)), divisions=3))
span = {'id': 'tuplet-1', 'voice': '1', 'start': tests.test_musicxml.notes(source)[0]['id'], 'end': tests.test_musicxml.notes(source)[2]['id'], 'note_count': 3, 'space_count': 2, 'depth': 0, 'bracket': 'auto', 'show_number': 'actual'}
span = {'id': 'tuplet_1', 'voice': '1', 'start': tests.test_musicxml.notes(source)[0]['id'], 'end': tests.test_musicxml.notes(source)[2]['id'], 'note_count': 3, 'space_count': 2, 'depth': 0, 'bracket': 'auto', 'show_number': 'actual'}
source['staves'][0]['tuplets'] = [span]
for note in tests.test_musicxml.notes(source)[:3]:
note['tuplet_ids'] = ['tuplet-1']
note['tuplet_ids'] = ['tuplet_1']
changed = copy.deepcopy(source)
changed['staves'][0]['tuplets'][0].update(start=tests.test_musicxml.notes(changed)[3]['id'], end=tests.test_musicxml.notes(changed)[5]['id'])
for index, note in enumerate(tests.test_musicxml.notes(changed)):
note['tuplet_ids'] = ['tuplet-1'] if index >= 3 else []
note['tuplet_ids'] = ['tuplet_1'] if index >= 3 else []
with tempfile.TemporaryDirectory() as temporary:
before, after = pathlib.Path(temporary) / 'source.xml', pathlib.Path(temporary) / 'output.xml'
generator.musicxml_writer.write_musicxml(before, source)
@@ -268,7 +269,7 @@ class ScoreBundleTests(unittest.TestCase):
root = pathlib.Path(temporary)
assets = root / 'assets'
assets.mkdir()
data = pathlib.Path('tests/fixtures/engraving.png').read_bytes()
data = pathlib.Path('tests/fixtures/engraving_python.png').read_bytes()
page = assets / 'page.png'
page.write_bytes(data)
document = generator.musicxml.load_musicxml(pathlib.Path('tests/fixtures/engraving.xml'))
@@ -304,7 +305,7 @@ class ScoreBundleTests(unittest.TestCase):
with unittest.mock.patch('generator.engraving.validation.check_content', wraps=generator.engraving.validation.check_content) as decode:
generator.score.validate_generated_tree(root)
decode.assert_called_once()
PIL.Image.new('RGB', (976, 484), 'white').save(page)
PIL.Image.new('RGB', (976, 484), generator.theme.rgb('background')).save(page)
path = root / 'songs/2.json'
bundle = json.loads(path.read_text())
bundle['presentation']['pages'][0]['sha256'] = hashlib.sha256(page.read_bytes()).hexdigest()
@@ -323,7 +324,7 @@ class ScoreBundleTests(unittest.TestCase):
self.assertEqual(bundle['music'], document)
self.assertEqual(len(bundle['presentation']['pages']), 1)
page = root / bundle['presentation']['pages'][0]['path']
with PIL.Image.open(page) as actual, PIL.Image.open('tests/fixtures/engraving-python.png') as expected:
with PIL.Image.open(page) as actual, PIL.Image.open('tests/fixtures/engraving_python.png') as expected:
self.assertIsNone(PIL.ImageChops.difference(actual.convert('RGB'), expected.convert('RGB')).getbbox())
if before is not None:
self.assertEqual(bundle, before)
+33
View File
@@ -0,0 +1,33 @@
import pathlib
import tempfile
import unittest
import unittest.mock
import generator.theme
class ThemeTests(unittest.TestCase):
def test_foregrounds_remain_readable_on_dark_surfaces(self):
def luminance(color):
channels = [value / 255 for value in color]
linear = [value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4 for value in channels]
return sum(value * weight for value, weight in zip(linear, (0.2126, 0.7152, 0.0722)))
for surface in ('background', 'panel', 'selection'):
background = luminance(generator.theme.rgb(surface))
self.assertLess(background, 0.06)
for name in ('ink', 'muted', 'accent', 'connecting', 'retrying', 'connected', 'danger'):
contrast = (luminance(generator.theme.rgb(name)) + 0.05) / (background + 0.05)
self.assertGreaterEqual(contrast, 4.5, (name, surface))
def test_cpp_colors_match_the_renderer_palette_without_rewriting(self):
with tempfile.TemporaryDirectory() as temporary:
header = pathlib.Path(temporary) / 'palette.hpp'
generator.theme.generate_header(header)
stamp = header.stat().st_mtime_ns
for name in generator.theme.COLORS:
red, green, blue = generator.theme.rgb(name)
self.assertIn(f'{name} = vamp_style::color{{{red}, {green}, {blue}, 255}};', header.read_text())
generator.theme.generate_header(header)
self.assertEqual(stamp, header.stat().st_mtime_ns)
def test_invalid_colors_fail_before_writing(self):
with unittest.mock.patch.dict(generator.theme.COLORS, {'background': '#xyz'}):
with self.assertRaisesRegex(ValueError, 'background'):
generator.theme.rgb('background')
+9 -8
View File
@@ -99,7 +99,7 @@ public:
auto size = MeasureTextEx(font(style.size), std::string{text}.c_str(), style.size, 1.0F);
return {size.x, size.y};
}
Image image{GenImageColor(1024, 600, WHITE)};
Image image{GenImageColor(1024, 600, {vamp_style::background.r, vamp_style::background.g, vamp_style::background.b, 255})};
private:
auto font(std::float32_t size) const -> Font {
auto key = static_cast<std::int32_t>(std::ceil(size));
@@ -133,7 +133,7 @@ auto preview(vamp_zero::app_state const &state, std::string const &name, vamp_pi
auto context = image_context{};
vamp_zero::draw_header(context, {0.0F, 0.0F, 1024.0F, 600.0F}, state);
vamp_zero::draw_menu(context, {0.0F, 80.0F, 1024.0F, 520.0F}, state);
BOOST_REQUIRE(ExportImage(context.image, (directory / ("zero-" + name + ".png")).c_str()));
BOOST_REQUIRE(ExportImage(context.image, (directory / ("zero_" + name + ".png")).c_str()));
auto remote = vamp_pico::remote_display_state{};
BOOST_REQUIRE(vamp_pico::parse_remote_display_body(vamp_zero::remote_display_text(state), remote));
auto title = vamp_pico::title_cache{};
@@ -144,18 +144,19 @@ auto preview(vamp_zero::app_state const &state, std::string const &name, vamp_pi
auto atlas = read_bytes(directory / "static.p4");
auto image = GenImageColor(320, 240, WHITE);
auto row = std::array<std::uint16_t, vamp_pico::display_width>{};
auto dark = std::size_t{};
auto ink = std::size_t{};
for (auto y = 0U; y < 240U; ++y) {
vamp_pico::render_scanline(row, y, remote, link, busy, title, atlas);
for (auto x = 0U; x < 320U; ++x) {
auto pixel = row[x];
auto value = Color{static_cast<std::uint8_t>((pixel >> 11U) * 255U / 31U), static_cast<std::uint8_t>(((pixel >> 5U) & 63U) * 255U / 63U), static_cast<std::uint8_t>((pixel & 31U) * 255U / 31U), 255};
ImageDrawPixel(&image, x, y, value);
dark += value.r < 128U && value.g < 128U && value.b < 128U ? 1U : 0U;
ink += value.r > 128U && value.g > 128U && value.b > 128U ? 1U : 0U;
}
}
BOOST_TEST(dark > 200U);
BOOST_REQUIRE(ExportImage(image, (directory / ("pico-" + name + ".png")).c_str()));
BOOST_TEST(ink > 200U);
BOOST_TEST(ink < 320U * 240U / 2U);
BOOST_REQUIRE(ExportImage(image, (directory / ("pico_" + name + ".png")).c_str()));
UnloadImage(image);
}
@@ -178,7 +179,7 @@ BOOST_AUTO_TEST_CASE(device_menu_rendering_uses_petaluma_and_fits_both_displays)
state.mode = vamp_zero::app_mode::PERFORMANCE_SONG_MENU;
preview(state, "empty");
auto catalog = std::make_shared<vamp_zero::catalog>();
auto score = boost::json::parse(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn-leaves-electric.json"));
auto score = boost::json::parse(read_bytes(std::filesystem::path{VAMP_TEST_ASSETS_DIR} / "songs/autumn_leaves_electric.json"));
auto title = std::string{score.at("presentation").at("title_image").as_string()};
catalog->parts.push_back({.id = "electric", .title = "Autumn Leaves", .composer = "Joseph Kosma", .part = "Electric", .total_pages = 32U, .title_image = title});
catalog->groups.push_back({"Autumn Leaves", "Joseph Kosma", {0U}});
@@ -190,5 +191,5 @@ BOOST_AUTO_TEST_CASE(device_menu_rendering_uses_petaluma_and_fits_both_displays)
preview(state, "parts");
catalog->groups[0].title = std::string(200U, 'M');
catalog->groups[0].composer = std::string(200U, 'W');
preview(state, "long-title");
preview(state, "long_title");
}
+1 -1
View File
@@ -151,7 +151,7 @@ class VoicingTests(unittest.TestCase):
self.assertEqual({key: (p.cost, predecessors[id(p.previous)]) for key, p in actual.items()}, expected)
states = actual
def test_real_autumn_leaves_melody_has_expected_shapes_and_unchanged_notes(self):
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn-leaves.xml'))
source = generator.musicxml.load_musicxml(pathlib.Path('data/songs/autumn_leaves.xml'))
before = copy.deepcopy(source)
result = generator.arrange.generate.arrange_document(source, 'electric', rhythms=tests.rhythm_fixtures.library())
generator.model.validate_document(result)