From 22f96694a96ed9fba169e77bc27d19d00edca083 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 12 Jul 2026 16:03:29 +0900 Subject: MML構文解析処理を追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_error_codes.py | 87 +++++++++++++++++++------------------ tests/test_lexer.py | 10 +++++ tests/test_parser.py | 39 ++++++++++------- tests/test_parser_detailed.py | 99 ++++++++++++++++++++----------------------- tests/test_synthesizer.py | 2 +- 5 files changed, 124 insertions(+), 113 deletions(-) (limited to 'tests') 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 --- -- cgit v1.2.3