mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-22 09:01:45 +03:00
2eaba14819
The idea is that we don't parallelize over a channel anymore, but instead just process filesystem events, one at a time. This would allow each handler to become a function that gets its state passed, and makes all the necessary calls verbatim, which in turn makes it easy to follow what's happening. If anything becomes to slow due to the serialization of event processing, selective parallelization can be re-added.
457 lines
13 KiB
Rust
457 lines
13 KiB
Rust
use std::{collections::HashMap, path, thread, time};
|
|
|
|
use anyhow::Result;
|
|
use gitbutler_core::{
|
|
deltas::{self, operations::Operation},
|
|
projects::{self, ApiProject, ProjectId},
|
|
reader,
|
|
sessions::{self, SessionId},
|
|
};
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
use gitbutler_testsupport::{init_opts_bare, Case, Suite};
|
|
|
|
fn new_test_remote_repository() -> Result<(git2::Repository, TempDir)> {
|
|
let tmp = tempfile::tempdir()?;
|
|
let repo_a = git2::Repository::init_opts(&tmp, &init_opts_bare())?;
|
|
Ok((repo_a, tmp))
|
|
}
|
|
|
|
#[test]
|
|
fn get_current_session_writer_should_use_existing_session() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case { gb_repository, .. } = &suite.new_case();
|
|
|
|
let current_session_1 = gb_repository.get_or_create_current_session()?;
|
|
let current_session_2 = gb_repository.get_or_create_current_session()?;
|
|
assert_eq!(current_session_1.id, current_session_2.id);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn must_not_return_init_session() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case { gb_repository, .. } = &suite.new_case();
|
|
|
|
assert!(gb_repository.get_current_session()?.is_none());
|
|
|
|
let iter = gb_repository.get_sessions_iterator()?;
|
|
assert_eq!(iter.count(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn must_not_flush_without_current_session() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case();
|
|
|
|
let session = gb_repository.flush(project_repository, None)?;
|
|
assert!(session.is_none());
|
|
|
|
let iter = gb_repository.get_sessions_iterator()?;
|
|
assert_eq!(iter.count(), 0);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn non_empty_repository() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case_with_files(HashMap::from([(path::PathBuf::from("test.txt"), "test")]));
|
|
|
|
gb_repository.get_or_create_current_session()?;
|
|
gb_repository.flush(project_repository, None)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn must_flush_current_session() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case();
|
|
|
|
gb_repository.get_or_create_current_session()?;
|
|
|
|
let session = gb_repository.flush(project_repository, None)?;
|
|
assert!(session.is_some());
|
|
|
|
let iter = gb_repository.get_sessions_iterator()?;
|
|
assert_eq!(iter.count(), 1);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn list_deltas_from_current_session() -> Result<()> {
|
|
let suite = Suite::default();
|
|
let Case { gb_repository, .. } = &suite.new_case();
|
|
|
|
let current_session = gb_repository.get_or_create_current_session()?;
|
|
let writer = deltas::Writer::new(gb_repository)?;
|
|
writer.write(
|
|
"test.txt",
|
|
&vec![deltas::Delta {
|
|
operations: vec![Operation::Insert((0, "Hello World".to_string()))],
|
|
timestamp_ms: 0,
|
|
}],
|
|
)?;
|
|
|
|
let session_reader = sessions::Reader::open(gb_repository, ¤t_session)?;
|
|
let deltas_reader = deltas::Reader::new(&session_reader);
|
|
let deltas = deltas_reader.read(None)?;
|
|
|
|
assert_eq!(deltas.len(), 1);
|
|
assert_eq!(
|
|
deltas[&path::PathBuf::from("test.txt")][0].operations.len(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
deltas[&path::PathBuf::from("test.txt")][0].operations[0],
|
|
Operation::Insert((0, "Hello World".to_string()))
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn list_deltas_from_flushed_session() {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case();
|
|
|
|
let writer = deltas::Writer::new(gb_repository).unwrap();
|
|
writer
|
|
.write(
|
|
"test.txt",
|
|
&vec![deltas::Delta {
|
|
operations: vec![Operation::Insert((0, "Hello World".to_string()))],
|
|
timestamp_ms: 0,
|
|
}],
|
|
)
|
|
.unwrap();
|
|
let session = gb_repository.flush(project_repository, None).unwrap();
|
|
|
|
let session_reader = sessions::Reader::open(gb_repository, &session.unwrap()).unwrap();
|
|
let deltas_reader = deltas::Reader::new(&session_reader);
|
|
let deltas = deltas_reader.read(None).unwrap();
|
|
|
|
assert_eq!(deltas.len(), 1);
|
|
assert_eq!(
|
|
deltas[&path::PathBuf::from("test.txt")][0].operations.len(),
|
|
1
|
|
);
|
|
assert_eq!(
|
|
deltas[&path::PathBuf::from("test.txt")][0].operations[0],
|
|
Operation::Insert((0, "Hello World".to_string()))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn list_files_from_current_session() {
|
|
let suite = Suite::default();
|
|
let Case { gb_repository, .. } = &suite.new_case_with_files(HashMap::from([(
|
|
path::PathBuf::from("test.txt"),
|
|
"Hello World",
|
|
)]));
|
|
|
|
let current = gb_repository.get_or_create_current_session().unwrap();
|
|
let reader = sessions::Reader::open(gb_repository, ¤t).unwrap();
|
|
let files = reader.files(None).unwrap();
|
|
|
|
assert_eq!(files.len(), 1);
|
|
assert_eq!(
|
|
files[&path::PathBuf::from("test.txt")],
|
|
reader::Content::UTF8("Hello World".to_string())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn list_files_from_flushed_session() {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case_with_files(HashMap::from([(
|
|
path::PathBuf::from("test.txt"),
|
|
"Hello World",
|
|
)]));
|
|
|
|
gb_repository.get_or_create_current_session().unwrap();
|
|
let session = gb_repository
|
|
.flush(project_repository, None)
|
|
.unwrap()
|
|
.unwrap();
|
|
let reader = sessions::Reader::open(gb_repository, &session).unwrap();
|
|
let files = reader.files(None).unwrap();
|
|
|
|
assert_eq!(files.len(), 1);
|
|
assert_eq!(
|
|
files[&path::PathBuf::from("test.txt")],
|
|
reader::Content::UTF8("Hello World".to_string())
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn remote_syncronization() {
|
|
// first, crate a remote, pretending it's a cloud
|
|
let (cloud, _tmp) = new_test_remote_repository().unwrap();
|
|
let api_project = ApiProject {
|
|
name: "test-sync".to_string(),
|
|
description: None,
|
|
repository_id: "123".to_string(),
|
|
git_url: cloud.path().to_str().unwrap().to_string(),
|
|
code_git_url: None,
|
|
created_at: 0_i32.to_string(),
|
|
updated_at: 0_i32.to_string(),
|
|
sync: true,
|
|
};
|
|
|
|
let suite = Suite::default();
|
|
let user = suite.sign_in();
|
|
|
|
// create first local project, add files, deltas and flush a session
|
|
let case_one = suite.new_case_with_files(HashMap::from([(
|
|
path::PathBuf::from("test.txt"),
|
|
"Hello World",
|
|
)]));
|
|
suite
|
|
.projects
|
|
.update(&projects::UpdateRequest {
|
|
id: case_one.project.id,
|
|
api: Some(api_project.clone()),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let case_one = case_one.refresh(&suite);
|
|
|
|
let writer = deltas::Writer::new(&case_one.gb_repository).unwrap();
|
|
writer
|
|
.write(
|
|
"test.txt",
|
|
&vec![deltas::Delta {
|
|
operations: vec![Operation::Insert((0, "Hello World".to_string()))],
|
|
timestamp_ms: 0,
|
|
}],
|
|
)
|
|
.unwrap();
|
|
let session_one = case_one
|
|
.gb_repository
|
|
.flush(&case_one.project_repository, Some(&user))
|
|
.unwrap()
|
|
.unwrap();
|
|
case_one.gb_repository.push(Some(&user)).unwrap();
|
|
|
|
// create second local project, fetch it and make sure session is there
|
|
let case_two = suite.new_case();
|
|
suite
|
|
.projects
|
|
.update(&projects::UpdateRequest {
|
|
id: case_two.project.id,
|
|
api: Some(api_project.clone()),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let case_two = case_two.refresh(&suite);
|
|
|
|
case_two.gb_repository.fetch(Some(&user)).unwrap();
|
|
|
|
// now it should have the session from the first local project synced
|
|
let sessions_two = case_two
|
|
.gb_repository
|
|
.get_sessions_iterator()
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(sessions_two.len(), 1);
|
|
assert_eq!(sessions_two[0].id, session_one.id);
|
|
|
|
let session_reader = sessions::Reader::open(&case_two.gb_repository, &sessions_two[0]).unwrap();
|
|
let deltas_reader = deltas::Reader::new(&session_reader);
|
|
let deltas = deltas_reader.read(None).unwrap();
|
|
let files = session_reader.files(None).unwrap();
|
|
assert_eq!(deltas.len(), 1);
|
|
assert_eq!(files.len(), 1);
|
|
assert_eq!(
|
|
files[&path::PathBuf::from("test.txt")],
|
|
reader::Content::UTF8("Hello World".to_string())
|
|
);
|
|
assert_eq!(
|
|
deltas[&path::PathBuf::from("test.txt")],
|
|
vec![deltas::Delta {
|
|
operations: vec![Operation::Insert((0, "Hello World".to_string()))],
|
|
timestamp_ms: 0,
|
|
}]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn remote_sync_order() {
|
|
// first, crate a remote, pretending it's a cloud
|
|
let (cloud, _tmp) = new_test_remote_repository().unwrap();
|
|
let api_project = projects::ApiProject {
|
|
name: "test-sync".to_string(),
|
|
description: None,
|
|
repository_id: "123".to_string(),
|
|
git_url: cloud.path().to_str().unwrap().to_string(),
|
|
code_git_url: None,
|
|
created_at: 0_i32.to_string(),
|
|
updated_at: 0_i32.to_string(),
|
|
sync: true,
|
|
};
|
|
|
|
let suite = Suite::default();
|
|
|
|
let case_one = suite.new_case();
|
|
suite
|
|
.projects
|
|
.update(&projects::UpdateRequest {
|
|
id: case_one.project.id,
|
|
api: Some(api_project.clone()),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let case_one = case_one.refresh(&suite);
|
|
|
|
let case_two = suite.new_case();
|
|
suite
|
|
.projects
|
|
.update(&projects::UpdateRequest {
|
|
id: case_two.project.id,
|
|
api: Some(api_project.clone()),
|
|
..Default::default()
|
|
})
|
|
.await
|
|
.unwrap();
|
|
let case_two = case_two.refresh(&suite);
|
|
|
|
let user = suite.sign_in();
|
|
|
|
// create session in the first project
|
|
case_one
|
|
.gb_repository
|
|
.get_or_create_current_session()
|
|
.unwrap();
|
|
let session_one_first = case_one
|
|
.gb_repository
|
|
.flush(&case_one.project_repository, Some(&user))
|
|
.unwrap()
|
|
.unwrap();
|
|
case_one.gb_repository.push(Some(&user)).unwrap();
|
|
|
|
thread::sleep(time::Duration::from_secs(1));
|
|
|
|
// create session in the second project
|
|
case_two
|
|
.gb_repository
|
|
.get_or_create_current_session()
|
|
.unwrap();
|
|
let session_two_first = case_two
|
|
.gb_repository
|
|
.flush(&case_two.project_repository, Some(&user))
|
|
.unwrap()
|
|
.unwrap();
|
|
case_two.gb_repository.push(Some(&user)).unwrap();
|
|
|
|
thread::sleep(time::Duration::from_secs(1));
|
|
|
|
// create second session in the first project
|
|
case_one
|
|
.gb_repository
|
|
.get_or_create_current_session()
|
|
.unwrap();
|
|
let session_one_second = case_one
|
|
.gb_repository
|
|
.flush(&case_one.project_repository, Some(&user))
|
|
.unwrap()
|
|
.unwrap();
|
|
case_one.gb_repository.push(Some(&user)).unwrap();
|
|
|
|
thread::sleep(time::Duration::from_secs(1));
|
|
|
|
// create second session in the second project
|
|
case_two
|
|
.gb_repository
|
|
.get_or_create_current_session()
|
|
.unwrap();
|
|
let session_two_second = case_two
|
|
.gb_repository
|
|
.flush(&case_two.project_repository, Some(&user))
|
|
.unwrap()
|
|
.unwrap();
|
|
case_two.gb_repository.push(Some(&user)).unwrap();
|
|
|
|
case_one.gb_repository.fetch(Some(&user)).unwrap();
|
|
let sessions_one = case_one
|
|
.gb_repository
|
|
.get_sessions_iterator()
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect::<Vec<_>>();
|
|
|
|
case_two.gb_repository.fetch(Some(&user)).unwrap();
|
|
let sessions_two = case_two
|
|
.gb_repository
|
|
.get_sessions_iterator()
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect::<Vec<_>>();
|
|
|
|
// make sure the sessions are the same on both repos
|
|
assert_eq!(sessions_one.len(), 4);
|
|
assert_eq!(sessions_two, sessions_one);
|
|
|
|
assert_eq!(sessions_one[0].id, session_two_second.id);
|
|
assert_eq!(sessions_one[1].id, session_one_second.id);
|
|
assert_eq!(sessions_one[2].id, session_two_first.id);
|
|
assert_eq!(sessions_one[3].id, session_one_first.id);
|
|
}
|
|
|
|
#[test]
|
|
fn gitbutler_file() {
|
|
let suite = Suite::default();
|
|
let Case {
|
|
gb_repository,
|
|
project_repository,
|
|
..
|
|
} = &suite.new_case();
|
|
|
|
let session = gb_repository.get_or_create_current_session().unwrap();
|
|
|
|
let gitbutler_file_path = project_repository.path().join(".git/gitbutler.json");
|
|
assert!(gitbutler_file_path.exists());
|
|
|
|
let file_content: serde_json::Value =
|
|
serde_json::from_str(&std::fs::read_to_string(&gitbutler_file_path).unwrap()).unwrap();
|
|
let sid: SessionId = file_content["sessionId"].as_str().unwrap().parse().unwrap();
|
|
assert_eq!(sid, session.id);
|
|
|
|
let pid: ProjectId = file_content["repositoryId"]
|
|
.as_str()
|
|
.unwrap()
|
|
.parse()
|
|
.unwrap();
|
|
assert_eq!(pid, project_repository.project().id);
|
|
}
|