summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-11 19:02:14 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-11 19:02:14 +0900
commitca35520338057512a71acd42bb116ffbb82cc192 (patch)
tree9faa2428aeb71b62497f21ceec93b99ef7f2a5ac /tests
parent7dca1616b8c891f5e140c7267323d1fcdde6741a (diff)
feat: implement Famicom-style MML composer MCP server
- Add FastMCP server with compose_mml tool - Support ppmck and pyxel MML syntax - Add lexer and parsers for both formats - Add NES APU-style synthesizer (pulse/triangle/noise) to WAV - Add MML templates (basic/melody/chord/drum/empty) for both modes - Add tests for lexer, parser, and server - Support stdio, http, sse, streamable-http transports - Update README, pyproject.toml, and .gitignore
Diffstat (limited to 'tests')
-rw-r--r--tests/test_lexer.py27
-rw-r--r--tests/test_parser.py53
-rw-r--r--tests/test_server.py33
3 files changed, 113 insertions, 0 deletions
diff --git a/tests/test_lexer.py b/tests/test_lexer.py
new file mode 100644
index 0000000..1bb2213
--- /dev/null
+++ b/tests/test_lexer.py
@@ -0,0 +1,27 @@
+"""Tests for the MML lexer."""
+
+from mml_composemusic_mcp.lexer import TokenType, tokenize
+
+
+def test_ppmck_tokens():
+ source = "A t150 l8 o4 v15 q2\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
+
+
+def test_pyxel_tokens():
+ source = "0: T150 L8 O4 V100 @1\nC D E F"
+ tokens = tokenize(source, "pyxel")
+ types = [t.type for t in tokens]
+ assert TokenType.TRACK_HEADER in types
+ assert TokenType.AT in types
+ assert TokenType.NOTE in types
+
+
+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)
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..b263aac
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,53 @@
+"""Tests for PPMCK and Pyxel parsers."""
+
+from mml_composemusic_mcp.ir import NoteEvent
+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
+
+PPMCK_EXAMPLE = """#TITLE "Test"
+A t150 l8 o4 v15 q2
+ c d e f
+B o3
+ c e g
+"""
+
+PYXEL_EXAMPLE = """0: T150 L8 O4 V100 @1
+ C D E F
+1: O3
+ C E G
+"""
+
+
+def test_ppmck_parse():
+ tokens = tokenize(PPMCK_EXAMPLE, "ppmck")
+ ns, errors = parse_ppmck(PPMCK_EXAMPLE, tokens)
+ 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
+
+
+def test_pyxel_parse():
+ tokens = tokenize(PYXEL_EXAMPLE, "pyxel")
+ ns, errors = parse_pyxel(PYXEL_EXAMPLE, tokens)
+ 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
+
+
+def test_pyxel_repeat_expansion():
+ 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
+
+
+def test_invalid_mode_error():
+ from mml_composemusic_mcp.server import _parse_mml
+
+ seq, errors = _parse_mml("A c", "unknown")
+ assert any(e.severity == "error" for e in errors)
diff --git a/tests/test_server.py b/tests/test_server.py
new file mode 100644
index 0000000..533c88b
--- /dev/null
+++ b/tests/test_server.py
@@ -0,0 +1,33 @@
+"""Tests for MCP server compose_mml tool."""
+
+from mml_composemusic_mcp.server import compose_mml
+
+PPMCK_MML = """#TITLE "Test"
+A t120 l4 o4 v15 q2
+ c e g c
+"""
+
+
+def test_compose_ppmck():
+ result = compose_mml(action="compose", mml=PPMCK_MML, mode="ppmck")
+ assert result["success"] is True
+ assert result["wav_path"] is not None
+ assert result["duration_sec"] > 0
+
+
+def test_validate_ppmck():
+ result = compose_mml(action="validate", mml=PPMCK_MML, mode="ppmck")
+ assert result["valid"] is True
+ assert result["channel_summary"]
+
+
+def test_template():
+ result = compose_mml(action="template", mode="pyxel", template="basic")
+ assert "mml" in result
+ assert "0:" in result["mml"]
+
+
+def test_compose_error():
+ result = compose_mml(action="compose", mml="A t0\n c", mode="ppmck")
+ assert result["success"] is False
+ assert any(e["severity"] == "error" for e in result["validation"]["errors"])