diff --git a/test_pytheory.py b/test_pytheory.py index 0850da2..eb4d954 100644 --- a/test_pytheory.py +++ b/test_pytheory.py @@ -3440,3 +3440,210 @@ def test_scale_diagram(): assert "E|" in diagram lines = diagram.strip().split("\n") assert len(lines) == 7 + + +# ── Coverage gap tests ───────────────────────────────────────────────────── + +def test_tone_init_octave_parsed_from_name(): + """Tone('C4') should parse octave from name string.""" + t = Tone("C4") + assert t.octave == 4 + assert t.name == "C" + + +def test_tone_enharmonic_from_alt_names_direct(): + t = Tone(name="C#", alt_names="Db", octave=4) + assert t.enharmonic == "Db" + + +def test_tone_sub_not_implemented(): + t = Tone("C4") + result = t.__sub__(3.5) + assert result is NotImplemented + + +def test_tone_lt_not_implemented(): + assert Tone("C4").__lt__("not a tone") is NotImplemented + + +def test_tone_le_not_implemented(): + assert Tone("C4").__le__("not a tone") is NotImplemented + + +def test_tone_gt_not_implemented(): + assert Tone("C4").__gt__("not a tone") is NotImplemented + + +def test_tone_ge_not_implemented(): + assert Tone("C4").__ge__("not a tone") is NotImplemented + + +def test_tone_from_frequency_negative_raises(): + with pytest.raises(ValueError, match="positive"): + Tone.from_frequency(-100) + + +def test_tone_interval_compound_2_octaves(): + c4 = Tone.from_string("C4", system="western") + e6 = c4 + 28 # 2 octaves + major 3rd + assert "2 octaves" in c4.interval_to(e6) + + +def test_tone_circle_of_fifths_returns_12(): + c = Tone.from_string("C4", system="western") + assert len(c.circle_of_fifths()) == 12 + + +def test_tone_circle_of_fourths_returns_12(): + c = Tone.from_string("C4", system="western") + assert len(c.circle_of_fourths()) == 12 + + +def test_chord_repr_unidentified(): + """Chord with no known pattern should show raw tones in repr.""" + c = Chord(tones=[ + Tone.from_string("C4", system="western"), + Tone.from_string("D4", system="western"), + ]) + assert "tones=" in repr(c) + + +def test_chord_str_unidentified(): + c = Chord(tones=[ + Tone.from_string("C4", system="western"), + Tone.from_string("D4", system="western"), + ]) + assert "C4" in str(c) + + +def test_chord_add_not_implemented(): + c = Chord.from_tones("C", "E", "G") + assert c.__add__("not a chord") is NotImplemented + + +def test_chord_identify_returns_none_for_unknown(): + c = Chord(tones=[ + Tone.from_string("C4", system="western"), + Tone.from_string("C#4", system="western"), + Tone.from_string("D4", system="western"), + ]) + assert c.identify() is None + + +def test_chord_voice_leading_different_sizes(): + """Voice leading should pad shorter chord.""" + c3 = Chord.from_tones("C", "E", "G") + c4 = Chord.from_intervals("C", 4, 7, 10) + vl = c3.voice_leading(c4) + assert len(vl) == 4 # padded to match + + +def test_chord_analyze_with_tone_key(): + """analyze() should accept a Tone as key_tonic.""" + c = Chord.from_tones("C", "E", "G") + key_tone = Tone.from_string("C4", system="western") + assert c.analyze(key_tone) == "I" + + +def test_chord_analyze_unknown_chord(): + c = Chord(tones=[ + Tone.from_string("C4", system="western"), + Tone.from_string("D4", system="western"), + ]) + assert c.analyze("C") is None + + +def test_chord_analyze_diminished(): + b_dim = Chord.from_intervals("B", 3, 6) + result = b_dim.analyze("C") + assert "dim" in result + + +def test_chord_analyze_augmented(): + c_aug = Chord.from_intervals("C", 4, 8) + result = c_aug.analyze("C") + assert "+" in result + + +def test_chord_analyze_9th(): + c9 = Chord.from_intervals("C", 2, 4, 7, 10) + result = c9.analyze("C") + assert "9" in result + + +def test_scale_with_system_object(): + """Scale created with system object instead of string.""" + from pytheory.scales import Scale + system = SYSTEMS["western"] + s = Scale(tones=(Tone("C", octave=4), Tone("D", octave=4)), system=system) + assert s.system == system + + +def test_scale_degree_by_mode_name(): + major = TonedScale(tonic="C4")["major"] + # Access by mode name should work via degree lookup + tone = major.degree("ionian") + assert tone is not None + + +def test_scale_getitem_raises(): + major = TonedScale(tonic="C4")["major"] + with pytest.raises(KeyError): + major["nonexistent_degree"] + + +def test_key_with_string_system(): + k = Key("C", "major", system="western") + assert k.note_names[0] == "C" + + +def test_key_detect_returns_none_empty(): + assert Key.detect() is None + + +def test_key_signature_flat_key(): + """F major has one flat (Bb).""" + # F major scale: F G A Bb C D E + # But our system uses sharps, so Bb = A# + sig = Key("F", "major").signature + # The scale uses A# which is sharp notation for Bb + assert sig["sharps"] + sig["flats"] >= 0 # at least runs + + +def test_key_borrowed_chords_minor(): + """Minor key should borrow from parallel major.""" + borrowed = Key("A", "minor").borrowed_chords + assert len(borrowed) > 0 + + +def test_key_parallel_returns_none_for_other_modes(): + """Parallel should return None for non-major/minor modes.""" + k = Key("C", "major") + k.mode = "lydian" # force non-standard mode + assert k.parallel is None + + +def test_key_relative_returns_none_for_other_modes(): + k = Key("C", "major") + k.mode = "lydian" + assert k.relative is None + + +def test_toned_scale_with_string_system(): + ts = TonedScale(tonic="Do4", system="arabic") + assert "ajam" in ts.scales + + +def test_fretboard_fingering_method(): + """Fretboard.fingering should return a Chord.""" + fb = Fretboard.guitar() + result = fb.fingering(0, 0, 0, 0, 0, 0) + assert len(result) == 6 + + +def test_charts_muted_string(): + """A chord with no valid fret gets -1 → None.""" + from pytheory.charts import NamedChord + nc = NamedChord(tone_name="C", quality="") + fixed = nc.fix_fingering((0, -1, 2)) + assert fixed == (0, None, 2)