diff options
| author | 2025-10-24 23:15:35 +0800 | |
|---|---|---|
| committer | 2025-10-24 23:15:35 +0800 | |
| commit | 08299b37dfda86e56e4f2b442f68ccd2da7a82e3 (patch) | |
| tree | e155d11412a26f692d08b8eb796fa689fc5a4019 /tests | |
| parent | 990048eb2163127615de60d9359c150bdfb99536 (diff) | |
| download | conventional_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.md | 63 | ||||
| -rw-r--r-- | tests/run_tests.py | 36 | ||||
| -rw-r--r-- | tests/test_parser.py | 142 | ||||
| -rw-r--r-- | tests/test_processor.py | 114 | ||||
| -rw-r--r-- | tests/test_renderers.py | 114 | ||||
| -rw-r--r-- | tests/test_rule_extractor.py | 99 |
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() |
