mod common;
use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use maa_framework::context::Context;
use maa_framework::controller::Controller;
use maa_framework::custom::{CustomAction, CustomRecognition};
use maa_framework::pipeline::{Action, Recognition};
use maa_framework::resource::Resource;
use maa_framework::tasker::Tasker;
use maa_framework::{self, MaaResult, sys};
use common::{get_test_resources_dir, init_test_env};
static CONTEXT_TESTS_PASSED: AtomicBool = AtomicBool::new(false);
struct PipelineTestRecognition;
impl CustomRecognition for PipelineTestRecognition {
fn analyze(
&self,
context: &Context,
_task_id: sys::MaaTaskId,
_node_name: &str,
_custom_recognition_name: &str,
_custom_recognition_param: &str,
_image: &maa_framework::buffer::MaaImageBuffer,
_roi: &maa_framework::common::Rect,
) -> Option<(maa_framework::common::Rect, String)> {
println!("\n=== PipelineTestRecognition.analyze ===");
let result = run_context_pipeline_tests(context);
if result.is_ok() {
CONTEXT_TESTS_PASSED.store(true, Ordering::SeqCst);
} else {
println!(" Context tests FAILED: {:?}", result);
}
Some((
maa_framework::common::Rect {
x: 0,
y: 0,
width: 100,
height: 100,
},
r#"{"test": "passed"}"#.to_string(),
))
}
}
struct PipelineTestAction;
impl CustomAction for PipelineTestAction {
fn run(
&self,
_context: &Context,
_task_id: sys::MaaTaskId,
_node_name: &str,
_custom_action_name: &str,
_custom_action_param: &str,
_reco_id: sys::MaaRecoId,
_box_rect: &maa_framework::common::Rect,
) -> bool {
true
}
}
fn run_context_pipeline_tests(context: &Context) -> MaaResult<()> {
test_context_get_node_data(context).map_err(|e| {
println!("FAILED: test_context_get_node_data: {:?}", e);
e
})?;
test_context_get_node_object(context).map_err(|e| {
println!("FAILED: test_context_get_node_object: {:?}", e);
e
})?;
test_context_override_pipeline(context).map_err(|e| {
println!("FAILED: test_context_override_pipeline: {:?}", e);
e
})?;
test_context_override_next(context).map_err(|e| {
println!("FAILED: test_context_override_next: {:?}", e);
e
})?;
test_and_or_override_inheritance(context).map_err(|e| {
println!("FAILED: test_and_or_override_inheritance: {:?}", e);
e
})?;
test_and_or_node_reference(context).map_err(|e| {
println!("FAILED: test_and_or_node_reference: {:?}", e);
e
})?;
test_recognition_types(context).map_err(|e| {
println!("FAILED: test_recognition_types: {:?}", e);
e
})?;
test_action_types(context).map_err(|e| {
println!("FAILED: test_action_types: {:?}", e);
e
})?;
test_node_attributes(context).map_err(|e| {
println!("FAILED: test_node_attributes: {:?}", e);
e
})?;
test_anchor_object_format(context).map_err(|e| {
println!("FAILED: test_anchor_object_format: {:?}", e);
e
})?;
test_v2_format(context).map_err(|e| {
println!("FAILED: test_v2_format: {:?}", e);
e
})?;
test_wait_freezes(context).map_err(|e| {
println!("FAILED: test_wait_freezes: {:?}", e);
e
})?;
test_repeat_params(context).map_err(|e| {
println!("FAILED: test_repeat_params: {:?}", e);
e
})?;
println!(" All Context-level pipeline tests PASSED");
Ok(())
}
fn test_context_get_node_data(context: &Context) -> MaaResult<()> {
println!(" Testing context.get_node_data...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(r#"{"TestDataNode": {"action": "DoNothing"}}"#)?;
let node_data = new_ctx.get_node_data("TestDataNode")?;
assert!(node_data.is_some(), "get_node_data should return Some");
let data = node_data.unwrap();
assert!(data.contains("action"), "node_data MUST contain 'action'");
println!(" PASS: context.get_node_data");
Ok(())
}
fn test_context_get_node_object(context: &Context) -> MaaResult<()> {
println!(" Testing context.get_node_object...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(r#"{"TestObjectNode": {}}"#)?;
let node_obj = new_ctx.get_node_object("TestObjectNode")?;
assert!(node_obj.is_some(), "get_node_object should return Some");
let obj = node_obj.unwrap();
if let Recognition::DirectHit(_) = obj.recognition {
} else {
panic!("Expected DirectHit, got {:?}", obj.recognition);
}
if let Action::DoNothing(_) = obj.action {
} else {
panic!("Expected DoNothing, got {:?}", obj.action);
}
println!(" PASS: context.get_node_object");
Ok(())
}
fn test_context_override_pipeline(context: &Context) -> MaaResult<()> {
println!(" Testing context.override_pipeline...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(r#"{"NodeA": {}, "NodeB": {}}"#)?;
let override_json = r#"{
"OverrideTestNode": {
"recognition": "OCR",
"expected": ["TestText"],
"action": "Click",
"target": [100, 200, 50, 50],
"next": ["NodeA", "NodeB"],
"timeout": 5000,
"rate_limit": 500,
"pre_delay": 100,
"post_delay": 300,
"max_hit": 3,
"enabled": true,
"inverse": false,
"anchor": ["my_anchor"],
"focus": {"key": "value"},
"attach": {"custom_data": 123}
}
}"#;
new_ctx.override_pipeline(override_json)?;
let node_obj = new_ctx
.get_node_object("OverrideTestNode")?
.expect("OverrideTestNode MUST exist");
match node_obj.recognition {
Recognition::OCR(ocr) => {
assert_eq!(ocr.expected, vec!["TestText"], "expected");
}
_ => panic!("Expected OCR recognition"),
}
match node_obj.action {
Action::Click(_) => {
}
_ => panic!("Expected Click action"),
}
assert_eq!(node_obj.timeout, 5000, "timeout");
assert_eq!(node_obj.rate_limit, 500, "rate_limit");
assert_eq!(node_obj.pre_delay, 100, "pre_delay");
assert_eq!(node_obj.post_delay, 300, "post_delay");
assert_eq!(node_obj.max_hit, 3, "max_hit");
assert_eq!(node_obj.enabled, true, "enabled");
assert_eq!(node_obj.inverse, false, "inverse");
match node_obj.anchor {
maa_framework::pipeline::Anchor::Map(m) => {
assert!(
m.contains_key("my_anchor"),
"anchor map should contain 'my_anchor', got {:?}",
m
);
assert_eq!(
m.get("my_anchor").map(|s| s.as_str()),
Some("OverrideTestNode"),
"anchor value"
);
}
_ => panic!(
"Expected Anchor::Map after C API round-trip, got {:?}",
node_obj.anchor
),
}
assert!(node_obj.attach.is_some(), "attach should exist");
let attach = node_obj.attach.as_ref().unwrap();
assert_eq!(
attach.get("custom_data").and_then(|v| v.as_i64()),
Some(123)
);
assert_eq!(node_obj.next.len(), 2, "next length");
assert_eq!(node_obj.next[0].name, "NodeA", "next[0].name");
assert_eq!(node_obj.next[1].name, "NodeB", "next[1].name");
println!(" PASS: context.override_pipeline");
Ok(())
}
fn test_context_override_next(context: &Context) -> MaaResult<()> {
println!(" Testing context.override_next...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"OverrideNextTestNode": {},
"NextNode1": {},
"NextNode2": {},
"MyAnchor": {"anchor": ["MyAnchor"]}
}"#,
)?;
let result = new_ctx.override_next(
"OverrideNextTestNode",
&["NextNode1", "[JumpBack]NextNode2", "[Anchor]MyAnchor"],
)?;
assert!(result, "override_next should succeed");
let node_obj = new_ctx
.get_node_object("OverrideNextTestNode")?
.expect("Node should exist");
assert_eq!(node_obj.next.len(), 3, "next length after override");
assert_eq!(node_obj.next[0].name, "NextNode1");
assert_eq!(node_obj.next[0].jump_back, false);
assert_eq!(node_obj.next[1].name, "NextNode2");
assert_eq!(node_obj.next[1].jump_back, true);
assert_eq!(node_obj.next[2].name, "MyAnchor");
assert_eq!(node_obj.next[2].anchor, true);
println!(" PASS: context.override_next");
Ok(())
}
fn test_and_or_override_inheritance(context: &Context) -> MaaResult<()> {
println!(" Testing And/Or override inheritance...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"AndTestNode": {
"recognition": {
"type": "And",
"param": {
"all_of": [
{"recognition": {"type": "DirectHit"}},
{"recognition": {"type": "DirectHit"}}
],
"box_index": 1
}
}
}
}"#,
)?;
new_ctx.override_pipeline(
r#"{
"AndTestNode": {
"recognition": {"param": {"box_index": 0}}
}
}"#,
)?;
let and_node = new_ctx
.get_node_object("AndTestNode")?
.expect("AndTestNode MUST exist");
match and_node.recognition {
Recognition::And(and) => {
assert_eq!(and.all_of.len(), 2, "all_of should have 2 elements");
assert_eq!(and.box_index, 0, "box_index should be updated to 0");
}
_ => panic!("Expected And recognition"),
}
new_ctx.override_pipeline(
r#"{
"OrTestNode": {
"recognition": {
"type": "Or",
"param": {
"any_of": [
{"recognition": {"type": "DirectHit"}}
]
}
}
}
}"#,
)?;
new_ctx.override_pipeline(r#"{"OrTestNode": {}}"#)?;
let or_node = new_ctx
.get_node_object("OrTestNode")?
.expect("OrTestNode MUST exist");
match or_node.recognition {
Recognition::Or(or) => {
assert_eq!(or.any_of.len(), 1, "any_of should have 1 element");
}
_ => panic!("Expected Or recognition"), }
println!(" PASS: And/Or override inheritance");
Ok(())
}
fn test_and_or_node_reference(context: &Context) -> MaaResult<()> {
println!(" Testing And/Or node name reference...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"BaseTemplateNode": {
"recognition": "TemplateMatch",
"template": ["test.png"],
"threshold": 0.8
},
"BaseOCRNode": {
"recognition": "OCR",
"expected": ["hello"]
}
}"#,
)?;
new_ctx.override_pipeline(
r#"{
"AndWithNodeRef": {
"recognition": {
"type": "And",
"param": {
"all_of": [
"BaseTemplateNode",
"BaseOCRNode",
{"recognition": {"type": "DirectHit"}}
],
"box_index": 0
}
}
}
}"#,
)?;
let and_node = new_ctx
.get_node_object("AndWithNodeRef")?
.expect("AndWithNodeRef MUST exist");
match and_node.recognition {
Recognition::And(and) => {
assert_eq!(and.all_of.len(), 3, "all_of length");
match &and.all_of[0] {
maa_framework::pipeline::RecognitionRef::NodeName(name) => {
assert_eq!(name, "BaseTemplateNode");
}
_ => panic!("Expected NodeName"),
}
match &and.all_of[1] {
maa_framework::pipeline::RecognitionRef::NodeName(name) => {
assert_eq!(name, "BaseOCRNode");
}
_ => panic!("Expected NodeName"),
}
match &and.all_of[2] {
maa_framework::pipeline::RecognitionRef::Inline(_) => {}
_ => panic!("Expected Inline"),
}
}
_ => panic!("Expected And recognition"),
}
new_ctx.override_pipeline(
r#"{
"OrWithNodeRef": {
"recognition": {
"type": "Or",
"param": {
"any_of": [
"BaseTemplateNode",
{"recognition": {"type": "DirectHit"}}
]
}
}
}
}"#,
)?;
let or_node = new_ctx
.get_node_object("OrWithNodeRef")?
.expect("OrWithNodeRef MUST exist");
match or_node.recognition {
Recognition::Or(or) => {
assert_eq!(or.any_of.len(), 2, "any_of length");
match &or.any_of[0] {
maa_framework::pipeline::RecognitionRef::NodeName(name) => {
assert_eq!(name, "BaseTemplateNode");
}
_ => panic!("Expected NodeName"),
}
match &or.any_of[1] {
maa_framework::pipeline::RecognitionRef::Inline(_) => {}
_ => panic!("Expected Inline"),
}
}
_ => panic!("Expected Or recognition"),
}
println!(" PASS: And/Or node name reference");
Ok(())
}
fn test_recognition_types(context: &Context) -> MaaResult<()> {
println!(" Testing recognition types parsing...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"RecoTemplateMatch": {
"recognition": "TemplateMatch",
"template": ["test.png"],
"threshold": 0.8,
"roi": [10, 20, 100, 200],
"order_by": "Score",
"index": 1,
"method": 3,
"green_mask": true
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoTemplateMatch")?.expect("node");
match obj.recognition {
Recognition::TemplateMatch(tm) => {
assert_eq!(tm.template, vec!["test.png".to_string()]);
assert_eq!(tm.threshold, vec![0.8]);
assert_eq!(tm.order_by, "Score");
assert_eq!(tm.index, 1);
assert_eq!(tm.method, 3);
assert_eq!(tm.green_mask, true);
}
_ => panic!("Expected TemplateMatch"),
}
new_ctx.override_pipeline(
r#"{
"RecoFeatureMatch": {
"recognition": "FeatureMatch",
"template": ["feature.png"],
"detector": "ORB",
"count": 10,
"ratio": 0.75
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoFeatureMatch")?.expect("node");
match obj.recognition {
Recognition::FeatureMatch(fm) => {
assert_eq!(fm.detector, "ORB");
assert_eq!(fm.count, 10);
assert_eq!(fm.ratio, 0.75);
}
_ => panic!("Expected FeatureMatch"),
}
new_ctx.override_pipeline(
r#"{
"RecoColorMatch": {
"recognition": "ColorMatch",
"lower": [[100, 100, 100]],
"upper": [[255, 255, 255]],
"count": 50,
"method": 40,
"connected": true
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoColorMatch")?.expect("node");
match obj.recognition {
Recognition::ColorMatch(cm) => {
assert_eq!(cm.lower, vec![vec![100, 100, 100]]);
assert_eq!(cm.upper, vec![vec![255, 255, 255]]);
assert_eq!(cm.count, 50);
assert_eq!(cm.method, 40);
assert_eq!(cm.connected, true);
}
_ => panic!("Expected ColorMatch"),
}
new_ctx.override_pipeline(
r#"{
"RecoOCR": {
"recognition": "OCR",
"expected": ["Hello", "World"],
"threshold": 0.5,
"replace": [["0", "O"], ["1", "I"]],
"only_rec": true,
"model": "custom_model",
"color_filter": "RecoColorMatch"
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoOCR")?.expect("node");
match obj.recognition {
Recognition::OCR(ocr) => {
assert_eq!(ocr.expected.len(), 2);
assert_eq!(ocr.only_rec, true);
assert_eq!(ocr.model, "custom_model");
assert_eq!(ocr.color_filter, "RecoColorMatch");
}
_ => panic!("Expected OCR"),
}
new_ctx.override_pipeline(
r#"{
"RecoNNClassify": {
"recognition": "NeuralNetworkClassify",
"model": "classify.onnx",
"expected": [0, 2],
"labels": ["Cat", "Dog", "Mouse"]
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoNNClassify")?.expect("node");
match obj.recognition {
Recognition::NeuralNetworkClassify(nn) => {
assert_eq!(nn.model, "classify.onnx");
assert_eq!(
nn.labels,
vec!["Cat".to_string(), "Dog".to_string(), "Mouse".to_string()]
);
}
_ => panic!("Expected NeuralNetworkClassify"),
}
new_ctx.override_pipeline(
r#"{
"RecoNNDetect": {
"recognition": "NeuralNetworkDetect",
"model": "detect.onnx",
"expected": [1],
"threshold": [0.5]
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoNNDetect")?.expect("node");
match obj.recognition {
Recognition::NeuralNetworkDetect(nn) => {
assert_eq!(nn.model, "detect.onnx");
assert_eq!(nn.threshold, vec![0.5]);
}
_ => panic!("Expected NeuralNetworkDetect"),
}
new_ctx.override_pipeline(
r#"{
"RecoCustom": {
"recognition": "Custom",
"custom_recognition": "MyCustomReco",
"custom_recognition_param": {"key": "value"},
"roi": [0, 0, 100, 100]
}
}"#,
)?;
let obj = new_ctx.get_node_object("RecoCustom")?.expect("node");
match obj.recognition {
Recognition::Custom(c) => {
assert_eq!(c.custom_recognition, "MyCustomReco");
assert_eq!(
c.custom_recognition_param
.get("key")
.and_then(|v| v.as_str()),
Some("value")
);
}
_ => panic!("Expected Custom"),
}
println!(" PASS: recognition types parsing");
Ok(())
}
fn test_action_types(context: &Context) -> MaaResult<()> {
println!(" Testing action types parsing...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"ActClick": {
"action": "Click",
"target": [100, 200, 50, 50],
"target_offset": [10, 10, 0, 0],
"contact": 1
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActClick")?.expect("node");
match obj.action {
Action::Click(c) => {
assert_eq!(c.contact, 1);
}
_ => panic!("Expected Click"),
}
new_ctx.override_pipeline(
r#"{
"ActLongPress": {
"action": "LongPress",
"duration": 2000
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActLongPress")?.expect("node");
match obj.action {
Action::LongPress(lp) => {
assert_eq!(lp.duration, 2000);
}
_ => panic!("Expected LongPress"),
}
new_ctx.override_pipeline(
r#"{
"ActSwipe": {
"action": "Swipe",
"begin": [100, 100],
"end": [300, 300],
"duration": 500
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActSwipe")?.expect("node");
match obj.action {
Action::Swipe(_) => {} _ => panic!("Expected Swipe"),
}
new_ctx.override_pipeline(
r#"{
"ActMultiSwipe": {
"action": "MultiSwipe",
"swipes": [
{"begin": [100, 100], "end": [200, 200]},
{"starting": 500, "begin": [300, 300], "end": [400, 400]}
]
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActMultiSwipe")?.expect("node");
match obj.action {
Action::MultiSwipe(ms) => {
assert_eq!(ms.swipes.len(), 2);
}
_ => panic!("Expected MultiSwipe"),
}
new_ctx.override_pipeline(
r#"{
"ActClickKey": {
"action": "ClickKey",
"key": 10
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActClickKey")?.expect("node");
match obj.action {
Action::ClickKey(k) => {
assert_eq!(k.key, vec![10]);
}
_ => panic!("Expected ClickKey"),
}
new_ctx.override_pipeline(
r#"{
"ActKeyDown": {
"action": "KeyDown",
"key": 11
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActKeyDown")?.expect("node");
match obj.action {
Action::KeyDown(k) => {
assert_eq!(k.key, 11);
}
_ => panic!("Expected KeyDown"),
}
new_ctx.override_pipeline(
r#"{
"ActKeyUp": {
"action": "KeyUp",
"key": 13
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActKeyUp")?.expect("node");
match obj.action {
Action::KeyUp(k) => {
assert_eq!(k.key, 13);
}
_ => panic!("Expected KeyUp"),
}
new_ctx.override_pipeline(
r#"{
"ActInputText": {
"action": "InputText",
"input_text": "Hello World"
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActInputText")?.expect("node");
match obj.action {
Action::InputText(it) => {
assert_eq!(it.input_text, "Hello World");
}
_ => panic!("Expected InputText"),
}
new_ctx.override_pipeline(
r#"{
"ActStartApp": {
"action": "StartApp",
"package": "com.example.app"
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActStartApp")?.expect("node");
match obj.action {
Action::StartApp(app) => {
assert_eq!(app.package, "com.example.app");
}
_ => panic!("Expected StartApp"),
}
new_ctx.override_pipeline(
r#"{
"ActStopApp": {
"action": "StopApp",
"package": "com.example.app"
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActStopApp")?.expect("node");
match obj.action {
Action::StopApp(app) => {
assert_eq!(app.package, "com.example.app");
}
_ => panic!("Expected StopApp"),
}
new_ctx.override_pipeline(
r#"{
"ActCommand": {
"action": "Command",
"exec": "python",
"args": ["script.py", "--arg1"],
"detach": true
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActCommand")?.expect("node");
match obj.action {
Action::Command(cmd) => {
assert_eq!(cmd.exec, "python");
assert_eq!(
cmd.args,
vec!["script.py".to_string(), "--arg1".to_string()]
);
assert_eq!(cmd.detach, true);
}
_ => panic!("Expected Command"),
}
new_ctx.override_pipeline(
r#"{
"ActShell": {
"action": "Shell",
"cmd": "ls -la",
"shell_timeout": 30000
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActShell")?.expect("node");
match obj.action {
Action::Shell(sh) => {
assert_eq!(sh.cmd, "ls -la");
assert_eq!(sh.shell_timeout, 30000);
}
_ => panic!("Expected Shell"),
}
new_ctx.override_pipeline(
r#"{
"ActScreencap": {
"action": "Screencap",
"filename": "test_capture",
"format": "jpg",
"quality": 85
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActScreencap")?.expect("node");
match obj.action {
Action::Screencap(sc) => {
assert_eq!(sc.filename, "test_capture");
assert_eq!(sc.format, "jpg");
assert_eq!(sc.quality, 85);
}
_ => panic!("Expected Screencap"),
}
new_ctx.override_pipeline(
r#"{
"ActScreencapDefault": {
"action": "Screencap"
}
}"#,
)?;
let obj = new_ctx
.get_node_object("ActScreencapDefault")?
.expect("node");
match obj.action {
Action::Screencap(sc) => {
assert_eq!(sc.filename, "");
assert_eq!(sc.format, "png");
assert_eq!(sc.quality, 100);
}
_ => panic!("Expected Screencap"),
}
new_ctx.override_pipeline(
r#"{
"ActScroll": {
"action": "Scroll",
"target": [100, 200, 50, 50],
"target_offset": [10, 10, 0, 0],
"dx": 0,
"dy": -360
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActScroll")?.expect("node");
match obj.action {
Action::Scroll(s) => {
if let maa_framework::pipeline::Target::Rect(r) = s.target {
assert_eq!(r, (100, 200, 50, 50));
} else {
panic!("Expected Rect target, got {:?}", s.target);
}
assert_eq!(s.target_offset, (10, 10, 0, 0));
assert_eq!(s.dx, 0);
assert_eq!(s.dy, -360);
}
_ => panic!("Expected Scroll"),
}
new_ctx.override_pipeline(
r#"{
"ActCustom": {
"action": "Custom",
"custom_action": "MyCustomAction",
"custom_action_param": {"data": 123}
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActCustom")?.expect("node");
match obj.action {
Action::Custom(c) => {
assert_eq!(c.custom_action, "MyCustomAction");
assert_eq!(
c.custom_action_param.get("data").and_then(|v| v.as_i64()),
Some(123)
);
}
_ => panic!("Expected Custom"),
}
new_ctx.override_pipeline(
r#"{
"ActDoNothing": {
"action": "DoNothing"
}
}"#,
)?;
let obj = new_ctx.get_node_object("ActDoNothing")?.expect("node");
if matches!(obj.action, Action::DoNothing(_)) {
} else {
panic!("Expected DoNothing");
}
println!(" PASS: action types parsing");
Ok(())
}
fn test_node_attributes(context: &Context) -> MaaResult<()> {
println!(" Testing node attributes...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"PlainNode": {},
"JumpBackNode": {},
"AnchorRef": {"anchor": ["AnchorRef"]},
"ObjectNode": {},
"AnchorObjNode": {"anchor": ["AnchorObjNode"]},
"ErrorHandler": {}
}"#,
)?;
new_ctx.override_pipeline(
r#"{
"NodeAttrTest": {
"next": [
"PlainNode",
"[JumpBack]JumpBackNode",
"[Anchor]AnchorRef",
{"name": "ObjectNode", "jump_back": true},
{"name": "AnchorObjNode", "anchor": true}
],
"on_error": ["[JumpBack]ErrorHandler"]
}
}"#,
)?;
let obj = new_ctx.get_node_object("NodeAttrTest")?.expect("node");
assert_eq!(obj.next.len(), 5, "next length");
assert_eq!(obj.next[0].name, "PlainNode");
assert_eq!(obj.next[0].jump_back, false);
assert_eq!(obj.next[0].anchor, false);
assert_eq!(obj.next[1].name, "JumpBackNode");
assert_eq!(obj.next[1].jump_back, true);
assert_eq!(obj.next[2].name, "AnchorRef");
assert_eq!(obj.next[2].anchor, true);
assert_eq!(obj.next[3].name, "ObjectNode");
assert_eq!(obj.next[3].jump_back, true);
assert_eq!(obj.next[4].name, "AnchorObjNode");
assert_eq!(obj.next[4].anchor, true);
assert_eq!(obj.on_error.len(), 1, "on_error length");
assert_eq!(obj.on_error[0].name, "ErrorHandler");
assert_eq!(obj.on_error[0].jump_back, true);
println!(" PASS: node attributes");
Ok(())
}
fn test_anchor_object_format(context: &Context) -> MaaResult<()> {
println!(" Testing anchor object format...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"AnchorString": {
"anchor": "StringAnchor"
},
"AnchorArray": {
"anchor": ["ArrayAnchor1", "ArrayAnchor2"]
},
"AnchorObject": {
"anchor": {
"ObjAnchor1": "TargetNode1",
"ObjAnchor2": "",
"ObjAnchor3": "TargetNode2"
}
},
"TargetNode1": {},
"TargetNode2": {}
}"#,
)?;
let obj1 = new_ctx
.get_node_object("AnchorString")?
.expect("AnchorString should exist");
match obj1.anchor {
maa_framework::pipeline::Anchor::Map(m) => {
assert_eq!(
m.get("StringAnchor").map(|s| s.as_str()),
Some("AnchorString"),
"string anchor should map to node name"
);
}
_ => panic!(
"Expected Anchor::Map for AnchorString, got {:?}",
obj1.anchor
),
}
let obj2 = new_ctx
.get_node_object("AnchorArray")?
.expect("AnchorArray should exist");
match obj2.anchor {
maa_framework::pipeline::Anchor::Map(m) => {
assert_eq!(m.len(), 2, "array anchor should have 2 entries");
assert_eq!(
m.get("ArrayAnchor1").map(|s| s.as_str()),
Some("AnchorArray")
);
assert_eq!(
m.get("ArrayAnchor2").map(|s| s.as_str()),
Some("AnchorArray")
);
}
_ => panic!(
"Expected Anchor::Map for AnchorArray, got {:?}",
obj2.anchor
),
}
let obj3 = new_ctx
.get_node_object("AnchorObject")?
.expect("AnchorObject should exist");
match obj3.anchor {
maa_framework::pipeline::Anchor::Map(m) => {
assert_eq!(m.get("ObjAnchor1").map(|s| s.as_str()), Some("TargetNode1"));
assert_eq!(m.get("ObjAnchor2").map(|s| s.as_str()), Some(""));
assert_eq!(m.get("ObjAnchor3").map(|s| s.as_str()), Some("TargetNode2"));
}
_ => panic!(
"Expected Anchor::Map for AnchorObject, got {:?}",
obj3.anchor
),
}
println!(" PASS: anchor object format");
Ok(())
}
fn test_v2_format(context: &Context) -> MaaResult<()> {
println!(" Testing v2 format parsing...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"V2FormatNode": {
"recognition": {
"type": "TemplateMatch",
"param": {
"template": ["v2.png"],
"threshold": 0.9,
"roi": [0, 0, 100, 100]
}
},
"action": {
"type": "Click",
"param": {"target": true, "contact": 2}
},
"pre_delay": 50,
"post_delay": 150
}
}"#,
)?;
let obj = new_ctx.get_node_object("V2FormatNode")?.expect("node");
match obj.recognition {
Recognition::TemplateMatch(tm) => {
assert_eq!(tm.template, vec!["v2.png".to_string()]);
assert_eq!(tm.threshold, vec![0.9]);
}
_ => panic!("Expected TemplateMatch"),
}
match obj.action {
Action::Click(c) => {
assert_eq!(c.contact, 2);
}
_ => panic!("Expected Click"),
}
assert_eq!(obj.pre_delay, 50);
assert_eq!(obj.post_delay, 150);
println!(" PASS: v2 format parsing");
Ok(())
}
fn test_wait_freezes(context: &Context) -> MaaResult<()> {
println!(" Testing wait_freezes...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"WaitFreezesTest": {
"pre_wait_freezes": {
"time": 500,
"threshold": 0.99,
"method": 3
},
"post_wait_freezes": {
"time": 1000,
"target": [10, 20, 30, 40]
}
}
}"#,
)?;
let obj = new_ctx.get_node_object("WaitFreezesTest")?.expect("node");
let pre = obj.pre_wait_freezes.as_ref().expect("pre_wait_freezes");
assert_eq!(pre.time, 500);
assert_eq!(pre.threshold, 0.99);
assert_eq!(pre.method, 3);
let post = obj.post_wait_freezes.as_ref().expect("post_wait_freezes");
assert_eq!(post.time, 1000);
println!(" PASS: wait_freezes");
Ok(())
}
fn test_repeat_params(context: &Context) -> MaaResult<()> {
println!(" Testing repeat params...");
let new_ctx = context.clone_context()?;
new_ctx.override_pipeline(
r#"{
"RepeatTest": {
"repeat": 5,
"repeat_delay": 300,
"repeat_wait_freezes": {"time": 200, "threshold": 0.9}
}
}"#,
)?;
let obj = new_ctx.get_node_object("RepeatTest")?.expect("node");
assert_eq!(obj.repeat, 5);
assert_eq!(obj.repeat_delay, 300);
let repeat_freezes = obj
.repeat_wait_freezes
.as_ref()
.expect("repeat_wait_freezes");
assert_eq!(repeat_freezes.time, 200);
assert_eq!(repeat_freezes.threshold, 0.9);
println!(" PASS: repeat params");
Ok(())
}
fn create_test_pipeline_resource(resource_dir: &Path) {
let pipeline_dir = resource_dir.join("pipeline");
fs::create_dir_all(&pipeline_dir).expect("create pipeline dir");
let test_pipeline = r#"{
"TestBasic": {},
"TestEntry": {
"next": ["TestReco"]
},
"TestReco": {
"recognition": "Custom",
"custom_recognition": "PipelineTestReco",
"action": "Custom",
"custom_action": "PipelineTestAct"
}
}"#;
fs::write(pipeline_dir.join("test.json"), test_pipeline).expect("write test pipeline");
}
fn create_temp_test_resource_dir() -> std::path::PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock before unix epoch")
.as_nanos();
let dir = std::env::temp_dir().join(format!("maa-framework-rs-pipeline-{suffix}"));
fs::create_dir_all(&dir).expect("create temp resource dir");
dir
}
#[test]
fn test_pipeline_node_execution() {
println!("\n=== test_pipeline_node_execution ===");
init_test_env().unwrap();
CONTEXT_TESTS_PASSED.store(false, Ordering::SeqCst);
let res_dir = get_test_resources_dir();
let base_resource_dir = res_dir.join("resource");
let screenshot_dir = res_dir.join("Screenshot");
let temp_root = create_temp_test_resource_dir();
let test_resource_dir = temp_root.join("resource");
create_test_pipeline_resource(&test_resource_dir);
assert!(
base_resource_dir.exists(),
"Test resources MUST exist at {:?}. Run: git submodule update --init",
base_resource_dir
);
assert!(
screenshot_dir.exists(),
"Screenshot directory MUST exist at {:?}",
screenshot_dir
);
let resource = Resource::new().unwrap();
resource
.post_bundle(base_resource_dir.to_str().unwrap())
.unwrap()
.wait();
resource
.post_bundle(test_resource_dir.to_str().unwrap())
.unwrap()
.wait();
assert!(resource.loaded(), "Resource MUST be loaded");
resource
.register_custom_recognition("PipelineTestReco", Box::new(PipelineTestRecognition))
.unwrap();
resource
.register_custom_action("PipelineTestAct", Box::new(PipelineTestAction))
.unwrap();
let controller = Controller::new_dbg(screenshot_dir.to_str().unwrap()).unwrap();
let conn_id = controller.post_connection().unwrap();
controller.wait(conn_id);
assert!(controller.connected(), "Controller MUST be connected");
let tasker = Tasker::new().unwrap();
tasker.bind_resource(&resource).unwrap();
tasker.bind_controller(&controller).unwrap();
assert!(tasker.inited(), "Tasker MUST be initialized");
let job = tasker
.post_task("TestEntry", "{}")
.expect("post_task MUST work");
let status = job.wait();
let detail = job.get(false).expect("get task detail MUST work");
assert!(status.done(), "Task MUST complete");
assert!(detail.is_some(), "Task detail MUST exist");
assert!(
CONTEXT_TESTS_PASSED.load(Ordering::SeqCst),
"Context-level pipeline tests MUST pass"
);
println!("PASS: pipeline node execution");
}
#[test]
fn test_pipeline_smoking() {
println!("\n=== test_pipeline_smoking ===");
init_test_env().unwrap();
let res_dir = get_test_resources_dir();
let resource_dir = res_dir.join("resource");
let recording_path = res_dir.join("MaaRecording.jsonl");
assert!(
resource_dir.exists(),
"Test resources MUST exist at {:?}. Run: git submodule update --init",
resource_dir
);
assert!(
recording_path.exists(),
"Replay recording MUST exist at {:?}",
recording_path
);
let controller = Controller::new_replay(recording_path.to_str().unwrap()).unwrap();
let conn_id = controller.post_connection().unwrap();
let resource = Resource::new().unwrap();
let res_job = resource
.post_bundle(resource_dir.to_str().unwrap())
.unwrap();
let conn_status = controller.wait(conn_id);
let res_status = res_job.wait();
assert!(
conn_status.succeeded(),
"ReplayController connection failed"
);
assert!(res_status.succeeded(), "Resource loading failed");
let info = controller.info().expect("controller info MUST work");
assert_eq!(
info.get("type").and_then(|v| v.as_str()),
Some("replay"),
"replay controller type should be 'replay'"
);
let tasker = Tasker::new().unwrap();
tasker.bind_resource(&resource).unwrap();
tasker.bind_controller(&controller).unwrap();
assert!(tasker.inited(), "Tasker must be initialized");
let job = tasker.post_task("Wilderness", "{}").unwrap();
let status = job.wait();
let detail = job.get(false).expect("get task detail MUST work");
assert!(status.succeeded(), "pipeline_smoking task should succeed");
assert!(detail.is_some(), "pipeline_smoking task detail MUST exist");
println!("PASS: pipeline smoking");
}
#[test]
fn test_resource_get_node_data() {
println!("\n=== test_resource_get_node_data ===");
init_test_env().unwrap();
let res_dir = get_test_resources_dir();
let resource_dir = res_dir.join("resource");
assert!(
resource_dir.exists(),
"Test resources not found at {:?}",
resource_dir
);
let resource = Resource::new().unwrap();
resource
.post_bundle(resource_dir.to_str().unwrap())
.unwrap()
.wait();
assert!(resource.loaded(), "Resource MUST be loaded");
let node_list = resource.node_list().expect("node_list MUST work");
println!(" node_list count: {}", node_list.len());
assert!(!node_list.is_empty(), "node_list MUST NOT be empty");
let first_node = &node_list[0];
let node_data = resource
.get_node_data(first_node)
.expect("get_node_data MUST NOT error");
assert!(
node_data.is_some(),
"get_node_data MUST return Some for existing node"
);
let data = node_data.unwrap();
let debug_path = std::env::temp_dir().join("maa_node_json.txt");
std::fs::write(&debug_path, &data).expect("write file");
println!(" JSON written to: {:?}", debug_path);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&data);
assert!(parsed.is_ok(), "node_data MUST be valid JSON");
let non_exist = resource
.get_node_data("NonExistentNode12345")
.expect("get_node_data should not error");
assert!(non_exist.is_none(), "Non-existent node should return None");
println!("PASS: resource_get_node_data");
}
#[test]
fn test_resource_get_node_object() {
println!("\n=== test_resource_get_node_object ===");
init_test_env().unwrap();
let res_dir = get_test_resources_dir();
let resource_dir = res_dir.join("resource");
assert!(
resource_dir.exists(),
"Test resources MUST exist at {:?}",
resource_dir
);
let resource = Resource::new().unwrap();
resource
.post_bundle(resource_dir.to_str().unwrap())
.unwrap()
.wait();
assert!(resource.loaded(), "Resource MUST load successfully");
let node_list = resource.node_list().expect("node_list MUST work");
assert!(!node_list.is_empty(), "Node list MUST NOT be empty");
let first_node = &node_list[0];
let obj = resource
.get_node_object(first_node)
.expect("get_node_object MUST NOT error")
.expect("get_node_object MUST return Some");
println!(" Testing node: {}", first_node);
println!(" recognition: {:?}", obj.recognition);
println!(" action: {:?}", obj.action);
println!(" next count: {}", obj.next.len());
println!("PASS: resource_get_node_object (STRICT)");
}
#[test]
fn test_resource_override_pipeline() {
println!("\n=== test_resource_override_pipeline ===");
init_test_env().unwrap();
let res_dir = get_test_resources_dir();
let resource_dir = res_dir.join("resource");
assert!(resource_dir.exists(), "Test resources MUST exist");
let resource = Resource::new().unwrap();
resource
.post_bundle(resource_dir.to_str().unwrap())
.unwrap()
.wait();
assert!(resource.loaded(), "Resource MUST load successfully");
let override_json = r#"{
"TestOverrideNode": {
"recognition": "DirectHit",
"action": "Click",
"target": [100, 200, 50, 50]
}
}"#;
resource
.override_pipeline(override_json)
.expect("override_pipeline MUST succeed");
println!(" override_pipeline: OK");
let node_data = resource
.get_node_data("TestOverrideNode")
.expect("get_node_data MUST NOT error")
.expect("TestOverrideNode MUST exist after override");
assert!(
node_data.contains("DirectHit"),
"Override node MUST have DirectHit recognition"
);
assert!(
node_data.contains("Click"),
"Override node MUST have Click action"
);
println!("PASS: resource_override_pipeline (STRICT)");
}
#[test]
fn test_resource_hash() {
println!("\n=== test_resource_hash ===");
init_test_env().unwrap();
let res_dir = get_test_resources_dir();
let resource_dir = res_dir.join("resource");
assert!(resource_dir.exists(), "Test resources MUST exist");
let resource = Resource::new().unwrap();
resource
.post_bundle(resource_dir.to_str().unwrap())
.unwrap()
.wait();
assert!(resource.loaded(), "Resource MUST load successfully");
let hash = resource.hash().expect("hash MUST NOT error");
assert!(!hash.is_empty(), "Hash MUST NOT be empty");
assert!(
hash.len() >= 8,
"Hash MUST be at least 8 chars, got {}",
hash.len()
);
println!(" resource.hash: {}", hash);
println!("PASS: resource_hash (STRICT)");
}