// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::HashMap; use std::collections::HashSet; use std::env; use std::ffi::OsStr; use std::fs; use std::fs::OpenOptions; use std::io::Write as _; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Stdio; use std::sync::Arc; use futures::AsyncReadExt as _; use itertools::Itertools as _; use jj_lib::backend; use jj_lib::backend::Backend; use jj_lib::backend::BackendInitError; use jj_lib::backend::ChangeId; use jj_lib::backend::CommitId; use jj_lib::backend::CopyHistory; use jj_lib::backend::CopyId; use jj_lib::backend::FileId; use jj_lib::backend::MillisSinceEpoch; use jj_lib::backend::Signature; use jj_lib::backend::Timestamp; use jj_lib::backend::TreeValue; use jj_lib::commit::Commit; use jj_lib::commit_builder::CommitBuilder; use jj_lib::config::ConfigLayer; use jj_lib::config::ConfigSource; use jj_lib::config::StackedConfig; use jj_lib::conflict_labels::ConflictLabels; use jj_lib::git_backend::GitBackend; use jj_lib::gitignore::GitIgnoreFile; use jj_lib::matchers::EverythingMatcher; use jj_lib::matchers::NothingMatcher; use jj_lib::merge::Merge; use jj_lib::merged_tree::MergedTree; use jj_lib::object_id::ObjectId as _; use jj_lib::repo::MutableRepo; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::repo::RepoLoader; use jj_lib::repo::StoreFactories; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathBuf; use jj_lib::repo_path::RepoPathComponent; use jj_lib::rewrite::RebaseOptions; use jj_lib::rewrite::RebasedCommit; use jj_lib::secret_backend::SecretBackend; use jj_lib::settings::UserSettings; use jj_lib::signing::Signer; use jj_lib::simple_backend::SimpleBackend; use jj_lib::store::Store; use jj_lib::transaction::Transaction; use jj_lib::tree::Tree; use jj_lib::tree_builder::TreeBuilder; use jj_lib::working_copy::SnapshotError; use jj_lib::working_copy::SnapshotOptions; use jj_lib::working_copy::SnapshotStats; use jj_lib::workspace::Workspace; use pollster::FutureExt as _; use tempfile::TempDir; use crate::test_backend::TestBackendFactory; pub mod git; pub mod proptest; pub mod test_backend; /// Convenient return type for test functions. pub type TestResult = eyre::Result; pub const HERMETIC_GIT_CONFIGS: &[(&str, &str)] = &[ // gitoxide uses "main" as the default branch name, whereas git uses "master". This also // prevents git CLI from issuing the initial branch name advice. ("init.defaultBranch", "master"), ]; // TODO: Consider figuring out a way to make `GitBackend` and `git(1)` calls in // tests ignore external configuration and removing this function. This is // somewhat tricky because `gix` looks at system and user configuration, and // `GitBackend` also calls into `git(1)` for things like garbage collection. pub fn hermetic_git() { let mut envs = [ // Prevent GitBackend from loading user and system configurations. For // gitoxide API use in tests, Config::isolated() is probably better. ("GIT_CONFIG_SYSTEM", "/dev/null"), ("GIT_CONFIG_GLOBAL", "/dev/null"), ] .map(|(key, value)| (key.to_string(), value.to_string())) .to_vec(); for (i, (key, value)) in HERMETIC_GIT_CONFIGS.iter().enumerate() { envs.push((format!("GIT_CONFIG_KEY_{i}"), key.to_string())); envs.push((format!("GIT_CONFIG_VALUE_{i}"), value.to_string())); } envs.push(( "GIT_CONFIG_COUNT".to_string(), HERMETIC_GIT_CONFIGS.len().to_string(), )); for (key, value) in envs { // SAFETY: This is actually not safe. `getenv` and `setenv` are not thread safe, // and we can't guarantee that the following call won't have race conditions. unsafe { env::set_var(key, value) }; } } pub fn new_temp_dir() -> TempDir { hermetic_git(); tempfile::Builder::new() .prefix("jj-test-") .tempdir() .unwrap() } /// Returns new low-level config object that includes fake user configuration /// needed to run basic operations. pub fn base_user_config() -> StackedConfig { let config_text = r#" user.name = "Test User" user.email = "test.user@example.com" operation.username = "test-username" operation.hostname = "host.example.com" debug.randomness-seed = 42 "#; let mut config = StackedConfig::with_defaults(); config.add_layer(ConfigLayer::parse(ConfigSource::User, config_text).unwrap()); config } /// Returns new immutable settings object that includes fake user configuration /// needed to run basic operations. pub fn user_settings() -> UserSettings { UserSettings::from_config(base_user_config()).unwrap() } /// Creates [`SnapshotOptions`] for use in tests. pub fn empty_snapshot_options() -> SnapshotOptions<'static> { SnapshotOptions { base_ignores: GitIgnoreFile::empty(), progress: None, start_tracking_matcher: &EverythingMatcher, force_tracking_matcher: &NothingMatcher, max_new_file_size: u64::MAX, } } /// Panic if `CI` environment variable is set to a non-empty value /// /// Most CI environments set this variable automatically. See e.g. /// #[track_caller] pub fn ensure_running_outside_ci(reason: &str) { let running_in_ci = std::env::var("CI").is_ok_and(|value| !value.is_empty()); assert!(!running_in_ci, "Running in CI, {reason}."); } /// Tests if an external tool is installed and in the PATH pub fn is_external_tool_installed(program_name: impl AsRef) -> bool { Command::new(program_name) .arg("--version") .stdout(Stdio::null()) .status() .is_ok() } #[derive(Debug)] pub struct TestEnvironment { temp_dir: TempDir, test_backend_factory: TestBackendFactory, } impl TestEnvironment { pub fn init() -> Self { Self { temp_dir: new_temp_dir(), test_backend_factory: TestBackendFactory::default(), } } pub fn root(&self) -> &Path { self.temp_dir.path() } pub fn default_store_factories(&self) -> StoreFactories { let mut factories = StoreFactories::default(); factories.add_backend("test", { let factory = self.test_backend_factory.clone(); Box::new(move |_settings, store_path| Ok(Box::new(factory.load(store_path)))) }); factories.add_backend( SecretBackend::name(), Box::new(|settings, store_path| { Ok(Box::new(SecretBackend::load(settings, store_path)?)) }), ); factories } pub fn load_repo_at_head( &self, settings: &UserSettings, repo_path: &Path, ) -> Arc { RepoLoader::init_from_file_system(settings, repo_path, &self.default_store_factories()) .unwrap() .load_at_head() .block_on() .unwrap() } } pub struct TestRepo { pub env: TestEnvironment, pub repo: Arc, repo_path: PathBuf, } #[derive(PartialEq, Eq, Copy, Clone)] pub enum TestRepoBackend { Git, Simple, Test, } impl TestRepoBackend { fn init_backend( &self, env: &TestEnvironment, settings: &UserSettings, store_path: &Path, ) -> Result, BackendInitError> { match self { Self::Git => Ok(Box::new(GitBackend::init_internal(settings, store_path)?)), Self::Simple => Ok(Box::new(SimpleBackend::init(store_path))), Self::Test => Ok(Box::new(env.test_backend_factory.init(store_path))), } } } impl TestRepo { pub fn init() -> Self { Self::init_with_backend(TestRepoBackend::Test) } pub fn init_with_backend(backend: TestRepoBackend) -> Self { Self::init_with_backend_and_settings(backend, &user_settings()) } pub fn init_with_settings(settings: &UserSettings) -> Self { Self::init_with_backend_and_settings(TestRepoBackend::Test, settings) } pub fn init_with_backend_and_settings( backend: TestRepoBackend, settings: &UserSettings, ) -> Self { let env = TestEnvironment::init(); let repo_dir = env.root().join("repo"); fs::create_dir(&repo_dir).unwrap(); let repo = ReadonlyRepo::init( settings, &repo_dir, &|settings, store_path| backend.init_backend(&env, settings, store_path), Signer::from_settings(settings).unwrap(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), ReadonlyRepo::default_submodule_store_initializer(), ) .block_on() .unwrap(); Self { env, repo, repo_path: repo_dir, } } pub fn repo_path(&self) -> &Path { &self.repo_path } } pub struct TestWorkspace { pub env: TestEnvironment, pub workspace: Workspace, pub repo: Arc, } impl TestWorkspace { pub fn init() -> Self { Self::init_with_backend(TestRepoBackend::Test) } pub fn init_with_backend(backend: TestRepoBackend) -> Self { Self::init_with_backend_and_settings(backend, &user_settings()) } pub fn init_with_settings(settings: &UserSettings) -> Self { Self::init_with_backend_and_settings(TestRepoBackend::Test, settings) } pub fn init_with_backend_and_settings( backend: TestRepoBackend, settings: &UserSettings, ) -> Self { let signer = Signer::from_settings(settings).unwrap(); Self::init_with_backend_and_signer(backend, signer, settings) } pub fn init_with_backend_and_signer( backend: TestRepoBackend, signer: Signer, settings: &UserSettings, ) -> Self { let env = TestEnvironment::init(); let workspace_root = env.root().join("repo"); fs::create_dir(&workspace_root).unwrap(); let (workspace, repo) = Workspace::init_with_backend( settings, &workspace_root, &|settings, store_path| backend.init_backend(&env, settings, store_path), signer, ) .block_on() .unwrap(); Self { env, workspace, repo, } } pub fn root_dir(&self) -> PathBuf { self.env.root().join("repo").join("..") } pub fn repo_path(&self) -> &Path { self.workspace.repo_path() } /// Snapshots the working copy and returns the tree. Updates the working /// copy state on disk, but does not update the working-copy commit (no /// new operation). pub fn snapshot_with_options( &mut self, options: &SnapshotOptions, ) -> Result<(MergedTree, SnapshotStats), SnapshotError> { let mut locked_ws = self .workspace .start_working_copy_mutation() .block_on() .unwrap(); let (tree, stats) = locked_ws.locked_wc().snapshot(options).block_on()?; // arbitrary operation id locked_ws .finish(self.repo.op_id().clone()) .block_on() .unwrap(); Ok((tree, stats)) } /// Like `snapshot_with_option()` but with default options pub fn snapshot(&mut self) -> Result { let (tree, _stats) = self.snapshot_with_options(&empty_snapshot_options())?; Ok(tree) } } pub fn commit_transactions(txs: Vec) -> Arc { let repo_loader = txs[0].base_repo().loader().clone(); let mut op_ids = vec![]; for tx in txs { op_ids.push(tx.commit("test").block_on().unwrap().op_id().clone()); std::thread::sleep(std::time::Duration::from_millis(1)); } let repo = repo_loader.load_at_head().block_on().unwrap(); // Test the setup. The assumption here is that the parent order matches the // order in which they were merged (which currently matches the transaction // commit order), so we want to know make sure they appear in a certain // order, so the caller can decide the order by passing them to this // function in a certain order. assert_eq!(*repo.operation().parent_ids(), op_ids); repo } pub fn repo_path_component(value: &str) -> &RepoPathComponent { RepoPathComponent::new(value).unwrap() } pub fn repo_path(value: &str) -> &RepoPath { RepoPath::from_internal_string(value).unwrap() } pub fn repo_path_buf(value: impl Into) -> RepoPathBuf { RepoPathBuf::from_internal_string(value).unwrap() } pub fn read_file(store: &Store, path: &RepoPath, id: &FileId) -> Vec { let mut reader = store.read_file(path, id).block_on().unwrap(); let mut content = vec![]; reader.read_to_end(&mut content).block_on().unwrap(); content } pub fn write_file(store: &Store, path: &RepoPath, contents: &str) -> FileId { store .write_file(path, &mut contents.as_bytes()) .block_on() .unwrap() } pub struct TestTreeBuilder { store: Arc, tree_builder: TreeBuilder, } impl TestTreeBuilder { pub fn new(store: Arc) -> Self { let tree_builder = TreeBuilder::new(store.clone(), store.empty_tree_id().clone()); Self { store, tree_builder, } } pub fn file( &mut self, path: &RepoPath, contents: impl AsRef<[u8]>, ) -> TestTreeFileEntryBuilder<'_> { TestTreeFileEntryBuilder { tree_builder: &mut self.tree_builder, path: path.to_owned(), contents: contents.as_ref().to_vec(), executable: false, copy_history: None, copy_id: CopyId::placeholder(), } } pub fn symlink(&mut self, path: &RepoPath, target: &str) { let id = self.store.write_symlink(path, target).block_on().unwrap(); self.tree_builder .set(path.to_owned(), TreeValue::Symlink(id)); } pub fn submodule(&mut self, path: &RepoPath, commit: CommitId) { self.tree_builder .set(path.to_owned(), TreeValue::GitSubmodule(commit)); } pub fn write_single_tree(self) -> Tree { let id = self.tree_builder.write_tree().block_on().unwrap(); self.store .get_tree(RepoPathBuf::root(), &id) .block_on() .unwrap() } pub fn write_merged_tree(self) -> MergedTree { let id = self.tree_builder.write_tree().block_on().unwrap(); MergedTree::resolved(self.store, id) } } pub struct TestTreeFileEntryBuilder<'a> { tree_builder: &'a mut TreeBuilder, path: RepoPathBuf, contents: Vec, executable: bool, copy_history: Option, copy_id: CopyId, } impl TestTreeFileEntryBuilder<'_> { pub fn executable(mut self, executable: bool) -> Self { self.executable = executable; self } /// Setting an explicit `CopyHistory` overrides any previous `.copy_id()` /// calls. pub fn copy_history(mut self, copy_history: &CopyHistory) -> Self { self.copy_history = Some(copy_history.clone()); self.copy_id = CopyId::placeholder(); self } /// Setting an explicit `CopyId` overrides any previous `.copy_history()` /// calls. pub fn copy_id(mut self, copy_id: CopyId) -> Self { self.copy_id = copy_id; self.copy_history = None; self } } impl Drop for TestTreeFileEntryBuilder<'_> { fn drop(&mut self) { let id = self .tree_builder .store() .write_file(&self.path, &mut self.contents.as_slice()) .block_on() .unwrap(); let path = std::mem::replace(&mut self.path, RepoPathBuf::root()); let copy_id = if let Some(copy_history) = self.copy_history.as_ref() { self.tree_builder .store() .backend() .write_copy(copy_history) .block_on() .unwrap() } else { self.copy_id.clone() }; self.tree_builder.set( path, TreeValue::File { id, executable: self.executable, copy_id, }, ); } } pub struct TestThreeWayMergeTreeBuilder { store: Arc, base_builder: TestTreeBuilder, parent1_builder: TestTreeBuilder, parent2_builder: TestTreeBuilder, } impl TestThreeWayMergeTreeBuilder { pub fn new(store: Arc) -> Self { Self { store: Arc::clone(&store), base_builder: TestTreeBuilder::new(Arc::clone(&store)), parent1_builder: TestTreeBuilder::new(Arc::clone(&store)), parent2_builder: TestTreeBuilder::new(store), } } pub fn base(&mut self) -> &mut TestTreeBuilder { &mut self.base_builder } pub fn parent1(&mut self) -> &mut TestTreeBuilder { &mut self.parent1_builder } pub fn parent2(&mut self) -> &mut TestTreeBuilder { &mut self.parent2_builder } pub fn write_merged_tree(self) -> MergedTree { let tree_ids = [ self.parent1_builder, self.base_builder, self.parent2_builder, ] .map(|builder| builder.write_single_tree().id().clone()); MergedTree::new( self.store.clone(), Merge::from_vec(tree_ids.to_vec()), ConflictLabels::unlabeled(), ) } } pub fn create_single_tree_with( repo: &Arc, build: impl FnOnce(&mut TestTreeBuilder), ) -> Tree { let mut builder = TestTreeBuilder::new(repo.store().clone()); build(&mut builder); builder.write_single_tree() } pub fn create_single_tree(repo: &Arc, path_contents: &[(&RepoPath, &str)]) -> Tree { create_single_tree_with(repo, |builder| { for (path, contents) in path_contents { builder.file(path, contents); } }) } pub fn create_tree_with( repo: &Arc, build: impl FnOnce(&mut TestTreeBuilder), ) -> MergedTree { let mut builder = TestTreeBuilder::new(repo.store().clone()); build(&mut builder); builder.write_merged_tree() } pub fn create_tree(repo: &Arc, path_contents: &[(&RepoPath, &str)]) -> MergedTree { create_tree_with(repo, |builder| { for (path, contents) in path_contents { builder.file(path, contents); } }) } pub fn create_tree_with_copy_history( repo: &Arc, histories: &HashMap, path_contents: &[(&RepoPath, &str)], ) -> MergedTree { create_tree_with(repo, |builder| { for (path, contents) in path_contents { builder.file(path, contents).copy_history(&histories[*path]); } }) } pub fn create_tree_with_copy_id( repo: &Arc, path_contents_id: &[(&RepoPath, &str, &CopyId)], ) -> MergedTree { create_tree_with(repo, |builder| { for (path, contents, id) in path_contents_id { builder.file(path, contents).copy_id((*id).clone()); } }) } #[must_use] pub fn create_random_tree(repo: &Arc) -> MergedTree { let number = rand::random::(); let path = repo_path_buf(format!("file{number}")); create_tree_with(repo, |builder| { builder.file(&path, "contents"); }) } pub fn create_random_commit(mut_repo: &mut MutableRepo) -> CommitBuilder<'_> { let tree = create_random_tree(mut_repo.base_repo()); let number = rand::random::(); mut_repo .new_commit(vec![mut_repo.store().root_commit_id().clone()], tree) .set_description(format!("random commit {number}")) } pub fn commit_with_tree(store: &Arc, tree: MergedTree) -> Commit { let signature = Signature { name: "Some One".to_string(), email: "someone@example.com".to_string(), timestamp: Timestamp { timestamp: MillisSinceEpoch(0), tz_offset: 0, }, }; let (root_tree, conflict_labels) = tree.into_tree_ids_and_labels(); let commit = backend::Commit { parents: vec![store.root_commit_id().clone()], predecessors: vec![], root_tree, conflict_labels: conflict_labels.into_merge(), change_id: ChangeId::from_hex("abcd"), description: "description".to_string(), author: signature.clone(), committer: signature, secure_sig: None, }; store.write_commit(commit, None).block_on().unwrap() } pub fn dump_tree(merged_tree: &MergedTree) -> String { use std::fmt::Write as _; let store = merged_tree.store(); let mut buf = String::new(); let trees = merged_tree.trees().block_on().unwrap(); writeln!(&mut buf, "merged tree (sides: {})", trees.num_sides()).unwrap(); for tree in &trees { writeln!(&mut buf, " tree {}", tree.id()).unwrap(); for (path, entry) in tree.entries_matching(&EverythingMatcher) { match entry { TreeValue::File { id, executable: _, copy_id: _, } => { let file_buf = read_file(store, &path, &id); let file_contents = String::from_utf8_lossy(&file_buf); writeln!(&mut buf, " file {path:?} ({id}): {file_contents:?}").unwrap(); } TreeValue::Symlink(id) => { writeln!(&mut buf, " symlink {path:?} ({id})").unwrap(); } TreeValue::GitSubmodule(id) => { writeln!(&mut buf, " submodule {path:?} ({id})").unwrap(); } _ => { writeln!(&mut buf, " entry {path:?}: {entry:?}").unwrap(); } } } } buf } #[macro_export] macro_rules! assert_tree_eq { ($left_tree:expr, $right_tree:expr $(,)?) => { assert_tree_eq!($left_tree, $right_tree, "trees are different") }; ($left_tree:expr, $right_tree:expr, $($args:tt)+) => {{ let left_tree: &::jj_lib::merged_tree::MergedTree = &$left_tree; let right_tree: &::jj_lib::merged_tree::MergedTree = &$right_tree; assert_eq!( left_tree.tree_ids_and_labels(), right_tree.tree_ids_and_labels(), "{}:\n left: {}\nright: {}", format_args!($($args)*), $crate::dump_tree(left_tree), $crate::dump_tree(right_tree), ) }}; } pub fn write_random_commit(mut_repo: &mut MutableRepo) -> Commit { write_random_commit_with_parents(mut_repo, &[]) } pub fn write_random_commit_with_parents(mut_repo: &mut MutableRepo, parents: &[&Commit]) -> Commit { let parents = if parents.is_empty() { &[&mut_repo.store().root_commit()] } else { parents }; create_random_commit(mut_repo) .set_parents(parents.iter().map(|commit| commit.id().clone()).collect()) .write_unwrap() } pub fn write_working_copy_file(workspace_root: &Path, path: &RepoPath, contents: impl AsRef<[u8]>) { let path = path.to_fs_path(workspace_root).unwrap(); if let Some(parent) = path.parent() { fs::create_dir_all(parent).unwrap(); } let mut file = OpenOptions::new() .write(true) .create(true) .truncate(true) .open(path) .unwrap(); file.write_all(contents.as_ref()).unwrap(); } /// Rebase descendants of the rewritten commits. Returns map of original commit /// ID to rebased (or abandoned parent) commit ID. pub fn rebase_descendants_with_options_return_map( repo: &mut MutableRepo, options: &RebaseOptions, ) -> HashMap { let mut rebased: HashMap = HashMap::new(); repo.rebase_descendants_with_options(options, |old_commit, rebased_commit| { let old_commit_id = old_commit.id().clone(); let new_commit_id = match rebased_commit { RebasedCommit::Rewritten(new_commit) => new_commit.id().clone(), RebasedCommit::Abandoned { parent_id } => parent_id, }; rebased.insert(old_commit_id, new_commit_id); }) .block_on() .unwrap(); rebased } fn assert_in_rebased_map( repo: &impl Repo, rebased: &HashMap, expected_old_commit: &Commit, ) -> Commit { let new_commit_id = rebased.get(expected_old_commit.id()).unwrap_or_else(|| { panic!( "Expected commit to have been rebased: {}", expected_old_commit.id().hex() ) }); repo.store().get_commit(new_commit_id).unwrap().clone() } pub fn assert_rebased_onto( repo: &impl Repo, rebased: &HashMap, expected_old_commit: &Commit, expected_new_parent_ids: &[&CommitId], ) -> Commit { let new_commit = assert_in_rebased_map(repo, rebased, expected_old_commit); assert_eq!( new_commit.parent_ids().to_vec(), expected_new_parent_ids .iter() .map(|x| (*x).clone()) .collect_vec() ); assert_eq!(new_commit.change_id(), expected_old_commit.change_id()); new_commit } /// Maps children of an abandoned commit to a new rebase target. /// /// If `expected_old_commit` was abandoned, the `rebased` map indicates the /// commit the children of `expected_old_commit` should be rebased to, which /// would have a different change id. This happens when the EmptyBehavior in /// RebaseOptions is not the default; because of the details of the /// implementation this returned parent commit is always singular. pub fn assert_abandoned_with_parent( repo: &impl Repo, rebased: &HashMap, expected_old_commit: &Commit, expected_new_parent_id: &CommitId, ) -> Commit { let new_parent_commit = assert_in_rebased_map(repo, rebased, expected_old_commit); assert_eq!(new_parent_commit.id(), expected_new_parent_id); assert_ne!( new_parent_commit.change_id(), expected_old_commit.change_id() ); new_parent_commit } pub fn assert_no_forgotten_test_files(test_dir: &Path) { // Parse the integration tests' main modules from the Cargo manifest. let manifest = { let file_path = test_dir.parent().unwrap().join("Cargo.toml"); let text = fs::read_to_string(&file_path).unwrap(); toml_edit::Document::parse(text).unwrap() }; let test_bin_mods = if let Some(item) = manifest.get("test") { let tables = item.as_array_of_tables().unwrap(); tables .iter() .map(|test| test.get("name").unwrap().as_str().unwrap().to_owned()) .collect() } else { vec![] }; // Add to that all submodules which are declared in the main test modules via // `mod`. let mut test_mods: HashSet<_> = test_bin_mods .iter() .flat_map(|test_mod| { let test_mod_path = test_dir.join(test_mod).with_extension("rs"); let test_mod_contents = fs::read_to_string(&test_mod_path).unwrap(); test_mod_contents .lines() .map(|line| line.trim_start_matches("pub ")) .filter_map(|line| line.strip_prefix("mod")) .filter_map(|line| line.strip_suffix(";")) .map(|line| line.trim().to_string()) .collect_vec() }) .collect(); test_mods.extend(test_bin_mods); // Gather list of Rust source files in test directory for comparison. let test_mod_files: HashSet<_> = fs::read_dir(test_dir) .unwrap() .map(|entry| entry.unwrap().path()) .filter(|path| path.extension().is_some_and(|ext| ext == "rs")) .filter_map(|path| { path.file_stem() .and_then(|stem| stem.to_os_string().into_string().ok()) }) .collect(); assert!( test_mod_files.is_subset(&test_mods), "the following test source files are not declared as integration tests nor included as \ submodules of one: {}", test_mod_files .difference(&test_mods) .map(|mod_stem| format!("{mod_stem}.rs")) .join(", "), ); } pub trait CommitBuilderExt { fn write_unwrap(self) -> Commit; } impl CommitBuilderExt for CommitBuilder<'_> { fn write_unwrap(self) -> Commit { self.write().block_on().unwrap() } } // Creates a CopyHistory graph and writes it to the test repo, returning the // resulting IDs and Histories. Items in `paths_and_parents` can refer to parent // paths occurring earlier in the list pub fn write_copy_histories( repo: &Arc, paths_and_parents: &[(&RepoPath, Vec<&RepoPath>)], ) -> HashMap { let mut ids = HashMap::::new(); let mut histories = HashMap::::new(); for (path, parents) in paths_and_parents { let history = CopyHistory { current_path: (*path).to_owned(), parents: parents.iter().map(|parent| ids[*parent].clone()).collect(), salt: vec![], }; let id = repo .store() .backend() .write_copy(&history) .block_on() .unwrap(); ids.insert((*path).to_owned(), id); histories.insert((*path).to_owned(), history); } histories } /// Returns true if the directory appears to be on a filesystem with strict /// UTF-8 validation, as on ZFS with the `utf8only=on` property set. #[cfg(unix)] pub fn check_strict_utf8_fs(dir: &Path) -> bool { use std::os::unix::ffi::OsStrExt as _; let test_file_normal = tempfile::Builder::new() .prefix(OsStr::from_bytes(b"strict-utf8-normal-")) .tempfile_in(dir); assert!(test_file_normal.is_ok()); let test_file_invalid = tempfile::Builder::new() .prefix(OsStr::from_bytes(b"strict-utf8-\xe0-")) .tempfile_in(dir); test_file_invalid.is_err() }