random_project_collaboration_tests.rs

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