use crate::prelude::*;
use crate::test::mono::capabilities::{Runtime, TestCapabilities};
use std::path::Path;
use std::process::{Command, Output};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub success: bool,
pub exit_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub runtime: Option<Runtime>,
}
impl ExecutionResult {
pub fn success(stdout: String, runtime: Runtime) -> Self {
Self {
success: true,
exit_code: Some(0),
stdout,
stderr: String::new(),
runtime: Some(runtime),
}
}
pub fn failure(exit_code: Option<i32>, stdout: String, stderr: String) -> Self {
Self {
success: false,
exit_code,
stdout,
stderr,
runtime: None,
}
}
pub fn is_success(&self) -> bool {
self.success
}
pub fn combined_output(&self) -> String {
if self.stderr.is_empty() {
self.stdout.clone()
} else if self.stdout.is_empty() {
self.stderr.clone()
} else {
format!("{}\n{}", self.stdout, self.stderr)
}
}
pub fn error_summary(&self) -> String {
if self.success {
"Success".to_string()
} else if !self.stderr.is_empty() {
self.stderr.clone()
} else if !self.stdout.is_empty() {
let error_lines: Vec<&str> = self
.stdout
.lines()
.filter(|line| line.contains("ERROR"))
.collect();
if !error_lines.is_empty() {
error_lines.join("\n")
} else {
self.stdout.clone()
}
} else {
format!("Failed with exit code {:?}", self.exit_code)
}
}
}
pub fn execute(capabilities: &TestCapabilities, assembly_path: &Path) -> Result<ExecutionResult> {
let runtime = match capabilities.runtime {
Some(r) => r,
None => {
return Ok(ExecutionResult::failure(
None,
String::new(),
"No .NET runtime available".to_string(),
))
}
};
execute_with_runtime(runtime, assembly_path)
}
pub fn execute_with_runtime(runtime: Runtime, assembly_path: &Path) -> Result<ExecutionResult> {
let output = match runtime {
Runtime::Mono => execute_with_mono(assembly_path)?,
Runtime::DotNet => execute_with_dotnet(assembly_path)?,
};
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code();
if output.status.success() {
let mut result = ExecutionResult::success(stdout, runtime);
result.stderr = stderr;
result.exit_code = exit_code;
Ok(result)
} else {
Ok(ExecutionResult::failure(exit_code, stdout, stderr))
}
}
fn execute_with_mono(assembly_path: &Path) -> Result<Output> {
Command::new("mono")
.arg(assembly_path)
.output()
.map_err(|e| Error::Other(format!("Failed to execute mono: {}", e)))
}
fn execute_with_dotnet(assembly_path: &Path) -> Result<Output> {
let mut cmd = Command::new("dotnet");
if let Some(parent) = assembly_path.parent() {
cmd.current_dir(parent);
if let Some(filename) = assembly_path.file_name() {
cmd.arg(filename);
} else {
cmd.arg(assembly_path);
}
} else {
cmd.arg(assembly_path);
}
cmd.output()
.map_err(|e| Error::Other(format!("Failed to execute dotnet: {}", e)))
}
pub fn execute_and_verify(
capabilities: &TestCapabilities,
assembly_path: &Path,
expected_output: &str,
) -> Result<ExecutionResult> {
let result = execute(capabilities, assembly_path)?;
if result.is_success() {
if result.stdout.contains(expected_output) {
Ok(result)
} else {
let output = result.stdout.trim().to_string();
Ok(ExecutionResult::failure(
result.exit_code,
result.stdout,
format!(
"Output mismatch: expected '{}', got '{}'",
expected_output, output
),
))
}
} else {
Ok(result)
}
}
pub fn execute_with_timeout(
capabilities: &TestCapabilities,
assembly_path: &Path,
timeout: Duration,
) -> Result<ExecutionResult> {
let runtime = match capabilities.runtime {
Some(r) => r,
None => {
return Ok(ExecutionResult::failure(
None,
String::new(),
"No .NET runtime available".to_string(),
))
}
};
let mut cmd = match runtime {
Runtime::Mono => {
let mut c = Command::new("mono");
c.arg(assembly_path);
c
}
Runtime::DotNet => {
let mut c = Command::new("dotnet");
if let Some(parent) = assembly_path.parent() {
c.current_dir(parent);
if let Some(filename) = assembly_path.file_name() {
c.arg(filename);
} else {
c.arg(assembly_path);
}
} else {
c.arg(assembly_path);
}
c
}
};
let mut child = cmd
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| Error::Other(format!("Failed to spawn process: {}", e)))?;
match child.wait_timeout(timeout) {
Ok(Some(status)) => {
let stdout = child
.stdout
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
let stderr = child
.stderr
.take()
.map(|mut s| {
let mut buf = String::new();
std::io::Read::read_to_string(&mut s, &mut buf).ok();
buf
})
.unwrap_or_default();
if status.success() {
let mut result = ExecutionResult::success(stdout, runtime);
result.stderr = stderr;
result.exit_code = status.code();
Ok(result)
} else {
Ok(ExecutionResult::failure(status.code(), stdout, stderr))
}
}
Ok(None) => {
child.kill().ok();
Ok(ExecutionResult::failure(
None,
String::new(),
format!("Execution timed out after {:?}", timeout),
))
}
Err(e) => Err(Error::Other(format!("Failed to wait for process: {}", e))),
}
}
trait ChildExt {
fn wait_timeout(
&mut self,
timeout: Duration,
) -> std::io::Result<Option<std::process::ExitStatus>>;
}
impl ChildExt for std::process::Child {
fn wait_timeout(
&mut self,
timeout: Duration,
) -> std::io::Result<Option<std::process::ExitStatus>> {
let start = std::time::Instant::now();
let poll_interval = Duration::from_millis(10);
loop {
match self.try_wait()? {
Some(status) => return Ok(Some(status)),
None => {
if start.elapsed() >= timeout {
return Ok(None);
}
std::thread::sleep(poll_interval);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::mono::compilation::{compile, templates};
use tempfile::TempDir;
#[test]
fn test_execution_result() {
let success = ExecutionResult::success("Hello".to_string(), Runtime::Mono);
assert!(success.is_success());
assert_eq!(success.stdout, "Hello");
let failure = ExecutionResult::failure(Some(1), String::new(), "Error".to_string());
assert!(!failure.is_success());
assert_eq!(failure.exit_code, Some(1));
}
#[test]
fn test_execute_hello_world() -> Result<()> {
let caps = TestCapabilities::detect();
if !caps.can_test() {
println!("Skipping: no compiler/runtime available");
return Ok(());
}
let temp_dir = TempDir::new()?;
let arch = caps.supported_architectures.first().unwrap();
let compile_result = compile(
&caps,
templates::HELLO_WORLD,
temp_dir.path(),
"hello",
arch,
)?;
assert!(compile_result.is_success(), "Compilation failed");
let exec_result = execute(&caps, compile_result.assembly_path())?;
assert!(
exec_result.is_success(),
"Execution failed: {}",
exec_result.error_summary()
);
assert!(exec_result.stdout.contains("Hello from dotscope test!"));
Ok(())
}
}