import dataclasses, unittest import fractions import generator.solo.types import generator.solo.harmonic import generator.solo.groups import tests.solo_fixtures import tests.test_independent_generation import tempfile import pathlib import generator.input_audit def melody(rows): return tuple(tests.solo_fixtures.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 = ((fractions.Fraction(1, 2), fractions.Fraction(5, 6)), (fractions.Fraction(5, 6), fractions.Fraction(7, 6)), (fractions.Fraction(7, 6), fractions.Fraction(3, 2)), (fractions.Fraction(3, 2), fractions.Fraction(5, 2))) result = generator.solo.groups.describe(pattern) self.assertEqual(result[0].family, 'triplet') self.assertEqual(result[-1].family, 'binary') self.assertEqual(result[0].displacement, fractions.Fraction(1, 2)) def test_partial_triplet_does_not_drift_into_binary(self): self.assertFalse(generator.solo.groups.valid(((fractions.Fraction(), fractions.Fraction(1, 3)), (fractions.Fraction(1, 3), fractions.Fraction(5, 6))))) self.assertFalse(generator.solo.groups.valid(((fractions.Fraction(1, 3), fractions.Fraction(4, 3)),))) self.assertEqual(generator.solo.groups.describe(((fractions.Fraction(1, 3), fractions.Fraction(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(generator.solo.groups.signature(original))), 1) moved = tuple((dataclasses.replace(n, start=n.start + 13, end=n.end + 13) for n in original)) self.assertEqual(generator.solo.groups.signature(original), generator.solo.groups.signature(moved)) def test_broadening_never_cuts_inside_tuplet(self): pattern = ((fractions.Fraction(), fractions.Fraction(1, 3)), (fractions.Fraction(1, 3), fractions.Fraction(2, 3)), (fractions.Fraction(2, 3), fractions.Fraction(1))) self.assertEqual(generator.solo.groups.broaden(pattern, fractions.Fraction(1, 2), fractions.Fraction(4)), ()) class PendingTreatments(unittest.TestCase): def test_complete_route_can_cross_a_search_frontier(self): fixture = tests.test_independent_generation.TypedExpressionContracts() fixture.setUp() notes, contract = fixture.make() partial = fixture.h.evaluate(notes[:2], until=fractions.Fraction(2), complete=False, expressions=(contract,)) self.assertTrue(partial.valid, partial.error) self.assertEqual(partial.pending, (contract,)) self.assertFalse(fixture.h.evaluate(notes[:2], until=fractions.Fraction(3), expressions=(contract,)).valid) complete = fixture.h.evaluate(notes, until=fractions.Fraction(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): with tempfile.TemporaryDirectory() as folder: forbidden = pathlib.Path(folder) / 'target.xml' forbidden.write_text('not musical input') for mode in ('r', 'r+'): with self.assertRaisesRegex(ValueError, 'Forbidden composition input'): with generator.input_audit.composition(pathlib.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 = generator.solo.harmonic.Harmony(tests.solo_fixtures.choices(('Gm7', 'C7'))) held = tests.solo_fixtures.note(fractions.Fraction(5, 2), fractions.Fraction(9, 2), 'f', event_id='held') target = tests.solo_fixtures.note(fractions.Fraction(9, 2), 6, 'e', event_id='resolution') contract = generator.solo.types.ExpressionContract('prepared', 'suspension', 'prepared_step_down', (held,), target, 'C7', boundary=fractions.Fraction(4)) self.assertTrue(h.evaluate((held, target), expressions=(contract,)).valid) self.assertTrue(h.evaluate((held, target)).valid) self.assertFalse(h.evaluate((held, target), infer=False).valid) changed = dataclasses.replace(held, start=fractions.Fraction(15, 4)) self.assertFalse(h.evaluate((changed, target), expressions=(dataclasses.replace(contract, events=(changed,)),)).valid) self.assertFalse(h.evaluate((held,), expressions=(contract,)).valid)