Cleanup
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""Independently authored operator, fitting-boundary and cache regressions."""
|
||||
import copy
|
||||
import dataclasses
|
||||
from fractions import Fraction as F
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import contextlib
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
|
||||
from generator import resources, vocabulary
|
||||
from generator.solo import expressions, harmonic, operators, selection, types
|
||||
from tests import solo_fixtures as fixture, rhythm_fixtures, scale_fixtures
|
||||
from tests.test_phrase_composer import builder
|
||||
|
||||
class Operators(unittest.TestCase):
|
||||
def test_supported_sixteenth_tail_can_finish_the_allocation(self):
|
||||
b=builder(('CM7',),count=1)
|
||||
actions=b.operators.at(F(15,4))
|
||||
self.assertTrue(any(a.kind=='tone' and a.end==4 for a in actions))
|
||||
# A source/role cutoff does not require a new long cadence.
|
||||
b.start=F(15,4)
|
||||
phrases=b.compose()
|
||||
self.assertEqual(phrases[-1].end,F(4))
|
||||
self.assertLessEqual(sum(len(p.notes) for p in phrases),1)
|
||||
|
||||
def test_source_relation_uses_order_and_does_not_reward_subdivision(self):
|
||||
b=builder(('CM7',),count=1)
|
||||
slow=operators.Action(F(),F(4),tuple(fixture.note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('c','d','e','f'))),'scalar')
|
||||
fast=dataclasses.replace(slow,end=F(2),notes=tuple(dataclasses.replace(n,start=n.start/2,end=n.end/2) for n in slow.notes))
|
||||
self.assertEqual(b.selector.evaluate(slow).raw['prior/source_relation'],b.selector.evaluate(fast).raw['prior/source_relation'])
|
||||
changed=dataclasses.replace(b.analysis,melody=tuple(fixture.note(i,i+1,p,event_id=str(i)) for i,p in enumerate(('e','f','g','a'))))
|
||||
other=selection.Selector(b.harmony,changed,b.selector.center,{})
|
||||
self.assertNotEqual(b.selector.evaluate(slow).raw.get('prior/source_relation',0),other.evaluate(slow).raw.get('prior/source_relation',0))
|
||||
|
||||
def test_shared_fitter_learns_an_independent_synthetic_decision(self):
|
||||
from audition.fit_selection import fit
|
||||
b=builder(('CM7',),count=1)
|
||||
low=operators.Action(F(),F(4),(fixture.note(0,4,'c',event_id='low'),),'tone')
|
||||
high=operators.Action(F(),F(4),(fixture.note(0,4,'e',event_id='high'),),'tone')
|
||||
b.selector.weights={}
|
||||
with patch.object(b.operators,'at',return_value=(low,high)),contextlib.redirect_stdout(io.StringIO()):
|
||||
result=fit([('synthetic','acoustic_solo',b,(high,))],10)
|
||||
self.assertTrue(result['complete'])
|
||||
b.selector.weights=result['weights']['acoustic_solo']
|
||||
self.assertLess(b.selector.evaluate(high).total,b.selector.evaluate(low).total)
|
||||
selection.validate_weights(b.selector.weights)
|
||||
|
||||
def test_fitting_normalization_covers_pickup_plus_whole_note(self):
|
||||
from audition.fit_selection import fit
|
||||
b=builder(('CM7',),count=2);b.end=F(17,4)
|
||||
low=operators.Action(F(),b.end,(fixture.note(0,F(1,4),'c',event_id='lead'),fixture.note(F(1,4),b.end,'e',event_id='low')),'anticipation')
|
||||
high=dataclasses.replace(low,notes=(low.notes[0],fixture.note(F(1,4),b.end,'g',event_id='high')))
|
||||
b.selector.weights={}
|
||||
with patch.object(b.operators,'at',return_value=(low,high)),contextlib.redirect_stdout(io.StringIO()):
|
||||
result=fit([('synthetic','acoustic_solo',b,(high,))],10)
|
||||
self.assertTrue(result['complete'])
|
||||
selection.validate_weights(result['weights']['acoustic_solo'])
|
||||
|
||||
def test_anticipated_reattack_is_not_an_invented_tie(self):
|
||||
b=builder(('C7','FM7'),count=2)
|
||||
actions=b.operators.at(F(7,2))
|
||||
a=next(a for a in actions if a.kind=='anticipation' and a.source=='early_reattack')
|
||||
self.assertEqual(len(a.notes),2)
|
||||
self.assertEqual(a.notes[0].end,a.notes[1].start)
|
||||
self.assertEqual(a.notes[0].pitch,a.notes[1].pitch)
|
||||
self.assertFalse(a.notes[1].continuation)
|
||||
self.assertTrue(b.harmony.evaluate(a.notes,expressions=a.contracts).valid)
|
||||
self.assertFalse(b.harmony.evaluate(a.notes[:1],expressions=a.contracts).valid)
|
||||
|
||||
def test_open_motion_and_all_color_octaves_are_constructed(self):
|
||||
b=builder(('C7','FM7'),count=2)
|
||||
actions=b.operators.at(F())
|
||||
self.assertTrue(any(len(a.notes)==1 and a.notes[0].pitch.number()%12==2 and a.end==F(1,2) for a in actions))
|
||||
h=builder(('E7','Am7'),count=2)
|
||||
values=h.operators.at(F())
|
||||
self.assertTrue(any(a.kind=='color' and len(a.notes)==1 and a.notes[0].pitch.number()==66 for a in values))
|
||||
self.assertTrue(any(a.kind=='rest' for a in values))
|
||||
|
||||
def test_anticipation_color_has_actual_future_resolution(self):
|
||||
h=harmonic.Harmony(fixture.choices(('Dm7','G7','CM7')))
|
||||
held=fixture.note(F(7,2),F(11,2),'c',alter=1,event_id='held')
|
||||
target=fixture.note(F(11,2),7,'d',event_id='target')
|
||||
c=types.ExpressionContract('anticipated','anticipated_color','lydian_color',(held,),target,'G7',boundary=F(4))
|
||||
self.assertTrue(h.evaluate((held,target),expressions=(c,)).valid)
|
||||
self.assertFalse(h.evaluate((held,),expressions=(c,)).valid)
|
||||
changed=dataclasses.replace(target,pitch=dataclasses.replace(target.pitch,octave=5))
|
||||
self.assertFalse(h.evaluate((held,changed),expressions=(c,)).valid)
|
||||
self.assertFalse(h.evaluate((held,target)).valid)
|
||||
|
||||
def test_delayed_approach_is_pending_and_expires(self):
|
||||
h=harmonic.Harmony(fixture.choices(('Dm7',)))
|
||||
approach=fixture.note(0,F(1,2),'c',alter=1,event_id='approach')
|
||||
target=fixture.note(3,F(7,2),'d',event_id='target')
|
||||
c=types.ExpressionContract('delayed','delayed_approach','bounded_semitone_resolution',(approach,),target,'Dm7')
|
||||
r=h.evaluate((approach,),expressions=(c,),until=F(1,2),complete=False)
|
||||
self.assertTrue(r.valid,r.error);self.assertEqual(r.pending,(c,))
|
||||
self.assertFalse(h.evaluate((approach,),expressions=(c,),until=4,complete=True).valid)
|
||||
self.assertTrue(h.evaluate((approach,target),expressions=(c,)).valid)
|
||||
|
||||
def test_distinct_pending_targets_are_visible_to_the_selector(self):
|
||||
b=builder(('Dm7',),count=2)
|
||||
actions=[a for a in b.operators.at(F()) if a.kind=='delayed_approach']
|
||||
first=actions[0]
|
||||
second=next(a for a in actions if a.notes==first.notes and a.contracts[0].target.start!=first.contracts[0].target.start)
|
||||
left,_=b.selector.features(first);right,_=b.selector.features(second)
|
||||
self.assertNotEqual(left,right)
|
||||
selection.validate_weights(dict.fromkeys(left,1))
|
||||
|
||||
def test_missed_pending_target_is_rejected_before_expensive_validation(self):
|
||||
from generator.solo.construction import State
|
||||
b=builder(('Dm7',),count=2)
|
||||
approach=fixture.note(0,F(1,2),'c',alter=1,event_id='approach')
|
||||
target=fixture.note(3,4,'d',event_id='target')
|
||||
c=types.ExpressionContract('delayed','delayed_approach','bounded_semitone_resolution',(approach,),target,'Dm7')
|
||||
state=State(pending=(c,))
|
||||
wrong=operators.Action(F(3),F(4),(fixture.note(3,4,'e',event_id='wrong'),),'tone')
|
||||
with patch.object(b.harmony,'evaluate',side_effect=AssertionError('unnecessary full check')):
|
||||
self.assertIsNone(b.advance(state,None,wrong))
|
||||
good=dataclasses.replace(wrong,notes=(target,))
|
||||
self.assertIsNotNone(b.advance(state,None,good))
|
||||
|
||||
def test_functional_degree_does_not_depend_on_enharmonic_label(self):
|
||||
h=harmonic.Harmony(fixture.choices(('E7','Am7')))
|
||||
n=fixture.note(0,2,'g',event_id='sharp9')
|
||||
c=types.ExpressionContract('color','color','dominant_sharp_ninth',(n,),n,'E7')
|
||||
self.assertTrue(h.evaluate((n,),expressions=(c,)).valid)
|
||||
wrong=dataclasses.replace(n,pitch=dataclasses.replace(n.pitch,alter=F(1)))
|
||||
self.assertFalse(h.evaluate((wrong,),expressions=(dataclasses.replace(c,events=(wrong,),target=wrong),)).valid)
|
||||
|
||||
def test_weight_contributions_and_target_free_features(self):
|
||||
b=builder(count=1)
|
||||
a=next(a for a in b.operators.at(F()) if a.kind=='tone')
|
||||
v=b.selector.evaluate(a)
|
||||
self.assertEqual(v.total,sum(v.contributions.values()))
|
||||
self.assertEqual(v.total,b.selector.cost(a))
|
||||
key='kind/tone';b.selector.weights={key:3}
|
||||
changed=b.selector.evaluate(a)
|
||||
self.assertEqual(changed.total,b.selector.cost(a))
|
||||
self.assertEqual(changed.total-v.total,3*v.raw[key])
|
||||
with self.assertRaises(ValueError):selection.validate_weights({'song/all_of_me':1})
|
||||
|
||||
def test_derivation_cannot_inject_an_illegal_reference_note(self):
|
||||
from audition.fit_selection import derive
|
||||
b=builder(('CM7',),count=1)
|
||||
target={'alignment':{'encoded_offset':'0'},'sounding_events':[{'start':'0','end':'4','sounding_midi':'99','pitch':{},'measure':1}]}
|
||||
path,result=derive(b,target)
|
||||
self.assertFalse(result['complete']);self.assertFalse(path)
|
||||
|
||||
class CompiledResources(unittest.TestCase):
|
||||
def test_compiled_and_xml_libraries_have_identical_contents(self):
|
||||
original=(scale_fixtures.library(),fixture.library(),rhythm_fixtures.library())
|
||||
import json
|
||||
encoded=json.loads(json.dumps(resources.encode(original),default=str))
|
||||
restored=resources.decode(encoded)
|
||||
self.assertEqual(original[0],restored[0])
|
||||
self.assertEqual(original[1].paths,restored[1].paths)
|
||||
self.assertEqual(original[2].snippets,restored[2].snippets)
|
||||
|
||||
def test_warm_cache_avoids_xml_import_and_corruption_recompiles(self):
|
||||
original=(scale_fixtures.library(),fixture.library(),rhythm_fixtures.library())
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
with patch('generator.resources.scales.load_library',return_value=original[0]) as sc,patch('generator.resources.sequences.load_library',return_value=original[1]),patch('generator.resources.rhythm.load_library',return_value=original[2]):
|
||||
resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root)
|
||||
self.assertEqual(sc.call_count,1)
|
||||
resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root)
|
||||
self.assertEqual(sc.call_count,1)
|
||||
next((Path(root)/'_resources').glob('*.json')).write_text('{}')
|
||||
resources._load.cache_clear();resources.load(Path(root),{'fixture':'1'},root)
|
||||
self.assertEqual(sc.call_count,2)
|
||||
+3
-1
@@ -193,7 +193,9 @@ class PartGenerationTests(unittest.TestCase):
|
||||
unittest.mock.patch('generator.playback.compile_tree'),
|
||||
):
|
||||
generator.__main__.generate_assets(args)
|
||||
load_library.assert_called_once_with(args.data_dir.resolve() / 'rhythms')
|
||||
# Role catalog loading is replaced by this infrastructure fixture. The
|
||||
# parent asset pipeline must not perform a redundant catalog parse.
|
||||
load_library.assert_not_called()
|
||||
self.assertEqual(load.call_args_list, [unittest.mock.call(args.data_dir.resolve() / 'songs/other.xml'), unittest.mock.call(args.data_dir.resolve() / 'songs/song.xml')])
|
||||
self.assertEqual(normalize.call_count, 2)
|
||||
original = {p.relative_to(args.output_dir): p.read_bytes() for p in args.output_dir.rglob('*') if p.is_file()}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""General, independently authored capabilities; no optional reference inputs."""
|
||||
import copy,dataclasses,unittest
|
||||
"""General, independently authored capabilities of the active action search."""
|
||||
import dataclasses, unittest
|
||||
from fractions import Fraction as F
|
||||
from unittest.mock import patch
|
||||
from generator import vocabulary,model
|
||||
from generator.scales import selection,timeline
|
||||
from generator.solo import analysis,construction,harmonic,profiles,types,scoring,composer,routes,placement
|
||||
from tests import solo_fixtures as fixtures,rhythm_fixtures,scale_fixtures
|
||||
from generator import model, vocabulary
|
||||
from generator.scales import selection, timeline
|
||||
from generator.solo import analysis, construction, harmonic, profiles, types, placement, operators
|
||||
from tests import solo_fixtures as fixtures, rhythm_fixtures, scale_fixtures
|
||||
|
||||
|
||||
def builder(symbols=('C7','FM7','Dm7','G7'),seed='independent',count=8):
|
||||
@@ -15,111 +14,78 @@ def builder(symbols=('C7','FM7','Dm7','G7'),seed='independent',count=8):
|
||||
return construction.Builder(a,ms,harmonic.Harmony(cs,profiles.Instrument(reference=60)),fixtures.library(),rhythm_fixtures.library(),end,seed)
|
||||
|
||||
class PhraseConstruction(unittest.TestCase):
|
||||
def test_complete_candidates_have_varied_functions_and_joint_expression(self):
|
||||
b=builder();values=[b.propose(F(),(),i) for i in range(64)];values=[p for p in values if p]
|
||||
self.assertTrue(values)
|
||||
self.assertIn('open',{p.ending for p in values})
|
||||
self.assertTrue(any(p.destination and p.destination.duration>=2 for p in values))
|
||||
self.assertTrue(any(p.expressions for p in values))
|
||||
for p in values:
|
||||
self.assertTrue(b.harmony.evaluate(p.notes,until=p.end,complete=True,expressions=p.expressions).valid)
|
||||
self.assertEqual(p.score,p.evaluation.total)
|
||||
self.assertTrue(all(p.start<=n.start<n.end<=p.end for n in p.notes))
|
||||
def test_candidates_include_open_motion_arrivals_and_joint_expression(self):
|
||||
b=builder(count=2);values=b.operators.at(F())
|
||||
self.assertTrue(any(a.kind=='tone' and a.end==F(1,2) for a in values))
|
||||
self.assertTrue(any(len(a.notes)==1 and a.end>=2 for a in values))
|
||||
self.assertTrue(any(a.contracts for a in values))
|
||||
for a in values:
|
||||
checked=b.harmony.evaluate(a.notes,until=a.end,complete=False,expressions=a.contracts)
|
||||
self.assertTrue(checked.valid,checked.error)
|
||||
evaluation=b.selector.evaluate(a)
|
||||
self.assertEqual(evaluation.total,sum(evaluation.contributions.values()))
|
||||
self.assertTrue(all(a.start<=n.start<n.end<=a.end for n in a.notes))
|
||||
|
||||
def test_all_operations_are_eligible_and_construct_complete_music(self):
|
||||
b=builder(count=12);first=b.candidates(F(),())[0]
|
||||
history=(first,dataclasses.replace(first,id='second'))
|
||||
self.assertEqual(set(b.actions(history)),set(vocabulary.data('solo')['actions'])-{'head'})
|
||||
history=(*history,dataclasses.replace(first,id='third'))
|
||||
self.assertEqual(set(b.actions(history)),set(vocabulary.data('solo')['actions']))
|
||||
# Request each operation through the same active constructor in eligible contexts.
|
||||
for action in b.actions(history):
|
||||
with self.subTest(action=action),patch.object(b,'actions',return_value=[action]):
|
||||
candidates=[b.propose(F(16),history,i) for i in range(32)]
|
||||
self.assertTrue(any(p and p.purpose==action for p in candidates))
|
||||
def test_audible_theme_returns_and_answers_are_reachable(self):
|
||||
b=builder(('CM7',),count=2)
|
||||
home=(fixtures.note(0,F(1,2),'c'),fixtures.note(F(1,2),1,'d'),fixtures.note(1,2,'e'))
|
||||
for kind in ('return','answer'):
|
||||
values=b.operators.related(F(4),home,kind)
|
||||
self.assertTrue(values,kind)
|
||||
self.assertTrue(all(len(a.notes)==len(home) and a.kind==kind for a in values))
|
||||
|
||||
def test_head_is_optional_not_required_rhythm(self):
|
||||
b=builder();b.analysis=dataclasses.replace(b.analysis,melody=())
|
||||
values=b.candidates(F(),())
|
||||
self.assertTrue(values)
|
||||
self.assertTrue(all(p.purpose!='head' for p in values))
|
||||
|
||||
def test_recomposed_opening_is_not_source_hold(self):
|
||||
source=fixtures.song(count=4,chords=('CM7',))
|
||||
n=source['staves'][0]['measures'][0]['voices'][0]['notes'][0]
|
||||
source['staves'][0]['measures'][0]['voices'][0]['notes']=[{**n,'duration':model.rational(4),'pitches':[{**n['pitches'][0],'duration':model.rational(4)}]}]
|
||||
result=fixtures.compose(source)
|
||||
self.assertTrue(result.notes());self.assertLess(result.notes()[0].end-result.notes()[0].start,4)
|
||||
|
||||
def test_disabled_expressions_are_strict_and_reproducible(self):
|
||||
b=builder();b.enabled=False
|
||||
def test_disabled_expressions_stay_strict(self):
|
||||
b=builder(count=1);b.operators.enabled=False
|
||||
ps=b.compose();ns=tuple(n for p in ps for n in p.notes)
|
||||
self.assertFalse(any(p.expressions for p in ps))
|
||||
self.assertTrue(all(n.pitch.number()%12 in types.permitted(b.harmony.choices,n.start,n.end) for n in ns))
|
||||
self.assertEqual(ps,b.compose())
|
||||
again=b.compose()
|
||||
self.assertEqual(ps,again)
|
||||
|
||||
def test_role_domains_and_complete_tied_position(self):
|
||||
for role in ('acoustic','electric'):
|
||||
p=profiles.for_part(role)
|
||||
self.assertEqual(p.strings,(1,2,3,4,5,6))
|
||||
p=profiles.for_part(role);self.assertEqual(p.strings,(1,2,3,4,5,6))
|
||||
for midi in range(41,77):self.assertTrue(placement.positions(midi,p))
|
||||
a=fixtures.note(0,2,'e',event_id='held');ns=(a,dataclasses.replace(a,start=F(2),end=F(4),continuation=True))
|
||||
positioned=placement.place(ns,F(120))[1]
|
||||
self.assertEqual(positioned[0].position,positioned[1].position)
|
||||
self.assertEqual(len(types.sounding_notes(positioned)),1)
|
||||
|
||||
def test_routes_keep_actual_pitches_across_collection_change(self):
|
||||
def test_complete_routes_cross_boundaries_without_a_forced_chord_tone(self):
|
||||
b=builder(('C7','DmM7'),count=2)
|
||||
ns=(fixtures.note(3,F(7,2),'g'),fixtures.note(F(7,2),4,'a'),fixtures.note(4,F(9,2),'c'),fixtures.note(F(9,2),5,'d'))
|
||||
values=b.routes.realize(ns,{0,3},'scalar','up','crossing',64)
|
||||
self.assertTrue(values)
|
||||
for route in values:
|
||||
self.assertEqual(route[0].pitch,ns[0].pitch);self.assertEqual(route[-1].pitch,ns[-1].pitch)
|
||||
self.assertTrue(all(n.pitch.number()%12 in types.permitted(b.harmony.choices,n.start,n.end) for n in route))
|
||||
values=b.operators.at(F(3))
|
||||
crossed=[a for a in values if a.start<4<a.end and a.kind=='scalar']
|
||||
self.assertTrue(crossed)
|
||||
for a in crossed:self.assertTrue(b.harmony.evaluate(a.notes).valid)
|
||||
one=builder(('C7',),count=1)
|
||||
self.assertTrue(any(a.kind=='tone' and a.notes[0].pitch.number()%12==5 and a.end==F(1,2) for a in one.operators.at(F())))
|
||||
|
||||
def test_no_internal_chordal_endpoint_requirement(self):
|
||||
b=builder(('C7',),count=2)
|
||||
ns=(fixtures.note(0,F(1,2),'e'),fixtures.note(F(1,2),1,'f'))
|
||||
paths=b.routes.realize(ns,{0,1},'scalar','up','open',64)
|
||||
self.assertTrue(paths)
|
||||
self.assertTrue(b.harmony.evaluate(paths[0]).valid)
|
||||
self.assertNotIn(paths[0][-1].pitch.number()%12,b.harmony.at(F(1,2)).roles.basic)
|
||||
def test_actual_incoming_state_changes_ranking(self):
|
||||
b=builder(count=1)
|
||||
a=b.candidates(F(),construction.State(recent=(fixtures.note(-1,0,'c',octave=3),)))
|
||||
c=b.candidates(F(),construction.State(recent=(fixtures.note(-1,0,'c',octave=5),)))
|
||||
self.assertNotEqual([v[1].key for v in a],[v[1].key for v in c])
|
||||
|
||||
def test_meaningful_incoming_state_changes_continuations(self):
|
||||
b=builder();history=(b.candidates(F(),())[0],)
|
||||
a=b.candidates(history[-1].end,history)
|
||||
old=history[0];last=old.notes[-1]
|
||||
changed=dataclasses.replace(old,id='changed',notes=(*old.notes[:-1],dataclasses.replace(last,pitch=dataclasses.replace(last.pitch,octave=last.pitch.octave+1))))
|
||||
c=b.candidates(changed.end,(changed,))
|
||||
self.assertNotEqual(tuple(p.notes for p in a),tuple(p.notes for p in c))
|
||||
def test_handoff_is_not_forced_to_have_a_long_note(self):
|
||||
b=builder(count=1)
|
||||
values=b.operators.at(F(7,2))
|
||||
self.assertTrue(any(a.kind=='tone' and a.end==4 for a in values))
|
||||
self.assertTrue(any(a.kind=='rest' and a.end==4 for a in values))
|
||||
|
||||
def test_no_short_accidental_handoff_tail(self):
|
||||
b=builder(count=4);ps=b.compose()
|
||||
self.assertEqual(ps[-1].end,b.end)
|
||||
if ps[-1].destination:self.assertGreaterEqual(ps[-1].destination.duration,2)
|
||||
|
||||
def test_cache_restores_exact_music_without_source_ancestry_growth(self):
|
||||
b=builder(count=4);cold=b.candidates(F(),());warm=b.candidates(F(),())
|
||||
self.assertEqual(cold,warm)
|
||||
self.assertTrue(all(not n.sources for p in cold for n in p.notes))
|
||||
def test_cached_actions_do_not_copy_source_ancestry(self):
|
||||
b=builder(count=1);a=b.operators.at(F());self.assertIs(a,b.operators.at(F()))
|
||||
self.assertTrue(all(not n.sources for action in a for n in action.notes))
|
||||
|
||||
class EvaluationContract(unittest.TestCase):
|
||||
def phrase(self,ns,**kwargs):return types.Phrase('test',F(),F(8),tuple(ns),'scalar','up','open',**kwargs)
|
||||
def test_configured_terms_are_authoritative_and_nonzero(self):
|
||||
ns=(fixtures.note(0,1,'c',octave=3),fixtures.note(1,2,'e',octave=4),fixtures.note(2,4,'c',octave=4))
|
||||
p=self.phrase(ns);incoming=fixtures.note(-1,0,'c',octave=5)
|
||||
base=scoring.evaluate(p,incoming,(p,),40)
|
||||
self.assertEqual(base.total,sum(base.contributions.values()))
|
||||
for name,value in base.raw.items():
|
||||
self.assertGreater(value,0,name)
|
||||
altered=scoring.evaluate(p,incoming,(p,),40,weights={**base.weights,name:base.weights[name]+1})
|
||||
self.assertAlmostEqual(altered.total-base.total,value)
|
||||
def test_silence_is_explicit_and_tie_partition_is_invariant(self):
|
||||
a=fixtures.note(0,4,'e',event_id='a')
|
||||
split=(dataclasses.replace(a,end=F(2)),dataclasses.replace(a,start=F(2),continuation=True))
|
||||
self.assertEqual(scoring.evaluate(self.phrase((a,)),None,(),60),scoring.evaluate(self.phrase(split),None,(),60))
|
||||
self.assertFalse(scoring.evaluate(self.phrase(()),None,(),60).valid)
|
||||
self.assertTrue(scoring.evaluate(dataclasses.replace(self.phrase(()),purpose='space'),None,(),60).valid)
|
||||
def test_tie_partition_is_invariant_and_reattack_is_distinct(self):
|
||||
b=builder(('C7',),count=1);n=fixtures.note(0,4,'e',event_id='hold')
|
||||
a=operators.Action(F(),F(4),(n,),'tone')
|
||||
split=(dataclasses.replace(n,end=F(2)),dataclasses.replace(n,start=F(2),continuation=True))
|
||||
x=b.selector.evaluate(a);y=b.selector.evaluate(dataclasses.replace(a,notes=split))
|
||||
self.assertEqual(x,y)
|
||||
reattack=tuple(dataclasses.replace(n,continuation=False) for n in split)
|
||||
self.assertNotEqual(x,b.selector.evaluate(dataclasses.replace(a,notes=reattack)))
|
||||
|
||||
def test_target_touch_and_repeated_attacks_do_not_fulfill_hold(self):
|
||||
a=fixtures.note(2,4,'e',event_id='arrival')
|
||||
anchor=types.Anchor('held',F(2),F(2),a.pitch,frozenset({4}),(41,76),F(2),'terminal','C7',hold_until=F(4))
|
||||
|
||||
@@ -39,6 +39,10 @@ class ScoreBundleTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tests.production_fixtures.install(self)
|
||||
def test_real_asset_regeneration_removes_pages_and_rolls_back_font_failure(self):
|
||||
from generator import resources
|
||||
# Validation now belongs to the catalog-consuming worker, not a redundant
|
||||
# parent parse. Exercise its real content keys/importer in this rollback test.
|
||||
self.enterContext(unittest.mock.patch('generator.production._solo_libraries',side_effect=lambda directory,keys,cache=None:resources.load(directory,cache_dir=cache)))
|
||||
self.enterContext(unittest.mock.patch('generator.rhythm.load_library', return_value=tests.rhythm_fixtures.library()))
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = pathlib.Path(temporary)
|
||||
|
||||
@@ -7,10 +7,13 @@ from pathlib import Path
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
from audition import reference_review, solo_compare
|
||||
from generator import vocabulary, production
|
||||
from generator.solo import export, harmonic, notation, placement, routes, types
|
||||
from generator.solo import export, harmonic, notation, placement, types
|
||||
from tests.solo_fixtures import song
|
||||
from tests.solo_fixtures import note
|
||||
from tests.solo_fixtures import choices
|
||||
@@ -26,6 +29,17 @@ def pitched(duration, tie='', octave=5):
|
||||
|
||||
|
||||
class ComparisonTests(unittest.TestCase):
|
||||
def test_exact_cli_is_a_real_exit_gate(self):
|
||||
args=['solo_compare','--generated-dir','unused','--output','unused','--reference','fixture/acoustic=unused','--require-exact']
|
||||
review={'exact_musical_match':False,'exact_matching_attacks':0,'differences':[{}],'rest_difference':True}
|
||||
with patch('sys.argv',args),patch.object(solo_compare,'run',return_value={'solos':{'fixture/acoustic':review}}),contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(solo_compare.main(),1)
|
||||
review.update(exact_musical_match=True,differences=[],rest_difference=False)
|
||||
self.assertEqual(solo_compare.main(),0)
|
||||
with patch('sys.argv',args[:-3]+['--require-exact']),contextlib.redirect_stderr(io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as error:solo_compare.main()
|
||||
self.assertEqual(error.exception.code,2)
|
||||
|
||||
def test_note_container_does_not_turn_color_label_into_fingering(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
note_xml=pitched(24).replace('</note>','<notations><technical><fingering>6</fingering></technical></notations></note>')
|
||||
|
||||
Reference in New Issue
Block a user