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}; mod repository { use std::path::PathBuf; use anyhow::Result; use pretty_assertions::assert_eq; use gitbutler_testsupport::{Case, Suite}; #[test] fn alternates_file_being_set() -> Result<()> { let suite = Suite::default(); let Case { gb_repository, project_repository, .. } = &suite.new_case(); let file_content = std::fs::read_to_string( gb_repository .git_repository_path() .join("objects/info/alternates"), )?; let file_content = PathBuf::from(file_content.trim()); let project_path = project_repository.path().to_path_buf().join(".git/objects"); assert_eq!(file_content, project_path); Ok(()) } } fn new_test_remote_repository() -> Result<(git2::Repository, TempDir)> { let tmp = tempfile::tempdir()?; let path = tmp.path().to_str().unwrap().to_string(); let repo_a = git2::Repository::init_opts(path, &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::>(); 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::>(); case_two.gb_repository.fetch(Some(&user)).unwrap(); let sessions_two = case_two .gb_repository .get_sessions_iterator() .unwrap() .map(Result::unwrap) .collect::>(); // 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); }