Skip to content

Commit 47a5db1

Browse files
authored
test(ads-client): Time dependent tests now use a fake clock (#7059)
inline not needed cleanup wording fixed reorg
1 parent 799ae23 commit 47a5db1

File tree

6 files changed

+185
-28
lines changed

6 files changed

+185
-28
lines changed

components/ads-client/src/http_cache.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod builder;
66
mod bytesize;
77
mod cache_control;
8+
mod clock;
89
mod connection_initializer;
910
mod request_hash;
1011
mod store;
@@ -216,7 +217,7 @@ mod tests {
216217
HttpCache::builder("ignored_in_tests.db")
217218
.default_ttl(Duration::from_secs(secs))
218219
.max_size(ByteSize::mib(4))
219-
.build()
220+
.build_for_time_dependent_tests()
220221
.expect("cache build should succeed")
221222
}
222223

@@ -409,7 +410,7 @@ mod tests {
409410
assert!(matches!(out.cache_outcome, CacheOutcome::MissStored));
410411

411412
// After ~>1s, cleanup should remove it
412-
std::thread::sleep(std::time::Duration::from_secs(2));
413+
cache.store.get_clock().advance(2);
413414

414415
cache.store.delete_expired_entries().unwrap();
415416

@@ -439,12 +440,12 @@ mod tests {
439440
assert!(matches!(out.cache_outcome, CacheOutcome::MissStored));
440441

441442
// Not expired yet at ~1s
442-
std::thread::sleep(std::time::Duration::from_secs(1));
443+
cache.store.get_clock().advance(1);
443444
cache.store.delete_expired_entries().unwrap();
444445
assert!(cache.store.lookup(&req).unwrap().is_some());
445446

446447
// Expired after ~2s
447-
std::thread::sleep(std::time::Duration::from_secs(2));
448+
cache.store.get_clock().advance(2);
448449
cache.store.delete_expired_entries().unwrap();
449450
assert!(cache.store.lookup(&req).unwrap().is_none());
450451
}
@@ -469,12 +470,12 @@ mod tests {
469470
assert!(matches!(out.cache_outcome, CacheOutcome::MissStored));
470471

471472
// Not expired at ~1s
472-
std::thread::sleep(std::time::Duration::from_secs(1));
473+
cache.store.get_clock().advance(1);
473474
cache.store.delete_expired_entries().unwrap();
474475
assert!(cache.store.lookup(&req).unwrap().is_some());
475476

476477
// Expired after ~3s
477-
std::thread::sleep(std::time::Duration::from_secs(3));
478+
cache.store.get_clock().advance(3);
478479
cache.store.delete_expired_entries().unwrap();
479480
assert!(cache.store.lookup(&req).unwrap().is_none());
480481
}

components/ads-client/src/http_cache/builder.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::http_cache::HttpCache;
77
use super::bytesize::ByteSize;
88
use super::connection_initializer::HttpCacheConnectionInitializer;
99
use super::store::HttpCacheStore;
10+
use rusqlite::Connection;
1011
use sql_support::open_database;
1112
use std::path::PathBuf;
1213
use std::time::Duration;
@@ -57,6 +58,15 @@ impl HttpCacheBuilder {
5758
}
5859
}
5960

61+
#[cfg(test)]
62+
pub fn new_for_tests(db_path: impl Into<PathBuf>) -> Self {
63+
Self {
64+
db_path: db_path.into(),
65+
max_size: None,
66+
default_ttl: None,
67+
}
68+
}
69+
6070
pub fn max_size(mut self, max_size: ByteSize) -> Self {
6171
self.max_size = Some(max_size);
6272
self
@@ -95,16 +105,20 @@ impl HttpCacheBuilder {
95105
Ok(())
96106
}
97107

98-
pub fn build(self) -> Result<HttpCache, Error> {
99-
self.validate()?;
100-
108+
fn open_connection(&self) -> Result<Connection, Error> {
101109
let initializer = HttpCacheConnectionInitializer {};
102110
let conn = if cfg!(test) {
103111
open_database::open_memory_database(&initializer)?
104112
} else {
105113
open_database::open_database(&self.db_path, &initializer)?
106114
};
115+
Ok(conn)
116+
}
117+
118+
pub fn build(&self) -> Result<HttpCache, Error> {
119+
self.validate()?;
107120

121+
let conn = self.open_connection()?;
108122
let max_size = self.max_size.unwrap_or(DEFAULT_MAX_SIZE);
109123
let store = HttpCacheStore::new(conn);
110124
let default_ttl = self.default_ttl.unwrap_or(DEFAULT_TTL);
@@ -115,6 +129,22 @@ impl HttpCacheBuilder {
115129
default_ttl,
116130
})
117131
}
132+
133+
#[cfg(test)]
134+
pub fn build_for_time_dependent_tests(&self) -> Result<HttpCache, Error> {
135+
self.validate()?;
136+
137+
let conn = self.open_connection()?;
138+
let max_size = self.max_size.unwrap_or(DEFAULT_MAX_SIZE);
139+
let store = HttpCacheStore::new_with_test_clock(conn);
140+
let default_ttl = self.default_ttl.unwrap_or(DEFAULT_TTL);
141+
142+
Ok(HttpCache {
143+
max_size,
144+
store,
145+
default_ttl,
146+
})
147+
}
118148
}
119149

120150
#[cfg(test)]
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
pub trait Clock: Send + Sync + 'static {
2+
fn now_epoch_seconds(&self) -> i64;
3+
#[cfg(test)]
4+
fn advance(&self, secs: i64);
5+
}
6+
7+
pub struct CacheClock;
8+
9+
impl Clock for CacheClock {
10+
fn now_epoch_seconds(&self) -> i64 {
11+
chrono::Utc::now().timestamp()
12+
}
13+
#[cfg(test)]
14+
fn advance(&self, _secs: i64) {
15+
panic!(
16+
"
17+
You cannot advance a non-test clock.
18+
Be sure to build the cache or store with the test clock for time-dependent tests.
19+
"
20+
)
21+
}
22+
}
23+
24+
#[cfg(test)]
25+
pub struct TestClock {
26+
now: std::sync::atomic::AtomicI64,
27+
}
28+
29+
#[cfg(test)]
30+
impl TestClock {
31+
pub fn new(start: i64) -> Self {
32+
Self {
33+
now: std::sync::atomic::AtomicI64::new(start),
34+
}
35+
}
36+
}
37+
38+
#[cfg(test)]
39+
impl Clock for TestClock {
40+
fn now_epoch_seconds(&self) -> i64 {
41+
self.now.load(std::sync::atomic::Ordering::Relaxed)
42+
}
43+
fn advance(&self, secs: i64) {
44+
self.now
45+
.fetch_add(secs, std::sync::atomic::Ordering::Relaxed);
46+
}
47+
}

components/ads-client/src/http_cache/store.rs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5-
use std::{collections::HashMap, time::Duration};
5+
use std::{collections::HashMap, sync::Arc, time::Duration};
66

7-
use crate::http_cache::{request_hash::RequestHash, ByteSize};
7+
use crate::http_cache::{
8+
clock::{CacheClock, Clock},
9+
request_hash::RequestHash,
10+
ByteSize,
11+
};
812
use parking_lot::Mutex;
913
use rusqlite::{params, Connection, OptionalExtension, Result as SqliteResult};
1014
use viaduct::{Header, Request, Response};
@@ -21,6 +25,7 @@ pub enum FaultKind {
2125

2226
pub struct HttpCacheStore {
2327
conn: Mutex<Connection>,
28+
clock: Arc<dyn Clock>,
2429
#[cfg(test)]
2530
fault: parking_lot::Mutex<FaultKind>,
2631
}
@@ -29,11 +34,29 @@ impl HttpCacheStore {
2934
pub fn new(conn: Connection) -> Self {
3035
Self {
3136
conn: Mutex::new(conn),
37+
clock: Arc::new(CacheClock),
3238
#[cfg(test)]
3339
fault: parking_lot::Mutex::new(FaultKind::None),
3440
}
3541
}
3642

43+
#[cfg(test)]
44+
pub fn new_with_test_clock(conn: Connection) -> Self {
45+
use crate::http_cache::clock::TestClock;
46+
47+
Self {
48+
conn: Mutex::new(conn),
49+
clock: Arc::new(TestClock::new(chrono::Utc::now().timestamp())),
50+
#[cfg(test)]
51+
fault: parking_lot::Mutex::new(FaultKind::None),
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
pub fn get_clock(&self) -> &(dyn Clock) {
57+
&*self.clock
58+
}
59+
3760
/// Removes all entries from cache.
3861
pub fn clear_all(&self) -> SqliteResult<usize> {
3962
let conn = self.conn.lock();
@@ -60,7 +83,7 @@ impl HttpCacheStore {
6083
let conn = self.conn.lock();
6184
conn.execute(
6285
"DELETE FROM http_cache WHERE expires_at < ?1",
63-
params![chrono::Utc::now().timestamp()],
86+
params![self.clock.now_epoch_seconds()],
6487
)
6588
}
6689

@@ -118,7 +141,7 @@ impl HttpCacheStore {
118141
let headers_map: HashMap<String, String> = response.headers.clone().into();
119142
let response_headers = serde_json::to_vec(&headers_map).unwrap_or_default();
120143
let size_bytes = (response_headers.len() + response.body.len()) as i64;
121-
let now = chrono::Utc::now().timestamp();
144+
let now = self.clock.now_epoch_seconds();
122145
let ttl_seconds = ttl.as_secs();
123146
let expires_at = now + ttl_seconds as i64;
124147

@@ -252,7 +275,7 @@ mod tests {
252275
let initializer = HttpCacheConnectionInitializer {};
253276
let conn = open_database::open_memory_database(&initializer)
254277
.expect("failed to open memory cache db");
255-
HttpCacheStore::new(conn)
278+
HttpCacheStore::new_with_test_clock(conn)
256279
}
257280

258281
#[test]
@@ -358,16 +381,16 @@ mod tests {
358381
let (c1, e1, t1) = fetch_timestamps(&store, &req);
359382
assert_eq!(t1, 300);
360383

361-
// Change TTL to 1s and upsert; wait a tick so cached_at likely changes
362-
std::thread::sleep(std::time::Duration::from_millis(50));
384+
store.get_clock().advance(3);
385+
363386
store
364387
.store_with_ttl(&req, &resp, &Duration::new(1, 0))
365388
.unwrap();
366389
let (c2, e2, t2) = fetch_timestamps(&store, &req);
367390
assert_eq!(t2, 1);
368391
// cached_at should be >= previous cached_at; expires_at should move accordingly
369-
assert!(c2 >= c1);
370-
assert!(e2 <= e1, "expires_at should move earlier when TTL shrinks");
392+
assert!(c2 > c1);
393+
assert!(e2 < e1, "expires_at should move earlier when TTL shrinks");
371394
}
372395

373396
#[test]
@@ -390,7 +413,7 @@ mod tests {
390413
assert!(store.lookup(&req_fresh).unwrap().is_some());
391414

392415
// Let first one expire; then cleanup
393-
std::thread::sleep(std::time::Duration::from_secs(2));
416+
store.clock.advance(2);
394417
let removed = store.delete_expired_entries().unwrap();
395418
assert!(
396419
removed >= 1,
@@ -412,7 +435,7 @@ mod tests {
412435
.store_with_ttl(&req, &resp, &Duration::new(1, 0))
413436
.unwrap();
414437
// Check that lookup still returns (store is policy-agnostic).
415-
std::thread::sleep(std::time::Duration::from_secs(2));
438+
store.clock.advance(2);
416439
assert!(store.lookup(&req).unwrap().is_some());
417440

418441
// Test cleanup still removes it
@@ -434,7 +457,7 @@ mod tests {
434457
assert!(store.lookup(&req).unwrap().is_some());
435458

436459
// Advance a second so now > expires_at
437-
std::thread::sleep(std::time::Duration::from_secs(1));
460+
store.clock.advance(2);
438461
let removed = store.delete_expired_entries().unwrap();
439462
assert!(removed >= 1);
440463
assert!(store.lookup(&req).unwrap().is_none());
@@ -458,10 +481,7 @@ mod tests {
458481

459482
#[test]
460483
fn test_ttl_expiration() {
461-
let initializer = HttpCacheConnectionInitializer {};
462-
let conn = open_database::open_memory_database(&initializer)
463-
.expect("failed to open memory cache db");
464-
let store = HttpCacheStore::new(conn);
484+
let store = create_test_store();
465485

466486
let request = create_test_request("https://example.com/api", b"test body");
467487
let response = create_test_response(200, b"test response");
@@ -473,7 +493,7 @@ mod tests {
473493
let retrieved = store.lookup(&request).unwrap().unwrap();
474494
assert_eq!(retrieved.0.body, b"test response");
475495

476-
std::thread::sleep(Duration::from_secs(2));
496+
store.clock.advance(2);
477497

478498
let retrieved_after_expiry = store.lookup(&request).unwrap();
479499
assert!(retrieved_after_expiry.is_some());

components/ads-client/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use crate::http_cache::{ByteSize, CacheMode, HttpCacheError, RequestCachePolicy}
2525
use crate::models::AdPlacementRequest;
2626

2727
mod error;
28-
mod http_cache;
28+
pub mod http_cache;
2929
mod instrument;
3030
mod mars;
3131
mod models;

0 commit comments

Comments
 (0)