import pytest import sys sys.path.insert(0, "src") from src.pitch import Pitch, DIMS_4, DIMS_7 from src.chord import Chord from src.harmonic_space import HarmonicSpace class TestPitch: """Tests for Pitch class.""" def test_fundamental(self): p = Pitch((0, 0, 0, 0), DIMS_4) assert p.to_fraction() == 1 def test_octave(self): p = Pitch((1, 0, 0, 0), DIMS_4) assert p.to_fraction() == 2 def test_fifth(self): p = Pitch((0, 1, 0, 0), DIMS_4) assert p.to_fraction() == 3 def test_compound(self): p = Pitch((1, 1, 0, 0), DIMS_4) assert p.to_fraction() == 6 def test_cents_fundamental(self): p = Pitch((0, 0, 0, 0), DIMS_4) assert p.to_cents() == 0.0 def test_cents_octave(self): p = Pitch((1, 0, 0, 0), DIMS_4) assert p.to_cents() == 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) 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_pitch_difference(self): 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_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 TestChord: """Tests for Chord class.""" 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_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 TestHarmonicSpace: """Tests for HarmonicSpace class.""" 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 TestCLI: """Tests for CLI functionality.""" def test_args_parsing(self): import argparse from src.io import main # 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 def test_target_range_weight(self): """Test that target range weight can be enabled and influences path generation.""" from src.harmonic_space import HarmonicSpace from src.graph import PathFinder space = HarmonicSpace(DIMS_4) chords = space.generate_connected_sets(3, 3) graph = space.build_voice_leading_graph(chords, 2, 2) path_finder = PathFinder(graph) # Test with target range disabled (default) config_no_target = path_finder._default_weights_config() assert config_no_target.get("target_range") is False # Test with target range enabled config_target = path_finder._default_weights_config() config_target["target_range"] = True config_target["target_range_octaves"] = 2.0 assert config_target.get("target_range") is True assert config_target.get("target_range_octaves") == 2.0 if __name__ == "__main__": pytest.main([__file__, "-v"])