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