use std::io::Read;
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct Output {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
pub timed_out: bool,
}
impl Output {
pub fn success(&self) -> bool {
self.status.success() && !self.timed_out
}
pub fn stdout_text(&self) -> String {
String::from_utf8_lossy(&self.stdout).into_owned()
}
pub fn stderr_text(&self) -> String {
String::from_utf8_lossy(&self.stderr).into_owned()
}
pub fn describe_status(&self) -> String {
if self.timed_out {
return "timed out".to_string();
}
if let Some(code) = self.status.code() {
return format!("exit code {code}");
}
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if let Some(signal) = self.status.signal() {
return format!("killed by signal {signal}");
}
}
format!("{}", self.status)
}
}
pub fn run(command: &mut Command, timeout: Duration) -> std::io::Result<Output> {
command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = command.spawn()?;
let stdout = child.stdout.take().map(drain);
let stderr = child.stderr.take().map(drain);
let start = Instant::now();
let mut delay = Duration::from_millis(1);
let mut timed_out = false;
let status = loop {
if let Some(status) = child.try_wait()? {
break status;
}
if start.elapsed() > timeout {
let _ = child.kill();
timed_out = true;
break child.wait()?;
}
thread::sleep(delay);
delay = (delay * 2).min(Duration::from_millis(20));
};
let join = |handle: Option<thread::JoinHandle<Vec<u8>>>| {
handle
.map(|h| h.join().unwrap_or_default())
.unwrap_or_default()
};
Ok(Output {
status,
stdout: join(stdout),
stderr: join(stderr),
timed_out,
})
}
fn drain(mut pipe: impl Read + Send + 'static) -> thread::JoinHandle<Vec<u8>> {
thread::spawn(move || {
let mut buffer = Vec::new();
let _ = pipe.read_to_end(&mut buffer);
buffer
})
}
pub fn shell(sh: &Path, script: &str, dir: &Path) -> Command {
let mut command = Command::new(sh);
command
.arg("-c")
.arg(script)
.current_dir(dir)
.env("LC_ALL", "C");
command
}
pub fn split_words(text: &str) -> Result<Vec<String>, String> {
let mut words = Vec::new();
let mut current = String::new();
let mut in_word = false;
let mut chars = text.chars();
while let Some(c) = chars.next() {
match c {
' ' | '\t' | '\n' | '\r' => {
if in_word {
words.push(std::mem::take(&mut current));
in_word = false;
}
}
'\'' => {
in_word = true;
loop {
match chars.next() {
Some('\'') => break,
Some(c) => current.push(c),
None => return Err(format!("unterminated ' in {text:?}")),
}
}
}
'"' => {
in_word = true;
loop {
match chars.next() {
Some('"') => break,
Some('\\') => match chars.next() {
Some(c @ ('"' | '\\' | '$' | '`')) => current.push(c),
Some('\n') => {}
Some(c) => {
current.push('\\');
current.push(c);
}
None => return Err(format!("unterminated \" in {text:?}")),
},
Some(c) => current.push(c),
None => return Err(format!("unterminated \" in {text:?}")),
}
}
}
'\\' => {
in_word = true;
match chars.next() {
Some('\n') => {}
Some(c) => current.push(c),
None => return Err(format!("trailing backslash in {text:?}")),
}
}
c => {
in_word = true;
current.push(c);
}
}
}
if in_word {
words.push(current);
}
Ok(words)
}
pub fn shell_quote(word: &str) -> String {
let safe = |c: char| c.is_ascii_alphanumeric() || "-_./=:,+@%".contains(c);
if !word.is_empty() && word.chars().all(safe) {
word.to_string()
} else {
format!("'{}'", word.replace('\'', r"'\''"))
}
}
pub fn job_count() -> usize {
std::env::var("QLD_TEST_JOBS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&n| n > 0)
.unwrap_or_else(|| thread::available_parallelism().map_or(4, |n| n.get()))
}
pub fn parallel_map<T: Sync, R: Send>(
items: &[T],
jobs: usize,
job: impl Fn(&T) -> R + Sync,
) -> Vec<R> {
let next = AtomicUsize::new(0);
let results: Mutex<Vec<Option<R>>> = Mutex::new((0..items.len()).map(|_| None).collect());
thread::scope(|scope| {
for _ in 0..jobs.clamp(1, items.len().max(1)) {
scope.spawn(|| {
loop {
let index = next.fetch_add(1, Ordering::SeqCst);
let Some(item) = items.get(index) else { break };
let result = job(item);
results.lock().unwrap_or_else(|e| e.into_inner())[index] = Some(result);
}
});
}
});
results
.into_inner()
.unwrap_or_else(|e| e.into_inner())
.into_iter()
.map(|r| r.expect("every job ran"))
.collect()
}