use std::panic;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, RecvTimeoutError};
use std::sync::{Arc, Mutex, PoisonError};
use std::thread;
use std::time::Duration;
use crate::error::{At, Error, Reason};
use crate::form::Form;
#[allow(unused_imports)] use crate::form::{MAX_COMMENTED_DEPTH, MAX_DEPTH, MAX_SOURCE};
pub const PATIENCE: Duration = Duration::from_secs(1);
pub const MAX_ABANDONED: usize = 4;
static ABANDONED: Mutex<Vec<Arc<AtomicBool>>> = Mutex::new(Vec::new());
impl Form {
pub fn parse_within(source: &str, limit: Duration) -> Result<Self, Error> {
let wedged = still_running(&ABANDONED);
if wedged >= MAX_ABANDONED {
return Err(Error::new(
At::START,
Reason::NoThread { abandoned: wedged },
));
}
let done = Arc::new(AtomicBool::new(false));
let finished = Arc::clone(&done);
let owned = source.to_string();
let (sender, results) = mpsc::sync_channel(1);
let Ok(worker) = thread::Builder::new()
.name(String::from("dform-parse"))
.spawn(move || {
let parsed = Self::parse(&owned);
let _ = sender.send(parsed);
finished.store(true, Ordering::Release);
})
else {
return Err(Error::new(
At::START,
Reason::NoThread { abandoned: wedged },
));
};
match results.recv_timeout(limit) {
Ok(parsed) => parsed,
Err(RecvTimeoutError::Timeout) => {
abandon(&ABANDONED, done);
Err(Error::new(At::START, Reason::TooSlow { limit }))
}
Err(RecvTimeoutError::Disconnected) => match worker.join() {
Err(panicked) => panic::resume_unwind(panicked),
Ok(()) => unreachable!("the worker returned without sending a result"),
},
}
}
}
fn still_running(abandoned: &Mutex<Vec<Arc<AtomicBool>>>) -> usize {
let mut wedged = abandoned.lock().unwrap_or_else(PoisonError::into_inner);
wedged.retain(|done| !done.load(Ordering::Acquire));
wedged.len()
}
fn abandon(abandoned: &Mutex<Vec<Arc<AtomicBool>>>, done: Arc<AtomicBool>) {
abandoned
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(done);
}
#[cfg(test)]
mod tests {
use super::*;
fn flag(finished: bool) -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(finished))
}
#[test]
fn a_form_parses_within_the_default() {
let source = std::fs::read_to_string("../forms/reference.dform").expect("the form is here");
let form = Form::parse_within(&source, PATIENCE).expect("the reference form, in a second");
assert_eq!(form.text(), source);
}
#[test]
fn a_deadline_of_nothing_is_never_met() {
let source = std::fs::read_to_string("../forms/reference.dform").expect("the form is here");
let error = Form::parse_within(&source, Duration::ZERO).expect_err("no time at all");
assert_eq!(
error.reason,
Reason::TooSlow {
limit: Duration::ZERO
}
);
assert!(error.to_string().contains("longer than"), "{error}");
}
#[test]
fn the_error_a_missed_deadline_gives_is_not_about_the_file() {
let source = std::fs::read_to_string("../forms/reference.dform").expect("the form is here");
let error = Form::parse_within(&source, Duration::ZERO).expect_err("no time at all");
assert_eq!(error.at, At::START);
}
#[test]
fn a_deadline_changes_nothing_about_what_a_form_means() {
let refused = "form \"F\" version=1 width=1 height=1 {\n panel \"p\" x=0 y=0 w=1 h=1\n";
let direct = Form::parse(refused).expect_err("an unclosed brace");
let bounded = Form::parse_within(refused, PATIENCE).expect_err("an unclosed brace");
assert_eq!(direct.at, bounded.at);
assert_eq!(direct.reason, bounded.reason);
assert_eq!(bounded.reason, Reason::Unbalanced { open: true });
}
#[test]
fn a_parse_that_has_finished_stops_being_counted() {
let list = Mutex::new(vec![flag(true), flag(false), flag(true)]);
assert_eq!(still_running(&list), 1);
assert_eq!(list.lock().expect("not poisoned").len(), 1);
}
#[test]
fn wedged_parses_pile_up_until_the_limit() {
let list = Mutex::new(Vec::new());
for _ in 0..MAX_ABANDONED {
assert!(still_running(&list) < MAX_ABANDONED);
abandon(&list, flag(false));
}
assert_eq!(still_running(&list), MAX_ABANDONED);
}
#[test]
fn no_thread_says_which_of_the_two_things_went_wrong() {
let full = Error::new(
At::START,
Reason::NoThread {
abandoned: MAX_ABANDONED,
},
);
assert!(full.to_string().contains("restart"), "{full}");
let refused = Error::new(At::START, Reason::NoThread { abandoned: 0 });
assert!(refused.to_string().contains("thread"), "{refused}");
assert!(!refused.to_string().contains("restart"), "{refused}");
}
}