random_project_collaboration_tests.rs

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