diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-07-12 17:58:19 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-07-12 17:59:16 +0900 |
| commit | bf36a773918959eeb6ffe7dae2fb8b0874438eb4 (patch) | |
| tree | da7ff0037ea923fea255d2ed69ed2fb51395d9b9 /tests/test_mml_conformance.py | |
| parent | f1cd4d7d16f6e75a50bb0c587c0f759f7cf710cd (diff) | |
feat(ppmck): mckc準拠の構文拡張とIR保持コマンド追加
- @をデューティ比、qをクオンタイズに修正(mckc原本準拠)
- ^タイ、Dディチューン、sスイープ、v+/v-相対音量を追加
- @v/@@/@MP/@EP/@EN エンベロープ・LFO系を構文解析してIR保持
- 上記IR保持コマンドは合成未対応を SEMANTIC_UNSUPPORTED_FEATURE で警告
- Design.md / MML_BNF.md を更新
- 全200テスト + ruff チェック通過
Diffstat (limited to 'tests/test_mml_conformance.py')
| -rw-r--r-- | tests/test_mml_conformance.py | 53 |
1 files changed, 51 insertions, 2 deletions
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)) |
