File: test_error_channel_error.rs

package info (click to toggle)
rust-flexi-logger 0.29.8-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,084 kB
  • sloc: makefile: 2
file content (137 lines) | stat: -rw-r--r-- 4,228 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
mod test_utils;

use std::{
    env,
    fs::{create_dir_all, remove_file, OpenOptions},
    io::Write,
    path::{Path, PathBuf},
    process::{Command, Stdio},
};

const CTRL_INDEX: &str = "CTRL_INDEX";
const CRASHFILE: &str = "CRASHFILE";
const RUNS: usize = 3;
const MILLIS: u64 = 50;

// use the same technique as test_utils::dispatch to launch itself in child mode,
// but do it twice:
//   controller starts parent, parent starts child
//   controller keeps running and verifies that the child's panic file is created (or not),
//   parent terminates directly and thus destroys the stderr of child, thus forcing child to panic
#[test]
fn main() {
    match env::var(CTRL_INDEX).as_ref() {
        Err(_) => {
            controller();
        }
        Ok(s) if s == "parent" => {
            parent(false);
        }
        Ok(s) if s == "parent_panic" => {
            parent(true);
        }
        Ok(s) if s == "child" => {
            child(false);
        }
        Ok(s) if s == "child_panic" => {
            child(true);
        }
        Ok(s) => panic!("Unexpected value {s}"),
    }
}

fn controller() {
    let progpath = env::args().next().unwrap();

    create_dir_all(crashdump_file().parent().unwrap()).unwrap();

    remove_file(crashdump_file()).ok();

    // First run: don't panic
    let mut child = Command::new(progpath.clone())
        .env(CTRL_INDEX, "parent")
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    assert!(child.wait().expect("failed to wait on child").success());

    // check that no crashdump_file was written
    std::thread::sleep(std::time::Duration::from_millis(200));
    assert!(!Path::new(&crashdump_file()).try_exists().unwrap());

    // Second run: panic
    let mut child = Command::new(progpath)
        .env(CTRL_INDEX, "parent_panic")
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
    assert!(child.wait().expect("failed to wait on child").success());

    // check that crashdump_file was written
    std::thread::sleep(std::time::Duration::from_millis(200));
    assert!(Path::new(&crashdump_file()).try_exists().unwrap());
}

fn parent(panic: bool) {
    let progpath = std::env::args().next().unwrap();
    // we don't want to wait here, and it's not an issue because this is not a long running program
    #[allow(clippy::zombie_processes)]
    // spawn child and terminate directly, thus destroying the child's stderr
    Command::new(progpath)
        .env(CTRL_INDEX, if panic { "child_panic" } else { "child" })
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();
}

fn child(panic: bool) {
    let original_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |panic| {
        let backtrace = std::backtrace::Backtrace::capture();

        let mut file = OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(true)
            .open(crashdump_file())
            .unwrap();
        file.write_all(format!("Panic occured:\n{}\n{}\n", panic, backtrace).as_bytes())
            .unwrap();
        file.flush().unwrap();

        original_hook(panic);
    }));

    let _logger = flexi_logger::Logger::try_with_str("info")
        .unwrap()
        .log_to_stderr()
        .panic_if_error_channel_is_broken(panic)
        .start()
        .unwrap();

    for i in 0..RUNS {
        log::info!("log test ({i})"); // <-- causes panic when parent terminated
        std::thread::sleep(std::time::Duration::from_millis(MILLIS));
    }
}

// controller is first caller and writes name to env, all other calls should find the env
// and take the value from there
fn crashdump_file() -> PathBuf {
    match std::env::var(CRASHFILE) {
        Ok(s) => Path::new(&s).to_path_buf(),
        Err(_) => {
            let progname = PathBuf::from(std::env::args().next().unwrap())
                .file_name()
                .unwrap()
                .to_string_lossy()
                .to_string();
            let path = test_utils::file(&format!("./{progname}.log"));
            std::env::set_var(CRASHFILE, &path);
            path
        }
    }
}