yash_env/trap/
state.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2023 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Items that manage the state of a single signal.
18
19#[cfg(doc)]
20use super::TrapSet;
21use super::{Condition, SignalSystem};
22use crate::system::{Disposition, Errno};
23use std::collections::btree_map::{Entry, VacantEntry};
24use std::rc::Rc;
25use thiserror::Error;
26use yash_syntax::source::Location;
27
28/// Action performed when a [`Condition`] is met
29#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)]
30pub enum Action {
31    /// Performs the default action.
32    ///
33    /// For signal conditions, the behavior depends on the signal delivered.
34    /// For other conditions, this is equivalent to `Ignore`.
35    #[default]
36    Default,
37
38    /// Pretends as if the condition was not met.
39    Ignore,
40
41    /// Executes a command string.
42    Command(Rc<str>),
43}
44
45impl From<&Action> for Disposition {
46    fn from(trap: &Action) -> Self {
47        match trap {
48            Action::Default => Disposition::Default,
49            Action::Ignore => Disposition::Ignore,
50            Action::Command(_) => Disposition::Catch,
51        }
52    }
53}
54
55/// Error that may happen in [`TrapSet::set_action`].
56#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
57pub enum SetActionError {
58    /// Attempt to set a trap that has been ignored since the shell startup.
59    #[error("the signal has been ignored since startup")]
60    InitiallyIgnored,
61
62    /// Attempt to set a trap for the `SIGKILL` signal.
63    #[error("cannot set a trap for SIGKILL")]
64    SIGKILL,
65
66    /// Attempt to set a trap for the `SIGSTOP` signal.
67    #[error("cannot set a trap for SIGSTOP")]
68    SIGSTOP,
69
70    /// Error from the underlying system interface.
71    #[error(transparent)]
72    SystemError(#[from] Errno),
73}
74
75/// State of the trap action for a condition.
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct TrapState {
78    /// Action taken when the condition is met.
79    pub action: Action,
80    /// Location of the simple command that invoked the trap built-in that set
81    /// the current action.
82    pub origin: Location,
83    /// True iff a signal specified by the condition has been caught and the
84    /// action command has not yet executed.
85    pub pending: bool,
86}
87
88/// User-visible trap setting.
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub enum Setting {
91    /// The user has not yet set a trap for the signal specified by the
92    /// condition, and the signal disposition the shell has inherited from the
93    /// pre-exec process is `SIG_DFL`.
94    InitiallyDefaulted,
95    /// The user has not yet set a trap for the signal specified by the
96    /// condition, and the signal disposition the shell has inherited from the
97    /// pre-exec process is `SIG_IGN`.
98    InitiallyIgnored,
99    /// User-defined trap.
100    UserSpecified(TrapState),
101}
102
103impl Setting {
104    pub fn as_trap(&self) -> Option<&TrapState> {
105        if let Setting::UserSpecified(trap) = self {
106            Some(trap)
107        } else {
108            None
109        }
110    }
111
112    fn is_user_defined_command(&self) -> bool {
113        matches!(
114            self,
115            Setting::UserSpecified(TrapState {
116                action: Action::Command(_),
117                ..
118            })
119        )
120    }
121
122    pub fn from_initial_disposition(disposition: Disposition) -> Self {
123        match disposition {
124            Disposition::Default | Disposition::Catch => Self::InitiallyDefaulted,
125            Disposition::Ignore => Self::InitiallyIgnored,
126        }
127    }
128}
129
130impl From<&Setting> for Disposition {
131    fn from(state: &Setting) -> Self {
132        match state {
133            Setting::InitiallyDefaulted => Disposition::Default,
134            Setting::InitiallyIgnored => Disposition::Ignore,
135            Setting::UserSpecified(trap) => (&trap.action).into(),
136        }
137    }
138}
139
140/// Option for [`GrandState::enter_subshell`]
141#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub enum EnterSubshellOption {
143    /// Keeps the current internal disposition configuration.
144    KeepInternalDisposition,
145    /// Resets the internal disposition configuration to the default.
146    ClearInternalDisposition,
147    /// Resets the internal disposition configuration to the default and sets the
148    /// signal disposition to `Ignore`.
149    Ignore,
150}
151
152/// Whole configuration and state for a trap condition
153#[derive(Clone, Debug)]
154pub struct GrandState {
155    /// Setting that is effective in the current environment
156    current_setting: Setting,
157
158    /// Setting that was effective in the parent environment
159    parent_setting: Option<Setting>,
160
161    /// Current internal disposition
162    ///
163    /// The internal disposition is the signal disposition that is set by the
164    /// shell itself, not by the user. This is used to handle some specific
165    /// signals like `SIGCHLD` and `SIGTSTP`. When the user sets a trap for
166    /// these signals, the actual signal disposition registered in the system
167    /// is computed as the maximum of the user-defined disposition and the
168    /// internal disposition.
169    internal_disposition: Disposition,
170}
171
172impl GrandState {
173    /// Returns the current and parent trap states.
174    ///
175    /// This function returns a pair of optional trap states. The first is the
176    /// currently configured trap action, and the second is the action set
177    /// before [`enter_subshell`](Self::enter_subshell) was called.
178    ///
179    /// This function does not reflect the initial signal actions the shell
180    /// inherited on startup.
181    #[must_use]
182    pub fn get_state(&self) -> (Option<&TrapState>, Option<&TrapState>) {
183        let current = self.current_setting.as_trap();
184        let parent = self.parent_setting.as_ref().and_then(Setting::as_trap);
185        (current, parent)
186    }
187
188    /// Clears the parent trap state.
189    pub fn clear_parent_setting(&mut self) {
190        self.parent_setting = None;
191    }
192
193    /// Updates the entry with the new action.
194    pub fn set_action<S: SignalSystem>(
195        system: &mut S,
196        entry: Entry<Condition, GrandState>,
197        action: Action,
198        origin: Location,
199        override_ignore: bool,
200    ) -> Result<(), SetActionError> {
201        let cond = *entry.key();
202        let setting = Setting::UserSpecified(TrapState {
203            action,
204            origin,
205            pending: false,
206        });
207        let disposition = (&setting).into();
208
209        match entry {
210            Entry::Vacant(vacant) => {
211                if let Condition::Signal(signal) = cond {
212                    if !override_ignore {
213                        let initial_disposition =
214                            system.set_disposition(signal, Disposition::Ignore)?;
215                        if initial_disposition == Disposition::Ignore {
216                            vacant.insert(GrandState {
217                                current_setting: Setting::InitiallyIgnored,
218                                parent_setting: None,
219                                internal_disposition: Disposition::Default,
220                            });
221                            return Err(SetActionError::InitiallyIgnored);
222                        }
223                    }
224
225                    if override_ignore || disposition != Disposition::Ignore {
226                        system.set_disposition(signal, disposition)?;
227                    }
228                }
229
230                vacant.insert(GrandState {
231                    current_setting: setting,
232                    parent_setting: None,
233                    internal_disposition: Disposition::Default,
234                });
235            }
236
237            Entry::Occupied(mut occupied) => {
238                let state = occupied.get_mut();
239                if !override_ignore && state.current_setting == Setting::InitiallyIgnored {
240                    return Err(SetActionError::InitiallyIgnored);
241                }
242
243                if let Condition::Signal(signal) = cond {
244                    let internal = state.internal_disposition;
245                    let old_disposition = internal.max((&state.current_setting).into());
246                    let new_disposition = internal.max(disposition);
247                    if old_disposition != new_disposition {
248                        system.set_disposition(signal, new_disposition)?;
249                    }
250                }
251
252                state.current_setting = setting;
253            }
254        }
255
256        Ok(())
257    }
258
259    /// Returns the current internal disposition.
260    #[must_use]
261    pub fn internal_disposition(&self) -> Disposition {
262        self.internal_disposition
263    }
264
265    /// Sets the internal disposition.
266    ///
267    /// The condition of the given entry must be a signal, or this function
268    /// panics.
269    pub fn set_internal_disposition<S: SignalSystem>(
270        system: &mut S,
271        entry: Entry<Condition, GrandState>,
272        disposition: Disposition,
273    ) -> Result<(), Errno> {
274        let signal = match *entry.key() {
275            Condition::Signal(signal) => signal,
276            Condition::Exit => panic!("exit condition cannot have an internal disposition"),
277        };
278
279        match entry {
280            Entry::Vacant(_) if disposition == Disposition::Default => (),
281
282            Entry::Vacant(vacant) => {
283                let initial_disposition = system.set_disposition(signal, disposition)?;
284                vacant.insert(GrandState {
285                    current_setting: Setting::from_initial_disposition(initial_disposition),
286                    parent_setting: None,
287                    internal_disposition: disposition,
288                });
289            }
290
291            Entry::Occupied(mut occupied) => {
292                let state = occupied.get_mut();
293                let setting = (&state.current_setting).into();
294                let old_disposition = state.internal_disposition.max(setting);
295                let new_disposition = disposition.max(setting);
296                if old_disposition != new_disposition {
297                    system.set_disposition(signal, new_disposition)?;
298                }
299                state.internal_disposition = disposition;
300            }
301        }
302
303        Ok(())
304    }
305
306    /// Updates the trap states and gets ready for executing the body a
307    /// subshell.
308    ///
309    /// If the current state has a user-specified command
310    /// (`Action::Command(_)`), it is saved in the parent state and reset to the
311    /// default. Additionally, the signal disposition is updated depending on the
312    /// `option`.
313    pub fn enter_subshell<S: SignalSystem>(
314        &mut self,
315        system: &mut S,
316        cond: Condition,
317        option: EnterSubshellOption,
318    ) -> Result<(), Errno> {
319        let old_setting = (&self.current_setting).into();
320        let old_disposition = self.internal_disposition.max(old_setting);
321
322        if self.current_setting.is_user_defined_command() {
323            self.parent_setting = Some(std::mem::replace(
324                &mut self.current_setting,
325                Setting::InitiallyDefaulted,
326            ));
327        }
328
329        let new_setting = (&self.current_setting).into();
330        let new_disposition = match option {
331            EnterSubshellOption::KeepInternalDisposition => {
332                self.internal_disposition.max(new_setting)
333            }
334            EnterSubshellOption::ClearInternalDisposition => new_setting,
335            EnterSubshellOption::Ignore => Disposition::Ignore,
336        };
337        if old_disposition != new_disposition {
338            if let Condition::Signal(signal) = cond {
339                system.set_disposition(signal, new_disposition)?;
340            }
341        }
342        self.internal_disposition = match option {
343            EnterSubshellOption::KeepInternalDisposition => self.internal_disposition,
344            EnterSubshellOption::ClearInternalDisposition | EnterSubshellOption::Ignore => {
345                Disposition::Default
346            }
347        };
348        Ok(())
349    }
350
351    /// Sets the disposition to `Ignore` for the given signal condition.
352    ///
353    /// This function creates a new entry having `Setting::InitiallyDefaulted`
354    /// or `Setting::InitiallyIgnored` based on the current setting.
355    ///
356    /// You should call this function in place of [`Self::enter_subshell`] if
357    /// there is no entry for the condition yet.
358    ///
359    /// This function panics if the condition is not a signal.
360    pub fn ignore<S: SignalSystem>(
361        system: &mut S,
362        vacant: VacantEntry<Condition, GrandState>,
363    ) -> Result<(), Errno> {
364        let signal = match *vacant.key() {
365            Condition::Signal(signal) => signal,
366            Condition::Exit => panic!("exit condition cannot be ignored"),
367        };
368        let initial_disposition = system.set_disposition(signal, Disposition::Ignore)?;
369        vacant.insert(GrandState {
370            current_setting: Setting::from_initial_disposition(initial_disposition),
371            parent_setting: None,
372            internal_disposition: Disposition::Default,
373        });
374        Ok(())
375    }
376
377    /// Marks this signal as caught.
378    ///
379    /// This function does nothing unless a user-specified trap action is set.
380    pub fn mark_as_caught(&mut self) {
381        if let Setting::UserSpecified(state) = &mut self.current_setting {
382            state.pending = true;
383        }
384    }
385
386    /// Clears the mark of this signal being caught and returns the trap state.
387    pub fn handle_if_caught(&mut self) -> Option<&TrapState> {
388        match &mut self.current_setting {
389            Setting::UserSpecified(trap) if trap.pending => {
390                trap.pending = false;
391                Some(trap)
392            }
393            _ => None,
394        }
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::super::tests::DummySystem;
401    use super::*;
402    use crate::system::r#virtual::{SIGCHLD, SIGQUIT, SIGTSTP, SIGTTOU, SIGUSR1};
403    use assert_matches::assert_matches;
404    use std::collections::BTreeMap;
405
406    #[test]
407    fn setting_trap_to_ignore_without_override_ignore() {
408        let mut system = DummySystem::default();
409        let mut map = BTreeMap::new();
410        let entry = map.entry(SIGCHLD.into());
411        let origin = Location::dummy("origin");
412
413        let result =
414            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false);
415        assert_eq!(result, Ok(()));
416        assert_eq!(
417            map[&SIGCHLD.into()].get_state(),
418            (
419                Some(&TrapState {
420                    action: Action::Ignore,
421                    origin,
422                    pending: false
423                }),
424                None
425            )
426        );
427        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
428    }
429
430    #[test]
431    fn setting_trap_to_ignore_with_override_ignore() {
432        let mut system = DummySystem::default();
433        let mut map = BTreeMap::new();
434        let entry = map.entry(SIGCHLD.into());
435        let origin = Location::dummy("origin");
436
437        let result =
438            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), true);
439        assert_eq!(result, Ok(()));
440        assert_eq!(
441            map[&SIGCHLD.into()].get_state(),
442            (
443                Some(&TrapState {
444                    action: Action::Ignore,
445                    origin,
446                    pending: false
447                }),
448                None
449            )
450        );
451        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
452    }
453
454    #[test]
455    fn setting_trap_to_command() {
456        let mut system = DummySystem::default();
457        let mut map = BTreeMap::new();
458        let entry = map.entry(SIGCHLD.into());
459        let action = Action::Command("echo".into());
460        let origin = Location::dummy("origin");
461
462        let result =
463            GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
464        assert_eq!(result, Ok(()));
465        assert_eq!(
466            map[&SIGCHLD.into()].get_state(),
467            (
468                Some(&TrapState {
469                    action,
470                    origin,
471                    pending: false
472                }),
473                None
474            )
475        );
476        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
477    }
478
479    #[test]
480    fn setting_trap_to_default() {
481        let mut system = DummySystem::default();
482        let mut map = BTreeMap::new();
483        let entry = map.entry(SIGCHLD.into());
484        let origin = Location::dummy("foo");
485        GrandState::set_action(&mut system, entry, Action::Ignore, origin, false).unwrap();
486
487        let entry = map.entry(SIGCHLD.into());
488        let origin = Location::dummy("bar");
489        let result =
490            GrandState::set_action(&mut system, entry, Action::Default, origin.clone(), false);
491        assert_eq!(result, Ok(()));
492        assert_eq!(
493            map[&SIGCHLD.into()].get_state(),
494            (
495                Some(&TrapState {
496                    action: Action::Default,
497                    origin,
498                    pending: false
499                }),
500                None
501            )
502        );
503        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
504    }
505
506    #[test]
507    fn resetting_trap_from_ignore_no_override() {
508        let mut system = DummySystem::default();
509        system.0.insert(SIGCHLD, Disposition::Ignore);
510        let mut map = BTreeMap::new();
511        let entry = map.entry(SIGCHLD.into());
512        let origin = Location::dummy("foo");
513        let result = GrandState::set_action(&mut system, entry, Action::Ignore, origin, false);
514        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
515
516        // Idempotence
517        let entry = map.entry(SIGCHLD.into());
518        let origin = Location::dummy("bar");
519        let result = GrandState::set_action(&mut system, entry, Action::Ignore, origin, false);
520        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
521
522        assert_eq!(map[&SIGCHLD.into()].get_state(), (None, None));
523        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
524    }
525
526    #[test]
527    fn resetting_trap_from_ignore_override() {
528        let mut system = DummySystem::default();
529        system.0.insert(SIGCHLD, Disposition::Ignore);
530        let mut map = BTreeMap::new();
531        let entry = map.entry(SIGCHLD.into());
532        let origin = Location::dummy("origin");
533        let result =
534            GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), true);
535        assert_eq!(result, Ok(()));
536        assert_eq!(
537            map[&SIGCHLD.into()].get_state(),
538            (
539                Some(&TrapState {
540                    action: Action::Ignore,
541                    origin,
542                    pending: false
543                }),
544                None
545            )
546        );
547        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
548    }
549
550    #[test]
551    fn internal_disposition_ignore() {
552        let mut system = DummySystem::default();
553        let mut map = BTreeMap::new();
554        let entry = map.entry(SIGCHLD.into());
555
556        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
557        assert_eq!(result, Ok(()));
558        assert_eq!(
559            map[&SIGCHLD.into()].internal_disposition(),
560            Disposition::Ignore
561        );
562        assert_eq!(map[&SIGCHLD.into()].get_state(), (None, None));
563        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
564    }
565
566    #[test]
567    fn internal_disposition_catch() {
568        let mut system = DummySystem::default();
569        let mut map = BTreeMap::new();
570        let entry = map.entry(SIGCHLD.into());
571
572        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch);
573        assert_eq!(result, Ok(()));
574        assert_eq!(
575            map[&SIGCHLD.into()].internal_disposition(),
576            Disposition::Catch
577        );
578        assert_eq!(map[&SIGCHLD.into()].get_state(), (None, None));
579        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
580    }
581
582    #[test]
583    fn action_ignore_and_internal_disposition_catch() {
584        let mut system = DummySystem::default();
585        let mut map = BTreeMap::new();
586        let entry = map.entry(SIGCHLD.into());
587        let origin = Location::dummy("origin");
588        let _ = GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false);
589        let entry = map.entry(SIGCHLD.into());
590
591        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch);
592        assert_eq!(result, Ok(()));
593        assert_eq!(
594            map[&SIGCHLD.into()].internal_disposition(),
595            Disposition::Catch
596        );
597        assert_matches!(map[&SIGCHLD.into()].get_state(), (Some(state), None) => {
598            assert_eq!(state.action, Action::Ignore);
599            assert_eq!(state.origin, origin);
600        });
601        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
602    }
603
604    #[test]
605    fn action_catch_and_internal_disposition_ignore() {
606        let mut system = DummySystem::default();
607        let mut map = BTreeMap::new();
608        let entry = map.entry(SIGCHLD.into());
609        let origin = Location::dummy("origin");
610        let action = Action::Command("echo".into());
611        let _ = GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
612        let entry = map.entry(SIGCHLD.into());
613
614        let result = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
615        assert_eq!(result, Ok(()));
616        assert_eq!(
617            map[&SIGCHLD.into()].internal_disposition(),
618            Disposition::Ignore
619        );
620        assert_matches!(map[&SIGCHLD.into()].get_state(), (Some(state), None) => {
621            assert_eq!(state.action, action);
622            assert_eq!(state.origin, origin);
623        });
624        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
625    }
626
627    #[test]
628    fn set_internal_disposition_for_initially_defaulted_signal_then_allow_override() {
629        let mut system = DummySystem::default();
630        let mut map = BTreeMap::new();
631        let entry = map.entry(SIGTTOU.into());
632        let _ = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
633        let entry = map.entry(SIGTTOU.into());
634        let origin = Location::dummy("origin");
635        let action = Action::Command("echo".into());
636
637        let result =
638            GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false);
639        assert_eq!(result, Ok(()));
640        assert_eq!(
641            map[&SIGTTOU.into()].internal_disposition(),
642            Disposition::Ignore
643        );
644        assert_eq!(
645            map[&SIGTTOU.into()].get_state(),
646            (
647                Some(&TrapState {
648                    action,
649                    origin,
650                    pending: false
651                }),
652                None
653            )
654        );
655        assert_eq!(system.0[&SIGTTOU], Disposition::Catch);
656    }
657
658    #[test]
659    fn set_internal_disposition_for_initially_ignored_signal_then_reject_override() {
660        let mut system = DummySystem::default();
661        system.0.insert(SIGTTOU, Disposition::Ignore);
662        let mut map = BTreeMap::new();
663        let cond = SIGTTOU.into();
664        let entry = map.entry(cond);
665        let _ = GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore);
666        let entry = map.entry(cond);
667        let origin = Location::dummy("origin");
668        let action = Action::Command("echo".into());
669
670        let result = GrandState::set_action(&mut system, entry, action, origin, false);
671        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
672        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
673        assert_eq!(map[&cond].get_state(), (None, None));
674        assert_eq!(system.0[&SIGTTOU], Disposition::Ignore);
675    }
676
677    #[test]
678    fn enter_subshell_with_internal_disposition_keeping_internal_disposition() {
679        let mut system = DummySystem::default();
680        let mut map = BTreeMap::new();
681        let cond = SIGCHLD.into();
682        GrandState::set_internal_disposition(&mut system, map.entry(cond), Disposition::Catch)
683            .unwrap();
684
685        let result = map.get_mut(&cond).unwrap().enter_subshell(
686            &mut system,
687            cond,
688            EnterSubshellOption::KeepInternalDisposition,
689        );
690        assert_eq!(result, Ok(()));
691        assert_eq!(map[&cond].internal_disposition(), Disposition::Catch);
692        assert_eq!(map[&cond].get_state(), (None, None));
693        assert_eq!(system.0[&SIGCHLD], Disposition::Catch);
694    }
695
696    #[test]
697    fn enter_subshell_with_internal_disposition_clearing_internal_disposition() {
698        let mut system = DummySystem::default();
699        let mut map = BTreeMap::new();
700        let cond = SIGCHLD.into();
701        let entry = map.entry(cond);
702        GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch).unwrap();
703
704        let result = map.get_mut(&cond).unwrap().enter_subshell(
705            &mut system,
706            cond,
707            EnterSubshellOption::ClearInternalDisposition,
708        );
709        assert_eq!(result, Ok(()));
710        assert_eq!(
711            map[&SIGCHLD.into()].internal_disposition(),
712            Disposition::Default
713        );
714        assert_eq!(map[&cond].get_state(), (None, None));
715        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
716    }
717
718    #[test]
719    fn enter_subshell_with_ignore_and_no_internal_disposition() {
720        let mut system = DummySystem::default();
721        let mut map = BTreeMap::new();
722        let cond = SIGCHLD.into();
723        let entry = map.entry(cond);
724        let origin = Location::dummy("foo");
725        GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false).unwrap();
726
727        let result = map.get_mut(&cond).unwrap().enter_subshell(
728            &mut system,
729            cond,
730            EnterSubshellOption::KeepInternalDisposition,
731        );
732        assert_eq!(result, Ok(()));
733        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
734        assert_eq!(
735            map[&cond].get_state(),
736            (
737                Some(&TrapState {
738                    action: Action::Ignore,
739                    origin,
740                    pending: false
741                }),
742                None
743            )
744        );
745        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
746    }
747
748    #[test]
749    fn enter_subshell_with_ignore_clearing_internal_disposition() {
750        let mut system = DummySystem::default();
751        let mut map = BTreeMap::new();
752        let cond = SIGCHLD.into();
753        let entry = map.entry(cond);
754        let origin = Location::dummy("foo");
755        GrandState::set_action(&mut system, entry, Action::Ignore, origin.clone(), false).unwrap();
756        let entry = map.entry(cond);
757        GrandState::set_internal_disposition(&mut system, entry, Disposition::Catch).unwrap();
758
759        let result = map.get_mut(&cond).unwrap().enter_subshell(
760            &mut system,
761            cond,
762            EnterSubshellOption::ClearInternalDisposition,
763        );
764        assert_eq!(result, Ok(()));
765        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
766        assert_eq!(
767            map[&cond].get_state(),
768            (
769                Some(&TrapState {
770                    action: Action::Ignore,
771                    origin,
772                    pending: false
773                }),
774                None
775            )
776        );
777        assert_eq!(system.0[&SIGCHLD], Disposition::Ignore);
778    }
779
780    #[test]
781    fn enter_subshell_with_command_and_no_internal_disposition() {
782        let mut system = DummySystem::default();
783        let mut map = BTreeMap::new();
784        let cond = SIGCHLD.into();
785        let entry = map.entry(cond);
786        let origin = Location::dummy("foo");
787        let action = Action::Command("echo".into());
788        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
789
790        let result = map.get_mut(&cond).unwrap().enter_subshell(
791            &mut system,
792            cond,
793            EnterSubshellOption::ClearInternalDisposition,
794        );
795        assert_eq!(result, Ok(()));
796        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
797        assert_eq!(
798            map[&cond].get_state(),
799            (
800                None,
801                Some(&TrapState {
802                    action,
803                    origin,
804                    pending: false
805                }),
806            )
807        );
808        assert_eq!(system.0[&SIGCHLD], Disposition::Default);
809    }
810
811    #[test]
812    fn enter_subshell_with_command_keeping_internal_disposition() {
813        let mut system = DummySystem::default();
814        let mut map = BTreeMap::new();
815        let cond = SIGTSTP.into();
816        let entry = map.entry(cond);
817        let origin = Location::dummy("foo");
818        let action = Action::Command("echo".into());
819        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
820        let entry = map.entry(cond);
821        GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore).unwrap();
822
823        let result = map.get_mut(&cond).unwrap().enter_subshell(
824            &mut system,
825            cond,
826            EnterSubshellOption::KeepInternalDisposition,
827        );
828        assert_eq!(result, Ok(()));
829        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
830        assert_eq!(
831            map[&cond].get_state(),
832            (
833                None,
834                Some(&TrapState {
835                    action,
836                    origin,
837                    pending: false
838                }),
839            )
840        );
841        assert_eq!(system.0[&SIGTSTP], Disposition::Ignore);
842    }
843
844    #[test]
845    fn enter_subshell_with_command_clearing_internal_disposition() {
846        let mut system = DummySystem::default();
847        let mut map = BTreeMap::new();
848        let cond = SIGTSTP.into();
849        let entry = map.entry(cond);
850        let origin = Location::dummy("foo");
851        let action = Action::Command("echo".into());
852        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
853        let entry = map.entry(cond);
854        GrandState::set_internal_disposition(&mut system, entry, Disposition::Ignore).unwrap();
855
856        let result = map.get_mut(&cond).unwrap().enter_subshell(
857            &mut system,
858            cond,
859            EnterSubshellOption::ClearInternalDisposition,
860        );
861        assert_eq!(result, Ok(()));
862        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
863        assert_eq!(
864            map[&cond].get_state(),
865            (
866                None,
867                Some(&TrapState {
868                    action,
869                    origin,
870                    pending: false
871                }),
872            )
873        );
874        assert_eq!(system.0[&SIGTSTP], Disposition::Default);
875    }
876
877    #[test]
878    fn enter_subshell_with_command_ignoring() {
879        let mut system = DummySystem::default();
880        let mut map = BTreeMap::new();
881        let cond = SIGQUIT.into();
882        let entry = map.entry(cond);
883        let origin = Location::dummy("foo");
884        let action = Action::Command("echo".into());
885        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
886
887        let result = map.get_mut(&cond).unwrap().enter_subshell(
888            &mut system,
889            cond,
890            EnterSubshellOption::Ignore,
891        );
892        assert_eq!(result, Ok(()));
893        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
894        assert_eq!(
895            map[&cond].get_state(),
896            (
897                None,
898                Some(&TrapState {
899                    action,
900                    origin,
901                    pending: false
902                }),
903            )
904        );
905        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
906    }
907
908    #[test]
909    fn ignoring_initially_defaulted_signal() {
910        let mut system = DummySystem::default();
911        let mut map = BTreeMap::new();
912        let cond = SIGQUIT.into();
913        let entry = map.entry(cond);
914        let vacant = assert_matches!(entry, Entry::Vacant(vacant) => vacant);
915
916        let result = GrandState::ignore(&mut system, vacant);
917        assert_eq!(result, Ok(()));
918        assert_eq!(map[&cond].get_state(), (None, None));
919        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
920
921        let entry = map.entry(cond);
922        let origin = Location::dummy("foo");
923        let action = Action::Command("echo".into());
924        let result = GrandState::set_action(&mut system, entry, action, origin, false);
925        assert_eq!(result, Ok(()));
926    }
927
928    #[test]
929    fn ignoring_initially_ignored_signal() {
930        let mut system = DummySystem::default();
931        system.0.insert(SIGQUIT, Disposition::Ignore);
932        let mut map = BTreeMap::new();
933        let cond = SIGQUIT.into();
934        let entry = map.entry(cond);
935        let vacant = assert_matches!(entry, Entry::Vacant(vacant) => vacant);
936
937        let result = GrandState::ignore(&mut system, vacant);
938        assert_eq!(result, Ok(()));
939        assert_eq!(map[&cond].get_state(), (None, None));
940        assert_eq!(system.0[&SIGQUIT], Disposition::Ignore);
941
942        let entry = map.entry(cond);
943        let origin = Location::dummy("foo");
944        let action = Action::Command("echo".into());
945        let result = GrandState::set_action(&mut system, entry, action, origin, false);
946        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
947    }
948
949    #[test]
950    fn clearing_parent_setting() {
951        let mut system = DummySystem::default();
952        let mut map = BTreeMap::new();
953        let cond = SIGCHLD.into();
954        let entry = map.entry(cond);
955        let origin = Location::dummy("foo");
956        let action = Action::Command("echo".into());
957        GrandState::set_action(&mut system, entry, action, origin, false).unwrap();
958        let state = map.get_mut(&cond).unwrap();
959        state
960            .enter_subshell(
961                &mut system,
962                cond,
963                EnterSubshellOption::ClearInternalDisposition,
964            )
965            .unwrap();
966
967        state.clear_parent_setting();
968        assert_eq!(state.get_state(), (None, None));
969    }
970
971    #[test]
972    fn marking_as_caught_and_handling() {
973        let mut system = DummySystem::default();
974        let mut map = BTreeMap::new();
975        let cond = SIGUSR1.into();
976        let entry = map.entry(cond);
977        let origin = Location::dummy("foo");
978        let action = Action::Command("echo".into());
979        GrandState::set_action(&mut system, entry, action.clone(), origin.clone(), false).unwrap();
980
981        let state = &mut map.get_mut(&cond).unwrap();
982        state.mark_as_caught();
983        let expected_trap = TrapState {
984            action,
985            origin,
986            pending: true,
987        };
988        assert_eq!(state.get_state(), (Some(&expected_trap), None));
989
990        let trap = state.handle_if_caught();
991        let expected_trap = TrapState {
992            pending: false,
993            ..expected_trap
994        };
995        assert_eq!(trap, Some(&expected_trap));
996
997        let trap = state.handle_if_caught();
998        assert_eq!(trap, None);
999    }
1000}