From bf36a773918959eeb6ffe7dae2fb8b0874438eb4 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 12 Jul 2026 17:58:19 +0900 Subject: feat(ppmck): mckc準拠の構文拡張とIR保持コマンド追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - @をデューティ比、qをクオンタイズに修正(mckc原本準拠) - ^タイ、Dディチューン、sスイープ、v+/v-相対音量を追加 - @v/@@/@MP/@EP/@EN エンベロープ・LFO系を構文解析してIR保持 - 上記IR保持コマンドは合成未対応を SEMANTIC_UNSUPPORTED_FEATURE で警告 - Design.md / MML_BNF.md を更新 - 全200テスト + ruff チェック通過 --- tests/test_mml_conformance.py | 53 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) (limited to 'tests/test_mml_conformance.py') 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)) -- cgit v1.2.3