Skip to main content

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::source::Location;
23use crate::system::{Disposition, Errno};
24use std::collections::btree_map::{Entry, VacantEntry};
25use std::rc::Rc;
26use thiserror::Error;
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/// Origin of the current trap action
76///
77/// The `Origin` enum indicates how the current trap action was set.
78#[derive(Clone, Debug, Default, Eq, PartialEq)]
79pub enum Origin {
80    /// The current trap action was inherited from the previous process that
81    /// `exec`ed the shell.
82    ///
83    /// This is the default value.
84    #[default]
85    Inherited,
86
87    /// The current trap action was set by the shell when entering a subshell.
88    Subshell,
89
90    /// The current trap action was set by the user.
91    ///
92    /// The location indicates the simple command that invoked the trap built-in
93    /// that set the current action.
94    User(Location),
95}
96
97/// State of the trap action for a condition
98#[derive(Clone, Debug, Default, Eq, PartialEq)]
99pub struct TrapState {
100    /// Action taken when the condition is met
101    pub action: Action,
102
103    /// Origin of the current action
104    pub origin: Origin,
105
106    /// True iff a signal specified by the condition has been caught and the
107    /// action command has not yet executed.
108    pub pending: bool,
109}
110
111impl TrapState {
112    fn from_initial_disposition(disposition: Disposition) -> Self {
113        let action = match disposition {
114            Disposition::Default => Action::Default,
115            Disposition::Ignore => Action::Ignore,
116            // The Rust runtime installs a handler for SIGSEGV and SIGBUS before
117            // the shell is initialized. We should treat these signals as if
118            // they have the default disposition.
119            // Disposition::Catch => panic!("initial disposition cannot be `Catch`"),
120            Disposition::Catch => Action::Default,
121        };
122        TrapState {
123            action,
124            origin: Origin::Inherited,
125            pending: false,
126        }
127    }
128}
129
130/// Option for [`GrandState::enter_subshell`]
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub enum EnterSubshellOption {
133    /// Keeps the current internal disposition configuration.
134    KeepInternalDisposition,
135    /// Resets the internal disposition configuration to the default.
136    ClearInternalDisposition,
137    /// Resets the internal disposition configuration to the default and sets the
138    /// signal disposition to `Ignore`.
139    Ignore,
140}
141
142/// Whole configuration and state for a trap condition
143#[derive(Clone, Debug)]
144pub struct GrandState {
145    /// Current trap state
146    current_state: TrapState,
147
148    /// Trap state that was effective in the parent environment
149    parent_state: Option<TrapState>,
150
151    /// Current internal disposition
152    ///
153    /// The internal disposition is the signal disposition that is set by the
154    /// shell itself, not by the user. This is used to handle some specific
155    /// signals like `SIGCHLD` and `SIGTSTP`. When the user sets a trap for
156    /// these signals, the actual signal disposition registered in the system
157    /// is computed as the maximum of the user-defined disposition and the
158    /// internal disposition.
159    internal_disposition: Disposition,
160}
161
162impl GrandState {
163    /// Returns the current trap state.
164    #[inline]
165    #[must_use]
166    pub fn current_state(&self) -> &TrapState {
167        &self.current_state
168    }
169
170    /// Returns the parent trap state.
171    ///
172    /// This is the trap state that was effective in the parent environment
173    /// before the current environment was created as a subshell of the parent.
174    /// Returns `None` if the current environment is not a subshell or the parent
175    /// state was [cleared](Self::clear_parent_state).
176    #[inline]
177    #[must_use]
178    pub fn parent_state(&self) -> Option<&TrapState> {
179        self.parent_state.as_ref()
180    }
181
182    /// Clears the parent trap state.
183    pub fn clear_parent_state(&mut self) {
184        self.parent_state = None;
185    }
186
187    /// Inserts a new entry if the entry is vacant.
188    ///
189    /// If the condition is a signal, the new entry is initialized with the
190    /// current signal disposition obtained from the system.
191    pub fn insert_from_system_if_vacant<'a, S: SignalSystem>(
192        system: &S,
193        entry: Entry<'a, Condition, GrandState>,
194    ) -> Result<&'a GrandState, Errno> {
195        match entry {
196            Entry::Vacant(vacant) => {
197                let disposition = match *vacant.key() {
198                    Condition::Signal(signal) => system.get_disposition(signal)?,
199                    Condition::Exit => Disposition::Default,
200                };
201                let state = GrandState {
202                    current_state: TrapState::from_initial_disposition(disposition),
203                    parent_state: None,
204                    internal_disposition: Disposition::Default,
205                };
206                Ok(vacant.insert(state))
207            }
208
209            Entry::Occupied(occupied) => Ok(occupied.into_mut()),
210        }
211    }
212
213    /// Updates the entry with the new action.
214    pub async fn set_action<S: SignalSystem>(
215        system: &S,
216        entry: Entry<'_, Condition, GrandState>,
217        action: Action,
218        origin: Location,
219        override_ignore: bool,
220    ) -> Result<(), SetActionError> {
221        let cond = *entry.key();
222        let disposition = (&action).into();
223        let new_state = TrapState {
224            action,
225            origin: Origin::User(origin),
226            pending: false,
227        };
228
229        match entry {
230            Entry::Vacant(vacant) => {
231                if let Condition::Signal(signal) = cond {
232                    if !override_ignore {
233                        let initial_disposition =
234                            system.set_disposition(signal, Disposition::Ignore).await?;
235                        if initial_disposition == Disposition::Ignore {
236                            vacant.insert(GrandState {
237                                current_state: TrapState::from_initial_disposition(
238                                    initial_disposition,
239                                ),
240                                parent_state: None,
241                                internal_disposition: Disposition::Default,
242                            });
243                            return Err(SetActionError::InitiallyIgnored);
244                        }
245                    }
246
247                    if override_ignore || disposition != Disposition::Ignore {
248                        system.set_disposition(signal, disposition).await?;
249                    }
250                }
251
252                vacant.insert(GrandState {
253                    current_state: new_state,
254                    parent_state: None,
255                    internal_disposition: Disposition::Default,
256                });
257            }
258
259            Entry::Occupied(mut occupied) => {
260                let state = occupied.get_mut();
261                if !override_ignore
262                    && state.current_state.action == Action::Ignore
263                    && state.current_state.origin == Origin::Inherited
264                {
265                    return Err(SetActionError::InitiallyIgnored);
266                }
267
268                if let Condition::Signal(signal) = cond {
269                    let internal = state.internal_disposition;
270                    let old_disposition = internal.max((&state.current_state.action).into());
271                    let new_disposition = internal.max(disposition);
272                    if old_disposition != new_disposition {
273                        system.set_disposition(signal, new_disposition).await?;
274                    }
275                }
276
277                state.current_state = new_state;
278            }
279        }
280
281        Ok(())
282    }
283
284    /// Returns the current internal disposition.
285    #[must_use]
286    pub fn internal_disposition(&self) -> Disposition {
287        self.internal_disposition
288    }
289
290    /// Sets the internal disposition.
291    ///
292    /// The condition of the given entry must be a signal, or this function
293    /// panics.
294    pub async fn set_internal_disposition<S: SignalSystem>(
295        system: &S,
296        entry: Entry<'_, Condition, GrandState>,
297        disposition: Disposition,
298    ) -> Result<(), Errno> {
299        let signal = match *entry.key() {
300            Condition::Signal(signal) => signal,
301            Condition::Exit => panic!("exit condition cannot have an internal disposition"),
302        };
303
304        match entry {
305            Entry::Vacant(_) if disposition == Disposition::Default => (),
306
307            Entry::Vacant(vacant) => {
308                let initial_disposition = system.set_disposition(signal, disposition).await?;
309                vacant.insert(GrandState {
310                    current_state: TrapState::from_initial_disposition(initial_disposition),
311                    parent_state: None,
312                    internal_disposition: disposition,
313                });
314            }
315
316            Entry::Occupied(mut occupied) => {
317                let state = occupied.get_mut();
318                let setting = (&state.current_state.action).into();
319                let old_disposition = state.internal_disposition.max(setting);
320                let new_disposition = disposition.max(setting);
321                if old_disposition != new_disposition {
322                    system.set_disposition(signal, new_disposition).await?;
323                }
324                state.internal_disposition = disposition;
325            }
326        }
327
328        Ok(())
329    }
330
331    /// Updates the trap states and gets ready for executing the body a
332    /// subshell.
333    ///
334    /// If the current state has a user-specified command
335    /// (`Action::Command(_)`), it is saved in the parent state and reset to the
336    /// default. Additionally, the signal disposition is updated depending on the
337    /// `option`.
338    pub async fn enter_subshell<S: SignalSystem>(
339        &mut self,
340        system: &S,
341        cond: Condition,
342        option: EnterSubshellOption,
343    ) -> Result<(), Errno> {
344        let old_setting = (&self.current_state.action).into();
345        let old_disposition = self.internal_disposition.max(old_setting);
346
347        if matches!(self.current_state.action, Action::Command(_)) {
348            self.parent_state = Some(std::mem::replace(
349                &mut self.current_state,
350                TrapState {
351                    action: Action::Default,
352                    origin: Origin::Subshell,
353                    pending: false,
354                },
355            ));
356        }
357        if option == EnterSubshellOption::Ignore {
358            self.current_state.action = Action::Ignore;
359        }
360
361        let new_setting = (&self.current_state.action).into();
362        let new_disposition = match option {
363            EnterSubshellOption::KeepInternalDisposition => {
364                self.internal_disposition.max(new_setting)
365            }
366            EnterSubshellOption::ClearInternalDisposition => new_setting,
367            EnterSubshellOption::Ignore => Disposition::Ignore,
368        };
369        if old_disposition != new_disposition
370            && let Condition::Signal(signal) = cond
371        {
372            system.set_disposition(signal, new_disposition).await?;
373        }
374        self.internal_disposition = match option {
375            EnterSubshellOption::KeepInternalDisposition => self.internal_disposition,
376            EnterSubshellOption::ClearInternalDisposition | EnterSubshellOption::Ignore => {
377                Disposition::Default
378            }
379        };
380        Ok(())
381    }
382
383    /// Sets the disposition to `Ignore` for the given signal condition.
384    ///
385    /// This function creates a new `GrandState` entry with the current state
386    /// having `Action::Ignore`. If the signal disposition is default, the shell
387    /// sets the signal disposition to `Ignore` and the origin to `Subshell`. If
388    /// the signal disposition is already `Ignore`, the origin is set to
389    /// `Inherited` to disallow changing the action in a non-interactive shell.
390    ///
391    /// You should call this function in place of [`Self::enter_subshell`] if
392    /// there is no entry for the condition yet.
393    ///
394    /// This function panics if the condition is not a signal.
395    pub async fn ignore<S: SignalSystem>(
396        system: &S,
397        vacant: VacantEntry<'_, Condition, GrandState>,
398    ) -> Result<(), Errno> {
399        let signal = match *vacant.key() {
400            Condition::Signal(signal) => signal,
401            Condition::Exit => panic!("exit condition cannot be ignored"),
402        };
403        let initial_disposition = system.set_disposition(signal, Disposition::Ignore).await?;
404        let origin = match initial_disposition {
405            Disposition::Default => Origin::Subshell,
406            Disposition::Ignore => Origin::Inherited,
407            // The Rust runtime installs a handler for SIGSEGV and SIGBUS before
408            // the shell is initialized. We should treat these signals as if
409            // they have the default disposition (though the ignore function is
410            // normally not called for these signals).
411            // Disposition::Catch => panic!("initial disposition cannot be `Catch`"),
412            Disposition::Catch => Origin::Subshell,
413        };
414        vacant.insert(GrandState {
415            current_state: TrapState {
416                action: Action::Ignore,
417                origin,
418                pending: false,
419            },
420            parent_state: None,
421            internal_disposition: Disposition::Default,
422        });
423        Ok(())
424    }
425
426    /// Marks this signal as caught.
427    pub fn mark_as_caught(&mut self) {
428        self.current_state.pending = true;
429    }
430
431    /// Clears the mark of this signal being caught and returns the trap state.
432    pub fn handle_if_caught(&mut self) -> Option<&TrapState> {
433        if self.current_state.pending {
434            self.current_state.pending = false;
435            Some(&self.current_state)
436        } else {
437            None
438        }
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::super::tests::DummySystem;
445    use super::*;
446    use crate::system::Signals;
447    use crate::system::r#virtual::{
448        SIGABRT, SIGALRM, SIGBUS, SIGCHLD, SIGCONT, SIGFPE, SIGHUP, SIGILL, SIGINT, SIGIOT,
449        SIGKILL, SIGPIPE, SIGPROF, SIGQUIT, SIGSEGV, SIGSTOP, SIGSYS, SIGTERM, SIGTRAP, SIGTSTP,
450        SIGTTIN, SIGTTOU, SIGURG, SIGUSR1, SIGUSR2, SIGVTALRM, SIGWINCH, SIGXCPU, SIGXFSZ,
451    };
452    use futures_util::FutureExt as _;
453    use std::borrow::Cow;
454    use std::{collections::BTreeMap, ops::RangeInclusive};
455
456    struct UnusedSystem;
457
458    impl Signals for UnusedSystem {
459        const SIGABRT: crate::signal::Number = SIGABRT;
460        const SIGALRM: crate::signal::Number = SIGALRM;
461        const SIGBUS: crate::signal::Number = SIGBUS;
462        const SIGCHLD: crate::signal::Number = SIGCHLD;
463        const SIGCLD: Option<crate::signal::Number> = None;
464        const SIGCONT: crate::signal::Number = SIGCONT;
465        const SIGEMT: Option<crate::signal::Number> = None;
466        const SIGFPE: crate::signal::Number = SIGFPE;
467        const SIGHUP: crate::signal::Number = SIGHUP;
468        const SIGILL: crate::signal::Number = SIGILL;
469        const SIGINFO: Option<crate::signal::Number> = None;
470        const SIGINT: crate::signal::Number = SIGINT;
471        const SIGIO: Option<crate::signal::Number> = None;
472        const SIGIOT: crate::signal::Number = SIGIOT;
473        const SIGKILL: crate::signal::Number = SIGKILL;
474        const SIGLOST: Option<crate::signal::Number> = None;
475        const SIGPIPE: crate::signal::Number = SIGPIPE;
476        const SIGPOLL: Option<crate::signal::Number> = None;
477        const SIGPROF: crate::signal::Number = SIGPROF;
478        const SIGPWR: Option<crate::signal::Number> = None;
479        const SIGQUIT: crate::signal::Number = SIGQUIT;
480        const SIGSEGV: crate::signal::Number = SIGSEGV;
481        const SIGSTKFLT: Option<crate::signal::Number> = None;
482        const SIGSTOP: crate::signal::Number = SIGSTOP;
483        const SIGSYS: crate::signal::Number = SIGSYS;
484        const SIGTERM: crate::signal::Number = SIGTERM;
485        const SIGTHR: Option<crate::signal::Number> = None;
486        const SIGTRAP: crate::signal::Number = SIGTRAP;
487        const SIGTSTP: crate::signal::Number = SIGTSTP;
488        const SIGTTIN: crate::signal::Number = SIGTTIN;
489        const SIGTTOU: crate::signal::Number = SIGTTOU;
490        const SIGURG: crate::signal::Number = SIGURG;
491        const SIGUSR1: crate::signal::Number = SIGUSR1;
492        const SIGUSR2: crate::signal::Number = SIGUSR2;
493        const SIGVTALRM: crate::signal::Number = SIGVTALRM;
494        const SIGWINCH: crate::signal::Number = SIGWINCH;
495        const SIGXCPU: crate::signal::Number = SIGXCPU;
496        const SIGXFSZ: crate::signal::Number = SIGXFSZ;
497
498        fn sigrt_range(&self) -> Option<RangeInclusive<crate::signal::Number>> {
499            unreachable!()
500        }
501        fn iter_sigrt(&self) -> impl DoubleEndedIterator<Item = crate::signal::Number> + use<> {
502            unreachable!() as std::iter::Empty<_>
503        }
504        fn sig2str<S: Into<crate::signal::RawNumber>>(
505            &self,
506            signal: S,
507        ) -> Option<Cow<'static, str>> {
508            unreachable!("sig2str({:?})", signal.into())
509        }
510        fn str2sig(&self, name: &str) -> Option<crate::signal::Number> {
511            unreachable!("str2sig({name:?})")
512        }
513        fn validate_signal(
514            &self,
515            number: crate::signal::RawNumber,
516        ) -> Option<(crate::signal::Name, crate::signal::Number)> {
517            unreachable!("validate_signal({number:?})")
518        }
519        fn signal_name_from_number(&self, number: crate::signal::Number) -> crate::signal::Name {
520            unreachable!("signal_name_from_number({number:?})")
521        }
522        fn signal_number_from_name(
523            &self,
524            name: crate::signal::Name,
525        ) -> Option<crate::signal::Number> {
526            unreachable!("signal_number_from_name({name:?})")
527        }
528    }
529
530    impl SignalSystem for UnusedSystem {
531        fn get_disposition(&self, signal: crate::signal::Number) -> Result<Disposition, Errno> {
532            unreachable!("get_disposition({signal})")
533        }
534        fn set_disposition(
535            &self,
536            signal: crate::signal::Number,
537            disposition: Disposition,
538        ) -> impl Future<Output = Result<Disposition, Errno>> + use<> {
539            unreachable!("set_disposition({signal}, {disposition:?})") as std::future::Ready<_>
540        }
541    }
542
543    #[test]
544    fn insertion_with_default_inherited_disposition() {
545        let system = DummySystem::default();
546        let mut map = BTreeMap::new();
547        let entry = map.entry(SIGCHLD.into());
548        let state = GrandState::insert_from_system_if_vacant(&system, entry).unwrap();
549        assert_eq!(
550            state.current_state(),
551            &TrapState {
552                action: Action::Default,
553                origin: Origin::Inherited,
554                pending: false,
555            }
556        );
557        assert_eq!(state.parent_state(), None);
558        assert_eq!(state.internal_disposition(), Disposition::Default);
559    }
560
561    #[test]
562    fn insertion_with_inherited_disposition_of_ignore() {
563        let system = DummySystem::default();
564        system.0.borrow_mut().insert(SIGCHLD, Disposition::Ignore);
565        let mut map = BTreeMap::new();
566        let entry = map.entry(SIGCHLD.into());
567        let state = GrandState::insert_from_system_if_vacant(&system, entry).unwrap();
568        assert_eq!(
569            state.current_state(),
570            &TrapState {
571                action: Action::Ignore,
572                origin: Origin::Inherited,
573                pending: false,
574            }
575        );
576        assert_eq!(state.parent_state(), None);
577        assert_eq!(state.internal_disposition(), Disposition::Default);
578    }
579
580    #[test]
581    fn insertion_with_occupied_entry() {
582        let system = DummySystem::default();
583        let mut map = BTreeMap::new();
584        let entry = map.entry(SIGCHLD.into());
585        let origin = Location::dummy("origin");
586        let action = Action::Command("echo".into());
587        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
588            .now_or_never()
589            .unwrap()
590            .unwrap();
591
592        // If the entry is occupied, the function should return the existing
593        // state without accessing the system.
594        let entry = map.entry(SIGCHLD.into());
595        let state = GrandState::insert_from_system_if_vacant(&UnusedSystem, entry).unwrap();
596        assert_eq!(
597            state.current_state(),
598            &TrapState {
599                action,
600                origin: Origin::User(origin),
601                pending: false,
602            }
603        );
604        assert_eq!(state.parent_state(), None);
605        assert_eq!(state.internal_disposition(), Disposition::Default);
606    }
607
608    #[test]
609    fn insertion_with_non_signal_condition() {
610        let mut map = BTreeMap::new();
611        let entry = map.entry(Condition::Exit);
612        let state = GrandState::insert_from_system_if_vacant(&UnusedSystem, entry).unwrap();
613        assert_eq!(
614            state.current_state(),
615            &TrapState {
616                action: Action::Default,
617                origin: Origin::Inherited,
618                pending: false,
619            }
620        );
621        assert_eq!(state.parent_state(), None);
622        assert_eq!(state.internal_disposition(), Disposition::Default);
623    }
624
625    #[test]
626    fn setting_trap_to_ignore_without_override_ignore() {
627        let system = DummySystem::default();
628        let mut map = BTreeMap::new();
629        let entry = map.entry(SIGCHLD.into());
630        let origin = Location::dummy("origin");
631
632        let result = GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), false)
633            .now_or_never()
634            .unwrap();
635        assert_eq!(result, Ok(()));
636        assert_eq!(
637            map[&SIGCHLD.into()].current_state(),
638            &TrapState {
639                action: Action::Ignore,
640                origin: Origin::User(origin),
641                pending: false
642            }
643        );
644        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
645        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
646    }
647
648    #[test]
649    fn setting_trap_to_ignore_with_override_ignore() {
650        let system = DummySystem::default();
651        let mut map = BTreeMap::new();
652        let entry = map.entry(SIGCHLD.into());
653        let origin = Location::dummy("origin");
654
655        let result = GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), true)
656            .now_or_never()
657            .unwrap();
658        assert_eq!(result, Ok(()));
659        assert_eq!(
660            map[&SIGCHLD.into()].current_state(),
661            &TrapState {
662                action: Action::Ignore,
663                origin: Origin::User(origin),
664                pending: false
665            }
666        );
667        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
668        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
669    }
670
671    #[test]
672    fn setting_trap_to_command() {
673        let system = DummySystem::default();
674        let mut map = BTreeMap::new();
675        let entry = map.entry(SIGCHLD.into());
676        let action = Action::Command("echo".into());
677        let origin = Location::dummy("origin");
678
679        let result = GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
680            .now_or_never()
681            .unwrap();
682        assert_eq!(result, Ok(()));
683        assert_eq!(
684            map[&SIGCHLD.into()].current_state(),
685            &TrapState {
686                action,
687                origin: Origin::User(origin),
688                pending: false
689            }
690        );
691        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
692        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Catch);
693    }
694
695    #[test]
696    fn setting_trap_to_default() {
697        let system = DummySystem::default();
698        let mut map = BTreeMap::new();
699        let entry = map.entry(SIGCHLD.into());
700        let origin = Location::dummy("foo");
701        GrandState::set_action(&system, entry, Action::Ignore, origin, false)
702            .now_or_never()
703            .unwrap()
704            .unwrap();
705
706        let entry = map.entry(SIGCHLD.into());
707        let origin = Location::dummy("bar");
708        let result = GrandState::set_action(&system, entry, Action::Default, origin.clone(), false)
709            .now_or_never()
710            .unwrap();
711        assert_eq!(result, Ok(()));
712        assert_eq!(
713            map[&SIGCHLD.into()].current_state(),
714            &TrapState {
715                action: Action::Default,
716                origin: Origin::User(origin),
717                pending: false
718            }
719        );
720        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
721        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Default);
722    }
723
724    #[test]
725    fn resetting_trap_from_ignore_no_override() {
726        let system = DummySystem::default();
727        system.0.borrow_mut().insert(SIGCHLD, Disposition::Ignore);
728        let mut map = BTreeMap::new();
729        let entry = map.entry(SIGCHLD.into());
730        let origin = Location::dummy("foo");
731        let result = GrandState::set_action(&system, entry, Action::Ignore, origin, false)
732            .now_or_never()
733            .unwrap();
734        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
735
736        // Idempotence
737        let entry = map.entry(SIGCHLD.into());
738        let origin = Location::dummy("bar");
739        let result = GrandState::set_action(&system, entry, Action::Ignore, origin, false)
740            .now_or_never()
741            .unwrap();
742        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
743
744        assert_eq!(
745            map[&SIGCHLD.into()].current_state(),
746            &TrapState {
747                action: Action::Ignore,
748                origin: Origin::Inherited,
749                pending: false
750            }
751        );
752        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
753        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
754    }
755
756    #[test]
757    fn resetting_trap_from_ignore_override() {
758        let system = DummySystem::default();
759        system.0.borrow_mut().insert(SIGCHLD, Disposition::Ignore);
760        let mut map = BTreeMap::new();
761        let entry = map.entry(SIGCHLD.into());
762        let origin = Location::dummy("origin");
763        let result = GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), true)
764            .now_or_never()
765            .unwrap();
766        assert_eq!(result, Ok(()));
767        assert_eq!(
768            map[&SIGCHLD.into()].current_state(),
769            &TrapState {
770                action: Action::Ignore,
771                origin: Origin::User(origin),
772                pending: false
773            }
774        );
775        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
776        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
777    }
778
779    #[test]
780    fn internal_disposition_ignore() {
781        let system = DummySystem::default();
782        let mut map = BTreeMap::new();
783        let entry = map.entry(SIGCHLD.into());
784
785        let result = GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
786            .now_or_never()
787            .unwrap();
788        assert_eq!(result, Ok(()));
789        assert_eq!(
790            map[&SIGCHLD.into()].internal_disposition(),
791            Disposition::Ignore
792        );
793        assert_eq!(
794            map[&SIGCHLD.into()].current_state(),
795            &TrapState {
796                action: Action::Default,
797                origin: Origin::Inherited,
798                pending: false
799            }
800        );
801        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
802        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
803    }
804
805    #[test]
806    fn internal_disposition_catch() {
807        let system = DummySystem::default();
808        let mut map = BTreeMap::new();
809        let entry = map.entry(SIGCHLD.into());
810
811        let result = GrandState::set_internal_disposition(&system, entry, Disposition::Catch)
812            .now_or_never()
813            .unwrap();
814        assert_eq!(result, Ok(()));
815        assert_eq!(
816            map[&SIGCHLD.into()].internal_disposition(),
817            Disposition::Catch
818        );
819        assert_eq!(
820            map[&SIGCHLD.into()].current_state(),
821            &TrapState {
822                action: Action::Default,
823                origin: Origin::Inherited,
824                pending: false
825            }
826        );
827        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
828        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Catch);
829    }
830
831    #[test]
832    fn action_ignore_and_internal_disposition_catch() {
833        let system = DummySystem::default();
834        let mut map = BTreeMap::new();
835        let entry = map.entry(SIGCHLD.into());
836        let origin = Location::dummy("origin");
837        GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), false)
838            .now_or_never()
839            .unwrap()
840            .unwrap();
841        let entry = map.entry(SIGCHLD.into());
842
843        let result = GrandState::set_internal_disposition(&system, entry, Disposition::Catch)
844            .now_or_never()
845            .unwrap();
846        assert_eq!(result, Ok(()));
847        assert_eq!(
848            map[&SIGCHLD.into()].internal_disposition(),
849            Disposition::Catch
850        );
851        assert_eq!(
852            map[&SIGCHLD.into()].current_state(),
853            &TrapState {
854                action: Action::Ignore,
855                origin: Origin::User(origin),
856                pending: false
857            }
858        );
859        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
860        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Catch);
861    }
862
863    #[test]
864    fn action_catch_and_internal_disposition_ignore() {
865        let system = DummySystem::default();
866        let mut map = BTreeMap::new();
867        let entry = map.entry(SIGCHLD.into());
868        let origin = Location::dummy("origin");
869        let action = Action::Command("echo".into());
870        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
871            .now_or_never()
872            .unwrap()
873            .unwrap();
874        let entry = map.entry(SIGCHLD.into());
875
876        let result = GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
877            .now_or_never()
878            .unwrap();
879        assert_eq!(result, Ok(()));
880        assert_eq!(
881            map[&SIGCHLD.into()].internal_disposition(),
882            Disposition::Ignore
883        );
884        assert_eq!(
885            map[&SIGCHLD.into()].current_state(),
886            &TrapState {
887                action,
888                origin: Origin::User(origin),
889                pending: false
890            }
891        );
892        assert_eq!(map[&SIGCHLD.into()].parent_state(), None);
893        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Catch);
894    }
895
896    #[test]
897    fn set_internal_disposition_for_initially_defaulted_signal_then_allow_override() {
898        let system = DummySystem::default();
899        let mut map = BTreeMap::new();
900        let entry = map.entry(SIGTTOU.into());
901        GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
902            .now_or_never()
903            .unwrap()
904            .unwrap();
905        let entry = map.entry(SIGTTOU.into());
906        let origin = Location::dummy("origin");
907        let action = Action::Command("echo".into());
908
909        let result = GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
910            .now_or_never()
911            .unwrap();
912        assert_eq!(result, Ok(()));
913        assert_eq!(
914            map[&SIGTTOU.into()].internal_disposition(),
915            Disposition::Ignore
916        );
917        assert_eq!(
918            map[&SIGTTOU.into()].current_state(),
919            &TrapState {
920                action,
921                origin: Origin::User(origin),
922                pending: false
923            }
924        );
925        assert_eq!(map[&SIGTTOU.into()].parent_state(), None);
926        assert_eq!(system.0.borrow()[&SIGTTOU], Disposition::Catch);
927    }
928
929    #[test]
930    fn set_internal_disposition_for_initially_ignored_signal_then_reject_override() {
931        let system = DummySystem::default();
932        system.0.borrow_mut().insert(SIGTTOU, Disposition::Ignore);
933        let mut map = BTreeMap::new();
934        let cond = SIGTTOU.into();
935        let entry = map.entry(cond);
936        GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
937            .now_or_never()
938            .unwrap()
939            .unwrap();
940        let entry = map.entry(cond);
941        let origin = Location::dummy("origin");
942        let action = Action::Command("echo".into());
943
944        let result = GrandState::set_action(&system, entry, action, origin, false)
945            .now_or_never()
946            .unwrap();
947        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
948        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
949        assert_eq!(
950            map[&cond].current_state(),
951            &TrapState {
952                action: Action::Ignore,
953                origin: Origin::Inherited,
954                pending: false
955            }
956        );
957        assert_eq!(map[&cond].parent_state(), None);
958        assert_eq!(system.0.borrow()[&SIGTTOU], Disposition::Ignore);
959    }
960
961    #[test]
962    fn enter_subshell_with_internal_disposition_keeping_internal_disposition() {
963        let system = DummySystem::default();
964        let mut map = BTreeMap::new();
965        let cond = SIGCHLD.into();
966        GrandState::set_internal_disposition(&system, map.entry(cond), Disposition::Catch)
967            .now_or_never()
968            .unwrap()
969            .unwrap();
970
971        let result = map
972            .get_mut(&cond)
973            .unwrap()
974            .enter_subshell(&system, cond, EnterSubshellOption::KeepInternalDisposition)
975            .now_or_never()
976            .unwrap();
977        assert_eq!(result, Ok(()));
978        assert_eq!(map[&cond].internal_disposition(), Disposition::Catch);
979        assert_eq!(
980            map[&cond].current_state(),
981            &TrapState {
982                action: Action::Default,
983                origin: Origin::Inherited,
984                pending: false
985            }
986        );
987        assert_eq!(map[&cond].parent_state(), None);
988        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Catch);
989    }
990
991    #[test]
992    fn enter_subshell_with_internal_disposition_clearing_internal_disposition() {
993        let system = DummySystem::default();
994        let mut map = BTreeMap::new();
995        let cond = SIGCHLD.into();
996        let entry = map.entry(cond);
997        GrandState::set_internal_disposition(&system, entry, Disposition::Catch)
998            .now_or_never()
999            .unwrap()
1000            .unwrap();
1001
1002        let result = map
1003            .get_mut(&cond)
1004            .unwrap()
1005            .enter_subshell(&system, cond, EnterSubshellOption::ClearInternalDisposition)
1006            .now_or_never()
1007            .unwrap();
1008        assert_eq!(result, Ok(()));
1009        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1010        assert_eq!(
1011            map[&cond].current_state(),
1012            &TrapState {
1013                action: Action::Default,
1014                origin: Origin::Inherited,
1015                pending: false
1016            }
1017        );
1018        assert_eq!(map[&cond].parent_state(), None);
1019        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Default);
1020    }
1021
1022    #[test]
1023    fn enter_subshell_with_ignore_and_no_internal_disposition() {
1024        let system = DummySystem::default();
1025        let mut map = BTreeMap::new();
1026        let cond = SIGCHLD.into();
1027        let entry = map.entry(cond);
1028        let origin = Location::dummy("foo");
1029        GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), false)
1030            .now_or_never()
1031            .unwrap()
1032            .unwrap();
1033
1034        let result = map
1035            .get_mut(&cond)
1036            .unwrap()
1037            .enter_subshell(&system, cond, EnterSubshellOption::KeepInternalDisposition)
1038            .now_or_never()
1039            .unwrap();
1040        assert_eq!(result, Ok(()));
1041        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1042        assert_eq!(
1043            map[&cond].current_state(),
1044            &TrapState {
1045                action: Action::Ignore,
1046                origin: Origin::User(origin),
1047                pending: false
1048            }
1049        );
1050        assert_eq!(map[&cond].parent_state(), None);
1051        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
1052    }
1053
1054    #[test]
1055    fn enter_subshell_with_ignore_clearing_internal_disposition() {
1056        let system = DummySystem::default();
1057        let mut map = BTreeMap::new();
1058        let cond = SIGCHLD.into();
1059        let entry = map.entry(cond);
1060        let origin = Location::dummy("foo");
1061        GrandState::set_action(&system, entry, Action::Ignore, origin.clone(), false)
1062            .now_or_never()
1063            .unwrap()
1064            .unwrap();
1065        let entry = map.entry(cond);
1066        GrandState::set_internal_disposition(&system, entry, Disposition::Catch)
1067            .now_or_never()
1068            .unwrap()
1069            .unwrap();
1070
1071        let result = map
1072            .get_mut(&cond)
1073            .unwrap()
1074            .enter_subshell(&system, cond, EnterSubshellOption::ClearInternalDisposition)
1075            .now_or_never()
1076            .unwrap();
1077        assert_eq!(result, Ok(()));
1078        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1079        assert_eq!(
1080            map[&cond].current_state(),
1081            &TrapState {
1082                action: Action::Ignore,
1083                origin: Origin::User(origin),
1084                pending: false
1085            }
1086        );
1087        assert_eq!(map[&cond].parent_state(), None);
1088        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Ignore);
1089    }
1090
1091    #[test]
1092    fn enter_subshell_with_command_and_no_internal_disposition() {
1093        let system = DummySystem::default();
1094        let mut map = BTreeMap::new();
1095        let cond = SIGCHLD.into();
1096        let entry = map.entry(cond);
1097        let origin = Location::dummy("foo");
1098        let action = Action::Command("echo".into());
1099        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
1100            .now_or_never()
1101            .unwrap()
1102            .unwrap();
1103
1104        let result = map
1105            .get_mut(&cond)
1106            .unwrap()
1107            .enter_subshell(&system, cond, EnterSubshellOption::ClearInternalDisposition)
1108            .now_or_never()
1109            .unwrap();
1110        assert_eq!(result, Ok(()));
1111        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1112        assert_eq!(
1113            map[&cond].current_state(),
1114            &TrapState {
1115                action: Action::Default,
1116                origin: Origin::Subshell,
1117                pending: false
1118            }
1119        );
1120        assert_eq!(
1121            map[&cond].parent_state(),
1122            Some(&TrapState {
1123                action,
1124                origin: Origin::User(origin),
1125                pending: false
1126            })
1127        );
1128        assert_eq!(system.0.borrow()[&SIGCHLD], Disposition::Default);
1129    }
1130
1131    #[test]
1132    fn enter_subshell_with_command_keeping_internal_disposition() {
1133        let system = DummySystem::default();
1134        let mut map = BTreeMap::new();
1135        let cond = SIGTSTP.into();
1136        let entry = map.entry(cond);
1137        let origin = Location::dummy("foo");
1138        let action = Action::Command("echo".into());
1139        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
1140            .now_or_never()
1141            .unwrap()
1142            .unwrap();
1143        let entry = map.entry(cond);
1144        GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
1145            .now_or_never()
1146            .unwrap()
1147            .unwrap();
1148
1149        let result = map
1150            .get_mut(&cond)
1151            .unwrap()
1152            .enter_subshell(&system, cond, EnterSubshellOption::KeepInternalDisposition)
1153            .now_or_never()
1154            .unwrap();
1155        assert_eq!(result, Ok(()));
1156        assert_eq!(map[&cond].internal_disposition(), Disposition::Ignore);
1157        assert_eq!(
1158            map[&cond].current_state(),
1159            &TrapState {
1160                action: Action::Default,
1161                origin: Origin::Subshell,
1162                pending: false
1163            }
1164        );
1165        assert_eq!(
1166            map[&cond].parent_state(),
1167            Some(&TrapState {
1168                action,
1169                origin: Origin::User(origin),
1170                pending: false
1171            })
1172        );
1173        assert_eq!(system.0.borrow()[&SIGTSTP], Disposition::Ignore);
1174    }
1175
1176    #[test]
1177    fn enter_subshell_with_command_clearing_internal_disposition() {
1178        let system = DummySystem::default();
1179        let mut map = BTreeMap::new();
1180        let cond = SIGTSTP.into();
1181        let entry = map.entry(cond);
1182        let origin = Location::dummy("foo");
1183        let action = Action::Command("echo".into());
1184        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
1185            .now_or_never()
1186            .unwrap()
1187            .unwrap();
1188        let entry = map.entry(cond);
1189        GrandState::set_internal_disposition(&system, entry, Disposition::Ignore)
1190            .now_or_never()
1191            .unwrap()
1192            .unwrap();
1193
1194        let result = map
1195            .get_mut(&cond)
1196            .unwrap()
1197            .enter_subshell(&system, cond, EnterSubshellOption::ClearInternalDisposition)
1198            .now_or_never()
1199            .unwrap();
1200        assert_eq!(result, Ok(()));
1201        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1202        assert_eq!(
1203            map[&cond].current_state(),
1204            &TrapState {
1205                action: Action::Default,
1206                origin: Origin::Subshell,
1207                pending: false
1208            }
1209        );
1210        assert_eq!(
1211            map[&cond].parent_state(),
1212            Some(&TrapState {
1213                action,
1214                origin: Origin::User(origin),
1215                pending: false
1216            })
1217        );
1218        assert_eq!(system.0.borrow()[&SIGTSTP], Disposition::Default);
1219    }
1220
1221    #[test]
1222    fn enter_subshell_with_command_ignoring() {
1223        let system = DummySystem::default();
1224        let mut map = BTreeMap::new();
1225        let cond = SIGQUIT.into();
1226        let entry = map.entry(cond);
1227        let origin = Location::dummy("foo");
1228        let action = Action::Command("echo".into());
1229        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
1230            .now_or_never()
1231            .unwrap()
1232            .unwrap();
1233
1234        let result = map
1235            .get_mut(&cond)
1236            .unwrap()
1237            .enter_subshell(&system, cond, EnterSubshellOption::Ignore)
1238            .now_or_never()
1239            .unwrap();
1240        assert_eq!(result, Ok(()));
1241        assert_eq!(map[&cond].internal_disposition(), Disposition::Default);
1242        assert_eq!(
1243            map[&cond].current_state(),
1244            &TrapState {
1245                action: Action::Ignore,
1246                origin: Origin::Subshell,
1247                pending: false
1248            }
1249        );
1250        assert_eq!(
1251            map[&cond].parent_state(),
1252            Some(&TrapState {
1253                action,
1254                origin: Origin::User(origin),
1255                pending: false
1256            })
1257        );
1258        assert_eq!(system.0.borrow()[&SIGQUIT], Disposition::Ignore);
1259    }
1260
1261    #[test]
1262    fn ignoring_initially_defaulted_signal() {
1263        let system = DummySystem::default();
1264        let mut map = BTreeMap::new();
1265        let cond = SIGQUIT.into();
1266        let entry = map.entry(cond);
1267        let Entry::Vacant(vacant) = entry else {
1268            panic!("expected vacant entry: {entry:?}");
1269        };
1270
1271        let result = GrandState::ignore(&system, vacant).now_or_never().unwrap();
1272        assert_eq!(result, Ok(()));
1273        assert_eq!(
1274            map[&cond].current_state(),
1275            &TrapState {
1276                action: Action::Ignore,
1277                origin: Origin::Subshell,
1278                pending: false
1279            }
1280        );
1281        assert_eq!(map[&cond].parent_state(), None);
1282        assert_eq!(system.0.borrow()[&SIGQUIT], Disposition::Ignore);
1283
1284        let entry = map.entry(cond);
1285        let origin = Location::dummy("foo");
1286        let action = Action::Command("echo".into());
1287        let result = GrandState::set_action(&system, entry, action, origin, false)
1288            .now_or_never()
1289            .unwrap();
1290        assert_eq!(result, Ok(()));
1291    }
1292
1293    #[test]
1294    fn ignoring_initially_ignored_signal() {
1295        let system = DummySystem::default();
1296        system.0.borrow_mut().insert(SIGQUIT, Disposition::Ignore);
1297        let mut map = BTreeMap::new();
1298        let cond = SIGQUIT.into();
1299        let entry = map.entry(cond);
1300        let Entry::Vacant(vacant) = entry else {
1301            panic!("expected vacant entry: {entry:?}");
1302        };
1303
1304        let result = GrandState::ignore(&system, vacant).now_or_never().unwrap();
1305        assert_eq!(result, Ok(()));
1306        assert_eq!(
1307            map[&cond].current_state(),
1308            &TrapState {
1309                action: Action::Ignore,
1310                origin: Origin::Inherited,
1311                pending: false
1312            }
1313        );
1314        assert_eq!(map[&cond].parent_state(), None);
1315        assert_eq!(system.0.borrow()[&SIGQUIT], Disposition::Ignore);
1316
1317        let entry = map.entry(cond);
1318        let origin = Location::dummy("foo");
1319        let action = Action::Command("echo".into());
1320        let result = GrandState::set_action(&system, entry, action, origin, false)
1321            .now_or_never()
1322            .unwrap();
1323        assert_eq!(result, Err(SetActionError::InitiallyIgnored));
1324    }
1325
1326    #[test]
1327    fn clearing_parent_setting() {
1328        let system = DummySystem::default();
1329        let mut map = BTreeMap::new();
1330        let cond = SIGCHLD.into();
1331        let entry = map.entry(cond);
1332        let origin = Location::dummy("foo");
1333        let action = Action::Command("echo".into());
1334        GrandState::set_action(&system, entry, action, origin, false)
1335            .now_or_never()
1336            .unwrap()
1337            .unwrap();
1338        let state = map.get_mut(&cond).unwrap();
1339        state
1340            .enter_subshell(&system, cond, EnterSubshellOption::ClearInternalDisposition)
1341            .now_or_never()
1342            .unwrap()
1343            .unwrap();
1344
1345        state.clear_parent_state();
1346        assert_eq!(
1347            state.current_state(),
1348            &TrapState {
1349                action: Action::Default,
1350                origin: Origin::Subshell,
1351                pending: false
1352            }
1353        );
1354        assert_eq!(state.parent_state(), None);
1355    }
1356
1357    #[test]
1358    fn marking_as_caught_and_handling() {
1359        let system = DummySystem::default();
1360        let mut map = BTreeMap::new();
1361        let cond = SIGUSR1.into();
1362        let entry = map.entry(cond);
1363        let origin = Location::dummy("foo");
1364        let action = Action::Command("echo".into());
1365        GrandState::set_action(&system, entry, action.clone(), origin.clone(), false)
1366            .now_or_never()
1367            .unwrap()
1368            .unwrap();
1369
1370        let state = &mut map.get_mut(&cond).unwrap();
1371        state.mark_as_caught();
1372        let expected_trap = TrapState {
1373            action,
1374            origin: Origin::User(origin),
1375            pending: true,
1376        };
1377        assert_eq!(state.current_state(), &expected_trap);
1378        assert_eq!(state.parent_state(), None);
1379
1380        let trap = state.handle_if_caught();
1381        let expected_trap = TrapState {
1382            pending: false,
1383            ..expected_trap
1384        };
1385        assert_eq!(trap, Some(&expected_trap));
1386
1387        let trap = state.handle_if_caught();
1388        assert_eq!(trap, None);
1389    }
1390}