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