[wip] add test rng abstraction

cameron created

Change summary

Cargo.lock                                                       |  1 
Cargo.toml                                                       |  1 
crates/agent/src/edit_agent/evals.rs                             |  4 
crates/editor/benches/display_map.rs                             | 12 
crates/editor/benches/editor_render.rs                           |  4 
crates/extension_host/benches/extension_compilation_benchmark.rs | 12 
crates/gpui/Cargo.toml                                           |  3 
crates/gpui/src/app/test_context.rs                              |  6 
crates/gpui/src/executor.rs                                      |  4 
crates/gpui/src/platform/test/dispatcher.rs                      |  8 
crates/gpui/src/test.rs                                          | 52 +
crates/gpui/src/text_system/line_wrapper.rs                      |  4 
crates/gpui_macros/src/test.rs                                   |  2 
13 files changed, 84 insertions(+), 29 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7322,6 +7322,7 @@ dependencies = [
  "pretty_assertions",
  "profiling",
  "rand 0.9.2",
+ "rand_chacha 0.9.0",
  "raw-window-handle",
  "refineable",
  "reqwest_client",

Cargo.toml 🔗

@@ -602,6 +602,7 @@ prost-types = "0.9"
 pulldown-cmark = { version = "0.12.0", default-features = false }
 quote = "1.0.9"
 rand = "0.9"
+rand_chacha = "0.9"
 rayon = "1.8"
 ref-cast = "1.0.24"
 regex = "1.5"

crates/agent/src/edit_agent/evals.rs 🔗

@@ -7,7 +7,7 @@ use client::{Client, UserStore};
 use collections::HashMap;
 use fs::FakeFs;
 use futures::{FutureExt, future::LocalBoxFuture};
-use gpui::{AppContext, TestAppContext, Timer};
+use gpui::{AppContext, TestAppContext, TestRng, Timer};
 use http_client::StatusCode;
 use indoc::{formatdoc, indoc};
 use language_model::{
@@ -1402,7 +1402,7 @@ fn eval(
 }
 
 fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
-    let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
+    let dispatcher = gpui::TestDispatcher::new(TestRng::from_os_rng());
     let mut cx = TestAppContext::build(dispatcher, None);
     let output = cx.executor().block_test(async {
         let test = EditAgentTest::new(&mut cx).await;

crates/editor/benches/display_map.rs 🔗

@@ -1,19 +1,19 @@
 use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
 use editor::MultiBuffer;
-use gpui::TestDispatcher;
+use gpui::{TestDispatcher, TestRng};
 use itertools::Itertools;
-use rand::{Rng, SeedableRng, rngs::StdRng};
+use rand::{Rng, SeedableRng};
 use std::num::NonZeroU32;
 use text::Bias;
 use util::RandomCharIter;
 
 fn to_tab_point_benchmark(c: &mut Criterion) {
-    let rng = StdRng::seed_from_u64(1);
+    let rng = TestRng::seed_from_u64(1);
     let dispatcher = TestDispatcher::new(rng);
     let cx = gpui::TestAppContext::build(dispatcher, None);
 
     let create_tab_map = |length: usize| {
-        let mut rng = StdRng::seed_from_u64(1);
+        let mut rng = TestRng::seed_from_u64(1);
         let text = RandomCharIter::new(&mut rng)
             .take(length)
             .collect::<String>();
@@ -52,12 +52,12 @@ fn to_tab_point_benchmark(c: &mut Criterion) {
 }
 
 fn to_fold_point_benchmark(c: &mut Criterion) {
-    let rng = StdRng::seed_from_u64(1);
+    let rng = TestRng::seed_from_u64(1);
     let dispatcher = TestDispatcher::new(rng);
     let cx = gpui::TestAppContext::build(dispatcher, None);
 
     let create_tab_map = |length: usize| {
-        let mut rng = StdRng::seed_from_u64(1);
+        let mut rng = TestRng::seed_from_u64(1);
         let text = RandomCharIter::new(&mut rng)
             .take(length)
             .collect::<String>();

crates/editor/benches/editor_render.rs 🔗

@@ -3,7 +3,7 @@ use editor::{
     Editor, EditorMode, MultiBuffer,
     actions::{DeleteToPreviousWordStart, SelectAll, SplitSelectionIntoLines},
 };
-use gpui::{AppContext, Focusable as _, TestAppContext, TestDispatcher};
+use gpui::{AppContext, Focusable as _, TestAppContext, TestDispatcher, TestRng};
 use project::Project;
 use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
 use settings::SettingsStore;
@@ -117,7 +117,7 @@ fn editor_render(bencher: &mut Bencher<'_>, cx: &TestAppContext) {
 }
 
 pub fn benches() {
-    let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(1));
+    let dispatcher = TestDispatcher::new(TestRng::seed_from_u64(1));
     let cx = gpui::TestAppContext::build(dispatcher, None);
     cx.update(|cx| {
         let store = SettingsStore::test(cx);

crates/extension_host/benches/extension_compilation_benchmark.rs 🔗

@@ -8,10 +8,10 @@ use extension::{
 };
 use extension_host::wasm_host::WasmHost;
 use fs::RealFs;
-use gpui::{SemanticVersion, TestAppContext, TestDispatcher};
+use gpui::{SemanticVersion, TestAppContext, TestDispatcher, TestRng};
 use http_client::{FakeHttpClient, Response};
 use node_runtime::NodeRuntime;
-use rand::{SeedableRng, rngs::StdRng};
+use rand::SeedableRng;
 use reqwest_client::ReqwestClient;
 use serde_json::json;
 use settings::SettingsStore;
@@ -27,9 +27,9 @@ fn extension_benchmarks(c: &mut Criterion) {
     let wasm_bytes = wasm_bytes(&cx, &mut manifest);
     let manifest = Arc::new(manifest);
     let extensions_dir = TempTree::new(json!({
-        "installed": {},
-        "work": {}
-    }));
+  "installed": {},
+  "work": {}
+}));
     let wasm_host = wasm_host(&cx, &extensions_dir);
 
     group.bench_function(BenchmarkId::from_parameter(1), |b| {
@@ -48,7 +48,7 @@ fn extension_benchmarks(c: &mut Criterion) {
 
 fn init() -> TestAppContext {
     const SEED: u64 = 9999;
-    let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(SEED));
+    let dispatcher = TestDispatcher::new(TestRng::seed_from_u64(SEED));
     let cx = TestAppContext::build(dispatcher, None);
     cx.executor().allow_parking();
     cx.update(|cx| {

crates/gpui/Cargo.toml 🔗

@@ -22,6 +22,7 @@ test-support = [
     "leak-detection",
     "collections/test-support",
     "rand",
+    "rand_chacha",
     "util/test-support",
     "http_client/test-support",
     "wayland",
@@ -110,6 +111,7 @@ parking_lot.workspace = true
 postage.workspace = true
 profiling.workspace = true
 rand = { optional = true, workspace = true }
+rand_chacha = { optional = true, workspace = true }
 raw-window-handle = "0.6"
 refineable.workspace = true
 resvg = { version = "0.45.0", default-features = false, features = [
@@ -247,6 +249,7 @@ http_client = { workspace = true, features = ["test-support"] }
 lyon = { version = "1.0", features = ["extra"] }
 pretty_assertions.workspace = true
 rand.workspace = true
+rand_chacha.workspace = true
 reqwest_client = { workspace = true, features = ["test-support"] }
 unicode-segmentation.workspace = true
 util = { workspace = true, features = ["test-support"] }

crates/gpui/src/app/test_context.rs 🔗

@@ -3,13 +3,13 @@ use crate::{
     BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
     Element, Empty, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, Modifiers,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
-    Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
+    Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestRng,
     TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
     WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt, channel::oneshot};
-use rand::{SeedableRng, rngs::StdRng};
+use rand::SeedableRng;
 use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
 
 /// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides
@@ -144,7 +144,7 @@ impl TestAppContext {
 
     /// Create a single TestAppContext, for non-multi-client tests
     pub fn single() -> Self {
-        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let dispatcher = TestDispatcher::new(TestRng::seed_from_u64(0));
         Self::build(dispatcher, None)
     }
 

crates/gpui/src/executor.rs 🔗

@@ -22,7 +22,7 @@ use util::TryFutureExt;
 use waker_fn::waker_fn;
 
 #[cfg(any(test, feature = "test-support"))]
-use rand::rngs::StdRng;
+use rand::Rng;
 
 /// A pointer to the executor that is currently running,
 /// for spawning background tasks.
@@ -443,7 +443,7 @@ impl BackgroundExecutor {
 
     /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable
     #[cfg(any(test, feature = "test-support"))]
-    pub fn rng(&self) -> StdRng {
+    pub fn rng(&self) -> impl Rng {
         self.dispatcher.as_test().unwrap().rng()
     }
 

crates/gpui/src/platform/test/dispatcher.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{PlatformDispatcher, TaskLabel};
+use crate::{PlatformDispatcher, TaskLabel, TestRng};
 use async_task::Runnable;
 use backtrace::Backtrace;
 use collections::{HashMap, HashSet, VecDeque};
@@ -25,7 +25,7 @@ pub struct TestDispatcher {
 }
 
 struct TestDispatcherState {
-    random: StdRng,
+    random: TestRng,
     foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
     background: Vec<Runnable>,
     deprioritized_background: Vec<Runnable>,
@@ -43,7 +43,7 @@ struct TestDispatcherState {
 }
 
 impl TestDispatcher {
-    pub fn new(random: StdRng) -> Self {
+    pub fn new(random: TestRng) -> Self {
         let state = TestDispatcherState {
             random,
             foreground: HashMap::default(),
@@ -227,7 +227,7 @@ impl TestDispatcher {
         })
     }
 
-    pub fn rng(&self) -> StdRng {
+    pub fn rng(&self) -> impl Rng {
         self.state.lock().random.clone()
     }
 

crates/gpui/src/test.rs 🔗

@@ -54,7 +54,7 @@ pub fn run_test(
                 eprintln!("seed = {seed}");
             }
             let result = panic::catch_unwind(|| {
-                let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed));
+                let dispatcher = TestDispatcher::new(TestRng::seed_from_u64(seed));
                 test_fn(dispatcher, seed);
             });
 
@@ -159,3 +159,53 @@ pub fn observe<T: 'static>(entity: &Entity<T>, cx: &mut TestAppContext) -> Obser
 
     Observation { rx, _subscription }
 }
+
+pub use test_rng::TestRng;
+mod test_rng {
+    type Inner = rand_chacha::ChaCha20Rng;
+
+    /// A [portable][0] RNG, suitable for use in tests.
+    ///
+    /// Given the same seed, it is guaranteed to produce the same output on all platforms. The
+    /// values may change in minor version bumps.
+    ///
+    /// [0]: https://rust-random.github.io/book/crate-reprod.html#portable-items
+    #[derive(Debug, Clone)]
+    pub struct TestRng(Inner);
+
+    impl rand::RngCore for TestRng {
+        fn next_u32(&mut self) -> u32 {
+            self.0.next_u32()
+        }
+
+        fn next_u64(&mut self) -> u64 {
+            self.0.next_u64()
+        }
+
+        fn fill_bytes(&mut self, dst: &mut [u8]) {
+            self.0.fill_bytes(dst);
+        }
+    }
+
+    impl rand::SeedableRng for TestRng {
+        type Seed = <Inner as rand::SeedableRng>::Seed;
+        fn from_seed(seed: Self::Seed) -> Self {
+            Self(Inner::from_seed(seed))
+        }
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use super::TestRng;
+        use rand::{RngCore, SeedableRng};
+
+        #[test]
+        fn test_rng_produces_reproducible_values_for_known_seeds() {
+            let mut rng = TestRng::seed_from_u64(0);
+            let mut buf = [0; 10];
+            rng.fill_bytes(&mut buf);
+
+            assert_eq!(buf, [178, 247, 245, 129, 214, 222, 60, 6, 168, 34]);
+        }
+    }
+}

crates/gpui/src/text_system/line_wrapper.rs 🔗

@@ -316,14 +316,14 @@ impl Boundary {
 mod tests {
     use super::*;
     use crate::{
-        Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, font,
+        font, Font, FontFeatures, FontStyle, FontWeight, Hsla, TestAppContext, TestDispatcher, TestRng
     };
     #[cfg(target_os = "macos")]
     use crate::{TextRun, WindowTextSystem, WrapBoundary};
     use rand::prelude::*;
 
     fn build_wrapper() -> LineWrapper {
-        let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));
+        let dispatcher = TestDispatcher::new(TestRng::seed_from_u64(0));
         let cx = TestAppContext::build(dispatcher, None);
         let id = cx.text_system().resolve_font(&font(".ZedMono"));
         LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone())

crates/gpui_macros/src/test.rs 🔗

@@ -140,7 +140,7 @@ fn generate_test_function(
                 if let Type::Path(ty) = &*arg.ty {
                     let last_segment = ty.path.segments.last();
                     match last_segment.map(|s| s.ident.to_string()).as_deref() {
-                        Some("StdRng") => {
+                        Some("TestRng") => {
                             inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
                             continue;
                         }