summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test_error_codes.py2
-rw-r--r--tests/test_lexer.py74
-rw-r--r--tests/test_mml_conformance.py53
-rw-r--r--tests/test_parser.py10
-rw-r--r--tests/test_parser_detailed.py74
5 files changed, 200 insertions, 13 deletions
diff --git a/tests/test_error_codes.py b/tests/test_error_codes.py
index f9bd5bb..538056f 100644
--- a/tests/test_error_codes.py
+++ b/tests/test_error_codes.py
@@ -192,7 +192,7 @@ def test_semantic_note_out_of_range_pyxel(compose_mml):
# --- SEMANTIC_CHANNEL_MISMATCH ---
def test_semantic_channel_mismatch_ppmck_triangle_duty(compose_mml):
- result = compose_mml(action="validate", mml="T t120 l4 q2\n c", mode="ppmck")
+ result = compose_mml(action="validate", mml="T t120 l4 @2\n c", mode="ppmck")
assert _find_code(result, ErrorCode.SEMANTIC_CHANNEL_MISMATCH.value, "warnings")
diff --git a/tests/test_lexer.py b/tests/test_lexer.py
index 64fa17c..1d7f8e7 100644
--- a/tests/test_lexer.py
+++ b/tests/test_lexer.py
@@ -4,12 +4,13 @@ from mml_composemusic_mcp.lexer import TokenType, tokenize
def test_ppmck_tokens():
- source = "A t150 l8 o4 v15 q2\nc d e f"
+ source = "A t150 l8 o4 v15 @2\nc d e f"
tokens = tokenize(source, "ppmck")
types = [t.type for t in tokens]
assert TokenType.TRACK_HEADER in types
assert TokenType.TEMPO in types
assert TokenType.NOTE in types
+ assert TokenType.DUTY in types
def test_pyxel_tokens():
@@ -35,3 +36,74 @@ def test_ppmck_invalid_token():
def test_pyxel_invalid_token():
tokens = tokenize("0: T120 L4 O4 X C", "pyxel")
assert any(t.type == TokenType.INVALID for t in tokens)
+
+
+def test_ppmck_at_duty():
+ tokens = tokenize("A @2 c", "ppmck")
+ assert any(t.type == TokenType.DUTY for t in tokens)
+
+
+def test_ppmck_quantize():
+ tokens = tokenize("A q4 c", "ppmck")
+ assert any(t.type == TokenType.QUANTIZE for t in tokens)
+
+
+def test_ppmck_tie_cmd():
+ tokens = tokenize("A c4 ^4", "ppmck")
+ assert any(t.type == TokenType.TIE_CMD for t in tokens)
+
+
+def test_ppmck_detune():
+ tokens = tokenize("A D10 c", "ppmck")
+ assert any(t.type == TokenType.DETUNE for t in tokens)
+
+
+def test_ppmck_sweep():
+ tokens = tokenize("A s1,2 c", "ppmck")
+ assert any(t.type == TokenType.SWEEP for t in tokens)
+
+
+def test_ppmck_rel_vol():
+ tokens = tokenize("A v+5 v-3 c", "ppmck")
+ assert any(t.type == TokenType.REL_VOL_UP for t in tokens)
+ assert any(t.type == TokenType.REL_VOL_DOWN for t in tokens)
+
+
+def test_ppmck_vol_env():
+ tokens = tokenize("A @v0 c", "ppmck")
+ assert any(t.type == TokenType.VOL_ENV for t in tokens)
+
+
+def test_ppmck_vol_env_def():
+ tokens = tokenize("@v0 = { 15, 10, | 5 }", "ppmck")
+ assert any(t.type == TokenType.VOL_ENV for t in tokens)
+ assert any(t.type == TokenType.BRACE_OPEN for t in tokens)
+ assert any(t.type == TokenType.BRACE_CLOSE for t in tokens)
+
+
+def test_ppmck_lfo_def():
+ tokens = tokenize("@MP0 = { 0, 10, 5, 0 }", "ppmck")
+ assert any(t.type == TokenType.LFO_DEF for t in tokens)
+
+
+def test_ppmck_lfo_use_off():
+ tokens = tokenize("A MP1 MPOF c", "ppmck")
+ assert any(t.type == TokenType.LFO_USE for t in tokens)
+ assert any(t.type == TokenType.LFO_OFF for t in tokens)
+
+
+def test_ppmck_pitch_env():
+ tokens = tokenize("A EP1 EPOF c", "ppmck")
+ assert any(t.type == TokenType.PITCH_ENV_USE for t in tokens)
+ assert any(t.type == TokenType.PITCH_ENV_OFF for t in tokens)
+
+
+def test_ppmck_note_env():
+ tokens = tokenize("A EN1 ENOF c", "ppmck")
+ assert any(t.type == TokenType.NOTE_ENV_USE for t in tokens)
+ assert any(t.type == TokenType.NOTE_ENV_OFF for t in tokens)
+
+
+def test_ppmck_duty_env_use():
+ tokens = tokenize("A @@0 c", "ppmck")
+ assert any(t.type == TokenType.DUTY_ENV_USE for t in tokens)
diff --git a/tests/test_mml_conformance.py b/tests/test_mml_conformance.py
index 0e38945..f611f6e 100644
--- a/tests/test_mml_conformance.py
+++ b/tests/test_mml_conformance.py
@@ -5,7 +5,7 @@ from hypothesis import given, seed, settings
from hypothesis import strategies as st
from mml_composemusic_mcp.ast_nodes import NoteStmt, TransposeStmt
-from mml_composemusic_mcp.ir import ErrorCode, NoteEvent
+from mml_composemusic_mcp.ir import DutyEvent, ErrorCode, NoteEvent
from mml_composemusic_mcp.lexer import TokenType, tokenize
from mml_composemusic_mcp.parser_ppmck import parse_ppmck
from mml_composemusic_mcp.parser_pyxel import parse_pyxel
@@ -58,7 +58,7 @@ def test_note_follows_bnf_accidental_before_length(
@pytest.mark.parametrize(
("mode", "source"),
[
- ("ppmck", "A o l v q t c"),
+ ("ppmck", "A o l v @ t c"),
("pyxel", "0: O L V Q @ T K Y C"),
("pyxel", "0: K+ Y- C"),
],
@@ -149,6 +149,55 @@ def test_validate_full_pyxel_path_preserves_warning_and_summary():
)
+@pytest.mark.parametrize(
+ ("source", "expected_warning_code"),
+ [
+ ("A D10 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A s1,2 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A v+5 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A @v0 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A @@0 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A MP1 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A MPOF c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A EP1 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A EPOF c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A EN1 c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ("A ENOF c", ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE),
+ ],
+)
+def test_ppmck_new_commands_produce_warning(source, expected_warning_code):
+ _, _, _, errors = parse_pipeline(source, "ppmck")
+ assert not any(e.severity == "error" for e in errors)
+ assert expected_warning_code in error_codes(errors)
+
+
+def test_ppmck_at_duty_in_pulse_channel():
+ _, _, ns, errors = parse_pipeline("A @2 c", "ppmck")
+ assert not any(e.severity == "error" for e in errors)
+ duties = [e for e in ns.channels["Pulse1"].events if isinstance(e, DutyEvent)]
+ assert duties and duties[0].value == 2
+
+
+def test_ppmck_at_duty_in_triangle_channel_warns():
+ _, _, _, errors = parse_pipeline("T @2 c", "ppmck")
+ assert ErrorCode.SEMANTIC_CHANNEL_MISMATCH in error_codes(errors)
+
+
+def test_ppmck_quantize_sets_gate_time():
+ _, _, ns, errors = parse_pipeline("A q4 c", "ppmck")
+ assert not any(e.severity == "error" for e in errors)
+ notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
+ assert notes and notes[0].gate_time == 0.5
+
+
+def test_ppmck_tie_cmd_extends_duration():
+ _, _, ns, errors = parse_pipeline("A c4 ^4", "ppmck")
+ assert not any(e.severity == "error" for e in errors)
+ notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
+ assert len(notes) == 1
+ assert notes[0].duration == 384
+
+
note_names = st.sampled_from(tuple("cdefgab"))
accidentals = st.sampled_from(("", "+", "#", "-"))
lengths = st.one_of(st.just(""), st.integers(1, 192).map(str))
diff --git a/tests/test_parser.py b/tests/test_parser.py
index a0f11c7..291eb9b 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -6,7 +6,7 @@ from mml_composemusic_mcp.parser_ppmck import parse_ppmck
from mml_composemusic_mcp.parser_pyxel import parse_pyxel
PPMCK_EXAMPLE = """#TITLE "Test"
-A t150 l8 o4 v15 q2
+A t150 l8 o4 v15 @2
c d e f
B o3
c e g
@@ -27,9 +27,7 @@ def test_ppmck_parse():
assert len(program.tracks) == 2
assert program.tracks[0].channel == "Pulse1"
assert program.tracks[1].channel == "Pulse2"
- notes = [
- s for s in program.tracks[0].statements if isinstance(s, NoteStmt)
- ]
+ notes = [s for s in program.tracks[0].statements if isinstance(s, NoteStmt)]
assert notes[0].note_name == "c"
@@ -40,9 +38,7 @@ def test_pyxel_parse():
assert not any(e.severity == "error" for e in errors)
assert len(program.tracks) == 2
assert program.tracks[0].channel == "Pulse1"
- notes = [
- s for s in program.tracks[0].statements if isinstance(s, NoteStmt)
- ]
+ notes = [s for s in program.tracks[0].statements if isinstance(s, NoteStmt)]
assert notes[0].note_name == "c"
diff --git a/tests/test_parser_detailed.py b/tests/test_parser_detailed.py
index 7033d54..f1f4b6a 100644
--- a/tests/test_parser_detailed.py
+++ b/tests/test_parser_detailed.py
@@ -3,6 +3,7 @@
from mml_composemusic_mcp.ir import (
DutyEvent,
EnvelopeEvent,
+ ErrorCode,
NoteEvent,
)
from mml_composemusic_mcp.lexer import tokenize
@@ -54,13 +55,82 @@ def test_pyxel_channel_assignment():
# --- Duty ---
-def test_ppmck_duty_q():
- source = "A t120 l4 o4 q3\n c"
+def test_ppmck_duty_at():
+ source = "A t120 l4 o4 @3\n c"
ns, errors = _analyze_ppmck(source)
duties = [e for e in ns.channels["Pulse1"].events if isinstance(e, DutyEvent)]
assert duties and duties[0].value == 3
+def test_ppmck_quantize():
+ source = "A t120 l4 o4 q4\n c"
+ ns, errors = _analyze_ppmck(source)
+ notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
+ assert notes and notes[0].gate_time == 0.5
+
+
+def test_ppmck_tie_cmd():
+ source = "A t120 l4 o4\n c4 ^4"
+ ns, errors = _analyze_ppmck(source)
+ notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
+ assert len(notes) == 1
+ assert notes[0].duration == 384
+
+
+def test_ppmck_detune_warning():
+ source = "A t120 l4 o4 D10\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_sweep_warning():
+ source = "A t120 l4 o4 s1,2\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_rel_vol_warning():
+ source = "A t120 l4 o4 v+5\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_vol_env_use_warning():
+ source = "A t120 l4 o4 @v0\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_vol_env_def_warning():
+ source = "A t120 l4 o4\n@v0 = { 15, 10, | 5 }\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_lfo_def_warning():
+ source = "A t120 l4 o4\n@MP0 = { 0, 10, 5, 0 }\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_duty_env_use_warning():
+ source = "A t120 l4 o4 @@0\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_UNSUPPORTED_FEATURE for e in errors)
+
+
+def test_ppmck_duty_triangle_error():
+ source = "T t120 l4 o3 @2\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_CHANNEL_MISMATCH for e in errors)
+
+
+def test_ppmck_quantize_range_error():
+ source = "A t120 l4 o4 q9\n c"
+ ns, errors = _analyze_ppmck(source)
+ assert any(e.code == ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE for e in errors)
+
+
def test_pyxel_at_duty():
source = "0: T120 L4 O4 @1\n C"
ns, errors = _analyze_pyxel(source)