Add env vars to store and load test plan from JSON files

Max Brunsfeld created

Change summary

crates/collab/src/tests/randomized_integration_tests.rs | 178 +++++++++-
1 file changed, 153 insertions(+), 25 deletions(-)

Detailed changes

crates/collab/src/tests/randomized_integration_tests.rs 🔗

@@ -15,6 +15,7 @@ use lsp::FakeLanguageServer;
 use parking_lot::Mutex;
 use project::{search::SearchQuery, Project, ProjectPath};
 use rand::prelude::*;
+use serde::{Deserialize, Serialize};
 use std::{
     env,
     ops::Range,
@@ -28,18 +29,20 @@ use util::ResultExt;
 async fn test_random_collaboration(
     cx: &mut TestAppContext,
     deterministic: Arc<Deterministic>,
-    mut rng: StdRng,
+    rng: StdRng,
 ) {
     deterministic.forbid_parking();
 
     let max_peers = env::var("MAX_PEERS")
         .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
-        .unwrap_or(5);
-
+        .unwrap_or(3);
     let max_operations = env::var("OPERATIONS")
         .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
         .unwrap_or(10);
 
+    let plan_load_path = path_env_var("LOAD_PLAN");
+    let plan_save_path = path_env_var("SAVE_PLAN");
+
     let mut server = TestServer::start(&deterministic).await;
     let db = server.app_state.db.clone();
 
@@ -64,6 +67,7 @@ async fn test_random_collaboration(
             username,
             online: false,
             next_root_id: 0,
+            operation_ix: 0,
         });
     }
 
@@ -84,15 +88,12 @@ async fn test_random_collaboration(
         }
     }
 
-    let plan = Arc::new(Mutex::new(TestPlan {
-        allow_server_restarts: rng.gen_bool(0.7),
-        allow_client_reconnection: rng.gen_bool(0.7),
-        allow_client_disconnection: rng.gen_bool(0.1),
-        operation_ix: 0,
-        max_operations,
-        users,
-        rng,
-    }));
+    let plan = Arc::new(Mutex::new(TestPlan::new(rng, users, max_operations)));
+
+    if let Some(path) = &plan_load_path {
+        eprintln!("loaded plan from path {:?}", path);
+        plan.lock().load(path);
+    }
 
     let mut clients = Vec::new();
     let mut client_tasks = Vec::new();
@@ -250,6 +251,11 @@ async fn test_random_collaboration(
     deterministic.finish_waiting();
     deterministic.run_until_parked();
 
+    if let Some(path) = &plan_save_path {
+        eprintln!("saved test plan to path {:?}", path);
+        plan.lock().save(path);
+    }
+
     for (client, client_cx) in &clients {
         for guest_project in client.remote_projects().iter() {
             guest_project.read_with(client_cx, |guest_project, cx| {
@@ -760,12 +766,14 @@ async fn apply_client_operation(
 
         ClientOperation::SearchProject {
             project_root_name,
+            is_local,
             query,
             detach,
         } => {
             log::info!(
-                "{}: search project {} for {:?}{}",
+                "{}: search {} project {} for {:?}{}",
                 client.username,
+                if is_local { "local" } else { "remote" },
                 project_root_name,
                 query,
                 if detach { ", detaching" } else { ", awaiting" }
@@ -811,6 +819,8 @@ async fn apply_client_operation(
 
 struct TestPlan {
     rng: StdRng,
+    replay: bool,
+    stored_operations: Vec<StoredOperation>,
     max_operations: usize,
     operation_ix: usize,
     users: Vec<UserTestPlan>,
@@ -823,10 +833,21 @@ struct UserTestPlan {
     user_id: UserId,
     username: String,
     next_root_id: usize,
+    operation_ix: usize,
     online: bool,
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(untagged)]
+enum StoredOperation {
+    Server(Operation),
+    Client {
+        user_id: UserId,
+        operation: ClientOperation,
+    },
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
 enum Operation {
     AddConnection {
         user_id: UserId,
@@ -844,7 +865,7 @@ enum Operation {
     },
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 enum ClientOperation {
     AcceptIncomingCall,
     RejectIncomingCall,
@@ -873,6 +894,7 @@ enum ClientOperation {
     },
     SearchProject {
         project_root_name: String,
+        is_local: bool,
         query: String,
         detach: bool,
     },
@@ -913,7 +935,7 @@ enum ClientOperation {
     },
 }
 
-#[derive(Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 enum LspRequestKind {
     Rename,
     Completion,
@@ -923,15 +945,109 @@ enum LspRequestKind {
 }
 
 impl TestPlan {
+    fn new(mut rng: StdRng, users: Vec<UserTestPlan>, max_operations: usize) -> Self {
+        Self {
+            replay: false,
+            allow_server_restarts: rng.gen_bool(0.7),
+            allow_client_reconnection: rng.gen_bool(0.7),
+            allow_client_disconnection: rng.gen_bool(0.1),
+            stored_operations: Vec::new(),
+            operation_ix: 0,
+            max_operations,
+            users,
+            rng,
+        }
+    }
+
+    fn load(&mut self, path: &Path) {
+        let json = std::fs::read_to_string(path).unwrap();
+        self.replay = true;
+        self.stored_operations = serde_json::from_str(&json).unwrap();
+    }
+
+    fn save(&mut self, path: &Path) {
+        // Format each operation as one line
+        let mut json = Vec::new();
+        json.push(b'[');
+        for (i, stored_operation) in self.stored_operations.iter().enumerate() {
+            if i > 0 {
+                json.push(b',');
+            }
+            json.extend_from_slice(b"\n  ");
+            serde_json::to_writer(&mut json, stored_operation).unwrap();
+        }
+        json.extend_from_slice(b"\n]\n");
+        std::fs::write(path, &json).unwrap();
+    }
+
     async fn next_operation(
         &mut self,
         clients: &[(Rc<TestClient>, TestAppContext)],
+    ) -> Option<Operation> {
+        if self.replay {
+            while let Some(stored_operation) = self.stored_operations.get(self.operation_ix) {
+                self.operation_ix += 1;
+                if let StoredOperation::Server(operation) = stored_operation {
+                    return Some(operation.clone());
+                }
+            }
+            None
+        } else {
+            let operation = self.generate_operation(clients).await;
+            if let Some(operation) = &operation {
+                self.stored_operations
+                    .push(StoredOperation::Server(operation.clone()))
+            }
+            operation
+        }
+    }
+
+    async fn next_client_operation(
+        &mut self,
+        client: &TestClient,
+        cx: &TestAppContext,
+    ) -> Option<ClientOperation> {
+        let current_user_id = client.current_user_id(cx);
+        let user_ix = self
+            .users
+            .iter()
+            .position(|user| user.user_id == current_user_id)
+            .unwrap();
+        let user_plan = &mut self.users[user_ix];
+
+        if self.replay {
+            while let Some(stored_operation) = self.stored_operations.get(user_plan.operation_ix) {
+                user_plan.operation_ix += 1;
+                if let StoredOperation::Client { user_id, operation } = stored_operation {
+                    if user_id == &current_user_id {
+                        return Some(operation.clone());
+                    }
+                }
+            }
+            None
+        } else {
+            let operation = self
+                .generate_client_operation(current_user_id, client, cx)
+                .await;
+            if let Some(operation) = &operation {
+                self.stored_operations.push(StoredOperation::Client {
+                    user_id: current_user_id,
+                    operation: operation.clone(),
+                })
+            }
+            operation
+        }
+    }
+
+    async fn generate_operation(
+        &mut self,
+        clients: &[(Rc<TestClient>, TestAppContext)],
     ) -> Option<Operation> {
         if self.operation_ix == self.max_operations {
             return None;
         }
 
-        let operation = loop {
+        Some(loop {
             break match self.rng.gen_range(0..100) {
                 0..=29 if clients.len() < self.users.len() => {
                     let user = self
@@ -980,12 +1096,12 @@ impl TestPlan {
                 }
                 _ => continue,
             };
-        };
-        Some(operation)
+        })
     }
 
-    async fn next_client_operation(
+    async fn generate_client_operation(
         &mut self,
+        user_id: UserId,
         client: &TestClient,
         cx: &TestAppContext,
     ) -> Option<ClientOperation> {
@@ -993,9 +1109,9 @@ impl TestPlan {
             return None;
         }
 
-        let user_id = client.current_user_id(cx);
+        self.operation_ix += 1;
         let call = cx.read(ActiveCall::global);
-        let operation = loop {
+        Some(loop {
             match self.rng.gen_range(0..100_u32) {
                 // Mutate the call
                 0..=29 => {
@@ -1237,6 +1353,7 @@ impl TestPlan {
                             let detach = self.rng.gen_bool(0.3);
                             break ClientOperation::SearchProject {
                                 project_root_name,
+                                is_local,
                                 query,
                                 detach,
                             };
@@ -1293,9 +1410,7 @@ impl TestPlan {
                     break ClientOperation::CreateFsEntry { path, is_dir };
                 }
             }
-        };
-        self.operation_ix += 1;
-        Some(operation)
+        })
     }
 
     fn next_root_dir_name(&mut self, user_id: UserId) -> String {
@@ -1572,3 +1687,16 @@ fn gen_file_name(rng: &mut StdRng) -> String {
     }
     name
 }
+
+fn path_env_var(name: &str) -> Option<PathBuf> {
+    let value = env::var(name).ok()?;
+    let mut path = PathBuf::from(value);
+    if path.is_relative() {
+        let mut abs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        abs_path.pop();
+        abs_path.pop();
+        abs_path.push(path);
+        path = abs_path
+    }
+    Some(path)
+}