/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* 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.
/// A simple thread-safe counter which can be used in tests to mark that certain code paths were /// hit. #[derive(Clone, Default)] pubstruct Counter(Arc<AtomicUsize>);
impl Counter { /// Create a new zero counter. pubfn new() -> Self { Self::default()
}
/// Increment the counter. pubfn inc(&self) { self.0.fetch_add(1, Relaxed);
}
/// Get the current count. pubfn count(&self) -> usize { self.0.load(Relaxed)
}
/// Assert that the current count is 1. pubfn assert_one(&self) {
assert_eq!(self.count(), 1);
}
}
/// Fluent wraps arguments with the unicode BiDi characters. struct FluentArg<T>(T);
/// 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,
{ letmut spawned_interact = Interact::spawn(interact); let result = gui(); if result.is_err() {
spawned_interact.ignore_panic();
}
result
}
/// 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 pubfn 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(),
);
/// Enable glean pings (which will serialize the test run with other glean tests). pubfn enable_glean_pings(&mutself) { 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. pubfn before_run(&mutself, 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. pubfn try_run<F: FnOnce(&Interact) + Send + 'static>(
&mutself,
interact: F,
) -> anyhow::Result<bool> { let GuiTest { refmut config, refmut mock, ref enable_glean,
..
} = self; let before_run = self.before_run.take(); letmut 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(
|| { iflet 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). pubfn run<F: FnOnce(&Interact) + Send + 'static>(&mut self, interact: F) { iflet Err(e) = self.try_run(interact) {
panic!( "gui failure:{}",
e.chain().map(|e| format!("\n {e}")).collect::<String>()
);
}
}
/// 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,
}
/// Set the data dir if not the default. pubfn set_data_dir<S: ToString>(&mutself, data_dir: S) -> &e='color:red'>mutSelf { 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. pubfn submitted(&mutself) -> &mutSelf { 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. pubfn saved_settings(&mutself, settings: Settings) -> &='color:red'>mutSelf { 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). pubfn pending_unchanged_extra(&mutself) -> &mutSelf { 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. pubfn pending(&mutself) -> &mutSelf { 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. pubfn pending_with_change(&mutself, new_dmp: &[u8], new_extra: &str) -> &mutSelf { 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. pubfn submission_event(&mutself, success: bool) -> &mutSelf { 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
}
}
#[test] fn auto_submit() { letmut 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();
}
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.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(&())
});
}); letmut 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);
}
// 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());
});
#[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 [false, true] { letmut 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] fn eol_version() { letmut 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() { letmut 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 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() { letmut 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());
}
/// Test that the primary network backend (Necko) falls back to using curl if it fails. #[test] fn background_task_curl_fallback() { letmut 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();
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")
),
);
}
/// 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. pubfn 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. pubfn path(&self) -> &::std::path::Path {
&self.path
}
}
impl Drop for TempDir { fn drop(&mutself) { // 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. pubfn run() -> Self { let (shutdown, rx) = tokio::sync::oneshot::channel();
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(asyncmove { 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();
/// Get the url to which to submit crash reports for this mocked server. pubfn submit_url(&self) -> String {
format!("http://{}/submit", self.addr)
}
}
impl Drop for TestCrashReportServer { fn drop(&mutself) { let (shutdown, thread) = self.shutdown_and_thread.take().unwrap(); let _ = shutdown.send(());
thread.join().unwrap();
}
}
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.