Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/crashreporter/client/app/src/   (Firefox Browser Version 153.0.1©)  Datei vom 27.6.2026 mit Größe 63 kB image not shown  

Quelle  test.rs

  Sprache: Rust
 

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


//! Tests here mostly interact with the [test UI](crate::ui::test). As such, most tests read a bit
//! more like integration tests than unit tests, testing the behavior of the application as a
//! whole.

use super::*;
use crate::config::Config;
use crate::settings::Settings;
use crate::std::{
    ffi::OsString,
    fs::{MockFS, MockFiles, OpenOptions},
    io::ErrorKind,
    mock,
    process::{Command, Output},
    sync::{
        atomic::{AtomicUsize, Ordering::Relaxed},
        Arc,
    },
};
use crate::ui::{self, test::model, ui_impl::Interact};

/// A simple thread-safe counter which can be used in tests to mark that certain code paths were
/// hit.
#[derive(Clone, Default)]
pub struct Counter(Arc<AtomicUsize>);

impl Counter {
    /// Create a new zero counter.
    pub fn new() -> Self {
        Self::default()
    }

    /// Increment the counter.
    pub fn inc(&self) {
        self.0.fetch_add(1, Relaxed);
    }

    /// Get the current count.
    pub fn count(&self) -> usize {
        self.0.load(Relaxed)
    }

    /// Assert that the current count is 1.
    pub fn assert_one(&self) {
        assert_eq!(self.count(), 1);
    }
}

/// Fluent wraps arguments with the unicode BiDi characters.
struct FluentArg<T>(T);

impl<T: std::fmt::Display> std::fmt::Display for FluentArg<T> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use crate::std::fmt::Write;
        f.write_char('\u{2068}')?;
        self.0.fmt(f)?;
        f.write_char('\u{2069}')
    }
}

/// Run a gui and interaction on separate threads.
///
/// If the `gui` function returns an error, any panics in the interaction thread are ignored.
fn gui_interact<G, I, R>(gui: G, interact: I) -> anyhow::Result<R>
where
    G: FnOnce() -> anyhow::Result<R>,
    I: FnOnce(&Interact) + Send + 'static,
{
    let mut spawned_interact = Interact::spawn(interact);
    let result = gui();
    if result.is_err() {
        spawned_interact.ignore_panic();
    }
    result
}

const MOCK_MINIDUMP_EXTRA: &str = r#"{
        "Vendor""FooCorp",
        "ProductName""Bar",
        "ReleaseChannel""release",
        "BuildID""1234",
        "AsyncShutdownTimeout""{}",
        "StackTraces""{}",
        "Version""100.0",
        "ServerURL""https://reports.example.com",
        "TelemetryServerURL""https://telemetry.example.com",
        "TelemetryClientId""telemetry_client",
        "TelemetryProfileGroupId""telemetry_profile_group",
        "TelemetrySessionId""telemetry_session",
        "SomeNestedJson": { "foo""bar" },
        "URL""https://url.example.com"
    }"#;

static MOCK_MINIDUMP_EXTRA_EXPECTED: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
    format!(
        r#"{{
        "Vendor""FooCorp",
        "ProductName""Bar",
        "ReleaseChannel""release",
        "BuildID""1234",
        "AsyncShutdownTimeout""{{}}",
        "StackTraces""{{}}",
        "Version""100.0",
        "ServerURL""https://reports.example.com",
        "TelemetryServerURL""https://telemetry.example.com",
        "TelemetryClientId""telemetry_client",
        "TelemetryProfileGroupId""telemetry_profile_group",
        "TelemetrySessionId""telemetry_session",
        "SomeNestedJson": {{ "foo""bar" }},
        "URL""https://url.example.com",
        "ProcessType""main",
        "CrashTime""{time}",
        "MinidumpSha256Hash""{MOCK_MINIDUMP_SHA256}"
    }}"#,
        time = current_unix_time()
    )
});

fn compact_json(json: &str) -> String {
    let value: serde_json::Value = serde_json::from_str(json).unwrap();
    serde_json::to_string(&value).unwrap()
}

// Actual content doesn't matter, aside from the hash that is generated.
const MOCK_MINIDUMP_FILE: &[u8] = &[1234];
const MOCK_MINIDUMP_SHA256: &str =
    "9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a";
macro_rules! current_date {
    () => {
        "2004-11-09"
    };
}
const MOCK_CURRENT_TIME: &str = concat!(current_date!(), "T12:34:56.000Z");
const MOCK_REMOTE_CRASH_ID: &str = "8cbb847c-def2-4f68-be9e-000000000000";

fn current_datetime() -> time::OffsetDateTime {
    time::OffsetDateTime::parse(
        MOCK_CURRENT_TIME,
        &time::format_description::well_known::Iso8601::DEFAULT,
    )
    .unwrap()
}

fn current_unix_time() -> i64 {
    current_datetime().unix_timestamp()
}

fn current_system_time() -> ::std::time::SystemTime {
    current_datetime().into()
}

/// A basic configuration which populates some necessary/useful fields.
fn test_config() -> Config {
    let mut cfg = Config::default();
    cfg.data_dir = Some("data_dir".into());
    cfg.events_dir = Some("events_dir".into());
    cfg.dump_file = Some("minidump.dmp".into());
    cfg.strings = Some(Default::default());
    // Set delete_dump to true: this matches the default case in practice.
    cfg.delete_dump = true;
    cfg
}

fn init_test_logger() {
    static INIT: std::sync::Once = std::sync::Once::new();
    INIT.call_once(|| {
        env_logger::builder()
            .target(env_logger::Target::Stderr)
            .filter(Some("crashreporter"), log::LevelFilter::Debug)
            .is_test(true)
            .init();
    })
}

/// A test fixture to make configuration, mocking, and assertions easier.
struct GuiTest {
    /// The configuration used in the test. Initialized to [`test_config`].
    pub config: Config,
    /// The mock builder used in the test, initialized with a basic set of mocked values to ensure
    /// most things will work out of the box.
    pub mock: mock::Builder,
    /// The mocked filesystem, which can be used for mock setup and assertions after completion.
    pub files: MockFiles,
    /// Whether glean should be initialized.
    enable_glean: bool,
    /// Callback to call before `try_run` but after test setup.
    before_run: Option<Box<dyn FnOnce()>>,
}

impl GuiTest {
    /// Create a new GuiTest with enough configured for the application to run
    pub fn new() -> Self {
        init_test_logger();

        // Create a default set of files which allow successful operation.
        let mock_files = MockFiles::new();
        mock_files
            .add_file_result(
                "minidump.dmp",
                Ok(MOCK_MINIDUMP_FILE.into()),
                current_system_time(),
            )
            .add_file_result(
                "minidump.extra",
                Ok(compact_json(MOCK_MINIDUMP_EXTRA).into()),
                current_system_time(),
            );

        // Create a default mock environment which allows successful operation.
        let mut mock = mock::builder();
        mock.set(
            Command::mock("work_dir/pingsender"),
            Box::new(|_| Ok(crate::std::process::success_output())),
        )
        .set(
            Command::mock("curl"),
            Box::new(|_| {
                let mut output = crate::std::process::success_output();
                output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
                Ok(output)
            }),
        )
        .set(MockFS, mock_files.clone())
        .set(
            crate::std::env::MockCurrentExe,
            "work_dir/crashreporter".into(),
        )
        .set(crate::std::env::MockTempDir, "tmp".into())
        .set(crate::std::time::MockCurrentTime, current_system_time())
        .set(mock::MockHook::new("enable_glean_pings"), false);

        GuiTest {
            config: test_config(),
            mock,
            files: mock_files,
            enable_glean: false,
            before_run: None,
        }
    }

    /// Enable glean pings (which will serialize the test run with other glean tests).
    pub fn enable_glean_pings(&mut self) {
        self.enable_glean = true;
        self.mock
            .set(mock::MockHook::new("enable_glean_pings"), true);
    }

    /// Run the given callback after test setup but before running the tests.
    pub fn before_run(&mut self, f: impl FnOnce() + 'static) {
        self.before_run = Some(Box::new(f));
    }

    /// Run the test as configured, using the given function to interact with the GUI.
    ///
    /// Returns the final result of the application logic.
    pub fn try_run<F: FnOnce(&Interact) + Send + 'static>(
        &mut self,
        interact: F,
    ) -> anyhow::Result<bool> {
        let GuiTest {
            ref mut config,
            ref mut mock,
            ref enable_glean,
            ..
        } = self;
        let before_run = self.before_run.take();
        let mut test_config = Arc::new(std::mem::take(config));

        // Run the mock environment.
        let result = mock.run(|| {
            let _glean = if *enable_glean {
                Some(glean::test_init(&test_config))
            } else {
                None
            };
            gui_interact(
                || {
                    if let Some(f) = before_run {
                        f();
                    }
                    try_run(&mut test_config)
                },
                interact,
            )
        });

        *config = Arc::into_inner(test_config).unwrap();

        result
    }

    /// Run the test as configured, using the given function to interact with the GUI.
    ///
    /// Panics if the application logic returns an error (which would normally be displayed to the
    /// user).
    pub fn run<F: FnOnce(&Interact) + Send + 'static>(&mut self, interact: F) {
        if let Err(e) = self.try_run(interact) {
            panic!(
                "gui failure:{}",
                e.chain().map(|e| format!("\n  {e}")).collect::<String>()
            );
        }
    }

    /// Get the file assertion helper.
    pub fn assert_files(&self) -> AssertFiles {
        AssertFiles {
            data_dir: "data_dir".into(),
            events_dir: "events_dir".into(),
            inner: self.files.assert_files(),
        }
    }
}

/// A wrapper around the mock [`AssertFiles`](crate::std::fs::AssertFiles).
///
/// This implements higher-level assertions common across tests, but also supports the lower-level
/// assertions (though those return the [`AssertFiles`](crate::std::fs::AssertFiles) reference so
/// higher-level assertions must be chained first).
struct AssertFiles {
    data_dir: String,
    events_dir: String,
    inner: std::fs::AssertFiles,
}

impl AssertFiles {
    fn data(&self, rest: &str) -> String {
        format!("{}/{rest}", &self.data_dir)
    }

    fn events(&self, rest: &str) -> String {
        format!("{}/{rest}", &self.events_dir)
    }

    /// Set the data dir if not the default.
    pub fn set_data_dir<S: ToString>(&mut self, data_dir: S) -> &e='color:red'>mut Self {
        let data_dir = data_dir.to_string();
        // Data dir should be relative to root.
        self.data_dir = data_dir.trim_start_matches('/').to_string();
        self
    }

    /// Assert that the crash report was submitted according to the filesystem.
    pub fn submitted(&mut self) -> &mut Self {
        self.inner.check(
            self.data(&format!("submitted/{MOCK_REMOTE_CRASH_ID}.txt")),
            format!("Crash ID: {}\n", FluentArg(MOCK_REMOTE_CRASH_ID)),
        );
        self
    }

    /// Assert that the given settings where saved.
    pub fn saved_settings(&mut self, settings: Settings) -> &='color:red'>mut Self {
        self.inner.check(
            self.data("crashreporter_settings.json"),
            settings.to_string(),
        );
        self
    }

    /// Assert that a crash is pending according to the filesystem. The pending crash will have an
    /// unchanged extra file (due to the crash report not being submitted).
    pub fn pending_unchanged_extra(&mut self) -> &mut Self {
        let dmp = self.data("pending/minidump.dmp");
        self.inner
            .check(
                self.data("pending/minidump.extra"),
                compact_json(MOCK_MINIDUMP_EXTRA),
            )
            .check_bytes(dmp, MOCK_MINIDUMP_FILE);
        self
    }

    /// Assert that a crash is pending according to the filesystem.
    pub fn pending(&mut self) -> &mut Self {
        let dmp = self.data("pending/minidump.dmp");
        self.inner
            .check(
                self.data("pending/minidump.extra"),
                compact_json(&*MOCK_MINIDUMP_EXTRA_EXPECTED),
            )
            .check_bytes(dmp, MOCK_MINIDUMP_FILE);
        self
    }

    /// Assert that a crash is pending according to the filesystem, with updated files.
    pub fn pending_with_change(&mut self, new_dmp: &[u8], new_extra: &str)&nbsp;-> &mut Self {
        let dmp = self.data("pending/minidump.dmp");
        self.inner
            .check(self.data("pending/minidump.extra"), new_extra)
            .check_bytes(dmp, new_dmp);
        self
    }

    /// Assert that a crash submission event was written with the given submission status.
    pub fn submission_event(&mut self, success: bool) -> &mut Self {
        self.inner.check(
            self.events("minidump-submission"),
            format!(
                "crash.submission.1\n\
                {}\n\
                minidump\n\
                {success}\n\
                {}",
                current_unix_time(),
                if success { MOCK_REMOTE_CRASH_ID } else { "" }
            ),
        );
        self
    }
}

impl std::ops::Deref for AssertFiles {
    type Target = std::fs::AssertFiles;
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl std::ops::DerefMut for AssertFiles {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.inner
    }
}

#[test]
fn error_dialog() {
    gui_interact(
        || {
            let cfg = Config::default();
            ui::error_dialog(Arc::new(cfg), "an error occurred");
            Ok(())
        },
        |interact| {
            interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
        },
    )
    .unwrap();
}

#[test]
fn error_dialog_restart() {
    let mut config = Config::default();
    config.restart_command = Some("my_process".into());
    config.restart_args = vec!["a".into(), "b".into()];
    let ran_process = Counter::new();
    let mock_ran_process = ran_process.clone();
    mock::builder()
        .set(
            Command::mock("my_process"),
            Box::new(move |cmd| {
                assert_eq!(cmd.args, &["a""b"]);
                mock_ran_process.inc();
                Ok(crate::std::process::success_output())
            }),
        )
        .run(|| {
            gui_interact(
                move || {
                    ui::error_dialog(Arc::new(config), "an error occurred");
                    Ok(())
                },
                |interact| {
                    interact.element("restart", |_style, b: &model::Button| b.click.fire(&()));
                },
            )
        })
        .unwrap();
    ran_process.assert_one();
}

#[test]
fn no_dump_file() {
    let mut cfg = Arc::new(Config::default());
    {
        let cfg = Arc::get_mut(&mut cfg).unwrap();
        cfg.strings = Some(Default::default());
    }
    assert!(try_run(&mut cfg).is_err());
    Arc::get_mut(&mut cfg).unwrap().auto_submit = true;
    assert!(try_run(&mut cfg).is_ok());
}

#[test]
fn dump_file_does_not_exist() {
    let mut test = GuiTest::new();
    test.config.dump_file = Some("does_not_exist.dmp".into());
    test.try_run(|_interact| {})
        .expect_err("the gui should fail with an error");
}

#[test]
fn no_extra_file() {
    mock::builder()
        .set(
            crate::std::env::MockCurrentExe,
            "work_dir/crashreporter".into(),
        )
        .set(MockFS, {
            let files = MockFiles::new();
            files.add_file_result(
                "minidump.extra",
                Err(ErrorKind::NotFound.into()),
                ::std::time::SystemTime::UNIX_EPOCH,
            );
            files
        })
        .run(|| {
            let cfg = test_config();
            assert!(try_run(&mut Arc::new(cfg)).is_err());
        });
}

#[test]
fn auto_submit() {
    let mut test = GuiTest::new();
    test.config.auto_submit = true;
    // auto_submit should not do any GUI things, including creating the crashreporter_settings.json
    // file.
    test.mock.run(|| {
        assert!(try_run(&mut Arc::new(std::mem::take(&mut test.config))).is_ok());
    });
    test.assert_files().submitted();
}

fn prepare_restart_test() -> (GuiTest, Counter) {
    let mut test = GuiTest::new();
    test.config.restart_command = Some("my_process".into());
    test.config.restart_args = vec!["a".into(), "b".into()];
    let ran_process = Counter::new();
    let mock_ran_process = ran_process.clone();
    test.mock.set(
        Command::mock("my_process"),
        Box::new(move |cmd| {
            assert_eq!(cmd.args, &["a""b"]);
            mock_ran_process.inc();
            Ok(crate::std::process::success_output())
        }),
    );
    (test, ran_process)
}

fn customize_extra_file(test: &mut GuiTest, extra_fields: &[(&str, &str)])&nbsp;-> String {
    let extra_fields_json = extra_fields
        .iter()
        .map(|(k, v)| format!(r#""{k}""{v}","#))
        .collect::<Vec<_>>()
        .join("\n");
    let minidump_extra_contents = format!(
        r#"{{
            "Vendor""FooCorp",
            "ProductName""Bar",
            "ReleaseChannel""release",
            "BuildID""1234",
            "StackTraces""{{}}",
            "Version""100.0",
            "ServerURL""https://reports.example.com",
            "TelemetryServerURL""https://telemetry.example.com",
            "TelemetryClientId""telemetry_client",
            "TelemetryProfileGroupId""telemetry_profile_group",
            "TelemetrySessionId""telemetry_session",
            "SomeNestedJson": {{ "foo""bar" }},
            "URL""https://url.example.com",
            {extra_fields_json}
            "ProcessType""main",
            "CrashTime""{time}",
            "MinidumpSha256Hash""{MOCK_MINIDUMP_SHA256}"
        }}"#,
        time = current_unix_time()
    );
    test.files = {
        let mock_files = MockFiles::new();
        mock_files
            .add_file_result(
                "minidump.dmp",
                Ok(MOCK_MINIDUMP_FILE.into()),
                current_system_time(),
            )
            .add_file_result(
                "minidump.extra",
                Ok(minidump_extra_contents.as_str().into()),
                current_system_time(),
            );
        test.mock.set(MockFS, mock_files.clone());
        mock_files
    };
    minidump_extra_contents
}

#[test]
fn restart() {
    let (mut test, ran_process )= prepare_restart_test();
    test.run(|interact| {
        interact.element("restart", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
    ran_process.assert_one();
}

#[test]
fn no_restart_on_browser_shutdown() {
    let (mut test, ran_process) = prepare_restart_test();
    customize_extra_file(
        &mut test,
        &[("ShutdownProgress""xpcom-will-shutdown"), ("ShutdownReason""Unknown")]
    );

    test.run(|interact| {
        interact.element("restart", |style, b: &model::Button| {
            // Check that the button is hidden, and invoke the click anyway to ensure the process
            // isn't restarted (the window will still be closed).
            assert_eq!(style.visible.get(), false);
            b.click.fire(&())
        });
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
    assert_eq!(ran_process.count(), 0);
}

#[test]
fn no_restart_with_windows_error_reporting() {
    let (mut test, ran_process) = prepare_restart_test();
    let minidump_extra_contents = customize_extra_file(
        &mut test,
        &[("WindowsErrorReporting""1")]
    );
    test.config.delete_dump = false;

    test.run(|interact| {
        interact.element("restart", |style, b: &model::Button| {
            // Check that the button is hidden, and invoke the click anyway to ensure the process
            // isn't restarted (the window will still be closed).
            assert_eq!(style.visible.get(), false);
            b.click.fire(&())
        });
    });
    let mut assert_files = test.assert_files();
    assert_files.saved_settings(Settings::default()).submitted();
    {
        let dmp = assert_files.data("pending/minidump.dmp");
        let extra = assert_files.data("pending/minidump.extra");
        assert_files
            .check(extra, compact_json(&minidump_extra_contents))
            .check_bytes(dmp, MOCK_MINIDUMP_FILE);
    }

    assert_eq!(ran_process.count(), 0);
}

#[test]
fn quit() {
    let mut test = GuiTest::new();
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn no_delete_dump() {
    let mut test = GuiTest::new();
    test.config.delete_dump = false;
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted()
        .pending();
}

#[test]
fn no_submit() {
    let mut test = GuiTest::new();
    test.files.add_dir("data_dir").add_file(
        "data_dir/crashreporter_settings.json",
        Settings {
            submit_report: true,
            include_url: false,
            test_hardware: false,
        }
        .to_string(),
    );
    test.run(|interact| {
        interact.element("send", |_style, c: &model::Checkbox| {
            assert!(c.checked.get())
        });
        interact.element("include-url", |_style, c: &model::Checkbox| {
            assert!(!c.checked.get())
        });
        interact.element("send", |_style, c: &model::Checkbox| c.checked.set(false));
        interact.element("include-url", |_style, c: &model::Checkbox| {
            c.checked.set(false)
        });

        // When submission is unchecked, the following elements should be disabled.
        interact.element("details", |style, _: &model::Button| {
            assert!(!style.enabled.get());
        });
        interact.element("comment", |style, _: &model::TextBox| {
            assert!(!style.enabled.get());
        });
        interact.element("include-url", |style, _: &model::Checkbox| {
            assert!(!style.enabled.get());
        });

        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings {
            submit_report: false,
            include_url: false,
            test_hardware: false,
        })
        .pending_unchanged_extra();
}

#[test]
fn ping_and_event_files() {
    let mut test = GuiTest::new();
    test.files.add_dir("events_dir").add_file(
        "events_dir/minidump",
        "1\n\
         12:34:56\n\
         e0423878-8d59-4452-b82e-cad9c846836e\n\
         {\"foo\":\"bar\"}",
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted()
        .submission_event(true)
        .check(
            "events_dir/minidump",
            format!(
                "1\n\
                12:34:56\n\
                e0423878-8d59-4452-b82e-cad9c846836e\n\
                {}",
                serde_json::json! {{
                    "foo""bar",
                    "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
                    "StackTraces""{}"
                }}
            ),
        );
}

#[test]
fn network_failure() {
    let invoked = Counter::new();
    let mut test = GuiTest::new();
    test.files.add_dir("events_dir").add_file(
        "events_dir/minidump",
        "1\n\
         12:34:56\n\
         e0423878-8d59-4452-b82e-cad9c846836e\n\
         {\"foo\":\"bar\"}",
    );
    test.mock.set(
        net::http::MockHttp,
        Box::new(cc! { (invoked) move |_request, _url| {
            invoked.inc();
            Ok(Err(std::io::ErrorKind::HostUnreachable.into()))
        }}),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    assert!(invoked.count() > 0);
    test.assert_files()
        .saved_settings(Settings::default())
        .pending()
        .submission_event(false)
        .check(
            "events_dir/minidump",
            format!(
                "1\n\
                12:34:56\n\
                e0423878-8d59-4452-b82e-cad9c846836e\n\
                {}",
                serde_json::json! {{
                    "foo""bar",
                    "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
                    "StackTraces""{}"
                }}
            ),
        );
}

#[test]
fn pingsender_failure() {
    let mut test = GuiTest::new();
    test.mock.set(
        Command::mock("work_dir/pingsender"),
        Box::new(|_| Err(ErrorKind::NotFound.into())),
    );
    test.files.add_dir("events_dir").add_file(
        "events_dir/minidump",
        "1\n\
         12:34:56\n\
         e0423878-8d59-4452-b82e-cad9c846836e\n\
         {\"foo\":\"bar\"}",
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted()
        .submission_event(true)
        .check(
            "events_dir/minidump",
            format!(
                "1\n\
                12:34:56\n\
                e0423878-8d59-4452-b82e-cad9c846836e\n\
                {}",
                serde_json::json! {{
                    "foo""bar",
                    "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
                    // No crash ping UUID since pingsender fails
                    "StackTraces""{}"
                }}
            ),
        );
}

#[test]
fn glean_ping() {
    let mut test = GuiTest::new();
    test.enable_glean_pings();
    let submitted_glean_ping = Counter::new();
    cc! { (submitted_glean_ping)
        test.before_run(move || {
            crashping::test_before_next_send(move |_| {
                submitted_glean_ping.inc();
            });
        })
    };
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    submitted_glean_ping.assert_one();
}

#[test]
fn reads_profile_directory() {
    let mut test = GuiTest::new();
    let minidump_extra_contents = r#"{
        "Vendor""FooCorp",
        "ProductName""Bar",
        "ProfileDirectory""profile_dir",
        "ReleaseChannel""release",
        "BuildID""1234",
        "AsyncShutdownTimeout""{}",
        "StackTraces": {
            "status""OK"
        },
        "Version""100.0",
        "ServerURL""https://reports.example.com",
        "TelemetryServerURL""https://telemetry.example.com",
        "TelemetryClientId""telemetry_client",
        "TelemetryProfileGroupId""telemetry_profile_group",
        "TelemetrySessionId""telemetry_session",
        "SomeNestedJson": { "foo""bar" },
        "URL""https://url.example.com"
    }"#;
    test.files = {
        let mock_files = MockFiles::new();
        mock_files
            .add_file_result(
                "minidump.dmp",
                Ok(MOCK_MINIDUMP_FILE.into()),
                current_system_time(),
            )
            .add_file_result(
                "minidump.extra",
                Ok(minidump_extra_contents.into()),
                current_system_time(),
            )
            .add_dir("profile_dir");
        test.mock.set(MockFS, mock_files.clone());
        mock_files
    };
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    assert_eq!(test.config.profile_dir, Some("profile_dir".into()));
}

#[test]
#[ignore = "This test often passes, however it relies on Glean network scheduling, which has been
    found to be unreliable for testing purposes. A more reliable unit test is in the glean module."]
fn glean_ping_uses_pref() {
    for pref_value in [falsetrue] {
        let mut test = GuiTest::new();
        test.enable_glean_pings();
        // Set profile dir manually because glean is initialized earlier than the extra file is read
        // in tests. We check that the profile dir is correctly read in another test.
        test.config.profile_dir = Some("profile_dir".into());
        test.files.add_dir("profile_dir").add_file(
            "profile_dir/prefs.js",
            format!(r#"user_pref("datareporting.healthreport.uploadEnabled", {pref_value});"#),
        );

        // Set a mock hook at the HTTP layer to check whether the ping is sent.
        // test_before_next_send is called whether upload is enabled or not.
        let submitted_glean_ping = Counter::new();
        test.mock.set(
            net::http::MockHttp,
            Box::new(cc! { (submitted_glean_ping) move |_request, url| {
                if url.starts_with("https://incoming.glean.example.com/submit/firefox-crashreporter-mock/crash") {
                    submitted_glean_ping.inc();
                }
                Ok(Ok(vec!()))
            }}),
        );

        test.run(|interact| {
            interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
        });
        assert_eq!(submitted_glean_ping.count(), if pref_value { 1 } else { 0 });
    }
}

#[test]
fn eol_version() {
    let mut test = GuiTest::new();
    test.files
        .add_dir("data_dir")
        .add_file("data_dir/EndOfLife100.0""");
    // Should fail before opening the gui
    let result = test.try_run(|_| ());
    assert_eq!(
        result.expect_err("should fail on EOL version").to_string(),
        "Version end of life: crash reports are no longer accepted."
    );
    test.assert_files().ignore("data_dir/EndOfLife100.0");
}

#[test]
fn details_window() {
    let mut test = GuiTest::new();
    test.run(|interact| {
        let details_visible = || {
            interact.window("crash-details-window", |style, _w: &model::Window| {
                style.visible.get()
            })
        };
        assert_eq!(details_visible(), false);
        interact.element("details", |_style, b: &model::Button| b.click.fire(&()));
        assert_eq!(details_visible(), true);
        let details_text = loop {
            let v = interact.element("details-text", |_style, t: &model::TextBox| t.content.get());
            if v == "Loading…" {
                // Wait for the details to be populated.
                std::thread::sleep(std::time::Duration::from_millis(50));
                continue;
            } else {
                break v;
            }
        };
        interact.element("close-details", |_style, b: &model::Button| b.click.fire(&()));
        assert_eq!(details_visible(), false);
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
        assert_eq!(details_text,
            format!("AsyncShutdownTimeout: {{}}\n\
             BuildID: 1234\n\
             CrashTime: {time}\n\
             MinidumpSha256Hash: {MOCK_MINIDUMP_SHA256}\n\
             ProcessType: main\n\
             ProductName: Bar\n\
             ReleaseChannel: release\n\
             StackTraces: {{}}\n\
             SubmittedFrom: Client\n\
             Throttleable: 1\n\
             URL: https://url.example.com\n\
             Vendor: FooCorp\n\
             Version: 100.0\n\
             This report also contains technical information about the state of the application when it crashed.\n",
             time = current_unix_time()
             )
        );
    });
}

#[test]
fn data_dir_default() {
    let mut test = GuiTest::new();
    test.config.data_dir = None;
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .set_data_dir("data_dir/FooCorp/Bar/Crash Reports")
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn include_url() {
    for setting in [falsetrue] {
        let mut test = GuiTest::new();
        test.files.add_dir("data_dir").add_file(
            "data_dir/crashreporter_settings.json",
            Settings {
                submit_report: true,
                include_url: setting,
                test_hardware: false,
            }
            .to_string(),
        );
        test.mock.set(
            net::report::MockReport,
            Box::new(move |report| {
                assert_eq!(
                    report.extra.get("URL").and_then(|v| v.as_str()),
                    setting.then_some("https://url.example.com")
                );
                Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
            }),
        );
        test.run(|interact| {
            interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
        });
    }
}

#[test]
fn persistent_settings() {
    let mut test = GuiTest::new();
    test.run(|interact| {
        interact.element("include-url", |_style, c: &model::Checkbox| {
            c.checked.set(false)
        });
        interact.element("send", |_style, c: &model::Checkbox| c.checked.set(false));
        interact.element("test-hardware", |_style, c: &model::Checkbox| {
            c.checked.set(false)
        });
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .saved_settings(Settings {
            submit_report: false,
            include_url: false,
            test_hardware: false,
        })
        .pending_unchanged_extra();
}

#[test]
fn partial_settings_default() {
    let mut test = GuiTest::new();
    test.files.add_dir("data_dir").add_file(
        "data_dir/crashreporter_settings.json",
        "{\"include_url\":false}",
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings {
            submit_report: true,
            include_url: false,
            test_hardware: true,
        })
        .submitted();
}

#[test]
fn corrupt_settings_default() {
    let mut test = GuiTest::new();
    test.files
        .add_dir("data_dir")
        .add_file("data_dir/crashreporter_settings.json""not valid json");
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings {
            submit_report: true,
            include_url: true,
            test_hardware: true,
        })
        .submitted();
}

#[test]
fn send_memtest_output() {
    let mut test = GuiTest::new();
    test.config.run_memtest = true;
    let invoked = Counter::new();
    let mock_invoked = invoked.clone();
    test.mock
        .set(
            // memtest is the only expected process spawned using current exe
            Command::mock("work_dir/crashreporter"),
            Box::new(|cmd| assert_mock_memtest(cmd)),
        )
        .set(
            net::report::MockReport,
            Box::new(move |report| {
                mock_invoked.inc();
                assert_eq!(
                    report.extra.get("MemtestOutput").and_then(|v| v.as_str()),
                    Some("memtest output")
                );
                Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
            }),
        );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    invoked.assert_one();
}

#[test]
fn add_memtest_output_to_extra() {
    let mut test = GuiTest::new();
    test.config.run_memtest = true;
    test.config.delete_dump = false;
    test.files.add_dir("data_dir").add_file(
        "data_dir/crashreporter_settings.json",
        Settings {
            submit_report: true,
            include_url: true,
            test_hardware: true,
        }
        .to_string(),
    );
    test.mock.set(
        Command::mock("work_dir/crashreporter"),
        Box::new(|cmd| assert_mock_memtest(cmd)),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    let mut value: serde_json::Value =
        serde_json::from_str(&*MOCK_MINIDUMP_EXTRA_EXPECTED).unwrap();
    value["MemtestOutput"] = "memtest output".into();
    let new_extra = serde_json::to_string(&value).unwrap();

    test.assert_files()
        .saved_settings(Settings {
            submit_report: true,
            include_url: true,
            test_hardware: true,
        })
        .submitted()
        .pending_with_change(MOCK_MINIDUMP_FILE, &new_extra);
}

#[test]
fn toggle_memtest_spawn() {
    let mut test = GuiTest::new();
    test.config.run_memtest = true;
    test.files.add_dir("data_dir").add_file(
        "data_dir/crashreporter_settings.json",
        Settings {
            submit_report: true,
            include_url: false,
            test_hardware: false,
        }
        .to_string(),
    );
    let invoked = Counter::new();
    let mock_invoked = invoked.clone();
    test.mock
        .set(
            Command::mock("work_dir/crashreporter"),
            Box::new(|cmd| assert_mock_memtest(cmd)),
        )
        .set(
            net::report::MockReport,
            Box::new(move |report| {
                mock_invoked.inc();
                assert_eq!(
                    report.extra.get("MemtestOutput").and_then(|v| v.as_str()),
                    Some("memtest output")
                );
                Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
            }),
        );
    test.run(|interact| {
        interact.element("test-hardware", |_style, c: &model::Checkbox| {
            c.checked.set(true)
        });
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    invoked.assert_one();
}

#[test]
fn toggle_memtest_kill() {
    let mut test = GuiTest::new();
    test.config.run_memtest = true;
    test.files.add_dir("data_dir").add_file(
        "data_dir/crashreporter_settings.json",
        Settings {
            submit_report: true,
            include_url: false,
            test_hardware: true,
        }
        .to_string(),
    );
    let ran_process = Counter::new();
    let mock_ran_process = ran_process.clone();
    test.mock
        .set(
            Command::mock("work_dir/crashreporter"),
            Box::new(move |cmd| {
                // To allow accurate count of ran_process, Early return when spawning
                if cmd.spawning {
                    return Ok(crate::std::process::success_output());
                }
                mock_ran_process.inc();
                assert_mock_memtest(cmd)
            }),
        )
        .set(
            net::report::MockReport,
            Box::new(move |report| {
                assert!(report.extra.get("MemtestOutput").is_none());
                Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
            }),
        );
    test.run(|interact| {
        interact.element("test-hardware", |_style, c: &model::Checkbox| {
            c.checked.set(false)
        });
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    ran_process.assert_one();
}

fn assert_mock_memtest(cmd: &Command) -> std::io::Result<Output> {
    use ::std::borrow::Borrow;
    assert_eq!(cmd.args.len(), 3);
    assert_eq!(cmd.args[0], "--memtest");
    assert!(cmd.args[1].to_string_lossy().parse::<u32>().is_ok());
    assert!(
        serde_json::from_str::<memtest::RunnerArgs>(cmd.args[2].to_string_lossy().borrow()).is_ok()
    );

    let mut output = crate::std::process::success_output();
    output.stdout = "memtest output".into();
    Ok(output)
}

#[test]
fn comment() {
    const COMMENT: &str = "My program crashed";

    for set_comment in [falsetrue] {
        let invoked = Counter::new();
        let mock_invoked = invoked.clone();
        let mut test = GuiTest::new();
        test.mock.set(
            net::report::MockReport,
            Box::new(move |report| {
                mock_invoked.inc();
                assert_eq!(
                    report.extra.get("Comments").and_then(|v| v.as_str()),
                    set_comment.then_some(COMMENT)
                );
                Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
            }),
        );
        test.run(move |interact| {
            if set_comment {
                interact.element("comment", |_style, c: &model::TextBox| {
                    c.content.set(COMMENT.into())
                });
            }
            interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
        });

        invoked.assert_one();
    }
}

fn platform_path(s: &str) -> String {
    s.replace('/', std::path::MAIN_SEPARATOR_STR)
}

/// Test the interface to the primary network backend (Necko, through a background task).
///
/// This doesn't yet test Glean pings because of reliability issues (see Bug 1937295).
#[test]
fn background_task_network_backend() {
    let mut test = GuiTest::new();
    test.files.add_file("minidump.memory.json.gz""");
    let ran_process = Counter::new();
    let mock_ran_process = ran_process.clone();
    test.mock.set(
        Command::mock("work_dir/firefox"),
        Box::new(move |cmd| {
            if cmd.spawning {
                return Ok(crate::std::process::success_output());
            }

            mock_ran_process.inc();

            let expected_args: Vec<OsString> = [
                "--backgroundtask",
                "crashreporterNetworkBackend",
                "https://reports.example.com",
                net::http::user_agent(),
            ]
            .into_iter()
            .map(Into::into)
            .collect();

            assert_eq!(cmd.args.len(), 5);
            assert_eq!(cmd.args[..4], expected_args);
            let request_file = &cmd.args[4];

            let expected_contents = serde_json::json!({
                "type""MimePost",
                "parts": [
                    {
                        "name""extra",
                        "content": {
                            "type""String",
                            "value": serde_json::json!({
                                "Vendor":"FooCorp",
                                "ProductName":"Bar",
                                "ReleaseChannel":"release",
                                "BuildID":"1234",
                                "AsyncShutdownTimeout":"{}",
                                "StackTraces":"{}",
                                "Version":"100.0",
                                "URL":"https://url.example.com",
                                "Throttleable":"1",
                                "CrashTime": current_unix_time().to_string(),
                                "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
                                "SubmittedFrom":"Client",
                            }).to_string(),
                        },
                        "filename""extra.json",
                        "mime_type""application/json",
                    },
                    {
                        "name""upload_file_minidump",
                        "content": {
                            "type""File",
                            "value": platform_path("data_dir/pending/minidump.dmp"),
                        },
                    },
                    {
                        "name""memory_report",
                        "content": {
                            "type""File",
                            "value": platform_path("data_dir/pending/minidump.memory.json.gz"),
                        },
                    }
                ]
            })
            .to_string();

            use ::std::io::{Read, Seek, Write};
            let mut file = OpenOptions::new()
                .read(true)
                .write(true)
                .open(request_file)
                .expect("cannot open request file");
            {
                let mut contents = String::new();
                file.read_to_string(&mut contents)
                    .expect("cannot read request file");
                assert_eq!(contents, expected_contents);
            }

            file.rewind().expect("cannot rewind file");
            file.set_len(0).expect("cannot truncate file");
            file.write_all(format!("CrashID={MOCK_REMOTE_CRASH_ID}").as_bytes())
                .expect("cannot write to request file");

            Ok(crate::std::process::success_output())
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    ran_process.assert_one();

    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn curl_binary() {
    let mut test = GuiTest::new();
    test.files.add_file("minidump.memory.json.gz""");
    let ran_process = Counter::new();
    let mock_ran_process = ran_process.clone();
    test.mock.set(
        Command::mock("curl"),
        Box::new(move |cmd| {
            if cmd.spawning {
                return Ok(crate::std::process::success_output());
            }

            // Curl strings need backslashes escaped.
            let curl_escaped_separator = if std::path::MAIN_SEPARATOR == '\\' {
                "\\\\"
            } else {
                std::path::MAIN_SEPARATOR_STR
            };

            let expected_args: Vec<OsString> = [
                "--fail",
                "--user-agent",
                net::http::user_agent(),
                "--form",
                "extra=@-;filename=extra.json;type=application/json",
                "--form",
                &format!(
                    "upload_file_minidump=@\"data_dir{0}pending{0}minidump.dmp\"",
                    curl_escaped_separator
                ),
                "--form",
                &format!(
                    "memory_report=@\"data_dir{0}pending{0}minidump.memory.json.gz\"",
                    curl_escaped_separator
                ),
                "https://reports.example.com",
            ]
            .into_iter()
            .map(Into::into)
            .collect();
            assert_eq!(cmd.args, expected_args);
            let mut output = crate::std::process::success_output();
            output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
            mock_ran_process.inc();
            Ok(output)
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    ran_process.assert_one();
}

/// Test that the primary network backend (Necko) falls back to using curl if it fails.
#[test]
fn background_task_curl_fallback() {
    let mut test = GuiTest::new();
    let ran_bgtask = Counter::new();
    let mock_ran_bgtask = ran_bgtask.clone();
    let ran_curl = Counter::new();
    let mock_ran_curl = ran_curl.clone();
    let background_task_attempts = Arc::new(net::http::BackgroundTaskAttempts::new(2));
    test.mock
        .set(
            net::http::BACKGROUND_TASK_ATTEMPTS,
            background_task_attempts.clone(),
        )
        .set(
            Command::mock("work_dir/firefox"),
            Box::new(move |cmd| {
                if cmd.spawning {
                    return Ok(crate::std::process::success_output());
                }
                mock_ran_bgtask.inc();

                let expected_args: Vec<OsString> = [
                    "--backgroundtask",
                    "crashreporterNetworkBackend",
                    "https://reports.example.com",
                    net::http::user_agent(),
                ]
                .into_iter()
                .map(Into::into)
                .collect();

                assert_eq!(cmd.args.len(), 5);
                assert_eq!(cmd.args[..4], expected_args);
                let request_file = &cmd.args[4];

                let expected_contents = serde_json::json!({
                    "type""MimePost",
                    "parts": [
                        {
                            "name""extra",
                            "content": {
                                "type""String",
                                "value": serde_json::json!({
                                    "Vendor":"FooCorp",
                                    "ProductName":"Bar",
                                    "ReleaseChannel":"release",
                                    "BuildID":"1234",
                                    "AsyncShutdownTimeout":"{}",
                                    "StackTraces""{}",
                                    "Version":"100.0",
                                    "URL":"https://url.example.com",
                                    "Throttleable":"1",
                                    "CrashTime": current_unix_time().to_string(),
                                    "MinidumpSha256Hash": MOCK_MINIDUMP_SHA256,
                                    "SubmittedFrom":"Client",
                                }).to_string(),
                            },
                            "filename""extra.json",
                            "mime_type""application/json",
                        },
                        {
                            "name""upload_file_minidump",
                            "content": {
                                "type""File",
                                "value": platform_path("data_dir/pending/minidump.dmp"),
                            },
                        }
                    ]
                })
                .to_string();

                use ::std::io::Read;
                let mut file = OpenOptions::new()
                    .read(true)
                    .write(true)
                    .open(request_file)
                    .expect("cannot open request file");
                {
                    let mut contents = String::new();
                    file.read_to_string(&mut contents)
                        .expect("cannot read request file");
                    assert_eq!(contents, expected_contents);
                }

                Ok(crate::std::process::Output {
                    status: crate::std::process::exit_status(3),
                    stdout: vec![],
                    stderr: vec![],
                })
            }),
        )
        .set(
            Command::mock("curl"),
            Box::new(move |cmd| {
                if cmd.spawning {
                    return Ok(crate::std::process::success_output());
                }

                // Curl strings need backslashes escaped.
                let curl_escaped_separator = if std::path::MAIN_SEPARATOR == '\\' {
                    "\\\\"
                } else {
                    std::path::MAIN_SEPARATOR_STR
                };

                let expected_args: Vec<OsString> = [
                    "--fail",
                    "--user-agent",
                    net::http::user_agent(),
                    "--form",
                    "extra=@-;filename=extra.json;type=application/json",
                    "--form",
                    &format!(
                        "upload_file_minidump=@\"data_dir{0}pending{0}minidump.dmp\"",
                        curl_escaped_separator
                    ),
                    "https://reports.example.com",
                ]
                .into_iter()
                .map(Into::into)
                .collect();
                assert_eq!(cmd.args, expected_args);
                let mut output = crate::std::process::success_output();
                output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
                mock_ran_curl.inc();
                Ok(output)
            }),
        );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    ran_bgtask.assert_one();
    ran_curl.assert_one();

    // Verify that background tasks are still enabled.
    assert!(background_task_attempts.should_attempt());

    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn background_task_disables() {
    let mut test = GuiTest::new();
    let ran_bgtask = Counter::new();
    let mock_ran_bgtask = ran_bgtask.clone();
    let ran_curl = Counter::new();
    let mock_ran_curl = ran_curl.clone();
    let background_task_attempts = Arc::new(net::http::BackgroundTaskAttempts::new(1));
    test.mock
        .set(
            net::http::BACKGROUND_TASK_ATTEMPTS,
            background_task_attempts.clone(),
        )
        .set(
            Command::mock("work_dir/firefox"),
            Box::new(move |cmd| {
                if cmd.spawning {
                    return Ok(crate::std::process::success_output());
                }
                mock_ran_bgtask.inc();

                Ok(crate::std::process::Output {
                    status: crate::std::process::exit_status(255),
                    stdout: vec![],
                    stderr: vec![],
                })
            }),
        )
        .set(
            Command::mock("curl"),
            Box::new(move |cmd| {
                if cmd.spawning {
                    return Ok(crate::std::process::success_output());
                }

                let mut output = crate::std::process::success_output();
                output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}").into();
                mock_ran_curl.inc();
                Ok(output)
            }),
        );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    ran_bgtask.assert_one();
    ran_curl.assert_one();

    // Verify that background tasks are now disabled.
    assert_eq!(false, background_task_attempts.should_attempt());

    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn curl_library() {
    let invoked = Counter::new();
    let mock_invoked = invoked.clone();
    let mut test = GuiTest::new();
    test.mock.set(
        net::report::MockReport,
        Box::new(move |_| {
            mock_invoked.inc();
            Ok(Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}")))
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    invoked.assert_one();
}

#[test]
fn report_not_sent() {
    let mut test = GuiTest::new();
    test.files.add_dir("events_dir");
    test.mock.set(
        net::report::MockReport,
        Box::new(move |_| Err(std::io::ErrorKind::NotFound.into())),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .saved_settings(Settings::default())
        .submission_event(false)
        .pending();
}

#[test]
fn report_response_failed() {
    let mut test = GuiTest::new();
    test.files.add_dir("events_dir");
    test.mock.set(
        net::report::MockReport,
        Box::new(move |_| Ok(Err(std::io::ErrorKind::NotFound.into()))),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .saved_settings(Settings::default())
        .submission_event(false)
        .pending();
}

#[test]
fn response_indicates_discarded() {
    let mut test = GuiTest::new();
    test.mock.set(
        Command::mock("curl"),
        Box::new(|_| {
            let mut output = crate::std::process::success_output();
            output.stdout = format!("CrashID={MOCK_REMOTE_CRASH_ID}\nDiscarded=1").into();
            Ok(output)
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    let mut assert_files = test.assert_files();
    assert_files.saved_settings(Settings::default()).pending();
}

#[test]
fn response_view_url() {
    let mut test = GuiTest::new();
    test.mock.set(
        Command::mock("curl"),
        Box::new(|_| {
            let mut output = crate::std::process::success_output();
            output.stdout =
                format!("CrashID={MOCK_REMOTE_CRASH_ID}\nViewURL=https://foo.bar.example").into();
            Ok(output)
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .saved_settings(Settings::default())
        .check(
            format!("data_dir/submitted/{MOCK_REMOTE_CRASH_ID}.txt"),
            format!(
                "\
                Crash ID: {}\n\
                You can view details of this crash at {}.\n",
                FluentArg(MOCK_REMOTE_CRASH_ID),
                FluentArg("https://foo.bar.example")
            ),
        );
}

#[test]
fn response_stop_sending_reports() {
    let mut test = GuiTest::new();
    test.mock.set(
        Command::mock("curl"),
        Box::new(|_| {
            let mut output = crate::std::process::success_output();
            output.stdout =
                format!("CrashID={MOCK_REMOTE_CRASH_ID}\nStopSendingReportsFor=100.0").into();
            Ok(output)
        }),
    );
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .saved_settings(Settings::default())
        .submitted()
        .check_exists("data_dir/EndOfLife100.0");
}

#[test]
fn rename_failure_uses_copy() {
    let mut test = GuiTest::new();
    test.mock.set(mock::MockHook::new("rename_fail"), true);
    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });
    test.assert_files()
        .saved_settings(Settings::default())
        .submitted();
}

/// A real temporary directory in the host filesystem.
///
/// The directory is guaranteed to be unique to the test suite process (in case of crash, it can be
/// inspected).
///
/// When dropped, the directory is deleted.
struct TempDir {
    path: ::std::path::PathBuf,
}

impl TempDir {
    /// Create a new directory with the given identifying name.
    ///
    /// The name should be unique to deconflict amongst concurrent tests.
    pub fn new(name: &str) -> Self {
        let path = ::std::env::temp_dir().join(format!(
            "{}-test-{}-{name}",
            env!("CARGO_PKG_NAME"),
            std::process::id()
        ));
        ::std::fs::create_dir_all(&path).unwrap();
        TempDir { path }
    }

    /// Get the temporary directory path.
    pub fn path(&self) -> &::std::path::Path {
        &self.path
    }
}

impl Drop for TempDir {
    fn drop(&mut self) {
        // Best-effort removal, ignore errors.
        let _ = ::std::fs::remove_dir_all(&self.path);
    }
}

/// A mock crash report server.
///
/// When dropped, the server is shutdown.
struct TestCrashReportServer {
    addr: ::std::net::SocketAddr,
    shutdown_and_thread: Option<(
        tokio::sync::oneshot::Sender<()>,
        std::thread::JoinHandle<()>,
    )>,
}

impl TestCrashReportServer {
    /// Create and start a mock crash report server on an ephemeral port, returning a handle to the
    /// server.
    pub fn run() -> Self {
        let (shutdown, rx) = tokio::sync::oneshot::channel();

        use warp::Filter;

        let submit = warp::path("submit")
            .and(warp::filters::method::post())
            .and(warp::filters::header::header("content-type"))
            .and(warp::filters::body::bytes())
            .and_then(|content_type: String, body: bytes::Bytes| async move {
                let Some(boundary) = content_type.strip_prefix("multipart/form-data; boundary=")
                else {
                    return Err(warp::reject());
                };

                let body = String::from_utf8_lossy(&*body).to_owned();

                for part in body.split(&format!("--{boundary}")).skip(1) {
                    if part == "--\r\n" {
                        break;
                    }

                    let (_headers, _data) = part.split_once("\r\n\r\n").unwrap_or(("", part));
                    // TODO validate parts
                }
                Ok(format!("CrashID={MOCK_REMOTE_CRASH_ID}"))
            });

        let (addr_channel_tx, addr_channel_rx) = std::sync::mpsc::sync_channel(0);

        let thread = ::std::thread::spawn(move || {
            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("failed to create tokio runtime");
            let _guard = rt.enter();

            rt.block_on(async move {
                let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
                    .await
                    .expect("failed to bind");
                let addr = listener.local_addr().expect("failed to get local addr");
                addr_channel_tx.send(addr).unwrap();

                warp::serve(submit)
                    .incoming(listener)
                    .graceful(async move {
                        rx.await.ok();
                    })
                    .run()
                    .await;
            })
        });

        let addr = addr_channel_rx.recv().unwrap();

        TestCrashReportServer {
            addr,
            shutdown_and_thread: Some((shutdown, thread)),
        }
    }

    /// Get the url to which to submit crash reports for this mocked server.
    pub fn submit_url(&self) -> String {
        format!("http://{}/submit", self.addr)
    }
}

impl Drop for TestCrashReportServer {
    fn drop(&mut self) {
        let (shutdown, thread) = self.shutdown_and_thread.take().unwrap();
        let _ = shutdown.send(());
        thread.join().unwrap();
    }
}

#[test]
fn real_curl_binary() {
    if ::std::process::Command::new("curl").output().is_err() {
        eprintln!("no curl binary; skipping real_curl_binary test");
        return;
    }

    let server = TestCrashReportServer::run();

    let mut test = GuiTest::new();
    test.mock.set(
        Command::mock("curl"),
        Box::new(|cmd| cmd.output_from_real_command()),
    );
    test.config.report_url = Some(server.submit_url().into());

    // We need the dump file to actually exist since the curl binary is passed the file path.
    // The dump file needs to exist at the pending dir location.

    let tempdir = TempDir::new("real_curl_binary");
    let data_dir = tempdir.path().to_owned();
    let pending_dir = data_dir.join("pending");
    test.config.data_dir = Some(data_dir.clone().into());
    ::std::fs::create_dir_all(&pending_dir).unwrap();
    let dump_file = pending_dir.join("minidump.dmp");
    ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap();

    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .set_data_dir(data_dir.display())
        .saved_settings(Settings::default())
        .submitted();
}

#[test]
fn real_curl_library() {
    if !crate::net::can_load_libcurl() {
        eprintln!("no libcurl; skipping real_libcurl test");
        return;
    }

    let server = TestCrashReportServer::run();

    let mut test = GuiTest::new();
    test.mock
        .set(
            Command::mock("curl"),
            Box::new(|_| Err(std::io::ErrorKind::NotFound.into())),
        )
        .set(mock::MockHook::new("use_system_libcurl"), true);
    test.config.report_url = Some(server.submit_url().into());

    // We need the dump file to actually exist since libcurl is passed the file path.
    // The dump file needs to exist at the pending dir location.

    let tempdir = TempDir::new("real_libcurl");
    let data_dir = tempdir.path().to_owned();
    let pending_dir = data_dir.join("pending");
    test.config.data_dir = Some(data_dir.clone().into());
    ::std::fs::create_dir_all(&pending_dir).unwrap();
    let dump_file = pending_dir.join("minidump.dmp");
    ::std::fs::write(&dump_file, MOCK_MINIDUMP_FILE).unwrap();

    test.run(|interact| {
        interact.element("quit", |_style, b: &model::Button| b.click.fire(&()));
    });

    test.assert_files()
        .set_data_dir(data_dir.display())
        .saved_settings(Settings::default())
        .submitted();
}

Messung V0.5 in Prozent
C=94 H=99 G=96

¤ Dauer der Verarbeitung: 0.45 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.