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