use crate::exhaustive_sync::trial::Action::*; use crate::exhaustive_sync::trial::Status::{Failed, Ready, Running, Succeeded}; use crate::exhaustive_sync::utils::{find_by_name, random_filename, random_utf8}; use lb_rs::service::api_service::no_network::{CoreIP, InProcess}; use lb_rs::CoreError; use lockbook_server_lib::config::AdminConfig; use lockbook_shared::file::ShareMode; use lockbook_shared::file_metadata::FileType::{Document, Folder, Link}; use std::fmt::{Debug, Formatter}; use std::time::Instant; use std::{fs, thread}; use test_utils::*; use uuid::Uuid; use variant_count::VariantCount; use super::coordinator::ThreadID; use super::trial_cache::TrialCache; #[derive(VariantCount, Debug, Clone, PartialEq, Eq, Hash)] pub enum Action { NewDocument { user_index: usize, device_index: usize, parent: String, name: String }, NewMarkdownDocument { user_index: usize, device_index: usize, parent: String, name: String }, NewFolder { user_index: usize, device_index: usize, parent: String, name: String }, UpdateDocument { user_index: usize, device_index: usize, name: String, new_content: String }, RenameFile { user_index: usize, device_index: usize, name: String, new_name: String }, MoveFile { user_index: usize, device_index: usize, doc_name: String, destination_name: String }, DeleteFile { user_index: usize, device_index: usize, name: String }, ShareFile { user_index: usize, device_index: usize, target_user_index: usize, name: String }, NewLink { user_index: usize, device_index: usize, parent: String, name: String, id: Uuid }, DeleteShare { user_index: usize, device_index: usize, id: Uuid }, SyncAndCheck, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum Status { Ready, Running, Succeeded, Failed(String), } impl Status { pub fn failed(&self) -> bool { match self { Ready | Running | Succeeded => false, Failed(_) => true, } } } #[derive(Clone)] pub struct Trial { pub id: Uuid, pub devices_by_user: Vec>, pub target_devices_by_user: Vec, pub target_steps: usize, pub steps: Vec, pub completed_steps: usize, pub status: Status, pub start_time: Instant, pub end_time: Instant, } impl Debug for Trial { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut result = &mut f.debug_struct("Trial"); result = result.field("id", &self.id); result = result.field("target_clients", &self.target_devices_by_user); result = result.field("target_steps", &self.target_steps); result = result.field("steps", &self.steps); result = result.field("completed_steps", &self.completed_steps); result = result.field("status", &self.status); result = result.field("start_time", &self.start_time); result = result.field("end_time", &self.end_time); result.finish() } } impl Trial { fn create_clients(&mut self) -> Result<(), Status> { let mut usernames = Vec::new(); for _user_index in 0..self.target_devices_by_user.len() { usernames.push(random_name()); } let server = InProcess::init( test_config(), AdminConfig { admins: usernames.iter().cloned().collect() }, ); for (user_index, target_devices) in self.target_devices_by_user.iter().enumerate() { let mut devices_by_user = Vec::new(); let mut maybe_account_string: Option = None; for _device_index in 0..*target_devices { let device = CoreIP::init_in_process(&test_config(), server.clone()); if let Some(ref account_string) = maybe_account_string { device .import_account(account_string, Some(&url())) .map_err(|err| Failed(format!("{:#?}", err)))?; device .sync(None) .map_err(|err| Failed(format!("{:#?}", err)))?; } else { device .create_account(&usernames[user_index], &url(), false) .map_err(|err| Failed(format!("failed to create account: {:#?}", err)))?; maybe_account_string = Some(device.export_account().unwrap()); } devices_by_user.push(device); } self.devices_by_user.push(devices_by_user); } Ok(()) } fn perform_all_known_actions(&mut self) { let mut additional_completed_steps = 0; 'steps: for index in self.completed_steps..self.steps.len() { let step = self.steps[index].clone(); additional_completed_steps += 1; match step { NewDocument { user_index, device_index, parent, name } => { let db = &self.devices_by_user[user_index][device_index]; let parent = find_by_name(db, &parent).id; if let Err(err) = db.create_file(&name, parent, Document) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } NewMarkdownDocument { user_index, device_index, parent, name } => { let db = &self.devices_by_user[user_index][device_index]; let parent = find_by_name(db, &parent).id; if let Err(err) = db.create_file(&name, parent, Document) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } NewFolder { user_index, device_index, parent, name } => { let db = &self.devices_by_user[user_index][device_index]; let parent = find_by_name(db, &parent).id; if let Err(err) = db.create_file(&name, parent, Folder) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } UpdateDocument { user_index, device_index, name, new_content } => { let db = &self.devices_by_user[user_index][device_index]; let doc = find_by_name(db, &name).id; if let Err(err) = db.write_document(doc, new_content.as_bytes()) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } RenameFile { user_index, device_index, name, new_name } => { let db = &self.devices_by_user[user_index][device_index]; let doc = find_by_name(db, &name).id; if let Err(err) = db.rename_file(doc, &new_name) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } MoveFile { user_index, device_index, doc_name: non_folder_name, destination_name, } => { let db = &self.devices_by_user[user_index][device_index]; let non_folder = find_by_name(db, &non_folder_name).id; let dest = find_by_name(db, &destination_name).id; let move_file_result = db.move_file(non_folder, dest); match move_file_result { Ok(()) => {} Err(err) => match err.kind { CoreError::LinkInSharedFolder | CoreError::FolderMovedIntoSelf => {} _ => { self.status = Failed(format!("{:#?}", err)); break 'steps; } }, } } DeleteFile { user_index, device_index, name } => { let db = &self.devices_by_user[user_index][device_index]; let file = find_by_name(db, &name).id; if let Err(err) = db.delete_file(file) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } ShareFile { user_index, device_index, target_user_index, name } => { let db = &self.devices_by_user[user_index][device_index]; let file = find_by_name(db, &name).id; let target_username = match &self.devices_by_user[target_user_index][0].get_account() { Ok(account) => account.username.clone(), Err(err) => { self.status = Failed(format!("{:#?}", err)); break 'steps; } }; if let Err(err) = db.share_file(file, &target_username, ShareMode::Write) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } NewLink { user_index, device_index, parent, name, id: target } => { let db = &self.devices_by_user[user_index][device_index]; let parent = find_by_name(db, &parent).id; if let Err(err) = db.create_file(&name, parent, Link { target }) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } DeleteShare { user_index, device_index, id } => { let db = &self.devices_by_user[user_index][device_index]; if let Err(err) = db.delete_pending_share(id) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } SyncAndCheck => { for _ in 0..2 { for user_index in 0..self.target_devices_by_user.len() { for device_index in 0..self.target_devices_by_user[user_index] { let device = &self.devices_by_user[user_index][device_index]; if let Err(err) = device.sync(None) { self.status = Failed(format!("{:#?}", err)); break 'steps; } } } } for user_index in 0..self.target_devices_by_user.len() { for device_index in 0..self.target_devices_by_user[user_index] { let device = &self.devices_by_user[user_index][device_index]; if let Err(err) = device.validate() { self.status = Failed(format!("Repo integrity compromised: {:#?}", err)); break 'steps; } if !device.calculate_work().unwrap().work_units.is_empty() { self.status = Failed(format!( "work units not empty, client: {}", device.get_config().unwrap().writeable_path )); break 'steps; } for compare_device_index in 0..self.target_devices_by_user[user_index] { if compare_device_index != device_index { let compare_device = &self.devices_by_user[user_index][compare_device_index]; if !dbs_equal(device, compare_device) { self.status = Failed(format!( "db {} is not equal to {} after a sync.", device.get_config().unwrap().writeable_path, compare_device.get_config().unwrap().writeable_path, )); break 'steps; } } } } } match self.devices_by_user[0][0].admin_validate_server() { Ok(validations) => { if validations != Default::default() { self.status = Failed(format!( "Server reported validation failures: {:#?}", validations )); break 'steps; } } Err(err) => { self.status = Failed(format!("{:#?}", err)); break 'steps; } } } } } self.completed_steps += additional_completed_steps; if self.status == Running && self.completed_steps == self.target_steps { self.status = Succeeded; } } fn generate_mutations(&self) -> Vec { let mut mutants: Vec = vec![]; if self.status != Running { return mutants; } for user_index in 0..self.target_devices_by_user.len() { for device_index in 0..self.target_devices_by_user[user_index] { let device = &self.devices_by_user[user_index][device_index]; let all_files = device.list_metadatas().unwrap(); let mut folders = all_files.clone(); folders.retain(|f| f.is_folder()); let mut docs = all_files.clone(); docs.retain(|f| f.is_document()); let mut not_shared_by_me_files = all_files.clone(); not_shared_by_me_files.retain(|f| { !f.shares .iter() .all(|s| s.shared_by == device.get_account().unwrap().username) }); let pending_shares = device.get_pending_shares().unwrap(); let mut shared_with_me_files = all_files .clone() .into_iter() .chain(pending_shares.clone()) .collect::>(); shared_with_me_files.retain(|f| { f.shares .iter() .any(|s| s.shared_with == device.get_account().unwrap().username) }); for file in all_files.clone() { if file.id != file.parent { mutants.push(self.create_mutation(RenameFile { user_index, device_index, name: file.name.clone(), new_name: random_filename(), })); mutants.push(self.create_mutation(DeleteFile { user_index, device_index, name: file.name.clone(), })); } } for not_shared_by_me_file in not_shared_by_me_files.clone() { if not_shared_by_me_file.id != not_shared_by_me_file.parent { for sharee_index in 0..self.target_devices_by_user.len() { if user_index == sharee_index { continue; } mutants.push(self.create_mutation(ShareFile { user_index, device_index, target_user_index: sharee_index, name: not_shared_by_me_file.name.clone(), })); } } } for folder in folders.clone() { let parent_name = if folder.id == folder.parent { "root".to_string() } else { folder.name }; mutants.push(self.create_mutation(NewDocument { user_index, device_index, parent: parent_name.clone(), name: random_filename(), })); mutants.push(self.create_mutation(NewMarkdownDocument { user_index, device_index, parent: parent_name.clone(), name: random_filename() + ".md", })); mutants.push(self.create_mutation(NewFolder { user_index, device_index, parent: parent_name.clone(), name: random_filename(), })); for file in all_files.clone() { if file.id != file.parent { mutants.push(self.create_mutation(MoveFile { user_index, device_index, doc_name: file.name.clone(), destination_name: parent_name.clone(), })) } } for pending_share in pending_shares.clone() { mutants.push(self.create_mutation(NewLink { user_index, device_index, parent: parent_name.clone(), name: random_filename(), id: pending_share.id, })); } } for doc in docs { mutants.push(self.create_mutation(UpdateDocument { user_index, device_index, name: doc.name.clone(), new_content: random_utf8(), })); } for shared_file in shared_with_me_files { mutants.push(self.create_mutation(DeleteShare { user_index, device_index, id: shared_file.id, })); } } } mutants.push(self.create_mutation(SyncAndCheck)); mutants } pub fn execute(&mut self, th_id: usize, cache: &TrialCache) -> Vec { self.start_time = Instant::now(); if cache.ready() { let (resp, steps) = cache.get(&self.steps); self.devices_by_user = resp; self.completed_steps = steps; } self.status = if self.devices_by_user.is_empty() { if let Err(err) = self.create_clients() { err } else { Running } } else { Running }; cache.populate(self); self.persist(th_id); let mut all_mutations = vec![]; while self.status == Running { self.perform_all_known_actions(); let mut mutations = self.generate_mutations(); if let Some(next_action) = mutations.pop() { self.steps.push(next_action.steps.last().unwrap().clone()); all_mutations.extend(mutations); cache.populate(self); } } self.end_time = Instant::now(); // self.cleanup(); all_mutations } fn create_mutation(&self, new_action: Action) -> Trial { let mut clone = self.clone(); clone.steps.push(new_action); clone.status = Ready; clone.completed_steps = 0; clone.start_time = Instant::now(); clone.end_time = Instant::now(); clone.devices_by_user = vec![]; clone.id = Uuid::new_v4(); clone } } impl Trial { pub fn file_name(&self, thread: ThreadID) -> String { if self.failed() { format!("trials/{}/{}.fail", thread, self.id) } else { format!("trials/{}/{}", thread, self.id) } } pub fn persist(&self, thread: ThreadID) { fs::write(self.file_name(thread), format!("{:#?}", self)).unwrap_or_else(|err| { eprintln!("Unable to write file: {}/{:?}, {:?}", thread, self, err) }); } pub fn maybe_cleanup(&self, thread: ThreadID) { match self.status { Failed(_) => self.persist(thread), _ => fs::remove_file(self.file_name(thread)).unwrap_or_else(|err| { eprintln!("Unable to cleanup file: {}/{}, {:?}", thread, self.id, err) }), } } } impl Default for Trial { fn default() -> Self { Self { id: Uuid::new_v4(), devices_by_user: vec![], target_devices_by_user: vec![1, 2, 3], target_steps: 10, steps: vec![], completed_steps: 0, status: Ready, start_time: Instant::now(), end_time: Instant::now(), } } } impl Drop for Trial { fn drop(&mut self) { if thread::panicking() { println!("{} is stuck in {:?}", self.id, self.status); } } } impl Trial { pub fn failed(&self) -> bool { matches!(self.status, Failed(_)) } }