summaryrefslogtreecommitdiffstats
path: root/toolkit/crashreporter/client/app/src/std
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/crashreporter/client/app/src/std')
-rw-r--r--toolkit/crashreporter/client/app/src/std/env.rs45
-rw-r--r--toolkit/crashreporter/client/app/src/std/fs.rs559
-rw-r--r--toolkit/crashreporter/client/app/src/std/mock.rs254
-rw-r--r--toolkit/crashreporter/client/app/src/std/mock_stub.rs20
-rw-r--r--toolkit/crashreporter/client/app/src/std/mod.rs33
-rw-r--r--toolkit/crashreporter/client/app/src/std/net.rs5
-rw-r--r--toolkit/crashreporter/client/app/src/std/path.rs157
-rw-r--r--toolkit/crashreporter/client/app/src/std/process.rs201
-rw-r--r--toolkit/crashreporter/client/app/src/std/thread.rs45
-rw-r--r--toolkit/crashreporter/client/app/src/std/time.rs32
10 files changed, 1351 insertions, 0 deletions
diff --git a/toolkit/crashreporter/client/app/src/std/env.rs b/toolkit/crashreporter/client/app/src/std/env.rs
new file mode 100644
index 0000000000..edc22ded8d
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/env.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+use super::mock::{mock_key, MockKey};
+pub use std::env::VarError;
+use std::ffi::{OsStr, OsString};
+
+mock_key! {
+ pub struct MockCurrentExe => std::path::PathBuf
+}
+
+pub struct ArgsOs {
+ argv0: Option<OsString>,
+}
+
+impl Iterator for ArgsOs {
+ type Item = OsString;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ Some(
+ self.argv0
+ .take()
+ .expect("only argv[0] is available when mocked"),
+ )
+ }
+}
+
+pub fn var<K: AsRef<OsStr>>(_key: K) -> Result<String, VarError> {
+ unimplemented!("no var access in tests")
+}
+
+pub fn var_os<K: AsRef<OsStr>>(_key: K) -> Option<OsString> {
+ unimplemented!("no var access in tests")
+}
+
+pub fn args_os() -> ArgsOs {
+ MockCurrentExe.get(|r| ArgsOs {
+ argv0: Some(r.clone().into()),
+ })
+}
+
+pub fn current_exe() -> std::io::Result<super::path::PathBuf> {
+ Ok(MockCurrentExe.get(|r| r.clone().into()))
+}
diff --git a/toolkit/crashreporter/client/app/src/std/fs.rs b/toolkit/crashreporter/client/app/src/std/fs.rs
new file mode 100644
index 0000000000..8ba2c572d5
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/fs.rs
@@ -0,0 +1,559 @@
+/* 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/. */
+
+use crate::std::mock::{mock_key, MockKey};
+use std::collections::HashMap;
+use std::ffi::OsString;
+use std::io::{ErrorKind, Read, Result, Seek, SeekFrom, Write};
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+use std::time::SystemTime;
+
+/// Mock filesystem file content.
+#[derive(Debug, Default, Clone)]
+pub struct MockFileContent(Arc<Mutex<Vec<u8>>>);
+
+impl MockFileContent {
+ pub fn empty() -> Self {
+ Self::default()
+ }
+
+ pub fn new(data: String) -> Self {
+ Self::new_bytes(data.into())
+ }
+
+ pub fn new_bytes(data: Vec<u8>) -> Self {
+ MockFileContent(Arc::new(Mutex::new(data)))
+ }
+}
+
+impl From<()> for MockFileContent {
+ fn from(_: ()) -> Self {
+ Self::empty()
+ }
+}
+
+impl From<String> for MockFileContent {
+ fn from(s: String) -> Self {
+ Self::new(s)
+ }
+}
+
+impl From<&str> for MockFileContent {
+ fn from(s: &str) -> Self {
+ Self::new(s.to_owned())
+ }
+}
+
+impl From<Vec<u8>> for MockFileContent {
+ fn from(bytes: Vec<u8>) -> Self {
+ Self::new_bytes(bytes)
+ }
+}
+
+impl From<&[u8]> for MockFileContent {
+ fn from(bytes: &[u8]) -> Self {
+ Self::new_bytes(bytes.to_owned())
+ }
+}
+
+/// Mocked filesystem directory entries.
+pub type MockDirEntries = HashMap<OsString, MockFSItem>;
+
+/// The content of a mock filesystem item.
+pub enum MockFSContent {
+ /// File content.
+ File(Result<MockFileContent>),
+ /// A directory with the given entries.
+ Dir(MockDirEntries),
+}
+
+impl std::fmt::Debug for MockFSContent {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::File(_) => f.debug_tuple("File").finish(),
+ Self::Dir(e) => f.debug_tuple("Dir").field(e).finish(),
+ }
+ }
+}
+
+/// A mock filesystem item.
+#[derive(Debug)]
+pub struct MockFSItem {
+ /// The content of the item (file/dir).
+ pub content: MockFSContent,
+ /// The modification time of the item.
+ pub modified: SystemTime,
+}
+
+impl From<MockFSContent> for MockFSItem {
+ fn from(content: MockFSContent) -> Self {
+ MockFSItem {
+ content,
+ modified: SystemTime::UNIX_EPOCH,
+ }
+ }
+}
+
+/// A mock filesystem.
+#[derive(Debug, Clone)]
+pub struct MockFiles {
+ root: Arc<Mutex<MockFSItem>>,
+}
+
+impl Default for MockFiles {
+ fn default() -> Self {
+ MockFiles {
+ root: Arc::new(Mutex::new(MockFSContent::Dir(Default::default()).into())),
+ }
+ }
+}
+
+impl MockFiles {
+ /// Create a new, empty filesystem.
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Add a mocked file with the given content. The modification time will be the unix epoch.
+ ///
+ /// Pancis if the parent directory is not already mocked.
+ pub fn add_file<P: AsRef<Path>, C: Into<MockFileContent>>(&self, path: P, content: C) -> &Self {
+ self.add_file_result(path, Ok(content.into()), SystemTime::UNIX_EPOCH)
+ }
+
+ /// Add a mocked directory.
+ pub fn add_dir<P: AsRef<Path>>(&self, path: P) -> &Self {
+ self.path(path, true, |_| ()).unwrap();
+ self
+ }
+
+ /// Add a mocked file that returns the given result and has the given modification time.
+ ///
+ /// Pancis if the parent directory is not already mocked.
+ pub fn add_file_result<P: AsRef<Path>>(
+ &self,
+ path: P,
+ result: Result<MockFileContent>,
+ modified: SystemTime,
+ ) -> &Self {
+ let name = path.as_ref().file_name().expect("invalid path");
+ self.parent_dir(path.as_ref(), move |dir| {
+ if dir.contains_key(name) {
+ Err(ErrorKind::AlreadyExists.into())
+ } else {
+ dir.insert(
+ name.to_owned(),
+ MockFSItem {
+ content: MockFSContent::File(result),
+ modified,
+ },
+ );
+ Ok(())
+ }
+ })
+ .and_then(|r| r)
+ .unwrap();
+ self
+ }
+
+ /// If create_dirs is true, all missing path components (_including the final component_) are
+ /// created as directories. In this case `Err` is only returned if a file conflicts with
+ /// a directory component.
+ pub fn path<P: AsRef<Path>, F, R>(&self, path: P, create_dirs: bool, f: F) -> Result<R>
+ where
+ F: FnOnce(&mut MockFSItem) -> R,
+ {
+ let mut guard = self.root.lock().unwrap();
+ let mut cur_entry = &mut *guard;
+ for component in path.as_ref().components() {
+ use std::path::Component::*;
+ match component {
+ CurDir | RootDir | Prefix(_) => continue,
+ ParentDir => panic!("unsupported path: {}", path.as_ref().display()),
+ Normal(name) => {
+ let cur_dir = match &mut cur_entry.content {
+ MockFSContent::File(_) => return Err(ErrorKind::NotFound.into()),
+ MockFSContent::Dir(d) => d,
+ };
+ cur_entry = if create_dirs {
+ cur_dir
+ .entry(name.to_owned())
+ .or_insert_with(|| MockFSContent::Dir(Default::default()).into())
+ } else {
+ cur_dir.get_mut(name).ok_or(ErrorKind::NotFound)?
+ };
+ }
+ }
+ }
+ Ok(f(cur_entry))
+ }
+
+ /// Get the mocked parent directory of the given path and call a callback on the mocked
+ /// directory's entries.
+ pub fn parent_dir<P: AsRef<Path>, F, R>(&self, path: P, f: F) -> Result<R>
+ where
+ F: FnOnce(&mut MockDirEntries) -> R,
+ {
+ self.path(
+ path.as_ref().parent().unwrap_or(&Path::new("")),
+ false,
+ move |item| match &mut item.content {
+ MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
+ MockFSContent::Dir(d) => Ok(f(d)),
+ },
+ )
+ .and_then(|r| r)
+ }
+
+ /// Return a file assertion helper for the mocked filesystem.
+ pub fn assert_files(&self) -> AssertFiles {
+ let mut files = HashMap::new();
+ let root = self.root.lock().unwrap();
+
+ fn dir(files: &mut HashMap<PathBuf, MockFileContent>, path: &Path, item: &MockFSItem) {
+ match &item.content {
+ MockFSContent::File(Ok(c)) => {
+ files.insert(path.to_owned(), c.clone());
+ }
+ MockFSContent::Dir(d) => {
+ for (component, item) in d {
+ dir(files, &path.join(component), item);
+ }
+ }
+ _ => (),
+ }
+ }
+ dir(&mut files, Path::new(""), &*root);
+ AssertFiles { files }
+ }
+}
+
+/// A utility for asserting the state of the mocked filesystem.
+///
+/// All files must be accounted for; when dropped, a panic will occur if some files remain which
+/// weren't checked.
+#[derive(Debug)]
+pub struct AssertFiles {
+ files: HashMap<PathBuf, MockFileContent>,
+}
+
+// On windows we ignore drive prefixes. This is only relevant for real paths, which are only
+// present for edge case situations in tests (where AssertFiles is used).
+fn remove_prefix(p: &Path) -> &Path {
+ let mut iter = p.components();
+ if let Some(std::path::Component::Prefix(_)) = iter.next() {
+ iter.next(); // Prefix is followed by RootDir
+ iter.as_path()
+ } else {
+ p
+ }
+}
+
+impl AssertFiles {
+ /// Assert that the given path contains the given content (as a utf8 string).
+ pub fn check<P: AsRef<Path>, S: AsRef<str>>(&mut self, path: P, content: S) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ let Some(mfc) = self.files.remove(p) else {
+ panic!("missing file: {}", p.display());
+ };
+ let guard = mfc.0.lock().unwrap();
+ assert_eq!(
+ std::str::from_utf8(&*guard).unwrap(),
+ content.as_ref(),
+ "file content mismatch: {}",
+ p.display()
+ );
+ self
+ }
+
+ /// Assert that the given path contains the given byte content.
+ pub fn check_bytes<P: AsRef<Path>, B: AsRef<[u8]>>(
+ &mut self,
+ path: P,
+ content: B,
+ ) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ let Some(mfc) = self.files.remove(p) else {
+ panic!("missing file: {}", p.display());
+ };
+ let guard = mfc.0.lock().unwrap();
+ assert_eq!(
+ &*guard,
+ content.as_ref(),
+ "file content mismatch: {}",
+ p.display()
+ );
+ self
+ }
+
+ /// Ignore the given file (whether it exists or not).
+ pub fn ignore<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
+ self.files.remove(remove_prefix(path.as_ref()));
+ self
+ }
+
+ /// Assert that the given path exists without checking its content.
+ pub fn check_exists<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
+ let p = remove_prefix(path.as_ref());
+ if self.files.remove(p).is_none() {
+ panic!("missing file: {}", p.display());
+ }
+ self
+ }
+
+ /// Finish checking files.
+ ///
+ /// This panics if all files were not checked.
+ ///
+ /// This is also called when the value is dropped.
+ pub fn finish(&mut self) {
+ let files = std::mem::take(&mut self.files);
+ if !files.is_empty() {
+ panic!("additional files not expected: {:?}", files.keys());
+ }
+ }
+}
+
+impl Drop for AssertFiles {
+ fn drop(&mut self) {
+ if !std::thread::panicking() {
+ self.finish();
+ }
+ }
+}
+
+mock_key! {
+ pub struct MockFS => MockFiles
+}
+
+pub struct File {
+ content: MockFileContent,
+ pos: usize,
+}
+
+impl File {
+ pub fn open<P: AsRef<Path>>(path: P) -> Result<File> {
+ MockFS.get(move |files| {
+ files
+ .path(path, false, |item| match &item.content {
+ MockFSContent::File(result) => result
+ .as_ref()
+ .map(|b| File {
+ content: b.clone(),
+ pos: 0,
+ })
+ .map_err(|e| e.kind().into()),
+ MockFSContent::Dir(_) => Err(ErrorKind::NotFound.into()),
+ })
+ .and_then(|r| r)
+ })
+ }
+
+ pub fn create<P: AsRef<Path>>(path: P) -> Result<File> {
+ let path = path.as_ref();
+ MockFS.get(|files| {
+ let name = path.file_name().expect("invalid path");
+ files.parent_dir(path, move |d| {
+ if !d.contains_key(name) {
+ d.insert(
+ name.to_owned(),
+ MockFSItem {
+ content: MockFSContent::File(Ok(Default::default())),
+ modified: super::time::SystemTime::now().0,
+ },
+ );
+ }
+ })
+ })?;
+ Self::open(path)
+ }
+}
+
+impl Read for File {
+ fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+ let guard = self.content.0.lock().unwrap();
+ if self.pos >= guard.len() {
+ return Ok(0);
+ }
+ let to_read = std::cmp::min(buf.len(), guard.len() - self.pos);
+ buf[..to_read].copy_from_slice(&guard[self.pos..self.pos + to_read]);
+ self.pos += to_read;
+ Ok(to_read)
+ }
+}
+
+impl Seek for File {
+ fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
+ let len = self.content.0.lock().unwrap().len();
+ match pos {
+ SeekFrom::Start(n) => self.pos = n as usize,
+ SeekFrom::End(n) => {
+ if n < 0 {
+ let offset = -n as usize;
+ if offset > len {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "out of bounds",
+ ));
+ }
+ self.pos = len - offset;
+ } else {
+ self.pos = len + n as usize
+ }
+ }
+ SeekFrom::Current(n) => {
+ if n < 0 {
+ let offset = -n as usize;
+ if offset > self.pos {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "out of bounds",
+ ));
+ }
+ self.pos -= offset;
+ } else {
+ self.pos += n as usize;
+ }
+ }
+ }
+ Ok(self.pos as u64)
+ }
+}
+
+impl Write for File {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ let mut guard = self.content.0.lock().unwrap();
+ let end = self.pos + buf.len();
+ if end > guard.len() {
+ guard.resize(end, 0);
+ }
+ (&mut guard[self.pos..end]).copy_from_slice(buf);
+ self.pos = end;
+ Ok(buf.len())
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ Ok(())
+ }
+}
+
+pub fn create_dir_all<P: AsRef<Path>>(path: P) -> Result<()> {
+ MockFS.get(move |files| files.path(path, true, |_| ()))
+}
+
+pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
+ MockFS.get(move |files| {
+ let from_name = from.as_ref().file_name().expect("invalid path");
+ let item = files
+ .parent_dir(from.as_ref(), move |d| {
+ d.remove(from_name).ok_or(ErrorKind::NotFound.into())
+ })
+ .and_then(|r| r)?;
+
+ let to_name = to.as_ref().file_name().expect("invalid path");
+ files
+ .parent_dir(to.as_ref(), move |d| {
+ // Just error if `to` exists, which doesn't quite follow `std::fs::rename` behavior.
+ if d.contains_key(to_name) {
+ Err(ErrorKind::AlreadyExists.into())
+ } else {
+ d.insert(to_name.to_owned(), item);
+ Ok(())
+ }
+ })
+ .and_then(|r| r)
+ })
+}
+
+pub fn remove_file<P: AsRef<Path>>(path: P) -> Result<()> {
+ MockFS.get(move |files| {
+ let name = path.as_ref().file_name().expect("invalid path");
+ files
+ .parent_dir(path.as_ref(), |d| {
+ if let Some(MockFSItem {
+ content: MockFSContent::Dir(_),
+ ..
+ }) = d.get(name)
+ {
+ Err(ErrorKind::NotFound.into())
+ } else {
+ d.remove(name).ok_or(ErrorKind::NotFound.into()).map(|_| ())
+ }
+ })
+ .and_then(|r| r)
+ })
+}
+
+pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
+ File::create(path.as_ref())?.write_all(contents.as_ref())
+}
+
+pub struct ReadDir {
+ base: PathBuf,
+ children: Vec<OsString>,
+}
+
+impl ReadDir {
+ pub fn new(path: &Path) -> Result<Self> {
+ MockFS.get(move |files| {
+ files
+ .path(path, false, |item| match &item.content {
+ MockFSContent::Dir(d) => Ok(ReadDir {
+ base: path.to_owned(),
+ children: d.keys().cloned().collect(),
+ }),
+ MockFSContent::File(_) => Err(ErrorKind::NotFound.into()),
+ })
+ .and_then(|r| r)
+ })
+ }
+}
+
+impl Iterator for ReadDir {
+ type Item = Result<DirEntry>;
+ fn next(&mut self) -> Option<Self::Item> {
+ let child = self.children.pop()?;
+ Some(Ok(DirEntry(self.base.join(child))))
+ }
+}
+
+pub struct DirEntry(PathBuf);
+
+impl DirEntry {
+ pub fn path(&self) -> super::path::PathBuf {
+ super::path::PathBuf(self.0.clone())
+ }
+
+ pub fn metadata(&self) -> Result<Metadata> {
+ MockFS.get(|files| {
+ files.path(&self.0, false, |item| {
+ let is_dir = matches!(&item.content, MockFSContent::Dir(_));
+ Metadata {
+ is_dir,
+ modified: item.modified,
+ }
+ })
+ })
+ }
+}
+
+pub struct Metadata {
+ is_dir: bool,
+ modified: SystemTime,
+}
+
+impl Metadata {
+ pub fn is_file(&self) -> bool {
+ !self.is_dir
+ }
+
+ pub fn is_dir(&self) -> bool {
+ self.is_dir
+ }
+
+ pub fn modified(&self) -> Result<super::time::SystemTime> {
+ Ok(super::time::SystemTime(self.modified))
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/mock.rs b/toolkit/crashreporter/client/app/src/std/mock.rs
new file mode 100644
index 0000000000..ed942a09bd
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mock.rs
@@ -0,0 +1,254 @@
+/* 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/. */
+
+//! Mocking utilities.
+//!
+//! Mock data is set on a per-thread basis. [`crate::std::thread`] handles this automatically for
+//! scoped threads, and warns about creating threads otherwise (which won't be able to
+//! automatically share mocked data, but it can be easily done with [`SharedMockData`] when
+//! appropriate).
+//!
+//! Mock data is stored using type erasure, with a [`MockKey`] indexing arbitrary values. Use
+//! [`mock_key!`] to define keys and the values to which they map. This approach was taken as a
+//! matter of covenience for programmers, and the resulting creation and consumption APIs are
+//! succinct yet extensible.
+//!
+//! Consumers should define keys (and expose them for mockers), and at runtime create a mock key
+//! instance and call [`MockKey::get`] or [`MockKey::try_get`] to retrieve mocked values to use.
+//!
+//! Mockers should call [`builder`] to create a builder, [`set`](Builder::set) key/value mappings,
+//! and call [`run`](Builder::run) to execute code with the mock data set.
+
+use std::any::{Any, TypeId};
+use std::collections::{hash_map::DefaultHasher, HashMap};
+use std::hash::{Hash, Hasher};
+use std::sync::atomic::{AtomicPtr, Ordering::Relaxed};
+
+type MockDataMap = HashMap<Box<dyn MockKeyStored>, Box<dyn Any + Send + Sync>>;
+
+thread_local! {
+ static MOCK_DATA: AtomicPtr<MockDataMap> = Default::default();
+}
+
+/// A trait intended to be used as a trait object interface for mock keys.
+pub trait MockKeyStored: Any + std::fmt::Debug + Sync {
+ fn eq(&self, other: &dyn MockKeyStored) -> bool;
+ fn hash(&self, state: &mut DefaultHasher);
+}
+
+impl PartialEq for dyn MockKeyStored {
+ fn eq(&self, other: &Self) -> bool {
+ MockKeyStored::eq(self, other)
+ }
+}
+
+impl Eq for dyn MockKeyStored {}
+
+impl Hash for dyn MockKeyStored {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.type_id().hash(state);
+ let mut hasher = DefaultHasher::new();
+ MockKeyStored::hash(self, &mut hasher);
+ state.write_u64(hasher.finish());
+ }
+}
+
+impl dyn MockKeyStored {
+ pub fn downcast_ref<T: Any>(&self) -> Option<&T> {
+ if self.type_id() == TypeId::of::<T>() {
+ Some(unsafe { &*(self as *const _ as *const T) })
+ } else {
+ None
+ }
+ }
+}
+
+/// A type which can be used as a mock key.
+pub trait MockKey: MockKeyStored + Sized {
+ /// The value to which the key maps.
+ type Value: Any + Send + Sync;
+
+ /// Get the value set for this key, returning `None` if no data is set.
+ fn try_get<F, R>(&self, f: F) -> Option<R>
+ where
+ F: FnOnce(&Self::Value) -> R,
+ {
+ MOCK_DATA.with(move |ptr| {
+ let ptr = ptr.load(Relaxed);
+ if ptr.is_null() {
+ panic!("no mock data set");
+ }
+ unsafe { &*ptr }
+ .get(self as &dyn MockKeyStored)
+ .and_then(move |b| b.downcast_ref())
+ .map(f)
+ })
+ }
+
+ /// Get the value set for this key.
+ ///
+ /// Panics if no mock data is set for the key.
+ fn get<F, R>(&self, f: F) -> R
+ where
+ F: FnOnce(&Self::Value) -> R,
+ {
+ match self.try_get(f) {
+ Some(v) => v,
+ None => panic!("mock data for {self:?} not set"),
+ }
+ }
+}
+
+/// Mock data which can be shared amongst threads.
+pub struct SharedMockData(AtomicPtr<MockDataMap>);
+
+impl Clone for SharedMockData {
+ fn clone(&self) -> Self {
+ SharedMockData(AtomicPtr::new(self.0.load(Relaxed)))
+ }
+}
+
+impl SharedMockData {
+ /// Create a `SharedMockData` which stores the mock data from the current thread.
+ pub fn new() -> Self {
+ MOCK_DATA.with(|ptr| SharedMockData(AtomicPtr::new(ptr.load(Relaxed))))
+ }
+
+ /// Set the mock data on the current thread.
+ ///
+ /// # Safety
+ /// Callers must ensure that the mock data outlives the lifetime of the thread.
+ pub unsafe fn set(self) {
+ MOCK_DATA.with(|ptr| ptr.store(self.0.into_inner(), Relaxed));
+ }
+}
+
+/// Create a mock builder, which allows adding mock data and running functions under that mock
+/// environment.
+pub fn builder() -> Builder {
+ Builder::new()
+}
+
+/// A mock data builder.
+#[derive(Default)]
+pub struct Builder {
+ data: MockDataMap,
+}
+
+impl Builder {
+ /// Create a new, empty builder.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Set a mock data key/value mapping.
+ pub fn set<K: MockKey>(&mut self, key: K, value: K::Value) -> &mut Self {
+ self.data.insert(Box::new(key), Box::new(value));
+ self
+ }
+
+ /// Run the given function with mock data set.
+ pub fn run<F, R>(&mut self, f: F) -> R
+ where
+ F: FnOnce() -> R,
+ {
+ MOCK_DATA.with(|ptr| ptr.store(&mut self.data, Relaxed));
+ let ret = f();
+ MOCK_DATA.with(|ptr| ptr.store(std::ptr::null_mut(), Relaxed));
+ ret
+ }
+}
+
+/// A general-purpose [`MockKey`] keyed by an identifier string and the stored type.
+///
+/// Use [`hook`] or [`try_hook`] in code accessing the values.
+pub struct MockHook<T> {
+ name: &'static str,
+ _p: std::marker::PhantomData<fn() -> T>,
+}
+
+impl<T> std::fmt::Debug for MockHook<T> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ f.debug_struct(&format!("MockHook<{}>", std::any::type_name::<T>()))
+ .field("name", &self.name)
+ .finish()
+ }
+}
+
+impl<T: 'static> MockKeyStored for MockHook<T> {
+ fn eq(&self, other: &dyn MockKeyStored) -> bool {
+ std::any::TypeId::of::<Self>() == other.type_id()
+ && self.name == other.downcast_ref::<Self>().unwrap().name
+ }
+ fn hash(&self, state: &mut DefaultHasher) {
+ self.name.hash(state)
+ }
+}
+
+impl<T: Any + Send + Sync + 'static> MockKey for MockHook<T> {
+ type Value = T;
+}
+
+impl<T> MockHook<T> {
+ /// Create a new mock hook key with the given name.
+ pub fn new(name: &'static str) -> Self {
+ MockHook {
+ name,
+ _p: Default::default(),
+ }
+ }
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be
+/// used instead. Panics if the hook isn't set.
+pub fn hook<T: Any + Send + Sync + Clone>(_normally: T, name: &'static str) -> T {
+ MockHook::new(name).get(|v: &T| v.clone())
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set,
+/// the given value will be used instead.
+pub fn try_hook<T: Any + Send + Sync + Clone>(fallback: T, name: &'static str) -> T {
+ MockHook::new(name)
+ .try_get(|v: &T| v.clone())
+ .unwrap_or(fallback)
+}
+
+/// Create a mock key with an associated value type.
+///
+/// Supports the following syntaxes:
+/// * Unit struct: `<visibility> struct NAME => VALUE_TYPE`
+/// * Tuple struct: `<visibility> struct NAME(ITEMS) => VALUE_TYPE`
+/// * Normal struct: `<visibility> struct NAME { FIELDS } => VALUE_TYPE`
+macro_rules! mock_key {
+ ( $vis:vis struct $name:ident => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name;] $name $value }
+ };
+ ( $vis:vis struct $name:ident ($($tuple:tt)*) => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name($($tuple)*);] $name $value }
+ };
+ ( $vis:vis struct $name:ident {$($full:tt)*} => $value:ty ) => {
+ $crate::std::mock::mock_key! { @structdef[$vis struct $name{$($full)*}] $name $value }
+ };
+ ( @structdef [$($def:tt)+] $name:ident $value:ty ) => {
+ #[derive(Debug, PartialEq, Eq, Hash)]
+ $($def)+
+
+ impl crate::std::mock::MockKeyStored for $name {
+ fn eq(&self, other: &dyn crate::std::mock::MockKeyStored) -> bool {
+ std::any::TypeId::of::<Self>() == other.type_id()
+ && PartialEq::eq(self, other.downcast_ref::<Self>().unwrap())
+ }
+
+ fn hash(&self, state: &mut std::collections::hash_map::DefaultHasher) {
+ std::hash::Hash::hash(self, state)
+ }
+ }
+
+ impl crate::std::mock::MockKey for $name {
+ type Value = $value;
+ }
+ }
+}
+
+pub(crate) use mock_key;
diff --git a/toolkit/crashreporter/client/app/src/std/mock_stub.rs b/toolkit/crashreporter/client/app/src/std/mock_stub.rs
new file mode 100644
index 0000000000..a02ac3dea1
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mock_stub.rs
@@ -0,0 +1,20 @@
+/* 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/. */
+#![allow(dead_code)]
+
+//! Stubs used when mocking isn't enabled.
+
+/// Create a mock hook with the given name. When mocking isn't enabled, the given value will be
+/// used instead. Panics if the hook isn't set.
+#[inline(always)]
+pub fn hook<T: std::any::Any + Send + Sync + Clone>(normally: T, _name: &'static str) -> T {
+ normally
+}
+
+/// Create a mock hook with the given name. When mocking isn't enabled or the hook hasn't been set,
+/// the given value will be used instead.
+#[inline(always)]
+pub fn try_hook<T: std::any::Any + Send + Sync + Clone>(fallback: T, _name: &'static str) -> T {
+ fallback
+}
diff --git a/toolkit/crashreporter/client/app/src/std/mod.rs b/toolkit/crashreporter/client/app/src/std/mod.rs
new file mode 100644
index 0000000000..467d6b0c14
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/mod.rs
@@ -0,0 +1,33 @@
+/* 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/. */
+
+//! Standard library wrapper (for mocking in tests).
+//!
+//! In general this should always be used rather than `std` directly, and _especially_ when using
+//! `std` functions and types which interact with the runtime host environment.
+//!
+//! Note that, in some cases, this wrapper extends the `std` library. Notably, the [`mock`] module
+//! adds mocking functions.
+
+#![cfg_attr(mock, allow(unused))]
+
+pub use std::*;
+
+#[cfg_attr(not(mock), path = "mock_stub.rs")]
+pub mod mock;
+
+#[cfg(mock)]
+pub mod env;
+#[cfg(mock)]
+pub mod fs;
+#[cfg(mock)]
+pub mod net;
+#[cfg(mock)]
+pub mod path;
+#[cfg(mock)]
+pub mod process;
+#[cfg(mock)]
+pub mod thread;
+#[cfg(mock)]
+pub mod time;
diff --git a/toolkit/crashreporter/client/app/src/std/net.rs b/toolkit/crashreporter/client/app/src/std/net.rs
new file mode 100644
index 0000000000..dd51a44756
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/net.rs
@@ -0,0 +1,5 @@
+/* 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/. */
+
+//! Stub to avoid net use, if any.
diff --git a/toolkit/crashreporter/client/app/src/std/path.rs b/toolkit/crashreporter/client/app/src/std/path.rs
new file mode 100644
index 0000000000..c565615514
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/path.rs
@@ -0,0 +1,157 @@
+/* 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/. */
+
+//! We unfortunately have to mock `Path` because of `exists`, `try_exists`, and `metadata`.
+
+pub use std::path::*;
+
+use super::mock::MockKey;
+use std::ffi::OsStr;
+
+macro_rules! delegate {
+ ( fn $name:ident (&self $(, $arg:ident : $argty:ty )* ) -> $ret:ty ) => {
+ pub fn $name (&self, $($arg : $ty)*) -> $ret {
+ self.0.$name($($arg),*)
+ }
+ }
+}
+
+#[repr(transparent)]
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Path(std::path::Path);
+
+impl AsRef<std::path::Path> for Path {
+ fn as_ref(&self) -> &std::path::Path {
+ &self.0
+ }
+}
+
+impl AsRef<OsStr> for Path {
+ fn as_ref(&self) -> &OsStr {
+ self.0.as_ref()
+ }
+}
+
+impl AsRef<Path> for &str {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl AsRef<Path> for String {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl AsRef<Path> for &OsStr {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.as_ref())
+ }
+}
+
+impl Path {
+ fn from_path(path: &std::path::Path) -> &Self {
+ // # Safety
+ // Transparent wrapper is safe to transmute.
+ unsafe { std::mem::transmute(path) }
+ }
+
+ pub fn exists(&self) -> bool {
+ super::fs::MockFS
+ .try_get(|files| {
+ files
+ .path(self, false, |item| match &item.content {
+ super::fs::MockFSContent::File(r) => r.is_ok(),
+ _ => true,
+ })
+ .unwrap_or(false)
+ })
+ .unwrap_or(false)
+ }
+
+ pub fn read_dir(&self) -> super::io::Result<super::fs::ReadDir> {
+ super::fs::ReadDir::new(&self.0)
+ }
+
+ delegate!(fn display(&self) -> Display);
+ delegate!(fn file_stem(&self) -> Option<&OsStr>);
+ delegate!(fn file_name(&self) -> Option<&OsStr>);
+ delegate!(fn extension(&self) -> Option<&OsStr>);
+
+ pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
+ PathBuf(self.0.join(&path.as_ref().0))
+ }
+
+ pub fn parent(&self) -> Option<&Path> {
+ self.0.parent().map(Path::from_path)
+ }
+}
+
+#[repr(transparent)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct PathBuf(pub(super) std::path::PathBuf);
+
+impl PathBuf {
+ pub fn set_extension<S: AsRef<OsStr>>(&mut self, extension: S) -> bool {
+ self.0.set_extension(extension)
+ }
+
+ pub fn push<P: AsRef<Path>>(&mut self, path: P) {
+ self.0.push(path.as_ref())
+ }
+
+ pub fn pop(&mut self) -> bool {
+ self.0.pop()
+ }
+}
+
+impl std::ops::Deref for PathBuf {
+ type Target = Path;
+ fn deref(&self) -> &Self::Target {
+ Path::from_path(self.0.as_ref())
+ }
+}
+
+impl AsRef<Path> for PathBuf {
+ fn as_ref(&self) -> &Path {
+ Path::from_path(self.0.as_ref())
+ }
+}
+
+impl AsRef<std::path::Path> for PathBuf {
+ fn as_ref(&self) -> &std::path::Path {
+ self.0.as_ref()
+ }
+}
+
+impl AsRef<OsStr> for PathBuf {
+ fn as_ref(&self) -> &OsStr {
+ self.0.as_ref()
+ }
+}
+
+impl From<std::ffi::OsString> for PathBuf {
+ fn from(os_str: std::ffi::OsString) -> Self {
+ PathBuf(os_str.into())
+ }
+}
+
+impl From<std::path::PathBuf> for PathBuf {
+ fn from(pathbuf: std::path::PathBuf) -> Self {
+ PathBuf(pathbuf)
+ }
+}
+
+impl From<PathBuf> for std::ffi::OsString {
+ fn from(pathbuf: PathBuf) -> Self {
+ pathbuf.0.into()
+ }
+}
+
+impl From<&str> for PathBuf {
+ fn from(s: &str) -> Self {
+ PathBuf(s.into())
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/process.rs b/toolkit/crashreporter/client/app/src/std/process.rs
new file mode 100644
index 0000000000..49dd028447
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/process.rs
@@ -0,0 +1,201 @@
+/* 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/. */
+
+pub use std::process::*;
+
+use crate::std::mock::{mock_key, MockKey};
+
+use std::ffi::{OsStr, OsString};
+use std::io::Result;
+use std::sync::{Arc, Mutex};
+
+mock_key! {
+ // Uses PathBuf rather than OsString to avoid path separator differences.
+ pub struct MockCommand(::std::path::PathBuf) => Box<dyn Fn(&Command) -> Result<Output> + Send + Sync>
+}
+
+#[derive(Debug)]
+pub struct Command {
+ pub program: OsString,
+ pub args: Vec<OsString>,
+ pub env: std::collections::HashMap<OsString, OsString>,
+ pub stdin: Vec<u8>,
+ // XXX The spawn stuff is hacky, but for now there's only one case where we really need to
+ // interact with `spawn` so we live with it for testing.
+ pub spawning: bool,
+ pub spawned_child: Mutex<Option<::std::process::Child>>,
+}
+
+impl Command {
+ pub fn mock<S: AsRef<OsStr>>(program: S) -> MockCommand {
+ MockCommand(program.as_ref().into())
+ }
+
+ pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
+ Command {
+ program: program.as_ref().into(),
+ args: vec![],
+ env: Default::default(),
+ stdin: Default::default(),
+ spawning: false,
+ spawned_child: Mutex::new(None),
+ }
+ }
+
+ pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
+ self.args.push(arg.as_ref().into());
+ self
+ }
+
+ pub fn args<I, S>(&mut self, args: I) -> &mut Self
+ where
+ I: IntoIterator<Item = S>,
+ S: AsRef<OsStr>,
+ {
+ for arg in args.into_iter() {
+ self.arg(arg);
+ }
+ self
+ }
+
+ pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Self
+ where
+ K: AsRef<OsStr>,
+ V: AsRef<OsStr>,
+ {
+ self.env.insert(key.as_ref().into(), val.as_ref().into());
+ self
+ }
+
+ pub fn stdin<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn stdout<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn stderr<T: Into<Stdio>>(&mut self, _cfg: T) -> &mut Self {
+ self
+ }
+
+ pub fn output(&mut self) -> std::io::Result<Output> {
+ MockCommand(self.program.as_os_str().into()).get(|f| f(self))
+ }
+
+ pub fn spawn(&mut self) -> std::io::Result<Child> {
+ self.spawning = true;
+ self.output()?;
+ self.spawning = false;
+ let stdin = Arc::new(Mutex::new(vec![]));
+ Ok(Child {
+ stdin: Some(ChildStdin {
+ data: stdin.clone(),
+ }),
+ cmd: self.clone_for_child(),
+ stdin_data: Some(stdin),
+ })
+ }
+
+ #[cfg(windows)]
+ pub fn creation_flags(&mut self, _flags: u32) -> &mut Self {
+ self
+ }
+
+ pub fn output_from_real_command(&self) -> std::io::Result<Output> {
+ let mut spawned_child = self.spawned_child.lock().unwrap();
+ if spawned_child.is_none() {
+ *spawned_child = Some(
+ ::std::process::Command::new(self.program.clone())
+ .args(self.args.clone())
+ .envs(self.env.clone())
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .spawn()?,
+ );
+ }
+
+ if self.spawning {
+ return Ok(success_output());
+ }
+
+ let mut child = spawned_child.take().unwrap();
+ {
+ let mut input = child.stdin.take().unwrap();
+ std::io::copy(&mut std::io::Cursor::new(&self.stdin), &mut input)?;
+ }
+ child.wait_with_output()
+ }
+
+ fn clone_for_child(&self) -> Self {
+ Command {
+ program: self.program.clone(),
+ args: self.args.clone(),
+ env: self.env.clone(),
+ stdin: self.stdin.clone(),
+ spawning: false,
+ spawned_child: Mutex::new(self.spawned_child.lock().unwrap().take()),
+ }
+ }
+}
+
+pub struct Child {
+ pub stdin: Option<ChildStdin>,
+ cmd: Command,
+ stdin_data: Option<Arc<Mutex<Vec<u8>>>>,
+}
+
+impl Child {
+ pub fn wait_with_output(mut self) -> std::io::Result<Output> {
+ self.ref_wait_with_output().unwrap()
+ }
+
+ fn ref_wait_with_output(&mut self) -> Option<std::io::Result<Output>> {
+ drop(self.stdin.take());
+ if let Some(stdin) = self.stdin_data.take() {
+ self.cmd.stdin = Arc::try_unwrap(stdin)
+ .expect("stdin not dropped, wait_with_output may block")
+ .into_inner()
+ .unwrap();
+ Some(MockCommand(self.cmd.program.as_os_str().into()).get(|f| f(&self.cmd)))
+ } else {
+ None
+ }
+ }
+}
+
+pub struct ChildStdin {
+ data: Arc<Mutex<Vec<u8>>>,
+}
+
+impl std::io::Write for ChildStdin {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ self.data.lock().unwrap().write(buf)
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ Ok(())
+ }
+}
+
+#[cfg(unix)]
+pub fn success_exit_status() -> ExitStatus {
+ use std::os::unix::process::ExitStatusExt;
+ ExitStatus::from_raw(0)
+}
+
+#[cfg(windows)]
+pub fn success_exit_status() -> ExitStatus {
+ use std::os::windows::process::ExitStatusExt;
+ ExitStatus::from_raw(0)
+}
+
+pub fn success_output() -> Output {
+ Output {
+ status: success_exit_status(),
+ stdout: vec![],
+ stderr: vec![],
+ }
+}
diff --git a/toolkit/crashreporter/client/app/src/std/thread.rs b/toolkit/crashreporter/client/app/src/std/thread.rs
new file mode 100644
index 0000000000..d2dc74702a
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/thread.rs
@@ -0,0 +1,45 @@
+/* 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/. */
+
+pub use std::thread::*;
+
+// Mock `spawn` just to issue a warning that mocking within the thread won't work without manual
+// intervention.
+pub fn spawn<F, T>(f: F) -> JoinHandle<T>
+where
+ F: FnOnce() -> T + Send + 'static,
+ T: Send + 'static,
+{
+ eprintln!("warning: mocking won't work in `std::thread::spawn`ed threads by default. Use `std::mock::SharedMockData` if mocking is needed and it's safe to do so.");
+ std::thread::spawn(f)
+}
+
+pub struct Scope<'scope, 'env: 'scope> {
+ mock_data: super::mock::SharedMockData,
+ scope: &'scope std::thread::Scope<'scope, 'env>,
+}
+
+impl<'scope, 'env> Scope<'scope, 'env> {
+ pub fn spawn<F, T>(&self, f: F) -> ScopedJoinHandle<'scope, T>
+ where
+ F: FnOnce() -> T + Send + 'scope,
+ T: Send + 'scope,
+ {
+ let mock_data = self.mock_data.clone();
+ self.scope.spawn(move || {
+ // # Safety
+ // `thread::scope` guarantees that the mock data will outlive the thread.
+ unsafe { mock_data.set() };
+ f()
+ })
+ }
+}
+
+pub fn scope<'env, F, T>(f: F) -> T
+where
+ F: for<'scope> FnOnce(Scope<'scope, 'env>) -> T,
+{
+ let mock_data = super::mock::SharedMockData::new();
+ std::thread::scope(|scope| f(Scope { mock_data, scope }))
+}
diff --git a/toolkit/crashreporter/client/app/src/std/time.rs b/toolkit/crashreporter/client/app/src/std/time.rs
new file mode 100644
index 0000000000..5c351a7bcf
--- /dev/null
+++ b/toolkit/crashreporter/client/app/src/std/time.rs
@@ -0,0 +1,32 @@
+/* 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/. */
+
+use super::mock::{mock_key, MockKey};
+pub use std::time::{Duration, SystemTimeError};
+
+mock_key! {
+ pub struct MockCurrentTime => std::time::SystemTime
+}
+
+#[repr(transparent)]
+#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
+pub struct SystemTime(pub(super) std::time::SystemTime);
+
+impl From<SystemTime> for ::time::OffsetDateTime {
+ fn from(t: SystemTime) -> Self {
+ t.0.into()
+ }
+}
+
+impl SystemTime {
+ pub const UNIX_EPOCH: SystemTime = SystemTime(std::time::SystemTime::UNIX_EPOCH);
+
+ pub fn now() -> Self {
+ MockCurrentTime.get(|t| SystemTime(*t))
+ }
+
+ pub fn duration_since(&self, earlier: Self) -> Result<Duration, SystemTimeError> {
+ self.0.duration_since(earlier.0)
+ }
+}