summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-12 16:03:29 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-12 16:03:29 +0900
commit22f96694a96ed9fba169e77bc27d19d00edca083 (patch)
tree8f59c1d3f211c296b005a3f6ede8206bc3675a68 /tests
parent3bb311ad8e0f10617805c8737d92d13e6a627818 (diff)
MML構文解析処理を追加
Diffstat (limited to 'tests')
-rw-r--r--tests/test_error_codes.py87
-rw-r--r--tests/test_lexer.py10
-rw-r--r--tests/test_parser.py39
-rw-r--r--tests/test_parser_detailed.py99
-rw-r--r--tests/test_synthesizer.py2
5 files changed, 124 insertions, 113 deletions
diff --git a/tests/test_error_codes.py b/tests/test_error_codes.py
index eca73ad..f9bd5bb 100644
--- a/tests/test_error_codes.py
+++ b/tests/test_error_codes.py
@@ -1,4 +1,4 @@
-"""Exhaustive tests for all error codes defined in Design_err.md."""
+"""Exhaustive tests for all error codes defined in Design.md."""
from unittest.mock import patch
@@ -33,44 +33,44 @@ def test_syntax_invalid_token_mode(compose_mml):
def test_syntax_invalid_number_at(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4 O4 @x\n C", mode="pyxel")
assert not result["valid"]
- # @x -> lexer emits TEXT, parser reports unexpected or invalid number
+ # @x -> lexer emits INVALID, parser reports invalid token
assert result["errors"]
-# --- SYNTAX_VALUE_OUT_OF_RANGE ---
-def test_syntax_value_out_of_range_octave_ppmck(compose_mml):
+# --- SEMANTIC_VALUE_OUT_OF_RANGE ---
+def test_semantic_value_out_of_range_octave_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120 l4 o9\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
- assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
+ assert ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE.value in codes
-def test_syntax_value_out_of_range_volume_ppmck(compose_mml):
+def test_semantic_value_out_of_range_volume_ppmck(compose_mml):
result = compose_mml(action="validate", mml="A t120 l4 o4 v20\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
- assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
+ assert ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE.value in codes
-def test_syntax_value_out_of_range_volume_pyxel(compose_mml):
+def test_semantic_value_out_of_range_volume_pyxel(compose_mml):
result = compose_mml(action="validate", mml="0: T120 L4 O4 V200\n C", mode="pyxel")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
- assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
+ assert ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE.value in codes
-def test_syntax_value_out_of_range_length(compose_mml):
+def test_semantic_value_out_of_range_length(compose_mml):
result = compose_mml(action="validate", mml="A t120 l0\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
- assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
+ assert ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE.value in codes
-def test_syntax_value_out_of_range_tempo(compose_mml):
+def test_semantic_value_out_of_range_tempo(compose_mml):
result = compose_mml(action="validate", mml="A t0\n c", mode="ppmck")
assert not result["valid"]
codes = [e["code"] for e in result["errors"]]
- assert ErrorCode.SYNTAX_VALUE_OUT_OF_RANGE.value in codes
+ assert ErrorCode.SEMANTIC_VALUE_OUT_OF_RANGE.value in codes
# --- SYNTAX_UNEXPECTED_TOKEN ---
@@ -82,17 +82,17 @@ def test_syntax_unexpected_token(compose_mml):
assert result["valid"] is True
-# --- SYNTAX_OUTSIDE_TRACK ---
-def test_syntax_outside_track_ppmck(compose_mml):
+# --- SEMANTIC_OUTSIDE_TRACK ---
+def test_semantic_outside_track_ppmck(compose_mml):
result = compose_mml(action="validate", mml="t120\nA l4\n c", mode="ppmck")
assert result["valid"] is True
- assert _find_code(result, ErrorCode.SYNTAX_OUTSIDE_TRACK.value, "warnings")
+ assert _find_code(result, ErrorCode.SEMANTIC_OUTSIDE_TRACK.value, "warnings")
-def test_syntax_outside_track_pyxel(compose_mml):
+def test_semantic_outside_track_pyxel(compose_mml):
result = compose_mml(action="validate", mml="T120\n0: L4\n C", mode="pyxel")
assert result["valid"] is True
- assert _find_code(result, ErrorCode.SYNTAX_OUTSIDE_TRACK.value, "warnings")
+ assert _find_code(result, ErrorCode.SEMANTIC_OUTSIDE_TRACK.value, "warnings")
# --- SYNTAX_UNTERMINATED_REPEAT ---
@@ -153,27 +153,26 @@ def test_syntax_duplicate_track_pyxel(compose_mml):
assert _find_code(result, ErrorCode.SYNTAX_DUPLICATE_TRACK.value)
-# --- SYNTAX_EMPTY_TRACK ---
-def test_syntax_empty_track_ppmck(compose_mml):
+# --- SEMANTIC_EMPTY_TRACK ---
+def test_semantic_empty_track_ppmck(compose_mml):
# B track header with no notes/rests -> empty track error
result = compose_mml(action="validate", mml="A t120 l4\n c\nB", mode="ppmck")
# B alone sets track but has no events; check for empty track or just verify no crash
- # The parser may or may not flag this; verify result is a dict
assert isinstance(result, dict)
if not result["valid"]:
- assert _find_code(result, ErrorCode.SYNTAX_EMPTY_TRACK.value)
+ assert _find_code(result, ErrorCode.SEMANTIC_EMPTY_TRACK.value)
-def test_syntax_empty_track_pyxel(compose_mml):
+def test_semantic_empty_track_pyxel(compose_mml):
# 1: track with only tempo, no notes -> may or may not be flagged as empty
result = compose_mml(action="validate", mml="0: T120\n C\n1: T120", mode="pyxel")
assert isinstance(result, dict)
if not result["valid"]:
- assert _find_code(result, ErrorCode.SYNTAX_EMPTY_TRACK.value)
+ assert _find_code(result, ErrorCode.SEMANTIC_EMPTY_TRACK.value)
-# --- SYNTAX_NOTE_OUT_OF_RANGE ---
-def test_syntax_note_out_of_range_ppmck(compose_mml):
+# --- SEMANTIC_NOTE_OUT_OF_RANGE ---
+def test_semantic_note_out_of_range_ppmck(compose_mml):
# o7 b = MIDI 95, need many octave ups to exceed 127
result = compose_mml(
action="validate", mml="A t120 l4 o7\n b > > > > > c", mode="ppmck"
@@ -183,7 +182,7 @@ def test_syntax_note_out_of_range_ppmck(compose_mml):
assert any(e["severity"] == "warning" for e in result["warnings"])
-def test_syntax_note_out_of_range_pyxel(compose_mml):
+def test_semantic_note_out_of_range_pyxel(compose_mml):
result = compose_mml(
action="validate", mml="0: T120 L4 O7\n B > > > > > C", mode="pyxel"
)
@@ -191,20 +190,20 @@ def test_syntax_note_out_of_range_pyxel(compose_mml):
assert any(e["severity"] == "warning" for e in result["warnings"])
-# --- SYNTAX_CHANNEL_MISMATCH ---
-def test_syntax_channel_mismatch_ppmck_triangle_duty(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")
- assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
+ assert _find_code(result, ErrorCode.SEMANTIC_CHANNEL_MISMATCH.value, "warnings")
-def test_syntax_channel_mismatch_ppmck_noise_pitch(compose_mml):
+def test_semantic_channel_mismatch_ppmck_noise_pitch(compose_mml):
result = compose_mml(action="validate", mml="N t120 l4\n c", mode="ppmck")
- assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
+ assert _find_code(result, ErrorCode.SEMANTIC_CHANNEL_MISMATCH.value, "warnings")
-def test_syntax_channel_mismatch_pyxel_triangle_at(compose_mml):
+def test_semantic_channel_mismatch_pyxel_triangle_at(compose_mml):
result = compose_mml(action="validate", mml="2: T120 L4 @1\n C", mode="pyxel")
- assert _find_code(result, ErrorCode.SYNTAX_CHANNEL_MISMATCH.value, "warnings")
+ assert _find_code(result, ErrorCode.SEMANTIC_CHANNEL_MISMATCH.value, "warnings")
# --- SYNTAX_UNTERMINATED_HEADER ---
@@ -237,8 +236,8 @@ def test_validation_invalid_action(compose_mml):
assert _find_code(result, ErrorCode.VALIDATION_INVALID_ACTION.value)
-# --- SYSTEM_SYNTHESIS_FAILED ---
-def test_system_synthesis_failed(compose_mml):
+# --- RUNTIME_SYNTHESIS_FAILED ---
+def test_runtime_synthesis_failed(compose_mml):
with patch(
"mml_composemusic_mcp.server.synthesize",
side_effect=RuntimeError("overflow"),
@@ -249,11 +248,11 @@ def test_system_synthesis_failed(compose_mml):
mode="ppmck",
)
assert result["success"] is False
- assert _find_code(result, ErrorCode.SYSTEM_SYNTHESIS_FAILED.value)
+ assert _find_code(result, ErrorCode.RUNTIME_SYNTHESIS_FAILED.value)
-# --- SYSTEM_WAV_WRITE_FAILED ---
-def test_system_wav_write_failed(tmp_path):
+# --- RUNTIME_WAV_WRITE_FAILED ---
+def test_runtime_wav_write_failed(tmp_path):
bad_path = tmp_path / "nonexistent_dir" / "output.wav"
data = np.array([0.0, 0.1, -0.1], dtype=np.float64)
errors = write_wav(bad_path, data, 44100)
@@ -266,11 +265,11 @@ def test_system_wav_write_failed(tmp_path):
bad_path2 = blocker / "output.wav"
errors = write_wav(bad_path2, data, 44100)
if errors:
- assert errors[0].code == ErrorCode.SYSTEM_WAV_WRITE_FAILED
+ assert errors[0].code == ErrorCode.RUNTIME_WAV_WRITE_FAILED
-# --- SYSTEM_INTERNAL_ERROR ---
-def test_system_internal_error(compose_mml):
+# --- RUNTIME_INTERNAL_ERROR ---
+def test_runtime_internal_error(compose_mml):
with patch(
"mml_composemusic_mcp.server._parse_mml",
side_effect=RuntimeError("unexpected internal error"),
@@ -281,9 +280,9 @@ def test_system_internal_error(compose_mml):
mode="ppmck",
)
assert result["success"] is False
- assert _find_code(result, ErrorCode.SYSTEM_INTERNAL_ERROR.value)
+ assert _find_code(result, ErrorCode.RUNTIME_INTERNAL_ERROR.value)
-def test_system_internal_error_not_exposed(compose_mml):
+def test_runtime_internal_error_not_exposed(compose_mml):
result = compose_mml(action="compose", mml="A t120 l4 o4\n c", mode="ppmck")
assert isinstance(result, dict)
diff --git a/tests/test_lexer.py b/tests/test_lexer.py
index 1bb2213..64fa17c 100644
--- a/tests/test_lexer.py
+++ b/tests/test_lexer.py
@@ -25,3 +25,13 @@ def test_pyxel_repeat():
tokens = tokenize("[ C D ]2", "pyxel")
assert any(t.type == TokenType.REPEAT_START for t in tokens)
assert any(t.type == TokenType.REPEAT_END for t in tokens)
+
+
+def test_ppmck_invalid_token():
+ tokens = tokenize("A t120 l4 o4 x c", "ppmck")
+ assert any(t.type == TokenType.INVALID for t in tokens)
+
+
+def test_pyxel_invalid_token():
+ tokens = tokenize("0: T120 L4 O4 X C", "pyxel")
+ assert any(t.type == TokenType.INVALID for t in tokens)
diff --git a/tests/test_parser.py b/tests/test_parser.py
index b263aac..a0f11c7 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,6 +1,6 @@
-"""Tests for PPMCK and Pyxel parsers."""
+"""Tests for PPMCK and Pyxel parsers (AST output)."""
-from mml_composemusic_mcp.ir import NoteEvent
+from mml_composemusic_mcp.ast_nodes import NoteStmt, Program, Track
from mml_composemusic_mcp.lexer import tokenize
from mml_composemusic_mcp.parser_ppmck import parse_ppmck
from mml_composemusic_mcp.parser_pyxel import parse_pyxel
@@ -21,33 +21,42 @@ PYXEL_EXAMPLE = """0: T150 L8 O4 V100 @1
def test_ppmck_parse():
tokens = tokenize(PPMCK_EXAMPLE, "ppmck")
- ns, errors = parse_ppmck(PPMCK_EXAMPLE, tokens)
+ program, errors = parse_ppmck(PPMCK_EXAMPLE, tokens)
+ assert isinstance(program, Program)
assert not any(e.severity == "error" for e in errors)
- assert ns.channels["Pulse1"].events
- assert ns.channels["Pulse2"].events
- notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
- assert notes[0].note_number == 60 # C4
+ 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)
+ ]
+ assert notes[0].note_name == "c"
def test_pyxel_parse():
tokens = tokenize(PYXEL_EXAMPLE, "pyxel")
- ns, errors = parse_pyxel(PYXEL_EXAMPLE, tokens)
+ program, errors = parse_pyxel(PYXEL_EXAMPLE, tokens)
+ assert isinstance(program, Program)
assert not any(e.severity == "error" for e in errors)
- assert ns.channels["Pulse1"].events
- notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
- assert notes[0].note_number == 60 # C4
+ assert len(program.tracks) == 2
+ assert program.tracks[0].channel == "Pulse1"
+ notes = [
+ s for s in program.tracks[0].statements if isinstance(s, NoteStmt)
+ ]
+ assert notes[0].note_name == "c"
-def test_pyxel_repeat_expansion():
+def test_pyxel_repeat_ast():
source = "0: T120 L4 O4\n[ C D ]2"
tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
- notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
- assert len(notes) == 4
+ program, errors = parse_pyxel(source, tokens)
+ assert not any(e.severity == "error" for e in errors)
+ assert isinstance(program.tracks[0], Track)
def test_invalid_mode_error():
from mml_composemusic_mcp.server import _parse_mml
seq, errors = _parse_mml("A c", "unknown")
+ assert seq is None
assert any(e.severity == "error" for e in errors)
diff --git a/tests/test_parser_detailed.py b/tests/test_parser_detailed.py
index 3752562..7033d54 100644
--- a/tests/test_parser_detailed.py
+++ b/tests/test_parser_detailed.py
@@ -1,4 +1,4 @@
-"""Detailed parser tests for ppmck and pyxel modes."""
+"""Detailed parser/semantic tests for ppmck and pyxel modes."""
from mml_composemusic_mcp.ir import (
DutyEvent,
@@ -8,14 +8,32 @@ from mml_composemusic_mcp.ir import (
from mml_composemusic_mcp.lexer import tokenize
from mml_composemusic_mcp.parser_ppmck import parse_ppmck
from mml_composemusic_mcp.parser_pyxel import parse_pyxel
+from mml_composemusic_mcp.semantic_ppmck import analyze_ppmck
+from mml_composemusic_mcp.semantic_pyxel import analyze_pyxel
+
+
+def _analyze_ppmck(source: str):
+ tokens = tokenize(source, "ppmck")
+ program, errors = parse_ppmck(source, tokens)
+ if any(e.severity == "error" for e in errors):
+ return None, errors
+ return analyze_ppmck(source, program)
+
+
+def _analyze_pyxel(source: str):
+ tokens = tokenize(source, "pyxel")
+ program, errors = parse_pyxel(source, tokens)
+ if any(e.severity == "error" for e in errors):
+ return None, errors
+ return analyze_pyxel(source, program)
+
# --- Channel assignment ---
def test_ppmck_channel_assignment():
source = "A t120 l4 o4\n c\nB o4\n d\nT o3\n e\nN v10\n r"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
assert not any(e.severity == "error" for e in errors)
assert ns.channels["Pulse1"].events
assert ns.channels["Pulse2"].events
@@ -25,8 +43,7 @@ def test_ppmck_channel_assignment():
def test_pyxel_channel_assignment():
source = "0: T120 L4 O4\n C\n1: O4\n D\n2: O3\n E\n3: V80\n R"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
assert not any(e.severity == "error" for e in errors)
assert ns.channels["Pulse1"].events
assert ns.channels["Pulse2"].events
@@ -39,24 +56,21 @@ def test_pyxel_channel_assignment():
def test_ppmck_duty_q():
source = "A t120 l4 o4 q3\n c"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ 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_pyxel_at_duty():
source = "0: T120 L4 O4 @1\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
duties = [e for e in ns.channels["Pulse1"].events if isinstance(e, DutyEvent)]
assert duties and duties[0].value == 2 # @1 maps to duty 2 (50%)
def test_pyxel_at2_duty():
source = "0: T120 L4 O4 @2\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
duties = [e for e in ns.channels["Pulse1"].events if isinstance(e, DutyEvent)]
assert duties and duties[0].value == 1 # @2 maps to duty 1 (25%)
@@ -66,8 +80,7 @@ def test_pyxel_at2_duty():
def test_pyxel_volume_normalization():
source = "0: T120 L4 O4 V100\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes
# V100 -> round(100/127*15) = 12
@@ -76,16 +89,14 @@ def test_pyxel_volume_normalization():
def test_pyxel_volume_zero():
source = "0: T120 L4 O4 V0\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes and notes[0].velocity == 0
def test_pyxel_volume_max():
source = "0: T120 L4 O4 V127\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes and notes[0].velocity == 15
@@ -95,8 +106,7 @@ def test_pyxel_volume_max():
def test_pyxel_gate_time():
source = "0: T120 L4 O4 Q50\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes and notes[0].gate_time == 0.5
@@ -106,8 +116,7 @@ def test_pyxel_gate_time():
def test_pyxel_transpose():
source = "0: T120 L4 O4 K2\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# C4=60, transpose+2 -> 62
assert notes and notes[0].note_number == 62
@@ -118,8 +127,7 @@ def test_pyxel_transpose():
def test_pyxel_detune():
source = "0: T120 L4 O4 Y10\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes and notes[0].detune_cents == 10.0
@@ -129,24 +137,21 @@ def test_pyxel_detune():
def test_pyxel_repeat_expansion():
source = "0: T120 L4 O4\n[ C D ]2"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert len(notes) == 4
def test_pyxel_repeat_count3():
source = "0: T120 L4 O4\n[ C D ]3"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert len(notes) == 6
def test_pyxel_nested_repeat():
source = "0: T120 L4 O4\n[ [ C D ]2 E ]2"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
assert not any(e.severity == "error" for e in errors)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# Inner: [C D]2 -> C D C D (4 notes)
@@ -156,8 +161,7 @@ def test_pyxel_nested_repeat():
def test_pyxel_nested_repeat_count3():
source = "0: T120 L4 O4\n[ [ C D ]2 E ]3"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
assert not any(e.severity == "error" for e in errors)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# Inner: [C D]2 -> C D C D (4 notes)
@@ -170,8 +174,7 @@ def test_pyxel_nested_repeat_count3():
def test_ppmck_tie_note():
source = "A t120 l4 o4\n c4 & c4"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
# Tie should merge notes; check no error
assert not any(e.severity == "error" for e in errors)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
@@ -182,8 +185,7 @@ def test_ppmck_tie_note():
def test_pyxel_tie_note():
source = "0: T120 L4 O4\n C4 & C4"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
assert not any(e.severity == "error" for e in errors)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# C4 & C4 should produce a single note with doubled duration (192*2=384)
@@ -193,8 +195,7 @@ def test_pyxel_tie_note():
def test_pyxel_tie_length():
source = "0: T120 L4 O4\n C4 & 4"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# & 4 extends last note by l4 ticks
assert notes and notes[0].duration > 192 # 192 + 48 = 240
@@ -205,8 +206,7 @@ def test_pyxel_tie_length():
def test_ppmck_octave_up():
source = "A t120 l4 o4\n c > d"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes[0].note_number == 60 # o4 c
assert notes[1].note_number == 74 # o5 d
@@ -214,8 +214,7 @@ def test_ppmck_octave_up():
def test_pyxel_octave_down():
source = "0: T120 L4 O4\n C < D"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
assert notes[0].note_number == 60 # O4 C
assert notes[1].note_number == 50 # O3 D
@@ -226,8 +225,7 @@ def test_pyxel_octave_down():
def test_ppmck_dotted_note():
source = "A t120 l4 o4\n c."
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
notes = [e for e in ns.channels["Pulse1"].events if isinstance(e, NoteEvent)]
# l4 = 192 ticks, dotted = 192 + 96 = 288
assert notes and notes[0].duration == 288
@@ -238,8 +236,7 @@ def test_ppmck_dotted_note():
def test_ppmck_triangle_volume_ignored():
source = "T t120 l4 o3 v7\n c"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
notes = [e for e in ns.channels["Triangle"].events if isinstance(e, NoteEvent)]
assert notes and notes[0].velocity == 15 # default, v ignored
@@ -249,16 +246,14 @@ def test_ppmck_triangle_volume_ignored():
def test_pyxel_env_event():
source = "0: T120 L4 O4\n@ENV1 100 10\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
envs = [e for e in ns.channels["Pulse1"].events if isinstance(e, EnvelopeEvent)]
assert envs and envs[0].slot == 1
def test_pyxel_vib_event():
source = "0: T120 L4 O4\n@VIB1 10 20 5\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
from mml_composemusic_mcp.ir import VibratoEvent
vibs = [e for e in ns.channels["Pulse1"].events if isinstance(e, VibratoEvent)]
@@ -267,8 +262,7 @@ def test_pyxel_vib_event():
def test_pyxel_gli_event():
source = "0: T120 L4 O4\n@GLI1 100 50\n C"
- tokens = tokenize(source, "pyxel")
- ns, errors = parse_pyxel(source, tokens)
+ ns, errors = _analyze_pyxel(source)
from mml_composemusic_mcp.ir import GlideEvent
glis = [e for e in ns.channels["Pulse1"].events if isinstance(e, GlideEvent)]
@@ -280,8 +274,7 @@ def test_pyxel_gli_event():
def test_ppmck_noise_ignores_pitch():
source = "N t120 l4 v10\n c r"
- tokens = tokenize(source, "ppmck")
- ns, errors = parse_ppmck(source, tokens)
+ ns, errors = _analyze_ppmck(source)
notes = [e for e in ns.channels["Noise"].events if isinstance(e, NoteEvent)]
assert notes # NoteEvent is created but with midi=0
assert notes[0].note_number == 0 # pitch ignored
diff --git a/tests/test_synthesizer.py b/tests/test_synthesizer.py
index 722e64b..c3e110e 100644
--- a/tests/test_synthesizer.py
+++ b/tests/test_synthesizer.py
@@ -158,7 +158,7 @@ def test_write_wav_failure(tmp_path):
if errors:
from mml_composemusic_mcp.ir import ErrorCode
- assert errors[0].code == ErrorCode.SYSTEM_WAV_WRITE_FAILED
+ assert errors[0].code == ErrorCode.RUNTIME_WAV_WRITE_FAILED
# --- Channel summary ---