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