Skip to content

Commit

Permalink
Merge #81: Minimal Mockable Clock Implementation
Browse files Browse the repository at this point in the history
cab093c clock: use mockable clock in project (Cameron Garnham)
2ae7ab4 clock: add mockable clock (Cameron Garnham)

Pull request description:

  This is a new clock that lends itself for easier testing, it as two forms: the working clock, that tracks the system time; and the stopped clock that is paused, and must be manually set on a thread-local basis.

  When testing, the stopped clock is set to zero, or implicitly the unix epoch. When not testing the default value of the stopped clock is the time that the application was started.

ACKs for top commit:
  da2ce7:
    ACK cab093c

Tree-SHA512: 1422bfaa30d044f39bfdb727d6a1e9b7ce27a89486b630cffc269213cc5ca71857d04ba136bbc838f564100fa862fa41bc455d1896e21c966fc4311200da5e30
  • Loading branch information
da2ce7 committed Sep 14, 2022
2 parents 2a85fab + cab093c commit 5870954
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 36 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ lto = "fat"
strip = true

[dependencies]
tokio = { version = "1.7", features = ["rt-multi-thread", "net", "sync", "macros", "signal"] }
tokio = { version = "1.7", features = [
"rt-multi-thread",
"net",
"sync",
"macros",
"signal",
] }

serde = { version = "1.0", features = ["derive"] }
serde_bencode = "^0.2.3"
Expand All @@ -28,6 +34,7 @@ serde_with = "2.0.0"
hex = "0.4.3"
percent-encoding = "2.1.0"
binascii = "0.1"
lazy_static = "1.4.0"

openssl = { version = "0.10.41", features = ["vendored"] }

Expand Down
3 changes: 2 additions & 1 deletion src/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

use serde::{Deserialize, Serialize};
use warp::{filters, reply, serve, Filter};
Expand Down Expand Up @@ -268,7 +269,7 @@ pub fn start(socket_addr: SocketAddr, tracker: Arc<TorrentTracker>) -> impl warp
(seconds_valid, tracker)
})
.and_then(|(seconds_valid, tracker): (u64, Arc<TorrentTracker>)| async move {
match tracker.generate_auth_key(seconds_valid).await {
match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await {
Ok(auth_key) => Ok(warp::reply::json(&auth_key)),
Err(..) => Err(warp::reject::custom(ActionStatus::Err {
reason: "failed to generate key".into(),
Expand Down
7 changes: 4 additions & 3 deletions src/databases/mysql.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::str::FromStr;
use std::time::Duration;

use async_trait::async_trait;
use log::debug;
Expand Down Expand Up @@ -94,7 +95,7 @@ impl Database for MysqlDatabase {
"SELECT `key`, valid_until FROM `keys`",
|(key, valid_until): (String, i64)| AuthKey {
key,
valid_until: Some(valid_until as u64),
valid_until: Some(Duration::from_secs(valid_until as u64)),
},
)
.map_err(|_| database::Error::QueryReturnedNoRows)?;
Expand Down Expand Up @@ -187,7 +188,7 @@ impl Database for MysqlDatabase {
{
Some((key, valid_until)) => Ok(AuthKey {
key,
valid_until: Some(valid_until as u64),
valid_until: Some(Duration::from_secs(valid_until as u64)),
}),
None => Err(database::Error::InvalidQuery),
}
Expand All @@ -197,7 +198,7 @@ impl Database for MysqlDatabase {
let mut conn = self.pool.get().map_err(|_| database::Error::DatabaseError)?;

let key = auth_key.key.to_string();
let valid_until = auth_key.valid_until.unwrap_or(0).to_string();
let valid_until = auth_key.valid_until.unwrap_or(Duration::ZERO).as_secs().to_string();

match conn.exec_drop(
"INSERT INTO `keys` (`key`, valid_until) VALUES (:key, :valid_until)",
Expand Down
7 changes: 4 additions & 3 deletions src/databases/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use r2d2_sqlite::SqliteConnectionManager;

use crate::databases::database;
use crate::databases::database::{Database, Error};
use crate::protocol::clock::clock::SinceUnixEpoch;
use crate::tracker::key::AuthKey;
use crate::InfoHash;

Expand Down Expand Up @@ -85,7 +86,7 @@ impl Database for SqliteDatabase {

Ok(AuthKey {
key,
valid_until: Some(valid_until as u64),
valid_until: Some(SinceUnixEpoch::from_secs(valid_until as u64)),
})
})?;

Expand Down Expand Up @@ -192,7 +193,7 @@ impl Database for SqliteDatabase {

Ok(AuthKey {
key,
valid_until: Some(valid_until_i64 as u64),
valid_until: Some(SinceUnixEpoch::from_secs(valid_until_i64 as u64)),
})
} else {
Err(database::Error::QueryReturnedNoRows)
Expand All @@ -204,7 +205,7 @@ impl Database for SqliteDatabase {

match conn.execute(
"INSERT INTO keys (key, valid_until) VALUES (?1, ?2)",
[auth_key.key.to_string(), auth_key.valid_until.unwrap().to_string()],
[auth_key.key.to_string(), auth_key.valid_until.unwrap().as_secs().to_string()],
) {
Ok(updated) => {
if updated > 0 {
Expand Down
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,14 @@ pub mod protocol;
pub mod setup;
pub mod tracker;
pub mod udp;

#[macro_use]
extern crate lazy_static;

pub mod static_time {
use std::time::SystemTime;

lazy_static! {
pub static ref TIME_AT_APP_START: SystemTime = SystemTime::now();
}
}
5 changes: 4 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ use std::sync::Arc;

use log::info;
use torrust_tracker::tracker::tracker::TorrentTracker;
use torrust_tracker::{logging, setup, Configuration};
use torrust_tracker::{logging, setup, static_time, Configuration};

#[tokio::main]
async fn main() {
const CONFIG_PATH: &str = "config.toml";

// Set the time of Torrust app starting
lazy_static::initialize(&static_time::TIME_AT_APP_START);

// Initialize Torrust config
let config = match Configuration::load_from_file(CONFIG_PATH) {
Ok(config) => Arc::new(config),
Expand Down
248 changes: 248 additions & 0 deletions src/protocol/clock/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
use std::num::IntErrorKind;
pub use std::time::Duration;

pub type SinceUnixEpoch = Duration;

#[derive(Debug)]
pub enum ClockType {
WorkingClock,
StoppedClock,
}

#[derive(Debug)]
pub struct Clock<const T: usize>;

pub type WorkingClock = Clock<{ ClockType::WorkingClock as usize }>;
pub type StoppedClock = Clock<{ ClockType::StoppedClock as usize }>;

#[cfg(not(test))]
pub type DefaultClock = WorkingClock;

#[cfg(test)]
pub type DefaultClock = StoppedClock;

pub trait Time: Sized {
fn now() -> SinceUnixEpoch;
}

pub trait TimeNow: Time {
fn add(add_time: &Duration) -> Option<SinceUnixEpoch> {
Self::now().checked_add(*add_time)
}
fn sub(sub_time: &Duration) -> Option<SinceUnixEpoch> {
Self::now().checked_sub(*sub_time)
}
}

#[cfg(test)]
mod tests {
use std::any::TypeId;

use crate::protocol::clock::clock::{DefaultClock, StoppedClock, Time, WorkingClock};

#[test]
fn it_should_be_the_stopped_clock_as_default_when_testing() {
// We are testing, so we should default to the fixed time.
assert_eq!(TypeId::of::<StoppedClock>(), TypeId::of::<DefaultClock>());
assert_eq!(StoppedClock::now(), DefaultClock::now())
}

#[test]
fn it_should_have_different_times() {
assert_ne!(TypeId::of::<StoppedClock>(), TypeId::of::<WorkingClock>());
assert_ne!(StoppedClock::now(), WorkingClock::now())
}
}

mod working_clock {
use std::time::SystemTime;

use super::{SinceUnixEpoch, Time, TimeNow, WorkingClock};

impl Time for WorkingClock {
fn now() -> SinceUnixEpoch {
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap()
}
}

impl TimeNow for WorkingClock {}
}

pub trait StoppedTime: TimeNow {
fn local_set(unix_time: &SinceUnixEpoch);
fn local_set_to_unix_epoch() {
Self::local_set(&SinceUnixEpoch::ZERO)
}
fn local_set_to_app_start_time();
fn local_set_to_system_time_now();
fn local_add(duration: &Duration) -> Result<(), IntErrorKind>;
fn local_sub(duration: &Duration) -> Result<(), IntErrorKind>;
fn local_reset();
}

mod stopped_clock {
use std::num::IntErrorKind;
use std::time::Duration;

use super::{SinceUnixEpoch, StoppedClock, StoppedTime, Time, TimeNow};

impl Time for StoppedClock {
fn now() -> SinceUnixEpoch {
detail::FIXED_TIME.with(|time| {
return *time.borrow();
})
}
}

impl TimeNow for StoppedClock {}

impl StoppedTime for StoppedClock {
fn local_set(unix_time: &SinceUnixEpoch) {
detail::FIXED_TIME.with(|time| {
*time.borrow_mut() = *unix_time;
})
}

fn local_set_to_app_start_time() {
Self::local_set(&detail::get_app_start_time())
}

fn local_set_to_system_time_now() {
Self::local_set(&detail::get_app_start_time())
}

fn local_add(duration: &Duration) -> Result<(), IntErrorKind> {
detail::FIXED_TIME.with(|time| {
let time_borrowed = *time.borrow();
*time.borrow_mut() = match time_borrowed.checked_add(*duration) {
Some(time) => time,
None => {
return Err(IntErrorKind::PosOverflow);
}
};
Ok(())
})
}

fn local_sub(duration: &Duration) -> Result<(), IntErrorKind> {
detail::FIXED_TIME.with(|time| {
let time_borrowed = *time.borrow();
*time.borrow_mut() = match time_borrowed.checked_sub(*duration) {
Some(time) => time,
None => {
return Err(IntErrorKind::NegOverflow);
}
};
Ok(())
})
}

fn local_reset() {
Self::local_set(&detail::get_default_fixed_time())
}
}

#[cfg(test)]
mod tests {
use std::thread;
use std::time::Duration;

use crate::protocol::clock::clock::{SinceUnixEpoch, StoppedClock, StoppedTime, Time, TimeNow, WorkingClock};

#[test]
fn it_should_default_to_zero_when_testing() {
assert_eq!(StoppedClock::now(), SinceUnixEpoch::ZERO)
}

#[test]
fn it_should_possible_to_set_the_time() {
// Check we start with ZERO.
assert_eq!(StoppedClock::now(), Duration::ZERO);

// Set to Current Time and Check
let timestamp = WorkingClock::now();
StoppedClock::local_set(&timestamp);
assert_eq!(StoppedClock::now(), timestamp);

// Elapse the Current Time and Check
StoppedClock::local_add(&timestamp).unwrap();
assert_eq!(StoppedClock::now(), timestamp + timestamp);

// Reset to ZERO and Check
StoppedClock::local_reset();
assert_eq!(StoppedClock::now(), Duration::ZERO);
}

#[test]
fn it_should_default_to_zero_on_thread_exit() {
assert_eq!(StoppedClock::now(), Duration::ZERO);
let after5 = WorkingClock::add(&Duration::from_secs(5)).unwrap();
StoppedClock::local_set(&after5);
assert_eq!(StoppedClock::now(), after5);

let t = thread::spawn(move || {
// each thread starts out with the initial value of ZERO
assert_eq!(StoppedClock::now(), Duration::ZERO);

// and gets set to the current time.
let timestamp = WorkingClock::now();
StoppedClock::local_set(&timestamp);
assert_eq!(StoppedClock::now(), timestamp);
});

// wait for the thread to complete and bail out on panic
t.join().unwrap();

// we retain our original value of current time + 5sec despite the child thread
assert_eq!(StoppedClock::now(), after5);

// Reset to ZERO and Check
StoppedClock::local_reset();
assert_eq!(StoppedClock::now(), Duration::ZERO);
}
}

mod detail {
use std::cell::RefCell;
use std::time::SystemTime;

use crate::protocol::clock::clock::SinceUnixEpoch;
use crate::static_time;

pub fn get_app_start_time() -> SinceUnixEpoch {
(*static_time::TIME_AT_APP_START)
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
}

#[cfg(not(test))]
pub fn get_default_fixed_time() -> SinceUnixEpoch {
get_app_start_time()
}

#[cfg(test)]
pub fn get_default_fixed_time() -> SinceUnixEpoch {
SinceUnixEpoch::ZERO
}

thread_local!(pub static FIXED_TIME: RefCell<SinceUnixEpoch> = RefCell::new(get_default_fixed_time()));

#[cfg(test)]
mod tests {
use std::time::Duration;

use crate::protocol::clock::clock::stopped_clock::detail::{get_app_start_time, get_default_fixed_time};

#[test]
fn it_should_get_the_zero_start_time_when_testing() {
assert_eq!(get_default_fixed_time(), Duration::ZERO);
}

#[test]
fn it_should_get_app_start_time() {
const TIME_AT_WRITING_THIS_TEST: Duration = Duration::new(1662983731, 000022312);
assert!(get_app_start_time() > TIME_AT_WRITING_THIS_TEST);
}
}
}
}
1 change: 1 addition & 0 deletions src/protocol/clock/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod clock;
Loading

0 comments on commit 5870954

Please sign in to comment.