random_project_collaboration_tests.rs

   1use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
   2use crate::db::UserId;
   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::{executor::Deterministic, ModelHandle, 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::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    deterministic: Arc<Deterministic>,
  35    rng: StdRng,
  36) {
  37    run_randomized_test::<ProjectCollaborationTest>(cx, deterministic, 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(cx)
 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(cx)
 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().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                    .unwrap()
 669                    .await?;
 670            }
 671
 672            ClientOperation::OpenBuffer {
 673                project_root_name,
 674                is_local,
 675                full_path,
 676            } => {
 677                let project = project_for_root_name(client, &project_root_name, cx)
 678                    .ok_or(TestError::Inapplicable)?;
 679                let project_path = project_path_for_full_path(&project, &full_path, cx)
 680                    .ok_or(TestError::Inapplicable)?;
 681
 682                log::info!(
 683                    "{}: opening buffer {:?} in {} project {}",
 684                    client.username,
 685                    full_path,
 686                    if is_local { "local" } else { "remote" },
 687                    project_root_name,
 688                );
 689
 690                ensure_project_shared(&project, client, cx).await;
 691                let buffer = project
 692                    .update(cx, |project, cx| project.open_buffer(project_path, cx))
 693                    .await?;
 694                client.buffers_for_project(&project).insert(buffer);
 695            }
 696
 697            ClientOperation::EditBuffer {
 698                project_root_name,
 699                is_local,
 700                full_path,
 701                edits,
 702            } => {
 703                let project = project_for_root_name(client, &project_root_name, cx)
 704                    .ok_or(TestError::Inapplicable)?;
 705                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 706                    .ok_or(TestError::Inapplicable)?;
 707
 708                log::info!(
 709                    "{}: editing buffer {:?} in {} project {} with {:?}",
 710                    client.username,
 711                    full_path,
 712                    if is_local { "local" } else { "remote" },
 713                    project_root_name,
 714                    edits
 715                );
 716
 717                ensure_project_shared(&project, client, cx).await;
 718                buffer.update(cx, |buffer, cx| {
 719                    let snapshot = buffer.snapshot();
 720                    buffer.edit(
 721                        edits.into_iter().map(|(range, text)| {
 722                            let start = snapshot.clip_offset(range.start, Bias::Left);
 723                            let end = snapshot.clip_offset(range.end, Bias::Right);
 724                            (start..end, text)
 725                        }),
 726                        None,
 727                        cx,
 728                    );
 729                });
 730            }
 731
 732            ClientOperation::CloseBuffer {
 733                project_root_name,
 734                is_local,
 735                full_path,
 736            } => {
 737                let project = project_for_root_name(client, &project_root_name, cx)
 738                    .ok_or(TestError::Inapplicable)?;
 739                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 740                    .ok_or(TestError::Inapplicable)?;
 741
 742                log::info!(
 743                    "{}: closing buffer {:?} in {} project {}",
 744                    client.username,
 745                    full_path,
 746                    if is_local { "local" } else { "remote" },
 747                    project_root_name
 748                );
 749
 750                ensure_project_shared(&project, client, cx).await;
 751                cx.update(|_| {
 752                    client.buffers_for_project(&project).remove(&buffer);
 753                    drop(buffer);
 754                });
 755            }
 756
 757            ClientOperation::SaveBuffer {
 758                project_root_name,
 759                is_local,
 760                full_path,
 761                detach,
 762            } => {
 763                let project = project_for_root_name(client, &project_root_name, cx)
 764                    .ok_or(TestError::Inapplicable)?;
 765                let buffer = buffer_for_full_path(client, &project, &full_path, cx)
 766                    .ok_or(TestError::Inapplicable)?;
 767
 768                log::info!(
 769                    "{}: saving buffer {:?} in {} project {}, {}",
 770                    client.username,
 771                    full_path,
 772                    if is_local { "local" } else { "remote" },
 773                    project_root_name,
 774                    if detach { "detaching" } else { "awaiting" }
 775                );
 776
 777                ensure_project_shared(&project, client, cx).await;
 778                let requested_version = buffer.read_with(cx, |buffer, _| buffer.version());
 779                let save =
 780                    project.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 781                let save = cx.spawn(|cx| async move {
 782                    save.await
 783                        .map_err(|err| anyhow!("save request failed: {:?}", err))?;
 784                    assert!(buffer
 785                        .read_with(&cx, |buffer, _| { buffer.saved_version().to_owned() })
 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                let request = cx.foreground().spawn(project.update(cx, |project, cx| {
 822                    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                }));
 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, Vec::new(), Vec::new()).unwrap(),
 873                        cx,
 874                    )
 875                });
 876                drop(project);
 877                let search = cx.background().spawn(async move {
 878                    let mut results = HashMap::default();
 879                    while let Some((buffer, ranges)) = search.next().await {
 880                        results.entry(buffer).or_insert(ranges);
 881                    }
 882                    results
 883                });
 884                search.await;
 885            }
 886
 887            ClientOperation::WriteFsEntry {
 888                path,
 889                is_dir,
 890                content,
 891            } => {
 892                if !client
 893                    .fs()
 894                    .directories(false)
 895                    .contains(&path.parent().unwrap().to_owned())
 896                {
 897                    return Err(TestError::Inapplicable);
 898                }
 899
 900                if is_dir {
 901                    log::info!("{}: creating dir at {:?}", client.username, path);
 902                    client.fs().create_dir(&path).await.unwrap();
 903                } else {
 904                    let exists = client.fs().metadata(&path).await?.is_some();
 905                    let verb = if exists { "updating" } else { "creating" };
 906                    log::info!("{}: {} file at {:?}", verb, client.username, path);
 907
 908                    client
 909                        .fs()
 910                        .save(&path, &content.as_str().into(), text::LineEnding::Unix)
 911                        .await
 912                        .unwrap();
 913                }
 914            }
 915
 916            ClientOperation::GitOperation { operation } => match operation {
 917                GitOperation::WriteGitIndex {
 918                    repo_path,
 919                    contents,
 920                } => {
 921                    if !client.fs().directories(false).contains(&repo_path) {
 922                        return Err(TestError::Inapplicable);
 923                    }
 924
 925                    for (path, _) in contents.iter() {
 926                        if !client.fs().files().contains(&repo_path.join(path)) {
 927                            return Err(TestError::Inapplicable);
 928                        }
 929                    }
 930
 931                    log::info!(
 932                        "{}: writing git index for repo {:?}: {:?}",
 933                        client.username,
 934                        repo_path,
 935                        contents
 936                    );
 937
 938                    let dot_git_dir = repo_path.join(".git");
 939                    let contents = contents
 940                        .iter()
 941                        .map(|(path, contents)| (path.as_path(), contents.clone()))
 942                        .collect::<Vec<_>>();
 943                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
 944                        client.fs().create_dir(&dot_git_dir).await?;
 945                    }
 946                    client.fs().set_index_for_repo(&dot_git_dir, &contents);
 947                }
 948                GitOperation::WriteGitBranch {
 949                    repo_path,
 950                    new_branch,
 951                } => {
 952                    if !client.fs().directories(false).contains(&repo_path) {
 953                        return Err(TestError::Inapplicable);
 954                    }
 955
 956                    log::info!(
 957                        "{}: writing git branch for repo {:?}: {:?}",
 958                        client.username,
 959                        repo_path,
 960                        new_branch
 961                    );
 962
 963                    let dot_git_dir = repo_path.join(".git");
 964                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
 965                        client.fs().create_dir(&dot_git_dir).await?;
 966                    }
 967                    client
 968                        .fs()
 969                        .set_branch_name(&dot_git_dir, new_branch.clone());
 970                }
 971                GitOperation::WriteGitStatuses {
 972                    repo_path,
 973                    statuses,
 974                    git_operation,
 975                } => {
 976                    if !client.fs().directories(false).contains(&repo_path) {
 977                        return Err(TestError::Inapplicable);
 978                    }
 979                    for (path, _) in statuses.iter() {
 980                        if !client.fs().files().contains(&repo_path.join(path)) {
 981                            return Err(TestError::Inapplicable);
 982                        }
 983                    }
 984
 985                    log::info!(
 986                        "{}: writing git statuses for repo {:?}: {:?}",
 987                        client.username,
 988                        repo_path,
 989                        statuses
 990                    );
 991
 992                    let dot_git_dir = repo_path.join(".git");
 993
 994                    let statuses = statuses
 995                        .iter()
 996                        .map(|(path, val)| (path.as_path(), val.clone()))
 997                        .collect::<Vec<_>>();
 998
 999                    if client.fs().metadata(&dot_git_dir).await?.is_none() {
1000                        client.fs().create_dir(&dot_git_dir).await?;
1001                    }
1002
1003                    if git_operation {
1004                        client.fs().set_status_for_repo_via_git_operation(
1005                            &dot_git_dir,
1006                            statuses.as_slice(),
1007                        );
1008                    } else {
1009                        client.fs().set_status_for_repo_via_working_copy_change(
1010                            &dot_git_dir,
1011                            statuses.as_slice(),
1012                        );
1013                    }
1014                }
1015            },
1016        }
1017        Ok(())
1018    }
1019
1020    async fn on_client_added(client: &Rc<TestClient>, _: &mut TestAppContext) {
1021        let mut language = Language::new(
1022            LanguageConfig {
1023                name: "Rust".into(),
1024                path_suffixes: vec!["rs".to_string()],
1025                ..Default::default()
1026            },
1027            None,
1028        );
1029        language
1030            .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1031                name: "the-fake-language-server",
1032                capabilities: lsp::LanguageServer::full_capabilities(),
1033                initializer: Some(Box::new({
1034                    let fs = client.app_state.fs.clone();
1035                    move |fake_server: &mut FakeLanguageServer| {
1036                        fake_server.handle_request::<lsp::request::Completion, _, _>(
1037                            |_, _| async move {
1038                                Ok(Some(lsp::CompletionResponse::Array(vec![
1039                                    lsp::CompletionItem {
1040                                        text_edit: Some(lsp::CompletionTextEdit::Edit(
1041                                            lsp::TextEdit {
1042                                                range: lsp::Range::new(
1043                                                    lsp::Position::new(0, 0),
1044                                                    lsp::Position::new(0, 0),
1045                                                ),
1046                                                new_text: "the-new-text".to_string(),
1047                                            },
1048                                        )),
1049                                        ..Default::default()
1050                                    },
1051                                ])))
1052                            },
1053                        );
1054
1055                        fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
1056                            |_, _| async move {
1057                                Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
1058                                    lsp::CodeAction {
1059                                        title: "the-code-action".to_string(),
1060                                        ..Default::default()
1061                                    },
1062                                )]))
1063                            },
1064                        );
1065
1066                        fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
1067                            |params, _| async move {
1068                                Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1069                                    params.position,
1070                                    params.position,
1071                                ))))
1072                            },
1073                        );
1074
1075                        fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
1076                            let fs = fs.clone();
1077                            move |_, cx| {
1078                                let background = cx.background();
1079                                let mut rng = background.rng();
1080                                let count = rng.gen_range::<usize, _>(1..3);
1081                                let files = fs.as_fake().files();
1082                                let files = (0..count)
1083                                    .map(|_| files.choose(&mut *rng).unwrap().clone())
1084                                    .collect::<Vec<_>>();
1085                                async move {
1086                                    log::info!("LSP: Returning definitions in files {:?}", &files);
1087                                    Ok(Some(lsp::GotoDefinitionResponse::Array(
1088                                        files
1089                                            .into_iter()
1090                                            .map(|file| lsp::Location {
1091                                                uri: lsp::Url::from_file_path(file).unwrap(),
1092                                                range: Default::default(),
1093                                            })
1094                                            .collect(),
1095                                    )))
1096                                }
1097                            }
1098                        });
1099
1100                        fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
1101                            move |_, cx| {
1102                                let mut highlights = Vec::new();
1103                                let background = cx.background();
1104                                let mut rng = background.rng();
1105
1106                                let highlight_count = rng.gen_range(1..=5);
1107                                for _ in 0..highlight_count {
1108                                    let start_row = rng.gen_range(0..100);
1109                                    let start_column = rng.gen_range(0..100);
1110                                    let end_row = rng.gen_range(0..100);
1111                                    let end_column = rng.gen_range(0..100);
1112                                    let start = PointUtf16::new(start_row, start_column);
1113                                    let end = PointUtf16::new(end_row, end_column);
1114                                    let range = if start > end { end..start } else { start..end };
1115                                    highlights.push(lsp::DocumentHighlight {
1116                                        range: range_to_lsp(range.clone()),
1117                                        kind: Some(lsp::DocumentHighlightKind::READ),
1118                                    });
1119                                }
1120                                highlights.sort_unstable_by_key(|highlight| {
1121                                    (highlight.range.start, highlight.range.end)
1122                                });
1123                                async move { Ok(Some(highlights)) }
1124                            },
1125                        );
1126                    }
1127                })),
1128                ..Default::default()
1129            }))
1130            .await;
1131        client.app_state.languages.add(Arc::new(language));
1132    }
1133
1134    async fn on_quiesce(_: &mut TestServer, clients: &mut [(Rc<TestClient>, TestAppContext)]) {
1135        for (client, client_cx) in clients.iter() {
1136            for guest_project in client.remote_projects().iter() {
1137                guest_project.read_with(client_cx, |guest_project, cx| {
1138                        let host_project = clients.iter().find_map(|(client, cx)| {
1139                            let project = client
1140                                .local_projects()
1141                                .iter()
1142                                .find(|host_project| {
1143                                    host_project.read_with(cx, |host_project, _| {
1144                                        host_project.remote_id() == guest_project.remote_id()
1145                                    })
1146                                })?
1147                                .clone();
1148                            Some((project, cx))
1149                        });
1150
1151                        if !guest_project.is_read_only() {
1152                            if let Some((host_project, host_cx)) = host_project {
1153                                let host_worktree_snapshots =
1154                                    host_project.read_with(host_cx, |host_project, cx| {
1155                                        host_project
1156                                            .worktrees(cx)
1157                                            .map(|worktree| {
1158                                                let worktree = worktree.read(cx);
1159                                                (worktree.id(), worktree.snapshot())
1160                                            })
1161                                            .collect::<BTreeMap<_, _>>()
1162                                    });
1163                                let guest_worktree_snapshots = guest_project
1164                                    .worktrees(cx)
1165                                    .map(|worktree| {
1166                                        let worktree = worktree.read(cx);
1167                                        (worktree.id(), worktree.snapshot())
1168                                    })
1169                                    .collect::<BTreeMap<_, _>>();
1170
1171                                assert_eq!(
1172                                    guest_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1173                                    host_worktree_snapshots.values().map(|w| w.abs_path()).collect::<Vec<_>>(),
1174                                    "{} has different worktrees than the host for project {:?}",
1175                                    client.username, guest_project.remote_id(),
1176                                );
1177
1178                                for (id, host_snapshot) in &host_worktree_snapshots {
1179                                    let guest_snapshot = &guest_worktree_snapshots[id];
1180                                    assert_eq!(
1181                                        guest_snapshot.root_name(),
1182                                        host_snapshot.root_name(),
1183                                        "{} has different root name than the host for worktree {}, project {:?}",
1184                                        client.username,
1185                                        id,
1186                                        guest_project.remote_id(),
1187                                    );
1188                                    assert_eq!(
1189                                        guest_snapshot.abs_path(),
1190                                        host_snapshot.abs_path(),
1191                                        "{} has different abs path than the host for worktree {}, project: {:?}",
1192                                        client.username,
1193                                        id,
1194                                        guest_project.remote_id(),
1195                                    );
1196                                    assert_eq!(
1197                                        guest_snapshot.entries(false).collect::<Vec<_>>(),
1198                                        host_snapshot.entries(false).collect::<Vec<_>>(),
1199                                        "{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
1200                                        client.username,
1201                                        host_snapshot.abs_path(),
1202                                        id,
1203                                        guest_project.remote_id(),
1204                                    );
1205                                    assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
1206                                        "{} has different repositories than the host for worktree {:?} and project {:?}",
1207                                        client.username,
1208                                        host_snapshot.abs_path(),
1209                                        guest_project.remote_id(),
1210                                    );
1211                                    assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
1212                                        "{} has different scan id than the host for worktree {:?} and project {:?}",
1213                                        client.username,
1214                                        host_snapshot.abs_path(),
1215                                        guest_project.remote_id(),
1216                                    );
1217                                }
1218                            }
1219                        }
1220
1221                        for buffer in guest_project.opened_buffers(cx) {
1222                            let buffer = buffer.read(cx);
1223                            assert_eq!(
1224                                buffer.deferred_ops_len(),
1225                                0,
1226                                "{} has deferred operations for buffer {:?} in project {:?}",
1227                                client.username,
1228                                buffer.file().unwrap().full_path(cx),
1229                                guest_project.remote_id(),
1230                            );
1231                        }
1232                    });
1233            }
1234
1235            let buffers = client.buffers().clone();
1236            for (guest_project, guest_buffers) in &buffers {
1237                let project_id = if guest_project.read_with(client_cx, |project, _| {
1238                    project.is_local() || project.is_read_only()
1239                }) {
1240                    continue;
1241                } else {
1242                    guest_project
1243                        .read_with(client_cx, |project, _| project.remote_id())
1244                        .unwrap()
1245                };
1246                let guest_user_id = client.user_id().unwrap();
1247
1248                let host_project = clients.iter().find_map(|(client, cx)| {
1249                    let project = client
1250                        .local_projects()
1251                        .iter()
1252                        .find(|host_project| {
1253                            host_project.read_with(cx, |host_project, _| {
1254                                host_project.remote_id() == Some(project_id)
1255                            })
1256                        })?
1257                        .clone();
1258                    Some((client.user_id().unwrap(), project, cx))
1259                });
1260
1261                let (host_user_id, host_project, host_cx) =
1262                    if let Some((host_user_id, host_project, host_cx)) = host_project {
1263                        (host_user_id, host_project, host_cx)
1264                    } else {
1265                        continue;
1266                    };
1267
1268                for guest_buffer in guest_buffers {
1269                    let buffer_id =
1270                        guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
1271                    let host_buffer = host_project.read_with(host_cx, |project, cx| {
1272                        project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
1273                            panic!(
1274                                "host does not have buffer for guest:{}, peer:{:?}, id:{}",
1275                                client.username,
1276                                client.peer_id(),
1277                                buffer_id
1278                            )
1279                        })
1280                    });
1281                    let path = host_buffer
1282                        .read_with(host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
1283
1284                    assert_eq!(
1285                        guest_buffer.read_with(client_cx, |buffer, _| buffer.deferred_ops_len()),
1286                        0,
1287                        "{}, buffer {}, path {:?} has deferred operations",
1288                        client.username,
1289                        buffer_id,
1290                        path,
1291                    );
1292                    assert_eq!(
1293                        guest_buffer.read_with(client_cx, |buffer, _| buffer.text()),
1294                        host_buffer.read_with(host_cx, |buffer, _| buffer.text()),
1295                        "{}, buffer {}, path {:?}, differs from the host's buffer",
1296                        client.username,
1297                        buffer_id,
1298                        path
1299                    );
1300
1301                    let host_file = host_buffer.read_with(host_cx, |b, _| b.file().cloned());
1302                    let guest_file = guest_buffer.read_with(client_cx, |b, _| b.file().cloned());
1303                    match (host_file, guest_file) {
1304                        (Some(host_file), Some(guest_file)) => {
1305                            assert_eq!(guest_file.path(), host_file.path());
1306                            assert_eq!(guest_file.is_deleted(), host_file.is_deleted());
1307                            assert_eq!(
1308                                guest_file.mtime(),
1309                                host_file.mtime(),
1310                                "guest {} mtime does not match host {} for path {:?} in project {}",
1311                                guest_user_id,
1312                                host_user_id,
1313                                guest_file.path(),
1314                                project_id,
1315                            );
1316                        }
1317                        (None, None) => {}
1318                        (None, _) => panic!("host's file is None, guest's isn't"),
1319                        (_, None) => panic!("guest's file is None, hosts's isn't"),
1320                    }
1321
1322                    let host_diff_base = host_buffer
1323                        .read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
1324                    let guest_diff_base = guest_buffer
1325                        .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
1326                    assert_eq!(
1327                            guest_diff_base, host_diff_base,
1328                            "guest {} diff base does not match host's for path {path:?} in project {project_id}",
1329                            client.username
1330                        );
1331
1332                    let host_saved_version =
1333                        host_buffer.read_with(host_cx, |b, _| b.saved_version().clone());
1334                    let guest_saved_version =
1335                        guest_buffer.read_with(client_cx, |b, _| b.saved_version().clone());
1336                    assert_eq!(
1337                            guest_saved_version, host_saved_version,
1338                            "guest {} saved version does not match host's for path {path:?} in project {project_id}",
1339                            client.username
1340                        );
1341
1342                    let host_saved_version_fingerprint =
1343                        host_buffer.read_with(host_cx, |b, _| b.saved_version_fingerprint());
1344                    let guest_saved_version_fingerprint =
1345                        guest_buffer.read_with(client_cx, |b, _| b.saved_version_fingerprint());
1346                    assert_eq!(
1347                            guest_saved_version_fingerprint, host_saved_version_fingerprint,
1348                            "guest {} saved fingerprint does not match host's for path {path:?} in project {project_id}",
1349                            client.username
1350                        );
1351
1352                    let host_saved_mtime = host_buffer.read_with(host_cx, |b, _| b.saved_mtime());
1353                    let guest_saved_mtime =
1354                        guest_buffer.read_with(client_cx, |b, _| b.saved_mtime());
1355                    assert_eq!(
1356                            guest_saved_mtime, host_saved_mtime,
1357                            "guest {} saved mtime does not match host's for path {path:?} in project {project_id}",
1358                            client.username
1359                        );
1360
1361                    let host_is_dirty = host_buffer.read_with(host_cx, |b, _| b.is_dirty());
1362                    let guest_is_dirty = guest_buffer.read_with(client_cx, |b, _| b.is_dirty());
1363                    assert_eq!(guest_is_dirty, host_is_dirty,
1364                            "guest {} dirty status does not match host's for path {path:?} in project {project_id}",
1365                            client.username
1366                        );
1367
1368                    let host_has_conflict = host_buffer.read_with(host_cx, |b, _| b.has_conflict());
1369                    let guest_has_conflict =
1370                        guest_buffer.read_with(client_cx, |b, _| b.has_conflict());
1371                    assert_eq!(guest_has_conflict, host_has_conflict,
1372                            "guest {} conflict status does not match host's for path {path:?} in project {project_id}",
1373                            client.username
1374                        );
1375                }
1376            }
1377        }
1378    }
1379}
1380
1381fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation {
1382    fn generate_file_paths(
1383        repo_path: &Path,
1384        rng: &mut StdRng,
1385        client: &TestClient,
1386    ) -> Vec<PathBuf> {
1387        let mut paths = client
1388            .fs()
1389            .files()
1390            .into_iter()
1391            .filter(|path| path.starts_with(repo_path))
1392            .collect::<Vec<_>>();
1393
1394        let count = rng.gen_range(0..=paths.len());
1395        paths.shuffle(rng);
1396        paths.truncate(count);
1397
1398        paths
1399            .iter()
1400            .map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
1401            .collect::<Vec<_>>()
1402    }
1403
1404    let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
1405
1406    match rng.gen_range(0..100_u32) {
1407        0..=25 => {
1408            let file_paths = generate_file_paths(&repo_path, rng, client);
1409
1410            let contents = file_paths
1411                .into_iter()
1412                .map(|path| (path, Alphanumeric.sample_string(rng, 16)))
1413                .collect();
1414
1415            GitOperation::WriteGitIndex {
1416                repo_path,
1417                contents,
1418            }
1419        }
1420        26..=63 => {
1421            let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
1422
1423            GitOperation::WriteGitBranch {
1424                repo_path,
1425                new_branch,
1426            }
1427        }
1428        64..=100 => {
1429            let file_paths = generate_file_paths(&repo_path, rng, client);
1430
1431            let statuses = file_paths
1432                .into_iter()
1433                .map(|paths| {
1434                    (
1435                        paths,
1436                        match rng.gen_range(0..3_u32) {
1437                            0 => GitFileStatus::Added,
1438                            1 => GitFileStatus::Modified,
1439                            2 => GitFileStatus::Conflict,
1440                            _ => unreachable!(),
1441                        },
1442                    )
1443                })
1444                .collect::<Vec<_>>();
1445
1446            let git_operation = rng.gen::<bool>();
1447
1448            GitOperation::WriteGitStatuses {
1449                repo_path,
1450                statuses,
1451                git_operation,
1452            }
1453        }
1454        _ => unreachable!(),
1455    }
1456}
1457
1458fn buffer_for_full_path(
1459    client: &TestClient,
1460    project: &ModelHandle<Project>,
1461    full_path: &PathBuf,
1462    cx: &TestAppContext,
1463) -> Option<ModelHandle<language::Buffer>> {
1464    client
1465        .buffers_for_project(project)
1466        .iter()
1467        .find(|buffer| {
1468            buffer.read_with(cx, |buffer, cx| {
1469                buffer.file().unwrap().full_path(cx) == *full_path
1470            })
1471        })
1472        .cloned()
1473}
1474
1475fn project_for_root_name(
1476    client: &TestClient,
1477    root_name: &str,
1478    cx: &TestAppContext,
1479) -> Option<ModelHandle<Project>> {
1480    if let Some(ix) = project_ix_for_root_name(&*client.local_projects(), root_name, cx) {
1481        return Some(client.local_projects()[ix].clone());
1482    }
1483    if let Some(ix) = project_ix_for_root_name(&*client.remote_projects(), root_name, cx) {
1484        return Some(client.remote_projects()[ix].clone());
1485    }
1486    None
1487}
1488
1489fn project_ix_for_root_name(
1490    projects: &[ModelHandle<Project>],
1491    root_name: &str,
1492    cx: &TestAppContext,
1493) -> Option<usize> {
1494    projects.iter().position(|project| {
1495        project.read_with(cx, |project, cx| {
1496            let worktree = project.visible_worktrees(cx).next().unwrap();
1497            worktree.read(cx).root_name() == root_name
1498        })
1499    })
1500}
1501
1502fn root_name_for_project(project: &ModelHandle<Project>, cx: &TestAppContext) -> String {
1503    project.read_with(cx, |project, cx| {
1504        project
1505            .visible_worktrees(cx)
1506            .next()
1507            .unwrap()
1508            .read(cx)
1509            .root_name()
1510            .to_string()
1511    })
1512}
1513
1514fn project_path_for_full_path(
1515    project: &ModelHandle<Project>,
1516    full_path: &Path,
1517    cx: &TestAppContext,
1518) -> Option<ProjectPath> {
1519    let mut components = full_path.components();
1520    let root_name = components.next().unwrap().as_os_str().to_str().unwrap();
1521    let path = components.as_path().into();
1522    let worktree_id = project.read_with(cx, |project, cx| {
1523        project.worktrees(cx).find_map(|worktree| {
1524            let worktree = worktree.read(cx);
1525            if worktree.root_name() == root_name {
1526                Some(worktree.id())
1527            } else {
1528                None
1529            }
1530        })
1531    })?;
1532    Some(ProjectPath { worktree_id, path })
1533}
1534
1535async fn ensure_project_shared(
1536    project: &ModelHandle<Project>,
1537    client: &TestClient,
1538    cx: &mut TestAppContext,
1539) {
1540    let first_root_name = root_name_for_project(project, cx);
1541    let active_call = cx.read(ActiveCall::global);
1542    if active_call.read_with(cx, |call, _| call.room().is_some())
1543        && project.read_with(cx, |project, _| project.is_local() && !project.is_shared())
1544    {
1545        match active_call
1546            .update(cx, |call, cx| call.share_project(project.clone(), cx))
1547            .await
1548        {
1549            Ok(project_id) => {
1550                log::info!(
1551                    "{}: shared project {} with id {}",
1552                    client.username,
1553                    first_root_name,
1554                    project_id
1555                );
1556            }
1557            Err(error) => {
1558                log::error!(
1559                    "{}: error sharing project {}: {:?}",
1560                    client.username,
1561                    first_root_name,
1562                    error
1563                );
1564            }
1565        }
1566    }
1567}
1568
1569fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<ModelHandle<Project>> {
1570    client
1571        .local_projects()
1572        .iter()
1573        .chain(client.remote_projects().iter())
1574        .choose(rng)
1575        .cloned()
1576}
1577
1578fn gen_file_name(rng: &mut StdRng) -> String {
1579    let mut name = String::new();
1580    for _ in 0..10 {
1581        let letter = rng.gen_range('a'..='z');
1582        name.push(letter);
1583    }
1584    name
1585}