292 lines
9.6 KiB
Rust
292 lines
9.6 KiB
Rust
//! Integration tests for log size truncation
|
|
//!
|
|
//! Tests that verify stdout/stderr are properly truncated when they exceed
|
|
//! configured size limits, preventing OOM issues with large output.
|
|
|
|
use attune_worker::runtime::{ExecutionContext, PythonRuntime, Runtime, ShellRuntime};
|
|
use std::collections::HashMap;
|
|
|
|
#[tokio::test]
|
|
async fn test_python_stdout_truncation() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Create a Python script that outputs more than the limit
|
|
let code = r#"
|
|
import sys
|
|
# Output 1KB of data (will exceed 500 byte limit)
|
|
for i in range(100):
|
|
print("x" * 10)
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 1,
|
|
action_ref: "test.large_output".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 500, // Small limit to trigger truncation
|
|
max_stderr_bytes: 1024,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed but with truncated output
|
|
assert!(result.is_success());
|
|
assert!(result.stdout_truncated);
|
|
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
|
|
assert!(result.stdout_bytes_truncated > 0);
|
|
assert!(result.stdout.len() <= 500);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_python_stderr_truncation() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Create a Python script that outputs to stderr
|
|
let code = r#"
|
|
import sys
|
|
# Output 1KB of data to stderr
|
|
for i in range(100):
|
|
sys.stderr.write("error message line\n")
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 2,
|
|
action_ref: "test.large_stderr".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 10 * 1024 * 1024,
|
|
max_stderr_bytes: 300, // Small limit for stderr
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed but with truncated stderr
|
|
assert!(result.is_success());
|
|
assert!(!result.stdout_truncated);
|
|
assert!(result.stderr_truncated);
|
|
assert!(result.stderr.contains("[OUTPUT TRUNCATED"));
|
|
assert!(result.stderr.contains("stderr exceeded size limit"));
|
|
assert!(result.stderr_bytes_truncated > 0);
|
|
assert!(result.stderr.len() <= 300);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_shell_stdout_truncation() {
|
|
let runtime = ShellRuntime::new();
|
|
|
|
// Shell script that outputs more than the limit
|
|
let code = r#"
|
|
for i in {1..100}; do
|
|
echo "This is a long line of text that will add up quickly"
|
|
done
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 3,
|
|
action_ref: "test.shell_large_output".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "shell".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("shell".to_string()),
|
|
max_stdout_bytes: 400, // Small limit
|
|
max_stderr_bytes: 1024,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed but with truncated output
|
|
assert!(result.is_success());
|
|
assert!(result.stdout_truncated);
|
|
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
|
|
assert!(result.stdout_bytes_truncated > 0);
|
|
assert!(result.stdout.len() <= 400);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_no_truncation_under_limit() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Small output that won't trigger truncation
|
|
let code = r#"
|
|
print("Hello, World!")
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 4,
|
|
action_ref: "test.small_output".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 10 * 1024 * 1024, // Large limit
|
|
max_stderr_bytes: 10 * 1024 * 1024,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed without truncation
|
|
assert!(result.is_success());
|
|
assert!(!result.stdout_truncated);
|
|
assert!(!result.stderr_truncated);
|
|
assert_eq!(result.stdout_bytes_truncated, 0);
|
|
assert_eq!(result.stderr_bytes_truncated, 0);
|
|
assert!(result.stdout.contains("Hello, World!"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_both_streams_truncated() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Script that outputs to both stdout and stderr
|
|
let code = r#"
|
|
import sys
|
|
# Output to both streams
|
|
for i in range(50):
|
|
print("stdout line " + str(i))
|
|
sys.stderr.write("stderr line " + str(i) + "\n")
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 5,
|
|
action_ref: "test.dual_truncation".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 300, // Both limits are small
|
|
max_stderr_bytes: 300,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed but with both streams truncated
|
|
assert!(result.is_success());
|
|
assert!(result.stdout_truncated);
|
|
assert!(result.stderr_truncated);
|
|
assert!(result.stdout.contains("[OUTPUT TRUNCATED"));
|
|
assert!(result.stderr.contains("[OUTPUT TRUNCATED"));
|
|
assert!(result.stdout_bytes_truncated > 0);
|
|
assert!(result.stderr_bytes_truncated > 0);
|
|
assert!(result.stdout.len() <= 300);
|
|
assert!(result.stderr.len() <= 300);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_truncation_with_timeout() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Script that times out but should still capture truncated logs
|
|
let code = r#"
|
|
import time
|
|
for i in range(1000):
|
|
print(f"Line {i}")
|
|
time.sleep(30) # Will timeout before this
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 6,
|
|
action_ref: "test.timeout_truncation".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(2), // Short timeout
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 500,
|
|
max_stderr_bytes: 1024,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should timeout with truncated logs
|
|
assert!(!result.is_success());
|
|
assert!(result.error.is_some());
|
|
assert!(result.error.as_ref().unwrap().contains("timed out"));
|
|
// Logs may or may not be truncated depending on how fast it runs
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_exact_limit_no_truncation() {
|
|
let runtime = PythonRuntime::new();
|
|
|
|
// Output a small amount that won't trigger truncation
|
|
// The Python wrapper adds JSON result output, so we need headroom
|
|
let code = r#"
|
|
import sys
|
|
sys.stdout.write("Small output")
|
|
"#;
|
|
|
|
let context = ExecutionContext {
|
|
execution_id: 7,
|
|
action_ref: "test.exact_limit".to_string(),
|
|
parameters: HashMap::new(),
|
|
env: HashMap::new(),
|
|
secrets: HashMap::new(),
|
|
timeout: Some(10),
|
|
working_dir: None,
|
|
entry_point: "test_script".to_string(),
|
|
code: Some(code.to_string()),
|
|
code_path: None,
|
|
runtime_name: Some("python".to_string()),
|
|
max_stdout_bytes: 10 * 1024 * 1024, // Large limit to avoid truncation
|
|
max_stderr_bytes: 10 * 1024 * 1024,
|
|
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
|
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
|
};
|
|
|
|
let result = runtime.execute(context).await.unwrap();
|
|
|
|
// Should succeed without truncation
|
|
eprintln!(
|
|
"test_exact_limit_no_truncation: exit_code={}, error={:?}, stdout={:?}, stderr={:?}",
|
|
result.exit_code, result.error, result.stdout, result.stderr
|
|
);
|
|
assert!(result.is_success());
|
|
assert!(!result.stdout_truncated);
|
|
assert!(result.stdout.contains("Small output"));
|
|
}
|