diff --git a/tests/test_compact_sets.py b/tests/test_compact_sets.py index 6d3dc4e..b7221b3 100644 --- a/tests/test_compact_sets.py +++ b/tests/test_compact_sets.py @@ -1,151 +1,203 @@ import pytest import sys -import os -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, "src") -import compact_sets_optimized_2 as cs +from src.pitch import Pitch, DIMS_4, DIMS_7 +from src.chord import Chord +from src.harmonic_space import HarmonicSpace -@pytest.fixture(autouse=True) -def reset_dims(): - cs.dims = cs.DIMS_8 - yield - cs.dims = cs.DIMS_8 +class TestPitch: + """Tests for Pitch class.""" + def test_fundamental(self): + p = Pitch((0, 0, 0, 0), DIMS_4) + assert p.to_fraction() == 1 -class TestPitchConversion: - def test_hs_array_to_fr_fundamental(self): - root = (0, 0, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_fr(root) == 1.0 + def test_octave(self): + p = Pitch((1, 0, 0, 0), DIMS_4) + assert p.to_fraction() == 2 - def test_hs_array_to_fr_octave(self): - octave = (1, 0, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_fr(octave) == 2.0 + def test_fifth(self): + p = Pitch((0, 1, 0, 0), DIMS_4) + assert p.to_fraction() == 3 - def test_hs_array_to_fr_fifth(self): - fifth = (0, 1, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_fr(fifth) == 3.0 + def test_compound(self): + p = Pitch((1, 1, 0, 0), DIMS_4) + assert p.to_fraction() == 6 - def test_hs_array_to_fr_compound(self): - pitch = (1, 1, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_fr(pitch) == 6.0 + def test_cents_fundamental(self): + p = Pitch((0, 0, 0, 0), DIMS_4) + assert p.to_cents() == 0.0 - def test_hs_array_to_cents_fundamental(self): - root = (0, 0, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_cents(root) == 0.0 + def test_cents_octave(self): + p = Pitch((1, 0, 0, 0), DIMS_4) + assert p.to_cents() == 1200.0 - def test_hs_array_to_cents_octave(self): - octave = (1, 0, 0, 0, 0, 0, 0, 0) - assert cs.hs_array_to_cents(octave) == 1200.0 + def test_cents_fifth(self): + p = Pitch((0, 1, 0, 0), DIMS_4) + assert abs(p.to_cents() - 1901.96) < 0.01 + def test_transpose_positive(self): + p = Pitch((0, 1, 0, 0), DIMS_4) + trans = Pitch((1, 0, 0, 0), DIMS_4) + result = p.transpose(trans) + assert result.hs_array == (1, 1, 0, 0) -class TestPitchManipulation: - def test_transpose_pitch(self): - pitch = (0, 1, 0, 0, 0, 0, 0, 0) - trans = (1, 0, 0, 0, 0, 0, 0, 0) - result = cs.transpose_pitch(pitch, trans) - assert result == (1, 1, 0, 0, 0, 0, 0, 0) + def test_transpose_negative(self): + p = Pitch((1, 1, 0, 0), DIMS_4) + trans = Pitch((-1, 0, 0, 0), DIMS_4) + result = p.transpose(trans) + assert result.hs_array == (0, 1, 0, 0) - def test_transpose_pitch_negative(self): - pitch = (1, 1, 0, 0, 0, 0, 0, 0) - trans = (-1, 0, 0, 0, 0, 0, 0, 0) - result = cs.transpose_pitch(pitch, trans) - assert result == (0, 1, 0, 0, 0, 0, 0, 0) - - def test_transpose_chord(self): - chord = ((0, 0, 0, 0, 0, 0, 0, 0), (0, 1, 0, 0, 0, 0, 0, 0)) - trans = (1, 0, 0, 0, 0, 0, 0, 0) - result = cs.transpose_chord(chord, trans) - assert result == ((1, 0, 0, 0, 0, 0, 0, 0), (1, 1, 0, 0, 0, 0, 0, 0)) - - def test_expand_pitch(self): - pitch = (0, 1, 0, 0, 0, 0, 0, 0) - result = cs.expand_pitch(pitch) - assert isinstance(result, tuple) - - def test_collapse_pitch(self): - pitch = (3, 1, 0, 0, 0, 0, 0, 0) - result = cs.collapse_pitch(pitch) - assert result == (0, 1, 0, 0, 0, 0, 0, 0) - - def test_collapse_chord(self): - chord = ((3, 1, 0, 0, 0, 0, 0, 0), (2, 0, 1, 0, 0, 0, 0, 0)) - result = cs.collapse_chord(chord) - assert result == ((0, 1, 0, 0, 0, 0, 0, 0), (0, 0, 1, 0, 0, 0, 0, 0)) - - -class TestPitchDifference: def test_pitch_difference(self): - p1 = (0, 1, 0, 0, 0, 0, 0, 0) - p2 = (0, 0, 0, 0, 0, 0, 0, 0) - result = cs.pitch_difference(p1, p2) - assert result == (0, 1, 0, 0, 0, 0, 0, 0) + p1 = Pitch((0, 1, 0, 0), DIMS_4) + p2 = Pitch((0, 0, 0, 0), DIMS_4) + result = p1.pitch_difference(p2) + assert result.hs_array == (0, 1, 0, 0) - def test_cent_difference(self): - p1 = (0, 0, 0, 0, 0, 0, 0, 0) - p2 = (1, 0, 0, 0, 0, 0, 0, 0) - result = cs.cent_difference(p1, p2) - assert result == 1200.0 + def test_collapse(self): + p = Pitch((3, 1, 0, 0), DIMS_4) + collapsed = p.collapse() + # Collapse brings to [1, 2) range: 24 -> 1.5 = 2^-1 * 3 + assert collapsed.hs_array == (-1, 1, 0, 0) + + def test_collapse_below_one(self): + p = Pitch((-2, 0, 0, 0), DIMS_4) + collapsed = p.collapse() + assert collapsed.hs_array == (0, 0, 0, 0) + + def test_hash(self): + p1 = Pitch((0, 1, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + assert hash(p1) == hash(p2) + + def test_equality(self): + p1 = Pitch((0, 1, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + assert p1 == p2 + + def test_len(self): + p = Pitch((0, 1, 0, 0), DIMS_4) + assert len(p) == 4 -class TestCompactSets: - def test_compact_sets_root(self): - root = (0, 0, 0, 0) - result = list(cs.compact_sets(root, 1, 1)) - assert len(result) > 0 +class TestChord: + """Tests for Chord class.""" - def test_compact_sets_single_element(self): - root = (0, 0, 0, 0) - result = list(cs.compact_sets(root, 1, 1)) - assert all(len(chord) == 1 for chord in result) + def test_create_chord(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + chord = Chord((p1, p2), DIMS_4) + assert len(chord) == 2 - def test_compact_sets_returns_tuples(self): - root = (0, 0, 0, 0) - result = list(cs.compact_sets(root, 1, 1)) - assert all(isinstance(chord, tuple) for chord in result) + def test_transpose(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + chord = Chord((p1, p2), DIMS_4) + trans = Pitch((1, 0, 0, 0), DIMS_4) + transposed = chord.transpose(trans) + assert transposed[0].hs_array == (1, 0, 0, 0) + assert transposed[1].hs_array == (1, 1, 0, 0) + + def test_is_connected_single(self): + p = Pitch((0, 0, 0, 0), DIMS_4) + chord = Chord((p,), DIMS_4) + assert chord.is_connected() is True + + def test_is_connected_adjacent(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + chord = Chord((p1, p2), DIMS_4) + assert chord.is_connected() is True + + def test_is_connected_not_adjacent(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 2, 0, 0), DIMS_4) + chord = Chord((p1, p2), DIMS_4) + assert chord.is_connected() is False + + def test_sorted_by_frequency(self): + p1 = Pitch((0, 1, 0, 0), DIMS_4) # 3/2 + p2 = Pitch((0, 0, 0, 0), DIMS_4) # 1/1 + chord = Chord((p1, p2), DIMS_4) + sorted_pitches = chord.sorted_by_frequency() + assert sorted_pitches[0].hs_array == (0, 0, 0, 0) + assert sorted_pitches[1].hs_array == (0, 1, 0, 0) + + def test_symmetric_difference_size(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + p3 = Pitch((0, 2, 0, 0), DIMS_4) + chord1 = Chord((p1, p2), DIMS_4) + chord2 = Chord((p1, p3), DIMS_4) + # Both have p1, differ on p2 vs p3: symdiff = 2 + assert chord1.symmetric_difference_size(chord2) == 2 + + def test_hash(self): + p1 = Pitch((0, 0, 0, 0), DIMS_4) + p2 = Pitch((0, 1, 0, 0), DIMS_4) + c1 = Chord((p1, p2), DIMS_4) + c2 = Chord((p1, p2), DIMS_4) + assert hash(c1) == hash(c2) -class TestStochasticHamiltonian: - @pytest.mark.skip(reason="Requires full graph setup") - def test_stochastic_hamiltonian_from_graph(self, mock_write): - pass +class TestHarmonicSpace: + """Tests for HarmonicSpace class.""" - @pytest.mark.skip(reason="Requires full chord set setup") - def test_stochastic_hamiltonian_using_chord_set(self, mock_write): - pass + def test_create_space(self): + space = HarmonicSpace(DIMS_4) + assert space.dims == DIMS_4 + assert space.collapsed is True + + def test_root(self): + space = HarmonicSpace(DIMS_4) + root = space.root() + assert root.hs_array == (0, 0, 0, 0) + + def test_generate_connected_sets_size_3(self): + space = HarmonicSpace(DIMS_4) + chords = space.generate_connected_sets(3, 3) + assert len(chords) > 0 + assert all(len(c) == 3 for c in chords) + + def test_generate_connected_sets_all_connected(self): + space = HarmonicSpace(DIMS_4) + chords = space.generate_connected_sets(3, 3) + assert all(c.is_connected() for c in chords) + + def test_generate_connected_sets_size_2(self): + space = HarmonicSpace(DIMS_4) + chords = space.generate_connected_sets(2, 2) + assert len(chords) > 0 + assert all(len(c) == 2 for c in chords) + + def test_build_voice_leading_graph(self): + space = HarmonicSpace(DIMS_4) + chords = space.generate_connected_sets(3, 3) + graph = space.build_voice_leading_graph(chords, 2, 2) + assert graph.number_of_nodes() == len(chords) + assert graph.number_of_edges() > 0 + + def test_build_graph_symdiff_filter(self): + space = HarmonicSpace(DIMS_4) + chords = space.generate_connected_sets(3, 3) + # Test with symdiff range 1-3 + graph = space.build_voice_leading_graph(chords, 1, 3) + assert graph.number_of_edges() > 0 -class TestWeightFunctions: - def test_is_bass_rooted_true(self): - chord = ((0, 0, 0, 0), (0, 1, 0, 0)) - assert cs.is_bass_rooted(chord) == True +class TestCLI: + """Tests for CLI functionality.""" - def test_is_bass_rooted_false(self): - chord = ((0, 0, 0, 0), (0, 2, 0, 0)) - assert cs.is_bass_rooted(chord) == False + def test_args_parsing(self): + import argparse + from src.io import main - @pytest.mark.skip(reason="Requires complete edge structure") - def test_voice_crossing_weights_no_crossing(self): - pass - - def test_contrary_motion_weights(self): - edge = [ - [ - ((0, 0, 0), (0, 1, 0)), - ((0, 0, 0), (0, 2, 0)), - { - "transposition": (0, 0, 0), - "movements": { - (0, 0, 0): {"cent_difference": -100}, - (0, 1, 0): {"cent_difference": 0}, - (0, 2, 0): {"cent_difference": 100}, - }, - }, - ] - ] - weights = list(cs.contrary_motion_weights(edge)) - assert weights[0] == 10 + # This is a basic test - in practice we'd test the parser more thoroughly + # For now just verify the module can be imported + assert main is not None if __name__ == "__main__":