Add `SystemClock` (#8239)

Marshall Bowers created

This PR adds a `SystemClock` trait for abstracting away the system
clock.

This allows us to swap out the real system clock with a
`FakeSystemClock` in the tests, thus allowing the fake passage of time.

We're using this in `Telemetry` to better mock the clock for testing
purposes.

Release Notes:

- N/A

Change summary

Cargo.lock                                        |  5 +
crates/channel/src/channel_store_tests.rs         |  4 
crates/client/Cargo.toml                          |  4 
crates/client/src/client.rs                       | 58 ++++++++++-
crates/client/src/telemetry.rs                    | 82 +++++++++-------
crates/clock/Cargo.toml                           |  5 +
crates/clock/src/clock.rs                         | 10 +
crates/clock/src/system_clock.rs                  | 59 ++++++++++++
crates/collab/src/tests/test_server.rs            |  4 
crates/collab_ui/src/chat_panel/message_editor.rs |  4 
crates/project/src/project.rs                     |  5 
crates/project/src/worktree_tests.rs              | 20 +++
crates/workspace/Cargo.toml                       |  1 
crates/workspace/src/workspace.rs                 |  3 
crates/zed/Cargo.toml                             |  1 
crates/zed/src/main.rs                            |  3 
16 files changed, 213 insertions(+), 55 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1909,6 +1909,7 @@ dependencies = [
  "async-recursion 0.3.2",
  "async-tungstenite",
  "chrono",
+ "clock",
  "collections",
  "db",
  "feature_flags",
@@ -1946,6 +1947,8 @@ dependencies = [
 name = "clock"
 version = "0.1.0"
 dependencies = [
+ "chrono",
+ "parking_lot 0.11.2",
  "smallvec",
 ]
 
@@ -11721,6 +11724,7 @@ dependencies = [
  "bincode",
  "call",
  "client",
+ "clock",
  "collections",
  "db",
  "derive_more",
@@ -11945,6 +11949,7 @@ dependencies = [
  "chrono",
  "cli",
  "client",
+ "clock",
  "collab_ui",
  "collections",
  "command_palette",

crates/channel/src/channel_store_tests.rs 🔗

@@ -2,6 +2,7 @@ use crate::channel_chat::ChannelChatEvent;
 
 use super::*;
 use client::{test::FakeServer, Client, UserStore};
+use clock::FakeSystemClock;
 use gpui::{AppContext, Context, Model, TestAppContext};
 use rpc::proto::{self};
 use settings::SettingsStore;
@@ -337,8 +338,9 @@ fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
     release_channel::init("0.0.0", cx);
     client::init_settings(cx);
 
+    let clock = Arc::new(FakeSystemClock::default());
     let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone(), cx);
+    let client = Client::new(clock, http.clone(), cx);
     let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
 
     client::init(&client, cx);

crates/client/Cargo.toml 🔗

@@ -10,10 +10,11 @@ path = "src/client.rs"
 doctest = false
 
 [features]
-test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"]
+test-support = ["clock/test-support", "collections/test-support", "gpui/test-support", "rpc/test-support"]
 
 [dependencies]
 chrono = { version = "0.4", features = ["serde"] }
+clock.workspace = true
 collections.workspace = true
 db.workspace = true
 gpui.workspace = true
@@ -51,6 +52,7 @@ uuid.workspace = true
 url.workspace = true
 
 [dev-dependencies]
+clock = { workspace = true, features = ["test-support"] }
 collections = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 rpc = { workspace = true, features = ["test-support"] }

crates/client/src/client.rs 🔗

@@ -10,6 +10,7 @@ use async_tungstenite::tungstenite::{
     error::Error as WebsocketError,
     http::{Request, StatusCode},
 };
+use clock::SystemClock;
 use collections::HashMap;
 use futures::{
     channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
@@ -421,11 +422,15 @@ impl settings::Settings for TelemetrySettings {
 }
 
 impl Client {
-    pub fn new(http: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
+    pub fn new(
+        clock: Arc<dyn SystemClock>,
+        http: Arc<ZedHttpClient>,
+        cx: &mut AppContext,
+    ) -> Arc<Self> {
         let client = Arc::new(Self {
             id: AtomicU64::new(0),
             peer: Peer::new(0),
-            telemetry: Telemetry::new(http.clone(), cx),
+            telemetry: Telemetry::new(clock, http.clone(), cx),
             http,
             state: Default::default(),
 
@@ -1455,6 +1460,7 @@ mod tests {
     use super::*;
     use crate::test::FakeServer;
 
+    use clock::FakeSystemClock;
     use gpui::{BackgroundExecutor, Context, TestAppContext};
     use parking_lot::Mutex;
     use settings::SettingsStore;
@@ -1465,7 +1471,13 @@ mod tests {
     async fn test_reconnection(cx: &mut TestAppContext) {
         init_test(cx);
         let user_id = 5;
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         let server = FakeServer::for_client(user_id, &client, cx).await;
         let mut status = client.status();
         assert!(matches!(
@@ -1500,7 +1512,13 @@ mod tests {
     async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
         init_test(cx);
         let user_id = 5;
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         let mut status = client.status();
 
         // Time out when client tries to connect.
@@ -1573,7 +1591,13 @@ mod tests {
         init_test(cx);
         let auth_count = Arc::new(Mutex::new(0));
         let dropped_auth_count = Arc::new(Mutex::new(0));
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         client.override_authenticate({
             let auth_count = auth_count.clone();
             let dropped_auth_count = dropped_auth_count.clone();
@@ -1621,7 +1645,13 @@ mod tests {
     async fn test_subscribing_to_entity(cx: &mut TestAppContext) {
         init_test(cx);
         let user_id = 5;
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         let server = FakeServer::for_client(user_id, &client, cx).await;
 
         let (done_tx1, mut done_rx1) = smol::channel::unbounded();
@@ -1675,7 +1705,13 @@ mod tests {
     async fn test_subscribing_after_dropping_subscription(cx: &mut TestAppContext) {
         init_test(cx);
         let user_id = 5;
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         let server = FakeServer::for_client(user_id, &client, cx).await;
 
         let model = cx.new_model(|_| TestModel::default());
@@ -1704,7 +1740,13 @@ mod tests {
     async fn test_dropping_subscription_in_handler(cx: &mut TestAppContext) {
         init_test(cx);
         let user_id = 5;
-        let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+        let client = cx.update(|cx| {
+            Client::new(
+                Arc::new(FakeSystemClock::default()),
+                FakeHttpClient::with_404_response(),
+                cx,
+            )
+        });
         let server = FakeServer::for_client(user_id, &client, cx).await;
 
         let model = cx.new_model(|_| TestModel::default());

crates/client/src/telemetry.rs 🔗

@@ -2,6 +2,7 @@ mod event_coalescer;
 
 use crate::TelemetrySettings;
 use chrono::{DateTime, Utc};
+use clock::SystemClock;
 use futures::Future;
 use gpui::{AppContext, AppMetadata, BackgroundExecutor, Task};
 use once_cell::sync::Lazy;
@@ -24,6 +25,7 @@ use util::TryFutureExt;
 use self::event_coalescer::EventCoalescer;
 
 pub struct Telemetry {
+    clock: Arc<dyn SystemClock>,
     http_client: Arc<ZedHttpClient>,
     executor: BackgroundExecutor,
     state: Arc<Mutex<TelemetryState>>,
@@ -156,7 +158,11 @@ static ZED_CLIENT_CHECKSUM_SEED: Lazy<Option<Vec<u8>>> = Lazy::new(|| {
 });
 
 impl Telemetry {
-    pub fn new(client: Arc<ZedHttpClient>, cx: &mut AppContext) -> Arc<Self> {
+    pub fn new(
+        clock: Arc<dyn SystemClock>,
+        client: Arc<ZedHttpClient>,
+        cx: &mut AppContext,
+    ) -> Arc<Self> {
         let release_channel =
             ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
 
@@ -205,6 +211,7 @@ impl Telemetry {
 
         // TODO: Replace all hardware stuff with nested SystemSpecs json
         let this = Arc::new(Self {
+            clock,
             http_client: client,
             executor: cx.background_executor().clone(),
             state,
@@ -317,7 +324,8 @@ impl Telemetry {
             operation,
             copilot_enabled,
             copilot_enabled_for_language,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -333,7 +341,8 @@ impl Telemetry {
             suggestion_id,
             suggestion_accepted,
             file_extension,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -349,7 +358,8 @@ impl Telemetry {
             conversation_id,
             kind,
             model,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -365,7 +375,8 @@ impl Telemetry {
             operation,
             room_id,
             channel_id,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -375,7 +386,8 @@ impl Telemetry {
         let event = Event::Cpu {
             usage_as_percentage,
             core_count,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -389,24 +401,18 @@ impl Telemetry {
         let event = Event::Memory {
             memory_in_bytes,
             virtual_memory_in_bytes,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
     }
 
-    pub fn report_app_event(self: &Arc<Self>, operation: String) {
-        self.report_app_event_with_date_time(operation, Utc::now());
-    }
-
-    fn report_app_event_with_date_time(
-        self: &Arc<Self>,
-        operation: String,
-        date_time: DateTime<Utc>,
-    ) -> Event {
+    pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
         let event = Event::App {
             operation,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(date_time),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event.clone());
@@ -418,7 +424,8 @@ impl Telemetry {
         let event = Event::Setting {
             setting,
             value,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -433,7 +440,8 @@ impl Telemetry {
             let event = Event::Edit {
                 duration: end.timestamp_millis() - start.timestamp_millis(),
                 environment,
-                milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+                milliseconds_since_first_event: self
+                    .milliseconds_since_first_event(self.clock.utc_now()),
             };
 
             self.report_event(event);
@@ -444,7 +452,8 @@ impl Telemetry {
         let event = Event::Action {
             source,
             action,
-            milliseconds_since_first_event: self.milliseconds_since_first_event(Utc::now()),
+            milliseconds_since_first_event: self
+                .milliseconds_since_first_event(self.clock.utc_now()),
         };
 
         self.report_event(event)
@@ -590,29 +599,32 @@ impl Telemetry {
 mod tests {
     use super::*;
     use chrono::TimeZone;
+    use clock::FakeSystemClock;
     use gpui::TestAppContext;
     use util::http::FakeHttpClient;
 
     #[gpui::test]
     fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
         init_test(cx);
+        let clock = Arc::new(FakeSystemClock::new(
+            Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
+        ));
         let http = FakeHttpClient::with_200_response();
         let installation_id = Some("installation_id".to_string());
         let session_id = "session_id".to_string();
 
         cx.update(|cx| {
-            let telemetry = Telemetry::new(http, cx);
+            let telemetry = Telemetry::new(clock.clone(), http, cx);
 
             telemetry.state.lock().max_queue_size = 4;
             telemetry.start(installation_id, session_id, cx);
 
             assert!(is_empty_state(&telemetry));
 
-            let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
+            let first_date_time = clock.utc_now();
             let operation = "test".to_string();
 
-            let event =
-                telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
+            let event = telemetry.report_app_event(operation.clone());
             assert_eq!(
                 event,
                 Event::App {
@@ -627,9 +639,9 @@ mod tests {
                 Some(first_date_time)
             );
 
-            let mut date_time = first_date_time + chrono::Duration::milliseconds(100);
+            clock.advance(chrono::Duration::milliseconds(100));
 
-            let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
+            let event = telemetry.report_app_event(operation.clone());
             assert_eq!(
                 event,
                 Event::App {
@@ -644,9 +656,9 @@ mod tests {
                 Some(first_date_time)
             );
 
-            date_time += chrono::Duration::milliseconds(100);
+            clock.advance(chrono::Duration::milliseconds(100));
 
-            let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
+            let event = telemetry.report_app_event(operation.clone());
             assert_eq!(
                 event,
                 Event::App {
@@ -661,10 +673,10 @@ mod tests {
                 Some(first_date_time)
             );
 
-            date_time += chrono::Duration::milliseconds(100);
+            clock.advance(chrono::Duration::milliseconds(100));
 
             // Adding a 4th event should cause a flush
-            let event = telemetry.report_app_event_with_date_time(operation.clone(), date_time);
+            let event = telemetry.report_app_event(operation.clone());
             assert_eq!(
                 event,
                 Event::App {
@@ -680,22 +692,24 @@ mod tests {
     #[gpui::test]
     async fn test_connection_timeout(executor: BackgroundExecutor, cx: &mut TestAppContext) {
         init_test(cx);
+        let clock = Arc::new(FakeSystemClock::new(
+            Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap(),
+        ));
         let http = FakeHttpClient::with_200_response();
         let installation_id = Some("installation_id".to_string());
         let session_id = "session_id".to_string();
 
         cx.update(|cx| {
-            let telemetry = Telemetry::new(http, cx);
+            let telemetry = Telemetry::new(clock.clone(), http, cx);
             telemetry.state.lock().max_queue_size = 4;
             telemetry.start(installation_id, session_id, cx);
 
             assert!(is_empty_state(&telemetry));
 
-            let first_date_time = Utc.with_ymd_and_hms(1990, 4, 12, 12, 0, 0).unwrap();
+            let first_date_time = clock.utc_now();
             let operation = "test".to_string();
 
-            let event =
-                telemetry.report_app_event_with_date_time(operation.clone(), first_date_time);
+            let event = telemetry.report_app_event(operation.clone());
             assert_eq!(
                 event,
                 Event::App {

crates/clock/Cargo.toml 🔗

@@ -9,5 +9,10 @@ license = "GPL-3.0-or-later"
 path = "src/clock.rs"
 doctest = false
 
+[features]
+test-support = ["dep:parking_lot"]
+
 [dependencies]
+chrono.workspace = true
+parking_lot = { workspace = true, optional = true }
 smallvec.workspace = true

crates/clock/src/clock.rs 🔗

@@ -1,13 +1,17 @@
+mod system_clock;
+
 use smallvec::SmallVec;
 use std::{
     cmp::{self, Ordering},
     fmt, iter,
 };
 
-/// A unique identifier for each distributed node
+pub use system_clock::*;
+
+/// A unique identifier for each distributed node.
 pub type ReplicaId = u16;
 
-/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp),
+/// A [Lamport sequence number](https://en.wikipedia.org/wiki/Lamport_timestamp).
 pub type Seq = u32;
 
 /// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
@@ -18,7 +22,7 @@ pub struct Lamport {
     pub value: Seq,
 }
 
-/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock)
+/// A [vector clock](https://en.wikipedia.org/wiki/Vector_clock).
 #[derive(Clone, Default, Hash, Eq, PartialEq)]
 pub struct Global(SmallVec<[u32; 8]>);
 

crates/clock/src/system_clock.rs 🔗

@@ -0,0 +1,59 @@
+use chrono::{DateTime, Utc};
+
+pub trait SystemClock: Send + Sync {
+    /// Returns the current date and time in UTC.
+    fn utc_now(&self) -> DateTime<Utc>;
+}
+
+pub struct RealSystemClock;
+
+impl SystemClock for RealSystemClock {
+    fn utc_now(&self) -> DateTime<Utc> {
+        Utc::now()
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeSystemClockState {
+    now: DateTime<Utc>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+pub struct FakeSystemClock {
+    // Use an unfair lock to ensure tests are deterministic.
+    state: parking_lot::Mutex<FakeSystemClockState>,
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl Default for FakeSystemClock {
+    fn default() -> Self {
+        Self::new(Utc::now())
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl FakeSystemClock {
+    pub fn new(now: DateTime<Utc>) -> Self {
+        let state = FakeSystemClockState { now };
+
+        Self {
+            state: parking_lot::Mutex::new(state),
+        }
+    }
+
+    pub fn set_now(&self, now: DateTime<Utc>) {
+        self.state.lock().now = now;
+    }
+
+    /// Advances the [`FakeSystemClock`] by the specified [`Duration`](chrono::Duration).
+    pub fn advance(&self, duration: chrono::Duration) {
+        self.state.lock().now += duration;
+    }
+}
+
+#[cfg(any(test, feature = "test-support"))]
+impl SystemClock for FakeSystemClock {
+    fn utc_now(&self) -> DateTime<Utc> {
+        self.state.lock().now
+    }
+}

crates/collab/src/tests/test_server.rs 🔗

@@ -10,6 +10,7 @@ use channel::{ChannelBuffer, ChannelStore};
 use client::{
     self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
 };
+use clock::FakeSystemClock;
 use collab_ui::channel_view::ChannelView;
 use collections::{HashMap, HashSet};
 use fs::FakeFs;
@@ -163,6 +164,7 @@ impl TestServer {
             client::init_settings(cx);
         });
 
+        let clock = Arc::new(FakeSystemClock::default());
         let http = FakeHttpClient::with_404_response();
         let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
         {
@@ -185,7 +187,7 @@ impl TestServer {
                 .user_id
         };
         let client_name = name.to_string();
-        let mut client = cx.update(|cx| Client::new(http.clone(), cx));
+        let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
         let server = self.server.clone();
         let db = self.app_state.db.clone();
         let connection_killers = self.connection_killers.clone();

crates/collab_ui/src/chat_panel/message_editor.rs 🔗

@@ -385,6 +385,7 @@ impl Render for MessageEditor {
 mod tests {
     use super::*;
     use client::{Client, User, UserStore};
+    use clock::FakeSystemClock;
     use gpui::TestAppContext;
     use language::{Language, LanguageConfig};
     use rpc::proto;
@@ -455,8 +456,9 @@ mod tests {
             let settings = SettingsStore::test(cx);
             cx.set_global(settings);
 
+            let clock = Arc::new(FakeSystemClock::default());
             let http = FakeHttpClient::with_404_response();
-            let client = Client::new(http.clone(), cx);
+            let client = Client::new(clock, http.clone(), cx);
             let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
             theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);

crates/project/src/project.rs 🔗

@@ -851,10 +851,13 @@ impl Project {
         root_paths: impl IntoIterator<Item = &Path>,
         cx: &mut gpui::TestAppContext,
     ) -> Model<Project> {
+        use clock::FakeSystemClock;
+
         let mut languages = LanguageRegistry::test();
         languages.set_executor(cx.executor());
+        let clock = Arc::new(FakeSystemClock::default());
         let http_client = util::http::FakeHttpClient::with_404_response();
-        let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
+        let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let project = cx.update(|cx| {
             Project::local(

crates/project/src/worktree_tests.rs 🔗

@@ -5,6 +5,7 @@ use crate::{
 };
 use anyhow::Result;
 use client::Client;
+use clock::FakeSystemClock;
 use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions};
 use git::GITIGNORE;
 use gpui::{ModelContext, Task, TestAppContext};
@@ -1263,7 +1264,13 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
 async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
     init_test(cx);
     cx.executor().allow_parking();
-    let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+    let client_fake = cx.update(|cx| {
+        Client::new(
+            Arc::new(FakeSystemClock::default()),
+            FakeHttpClient::with_404_response(),
+            cx,
+        )
+    });
 
     let fs_fake = FakeFs::new(cx.background_executor.clone());
     fs_fake
@@ -1304,7 +1311,13 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
         assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
     });
 
-    let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+    let client_real = cx.update(|cx| {
+        Client::new(
+            Arc::new(FakeSystemClock::default()),
+            FakeHttpClient::with_404_response(),
+            cx,
+        )
+    });
 
     let fs_real = Arc::new(RealFs);
     let temp_root = temp_tree(json!({
@@ -2396,8 +2409,9 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
 }
 
 fn build_client(cx: &mut TestAppContext) -> Arc<Client> {
+    let clock = Arc::new(FakeSystemClock::default());
     let http_client = FakeHttpClient::with_404_response();
-    cx.update(|cx| Client::new(http_client, cx))
+    cx.update(|cx| Client::new(clock, http_client, cx))
 }
 
 #[track_caller]

crates/workspace/Cargo.toml 🔗

@@ -25,6 +25,7 @@ async-recursion = "1.0.0"
 bincode = "1.2.1"
 call.workspace = true
 client.workspace = true
+clock.workspace = true
 collections.workspace = true
 db.workspace = true
 derive_more.workspace = true

crates/workspace/src/workspace.rs 🔗

@@ -400,8 +400,9 @@ impl AppState {
 
         let fs = fs::FakeFs::new(cx.background_executor().clone());
         let languages = Arc::new(LanguageRegistry::test());
+        let clock = Arc::new(clock::FakeSystemClock::default());
         let http_client = util::http::FakeHttpClient::with_404_response();
-        let client = Client::new(http_client.clone(), cx);
+        let client = Client::new(clock, http_client.clone(), cx);
         let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
         let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
 

crates/zed/Cargo.toml 🔗

@@ -34,6 +34,7 @@ channel.workspace = true
 chrono = "0.4"
 cli.workspace = true
 client.workspace = true
+clock.workspace = true
 collab_ui.workspace = true
 collections.workspace = true
 command_palette.workspace = true

crates/zed/src/main.rs 🔗

@@ -140,9 +140,10 @@ fn main() {
         handle_keymap_file_changes(user_keymap_file_rx, cx);
         client::init_settings(cx);
 
+        let clock = Arc::new(clock::RealSystemClock);
         let http = http::zed_client(&client::ClientSettings::get_global(cx).server_url);
 
-        let client = client::Client::new(http.clone(), cx);
+        let client = client::Client::new(clock, http.clone(), cx);
         let mut languages = LanguageRegistry::new(login_shell_env_loaded);
         let copilot_language_server_id = languages.next_language_server_id();
         languages.set_executor(cx.background_executor().clone());