import dataclasses, unittest from fractions import Fraction as F from generator.solo import types, harmonic, groups from tests.solo_fixtures import note,choices def melody(rows):return tuple(note(a,b,p,event_id=str(i)) for i,(a,b,p) in enumerate(rows)) class RhythmGroups(unittest.TestCase): def test_complete_triplet_can_be_displaced_without_offsetting_binary_tail(self): pattern = ((F(1, 2), F(5, 6)), (F(5, 6), F(7, 6)), (F(7, 6), F(3, 2)), (F(3, 2), F(5, 2))) result = groups.describe(pattern) self.assertEqual(result[0].family, 'triplet') self.assertEqual(result[-1].family, 'binary') self.assertEqual(result[0].displacement, F(1, 2)) def test_partial_triplet_does_not_drift_into_binary(self): self.assertFalse(groups.valid(((F(), F(1, 3)), (F(1, 3), F(5, 6))))) self.assertFalse(groups.valid(((F(1, 3), F(4, 3)),))) self.assertEqual(groups.describe(((F(1, 3), F(4, 3)),), imported=True)[0].family, 'source') def test_relative_rhythm_identity_does_not_count_absolute_onsets(self): original = melody(((0, 1, 'c'), (1, 2, 'e'), (2, 3, 'd'))) self.assertEqual(len(set(groups.signature(original))), 1) moved = tuple((dataclasses.replace(n, start=n.start + 13, end=n.end + 13) for n in original)) self.assertEqual(groups.signature(original), groups.signature(moved)) def test_broadening_never_cuts_inside_tuplet(self): pattern = ((F(), F(1, 3)), (F(1, 3), F(2, 3)), (F(2, 3), F(1))) self.assertEqual(groups.broaden(pattern, F(1, 2), F(4)), ()) class PendingTreatments(unittest.TestCase): def test_complete_route_can_cross_a_search_frontier(self): from tests.test_independent_generation import TypedExpressionContracts fixture = TypedExpressionContracts() fixture.setUp() notes, contract = fixture.make() partial = fixture.h.evaluate(notes[:2], until=F(2), complete=False, expressions=(contract,)) self.assertTrue(partial.valid, partial.error) self.assertEqual(partial.pending, (contract,)) self.assertFalse(fixture.h.evaluate(notes[:2], until=F(3), expressions=(contract,)).valid) complete = fixture.h.evaluate(notes, until=F(4), complete=True, expressions=(contract,)) self.assertTrue(complete.valid) self.assertFalse(complete.pending) class InputFirewall(unittest.TestCase): def test_read_write_handles_cannot_bypass_boundary(self): import tempfile from pathlib import Path from generator.input_audit import composition with tempfile.TemporaryDirectory() as folder: forbidden = Path(folder) / 'target.xml' forbidden.write_text('not musical input') for mode in ('r', 'r+'): with self.assertRaisesRegex(ValueError, 'Forbidden composition input'): with composition(Path('data'), 'electric_solo'): with forbidden.open(mode) as stream: stream.read() class PreparedSuspensions(unittest.TestCase): def test_preparation_and_delayed_resolution_are_required(self): h = harmonic.Harmony(choices(('Gm7', 'C7'))) held = note(F(5, 2), F(9, 2), 'f', event_id='held') target = note(F(9, 2), 6, 'e', event_id='resolution') contract = types.ExpressionContract('prepared', 'suspension', 'prepared_step_down', (held,), target, 'C7', boundary=F(4)) self.assertTrue(h.evaluate((held, target), expressions=(contract,)).valid) self.assertFalse(h.evaluate((held, target)).valid) changed = dataclasses.replace(held, start=F(15, 4)) self.assertFalse(h.evaluate((changed, target), expressions=(dataclasses.replace(contract, events=(changed,)),)).valid) self.assertFalse(h.evaluate((held,), expressions=(contract,)).valid)