random_project_collaboration_tests.rs

   1use super::{RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
   2use crate::{db::UserId, tests::run_randomized_test};
   3use anyhow::{anyhow, Result};
   4use async_trait::async_trait;
   5use call::ActiveCall;
   6use collections::{BTreeMap, HashMap};
   7use editor::Bias;
   8use fs::{FakeFs, Fs as _};
   9use futures::StreamExt;
  10use git::repository::GitFileStatus;
  11use gpui::{BackgroundExecutor, Model, TestAppContext};
  12use language::{
  13    range_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, PointUtf16,
  14};
  15use lsp::FakeLanguageServer;
  16use pretty_assertions::assert_eq;
  17use project::{
  18    search::SearchQuery, search::SearchResult, Project, ProjectPath, DEFAULT_COMPLETION_CONTEXT,
  19};
  20use rand::{
  21    distributions::{Alphanumeric, DistString},
  22    prelude::*,
  23};
  24use serde::{Deserialize, Serialize};
  25use std::{
  26    ops::{Deref, Range},
  27    path::{Path, PathBuf},
  28    rc::Rc,
  29    sync::Arc,
  30};
  31use util::ResultExt;
  32
  33#[gpui::test(
  34    iterations = 100,
  35    on_failure = "crate::tests::save_randomized_test_plan"
  36)]
  37async fn test_random_project_collaboration(
  38    cx: &mut TestAppContext,
  39    executor: BackgroundExecutor,
  40    rng: StdRng,
  41) {
  42    run_randomized_test::<ProjectCollaborationTest>(cx, executor, rng).await;
  43}
  44
  45#[derive(Clone, Debug, Serialize, Deserialize)]
  46enum ClientOperation {
  47    AcceptIncomingCall,
  48    RejectIncomingCall,
  49    LeaveCall,
  50    InviteContactToCall {
  51        user_id: UserId,
  52    },
  53    OpenLocalProject {
  54        first_root_name: String,
  55    },
  56    OpenRemoteProject {
  57        host_id: UserId,
  58        first_root_name: String,
  59    },
  60    AddWorktreeToProject {
  61        project_root_name: String,
  62        new_root_path: PathBuf,
  63    },
  64    CloseRemoteProject {
  65        project_root_name: String,
  66    },
  67    OpenBuffer {
  68        project_root_name: String,
  69        is_local: bool,
  70        full_path: PathBuf,
  71    },
  72    SearchProject {
  73        project_root_name: String,
  74        is_local: bool,
  75        query: String,
  76        detach: bool,
  77    },
  78    EditBuffer {
  79        project_root_name: String,
  80        is_local: bool,
  81        full_path: PathBuf,
  82        edits: Vec<(Range<usize>, Arc<str>)>,
  83    },
  84    CloseBuffer {
  85        project_root_name: String,
  86        is_local: bool,
  87        full_path: PathBuf,
  88    },
  89    SaveBuffer {
  90        project_root_name: String,
  91        is_local: bool,
  92        full_path: PathBuf,
  93        detach: bool,
  94    },
  95    RequestLspDataInBuffer {
  96        project_root_name: String,
  97        is_local: bool,
  98        full_path: PathBuf,
  99        offset: usize,
 100        kind: LspRequestKind,
 101        detach: bool,
 102    },
 103    CreateWorktreeEntry {
 104        project_root_name: String,
 105        is_local: bool,
 106        full_path: PathBuf,
 107        is_dir: bool,
 108    },
 109    WriteFsEntry {
 110        path: PathBuf,
 111        is_dir: bool,
 112        content: String,
 113    },
 114    GitOperation {
 115        operation: GitOperation,
 116    },
 117}
 118
 119#[derive(Clone, Debug, Serialize, Deserialize)]
 120enum GitOperation {
 121    WriteGitIndex {
 122        repo_path: PathBuf,
 123        contents: Vec<(PathBuf, String)>,
 124    },
 125    WriteGitBranch {
 126        repo_path: PathBuf,
 127        new_branch: Option<String>,
 128    },
 129    WriteGitStatuses {
 130        repo_path: PathBuf,
 131        statuses: Vec<(PathBuf, GitFileStatus)>,
 132        git_operation: bool,
 133    },
 134}
 135
 136#[derive(Clone, Debug, Serialize, Deserialize)]
 137enum LspRequestKind {
 138    Rename,
 139    Completion,
 140    CodeAction,
 141    Definition,
 142    Highlights,
 143}
 144
 145struct ProjectCollaborationTest;
 146
 147#[async_trait(?Send)]
 148impl RandomizedTest for ProjectCollaborationTest {
 149    type Operation = ClientOperation;
 150
 151    async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) {
 152        let db = &server.app_state.db;
 153        for (ix, user_a) in users.iter().enumerate() {
 154            for user_b in &users[ix + 1..] {
 155                db.send_contact_request(user_a.user_id, user_b.user_id)
 156                    .await
 157                    .unwrap();
 158                db.respond_to_contact_request(user_b.user_id, user_a.user_id, true)
 159                    .await
 160                    .unwrap();
 161            }
 162        }
 163    }
 164
 165    fn generate_operation(
 166        client: &TestClient,
 167        rng: &mut StdRng,
 168        plan: &mut UserTestPlan,
 169        cx: &TestAppContext,
 170    ) -> ClientOperation {
 171        let call = cx.read(ActiveCall::global);
 172        loop {
 173            match rng.gen_range(0..100_u32) {
 174                // Mutate the call
 175                0..=29 => {
 176                    // Respond to an incoming call
 177                    if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
 178                        break if rng.gen_bool(0.7) {
 179                            ClientOperation::AcceptIncomingCall
 180                        } else {
 181                            ClientOperation::RejectIncomingCall
 182                        };
 183                    }
 184
 185                    match rng.gen_range(0..100_u32) {
 186                        // Invite a contact to the current call
 187                        0..=70 => {
 188                            let available_contacts =
 189                                client.user_store().read_with(cx, |user_store, _| {
 190                                    user_store
 191                                        .contacts()
 192                                        .iter()
 193                                        .filter(|contact| contact.online && !contact.busy)
 194                                        .cloned()
 195                                        .collect::<Vec<_>>()
 196                                });
 197                            if !available_contacts.is_empty() {
 198                                let contact = available_contacts.choose(rng).unwrap();
 199                                break ClientOperation::InviteContactToCall {
 200                                    user_id: UserId(contact.user.id as i32),
 201                                };
 202                            }
 203                        }
 204
 205                        // Leave the current call
 206                        71.. => {
 207                            if plan.allow_client_disconnection
 208                                && call.read_with(cx, |call, _| call.room().is_some())
 209                            {
 210                                break ClientOperation::LeaveCall;
 211                            }
 212                        }
 213                    }
 214                }
 215
 216                // Mutate projects
 217                30..=59 => match rng.gen_range(0..100_u32) {
 218                    // Open a new project
 219                    0..=70 => {
 220                        // Open a remote project
 221                        if let Some(room) = call.read_with(cx, |call, _| call.room().cloned()) {
 222                            let existing_dev_server_project_ids = cx.read(|cx| {
 223                                client
 224                                    .dev_server_projects()
 225                                    .iter()
 226                                    .map(|p| p.read(cx).remote_id().unwrap())
 227                                    .collect::<Vec<_>>()
 228                            });
 229                            let new_dev_server_projects = room.read_with(cx, |room, _| {
 230                                room.remote_participants()
 231                                    .values()
 232                                    .flat_map(|participant| {
 233                                        participant.projects.iter().filter_map(|project| {
 234                                            if existing_dev_server_project_ids.contains(&project.id)
 235                                            {
 236                                                None
 237                                            } else {
 238                                                Some((
 239                                                    UserId::from_proto(participant.user.id),
 240                                                    project.worktree_root_names[0].clone(),
 241                                                ))
 242                                            }
 243                                        })
 244                                    })
 245                                    .collect::<Vec<_>>()
 246                            });
 247                            if !new_dev_server_projects.is_empty() {
 248                                let (host_id, first_root_name) =
 249                                    new_dev_server_projects.choose(rng).unwrap().clone();
 250                                break ClientOperation::OpenRemoteProject {
 251                                    host_id,
 252                                    first_root_name,
 253                                };
 254                            }
 255                        }
 256                        // Open a local project
 257                        else {
 258                            let first_root_name = plan.next_root_dir_name();
 259                            break ClientOperation::OpenLocalProject { first_root_name };
 260                        }
 261                    }
 262
 263                    // Close a remote project
 264                    71..=80 => {
 265                        if !client.dev_server_projects().is_empty() {
 266                            let project = client.dev_server_projects().choose(rng).unwrap().clone();
 267                            let first_root_name = root_name_for_project(&project, cx);
 268                            break ClientOperation::CloseRemoteProject {
 269                                project_root_name: first_root_name,
 270                            };
 271                        }
 272                    }
 273
 274                    // Mutate project worktrees
 275                    81.. => match rng.gen_range(0..100_u32) {
 276                        // Add a worktree to a local project
 277                        0..=50 => {
 278                            let Some(project) = client.local_projects().choose(rng).cloned() else {
 279                                continue;
 280                            };
 281                            let project_root_name = root_name_for_project(&project, cx);
 282                            let mut paths = client.fs().paths(false);
 283                            paths.remove(0);
 284                            let new_root_path = if paths.is_empty() || rng.gen() {
 285                                Path::new("/").join(plan.next_root_dir_name())
 286                            } else {
 287                                paths.choose(rng).unwrap().clone()
 288                            };
 289                            break ClientOperation::AddWorktreeToProject {
 290                                project_root_name,
 291                                new_root_path,
 292                            };
 293                        }
 294
 295                        // Add an entry to a worktree
 296                        _ => {
 297                            let Some(project) = choose_random_project(client, rng) else {
 298                                continue;
 299                            };
 300                            let project_root_name = root_name_for_project(&project, cx);
 301                            let is_local = project.read_with(cx, |project, _| project.is_local());
 302                            let worktree = project.read_with(cx, |project, cx| {
 303                                project
 304                                    .worktrees(cx)
 305                                    .filter(|worktree| {
 306                                        let worktree = worktree.read(cx);
 307                                        worktree.is_visible()
 308                                            && worktree.entries(false, 0).any(|e| e.is_file())
 309                                            && worktree.root_entry().map_or(false, |e| e.is_dir())
 310                                    })
 311                                    .choose(rng)
 312                            });
 313                            let Some(worktree) = worktree else { continue };
 314                            let is_dir = rng.gen::<bool>();
 315                            let mut full_path =
 316                                worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
 317                            full_path.push(gen_file_name(rng));
 318                            if !is_dir {
 319                                full_path.set_extension("rs");
 320                            }
 321                            break ClientOperation::CreateWorktreeEntry {
 322                                project_root_name,
 323                                is_local,
 324                                full_path,
 325                                is_dir,
 326                            };
 327                        }
 328                    },
 329                },
 330
 331                // Query and mutate buffers
 332                60..=90 => {
 333                    let Some(project) = choose_random_project(client, rng) else {
 334                        continue;
 335                    };
 336                    let project_root_name = root_name_for_project(&project, cx);
 337                    let is_local = project.read_with(cx, |project, _| project.is_local());
 338
 339                    match rng.gen_range(0..100_u32) {
 340                        // Manipulate an existing buffer
 341                        0..=70 => {
 342                            let Some(buffer) = client
 343                                .buffers_for_project(&project)
 344                                .iter()
 345                                .choose(rng)
 346                                .cloned()
 347                            else {
 348                                continue;
 349                            };
 350
 351                            let full_path = buffer
 352                                .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
 353
 354                            match rng.gen_range(0..100_u32) {
 355                                // Close the buffer
 356                                0..=15 => {
 357                                    break ClientOperation::CloseBuffer {
 358                                        project_root_name,
 359                                        is_local,
 360                                        full_path,
 361                                    };
 362                                }
 363                                // Save the buffer
 364                                16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
 365                                    let detach = rng.gen_bool(0.3);
 366                                    break ClientOperation::SaveBuffer {
 367                                        project_root_name,
 368                                        is_local,
 369                                        full_path,
 370                                        detach,
 371                                    };
 372                                }
 373                                // Edit the buffer
 374                                30..=69 => {
 375                                    let edits = buffer
 376                                        .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3));
 377                                    break ClientOperation::EditBuffer {
 378                                        project_root_name,
 379                                        is_local,
 380                                        full_path,
 381                                        edits,
 382                                    };
 383                                }
 384                                // Make an LSP request
 385                                _ => {
 386                                    let offset = buffer.read_with(cx, |buffer, _| {
 387                                        buffer.clip_offset(
 388                                            rng.gen_range(0..=buffer.len()),
 389                                            language::Bias::Left,
 390                                        )
 391                                    });
 392                                    let detach = rng.gen();
 393                                    break ClientOperation::RequestLspDataInBuffer {
 394                                        project_root_name,
 395                                        full_path,
 396                                        offset,
 397                                        is_local,
 398                                        kind: match rng.gen_range(0..5_u32) {
 399                                            0 => LspRequestKind::Rename,
 400                                            1 => LspRequestKind::Highlights,
 401                                            2 => LspRequestKind::Definition,
 402                                            3 => LspRequestKind::CodeAction,
 403                                            4.. => LspRequestKind::Completion,
 404                                        },
 405                                        detach,
 406                                    };
 407                                }
 408                            }
 409                        }
 410
 411                        71..=80 => {
 412                            let query = rng.gen_range('a'..='z').to_string();
 413                            let detach = rng.gen_bool(0.3);
 414                            break ClientOperation::SearchProject {
 415                                project_root_name,
 416                                is_local,
 417                                query,
 418                                detach,
 419                            };
 420                        }
 421
 422                        // Open a buffer
 423                        81.. => {
 424                            let worktree = project.read_with(cx, |project, cx| {
 425                                project
 426                                    .worktrees(cx)
 427                                    .filter(|worktree| {
 428                                        let worktree = worktree.read(cx);
 429                                        worktree.is_visible()
 430                                            && worktree.entries(false, 0).any(|e| e.is_file())
 431                                    })
 432                                    .choose(rng)
 433                            });
 434                            let Some(worktree) = worktree else { continue };
 435                            let full_path = worktree.read_with(cx, |worktree, _| {
 436                                let entry = worktree
 437                                    .entries(false, 0)
 438                                    .filter(|e| e.is_file())
 439                                    .choose(rng)
 440                                    .unwrap();
 441                                if entry.path.as_ref() == Path::new("") {
 442                                    Path::new(worktree.root_name()).into()
 443                                } else {
 444                                    Path::new(worktree.root_name()).join(&entry.path)
 445                                }
 446                            });
 447                            break ClientOperation::OpenBuffer {
 448                                project_root_name,
 449                                is_local,
 450                                full_path,
 451                            };
 452                        }
 453                    }
 454                }
 455
 456                // Update a git related action
 457                91..=95 => {
 458                    break ClientOperation::GitOperation {
 459                        operation: generate_git_operation(rng, client),
 460                    };
 461                }
 462
 463                // Create or update a file or directory
 464                96.. => {
 465                    let is_dir = rng.gen::<bool>();
 466                    let content;
 467                    let mut path;
 468                    let dir_paths = client.fs().directories(false);
 469
 470                    if is_dir {
 471                        content = String::new();
 472                        path = dir_paths.choose(rng).unwrap().clone();
 473                        path.push(gen_file_name(rng));
 474                    } else {
 475                        content = Alphanumeric.sample_string(rng, 16);
 476
 477                        // Create a new file or overwrite an existing file
 478                        let file_paths = client.fs().files();
 479                        if file_paths.is_empty() || rng.gen_bool(0.5) {
 480                            path = dir_paths.choose(rng).unwrap().clone();
 481                            path.push(gen_file_name(rng));
 482                            path.set_extension("rs");
 483                        } else {
 484                            path = file_paths.choose(rng).unwrap().clone()
 485                        };
 486                    }
 487                    break ClientOperation::WriteFsEntry {
 488                        path,
 489                        is_dir,
 490                        content,
 491                    };
 492                }
 493            }
 494        }
 495    }
 496
 497    async fn apply_operation(
 498        client: &TestClient,
 499        operation: ClientOperation,
 500        cx: &mut TestAppContext,
 501    ) -> Result<(), TestError> {
 502        match operation {
 503            ClientOperation::AcceptIncomingCall => {
 504                let active_call = cx.read(ActiveCall::global);
 505                if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
 506                    Err(TestError::Inapplicable)?;
 507                }
 508
 509                log::info!("{}: accepting incoming call", client.username);
 510                active_call
 511                    .update(cx, |call, cx| call.accept_incoming(cx))
 512                    .await?;
 513            }
 514
 515            ClientOperation::RejectIncomingCall => {
 516                let active_call = cx.read(ActiveCall::global);
 517                if active_call.read_with(cx, |call, _| call.incoming().borrow().is_none()) {
 518                    Err(TestError::Inapplicable)?;
 519                }
 520
 521                log::info!("{}: declining incoming call", client.username);
 522                active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
 523            }
 524
 525            ClientOperation::LeaveCall => {
 526                let active_call = cx.read(ActiveCall::global);
 527                if active_call.read_with(cx, |call, _| call.room().is_none()) {
 528                    Err(TestError::Inapplicable)?;
 529                }
 530
 531                log::info!("{}: hanging up", client.username);
 532                active_call.update(cx, |call, cx| call.hang_up(cx)).await?;
 533            }
 534
 535            ClientOperation::InviteContactToCall { user_id } => {
 536                let active_call = cx.read(ActiveCall::global);
 537
 538                log::info!("{}: inviting {}", client.username, user_id,);
 539                active_call
 540                    .update(cx, |call, cx| call.invite(user_id.to_proto(), None, cx))
 541                    .await
 542                    .log_err();
 543            }
 544
 545            ClientOperation::OpenLocalProject { first_root_name } => {
 546                log::info!(
 547                    "{}: opening local project at {:?}",
 548                    client.username,
 549                    first_root_name
 550                );
 551
 552                let root_path = Path::new("/").join(&first_root_name);
 553                client.fs().create_dir(&root_path).await.unwrap();
 554                client
 555                    .fs()
 556                    .create_file(&root_path.join("main.rs"), Default::default())
 557                    .await
 558                    .unwrap();
 559                let project = client.build_local_project(root_path, cx).await.0;
 560                ensure_project_shared(&project, client, cx).await;
 561                client.local_projects_mut().push(project.clone());
 562            }
 563
 564            ClientOperation::AddWorktreeToProject {
 565                project_root_name,
 566                new_root_path,
 567            } => {
 568                let project = project_for_root_name(client, &project_root_name, cx)
 569                    .ok_or(TestError::Inapplicable)?;
 570
 571                log::info!(
 572                    "{}: finding/creating local worktree at {:?} to project with root path {}",
 573                    client.username,
 574                    new_root_path,
 575                    project_root_name
 576                );
 577
 578                ensure_project_shared(&project, client, cx).await;
 579                if !client.fs().paths(false).contains(&new_root_path) {
 580                    client.fs().create_dir(&new_root_path).await.unwrap();
 581                }
 582                project
 583                    .update(cx, |project, cx| {
 584                        project.find_or_create_worktree(&new_root_path, true, cx)
 585                    })
 586                    .await
 587                    .unwrap();
 588            }
 589
 590            ClientOperation::CloseRemoteProject { project_root_name } => {
 591                let project = project_for_root_name(client, &project_root_name, cx)
 592                    .ok_or(TestError::Inapplicable)?;
 593
 594                log::info!(
 595                    "{}: closing remote project with root path {}",
 596                    client.username,
 597                    project_root_name,
 598                );
 599
 600                let ix = client
 601                    .dev_server_projects()
 602                    .iter()
 603                    .position(|p| p == &project)
 604                    .unwrap();
 605                cx.update(|_| {
 606                    client.dev_server_projects_mut().remove(ix);
 607                    client.buffers().retain(|p, _| *p != project);
 608                    drop(project);
 609                });
 610            }
 611
 612            ClientOperation::OpenRemoteProject {
 613                host_id,
 614                first_root_name,
 615            } => {
 616                let active_call = cx.read(ActiveCall::global);
 617                let project = active_call
 618                    .update(cx, |call, cx| {
 619                        let room = call.room().cloned()?;
 620                        let participant = room
 621                            .read(cx)
 622                            .remote_participants()
 623                            .get(&host_id.to_proto())?;
 624                        let project_id = participant
 625                            .projects
 626                            .iter()
 627                            .find(|project| project.worktree_root_names[0] == first_root_name)?
 628                            .id;
 629                        Some(room.update(cx, |room, cx| {
 630                            room.join_project(
 631                                project_id,
 632                                client.language_registry().clone(),
 633                                FakeFs::new(cx.background_executor().clone()),
 634                                cx,
 635                            )
 636                        }))
 637                    })
 638                    .ok_or(TestError::Inapplicable)?;
 639
 640                log::info!(
 641                    "{}: joining remote project of user {}, root name {}",
 642                    client.username,
 643                    host_id,
 644                    first_root_name,
 645                );
 646
 647                let project = project.await?;
 648                client.dev_server_projects_mut().push(project.clone());
 649            }
 650
 651            ClientOperation::CreateWorktreeEntry {
 652                project_root_name,
 653                is_local,
 654                full_path,
 655                is_dir,
 656            } => {
 657                let project = project_for_root_name(client, &project_root_name, cx)
 658                    .ok_or(TestError::Inapplicable)?;
 659                let project_path = project_path_for_full_path(&project, &full_path, cx)
 660                    .ok_or(TestError::Inapplicable)?;
 661
 662                log::info!(
 663                    "{}: creating {} at path {:?} in {} project {}",
 664                    client.username,
 665                    if is_dir { "dir" } else { "file" },
 666                    full_path,
 667                    if is_local { "local" } else { "remote" },
 668                    project_root_name,
 669                );
 670
 671                ensure_project_shared(&project, client, cx).await;
 672                project
 673                    .update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
 674                    .await?;
 675            }
 676
 677            ClientOperation::OpenBuffer {
 678                project_root_name,
 679                is_local,
 680                full_path,
 681            } => {
 682                let project = project_for_root_name(client, &project_root_name, cx)
 683                    .ok_or(TestError::Inapplicable)?;
 684                let project_path = project_path_for_full_path(&project, &full_path, cx)
 685                    .ok_or(TestError::Inapplicable)?;
 686
 687                log::info!(
 688                    "{}: opening buffer {:?} in {} project {}",
 689                    client.username,
 690                    full_path,
 691                    if is_local { "local" } else { "remote" },
 692                    project_root_name,
 693                );
 694
 695                ensure_project_shared(&project, client, cx).await;
 696                let buffer = project
 697                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
 698                    .await?;
 699                client.buffers_for_project(&project).insert(buffer);
 700            }
 701
 702            ClientOperation::EditBuffer {
 703                project_root_name,
 704                is_local,
 705                full_path,
 706                edits,
 707            } => {
 708                let project = project_for_root_name(client, &project_root_name, cx)
 709                    .ok_or(TestError::Inapplicable)?;
 710                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 711                    .ok_or(TestError::Inapplicable)?;
 712
 713                log::info!(
 714                    "{}: editing buffer {:?} in {} project {} with {:?}",
 715                    client.username,
 716                    full_path,
 717                    if is_local { "local" } else { "remote" },
 718                    project_root_name,
 719                    edits
 720                );
 721
 722                ensure_project_shared(&project, client, cx).await;
 723                buffer.update(cx, |buffer, cx| {
 724                    let snapshot = buffer.snapshot();
 725                    buffer.edit(
 726                        edits.into_iter().map(|(range, text)| {
 727                            let start = snapshot.clip_offset(range.start, Bias::Left);
 728                            let end = snapshot.clip_offset(range.end, Bias::Right);
 729                            (start..end, text)
 730                        }),
 731                        None,
 732                        cx,
 733                    );
 734                });
 735            }
 736
 737            ClientOperation::CloseBuffer {
 738                project_root_name,
 739                is_local,
 740                full_path,
 741            } => {
 742                let project = project_for_root_name(client, &project_root_name, cx)
 743                    .ok_or(TestError::Inapplicable)?;
 744                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 745                    .ok_or(TestError::Inapplicable)?;
 746
 747                log::info!(
 748                    "{}: closing buffer {:?} in {} project {}",
 749                    client.username,
 750                    full_path,
 751                    if is_local { "local" } else { "remote" },
 752                    project_root_name
 753                );
 754
 755                ensure_project_shared(&project, client, cx).await;
 756                cx.update(|_| {
 757                    client.buffers_for_project(&project).remove(&buffer);
 758                    drop(buffer);
 759                });
 760            }
 761
 762            ClientOperation::SaveBuffer {
 763                project_root_name,
 764                is_local,
 765                full_path,
 766                detach,
 767            } => {
 768                let project = project_for_root_name(client, &project_root_name, cx)
 769                    .ok_or(TestError::Inapplicable)?;
 770                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 771                    .ok_or(TestError::Inapplicable)?;
 772
 773                log::info!(
 774                    "{}: saving buffer {:?} in {} project {}, {}",
 775                    client.username,
 776                    full_path,
 777                    if is_local { "local" } else { "remote" },
 778                    project_root_name,
 779                    if detach { "detaching" } else { "awaiting" }
 780                );
 781
 782                ensure_project_shared(&project, client, cx).await;
 783                let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
 784                let save =
 785                    project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 786                let save = cx.spawn(|cx| async move {
 787                    save.await
 788                        .map_err(|err| anyhow!("save request failed: {:?}", err))?;
 789                    assert!(buffer
 790                        .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
 791                        .expect("App should not be dropped")
 792                        .observed_all(&requested_version));
 793                    anyhow::Ok(())
 794                });
 795                if detach {
 796                    cx.update(|cx| save.detach_and_log_err(cx));
 797                } else {
 798                    save.await?;
 799                }
 800            }
 801
 802            ClientOperation::RequestLspDataInBuffer {
 803                project_root_name,
 804                is_local,
 805                full_path,
 806                offset,
 807                kind,
 808                detach,
 809            } => {
 810                let project = project_for_root_name(client, &project_root_name, cx)
 811                    .ok_or(TestError::Inapplicable)?;
 812                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 813                    .ok_or(TestError::Inapplicable)?;
 814
 815                log::info!(
 816                    "{}: request LSP {:?} for buffer {:?} in {} project {}, {}",
 817                    client.username,
 818                    kind,
 819                    full_path,
 820                    if is_local { "local" } else { "remote" },
 821                    project_root_name,
 822                    if detach { "detaching" } else { "awaiting" }
 823                );
 824
 825                use futures::{FutureExt as _, TryFutureExt as _};
 826                let offset = buffer.read_with(cx, |b, _| b.clip_offset(offset, Bias::Left));
 827
 828                let process_lsp_request = project.update(cx, |project, cx| match kind {
 829                    LspRequestKind::Rename => project
 830                        .prepare_rename(buffer, offset, cx)
 831                        .map_ok(|_| ())
 832                        .boxed(),
 833                    LspRequestKind::Completion => project
 834                        .completions(&buffer, offset, DEFAULT_COMPLETION_CONTEXT, cx)
 835                        .map_ok(|_| ())
 836                        .boxed(),
 837                    LspRequestKind::CodeAction => project
 838                        .code_actions(&buffer, offset..offset, None, cx)
 839                        .map(|_| Ok(()))
 840                        .boxed(),
 841                    LspRequestKind::Definition => project
 842                        .definition(&buffer, offset, cx)
 843                        .map_ok(|_| ())
 844                        .boxed(),
 845                    LspRequestKind::Highlights => project
 846                        .document_highlights(&buffer, offset, cx)
 847                        .map_ok(|_| ())
 848                        .boxed(),
 849                });
 850                let request = cx.foreground_executor().spawn(process_lsp_request);
 851                if detach {
 852                    request.detach();
 853                } else {
 854                    request.await?;
 855                }
 856            }
 857
 858            ClientOperation::SearchProject {
 859                project_root_name,
 860                is_local,
 861                query,
 862                detach,
 863            } => {
 864                let project = project_for_root_name(client, &project_root_name, cx)
 865                    .ok_or(TestError::Inapplicable)?;
 866
 867                log::info!(
 868                    "{}: search {} project {} for {:?}, {}",
 869                    client.username,
 870                    if is_local { "local" } else { "remote" },
 871                    project_root_name,
 872                    query,
 873                    if detach { "detaching" } else { "awaiting" }
 874                );
 875
 876                let mut search = project.update(cx, |project, cx| {
 877                    project.search(
 878                        SearchQuery::text(
 879                            query,
 880                            false,
 881                            false,
 882                            false,
 883                            Default::default(),
 884                            Default::default(),
 885                            None,
 886                        )
 887                        .unwrap(),
 888                        cx,
 889                    )
 890                });
 891                drop(project);
 892                let search = cx.executor().spawn(async move {
 893                    let mut results = HashMap::default();
 894                    while let Some(result) = search.next().await {
 895                        if let SearchResult::Buffer { buffer, ranges } = result {
 896                            results.entry(buffer).or_insert(ranges);
 897                        }
 898                    }
 899                    results
 900                });
 901                search.await;
 902            }
 903
 904            ClientOperation::WriteFsEntry {
 905                path,
 906                is_dir,
 907                content,
 908            } => {
 909                if !client
 910                    .fs()
 911                    .directories(false)
 912                    .contains(&path.parent().unwrap().to_owned())
 913                {
 914                    return Err(TestError::Inapplicable);
 915                }
 916
 917                if is_dir {
 918                    log::info!("{}: creating dir at {:?}", client.username, path);
 919                    client.fs().create_dir(&path).await.unwrap();
 920                } else {
 921                    let exists = client.fs().metadata(&path).await?.is_some();
 922                    let verb = if exists { "updating" } else { "creating" };
 923                    log::info!("{}: {} file at {:?}", verb, client.username, path);
 924
 925                    client
 926                        .fs()
 927                        .save(&path, &content.as_str().into(), text::LineEnding::Unix)
 928                        .await
 929                        .unwrap();
 930                }
 931            }
 932
 933            ClientOperation::GitOperation { operation } => match operation {
 934                GitOperation::WriteGitIndex {
 935                    repo_path,
 936                    contents,
 937                } => {
 938                    if !client.fs().directories(false).contains(&repo_path) {
 939                        return Err(TestError::Inapplicable);
 940                    }
 941
 942                    for (path, _) in contents.iter() {
 943                        if !client.fs().files().contains(&repo_path.join(path)) {
 944                            return Err(TestError::Inapplicable);
 945                        }
 946                    }
 947
 948                    log::info!(
 949                        "{}: writing git index for repo {:?}: {:?}",
 950                        client.username,
 951                        repo_path,
 952                        contents
 953                    );
 954
 955                    let dot_git_dir = repo_path.join(".git");
 956                    let contents = contents
 957                        .iter()
 958                        .map(|(path, contents)| (path.as_path(), contents.clone()))
 959                        .collect::<Vec<_>>();
 960                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
 961                        client.fs().create_dir(&dot_git_dir).await?;
 962                    }
 963                    client.fs().set_index_for_repo(&dot_git_dir, &contents);
 964                }
 965                GitOperation::WriteGitBranch {
 966                    repo_path,
 967                    new_branch,
 968                } => {
 969                    if !client.fs().directories(false).contains(&repo_path) {
 970                        return Err(TestError::Inapplicable);
 971                    }
 972
 973                    log::info!(
 974                        "{}: writing git branch for repo {:?}: {:?}",
 975                        client.username,
 976                        repo_path,
 977                        new_branch
 978                    );
 979
 980                    let dot_git_dir = repo_path.join(".git");
 981                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
 982                        client.fs().create_dir(&dot_git_dir).await?;
 983                    }
 984                    client
 985                        .fs()
 986                        .set_branch_name(&dot_git_dir, new_branch.clone());
 987                }
 988                GitOperation::WriteGitStatuses {
 989                    repo_path,
 990                    statuses,
 991                    git_operation,
 992                } => {
 993                    if !client.fs().directories(false).contains(&repo_path) {
 994                        return Err(TestError::Inapplicable);
 995                    }
 996                    for (path, _) in statuses.iter() {
 997                        if !client.fs().files().contains(&repo_path.join(path)) {
 998                            return Err(TestError::Inapplicable);
 999                        }
1000                    }
1001
1002                    log::info!(
1003                        "{}: writing git statuses for repo {:?}: {:?}",
1004                        client.username,
1005                        repo_path,
1006                        statuses
1007                    );
1008
1009                    let dot_git_dir = repo_path.join(".git");
1010
1011                    let statuses = statuses
1012                        .iter()
1013                        .map(|(path, val)| (path.as_path(), *val))
1014                        .collect::<Vec<_>>();
1015
1016                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
1017                        client.fs().create_dir(&dot_git_dir).await?;
1018                    }
1019
1020                    if git_operation {
1021                        client.fs().set_status_for_repo_via_git_operation(
1022                            &dot_git_dir,
1023                            statuses.as_slice(),
1024                        );
1025                    } else {
1026                        client.fs().set_status_for_repo_via_working_copy_change(
1027                            &dot_git_dir,
1028                            statuses.as_slice(),
1029                        );
1030                    }
1031                }
1032            },
1033        }
1034        Ok(())
1035    }
1036
1037    async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
1038        client.language_registry().add(Arc::new(Language::new(
1039            LanguageConfig {
1040                name: "Rust".into(),
1041                matcher: LanguageMatcher {
1042                    path_suffixes: vec!["rs".to_string()],
1043                    ..Default::default()
1044                },
1045                ..Default::default()
1046            },
1047            None,
1048        )));
1049        client.language_registry().register_fake_lsp(
1050            "Rust",
1051            FakeLspAdapter {
1052                name: "the-fake-language-server",
1053                capabilities: lsp::LanguageServer::full_capabilities(),
1054                initializer: Some(Box::new({
1055                    let fs = client.app_state.fs.clone();
1056                    move |fake_server: &mut FakeLanguageServer| {
1057                        fake_server.handle_request::<lsp::request::Completion, _, _>(
1058                            |_, _| async move {
1059                                Ok(Some(lsp::CompletionResponse::Array(vec![
1060                                    lsp::CompletionItem {
1061                                        text_edit: Some(lsp::CompletionTextEdit::Edit(
1062                                            lsp::TextEdit {
1063                                                range: lsp::Range::new(
1064                                                    lsp::Position::new(0, 0),
1065                                                    lsp::Position::new(0, 0),
1066                                                ),
1067                                                new_text: "the-new-text".to_string(),
1068                                            },
1069                                        )),
1070                                        ..Default::default()
1071                                    },
1072                                ])))
1073                            },
1074                        );
1075
1076                        fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1077                            |_, _| async move {
1078                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1079                                    lsp::CodeAction {
1080                                        title: "the-code-action".to_string(),
1081                                        ..Default::default()
1082                                    },
1083                                )]))
1084                            },
1085                        );
1086
1087                        fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1088                            |params, _| async move {
1089                                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1090                                    params.position,
1091                                    params.position,
1092                                ))))
1093                            },
1094                        );
1095
1096                        fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1097                            let fs = fs.clone();
1098                            move |_, cx| {
1099                                let background = cx.background_executor();
1100                                let mut rng = background.rng();
1101                                let count = rng.gen_range::<usize, _>(1..3);
1102                                let files = fs.as_fake().files();
1103                                let files = (0..count)
1104                                    .map(|_| files.choose(&mut rng).unwrap().clone())
1105                                    .collect::<Vec<_>>();
1106                                async move {
1107                                    log::info!("LSP: Returning definitions in files {:?}", &files);
1108                                    Ok(Some(lsp::GotoDefinitionResponse::Array(
1109                                        files
1110                                            .into_iter()
1111                                            .map(|file| lsp::Location {
1112                                                uri: lsp::Url::from_file_path(file).unwrap(),
1113                                                range: Default::default(),
1114                                            })
1115                                            .collect(),
1116                                    )))
1117                                }
1118                            }
1119                        });
1120
1121                        fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
1122                            move |_, cx| {
1123                                let mut highlights = Vec::new();
1124                                let background = cx.background_executor();
1125                                let mut rng = background.rng();
1126
1127                                let highlight_count = rng.gen_range(1..=5);
1128                                for _ in 0..highlight_count {
1129                                    let start_row = rng.gen_range(0..100);
1130                                    let start_column = rng.gen_range(0..100);
1131                                    let end_row = rng.gen_range(0..100);
1132                                    let end_column = rng.gen_range(0..100);
1133                                    let start = PointUtf16::new(start_row, start_column);
1134                                    let end = PointUtf16::new(end_row, end_column);
1135                                    let range = if start > end { end..start } else { start..end };
1136                                    highlights.push(lsp::DocumentHighlight {
1137                                        range: range_to_lsp(range.clone()).unwrap(),
1138                                        kind: Some(lsp::DocumentHighlightKind::READ),
1139                                    });
1140                                }
1141                                highlights.sort_unstable_by_key(|highlight| {
1142                                    (highlight.range.start, highlight.range.end)
1143                                });
1144                                async move { Ok(Some(highlights)) }
1145                            },
1146                        );
1147                    }
1148                })),
1149                ..Default::default()
1150            },
1151        );
1152    }
1153
1154    async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
1155        for (client, client_cx) in clients.iter() {
1156            for guest_project in client.dev_server_projects().iter() {
1157                guest_project.read_with(client_cx, |guest_project, cx| {
1158                        let host_project = clients.iter().find_map(|(client, cx)| {
1159                            let project = client
1160                                .local_projects()
1161                                .iter()
1162                                .find(|host_project| {
1163                                    host_project.read_with(cx, |host_project, _| {
1164                                        host_project.remote_id() == guest_project.remote_id()
1165                                    })
1166                                })?
1167                                .clone();
1168                            Some((project, cx))
1169                        });
1170
1171                        if !guest_project.is_disconnected(cx) {
1172                            if let Some((host_project, host_cx)) = host_project {
1173                                let host_worktree_snapshots =
1174                                    host_project.read_with(host_cx, |host_project, cx| {
1175                                        host_project
1176                                            .worktrees(cx)
1177                                            .map(|worktree| {
1178                                                let worktree = worktree.read(cx);
1179                                                (worktree.id(), worktree.snapshot())
1180                                            })
1181                                            .collect::<BTreeMap<_, _>>()
1182                                    });
1183                                let guest_worktree_snapshots = guest_project
1184                                    .worktrees(cx)
1185                                    .map(|worktree| {
1186                                        let worktree = worktree.read(cx);
1187                                        (worktree.id(), worktree.snapshot())
1188                                    })
1189                                    .collect::<BTreeMap<_, _>>();
1190
1191                                assert_eq!(
1192                                    guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1193                                    host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1194                                    "{} has different worktrees than the host for project {:?}",
1195                                    client.username, guest_project.remote_id(),
1196                                );
1197
1198                                for (id, host_snapshot) in &host_worktree_snapshots {
1199                                    let guest_snapshot = &guest_worktree_snapshots[id];
1200                                    assert_eq!(
1201                                        guest_snapshot.root_name(),
1202                                        host_snapshot.root_name(),
1203                                        "{} has different root name than the host for worktree {}, project {:?}",
1204                                        client.username,
1205                                        id,
1206                                        guest_project.remote_id(),
1207                                    );
1208                                    assert_eq!(
1209                                        guest_snapshot.abs_path(),
1210                                        host_snapshot.abs_path(),
1211                                        "{} has different abs path than the host for worktree {}, project: {:?}",
1212                                        client.username,
1213                                        id,
1214                                        guest_project.remote_id(),
1215                                    );
1216                                    assert_eq!(
1217                                        guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
1218                                        host_snapshot.entries(false, 0).collect::<Vec<_>>(),
1219                                        "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
1220                                        client.username,
1221                                        host_snapshot.abs_path(),
1222                                        id,
1223                                        guest_project.remote_id(),
1224                                    );
1225                                    assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
1226                                        "{} has different repositories than the host for worktree {:?} and project {:?}",
1227                                        client.username,
1228                                        host_snapshot.abs_path(),
1229                                        guest_project.remote_id(),
1230                                    );
1231                                    assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
1232                                        "{} has different scan id than the host for worktree {:?} and project {:?}",
1233                                        client.username,
1234                                        host_snapshot.abs_path(),
1235                                        guest_project.remote_id(),
1236                                    );
1237                                }
1238                            }
1239                        }
1240
1241                        for buffer in guest_project.opened_buffers(cx) {
1242                            let buffer = buffer.read(cx);
1243                            assert_eq!(
1244                                buffer.deferred_ops_len(),
1245                                0,
1246                                "{} has deferred operations for buffer {:?} in project {:?}",
1247                                client.username,
1248                                buffer.file().unwrap().full_path(cx),
1249                                guest_project.remote_id(),
1250                            );
1251                        }
1252                    });
1253            }
1254
1255            let buffers = client.buffers().clone();
1256            for (guest_project, guest_buffers) in &buffers {
1257                let project_id = if guest_project.read_with(client_cx, |project, cx| {
1258                    project.is_local() || project.is_disconnected(cx)
1259                }) {
1260                    continue;
1261                } else {
1262                    guest_project
1263                        .read_with(client_cx, |project, _| project.remote_id())
1264                        .unwrap()
1265                };
1266                let guest_user_id = client.user_id().unwrap();
1267
1268                let host_project = clients.iter().find_map(|(client, cx)| {
1269                    let project = client
1270                        .local_projects()
1271                        .iter()
1272                        .find(|host_project| {
1273                            host_project.read_with(cx, |host_project, _| {
1274                                host_project.remote_id() == Some(project_id)
1275                            })
1276                        })?
1277                        .clone();
1278                    Some((client.user_id().unwrap(), project, cx))
1279                });
1280
1281                let (host_user_id, host_project, host_cx) =
1282                    if let Some((host_user_id, host_project, host_cx)) = host_project {
1283                        (host_user_id, host_project, host_cx)
1284                    } else {
1285                        continue;
1286                    };
1287
1288                for guest_buffer in guest_buffers {
1289                    let buffer_id =
1290                        guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
1291                    let host_buffer = host_project.read_with(host_cx, |project, cx| {
1292                        project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
1293                            panic!(
1294                                "host does not have buffer for guest:{}, peer:{:?}, id:{}",
1295                                client.username,
1296                                client.peer_id(),
1297                                buffer_id
1298                            )
1299                        })
1300                    });
1301                    let path = host_buffer
1302                        .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1303
1304                    assert_eq!(
1305                        guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
1306                        0,
1307                        "{}, buffer {}, path {:?} has deferred operations",
1308                        client.username,
1309                        buffer_id,
1310                        path,
1311                    );
1312                    assert_eq!(
1313                        guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1314                        host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1315                        "{}, buffer {}, path {:?}, differs from the host's buffer",
1316                        client.username,
1317                        buffer_id,
1318                        path
1319                    );
1320
1321                    let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1322                    let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1323                    match (host_file, guest_file) {
1324                        (Some(host_file), Some(guest_file)) => {
1325                            assert_eq!(guest_file.path(), host_file.path());
1326                            assert_eq!(guest_file.disk_state(), host_file.disk_state(),
1327                                "guest {} disk_state does not match host {} for path {:?} in project {}",
1328                                guest_user_id,
1329                                host_user_id,
1330                                guest_file.path(),
1331                                project_id,
1332                            );
1333                        }
1334                        (None, None) => {}
1335                        (None, _) => panic!("host's file is None, guest's isn't"),
1336                        (_, None) => panic!("guest's file is None, hosts's isn't"),
1337                    }
1338
1339                    let host_diff_base = host_project.read_with(host_cx, |project, cx| {
1340                        project
1341                            .buffer_store()
1342                            .read(cx)
1343                            .get_unstaged_changes(host_buffer.read(cx).remote_id())
1344                            .unwrap()
1345                            .read(cx)
1346                            .base_text_string(cx)
1347                    });
1348                    let guest_diff_base = guest_project.read_with(client_cx, |project, cx| {
1349                        project
1350                            .buffer_store()
1351                            .read(cx)
1352                            .get_unstaged_changes(guest_buffer.read(cx).remote_id())
1353                            .unwrap()
1354                            .read(cx)
1355                            .base_text_string(cx)
1356                    });
1357                    assert_eq!(
1358                            guest_diff_base, host_diff_base,
1359                            "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1360                            client.username
1361                        );
1362
1363                    let host_saved_version =
1364                        host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1365                    let guest_saved_version =
1366                        guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1367                    assert_eq!(
1368                            guest_saved_version, host_saved_version,
1369                            "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1370                            client.username
1371                        );
1372
1373                    let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1374                    let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1375                    assert_eq!(
1376                            guest_is_dirty, host_is_dirty,
1377                            "guest {} dirty state does not match host's for path {path:?} in project {project_id}",
1378                            client.username
1379                        );
1380
1381                    let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1382                    let guest_saved_mtime =
1383                        guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1384                    assert_eq!(
1385                            guest_saved_mtime, host_saved_mtime,
1386                            "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1387                            client.username
1388                        );
1389
1390                    let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1391                    let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1392                    assert_eq!(guest_is_dirty, host_is_dirty,
1393                            "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1394                            client.username
1395                        );
1396
1397                    let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1398                    let guest_has_conflict =
1399                        guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1400                    assert_eq!(guest_has_conflict, host_has_conflict,
1401                            "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1402                            client.username
1403                        );
1404                }
1405            }
1406        }
1407    }
1408}
1409
1410fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1411    fn generate_file_paths(
1412        repo_path: &Path,
1413        rng: &mut StdRng,
1414        client: &TestClient,
1415    ) -> Vec<PathBuf> {
1416        let mut paths = client
1417            .fs()
1418            .files()
1419            .into_iter()
1420            .filter(|path| path.starts_with(repo_path))
1421            .collect::<Vec<_>>();
1422
1423        let count = rng.gen_range(0..=paths.len());
1424        paths.shuffle(rng);
1425        paths.truncate(count);
1426
1427        paths
1428            .iter()
1429            .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
1430            .collect::<Vec<_>>()
1431    }
1432
1433    let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1434
1435    match rng.gen_range(0..100_u32) {
1436        0..=25 => {
1437            let file_paths = generate_file_paths(&repo_path, rng, client);
1438
1439            let contents = file_paths
1440                .into_iter()
1441                .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
1442                .collect();
1443
1444            GitOperation::WriteGitIndex {
1445                repo_path,
1446                contents,
1447            }
1448        }
1449        26..=63 => {
1450            let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
1451
1452            GitOperation::WriteGitBranch {
1453                repo_path,
1454                new_branch,
1455            }
1456        }
1457        64..=100 => {
1458            let file_paths = generate_file_paths(&repo_path, rng, client);
1459
1460            let statuses = file_paths
1461                .into_iter()
1462                .map(|paths| {
1463                    (
1464                        paths,
1465                        match rng.gen_range(0..3_u32) {
1466                            0 => GitFileStatus::Added,
1467                            1 => GitFileStatus::Modified,
1468                            2 => GitFileStatus::Conflict,
1469                            _ => unreachable!(),
1470                        },
1471                    )
1472                })
1473                .collect::<Vec<_>>();
1474
1475            let git_operation = rng.gen::<bool>();
1476
1477            GitOperation::WriteGitStatuses {
1478                repo_path,
1479                statuses,
1480                git_operation,
1481            }
1482        }
1483        _ => unreachable!(),
1484    }
1485}
1486
1487fn buffer_for_full_path(
1488    client: &TestClient,
1489    project: &Model<Project>,
1490    full_path: &PathBuf,
1491    cx: &TestAppContext,
1492) -> Option<Model<language::Buffer>> {
1493    client
1494        .buffers_for_project(project)
1495        .iter()
1496        .find(|buffer| {
1497            buffer.read_with(cx, |buffer, cx| {
1498                buffer.file().unwrap().full_path(cx) == *full_path
1499            })
1500        })
1501        .cloned()
1502}
1503
1504fn project_for_root_name(
1505    client: &TestClient,
1506    root_name: &str,
1507    cx: &TestAppContext,
1508) -> Option<Model<Project>> {
1509    if let Some(ix) = project_ix_for_root_name(client.local_projects().deref(), root_name, cx) {
1510        return Some(client.local_projects()[ix].clone());
1511    }
1512    if let Some(ix) = project_ix_for_root_name(client.dev_server_projects().deref(), root_name, cx)
1513    {
1514        return Some(client.dev_server_projects()[ix].clone());
1515    }
1516    None
1517}
1518
1519fn project_ix_for_root_name(
1520    projects: &[Model<Project>],
1521    root_name: &str,
1522    cx: &TestAppContext,
1523) -> Option<usize> {
1524    projects.iter().position(|project| {
1525        project.read_with(cx, |project, cx| {
1526            let worktree = project.visible_worktrees(cx).next().unwrap();
1527            worktree.read(cx).root_name() == root_name
1528        })
1529    })
1530}
1531
1532fn root_name_for_project(project: &Model<Project>, cx: &TestAppContext) -> String {
1533    project.read_with(cx, |project, cx| {
1534        project
1535            .visible_worktrees(cx)
1536            .next()
1537            .unwrap()
1538            .read(cx)
1539            .root_name()
1540            .to_string()
1541    })
1542}
1543
1544fn project_path_for_full_path(
1545    project: &Model<Project>,
1546    full_path: &Path,
1547    cx: &TestAppContext,
1548) -> Option<ProjectPath> {
1549    let mut components = full_path.components();
1550    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1551    let path = components.as_path().into();
1552    let worktree_id = project.read_with(cx, |project, cx| {
1553        project.worktrees(cx).find_map(|worktree| {
1554            let worktree = worktree.read(cx);
1555            if worktree.root_name() == root_name {
1556                Some(worktree.id())
1557            } else {
1558                None
1559            }
1560        })
1561    })?;
1562    Some(ProjectPath { worktree_id, path })
1563}
1564
1565async fn ensure_project_shared(
1566    project: &Model<Project>,
1567    client: &TestClient,
1568    cx: &mut TestAppContext,
1569) {
1570    let first_root_name = root_name_for_project(project, cx);
1571    let active_call = cx.read(ActiveCall::global);
1572    if active_call.read_with(cx, |call, _| call.room().is_some())
1573        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1574    {
1575        match active_call
1576            .update(cx, |call, cx| call.share_project(project.clone(), cx))
1577            .await
1578        {
1579            Ok(project_id) => {
1580                log::info!(
1581                    "{}: shared project {} with id {}",
1582                    client.username,
1583                    first_root_name,
1584                    project_id
1585                );
1586            }
1587            Err(error) => {
1588                log::error!(
1589                    "{}: error sharing project {}: {:?}",
1590                    client.username,
1591                    first_root_name,
1592                    error
1593                );
1594            }
1595        }
1596    }
1597}
1598
1599fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Model<Project>> {
1600    client
1601        .local_projects()
1602        .deref()
1603        .iter()
1604        .chain(client.dev_server_projects().iter())
1605        .choose(rng)
1606        .cloned()
1607}
1608
1609fn gen_file_name(rng: &mut StdRng) -> String {
1610    let mut name = String::new();
1611    for _ in 0..10 {
1612        let letter = rng.gen_range('a'..='z');
1613        name.push(letter);
1614    }
1615    name
1616}