From 68bb7a5fa486c65dccf36fdda081d12989661f5c Mon Sep 17 00:00:00 2001 From: Loong Date: Tue, 12 Dec 2023 19:21:50 +0800 Subject: [PATCH] feat: better operator info and more tests --- maa-cli/src/config/asst.rs | 66 +++++++- maa-cli/src/config/mod.rs | 73 +++++++++ maa-cli/src/config/task/mod.rs | 5 +- maa-cli/src/dirs.rs | 47 ++++++ maa-cli/src/log.rs | 42 ++++- maa-cli/src/run/copilot.rs | 284 ++++++++++++++++++++++++++++----- 6 files changed, 467 insertions(+), 50 deletions(-) diff --git a/maa-cli/src/config/asst.rs b/maa-cli/src/config/asst.rs index fbc86226..b46699a7 100644 --- a/maa-cli/src/config/asst.rs +++ b/maa-cli/src/config/asst.rs @@ -460,6 +460,8 @@ impl InstanceOptions { mod tests { use super::*; + use crate::assert_matches; + mod serde { use super::*; @@ -754,8 +756,6 @@ mod tests { mod connection_config { use super::*; - use crate::assert_matches; - #[test] fn default() { assert_matches!( @@ -818,7 +818,7 @@ mod tests { mod resource_config { use super::*; - use crate::{assert_matches, dirs::Ensure}; + use crate::dirs::Ensure; use std::{env::temp_dir, fs}; @@ -889,6 +889,17 @@ mod tests { .. } if *path == PathBuf::from("iOS") ); + + assert_matches!( + ResourceConfig { + platform_diff_resource: Some(PathBuf::from("iOS")), + ..Default::default() + }.use_platform_diff_resource("Other"), + ResourceConfig { + platform_diff_resource: Some(path), + .. + } if *path == PathBuf::from("iOS") + ); } #[test] @@ -903,6 +914,28 @@ mod tests { ); } + #[test] + fn test_push_resource() { + let test_root = temp_dir().join("push_resource"); + + let resource_dir = test_root.join("resource"); + let unexists_resource_dir = test_root.join("unexists_resource"); + + resource_dir.ensure().unwrap(); + + assert_eq!( + push_resource(&mut Vec::new(), resource_dir.clone()), + &[resource_dir.clone()] + ); + + assert_eq!( + push_resource(&mut Vec::new(), unexists_resource_dir.clone()), + &Vec::::new() + ); + + fs::remove_dir_all(test_root).unwrap(); + } + #[test] fn resource_dir() { let test_root = temp_dir().join("resource_config"); @@ -971,4 +1004,31 @@ mod tests { fs::remove_dir_all(test_root).unwrap(); } } + + #[test] + fn instance_options() { + assert_matches!( + InstanceOptions { + touch_mode: None, + ..Default::default() + } + .force_playtools(), + InstanceOptions { + touch_mode: Some(TouchMode::MacPlayTools), + .. + } + ); + + assert_matches!( + InstanceOptions { + touch_mode: Some(TouchMode::ADB), + ..Default::default() + } + .force_playtools(), + InstanceOptions { + touch_mode: Some(TouchMode::MacPlayTools), + .. + } + ); + } } diff --git a/maa-cli/src/config/mod.rs b/maa-cli/src/config/mod.rs index f66d1574..0c1f0bba 100644 --- a/maa-cli/src/config/mod.rs +++ b/maa-cli/src/config/mod.rs @@ -116,3 +116,76 @@ pub mod asst; pub mod cli; pub mod task; + +#[cfg(test)] +mod tests { + use crate::assert_matches; + + use super::*; + use std::env::temp_dir; + + use serde::Deserialize; + + #[test] + fn find_file() { + #[derive(Deserialize, PartialEq, Debug)] + struct TestConfig { + a: i32, + b: String, + } + + impl Default for TestConfig { + fn default() -> Self { + Self { + a: 0, + b: String::new(), + } + } + } + + impl FromFile for TestConfig {} + + let test_root = temp_dir().join("find_file"); + std::fs::create_dir_all(&test_root).unwrap(); + + let test_file = test_root.join("test"); + let non_exist_file = test_root.join("not_exist"); + + std::fs::write( + &test_file.with_extension("json"), + r#"{ + "a": 1, + "b": "test" + }"#, + ) + .unwrap(); + + assert_eq!( + TestConfig::find_file(&test_file).unwrap(), + TestConfig { + a: 1, + b: "test".to_string() + } + ); + + assert_matches!( + TestConfig::find_file(&non_exist_file).unwrap_err(), + Error::FileNotFound(s) if s == non_exist_file.to_str().unwrap() + ); + + assert_eq!( + TestConfig::find_file_or_default(&test_file).unwrap(), + TestConfig { + a: 1, + b: "test".to_string() + } + ); + + assert_eq!( + TestConfig::find_file_or_default(&non_exist_file).unwrap(), + TestConfig::default() + ); + + std::fs::remove_dir_all(&test_root).unwrap(); + } +} diff --git a/maa-cli/src/config/task/mod.rs b/maa-cli/src/config/task/mod.rs index 7268c51a..a3dbce60 100644 --- a/maa-cli/src/config/task/mod.rs +++ b/maa-cli/src/config/task/mod.rs @@ -698,7 +698,8 @@ mod tests { MAATask::StartUp, object!( "start_game_enabled" => false) ), - Task::new_with_default(MAATask::Fight, object!("stage" => "1-7")) + Task::new_with_default(MAATask::Fight, object!("stage" => "1-7")), + Task::new_with_default(MAATask::CloseDown, object!("enable" => false)), ], } .init() @@ -717,7 +718,7 @@ mod tests { ) ), (MAATask::Fight.into(), object!("stage" => "1-7")), - (MAATask::CloseDown.into(), object!()), + (MAATask::CloseDown.into(), object!("enable" => true)), ] }, ); diff --git a/maa-cli/src/dirs.rs b/maa-cli/src/dirs.rs index 4a2f3bfc..e4d75f96 100644 --- a/maa-cli/src/dirs.rs +++ b/maa-cli/src/dirs.rs @@ -659,5 +659,52 @@ mod tests { assert!(!test_dir.exists()); assert_eq!(test_dir.ensure().unwrap(), test_dir); assert!(test_dir.exists()); + remove_dir_all(&test_root).unwrap(); + } + + #[test] + fn global_path_and_find() { + let test_root = temp_dir().join("maa-test-global-path"); + let test_dir1 = test_root.join("test1"); + let test_dir2 = test_root.join("test2"); + let test_file = test_dir1.join("test"); + + test_dir1.ensure_clean().unwrap(); + test_dir2.ensure_clean().unwrap(); + + std::fs::File::create(&test_file).unwrap(); + + assert_eq!( + global_path(&[&test_dir1, &test_dir2], "test"), + vec![test_file.clone()] + ); + assert_eq!( + global_path(&[&test_dir1, &test_dir2], "not_exist"), + Vec::::new() + ); + + assert_eq!( + global_find(&[&test_dir1, &test_dir2], |dir| { + if dir.join("test").exists() { + Some(dir.join("test")) + } else { + None + } + }), + vec![test_file.clone()] + ); + + assert_eq!( + global_find(&[&test_dir1, &test_dir2], |dir| { + if dir.join("not_exist").exists() { + Some(dir.join("not_exist")) + } else { + None + } + }), + Vec::::new() + ); + + remove_dir_all(&test_root).unwrap(); } } diff --git a/maa-cli/src/log.rs b/maa-cli/src/log.rs index d54fbd81..2d8fa228 100644 --- a/maa-cli/src/log.rs +++ b/maa-cli/src/log.rs @@ -25,12 +25,9 @@ impl> From for LogLevel { impl LogLevel { pub fn to_git_flag(self) -> &'static str { match self { - Self::Error => "-qq", - Self::Warning => "-q", - Self::Normal => "", - Self::Info => "-v", - Self::Debug => "-vv", - Self::Trace => "-vvv", + Self::Error | Self::Warning | Self::Normal => "-q", + Self::Info => "", + Self::Debug | Self::Trace => "-v", } } } @@ -204,3 +201,36 @@ macro_rules! trace { } }; } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::assert_matches; + + #[test] + fn log_level() { + assert_matches!(LogLevel::from(0), LogLevel::Error); + assert_matches!(LogLevel::from(1), LogLevel::Warning); + assert_matches!(LogLevel::from(2), LogLevel::Normal); + assert_matches!(LogLevel::from(3), LogLevel::Info); + assert_matches!(LogLevel::from(4), LogLevel::Debug); + assert_matches!(LogLevel::from(5), LogLevel::Trace); + assert_matches!(LogLevel::from(6), LogLevel::Trace); + + assert_eq!(LogLevel::Error.to_git_flag(), "-q"); + assert_eq!(LogLevel::Warning.to_git_flag(), "-q"); + assert_eq!(LogLevel::Normal.to_git_flag(), "-q"); + assert_eq!(LogLevel::Info.to_git_flag(), ""); + assert_eq!(LogLevel::Debug.to_git_flag(), "-v"); + assert_eq!(LogLevel::Trace.to_git_flag(), "-v"); + } + + #[test] + fn logger() { + let mut logger = Logger::new(LogLevel::Error); + assert_matches!(logger.level(), LogLevel::Error); + logger.set_level(LogLevel::Warning); + assert_matches!(logger.level(), LogLevel::Warning); + } +} diff --git a/maa-cli/src/run/copilot.rs b/maa-cli/src/run/copilot.rs index 9306debf..38ae1c8f 100644 --- a/maa-cli/src/run/copilot.rs +++ b/maa-cli/src/run/copilot.rs @@ -10,7 +10,7 @@ use crate::{ }; use std::{ - fs::{self, File}, + fs, io::Write, path::{Path, PathBuf}, }; @@ -22,7 +22,8 @@ use serde_json::Value as JsonValue; const MAA_COPILOT_API: &str = "https://prts.maa.plus/copilot/get/"; pub fn copilot(uri: impl AsRef, resource_dirs: &Vec) -> Result { - let (value, path) = CopilotJson::new(uri.as_ref())?.get_json_and_file()?; + let (value, path) = + CopilotJson::new(uri.as_ref())?.get_json_and_file(dirs::copilot().ensure()?)?; // Determine type of stage let task_type = match value["type"].as_str() { @@ -31,39 +32,25 @@ pub fn copilot(uri: impl AsRef, resource_dirs: &Vec) -> Result { Code(&'a str), File(&'a Path), @@ -84,14 +71,14 @@ impl CopilotJson<'_> { } } - pub fn get_json_and_file(&self) -> Result<(JsonValue, PathBuf)> { + pub fn get_json_and_file(&self, dir: impl AsRef) -> Result<(JsonValue, PathBuf)> { match self { CopilotJson::Code(code) => { - let json_file = dirs::copilot().ensure()?.join(code).with_extension("json"); + let json_file = dir.as_ref().join(code).with_extension("json"); if json_file.is_file() { debug!("Found cached json file:", json_file.display()); - return Ok((copilot_json_from_file(&json_file)?, json_file)); + return Ok((json_from_file(&json_file)?, json_file)); } let url = format!("{}{}", MAA_COPILOT_API, code); @@ -102,16 +89,16 @@ impl CopilotJson<'_> { .context("Failed to parse response")?; if resp["status_code"].as_i64().unwrap() == 200 { - let context = resp["data"]["content"].as_str().unwrap(); + let context = resp["data"]["content"] + .as_str() + .context("Failed to get copilot context")?; let value: JsonValue = serde_json::from_str(context).context("Failed to parse context")?; // Save json file - let mut file = File::create(&json_file).with_context(|| { - format!("Failed to create json file: {}", json_file.display()) - })?; - - file.write_all(context.as_bytes()) + fs::File::create(&json_file) + .context("Failed to create json file")? + .write_all(context.as_bytes()) .context("Failed to write json file")?; Ok((value, json_file)) @@ -121,20 +108,16 @@ impl CopilotJson<'_> { } CopilotJson::File(file) => { if file.is_absolute() { - Ok((copilot_json_from_file(file)?, file.to_path_buf())) + Ok((json_from_file(file)?, file.to_path_buf())) } else { let path = dirs::copilot().join(file); - Ok((copilot_json_from_file(&path)?, path)) + Ok((json_from_file(&path)?, path)) } } } } } -fn copilot_json_from_file(path: impl AsRef) -> Result { - Ok(serde_json::from_reader(File::open(path)?)?) -} - #[derive(Clone, Copy)] enum CopilotType { Copilot, @@ -165,8 +148,12 @@ impl CopilotType { }); if let Some(stage_file) = stage_files.last() { - let stage_info: JsonValue = serde_json::from_reader(File::open(stage_file)?)?; - Ok(format!("{} {}", stage_info["code"], stage_info["name"])) + let stage_info = json_from_file(stage_file)?; + Ok(format!( + "{} {}", + get_str_key(&stage_info, "code")?, + get_str_key(&stage_info, "name")? + )) } else { warning!("Failed to find stage file, maybe you resouces are outdated?"); Ok(stage_id.to_string()) @@ -204,3 +191,222 @@ impl AsRef for CopilotType { } } } + +fn json_from_file(path: impl AsRef) -> Result { + Ok(serde_json::from_reader(fs::File::open(path)?)?) +} + +fn operator_table(value: &JsonValue) -> Result { + let mut table = Table::new(); + table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["NAME", "SKILL"]); + + if let Some(opers) = value["opers"].as_array() { + for operator in opers { + table.add_row(row![get_str_key(operator, "name")?, operator["skill"]]); + } + } + + if let Some(groups) = value["groups"].as_array() { + for group in groups.iter() { + let opers = group["opers"].as_array().context("Failed to get opers")?; + let mut sub_table = Table::new(); + sub_table.set_format(*format::consts::FORMAT_NO_LINESEP); + for operator in opers { + sub_table.add_row(row![get_str_key(operator, "name")?, operator["skill"]]); + } + + let vertical_offset = (sub_table.len() + 2) >> 1; + + table.add_row(row![ + format!( + "{}[{}]", + "\n".repeat(vertical_offset - 1), + get_str_key(group, "name")? + ), + sub_table + ]); + } + } + + Ok(table) +} + +fn get_str_key<'a>(value: &'a JsonValue, key: impl AsRef) -> Result<&'a str> { + let key = key.as_ref(); + value[key] + .as_str() + .with_context(|| format!("Failed to get {}", key)) +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::env::temp_dir; + + mod copilot_json { + use super::*; + + #[test] + fn new() { + assert_eq!( + CopilotJson::new("maa://123").unwrap(), + CopilotJson::Code("123") + ); + assert_eq!( + CopilotJson::new("maa://123 ").unwrap(), + CopilotJson::Code("123") + ); + assert!(CopilotJson::new("maa:// 123").is_err()); + + assert_eq!( + CopilotJson::new("file.json").unwrap(), + CopilotJson::File(Path::new("file.json")) + ); + } + + #[test] + fn get_json_and_file() { + let test_root = temp_dir().join("maa-test-get-json-and-file"); + fs::create_dir_all(&test_root).unwrap(); + + let test_file = test_root.join("123.json"); + fs::File::create(&test_file) + .unwrap() + .write_all(b"{\"type\":\"SSS\"}") + .unwrap(); + + // Remote file but cache hit + assert_eq!( + CopilotJson::new("maa://123") + .unwrap() + .get_json_and_file(&test_root) + .unwrap(), + (serde_json::json!({"type": "SSS"}), test_file.clone()) + ); + + // Local file + assert_eq!( + CopilotJson::new(test_file.to_str().unwrap()) + .unwrap() + .get_json_and_file(&test_root) + .unwrap(), + (serde_json::json!({"type": "SSS"}), test_file.clone()) + ); + + fs::remove_dir_all(&test_root).unwrap(); + } + } + + mod copilot_type { + use super::*; + + #[test] + fn get_stage_name() { + let test_root = temp_dir().join("maa-test-get-stage-name"); + let arknights_tile_pos = test_root.join("Arknights-Tile-Pos"); + arknights_tile_pos.ensure().unwrap(); + + let stage_id = "act30side_01"; + + let test_file = arknights_tile_pos + .join("act30side_01-activities-act30side-level_act30side_01.json"); + + fs::File::create(&test_file) + .unwrap() + .write_all(r#"{ "code": "RS-1", "name": "注意事项" }"#.as_bytes()) + .unwrap(); + + assert_eq!( + CopilotType::Copilot + .get_stage_name(&vec![test_root.clone()], stage_id) + .unwrap(), + "RS-1 注意事项" + ); + + assert_eq!( + CopilotType::Copilot + .get_stage_name(&vec![test_root.clone()], "act30side_02") + .unwrap(), + "act30side_02" + ); + + fs::remove_dir_all(&test_root).unwrap(); + } + + #[test] + fn to_task() { + assert_eq!( + CopilotType::Copilot.to_task("filename"), + Task::new_with_default( + MAATask::Copilot, + object!( + "filename" => "filename", + "formation" => BoolInput::new(Some(true), Some("self-formation?")) + ) + ) + ); + + assert_eq!( + CopilotType::SSSCopilot.to_task("filename"), + Task::new_with_default( + MAATask::SSSCopilot, + object!( + "filename" => "filename", + "loop_times" => Input::::new(Some(1), Some("loop times:")) + ) + ) + ); + } + } + + #[test] + fn gen_operator_table() { + let json = serde_json::json!({ + "groups": [ + { + "name": "行医", + "opers": [ + { + "name": "纯烬艾雅法拉", + "skill": 1, + "skill_usage": 0 + }, + { + "name": "蜜莓", + "skill": 1, + "skill_usage": 0 + } + ] + } + ], + "opers": [ + { + "name": "桃金娘", + "skill": 1, + "skill_usage": 1 + }, + { + "name": "夜莺", + "skill": 3, + "skill_usage": 0 + } + ] + }); + + let mut expected_table = Table::new(); + expected_table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + expected_table.set_titles(row!["NAME", "SKILL"]); + expected_table.add_row(row!["桃金娘", 1]); + expected_table.add_row(row!["夜莺", 3]); + + let mut sub_table = Table::new(); + sub_table.set_format(*format::consts::FORMAT_NO_LINESEP); + sub_table.add_row(row!["纯烬艾雅法拉", 1]); + sub_table.add_row(row!["蜜莓", 1]); + expected_table.add_row(row!["\n[行医]", sub_table]); + + assert_eq!(operator_table(&json).unwrap(), expected_table); + } +}