Skip to content

Commit 2079c8e

Browse files
authored
test(python): 全面测试 (#1050)
1 parent c63b6c2 commit 2079c8e

6 files changed

Lines changed: 1798 additions & 255 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ jobs:
144144
run: |
145145
python3 -m pip install ./source/binding/Python
146146
python3 ./test/python/binding_test.py ./source/binding/Python ./install
147+
python3 ./test/python/pipeline_test.py ./source/binding/Python ./install
147148
python3 ./test/agent/agent_main_test.py ./source/binding/Python ./install
148149
149150
- name: Run NodeJS testing
@@ -271,6 +272,7 @@ jobs:
271272
source .venv/bin/activate
272273
python3 -m pip install ./source/binding/Python
273274
python3 ./test/python/binding_test.py ./source/binding/Python ./install
275+
python3 ./test/python/pipeline_test.py ./source/binding/Python ./install
274276
python3 ./test/agent/agent_main_test.py ./source/binding/Python ./install
275277
276278
- name: Run NodeJS testing
@@ -398,6 +400,7 @@ jobs:
398400
source .venv/bin/activate
399401
python3 -m pip install ./source/binding/Python
400402
python3 ./test/python/binding_test.py ./source/binding/Python ./install
403+
python3 ./test/python/pipeline_test.py ./source/binding/Python ./install
401404
python3 ./test/agent/agent_main_test.py ./source/binding/Python ./install
402405
403406
- name: Run NodeJS testing

AGENTS.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ MaaFramework/
183183
- [ ] 更新 Pipeline Schema(如涉及)
184184
- [ ] 添加/更新中英文文档
185185
- [ ] 更新 MaaMsg.h 及绑定层消息解析代码(如有新消息)
186-
187-
> **注意**:如没有特别说明,不需要更新测试用例。
186+
- [ ] 更新 `test/python/` 中的单元测试
187+
- [ ] 更新 `test/agent/` 中的 Agent 单元测试(如涉及 Agent 接口)
188188

189189
## 常见开发场景
190190

@@ -194,8 +194,9 @@ MaaFramework/
194194
2.`source/MaaFramework/Resource/PipelineParser.cpp` 添加解析逻辑
195195
3.`source/MaaFramework/Resource/PipelineDumper.cpp` 添加序列化逻辑
196196
4. 更新 `source/binding` 中的解析逻辑(注意:binding 中的数据来源于 `PipelineDumper` 序列化的 JSON,而非原始 Pipeline JSON。)
197-
5. 更新 `tools/pipeline.schema.json`
198-
6. 更新中英文文档 `docs/*/3.1-*`
197+
5.`test/python/pipeline_test.py` 添加相应的单元测试,验证字段的解析、override 和 get 功能
198+
6. 更新 `tools/pipeline.schema.json`
199+
7. 更新中英文文档 `docs/*/3.1-*`
199200

200201
### 修改回调消息(MaaMsg)
201202

@@ -214,12 +215,26 @@ MaaFramework/
214215
2.`source/MaaAgentClient/Client/AgentClient.cpp` 实现该方法
215216
3.`source/MaaAgentServer/RemoteInstance/Remote<Module>.h` 添加对应方法声明
216217
4.`source/MaaAgentServer/RemoteInstance/Remote<Module>.cpp` 实现远程调用逻辑
218+
5.`test/python/binding_test.py` 添加相应的 API 调用测试
219+
6.`test/agent/` 中添加 Agent 单元测试,验证远程调用功能
220+
221+
**Agent 单元测试说明**
222+
223+
- `test/agent/agent_main_test.py`:AgentClient 端测试(主进程),负责创建 Resource/Controller/Tasker,并测试 AgentClient 的连接管理、custom_*_list 等 API
224+
- `test/agent/agent_child_test.py`:AgentServer 端测试(子进程),在自定义识别器/动作中测试 Remote* 类提供的 Context/Tasker/Resource/Controller API
217225

218226
**Agent 架构说明**
219227

220228
- **AgentClient**:运行在主程序中,将 Custom 请求转发到 Server,并处理 Server 的远程调用
221229
- **AgentServer**:运行在用户进程中,注册自定义识别器/动作,通过 Remote* 类代理访问主程序实例
222230

231+
**AgentServer 不支持的 API**(参考 `source/MaaAgentServer/API/MaaAgentServerNotImpl.cpp`):
232+
233+
- 创建/销毁实例:`MaaResourceCreate``MaaTaskerCreate``MaaControllerDestroy`
234+
- 全局选项:`MaaGlobalSetOption``MaaSetGlobalOption`
235+
- 插件加载:`MaaGlobalLoadPlugin`
236+
- Sink 管理:Remote* 实例不支持 `add_sink`/`remove_sink`,需使用 `AgentServer.add_*_sink` 代替
237+
223238
### 修改 workflows 和 actions
224239

225240
修改 workflows 和 actions 后如果不会自动触发 workflow ,请提醒我 dry run 相关的所有 workflows。

test/agent/agent_child_test.py

Lines changed: 183 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
1+
"""
2+
AgentServer 端测试
3+
4+
测试范围:
5+
1. AgentServer: 注册自定义识别器/动作、事件监听器、生命周期管理
6+
2. Context: run_task/run_recognition/run_action、override_*、clone、anchor、hit_count
7+
3. Tasker: get_*_detail、running/stopping/post_stop、clear_cache
8+
4. Resource: get_node_data、node_list、custom_*_list、hash、override_*
9+
5. Controller: 各种输入操作、post_key_down/up、post_scroll
10+
"""
11+
112
import os
213
from pathlib import Path
314
import sys
415
import io
16+
import numpy
517

618
# Fix encoding issues on Windows (cp1252 cannot encode some Unicode characters)
7-
if sys.stdout.encoding != 'utf-8':
8-
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
9-
if sys.stderr.encoding != 'utf-8':
10-
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
19+
if sys.stdout.encoding != "utf-8":
20+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
21+
if sys.stderr.encoding != "utf-8":
22+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
1123

1224
if len(sys.argv) < 4:
1325
print("Call agent_main_test.py instead of this file.")
@@ -36,6 +48,10 @@
3648
from maa.library import Library
3749

3850

51+
analyzed: bool = False
52+
runned: bool = False
53+
54+
3955
def main():
4056
if len(sys.argv) < 2:
4157
print("Usage: python agent_main.py <socket_id>")
@@ -56,8 +72,14 @@ def analyze(
5672
argv: CustomRecognition.AnalyzeArg,
5773
) -> CustomRecognition.AnalyzeResult:
5874
print(
59-
f"on MyRecognition.analyze, context: {context}, image: {argv.image.shape}, task_detail: {argv.task_detail}, reco_name: {argv.custom_recognition_name}, reco_param: {argv.custom_recognition_param}"
75+
f"on MyRecognition.analyze, context: {context}, image: {argv.image.shape}, "
76+
f"task_detail: {argv.task_detail}, reco_name: {argv.custom_recognition_name}, "
77+
f"reco_param: {argv.custom_recognition_param}"
6078
)
79+
80+
# ============================================================
81+
# Context API 测试
82+
# ============================================================
6183
entry = "ColorMatch"
6284
ppover = {
6385
"ColorMatch": {
@@ -67,22 +89,95 @@ def analyze(
6789
"action": "Click",
6890
}
6991
}
92+
93+
# 测试 run_task
7094
context.run_task(entry, ppover)
71-
action_detail = context.run_action(entry, [114, 514, 191, 810], "RunAction Detail", ppover)
72-
print(f"action_detail: {action_detail}")
95+
96+
# 测试 run_action
97+
action_detail = context.run_action(
98+
entry, [114, 514, 191, 810], "RunAction Detail", ppover
99+
)
100+
print(f" action_detail: {action_detail}")
101+
102+
# 测试 run_recognition
73103
reco_detail = context.run_recognition(entry, argv.image, ppover)
74-
print(f"reco_detail: {reco_detail}")
104+
print(f" reco_detail: {reco_detail}")
75105

106+
# 测试 clone 和 override
76107
new_ctx = context.clone()
77108
new_ctx.override_pipeline({"TaskA": {}, "TaskB": {}})
78109
new_ctx.override_next(argv.node_name, ["TaskA", "TaskB"])
79110

80-
node_detail = new_ctx.tasker.get_latest_node("ColorMatch")
81-
print(node_detail)
111+
# 测试 get_node_data (Context 级别)
112+
node_data = new_ctx.get_node_data(argv.node_name)
113+
print(f" ctx.get_node_data keys: {list(node_data.keys()) if node_data else None}")
114+
115+
# 测试 anchor API
116+
new_ctx.set_anchor("test_anchor", "TaskA")
117+
anchor_result = new_ctx.get_anchor("test_anchor")
118+
print(f" anchor_result: {anchor_result}")
119+
assert anchor_result == "TaskA", f"anchor should be 'TaskA', got {anchor_result}"
120+
121+
# 测试 hit count API
122+
hit_count = new_ctx.get_hit_count(argv.node_name)
123+
print(f" hit_count: {hit_count}")
124+
new_ctx.clear_hit_count(argv.node_name)
82125

126+
# 测试 override_image (Context 级别)
127+
test_image = numpy.zeros((100, 100, 3), dtype=numpy.uint8)
128+
new_ctx.override_image("test_image", test_image)
129+
130+
# 测试 get_task_job
83131
task_job = new_ctx.get_task_job()
132+
print(f" task_job: {task_job}")
84133
new_task_detail = task_job.get()
85-
print(new_task_detail)
134+
print(
135+
f" task_detail entry: {new_task_detail.entry if new_task_detail else None}"
136+
)
137+
138+
# ============================================================
139+
# Tasker API 测试 (通过 context.tasker)
140+
# ============================================================
141+
tasker = new_ctx.tasker
142+
print(f" tasker.inited: {tasker.inited}")
143+
144+
# 测试 get_latest_node
145+
node_detail = tasker.get_latest_node("ColorMatch")
146+
print(f" latest_node ColorMatch: {node_detail}")
147+
148+
# 测试 running 和 stopping
149+
print(f" tasker.running: {tasker.running}")
150+
print(f" tasker.stopping: {tasker.stopping}")
151+
152+
# ============================================================
153+
# Resource API 测试 (通过 context.tasker.resource)
154+
# ============================================================
155+
resource = tasker.resource
156+
157+
# 测试 loaded (valid) 属性
158+
print(f" resource.loaded: {resource.loaded}")
159+
160+
# 测试 get_node_data (Resource 级别)
161+
res_node_data = resource.get_node_data(argv.node_name)
162+
print(
163+
f" res.get_node_data keys: {list(res_node_data.keys()) if res_node_data else None}"
164+
)
165+
166+
# 测试 hash 属性
167+
res_hash = resource.hash
168+
print(f" resource.hash: {res_hash[:16] if res_hash else None}...")
169+
170+
# 测试 node_list
171+
node_list = resource.node_list
172+
print(f" resource.node_list count: {len(node_list)}")
173+
174+
# 测试 custom_recognition_list 和 custom_action_list
175+
reco_list = resource.custom_recognition_list
176+
action_list = resource.custom_action_list
177+
print(f" custom_recognition_list: {reco_list}")
178+
print(f" custom_action_list: {action_list}")
179+
assert "MyRec" in reco_list, "MyRec should be in custom_recognition_list"
180+
assert "MyAct" in action_list, "MyAct should be in custom_action_list"
86181

87182
global analyzed
88183
analyzed = True
@@ -100,53 +195,121 @@ def run(
100195
argv: CustomAction.RunArg,
101196
) -> CustomAction.RunResult:
102197
print(
103-
f"on MyAction.run, context: {context}, task_detail: {argv.task_detail}, action_name: {argv.custom_action_name}, action_param: {argv.custom_action_param}, box: {argv.box}, reco_detail: {argv.reco_detail}"
198+
f"on MyAction.run, context: {context}, task_detail: {argv.task_detail}, "
199+
f"action_name: {argv.custom_action_name}, action_param: {argv.custom_action_param}, "
200+
f"box: {argv.box}, reco_detail: {argv.reco_detail}"
104201
)
202+
203+
# ============================================================
204+
# Controller API 测试 (通过 context.tasker.controller)
205+
# ============================================================
105206
controller = context.tasker.controller
207+
208+
# 测试 connected 和 uuid
209+
connected = controller.connected
210+
uuid = controller.uuid
211+
print(f" connected: {connected}, uuid: {uuid}")
212+
213+
# 测试 post_screencap
106214
new_image = controller.post_screencap().wait().get()
107-
print(f"new_image: {new_image.shape}")
215+
print(f" new_image: {new_image.shape}")
216+
217+
# 测试 cached_image
218+
cached_image = controller.cached_image
219+
print(f" cached_image shape: {cached_image.shape}")
220+
221+
# 测试基本输入操作
108222
controller.post_click(191, 98).wait()
109223
controller.post_swipe(100, 200, 300, 400, 100).wait()
110224
controller.post_input_text("Hello World!").wait()
111225
controller.post_click_key(32).wait()
226+
227+
# 测试触摸操作
112228
controller.post_touch_down(1, 100, 100, 0).wait()
113229
controller.post_touch_move(1, 200, 200, 0).wait()
114230
controller.post_touch_up(1).wait()
231+
232+
# 测试按键操作
233+
controller.post_key_down(65).wait()
234+
controller.post_key_up(65).wait()
235+
236+
# 测试滚动操作
237+
controller.post_scroll(0, 120).wait()
238+
239+
# 测试应用操作
115240
controller.post_start_app("aaa")
116241
controller.post_stop_app("bbb")
117242

118-
cached_image = controller.cached_image
119-
connected = controller.connected
120-
uuid = controller.uuid
243+
# ============================================================
244+
# Tasker API 补充测试 (详情获取)
245+
# ============================================================
246+
tasker = context.tasker
247+
248+
# 获取当前任务详情用于后续测试
249+
task_job = context.get_task_job()
250+
task_detail = task_job.get()
251+
252+
if task_detail:
253+
print(f" task_detail: entry={task_detail.entry}, status={task_detail.status}")
254+
255+
# 测试 get_task_detail
256+
fetched_task_detail = tasker.get_task_detail(task_detail.task_id)
257+
print(
258+
f" get_task_detail: {fetched_task_detail.entry if fetched_task_detail else None}"
259+
)
260+
261+
# 测试 get_node_detail
262+
if task_detail.nodes:
263+
node = task_detail.nodes[0]
264+
node_detail = tasker.get_node_detail(node.node_id)
265+
print(
266+
f" get_node_detail: {node_detail.name if node_detail else None}"
267+
)
268+
269+
# 测试 get_recognition_detail
270+
if node.recognition:
271+
reco_detail = tasker.get_recognition_detail(node.recognition.reco_id)
272+
print(
273+
f" get_recognition_detail: {reco_detail.name if reco_detail else None}"
274+
)
275+
276+
# 测试 clear_cache
277+
tasker.clear_cache()
278+
print(" tasker.clear_cache() called")
121279

122280
global runned
123281
runned = True
124282

125283
return CustomAction.RunResult(success=True)
126284

127285

286+
# ============================================================================
287+
# Event Sink 装饰器方式注册
288+
# ============================================================================
289+
290+
128291
@AgentServer.resource_sink()
129292
class MyResSink(ResourceEventSink):
130293
def on_raw_notification(self, resource, msg: str, details: dict):
131-
print(f"resource: {resource}, msg: {msg}, details: {details} ")
294+
print(f"[ResourceSink] msg: {msg}")
132295

133296

134297
@AgentServer.controller_sink()
135298
class MyCtrlSink(ControllerEventSink):
136299
def on_raw_notification(self, controller, msg: str, details: dict):
137-
print(f"controller: {controller}, msg: {msg}, details: {details} ")
300+
print(f"[ControllerSink] msg: {msg}")
138301

139302

140303
@AgentServer.tasker_sink()
141304
class MyTaskerSink(TaskerEventSink):
142305
def on_raw_notification(self, tasker, msg: str, details: dict):
143-
print(f"tasker: {tasker}, msg: {msg}, details: {details} ")
306+
print(f"[TaskerSink] msg: {msg}")
144307

145308

146309
@AgentServer.context_sink()
147310
class MyCtxSink(ContextEventSink):
148311
def on_raw_notification(self, context, msg: str, details: dict):
149-
print(f"context: {context}, msg: {msg}, details: {details} ")
312+
print(f"[ContextSink] msg: {msg}")
150313

151314

152315
if __name__ == "__main__":

0 commit comments

Comments
 (0)