use super::{Context, Input, Result};
use crate::Env;
use crate::io::Fd;
use crate::job::fmt::Accumulator;
use crate::option::{Interactive, Monitor, Off};
use crate::system::{Fcntl, Signals, Write};
use std::cell::RefCell;
#[derive(Debug)]
pub struct Reporter<'a, 'b, S, T> {
inner: T,
env: &'a RefCell<&'b mut Env<S>>,
}
impl<'a, 'b, S, T> Reporter<'a, 'b, S, T> {
pub fn new(inner: T, env: &'a RefCell<&'b mut Env<S>>) -> Self {
Self { inner, env }
}
}
impl<S, T: Clone> Clone for Reporter<'_, '_, S, T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
env: self.env,
}
}
}
impl<S: Fcntl + Signals + Write, T: Input> Input for Reporter<'_, '_, S, T> {
#[allow(clippy::await_holding_refcell_ref)]
async fn next_line(&mut self, context: &Context) -> Result {
report(&mut self.env.borrow_mut()).await;
self.inner.next_line(context).await
}
}
async fn report<S: Fcntl + Signals + Write>(env: &mut Env<S>) {
if env.options.get(Interactive) == Off || env.options.get(Monitor) == Off {
return;
}
let mut accumulator = Accumulator::new();
accumulator.current_job_index = env.jobs.current_job();
accumulator.previous_job_index = env.jobs.previous_job();
env.jobs
.iter()
.filter(|(_, job)| job.state_changed)
.for_each(|(index, job)| accumulator.add(index, job, &env.system));
if env
.system
.write_all(Fd::STDERR, accumulator.print.as_bytes())
.await
.is_ok()
{
for index in accumulator.indices_reported {
env.jobs.get_mut(index).unwrap().state_reported();
}
}
}
#[cfg(test)]
mod tests {
use super::super::Memory;
use super::*;
use crate::VirtualSystem;
use crate::job::{Job, Pid, ProcessState};
use crate::option::On;
use crate::system::r#virtual::SystemState;
use crate::test_helper::assert_stderr;
use futures_util::FutureExt as _;
use std::rc::Rc;
#[test]
fn reporter_reads_from_inner_input() {
let mut env = Env::new_virtual();
let ref_env = RefCell::new(&mut env);
let mut reporter = Reporter::new(Memory::new("echo hello"), &ref_env);
let result = reporter
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo hello");
}
#[test]
fn reporter_shows_job_status_before_reading_input() {
let system = VirtualSystem::new();
let state = system.state.clone();
let mut env = Env::with_system(system);
env.jobs.add({
let mut job = Job::new(Pid(10));
job.state_changed = true;
job.name = "echo hello".to_string();
job
});
env.options.set(Interactive, On);
env.options.set(Monitor, On);
struct InputMock(Rc<RefCell<SystemState>>);
impl Input for InputMock {
async fn next_line(&mut self, _: &Context) -> Result {
assert_stderr(&self.0, |stderr| {
assert!(stderr.starts_with("[1]"), "stderr: {stderr:?}")
});
Ok("foo".to_string())
}
}
let ref_env = RefCell::new(&mut env);
let mut reporter = Reporter::new(InputMock(state), &ref_env);
let result = reporter
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "foo"); }
#[test]
fn all_jobs_with_changed_status_are_reported() {
let system = VirtualSystem::new();
let state = system.state.clone();
let mut env = Env::with_system(system);
env.jobs.add({
let mut job = Job::new(Pid(10));
job.state_changed = true;
job.name = "echo hello".to_string();
job
});
env.jobs.add({
let mut job = Job::new(Pid(20));
job.state_changed = false;
job.name = "sleep 1".to_string();
job
});
env.jobs.add({
let mut job = Job::new(Pid(30));
job.state = ProcessState::exited(0);
job.state_changed = true;
job.name = "cat README".to_string();
job
});
env.options.set(Interactive, On);
env.options.set(Monitor, On);
let ref_env = RefCell::new(&mut env);
let memory = Memory::new("echo hello\n");
let mut reporter = Reporter::new(memory, &ref_env);
reporter
.next_line(&Context::default())
.now_or_never()
.unwrap()
.unwrap();
assert_stderr(&state, |stderr| {
let mut lines = stderr.lines();
let first = lines.next().unwrap();
assert!(first.starts_with("[1]"), "first: {first:?}");
assert!(first.contains("Running"), "first: {first:?}");
assert!(first.contains("echo hello"), "first: {first:?}");
let second = lines.next().unwrap();
assert!(second.starts_with("[3]"), "second: {second:?}");
assert!(second.contains("Done"), "second: {second:?}");
assert!(second.contains("cat README"), "second: {second:?}");
assert_eq!(lines.next(), None);
});
}
#[test]
fn reporter_clears_state_changed_flag() {
let mut env = Env::new_virtual();
let index = env.jobs.add({
let mut job = Job::new(Pid(10));
job.state_changed = true;
job.name = "echo hello".to_string();
job
});
env.options.set(Interactive, On);
env.options.set(Monitor, On);
let ref_env = RefCell::new(&mut env);
let memory = Memory::new("echo hello\n");
let mut reporter = Reporter::new(memory, &ref_env);
reporter
.next_line(&Context::default())
.now_or_never()
.unwrap()
.unwrap();
assert!(!env.jobs.get(index).unwrap().state_changed);
}
#[test]
fn no_report_if_not_interactive() {
let system = VirtualSystem::new();
let state = system.state.clone();
let mut env = Env::with_system(system);
env.jobs.add({
let mut job = Job::new(Pid(10));
job.state_changed = true;
job.name = "echo hello".to_string();
job
});
env.options.set(Monitor, On);
let ref_env = RefCell::new(&mut env);
let memory = Memory::new("echo hello\n");
let mut reporter = Reporter::new(memory, &ref_env);
reporter
.next_line(&Context::default())
.now_or_never()
.unwrap()
.unwrap();
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn no_report_if_not_monitor() {
let system = VirtualSystem::new();
let state = system.state.clone();
let mut env = Env::with_system(system);
env.jobs.add({
let mut job = Job::new(Pid(10));
job.state_changed = true;
job.name = "echo hello".to_string();
job
});
env.options.set(Interactive, On);
let ref_env = RefCell::new(&mut env);
let memory = Memory::new("echo hello\n");
let mut reporter = Reporter::new(memory, &ref_env);
reporter
.next_line(&Context::default())
.now_or_never()
.unwrap()
.unwrap();
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
}