956 lines · 30.2 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::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 tokio::io::AsyncReadExt as _; use crate::test_backend::TestBackendFactory; pub mod git;pub mod proptest;pub mod test_backend; /// Convenient return type for test functions.pub type TestResult<T = ()> = eyre::Result<T>; 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./// <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()            .block_on()            .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(),        )        .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<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,        )        .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().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<MergedTree, SnapshotError> {        let (tree, _stats) = self.snapshot_with_options(&empty_snapshot_options())?;        Ok(tree)    }} 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").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<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,            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<u8>,    executable: bool,    copy_history: Option<CopyHistory>,    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()`

Lines 1–500 of 956.