use std::{
fmt::Write,
path::{Path, PathBuf},
process::Stdio,
time::Duration,
};
use nix::libc::{EXIT_SUCCESS, SIGALRM};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator as _};
use similar::{ChangeTag, TextDiff};
use tempfile::NamedTempFile;
use tokio::{fs, io::AsyncWriteExt as _};
use super::{
sandbox::{RlimitConfig, Sandbox},
util, Command, Error, JudgeResult, JudgeVerdict, RawCommand, Result, TestCase, MAIN_FILE_NAME,
};
#[derive(Debug)]
pub struct Runner {
pub extension: &'static str,
pub compile_command: Option<Command>,
pub run_command: RawCommand,
pub time_out: u32,
pub rlimit_config: &'static [RlimitConfig],
pub scmp_black_list: &'static [&'static str],
#[cfg(test)]
pub infinite_loop_code: &'static str,
#[cfg(test)]
pub compilation_error_code: &'static str,
#[cfg(test)]
pub runtime_error_code: &'static str,
}
impl Runner {
async fn create_unique_project(&self, code: &str) -> Result<PathBuf> {
let project_path = util::generate_unique_path(code);
fs::create_dir_all(&project_path).await?;
let mut main_file_path = project_path.clone();
main_file_path.push(MAIN_FILE_NAME);
main_file_path.set_extension(self.extension);
let mut main_file = fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&main_file_path)
.await?;
main_file.write_all(code.as_bytes()).await?;
Ok(project_path)
}
async fn compile(&self, project_path: &Path) -> Result<()> {
let Some(Command {
binary: compiler,
args,
}) = self.compile_command
else {
return Ok(());
};
let process = tokio::process::Command::new(compiler)
.args(args)
.current_dir(project_path)
.stderr(Stdio::piped())
.spawn()?;
let compilation_error = process.wait_with_output().await?.stderr;
if !compilation_error.is_empty() {
return Err(Error::Compilation {
message: String::from_utf8(compilation_error)?,
});
}
Ok(())
}
fn run_one(&self, project_path: &Path, test_case: &TestCase) -> Result<JudgeResult> {
let user_output_path = NamedTempFile::new()?;
let user_error_path = NamedTempFile::new()?;
let mut sandbox = Sandbox::new(
self.scmp_black_list,
self.rlimit_config,
self.time_out,
project_path.to_path_buf(),
self.run_command,
&test_case.input_path,
user_output_path.path(),
user_error_path.path(),
)?;
sandbox.spawn()?;
let raw_run_result = sandbox.wait()?;
if raw_run_result.exit_signal == SIGALRM {
return Ok(JudgeResult {
verdict: JudgeVerdict::TimeLimitExceeded,
run_time: Duration::from_secs(self.time_out as u64),
});
}
let run_time = raw_run_result.real_time_cost;
let runtime_error = std::fs::read_to_string(&user_error_path)?;
if !runtime_error.is_empty() || raw_run_result.exit_signal != EXIT_SUCCESS {
return Ok(JudgeResult {
verdict: JudgeVerdict::RuntimeError,
run_time,
});
}
let output = std::fs::read_to_string(&user_output_path)?;
let correct_output = std::fs::read_to_string(&test_case.output_path)?;
let diff = TextDiff::from_lines(output.as_str().trim(), correct_output.as_str().trim())
.iter_all_changes()
.filter(|change| change.tag() != ChangeTag::Equal)
.fold(String::with_capacity(1000), |mut output, change| {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
_ => unreachable!()
};
let _ = writeln!(output, "{}{}", sign, change);
output
});
if !diff.is_empty() {
return Ok(JudgeResult {
verdict: JudgeVerdict::WrongAnswer { diff },
run_time,
});
}
Ok(JudgeResult {
verdict: JudgeVerdict::Accepted,
run_time,
})
}
pub async fn run(&self, code: &str, test_cases: Vec<TestCase>) -> Result<Vec<JudgeResult>> {
let project_path = self.create_unique_project(code).await?;
self.compile(&project_path).await?;
let judge_results: Vec<JudgeResult> = test_cases
.par_iter()
.map(|test_case| self.run_one(&project_path, test_case))
.collect::<Result<_>>()?;
Ok(judge_results)
}
}