#[cfg(test)]
#[allow(clippy::module_inception)]
mod python_tests {
use anyhow::Result;
use log::info;
use serde_json::json;
use crate::model::{CodeExecutor, LanguageScript};
use crate::tests::test_utils::setup;
#[tokio::test]
async fn test_python_basic_execution() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python.py")?;
info!("读取测试脚本: test_python.py");
info!("开始执行Python脚本...");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, None).await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
} else {
info!("脚本执行成功");
}
let logs_str = result.logs.join(" ");
assert!(
logs_str.contains("Handler function called"),
"日志应包含 'Handler function called'"
);
assert!(
logs_str.contains("Final calculation completed"),
"日志应包含 'Final calculation completed'"
);
assert!(
logs_str.contains("The product of [1, 2, 3, 4, 5] is 120"),
"日志应包含计算结果"
);
Ok(())
}
#[tokio::test]
async fn test_python_with_params() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python_params.py")?;
info!("读取测试脚本: test_python_params.py");
let params = json!({
"a": 10,
"b": 20
});
info!("准备测试参数: {params:?}");
info!("开始执行Python脚本...");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
} else {
info!("脚本执行成功");
}
if let Some(result_val) = &result.result {
info!("执行结果: {result_val:?}");
} else {
info!("无执行结果");
}
if let Some(result_val) = result.result {
assert!(result_val.get("sum").is_some(), "结果应包含 sum 字段");
assert!(
result_val.get("numbers").is_some(),
"结果应包含 numbers 字段"
);
assert!(
result_val.get("message").is_some(),
"结果应包含 message 字段"
);
}
Ok(())
}
#[tokio::test]
async fn test_python_different_return_types() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python_types.py")?;
info!("读取测试脚本: test_python_types.py");
let params = json!({"type": "string"});
info!("测试字符串类型, 参数: {params:?}");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
if let Some(result_val) = &result.result {
assert!(result_val.is_string(), "结果应为字符串类型");
}
let params = json!({"type": "number"});
info!("测试数字类型, 参数: {params:?}");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
if let Some(result_val) = &result.result {
assert!(result_val.is_number(), "结果应为数字类型");
assert_eq!(result_val.as_i64().unwrap(), 12345, "结果应为 12345");
}
let params = json!({"type": "boolean"});
info!("测试布尔类型, 参数: {params:?}");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
if let Some(result_val) = &result.result {
assert!(result_val.is_boolean(), "结果应为布尔类型");
assert!(result_val.as_bool().unwrap(), "结果应为 true");
}
let params = json!({"type": "list"});
info!("测试列表类型, 参数: {params:?}");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
if let Some(result_val) = &result.result {
assert!(result_val.is_array(), "结果应为数组类型");
assert_eq!(result_val.as_array().unwrap().len(), 6, "数组长度应为 6");
}
let params = json!({"type": "dict"});
info!("测试字典类型, 参数: {params:?}");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
if let Some(result_val) = &result.result {
assert!(result_val.is_object(), "结果应为对象类型");
assert!(result_val.get("name").is_some(), "结果应包含 name 字段");
assert!(result_val.get("age").is_some(), "结果应包含 age 字段");
assert!(result_val.get("tags").is_some(), "结果应包含 tags 字段");
}
let params = json!({"type": "null"});
info!("测试 None 类型, 参数: {params:?}");
let _result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
Ok(())
}
#[tokio::test]
async fn test_python_with_pandas() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/rfunction_test2.py")?;
info!("读取测试脚本: rfunction_test2.py");
let dependencies = crate::python_runner::parse_import(&code)?;
info!("识别到的依赖: {dependencies:?}");
assert!(!dependencies.is_empty(), "依赖列表不应为空");
assert!(
dependencies.contains(&"pandas".to_string()),
"依赖列表应包含pandas"
);
let params = json!({
"params": {
"input": "测试数据"
}
});
info!("准备测试参数: {params:?}");
info!("开始执行Python脚本...");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
return Ok(());
} else {
info!("脚本执行成功");
}
if let Some(result_val) = &result.result {
info!("执行结果: {result_val:?}");
} else {
info!("无执行结果");
}
if result.logs.is_empty() {
info!("警告: 日志为空,但脚本执行成功");
} else {
info!("捕获到的日志数量: {}", result.logs.len());
for (i, log) in result.logs.iter().enumerate() {
info!("日志[{i}]: {log}");
}
let logs_str = result.logs.join(" ");
info!("合并后的日志字符串: {logs_str}");
assert!(
logs_str.contains("Created data structure"),
"日志应包含'Created data structure'"
);
assert!(logs_str.contains("input args"), "日志应包含'input args'");
}
if let Some(result_val) = result.result {
assert!(result_val.get("key0").is_some(), "结果应包含 key0 字段");
assert_eq!(
result_val["key0"].as_str().unwrap(),
"测试数据",
"key0 应等于输入参数"
);
assert!(result_val.get("key1").is_some(), "结果应包含 key1 字段");
assert!(result_val["key1"].is_array(), "key1 应为数组");
assert!(result_val.get("key2").is_some(), "结果应包含 key2 字段");
assert!(result_val["key2"].is_object(), "key2 应为对象");
assert!(
result_val["key2"].get("key21").is_some(),
"key2.key21 应存在"
);
}
Ok(())
}
#[tokio::test]
async fn test_python_params_access() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python_simple.py")?;
info!("读取测试脚本: test_python_simple.py");
let params = json!({
"input": "直接提供的参数"
});
info!("准备测试参数: {params:?}");
info!("开始执行Python脚本...");
let result = CodeExecutor::execute_with_params_compat(
&code,
LanguageScript::Python,
Some(params.clone()),
)
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
panic!("执行出错: {error}");
} else {
info!("脚本执行成功");
}
if let Some(result_val) = &result.result {
info!("执行结果: {result_val:?}");
if let Some(result_str) = result_val.as_str() {
let json_val = serde_json::from_str::<serde_json::Value>(result_str)?;
assert_eq!(
json_val["direct_access"], "直接提供的参数",
"直接访问应该能获取到参数"
);
assert_eq!(
json_val["nested_access"], "直接提供的参数",
"嵌套访问也应该能获取到参数"
);
assert!(
json_val["args_structure"].get("input").is_some(),
"args结构应包含直接参数"
);
assert!(
json_val["args_structure"].get("params").is_some(),
"args结构应包含params参数"
);
}
}
let nested_params = json!({
"params": {
"input": "嵌套提供的参数"
}
});
info!("准备嵌套测试参数: {nested_params:?}");
info!("开始执行Python脚本...");
let result = CodeExecutor::execute_with_params_compat(
&code,
LanguageScript::Python,
Some(nested_params),
)
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
panic!("执行出错: {error}");
}
if let Some(result_val) = &result.result {
info!("执行结果: {result_val:?}");
if let Some(result_str) = result_val.as_str() {
let json_val = serde_json::from_str::<serde_json::Value>(result_str)?;
assert_eq!(
json_val["nested_access"], "嵌套提供的参数",
"应该能通过params.input访问到参数"
);
assert!(
json_val["args_structure"].get("params").is_some(),
"args结构应包含params参数"
);
}
}
Ok(())
}
#[tokio::test]
async fn test_python_logging() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python_logging.py")?;
info!("读取测试脚本: test_python_logging.py");
let params = json!({
"test": "日志测试"
});
info!("准备测试参数: {params:?}");
info!("开始执行Python脚本...");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
panic!("执行出错: {error}");
} else {
info!("脚本执行成功");
}
assert!(!result.logs.is_empty(), "日志不应为空");
info!("捕获到的日志数量: {}", result.logs.len());
for (i, log) in result.logs.iter().enumerate() {
info!("日志[{i}]: {log}");
}
let logs_str = result.logs.join(" ");
assert!(logs_str.contains("INFO级别"), "应捕获INFO级别的日志");
assert!(logs_str.contains("WARNING级别"), "应捕获WARNING级别的日志");
assert!(logs_str.contains("ERROR级别"), "应捕获ERROR级别的日志");
assert!(
logs_str.contains("CRITICAL级别"),
"应捕获CRITICAL级别的日志"
);
assert!(
logs_str.contains("格式化的JSON数据"),
"应捕获格式化的JSON数据"
);
if let Some(result_val) = result.result {
assert_eq!(
result_val["message"], "日志测试完成",
"返回的message字段不正确"
);
assert_eq!(result_val["log_count"], 6, "返回的log_count字段不正确");
} else {
panic!("应有返回结果");
}
Ok(())
}
#[tokio::test]
async fn test_python_with_timeout() -> Result<()> {
setup();
let code = r#"
import time
import logging
def main(args: dict) -> dict:
logging.info("开始执行耗时操作")
# 这个操作会运行10秒钟
for i in range(10):
logging.info(f"已经执行了 {i+1} 秒")
time.sleep(1)
logging.info("操作完成")
return {"result": "完成"}
"#;
info!("创建了一个会运行10秒的测试脚本");
let params = json!({
"test": "超时测试"
});
info!("准备测试参数: {params:?}");
info!("开始执行Python脚本,设置3秒超时...");
let start_time = std::time::Instant::now();
let result =
CodeExecutor::execute_with_params(code, LanguageScript::Python, Some(params), Some(3))
.await;
let elapsed = start_time.elapsed();
info!("脚本执行耗时: {elapsed:?}");
assert!(elapsed.as_secs() >= 3, "脚本应该至少运行3秒");
assert!(elapsed.as_secs() < 5, "脚本应该在5秒内超时");
match result {
Ok(exec_result) => {
if let Some(error) = exec_result.error {
info!("正确捕获到超时错误: {error}");
assert!(
error.contains("timed out")
|| error.contains("TimedOut")
|| error.contains("executor await error"),
"错误信息应该包含超时相关信息"
);
} else {
panic!("应该捕获到超时错误,但脚本执行成功了");
}
}
Err(e) => {
let full_error = format!("{e:#}");
info!("捕获到错误: {full_error}");
assert!(
full_error.contains("timed out"),
"完整错误链中应该包含'timed out'超时信息"
);
}
}
Ok(())
}
#[tokio::test]
async fn test_python_large_parameters() -> Result<()> {
setup();
let code = std::fs::read_to_string("fixtures/test_python_large_params.py")?;
info!("读取测试脚本: test_python_large_params.py");
fn generate_large_text(size_in_mb: usize) -> String {
let chunk = "This is a test chunk of text designed to generate large parameters for Python testing. ".repeat(100);
let iterations = (size_in_mb * 1024 * 1024) / chunk.len();
let mut result = String::new();
for i in 0..iterations {
result.push_str(&chunk);
result.push_str(&format!("Iteration {}\n", i));
}
result
}
let large_text = generate_large_text(3); info!(
"生成了3MB大小的文本参数,实际大小: {:.2} MB",
large_text.len() as f64 / 1024.0 / 1024.0
);
let params = json!({
"largeText": large_text
});
info!(
"准备测试参数,JSON大小: {:.2} MB",
serde_json::to_string(¶ms).unwrap().len() as f64 / 1024.0 / 1024.0
);
info!("开始执行Python脚本,测试大参数处理...");
let result =
CodeExecutor::execute_with_params_compat(&code, LanguageScript::Python, Some(params))
.await?;
info!("脚本执行完成, 日志: {:?}", result.logs);
if let Some(error) = &result.error {
info!("执行错误: {error}");
} else {
info!("脚本执行成功");
}
if let Some(result_val) = &result.result {
info!("执行结果: {result_val:?}");
} else {
info!("无执行结果");
}
assert!(!result.logs.is_empty(), "日志不应为空");
assert!(result.error.is_none(), "不应有错误,这表明大参数处理成功");
assert!(result.result.is_some(), "应有返回结果");
if let Some(result_val) = result.result {
let json_str = serde_json::to_string(&result_val)?;
assert!(json_str.contains("success"), "结果应包含 success 字段");
assert!(json_str.contains("true"), "success 应为 true");
assert!(
json_str.contains("Successfully processed large text parameter"),
"结果应包含成功消息"
);
assert!(json_str.contains("sizeMB"), "结果应包含 sizeMB 字段");
if let Some(obj) = result_val.as_object() {
if let Some(success) = obj.get("success") {
assert_eq!(success, true, "success 字段的值应为 true");
}
if let Some(text_size) = obj.get("textSize") {
assert!(text_size.is_number(), "textSize 应为数字");
let size = text_size.as_u64().unwrap();
assert!(size > 2 * 1024 * 1024, "文本大小应大于2MB");
}
}
}
info!("Python大参数测试通过!临时文件解决方案有效。");
Ok(())
}
}