random_project_collaboration_tests.rs

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