random_project_collaboration_tests.rs

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