use mandible_core::{Authority, CommandNode, Provenance, Source};
use mandible_extract::exec::read_limit;
use mandible_extract::{ExtractError, ExtractionTier, NodeHints, ResolvedTool, Runner};
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
pub type Call = (Vec<String>, Duration);
#[derive(Default)]
struct State {
calls: Vec<Call>,
held: HashSet<String>,
timing_out: HashSet<String>,
children: HashMap<String, Vec<String>>,
}
#[derive(Default)]
pub struct Spy {
state: Mutex<State>,
changed: Condvar,
}
impl Spy {
pub fn new() -> Arc<Spy> {
Arc::new(Spy::default())
}
pub fn runner(self: &Arc<Self>) -> Arc<Runner> {
Arc::new(Runner::new(vec![Box::new(SpyTier(Arc::clone(self)))]))
}
pub fn tool() -> ResolvedTool {
ResolvedTool {
name: "t".to_string(),
path: Some(std::path::PathBuf::from("/opt/t")),
version: None,
}
}
pub fn children(&self, word: &str, names: &[&str]) {
self.state.lock().unwrap().children.insert(
word.to_string(),
names.iter().map(|n| n.to_string()).collect(),
);
}
pub fn hold(&self, word: &str) {
self.state.lock().unwrap().held.insert(word.to_string());
}
pub fn release(&self) {
self.state.lock().unwrap().held.clear();
self.changed.notify_all();
}
pub fn time_out(&self, word: &str, yes: bool) {
let mut state = self.state.lock().unwrap();
if yes {
state.timing_out.insert(word.to_string());
} else {
state.timing_out.remove(word);
}
}
pub fn calls(&self) -> Vec<Call> {
self.state.lock().unwrap().calls.clone()
}
pub fn count(&self, path: &[String]) -> usize {
self.calls().iter().filter(|(p, _)| p == path).count()
}
pub fn forget(&self) {
self.state.lock().unwrap().calls.clear();
}
pub fn wait_for(&self, path: &[String], n: usize) -> bool {
let end = Instant::now() + Duration::from_secs(10);
while Instant::now() < end {
if self.count(path) >= n {
return true;
}
std::thread::sleep(Duration::from_millis(5));
}
false
}
}
struct SpyTier(Arc<Spy>);
impl ExtractionTier for SpyTier {
fn name(&self) -> &'static str {
"test::spy"
}
fn authority(&self) -> Authority {
Source::HelpText.authority()
}
fn detect(&self, _tool: &ResolvedTool) -> bool {
true
}
fn extract_node(
&self,
_tool: &ResolvedTool,
path: &[String],
_hints: NodeHints,
) -> Result<CommandNode, ExtractError> {
let word = path.last().cloned().unwrap_or_default();
let mut state = self.0.state.lock().unwrap();
state.calls.push((path.to_vec(), read_limit()));
let end = Instant::now() + Duration::from_secs(15);
while state.held.contains(&word) && Instant::now() < end {
state = self
.0
.changed
.wait_timeout(state, Duration::from_millis(50))
.unwrap()
.0;
}
if state.timing_out.contains(&word) {
return Err(ExtractError::TimedOut);
}
let mut node = CommandNode::new(word.clone(), Provenance::single(Source::HelpText));
node.children_filled = true;
node.heading_attested = true;
for name in state.children.get(&word).into_iter().flatten() {
let mut child = CommandNode::new(name.clone(), Provenance::single(Source::HelpText));
child.heading_attested = true;
node.subcommands.push(child);
}
Ok(node)
}
}