use crate::db::ChannelRole; use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use anyhow::Result; use async_trait::async_trait; use gpui::{BackgroundExecutor, TestAppContext}; use rand::prelude::*; use serde_derive::{Deserialize, Serialize}; use std::{ ops::{Deref, DerefMut, Range}, rc::Rc, sync::Arc, }; use text::Bias; #[gpui::test( iterations = 100, on_failure = "crate::tests::save_randomized_test_plan" )] async fn test_random_channel_buffers( cx: &mut TestAppContext, executor: BackgroundExecutor, rng: StdRng, ) { run_randomized_test::(cx, executor, rng).await; } struct RandomChannelBufferTest; #[derive(Clone, Serialize, Deserialize)] enum ChannelBufferOperation { JoinChannelNotes { channel_name: String, }, LeaveChannelNotes { channel_name: String, }, EditChannelNotes { channel_name: String, edits: Vec<(Range, Arc)>, }, Noop, } const CHANNEL_COUNT: usize = 3; #[async_trait(?Send)] impl RandomizedTest for RandomChannelBufferTest { type Operation = ChannelBufferOperation; async fn initialize(server: &mut TestServer, users: &[UserTestPlan]) { let db = &server.app_state.db; for ix in 0..CHANNEL_COUNT { let id = db .create_root_channel(&format!("channel-{ix}"), users[0].user_id) .await .unwrap(); for user in &users[1..] { db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member) .await .unwrap(); db.respond_to_channel_invite(id, user.user_id, true) .await .unwrap(); } } } fn generate_operation( client: &TestClient, rng: &mut StdRng, _: &mut UserTestPlan, cx: &TestAppContext, ) -> ChannelBufferOperation { let channel_store = client.channel_store().clone(); let mut channel_buffers = client.channel_buffers(); // When signed out, we can't do anything unless a channel buffer is // already open. if channel_buffers.deref_mut().is_empty() && channel_store.read_with(cx, |store, _| store.channel_count() == 0) { return ChannelBufferOperation::Noop; } loop { match rng.gen_range(0..100_u32) { 0..=29 => { let channel_name = client.channel_store().read_with(cx, |store, cx| { store.ordered_channels().find_map(|(_, channel)| { if store.has_open_channel_buffer(channel.id, cx) { None } else { Some(channel.name.clone()) } }) }); if let Some(channel_name) = channel_name { break ChannelBufferOperation::JoinChannelNotes { channel_name }; } } 30..=40 => { if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { let channel_name = buffer.read_with(cx, |b, cx| b.channel(cx).unwrap().name.clone()); break ChannelBufferOperation::LeaveChannelNotes { channel_name }; } } _ => { if let Some(buffer) = channel_buffers.deref().iter().choose(rng) { break buffer.read_with(cx, |b, cx| { let channel_name = b.channel(cx).unwrap().name.clone(); let edits = b .buffer() .read_with(cx, |buffer, _| buffer.get_random_edits(rng, 3)); ChannelBufferOperation::EditChannelNotes { channel_name, edits, } }); } } } } } async fn apply_operation( client: &TestClient, operation: ChannelBufferOperation, cx: &mut TestAppContext, ) -> Result<(), TestError> { match operation { ChannelBufferOperation::JoinChannelNotes { channel_name } => { let buffer = client.channel_store().update(cx, |store, cx| { let channel_id = store .ordered_channels() .find(|(_, c)| c.name == channel_name) .unwrap() .1 .id; if store.has_open_channel_buffer(channel_id, cx) { Err(TestError::Inapplicable) } else { Ok(store.open_channel_buffer(channel_id, cx)) } })?; log::info!( "{}: opening notes for channel {channel_name}", client.username ); client.channel_buffers().deref_mut().insert(buffer.await?); } ChannelBufferOperation::LeaveChannelNotes { channel_name } => { let buffer = cx.update(|cx| { let mut left_buffer = Err(TestError::Inapplicable); client.channel_buffers().deref_mut().retain(|buffer| { if buffer.read(cx).channel(cx).unwrap().name == channel_name { left_buffer = Ok(buffer.clone()); false } else { true } }); left_buffer })?; log::info!( "{}: closing notes for channel {channel_name}", client.username ); cx.update(|_| drop(buffer)); } ChannelBufferOperation::EditChannelNotes { channel_name, edits, } => { let channel_buffer = cx .read(|cx| { client .channel_buffers() .deref() .iter() .find(|buffer| { buffer.read(cx).channel(cx).unwrap().name == channel_name }) .cloned() }) .ok_or_else(|| TestError::Inapplicable)?; log::info!( "{}: editing notes for channel {channel_name} with {:?}", client.username, edits ); channel_buffer.update(cx, |buffer, cx| { let buffer = buffer.buffer(); buffer.update(cx, |buffer, cx| { let snapshot = buffer.snapshot(); buffer.edit( edits.into_iter().map(|(range, text)| { let start = snapshot.clip_offset(range.start, Bias::Left); let end = snapshot.clip_offset(range.end, Bias::Right); (start..end, text) }), None, cx, ); }); }); } ChannelBufferOperation::Noop => Err(TestError::Inapplicable)?, } Ok(()) } async fn on_quiesce(server: &mut TestServer, clients: &mut [(Rc, TestAppContext)]) { let channels = server.app_state.db.all_channels().await.unwrap(); for (client, client_cx) in clients.iter_mut() { client_cx.update(|cx| { client .channel_buffers() .deref_mut() .retain(|b| b.read(cx).is_connected()); }); } for (channel_id, channel_name) in channels { let mut prev_text: Option<(u64, String)> = None; let mut collaborator_user_ids = server .app_state .db .get_channel_buffer_collaborators(channel_id) .await .unwrap() .into_iter() .map(|id| id.to_proto()) .collect::>(); collaborator_user_ids.sort(); for (client, client_cx) in clients.iter() { let user_id = client.user_id().unwrap(); client_cx.read(|cx| { if let Some(channel_buffer) = client .channel_buffers() .deref() .iter() .find(|b| b.read(cx).channel_id == channel_id.to_proto()) { let channel_buffer = channel_buffer.read(cx); // Assert that channel buffer's text matches other clients' copies. let text = channel_buffer.buffer().read(cx).text(); if let Some((prev_user_id, prev_text)) = &prev_text { assert_eq!( &text, prev_text, "client {user_id} has different text than client {prev_user_id} for channel {channel_name}", ); } else { prev_text = Some((user_id, text.clone())); } // Assert that all clients and the server agree about who is present in the // channel buffer. let collaborators = channel_buffer.collaborators(); let mut user_ids = collaborators.values().map(|c| c.user_id).collect::>(); user_ids.sort(); assert_eq!( user_ids, collaborator_user_ids, "client {user_id} has different user ids for channel {channel_name} than the server", ); } }); } } } }