from __future__ import annotations
import sys
import types
from pathlib import Path
import pytest
FAKE_LLM_OUTPUT = """
<<<DESIGN_SUMMARY_START>>>
这里是设计摘要
<<<DESIGN_SUMMARY_END>>>
<<<SERVER_PY_START>>>
print("hello from server")
<<<SERVER_PY_END>>>
<<<MCP_CONFIG_JSON_START>>>
{"mcpServers": {"demo_mcp": {}}}
<<<MCP_CONFIG_JSON_END>>>
<<<MCP_RPM_YAML_START>>>
name: demo_mcp
summary: demo
description: demo
version: "1.0.0"
release: "1"
license: MIT
build_dependencies:
system: []
runtime_dependencies:
system: []
packages: []
files:
required: []
install_script: ""
<<<MCP_RPM_YAML_END>>>
"""
def _load_module():
"""以自定义包名加载 scripts/testsuite_mcp_generator.py,便于解析相对导入。"""
import importlib.util
lo_mod = types.ModuleType("langchain_openai")
class DummyChatOpenAI:
def __init__(self, *args, **kwargs) -> None:
"""Fake init."""
def invoke(self, messages):
return types.SimpleNamespace(content=FAKE_LLM_OUTPUT)
lo_mod.ChatOpenAI = DummyChatOpenAI
sys.modules["langchain_openai"] = lo_mod
pkg = types.ModuleType("testscripts")
pkg.__path__ = []
sys.modules["testscripts"] = pkg
agent_pkg = types.ModuleType("testscripts.agent")
agent_pkg.__path__ = []
sys.modules["testscripts.agent"] = agent_pkg
invoke_mod = types.ModuleType("testscripts.agent.invoke_llm")
def _get_llm_config(provider: str):
return "https://example.com", f"fake-model-{provider}"
invoke_mod._get_llm_config = _get_llm_config
sys.modules["testscripts.agent.invoke_llm"] = invoke_mod
tools_pkg = types.ModuleType("testscripts.tools")
tools_pkg.__path__ = []
sys.modules["testscripts.tools"] = tools_pkg
logger_mod = types.ModuleType("testscripts.tools.logger")
class DummyLogger:
def debug(self, *args, **kwargs) -> None:
pass
def info(self, *args, **kwargs) -> None:
pass
def error(self, *args, **kwargs) -> None:
pass
def exception(self, *args, **kwargs) -> None:
pass
def add_file_handler(logger, logfile) -> None:
"""测试中无需真正写入日志文件。"""
return None
logger_mod.logger = DummyLogger()
logger_mod.add_file_handler = add_file_handler
sys.modules["testscripts.tools.logger"] = logger_mod
script_path = Path(__file__).resolve().parents[1] / "testsuite_mcp_generator.py"
spec = importlib.util.spec_from_file_location(
"testscripts.testsuite_mcp_generator",
script_path,
)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
sys.modules["testscripts.testsuite_mcp_generator"] = module
spec.loader.exec_module(module)
return module
def test_extract_comment_and_run_test_basic(tmp_path):
mod = _load_module()
script = tmp_path / "case.sh"
script.write_text(
"""#!/bin/bash
# 这是测试用例
# 第二行注释
function pre_test() {
:
}
function run_test() {
echo "hello"
ls /
}
""",
encoding="utf-8",
)
info = mod._extract_comment_and_run_test(str(script))
assert info["script"] == "case.sh"
assert "这是测试用例" in info["description"]
assert "第二行注释" in info["description"]
assert 'echo "hello"' in info["run_test"]
assert "ls /" in info["run_test"]
def test_collect_testsuite_context_and_invalid_dir(tmp_path):
mod = _load_module()
suite_dir = tmp_path / "suite"
suite_dir.mkdir()
(suite_dir / "a.sh").write_text("function run_test() { echo a; }\n", encoding="utf-8")
(suite_dir / "b.txt").write_text("not shell", encoding="utf-8")
result = mod._collect_testsuite_context(str(suite_dir))
assert len(result) == 1
assert result[0]["script"] == "a.sh"
with pytest.raises(FileNotFoundError):
mod._collect_testsuite_context(str(tmp_path / "not_exist"))
def test_extract_between_markers():
mod = _load_module()
text = "AAA<<<X>>>\nhello\n<<<Y>>>BBB"
assert mod._extract_between_markers(text, "<<<X>>>", "<<<Y>>>") == "hello"
assert mod._extract_between_markers(text, "<<<NO>>>", "<<<Y>>>") == ""
def test_run_generate_mcp_from_config_success(tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path / "home"))
for name in list(sys.modules):
if name.startswith("testscripts") or name == "langchain_openai":
sys.modules.pop(name)
mod = _load_module()
suite_dir = tmp_path / "suite"
suite_dir.mkdir()
(suite_dir / "demo.sh").write_text(
"""#!/bin/bash
# demo case
function run_test() {
echo demo
}
""",
encoding="utf-8",
)
server_root = tmp_path / "servers_root"
config = {
"test_suite_dir": str(suite_dir),
"software_name": "demo",
"openai_key": "dummy-key",
"llm_provider": "openai",
"server_root": str(server_root),
}
result = mod.run_generate_mcp_from_config(config, debug_mode=False)
assert result["status"] == "success"
assert result["server_name"] == "demo_mcp"
files = result["files"]
server_py = Path(files["server.py"])
mcp_cfg = Path(files["mcp_config.json"])
mcp_rpm = Path(files["mcp-rpm.yaml"])
for p in (server_py, mcp_cfg, mcp_rpm):
assert p.is_file()
assert "设计摘要" in result["design_summary"]
def test_run_generate_mcp_from_config_invalid_inputs(tmp_path, monkeypatch):
for name in list(sys.modules):
if name.startswith("testscripts") or name == "langchain_openai":
sys.modules.pop(name)
mod = _load_module()
bad = mod.run_generate_mcp_from_config(
{
"test_suite_dir": str(tmp_path / "not-exist"),
"software_name": "demo",
"openai_key": "dummy",
}
)
assert bad["status"] == "failed"
assert "无效的测试套目录" in bad["message"]
ok_dir = tmp_path / "suite"
ok_dir.mkdir()
bad2 = mod.run_generate_mcp_from_config(
{
"test_suite_dir": str(ok_dir),
"software_name": "",
"openai_key": "dummy",
}
)
assert bad2["status"] == "failed"
assert "software_name 不能为空" in bad2["message"]
monkeypatch.delenv("OPENAI_KEY", raising=False)
bad3 = mod.run_generate_mcp_from_config(
{
"test_suite_dir": str(ok_dir),
"software_name": "demo",
}
)
assert bad3["status"] == "failed"
assert "openai_key 未提供" in bad3["message"]