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