use nix::unistd::Pid;
use std::collections::HashMap;
use std::os::fd::BorrowedFd;
use std::os::unix::io::RawFd;
pub type JobId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobStatus {
Running,
Stopped(i32), Done(i32), Terminated(i32), }
#[derive(Debug, Clone)]
pub struct Job {
pub id: JobId,
pub pgid: Pid,
pub pids: Vec<Pid>,
pub command: String,
pub status: JobStatus,
pub notified: bool,
pub foreground: bool,
saved_tmodes: Option<nix::sys::termios::Termios>,
}
impl Job {
pub fn saved_tmodes(&self) -> Option<&nix::sys::termios::Termios> {
self.saved_tmodes.as_ref()
}
pub fn set_saved_tmodes(&mut self, t: Option<nix::sys::termios::Termios>) {
self.saved_tmodes = t;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobSpec<'a> {
Current,
Previous,
Numeric(JobId),
Prefix(&'a str),
Substring(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobSpecError {
Malformed,
NoSuchJob,
Ambiguous,
}
pub fn parse_job_spec(s: &str) -> Result<JobSpec<'_>, JobSpecError> {
let rest = s.strip_prefix('%').ok_or(JobSpecError::Malformed)?;
match rest {
"" => Err(JobSpecError::Malformed),
"%" | "+" => Ok(JobSpec::Current),
"-" => Ok(JobSpec::Previous),
_ => {
if rest.bytes().all(|b| b.is_ascii_digit()) {
return rest
.parse::<JobId>()
.map(JobSpec::Numeric)
.map_err(|_| JobSpecError::Malformed);
}
if let Some(sub) = rest.strip_prefix('?') {
if sub.is_empty() {
return Err(JobSpecError::Malformed);
}
return Ok(JobSpec::Substring(sub));
}
Ok(JobSpec::Prefix(rest))
}
}
}
#[derive(Debug, Clone, Default)]
pub struct JobTable {
jobs: HashMap<JobId, Job>,
next_id: JobId,
current: Option<JobId>,
previous: Option<JobId>,
shell_tmodes: Option<nix::sys::termios::Termios>,
}
impl JobTable {
pub fn set_shell_tmodes(&mut self, t: nix::sys::termios::Termios) {
self.shell_tmodes = Some(t);
}
pub fn shell_tmodes(&self) -> Option<&nix::sys::termios::Termios> {
self.shell_tmodes.as_ref()
}
pub fn add_job(
&mut self,
pgid: Pid,
pids: Vec<Pid>,
command: impl Into<String>,
foreground: bool,
) -> JobId {
self.next_id += 1;
let id = self.next_id;
let job = Job {
id,
pgid,
pids,
command: command.into(),
status: JobStatus::Running,
notified: false,
foreground,
saved_tmodes: None,
};
self.jobs.insert(id, job);
self.previous = self.current;
self.current = Some(id);
id
}
pub fn remove_job(&mut self, id: JobId) {
self.jobs.remove(&id);
if self.current == Some(id) {
self.current = self.previous;
let new_current = self.current;
self.previous = self
.jobs
.keys()
.copied()
.filter(|&k| Some(k) != new_current)
.max();
} else if self.previous == Some(id) {
let cur = self.current;
self.previous = self.jobs.keys().copied().filter(|&k| Some(k) != cur).max();
}
}
pub fn get(&self, id: JobId) -> Option<&Job> {
self.jobs.get(&id)
}
pub fn get_mut(&mut self, id: JobId) -> Option<&mut Job> {
self.jobs.get_mut(&id)
}
#[allow(dead_code)] pub fn current_job(&self) -> Option<&Job> {
self.current.and_then(|id| self.jobs.get(&id))
}
#[allow(dead_code)] pub fn previous_job(&self) -> Option<&Job> {
self.previous.and_then(|id| self.jobs.get(&id))
}
pub fn current_id(&self) -> Option<JobId> {
self.current
}
#[allow(dead_code)] pub fn previous_id(&self) -> Option<JobId> {
self.previous
}
#[allow(dead_code)] pub fn is_empty(&self) -> bool {
self.jobs.is_empty()
}
pub fn update_status(&mut self, pid: Pid, status: JobStatus) {
if let Some(job) = self.jobs.values_mut().find(|j| j.pids.contains(&pid)) {
job.status = status;
job.notified = false;
}
}
#[allow(dead_code)] pub fn find_by_pgid(&self, pgid: Pid) -> Option<&Job> {
self.jobs.values().find(|j| j.pgid == pgid)
}
#[allow(dead_code)] pub fn find_by_pgid_mut(&mut self, pgid: Pid) -> Option<&mut Job> {
self.jobs.values_mut().find(|j| j.pgid == pgid)
}
pub fn last_bg_pid(&self) -> Option<Pid> {
self.jobs
.values()
.filter(|j| !j.foreground)
.max_by_key(|j| j.id)
.map(|j| j.pgid)
}
pub fn all_jobs(&self) -> impl Iterator<Item = &Job> {
let mut ids: Vec<JobId> = self.jobs.keys().copied().collect();
ids.sort();
let sorted: Vec<&Job> = ids.iter().map(|id| &self.jobs[id]).collect();
sorted.into_iter()
}
pub fn resolve_job_spec(&self, spec: &str) -> Result<JobId, JobSpecError> {
self.resolve(parse_job_spec(spec)?)
}
pub fn resolve(&self, spec: JobSpec<'_>) -> Result<JobId, JobSpecError> {
match spec {
JobSpec::Current => self.current.ok_or(JobSpecError::NoSuchJob),
JobSpec::Previous => self.previous.ok_or(JobSpecError::NoSuchJob),
JobSpec::Numeric(n) => {
if self.jobs.contains_key(&n) {
Ok(n)
} else {
Err(JobSpecError::NoSuchJob)
}
}
JobSpec::Prefix(s) => self.resolve_by(|cmd| cmd.starts_with(s)),
JobSpec::Substring(s) => self.resolve_by(|cmd| cmd.contains(s)),
}
}
fn resolve_by<F>(&self, mut pred: F) -> Result<JobId, JobSpecError>
where
F: FnMut(&str) -> bool,
{
let mut matched: Option<JobId> = None;
for job in self.jobs.values() {
if pred(&job.command) {
if matched.is_some() {
return Err(JobSpecError::Ambiguous);
}
matched = Some(job.id);
}
}
matched.ok_or(JobSpecError::NoSuchJob)
}
pub fn pending_notifications(&self) -> Vec<JobId> {
let mut ids: Vec<JobId> = self
.jobs
.values()
.filter(|j| {
!j.notified && matches!(j.status, JobStatus::Done(_) | JobStatus::Terminated(_))
})
.map(|j| j.id)
.collect();
ids.sort();
ids
}
pub fn mark_notified(&mut self, id: JobId) {
if let Some(job) = self.jobs.get_mut(&id) {
job.notified = true;
}
}
pub fn format_job(&self, id: JobId) -> Option<String> {
let job = self.jobs.get(&id)?;
let indicator = self.indicator(id);
let status_str = self.format_status(job.status);
Some(format!(
"[{}]{} {} {}",
job.id, indicator, status_str, job.command
))
}
pub fn format_job_long(&self, id: JobId) -> Option<String> {
let job = self.jobs.get(&id)?;
let indicator = self.indicator(id);
let status_str = self.format_status(job.status);
Some(format!(
"[{}]{} {} {} {}",
job.id,
indicator,
job.pgid.as_raw(),
status_str,
job.command
))
}
pub fn cleanup_notified(&mut self) {
let to_remove: Vec<JobId> = self
.jobs
.values()
.filter(|j| {
j.notified && matches!(j.status, JobStatus::Done(_) | JobStatus::Terminated(_))
})
.map(|j| j.id)
.collect();
for id in to_remove {
self.remove_job(id);
}
}
fn indicator(&self, id: JobId) -> char {
if self.current == Some(id) {
'+'
} else if self.previous == Some(id) {
'-'
} else {
' '
}
}
fn format_status(&self, status: JobStatus) -> String {
match status {
JobStatus::Running => "Running".to_string(),
JobStatus::Stopped(sig) => {
let name = crate::signal::signal_number_to_name(sig).unwrap_or("UNKNOWN");
format!("Stopped(SIG{})", name)
}
JobStatus::Done(0) => "Done".to_string(),
JobStatus::Done(code) => format!("Done({})", code),
JobStatus::Terminated(sig) => {
let name = crate::signal::signal_number_to_name(sig).unwrap_or("UNKNOWN");
format!("Terminated(SIG{})", name)
}
}
}
}
const TERMINAL_FD: RawFd = 0;
pub fn give_terminal(pgid: Pid) -> Result<(), nix::Error> {
let fd = unsafe { BorrowedFd::borrow_raw(TERMINAL_FD) };
nix::unistd::tcsetpgrp(fd, pgid)
}
pub fn take_terminal(shell_pgid: Pid) -> Result<(), nix::Error> {
let fd = unsafe { BorrowedFd::borrow_raw(TERMINAL_FD) };
nix::unistd::tcsetpgrp(fd, shell_pgid)
}
#[cfg(test)]
mod tests {
use super::*;
fn pid(n: i32) -> Pid {
Pid::from_raw(n)
}
#[test]
fn test_default_is_empty() {
let table = JobTable::default();
assert!(table.is_empty());
assert!(table.current_job().is_none());
assert!(table.previous_job().is_none());
}
#[test]
fn test_job_saved_tmodes_defaults_none() {
let mut table = JobTable::default();
let id = table.add_job(pid(42), vec![pid(42)], "cmd", false);
let job = table.get(id).expect("job should exist");
assert!(job.saved_tmodes().is_none(),
"saved_tmodes() should default to None on new job");
}
#[test]
fn test_job_table_shell_tmodes_defaults_none() {
let table = JobTable::default();
assert!(table.shell_tmodes().is_none(),
"shell_tmodes should default to None on new JobTable");
}
#[test]
fn test_set_shell_tmodes_stores_value() {
let mut table = JobTable::default();
let zeroed: libc::termios = unsafe { std::mem::zeroed() };
let t: nix::sys::termios::Termios = zeroed.into();
table.set_shell_tmodes(t);
assert!(table.shell_tmodes().is_some(),
"shell_tmodes should hold the value after set_shell_tmodes");
}
#[test]
fn test_job_status_equality() {
assert_eq!(JobStatus::Running, JobStatus::Running);
assert_eq!(JobStatus::Done(0), JobStatus::Done(0));
assert_ne!(JobStatus::Done(0), JobStatus::Done(1));
assert_eq!(JobStatus::Stopped(20), JobStatus::Stopped(20));
assert_eq!(JobStatus::Terminated(9), JobStatus::Terminated(9));
}
#[test]
fn test_add_job_assigns_incrementing_ids() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(100), vec![pid(100)], "sleep 1", false);
let id2 = table.add_job(pid(200), vec![pid(200)], "sleep 2", false);
let id3 = table.add_job(pid(300), vec![pid(300)], "sleep 3", false);
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn test_add_job_updates_current_previous() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(100), vec![pid(100)], "cmd1", false);
assert_eq!(table.current_id(), Some(id1));
assert!(table.previous_id().is_none());
let id2 = table.add_job(pid(200), vec![pid(200)], "cmd2", false);
assert_eq!(table.current_id(), Some(id2));
assert_eq!(table.previous_id(), Some(id1));
let id3 = table.add_job(pid(300), vec![pid(300)], "cmd3", false);
assert_eq!(table.current_id(), Some(id3));
assert_eq!(table.previous_id(), Some(id2));
}
#[test]
fn test_get_returns_correct_job() {
let mut table = JobTable::default();
let id = table.add_job(pid(42), vec![pid(42)], "echo hi", false);
let job = table.get(id).expect("job should exist");
assert_eq!(job.command, "echo hi");
assert_eq!(job.pgid, pid(42));
}
#[test]
fn test_get_returns_none_for_nonexistent() {
let table = JobTable::default();
assert!(table.get(99).is_none());
}
#[test]
fn test_get_mut_modifies_job() {
let mut table = JobTable::default();
let id = table.add_job(pid(10), vec![pid(10)], "cmd", false);
table.get_mut(id).unwrap().status = JobStatus::Done(0);
assert_eq!(table.get(id).unwrap().status, JobStatus::Done(0));
}
#[test]
fn test_remove_job_updates_current_previous() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
let id3 = table.add_job(pid(3), vec![pid(3)], "c", false);
table.remove_job(id3);
assert_eq!(table.current_id(), Some(id2));
assert_eq!(table.previous_id(), Some(id1));
}
#[test]
fn test_remove_non_current_job() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
table.remove_job(id1);
assert_eq!(table.current_id(), Some(id2));
assert!(table.previous_id().is_none());
}
#[test]
fn test_current_job_previous_job() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(10), vec![pid(10)], "first", false);
let id2 = table.add_job(pid(20), vec![pid(20)], "second", false);
assert_eq!(table.current_job().map(|j| j.id), Some(id2));
assert_eq!(table.previous_job().map(|j| j.id), Some(id1));
}
#[test]
fn test_update_status_by_pid() {
let mut table = JobTable::default();
let id = table.add_job(pid(55), vec![pid(55), pid(56)], "pipe", false);
table.update_status(pid(56), JobStatus::Done(0));
let job = table.get(id).unwrap();
assert_eq!(job.status, JobStatus::Done(0));
assert!(!job.notified, "notified should be reset to false");
}
#[test]
fn test_update_status_unknown_pid_is_noop() {
let mut table = JobTable::default();
let id = table.add_job(pid(70), vec![pid(70)], "sleep", false);
table.update_status(pid(9999), JobStatus::Done(1));
assert_eq!(table.get(id).unwrap().status, JobStatus::Running);
}
#[test]
fn test_find_by_pgid() {
let mut table = JobTable::default();
let id = table.add_job(pid(77), vec![pid(77)], "foo", false);
let found = table.find_by_pgid(pid(77)).expect("should find by pgid");
assert_eq!(found.id, id);
assert!(table.find_by_pgid(pid(9999)).is_none());
}
#[test]
fn test_last_bg_pid_none_when_empty() {
let table = JobTable::default();
assert!(table.last_bg_pid().is_none());
}
#[test]
fn test_last_bg_pid_returns_most_recent_bg_job() {
let mut table = JobTable::default();
table.add_job(pid(10), vec![pid(10)], "bg1", false); table.add_job(pid(20), vec![pid(20)], "fg", true); table.add_job(pid(30), vec![pid(30)], "bg2", false);
assert_eq!(table.last_bg_pid(), Some(pid(30)));
}
#[test]
fn test_last_bg_pid_none_when_all_foreground() {
let mut table = JobTable::default();
table.add_job(pid(5), vec![pid(5)], "fg", true);
assert!(table.last_bg_pid().is_none());
}
#[test]
fn test_all_jobs_sorted_by_id() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "a", false);
table.add_job(pid(2), vec![pid(2)], "b", false);
table.add_job(pid(3), vec![pid(3)], "c", false);
let ids: Vec<JobId> = table.all_jobs().map(|j| j.id).collect();
assert_eq!(ids, vec![1, 2, 3]);
}
#[test]
fn test_resolve_job_spec_numeric() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%1"), Ok(id));
}
#[test]
fn test_resolve_job_spec_percent_percent() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%%"), Ok(id));
}
#[test]
fn test_resolve_job_spec_plus() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve_job_spec("%+"), Ok(id));
}
#[test]
fn test_resolve_job_spec_minus() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let _id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
assert_eq!(table.resolve_job_spec("%-"), Ok(id1));
}
#[test]
fn test_resolve_job_spec_invalid() {
let table = JobTable::default();
assert_eq!(table.resolve_job_spec("%99"), Err(JobSpecError::NoSuchJob));
assert_eq!(table.resolve_job_spec("foo"), Err(JobSpecError::Malformed));
assert_eq!(table.resolve_job_spec("%abc"), Err(JobSpecError::NoSuchJob));
}
#[test]
fn test_resolve_job_spec_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 10", false);
table.add_job(pid(2), vec![pid(2)], "sleep 20", false);
assert_eq!(
table.resolve_job_spec("%sleep"),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_parse_current_percent() {
assert_eq!(parse_job_spec("%%"), Ok(JobSpec::Current));
}
#[test]
fn test_parse_current_plus() {
assert_eq!(parse_job_spec("%+"), Ok(JobSpec::Current));
}
#[test]
fn test_parse_previous() {
assert_eq!(parse_job_spec("%-"), Ok(JobSpec::Previous));
}
#[test]
fn test_parse_numeric() {
assert_eq!(parse_job_spec("%1"), Ok(JobSpec::Numeric(1)));
assert_eq!(parse_job_spec("%42"), Ok(JobSpec::Numeric(42)));
}
#[test]
fn test_parse_numeric_overflow() {
assert_eq!(
parse_job_spec("%99999999999999999999"),
Err(JobSpecError::Malformed)
);
}
#[test]
fn test_parse_prefix() {
assert_eq!(parse_job_spec("%foo"), Ok(JobSpec::Prefix("foo")));
assert_eq!(parse_job_spec("%vim"), Ok(JobSpec::Prefix("vim")));
}
#[test]
fn test_parse_substring() {
assert_eq!(parse_job_spec("%?bar"), Ok(JobSpec::Substring("bar")));
assert_eq!(parse_job_spec("%?READ"), Ok(JobSpec::Substring("READ")));
}
#[test]
fn test_parse_prefix_hyphen() {
assert_eq!(parse_job_spec("%-foo"), Ok(JobSpec::Prefix("-foo")));
}
#[test]
fn test_parse_prefix_double_percent() {
assert_eq!(parse_job_spec("%%foo"), Ok(JobSpec::Prefix("%foo")));
}
#[test]
fn test_parse_malformed_empty() {
assert_eq!(parse_job_spec(""), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_bare_percent() {
assert_eq!(parse_job_spec("%"), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_bare_question() {
assert_eq!(parse_job_spec("%?"), Err(JobSpecError::Malformed));
}
#[test]
fn test_parse_malformed_no_percent() {
assert_eq!(parse_job_spec("foo"), Err(JobSpecError::Malformed));
assert_eq!(parse_job_spec("1"), Err(JobSpecError::Malformed));
}
#[test]
fn test_pending_notifications_empty_when_running() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep", false);
assert!(table.pending_notifications().is_empty());
}
#[test]
fn test_pending_notifications_non_empty_when_done() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "ls", false);
table.update_status(pid(1), JobStatus::Done(0));
let pending = table.pending_notifications();
assert_eq!(pending, vec![id]);
}
#[test]
fn test_pending_notifications_sorted() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
table.update_status(pid(2), JobStatus::Done(0));
table.update_status(pid(1), JobStatus::Terminated(9));
let pending = table.pending_notifications();
assert_eq!(pending, vec![id1, id2]);
}
#[test]
fn test_mark_notified_clears_pending() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "ls", false);
table.update_status(pid(1), JobStatus::Done(0));
assert!(!table.pending_notifications().is_empty());
table.mark_notified(id);
assert!(table.pending_notifications().is_empty());
}
#[test]
fn test_format_job_running() {
let mut table = JobTable::default();
let id = table.add_job(pid(100), vec![pid(100)], "sleep 10", false);
let s = table.format_job(id).expect("format should succeed");
assert!(s.contains("[1]"), "should contain job id");
assert!(s.contains('+'), "current job should have + indicator");
assert!(s.contains("Running"), "should contain Running status");
assert!(s.contains("sleep 10"), "should contain command");
}
#[test]
fn test_format_job_done() {
let mut table = JobTable::default();
let id = table.add_job(pid(200), vec![pid(200)], "true", false);
table.update_status(pid(200), JobStatus::Done(0));
let s = table.format_job(id).expect("format should succeed");
assert!(s.contains("Done"), "should contain Done status");
}
#[test]
fn test_format_job_nonexistent() {
let table = JobTable::default();
assert!(table.format_job(99).is_none());
}
#[test]
fn test_resolve_current() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve(JobSpec::Current), Ok(id));
}
#[test]
fn test_resolve_current_unset() {
let table = JobTable::default();
assert_eq!(
table.resolve(JobSpec::Current),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_previous() {
let mut table = JobTable::default();
let id1 = table.add_job(pid(1), vec![pid(1)], "a", false);
let _id2 = table.add_job(pid(2), vec![pid(2)], "b", false);
assert_eq!(table.resolve(JobSpec::Previous), Ok(id1));
}
#[test]
fn test_resolve_previous_unset() {
let mut table = JobTable::default();
let _id = table.add_job(pid(1), vec![pid(1)], "a", false);
assert_eq!(
table.resolve(JobSpec::Previous),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_numeric_hit() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "x", false);
assert_eq!(table.resolve(JobSpec::Numeric(id)), Ok(id));
}
#[test]
fn test_resolve_numeric_miss() {
let table = JobTable::default();
assert_eq!(
table.resolve(JobSpec::Numeric(99)),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_prefix_single() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim README.md", false);
table.add_job(pid(2), vec![pid(2)], "sleep 30", false);
assert_eq!(table.resolve(JobSpec::Prefix("vim")), Ok(id));
}
#[test]
fn test_resolve_prefix_none() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 30", false);
assert_eq!(
table.resolve(JobSpec::Prefix("vim")),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_prefix_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 10", false);
table.add_job(pid(2), vec![pid(2)], "sleep 20", false);
assert_eq!(
table.resolve(JobSpec::Prefix("sleep")),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_resolve_substring_single() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim README.md", false);
table.add_job(pid(2), vec![pid(2)], "sleep 30", false);
assert_eq!(table.resolve(JobSpec::Substring("EADME")), Ok(id));
}
#[test]
fn test_resolve_substring_none() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "sleep 30", false);
assert_eq!(
table.resolve(JobSpec::Substring("vim")),
Err(JobSpecError::NoSuchJob)
);
}
#[test]
fn test_resolve_substring_ambiguous() {
let mut table = JobTable::default();
table.add_job(pid(1), vec![pid(1)], "cat foo", false);
table.add_job(pid(2), vec![pid(2)], "grep foo", false);
assert_eq!(
table.resolve(JobSpec::Substring("foo")),
Err(JobSpecError::Ambiguous)
);
}
#[test]
fn test_resolve_prefix_matches_done_job() {
let mut table = JobTable::default();
let id = table.add_job(pid(1), vec![pid(1)], "vim foo", false);
if let Some(job) = table.get_mut(id) {
job.status = JobStatus::Done(0);
}
assert_eq!(table.resolve(JobSpec::Prefix("vim")), Ok(id));
}
#[test]
fn test_terminal_functions_compile() {
let _: fn(Pid) -> Result<(), nix::Error> = give_terminal;
let _: fn(Pid) -> Result<(), nix::Error> = take_terminal;
}
#[test]
fn test_job_set_saved_tmodes_overwrites_with_none() {
let mut table = JobTable::default();
let id = table.add_job(pid(42), vec![pid(42)], "cmd", false);
let zeroed: libc::termios = unsafe { std::mem::zeroed() };
let t: nix::sys::termios::Termios = zeroed.into();
table
.get_mut(id)
.expect("job should exist")
.set_saved_tmodes(Some(t));
assert!(
table.get(id).unwrap().saved_tmodes().is_some(),
"saved_tmodes() should return Some after set_saved_tmodes(Some(_))"
);
table
.get_mut(id)
.expect("job should exist")
.set_saved_tmodes(None);
assert!(
table.get(id).unwrap().saved_tmodes().is_none(),
"saved_tmodes() should return None after set_saved_tmodes(None)"
);
}
}