aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/tests
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2025-10-24 23:15:35 +0800
committer简律纯 <i@jyunko.cn>2025-10-24 23:15:35 +0800
commit08299b37dfda86e56e4f2b442f68ccd2da7a82e3 (patch)
treee155d11412a26f692d08b8eb796fa689fc5a4019 /tests
parent990048eb2163127615de60d9359c150bdfb99536 (diff)
downloadconventional_role_play-08299b37dfda86e56e4f2b442f68ccd2da7a82e3.tar.gz
conventional_role_play-08299b37dfda86e56e4f2b442f68ccd2da7a82e3.zip
feat: Enhance Processor, RuleExtractor, and Renderers with type hints and improved documentation
- Added type hints to Processor methods for better clarity and type safety. - Improved documentation for Processor methods, including detailed descriptions of parameters and return types. - Refactored RuleExtractor to support optional configuration file loading and added error handling for file operations. - Enhanced MarkdownRenderer to handle both list and dictionary inputs, with improved rendering logic. - Created comprehensive examples and tests for all components, ensuring robust functionality and error handling. - Added example rules for D&D 5E and structured output files for various formats (JSON, HTML, Markdown). - Established a testing framework with clear instructions and coverage reporting.
Diffstat (limited to 'tests')
-rw-r--r--tests/README.md63
-rw-r--r--tests/run_tests.py36
-rw-r--r--tests/test_parser.py142
-rw-r--r--tests/test_processor.py114
-rw-r--r--tests/test_renderers.py114
-rw-r--r--tests/test_rule_extractor.py99
6 files changed, 568 insertions, 0 deletions
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..97a8028
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,63 @@
+# ConventionalRP 测试套件
+
+本目录包含 ConventionalRP SDK 的所有单元测试。
+
+## 测试文件
+
+- `test_parser.py` - Parser 解析器测试
+- `test_processor.py` - Processor 处理器测试
+- `test_rule_extractor.py` - RuleExtractor 规则提取器测试
+- `test_renderers.py` - 渲染器测试(HTML/JSON/Markdown)
+- `test_pyo3.py` - PyO3 Rust 扩展测试
+
+## 运行测试
+
+### 运行所有测试
+
+```bash
+python tests/run_tests.py
+```
+
+### 运行单个测试文件
+
+```bash
+python -m unittest tests/test_parser.py
+python -m unittest tests/test_processor.py
+```
+
+### 运行特定测试类
+
+```bash
+python -m unittest tests.test_parser.TestParser
+```
+
+### 运行特定测试方法
+
+```bash
+python -m unittest tests.test_parser.TestParser.test_load_rules_success
+```
+
+## 测试覆盖率
+
+要查看测试覆盖率,请安装 `coverage` 并运行:
+
+```bash
+pip install coverage
+coverage run -m unittest discover -s tests -p "test_*.py"
+coverage report
+coverage html # 生成 HTML 报告
+```
+
+## 测试数据
+
+测试使用临时文件来模拟规则文件和日志文件,测试完成后会自动清理。
+
+## 添加新测试
+
+创建新的测试文件时,请遵循以下约定:
+
+1. 文件名以 `test_` 开头
+2. 测试类继承自 `unittest.TestCase`
+3. 测试方法以 `test_` 开头
+4. 使用 `setUp()` 和 `tearDown()` 方法管理测试状态
+5. 添加清晰的文档字符串说明测试目的
diff --git a/tests/run_tests.py b/tests/run_tests.py
new file mode 100644
index 0000000..4cfc2d4
--- /dev/null
+++ b/tests/run_tests.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+"""
+测试套件运行器
+运行所有单元测试
+"""
+
+import sys
+import unittest
+from pathlib import Path
+
+# 添加 src 目录到路径
+src_path = Path(__file__).parent.parent / "src"
+sys.path.insert(0, str(src_path))
+
+
+def run_all_tests():
+ """运行所有测试"""
+ # 创建测试加载器
+ loader = unittest.TestLoader()
+
+ # 从当前目录加载所有测试
+ suite = loader.discover(
+ start_dir=Path(__file__).parent,
+ pattern='test_*.py'
+ )
+
+ # 运行测试
+ runner = unittest.TextTestRunner(verbosity=2)
+ result = runner.run(suite)
+
+ # 返回结果
+ return 0 if result.wasSuccessful() else 1
+
+
+if __name__ == "__main__":
+ sys.exit(run_all_tests())
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..595d0b4
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+"""
+Parser 模块单元测试
+"""
+
+import unittest
+import tempfile
+from pathlib import Path
+from conventionalrp.core.parser import Parser
+
+
+class TestParser(unittest.TestCase):
+ """Parser 类的单元测试"""
+
+ def setUp(self):
+ """设置测试环境"""
+ self.parser = Parser()
+
+ # 创建临时规则文件
+ self.temp_rules = tempfile.NamedTemporaryFile(
+ mode='w',
+ suffix='.json5',
+ delete=False,
+ encoding='utf-8'
+ )
+ self.temp_rules.write('''{
+ metadata: [{
+ type: "metadata",
+ patterns: ["^\\\\[(.+?)\\\\]\\\\s*<(.+?)>\\\\s*(.*)$"],
+ groups: ["timestamp", "speaker", "content"],
+ priority: 100
+ }],
+ content: [
+ {
+ type: "dice_roll",
+ match_type: "enclosed",
+ patterns: ["\\\\[d(\\\\d+)\\\\s*=\\\\s*(\\\\d+)\\\\]"],
+ groups: ["dice_type", "result"],
+ priority: 90
+ },
+ {
+ type: "dialogue",
+ match_type: "enclosed",
+ patterns: ["「(.+?)」"],
+ groups: ["dialogue_text"],
+ priority: 60
+ },
+ {
+ type: "text",
+ match_type: "prefix",
+ patterns: ["^(.+)$"],
+ groups: ["text_content"],
+ priority: 1
+ }
+ ]
+ }''')
+ self.temp_rules.close()
+
+ # 创建临时日志文件
+ self.temp_log = tempfile.NamedTemporaryFile(
+ mode='w',
+ suffix='.txt',
+ delete=False,
+ encoding='utf-8'
+ )
+ self.temp_log.write('''[2025-10-24 14:30:01] <艾莉娅> 「我要检查这扇门」
+[2025-10-24 14:30:05] <DiceBot> 检定结果: [d20 = 18]
+[2025-10-24 14:30:10] <DM> 你发现了陷阱
+''')
+ self.temp_log.close()
+
+ def tearDown(self):
+ """清理测试环境"""
+ Path(self.temp_rules.name).unlink(missing_ok=True)
+ Path(self.temp_log.name).unlink(missing_ok=True)
+
+ def test_load_rules_success(self):
+ """测试成功加载规则文件"""
+ self.parser.load_rules(self.temp_rules.name)
+ self.assertIn("metadata", self.parser.rules)
+ self.assertIn("content", self.parser.rules)
+
+ def test_load_rules_file_not_found(self):
+ """测试加载不存在的规则文件"""
+ with self.assertRaises(FileNotFoundError):
+ self.parser.load_rules("nonexistent_file.json5")
+
+ def test_parse_log_success(self):
+ """测试成功解析日志"""
+ self.parser.load_rules(self.temp_rules.name)
+ result = self.parser.parse_log(self.temp_log.name)
+
+ self.assertIsInstance(result, list)
+ self.assertGreater(len(result), 0)
+
+ # 检查第一条记录
+ first_entry = result[0]
+ self.assertIn("timestamp", first_entry)
+ self.assertIn("speaker", first_entry)
+ self.assertIn("content", first_entry)
+ self.assertEqual(first_entry["speaker"], "艾莉娅")
+
+ def test_parse_log_file_not_found(self):
+ """测试解析不存在的日志文件"""
+ self.parser.load_rules(self.temp_rules.name)
+ with self.assertRaises(FileNotFoundError):
+ self.parser.parse_log("nonexistent_log.txt")
+
+ def test_match_metadata(self):
+ """测试元数据匹配"""
+ self.parser.load_rules(self.temp_rules.name)
+ line = "[2025-10-24 14:30:01] <艾莉娅> 测试内容"
+ result = self.parser._match_metadata(line)
+
+ self.assertIsNotNone(result)
+ self.assertEqual(result["type"], "metadata")
+ self.assertEqual(result["timestamp"], "2025-10-24 14:30:01")
+ self.assertEqual(result["speaker"], "艾莉娅")
+
+ def test_parse_line_content_dialogue(self):
+ """测试解析对话内容"""
+ self.parser.load_rules(self.temp_rules.name)
+ line = "「这是一段对话」"
+ result = self.parser._parse_line_content(line)
+
+ self.assertIsInstance(result, list)
+ self.assertGreater(len(result), 0)
+ self.assertEqual(result[0]["type"], "dialogue")
+
+ def test_parse_line_content_dice_roll(self):
+ """测试解析骰子投掷"""
+ self.parser.load_rules(self.temp_rules.name)
+ line = "检定结果: [d20 = 18]"
+ result = self.parser._parse_line_content(line)
+
+ # 应该包含文本和骰子投掷
+ dice_tokens = [t for t in result if t["type"] == "dice_roll"]
+ self.assertGreater(len(dice_tokens), 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_processor.py b/tests/test_processor.py
new file mode 100644
index 0000000..c08fc52
--- /dev/null
+++ b/tests/test_processor.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""
+Processor 模块单元测试
+"""
+
+import unittest
+from conventionalrp.core.processor import Processor
+
+
+class TestProcessor(unittest.TestCase):
+ """Processor 类的单元测试"""
+
+ def setUp(self):
+ """设置测试环境"""
+ self.processor = Processor()
+ self.sample_tokens = [
+ {
+ "type": "metadata",
+ "timestamp": "2025-10-24 14:30:01",
+ "speaker": "艾莉娅",
+ "content": [
+ {"type": "dialogue", "content": "「测试对话」"}
+ ]
+ },
+ {
+ "type": "metadata",
+ "timestamp": "2025-10-24 14:30:05",
+ "speaker": "DM",
+ "content": [
+ {"type": "text", "content": "测试文本"}
+ ]
+ }
+ ]
+
+ def test_init_without_rules(self):
+ """测试无规则初始化"""
+ processor = Processor()
+ self.assertEqual(processor.rules, {})
+
+ def test_init_with_rules(self):
+ """测试带规则初始化"""
+ rules = {"test_rule": "value"}
+ processor = Processor(rules)
+ self.assertEqual(processor.rules, rules)
+
+ def test_process_tokens(self):
+ """测试处理 token 列表"""
+ result = self.processor.process_tokens(self.sample_tokens)
+
+ self.assertIsInstance(result, list)
+ self.assertEqual(len(result), len(self.sample_tokens))
+
+ # 检查处理标记
+ for token in result:
+ if "timestamp" in token:
+ self.assertTrue(token.get("processed"))
+
+ def test_apply_rules(self):
+ """测试应用规则到单个 token"""
+ token = self.sample_tokens[0]
+ result = self.processor.apply_rules(token)
+
+ self.assertIsInstance(result, dict)
+ self.assertIn("timestamp", result)
+ self.assertTrue(result.get("processed"))
+
+ def test_generate_json_output(self):
+ """测试生成 JSON 输出"""
+ output = self.processor.generate_json_output(self.sample_tokens)
+
+ self.assertIsInstance(output, str)
+ self.assertIn("timestamp", output)
+ self.assertIn("speaker", output)
+
+ def test_generate_html_output(self):
+ """测试生成 HTML 输出"""
+ output = self.processor.generate_html_output(self.sample_tokens)
+
+ self.assertIsInstance(output, str)
+ self.assertIn("<html>", output)
+ self.assertIn("</html>", output)
+
+ def test_generate_markdown_output(self):
+ """测试生成 Markdown 输出"""
+ output = self.processor.generate_markdown_output(self.sample_tokens)
+
+ self.assertIsInstance(output, str)
+ self.assertIn("-", output)
+
+ def test_generate_output_json(self):
+ """测试生成输出 - JSON 格式"""
+ output = self.processor.generate_output(self.sample_tokens, "json")
+ self.assertIsInstance(output, str)
+
+ def test_generate_output_html(self):
+ """测试生成输出 - HTML 格式"""
+ output = self.processor.generate_output(self.sample_tokens, "html")
+ self.assertIsInstance(output, str)
+
+ def test_generate_output_markdown(self):
+ """测试生成输出 - Markdown 格式"""
+ output = self.processor.generate_output(self.sample_tokens, "markdown")
+ self.assertIsInstance(output, str)
+
+ def test_generate_output_unsupported_format(self):
+ """测试生成输出 - 不支持的格式"""
+ with self.assertRaises(ValueError) as context:
+ self.processor.generate_output(self.sample_tokens, "pdf")
+
+ self.assertIn("Unsupported format type", str(context.exception))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
new file mode 100644
index 0000000..13e4540
--- /dev/null
+++ b/tests/test_renderers.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+"""
+Renderers 模块单元测试
+"""
+
+import unittest
+import json
+from conventionalrp.renderers.html_renderer import HTMLRenderer
+from conventionalrp.renderers.json_renderer import JSONRenderer
+from conventionalrp.renderers.markdown_renderer import MarkdownRenderer
+
+
+class TestRenderers(unittest.TestCase):
+ """测试所有渲染器"""
+
+ def setUp(self):
+ """设置测试数据"""
+ self.sample_data = [
+ {
+ "type": "metadata",
+ "timestamp": "2025-10-24 14:30:01",
+ "speaker": "艾莉娅",
+ "content": [
+ {"type": "dialogue", "content": "「测试对话」"}
+ ]
+ },
+ {
+ "type": "metadata",
+ "timestamp": "2025-10-24 14:30:05",
+ "speaker": "DM",
+ "content": [
+ {"type": "text", "content": "测试文本"}
+ ]
+ }
+ ]
+
+ self.dict_data = {
+ "title": "测试标题",
+ "content": "测试内容"
+ }
+
+ def test_html_renderer_basic(self):
+ """测试 HTML 渲染器基本功能"""
+ renderer = HTMLRenderer()
+ output = renderer.render(self.sample_data)
+
+ self.assertIsInstance(output, str)
+ self.assertIn("<html>", output)
+ self.assertIn("</html>", output)
+ self.assertIn("<title>", output)
+
+ def test_html_renderer_set_style(self):
+ """测试 HTML 渲染器设置样式"""
+ renderer = HTMLRenderer()
+ renderer.set_style("custom_style")
+ # 当前实现为占位符,仅测试不抛出异常
+ self.assertIsNotNone(renderer)
+
+ def test_json_renderer_basic(self):
+ """测试 JSON 渲染器基本功能"""
+ renderer = JSONRenderer()
+ output = renderer.render(self.sample_data)
+
+ self.assertIsInstance(output, str)
+
+ # 验证输出是有效的 JSON
+ parsed = json.loads(output)
+ self.assertIsInstance(parsed, list)
+ self.assertEqual(len(parsed), len(self.sample_data))
+
+ def test_json_renderer_unicode(self):
+ """测试 JSON 渲染器处理 Unicode"""
+ renderer = JSONRenderer()
+ output = renderer.render(self.sample_data)
+
+ # 应该保留中文字符
+ self.assertIn("艾莉娅", output)
+ self.assertIn("测试", output)
+
+ def test_markdown_renderer_basic(self):
+ """测试 Markdown 渲染器基本功能"""
+ renderer = MarkdownRenderer()
+ output = renderer.render(self.dict_data)
+
+ self.assertIsInstance(output, str)
+ self.assertIn("##", output) # 应该有标题标记
+ self.assertIn("测试标题", output)
+
+ def test_markdown_renderer_set_style(self):
+ """测试 Markdown 渲染器设置样式"""
+ renderer = MarkdownRenderer()
+ style = {"heading_level": 2}
+ renderer.set_style(style)
+ self.assertEqual(renderer.style, style)
+
+ def test_all_renderers_empty_data(self):
+ """测试所有渲染器处理空数据"""
+ empty_data = []
+
+ html_renderer = HTMLRenderer()
+ html_output = html_renderer.render(empty_data)
+ self.assertIsInstance(html_output, str)
+
+ json_renderer = JSONRenderer()
+ json_output = json_renderer.render(empty_data)
+ self.assertEqual(json_output, "[]")
+
+ md_renderer = MarkdownRenderer()
+ md_output = md_renderer.render({})
+ self.assertEqual(md_output, "")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_rule_extractor.py b/tests/test_rule_extractor.py
new file mode 100644
index 0000000..6c4d585
--- /dev/null
+++ b/tests/test_rule_extractor.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+"""
+RuleExtractor 模块单元测试
+"""
+
+import unittest
+import tempfile
+import json5
+from pathlib import Path
+from conventionalrp.extractors.rule_extractor import RuleExtractor
+
+
+class TestRuleExtractor(unittest.TestCase):
+ """RuleExtractor 类的单元测试"""
+
+ def setUp(self):
+ """设置测试环境"""
+ # 创建临时规则文件
+ self.temp_rules = tempfile.NamedTemporaryFile(
+ mode='w',
+ suffix='.json5',
+ delete=False,
+ encoding='utf-8'
+ )
+ self.temp_rules.write('''{
+ test_rule: "test_value",
+ metadata: [{type: "test"}],
+ content: [{type: "test_content"}]
+ }''')
+ self.temp_rules.close()
+
+ def tearDown(self):
+ """清理测试环境"""
+ Path(self.temp_rules.name).unlink(missing_ok=True)
+
+ def test_init_without_file(self):
+ """测试不带配置文件的初始化"""
+ extractor = RuleExtractor()
+ self.assertEqual(extractor.rules, {})
+ self.assertIsNone(extractor.config_file)
+
+ def test_init_with_file(self):
+ """测试带配置文件的初始化"""
+ extractor = RuleExtractor(self.temp_rules.name)
+ self.assertIsNotNone(extractor.rules)
+ self.assertIn("test_rule", extractor.rules)
+
+ def test_load_rules_from_file_success(self):
+ """测试成功加载规则文件"""
+ extractor = RuleExtractor()
+ rules = extractor.load_rules_from_file(self.temp_rules.name)
+
+ self.assertIsInstance(rules, dict)
+ self.assertIn("test_rule", rules)
+ self.assertEqual(rules["test_rule"], "test_value")
+
+ def test_load_rules_from_file_not_found(self):
+ """测试加载不存在的文件"""
+ extractor = RuleExtractor()
+ with self.assertRaises(FileNotFoundError):
+ extractor.load_rules_from_file("nonexistent.json5")
+
+ def test_load_rules_empty_file(self):
+ """测试加载空文件"""
+ empty_file = tempfile.NamedTemporaryFile(
+ mode='w',
+ suffix='.json5',
+ delete=False,
+ encoding='utf-8'
+ )
+ empty_file.write('')
+ empty_file.close()
+
+ try:
+ extractor = RuleExtractor()
+ with self.assertRaises(ValueError):
+ extractor.load_rules_from_file(empty_file.name)
+ finally:
+ Path(empty_file.name).unlink(missing_ok=True)
+
+ def test_load_rules_method(self):
+ """测试 load_rules 方法"""
+ extractor = RuleExtractor()
+ rules = extractor.load_rules(self.temp_rules.name)
+
+ self.assertIsInstance(rules, dict)
+ self.assertEqual(extractor.rules, rules)
+
+ def test_extract_method(self):
+ """测试 extract 方法"""
+ extractor = RuleExtractor(self.temp_rules.name)
+ extracted = extractor.extract()
+
+ self.assertIsInstance(extracted, dict)
+ self.assertEqual(extracted, extractor.rules)
+
+
+if __name__ == "__main__":
+ unittest.main()