761 lines · 24.0 KB · Rust
RawDownload
// 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 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::CopyId;use jj_lib::backend::FileId;use jj_lib::backend::MergedTreeId;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::git_backend::GitBackend;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 tokio::io::AsyncReadExt as _; use crate::test_backend::TestBackendFactory; pub mod git;pub mod proptest;pub mod test_backend; // 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() {    unsafe {        // Prevent GitBackend from loading user and system configurations. For        // gitoxide API use in tests, Config::isolated() is probably better.        env::set_var("GIT_CONFIG_SYSTEM", "/dev/null");        env::set_var("GIT_CONFIG_GLOBAL", "/dev/null");        // gitoxide uses "main" as the default branch name, whereas git        // uses "master".        env::set_var("GIT_CONFIG_KEY_0", "init.defaultBranch");        env::set_var("GIT_CONFIG_VALUE_0", "master");        env::set_var("GIT_CONFIG_COUNT", "1");    }} 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()} /// Panic if `CI` environment variable is set to a non-empty value////// Most CI environments set this variable automatically. See e.g./// <https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables>#[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 PATHpub fn is_external_tool_installed(program_name: impl AsRef<OsStr>) -> 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<ReadonlyRepo> {        RepoLoader::init_from_file_system(settings, repo_path, &self.default_store_factories())            .unwrap()            .load_at_head()            .unwrap()    }} pub struct TestRepo {    pub env: TestEnvironment,    pub repo: Arc<ReadonlyRepo>,    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<Box<dyn Backend>, 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(),        )        .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<ReadonlyRepo>,} 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,        )        .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().unwrap();        let (tree_id, stats) = locked_ws.locked_wc().snapshot(options)?;        // arbitrary operation id        locked_ws.finish(self.repo.op_id().clone()).unwrap();        Ok((self.repo.store().get_root_tree(&tree_id).unwrap(), stats))    }     /// Like `snapshot_with_option()` but with default options    pub fn snapshot(&mut self) -> Result<MergedTree, SnapshotError> {        let (tree_id, _stats) = self.snapshot_with_options(&SnapshotOptions::empty_for_test())?;        Ok(tree_id)    }} pub fn commit_transactions(txs: Vec<Transaction>) -> Arc<ReadonlyRepo> {    let repo_loader = txs[0].base_repo().loader().clone();    let mut op_ids = vec![];    for tx in txs {        op_ids.push(tx.commit("test").unwrap().op_id().clone());        std::thread::sleep(std::time::Duration::from_millis(1));    }    let repo = repo_loader.load_at_head().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<String>) -> RepoPathBuf {    RepoPathBuf::from_internal_string(value).unwrap()} pub fn read_file(store: &Store, path: &RepoPath, id: &FileId) -> Vec<u8> {    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<Store>,    tree_builder: TreeBuilder,} impl TestTreeBuilder {    pub fn new(store: Arc<Store>) -> 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,        }    }     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().unwrap();        self.store.get_tree(RepoPathBuf::root(), &id).unwrap()    }     pub fn write_merged_tree(self) -> MergedTree {        MergedTree::resolved(self.write_single_tree())    }} pub struct TestTreeFileEntryBuilder<'a> {    tree_builder: &'a mut TreeBuilder,    path: RepoPathBuf,    contents: Vec<u8>,    executable: bool,} impl TestTreeFileEntryBuilder<'_> {    pub fn executable(mut self, executable: bool) -> Self {        self.executable = executable;        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());        self.tree_builder.set(            path,            TreeValue::File {                id,                executable: self.executable,                copy_id: CopyId::placeholder(),            },        );    }} pub fn create_single_tree_with(    repo: &Arc<ReadonlyRepo>,    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<ReadonlyRepo>, 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<ReadonlyRepo>,    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<ReadonlyRepo>, path_contents: &[(&RepoPath, &str)]) -> MergedTree {    create_tree_with(repo, |builder| {        for (path, contents) in path_contents {

Lines 1–500 of 761.