summaryrefslogtreecommitdiff
path: root/lambda-calcul/rust/src/web.rs
diff options
context:
space:
mode:
Diffstat (limited to 'lambda-calcul/rust/src/web.rs')
-rw-r--r--lambda-calcul/rust/src/web.rs899
1 files changed, 899 insertions, 0 deletions
diff --git a/lambda-calcul/rust/src/web.rs b/lambda-calcul/rust/src/web.rs
new file mode 100644
index 0000000..3f8f056
--- /dev/null
+++ b/lambda-calcul/rust/src/web.rs
@@ -0,0 +1,899 @@
+use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder};
+use chrono::{DateTime, Utc};
+use clap::Parser;
+use handlebars::{DirectorySourceOptions, Handlebars};
+use log::info;
+use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner};
+use rand::Rng;
+use serde::{Deserialize, Serialize};
+use std::sync::Mutex;
+use std::time::Duration;
+use std::{collections::HashMap, sync::Arc};
+use tokio::task::{self, JoinHandle};
+use uuid::Uuid;
+
+use lambda::lambda::{eval_all, eval_whnf, generate_expr, generate_exprs, gensym, Environment};
+use lambda::parser::{parse, parse_total};
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
+struct Registration {
+ url: String,
+ name: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct ClientData {
+ name: String,
+ grade: u8,
+ last_query: DateTime<Utc>,
+ success: bool,
+}
+
+impl ClientData {
+ fn from(client: &Client) -> Self {
+ ClientData {
+ name: client.name.clone(),
+ grade: client.grade,
+ last_query: client
+ .results
+ .last()
+ .map_or(chrono::offset::Utc::now(), |q| q.timestamp),
+ success: client
+ .results
+ .last()
+ .map_or(false, |q| matches!(q.result, TestResult::TestSucceeded)),
+ }
+ }
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+struct Leaderboard {
+ clients: Vec<ClientData>,
+}
+
+trait AppState: Send + Sync {
+ fn register(&mut self, registration: &Registration) -> RegistrationResult;
+ fn unregister(&mut self, url: &String);
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
+enum RegistrationResult {
+ RegistrationSuccess { id: String, url: String },
+ UrlAlreadyRegistered { url: String },
+}
+
+#[derive(Debug, Clone)]
+struct Client {
+ id: Uuid,
+ name: String,
+ url: String,
+ grade: u8,
+ runner: TestRunner,
+ results: Vec<Test>,
+ delay: std::time::Duration,
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
+struct Test {
+ timestamp: DateTime<Utc>,
+ result: TestResult,
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
+enum TestResult {
+ TestFailed(String),
+ ErrorSendingTest(String),
+ TestSucceeded,
+}
+
+impl Client {
+ fn new(url: String, name: String, delay: Duration) -> Self {
+ let id = Uuid::new_v4();
+ let runner = TestRunner::new_with_rng(
+ Config::default(),
+ TestRng::from_seed(RngAlgorithm::XorShift, &id.to_bytes_le()),
+ );
+ Self {
+ id,
+ url,
+ name,
+ grade: 1,
+ runner,
+ results: Vec::new(),
+ delay,
+ }
+ }
+
+ fn time_to_next_test(&self) -> Duration {
+ self.delay
+ }
+
+ fn generate_expr(&mut self) -> (String, String) {
+ if self.grade >= 10 {
+ self.generate_exprs()
+ } else {
+ let input = generate_expr(self.grade.into(), &mut self.runner);
+ let expected = eval_whnf(&input, &mut Environment::new());
+ (input.to_string(), expected.to_string())
+ }
+ }
+
+ fn generate_exprs(&mut self) -> (String, String) {
+ let exprs = generate_exprs(self.grade.into(), &mut self.runner);
+ let input = exprs
+ .iter()
+ .map(|v| format!("{}", v))
+ .collect::<Vec<_>>()
+ .join("\n");
+ let expected = eval_all(&exprs);
+ (
+ input,
+ expected
+ .iter()
+ .map(|v| format!("{}", v))
+ .collect::<Vec<_>>()
+ .join("\n"),
+ )
+ }
+
+ /// Applies a `Test` to update client's state
+ fn apply(&mut self, test: &Test) {
+ match test.result {
+ TestResult::TestSucceeded => {
+ self.grade = self.grade.saturating_add(1);
+ self.delay = Duration::from_secs_f64(self.delay.as_secs_f64() * 0.8);
+ if self.delay.as_millis() < 500 {
+ self.delay = Duration::from_millis(500);
+ }
+ }
+ TestResult::TestFailed(_) => {
+ self.delay = Duration::from_secs_f64(self.delay.as_secs_f64() * 1.2);
+ if self.delay.as_secs() > 30 {
+ self.delay = Duration::from_secs(30);
+ }
+ }
+ _ => (),
+ }
+ self.results.push(test.clone());
+ }
+
+ fn check_result(&self, expected: &String, response: &Result<String, TestResult>) -> Test {
+ let result = match response {
+ Ok(expr) => {
+ let vals = parse(expr);
+ let actual = eval_all(&vals)
+ .iter()
+ .map(|v| format!("{}", v))
+ .collect::<Vec<_>>()
+ .join("\n");
+ if actual == *expected {
+ TestResult::TestSucceeded
+ } else {
+ TestResult::TestFailed(actual)
+ }
+ }
+ Err(res) => res.clone(),
+ };
+ Test {
+ result,
+ timestamp: chrono::offset::Utc::now(),
+ }
+ }
+}
+
+#[derive(Debug)]
+struct State {
+ base_duration: Duration,
+ clients: HashMap<String, (Arc<Mutex<Client>>, JoinHandle<()>)>,
+}
+
+impl State {
+ fn new() -> Self {
+ State::with_duration(Duration::from_secs(10))
+ }
+
+ fn with_duration(base_duration: Duration) -> Self {
+ Self {
+ base_duration,
+ clients: HashMap::new(),
+ }
+ }
+
+ fn client_events(&self, url: &String) -> usize {
+ let client = self.clients.get(url).unwrap().0.lock().unwrap();
+ client.results.len()
+ }
+}
+
+impl AppState for State {
+ fn register(&mut self, registration: &Registration) -> RegistrationResult {
+ if self.clients.contains_key(&registration.url) {
+ RegistrationResult::UrlAlreadyRegistered {
+ url: registration.url.clone(),
+ }
+ } else {
+ let client = Client::new(
+ registration.url.clone(),
+ registration.name.clone(),
+ self.base_duration,
+ );
+ let id = client.id.to_string();
+ let client_ref = Arc::new(Mutex::new(client));
+ // let it run in the background
+ // FIXME: should find a way to handle graceful termination
+ let client_handle = task::spawn(send_tests(client_ref.clone()));
+
+ self.clients.insert(
+ registration.url.clone(),
+ (client_ref.clone(), client_handle),
+ );
+
+ RegistrationResult::RegistrationSuccess {
+ id,
+ url: registration.url.clone(),
+ }
+ }
+ }
+
+ fn unregister(&mut self, url: &String) {
+ let (_, handle) = self.clients.get(url).unwrap();
+ handle.abort()
+ }
+}
+
+#[post("/register")]
+async fn register(
+ app_state: web::Data<Arc<Mutex<State>>>,
+ registration: web::Json<Registration>,
+) -> impl Responder {
+ let result = app_state.lock().unwrap().register(&registration);
+ match result {
+ RegistrationResult::RegistrationSuccess { .. } => HttpResponse::Ok().json(result),
+ RegistrationResult::UrlAlreadyRegistered { .. } => HttpResponse::BadRequest().json(result),
+ }
+}
+
+#[post("/eval")]
+async fn eval(input: String) -> impl Responder {
+ let exprs = parse_total(&input);
+ match exprs {
+ Ok(exprs) => {
+ let mut rng = rand::thread_rng();
+ if rng.gen_range(0..10) <= 2 {
+ return HttpResponse::Ok().body(gensym());
+ }
+ let output = eval_all(&exprs)
+ .iter()
+ .map(|v| format!("{}", v))
+ .collect::<Vec<_>>()
+ .join("\n");
+ HttpResponse::Ok().body(output.to_string())
+ }
+ Err(e) => HttpResponse::BadRequest().body(e.to_string()),
+ }
+}
+
+#[get("/leaderboard")]
+async fn leaderboard(
+ app_state: web::Data<Arc<Mutex<State>>>,
+ hb: web::Data<Handlebars<'_>>,
+) -> impl Responder {
+ let clients = &app_state.lock().unwrap().clients;
+ let mut client_data = vec![];
+ for client in clients.values() {
+ let client = client.0.lock().unwrap();
+ client_data.push(ClientData::from(&client));
+ }
+ client_data.sort_by(|a, b| b.grade.cmp(&a.grade));
+
+ let body = hb
+ .render(
+ "leaderboard",
+ &Leaderboard {
+ clients: client_data,
+ },
+ )
+ .unwrap();
+
+ web::Html::new(body)
+}
+
+#[derive(Parser, Debug)]
+struct Options {
+ /// The port to listen on
+ /// Defaults to 8080
+ #[arg(short, long, default_value_t = 8080)]
+ port: u16,
+ /// The host to bind the server to
+ /// Defaults to 127.0.0.1
+ #[arg(long, default_value = "127.0.0.1")]
+ host: String,
+}
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ let options = Options::parse();
+ let app_state = Arc::new(Mutex::new(State::new()));
+
+ env_logger::init();
+ // Handlebars uses a repository for the compiled templates. This object must be
+ // shared between the application threads, and is therefore passed to the
+ // Application Builder as an atomic reference-counted pointer.
+ let mut handlebars = Handlebars::new();
+ handlebars
+ .register_templates_directory(
+ "./templates",
+ DirectorySourceOptions {
+ tpl_extension: ".html".to_owned(),
+ hidden: false,
+ temporary: false,
+ },
+ )
+ .unwrap();
+
+ HttpServer::new(move || {
+ App::new()
+ .wrap(Logger::default())
+ .app_data(web::Data::new(app_state.clone()))
+ .app_data(web::Data::new(handlebars.clone()))
+ .service(register)
+ .service(eval)
+ .service(leaderboard)
+ })
+ .bind((options.host, options.port))?
+ .run()
+ .await
+}
+
+fn get_test(client_m: &Mutex<Client>) -> (String, String, String) {
+ let mut client = client_m.lock().unwrap();
+ let (input, expected) = client.generate_expr();
+ (input, client.url.clone(), expected)
+}
+
+async fn send_tests(client_m: Arc<Mutex<Client>>) {
+ loop {
+ let sleep = sleep_time(&client_m);
+ tokio::time::sleep(sleep).await;
+ {
+ let (input, url, expected) = get_test(&client_m);
+
+ let response = send_test(&input, &url, sleep).await;
+
+ apply_result(&client_m, expected, response);
+ }
+ }
+}
+
+fn apply_result(client_m: &Mutex<Client>, expected: String, response: Result<String, TestResult>) {
+ let mut client = client_m.lock().unwrap();
+ let test = client.check_result(&expected, &response);
+ client.apply(&test);
+}
+
+fn sleep_time(client_m: &Arc<Mutex<Client>>) -> Duration {
+ client_m.lock().unwrap().time_to_next_test()
+}
+
+async fn send_test(input: &String, url: &String, timeout: Duration) -> Result<String, TestResult> {
+ info!("Sending {} to {}", input, url);
+ let body = input.clone();
+ let response = reqwest::Client::new()
+ .post(url)
+ .timeout(timeout)
+ .header("content-type", "text/plain")
+ .body(body)
+ .send()
+ .await;
+ match response {
+ Ok(response) => {
+ let body = response.text().await.unwrap();
+ info!("Response from {}: {}", url, body);
+ Ok(body)
+ }
+ Err(e) => {
+ info!("Error sending test: {}", e);
+ Err(TestResult::ErrorSendingTest(e.to_string()))
+ }
+ }
+}
+
+#[cfg(test)]
+mod app_tests {
+ use std::str::from_utf8;
+ use std::sync::Arc;
+
+ use actix_web::http::header::TryIntoHeaderValue;
+ use actix_web::{body, http::header::ContentType, middleware::Logger, test, App};
+ use lambda::ast::Value;
+
+ use super::*;
+
+ #[actix_web::test]
+ async fn post_registration_returns_success_with_unique_id() {
+ let state = Arc::new(Mutex::new(State::new()));
+ // FIXME should only be called once, move to setup
+ env_logger::init();
+
+ let app = test::init_service(
+ App::new()
+ .wrap(Logger::default())
+ .app_data(web::Data::new(state))
+ .service(register),
+ )
+ .await;
+ let url = "http://192.168.1.1".to_string();
+ let req = test::TestRequest::post()
+ .uri("/register")
+ .set_json(Registration {
+ url: url.clone(),
+ name: "foo".to_string(),
+ })
+ .insert_header(ContentType::json())
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_success());
+
+ let body = resp.into_body();
+ let bytes = body::to_bytes(body).await;
+ match serde_json::from_slice::<RegistrationResult>(bytes.as_ref().unwrap()).unwrap() {
+ RegistrationResult::RegistrationSuccess { id: _, url: url1 } => assert_eq!(url1, url),
+ _ => panic!("Expected RegistrationSuccess, got {:?}", bytes.unwrap()),
+ };
+ }
+
+ #[actix_web::test]
+ async fn post_registration_returns_400_when_register_fails() {
+ let state = Arc::new(Mutex::new(State::new()));
+
+ let app = test::init_service(
+ App::new()
+ .wrap(Logger::default())
+ .app_data(web::Data::new(state.clone()))
+ .service(register),
+ )
+ .await;
+ let url = "http://192.168.1.1".to_string();
+ let registration = Registration {
+ url: url.clone(),
+ name: "foo".to_string(),
+ };
+
+ state.lock().unwrap().register(&registration);
+
+ let req = test::TestRequest::post()
+ .uri("/register")
+ .set_json(registration)
+ .insert_header(ContentType::json())
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_client_error());
+ assert_eq!(
+ ContentType::json().try_into_value().unwrap(),
+ resp.headers().get("content-type").unwrap()
+ );
+ }
+
+ #[actix_web::test]
+ async fn post_expression_returns_evaluation() {
+ let app = test::init_service(App::new().wrap(Logger::default()).service(eval)).await;
+
+ let req = test::TestRequest::post()
+ .uri("/eval")
+ .set_payload("((lam (x y) x) 1 2)")
+ .insert_header(ContentType::plaintext())
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_success());
+
+ let body = resp.into_body();
+ let bytes = body::to_bytes(body).await.unwrap();
+ assert_eq!(bytes, "1".to_string().into_bytes());
+ }
+
+ #[actix_web::test]
+ async fn post_expression_returns_multiple_evaluations() {
+ let app = test::init_service(App::new().wrap(Logger::default()).service(eval)).await;
+
+ let req = test::TestRequest::post()
+ .uri("/eval")
+ .set_payload("((lam (x y) x) 1 2)\n42")
+ .insert_header(ContentType::plaintext())
+ .to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_success());
+
+ let body = resp.into_body();
+ let bytes = body::to_bytes(body).await.unwrap();
+ assert_eq!("1\n42".to_string().into_bytes(), bytes);
+ }
+
+ #[actix_web::test]
+ async fn get_leaderboard_returns_html_page_listing_clients_state() {
+ let app_state = Arc::new(Mutex::new(State::new()));
+ app_state.lock().unwrap().register(&Registration {
+ url: "http://1.2.3.4".to_string(),
+ name: "client1".to_string(),
+ });
+
+ let mut handlebars = Handlebars::new();
+ handlebars
+ .register_templates_directory(
+ "./templates",
+ DirectorySourceOptions {
+ tpl_extension: ".html".to_owned(),
+ hidden: false,
+ temporary: false,
+ },
+ )
+ .unwrap();
+
+ let app = test::init_service(
+ App::new()
+ .wrap(Logger::default())
+ .app_data(web::Data::new(app_state.clone()))
+ .app_data(web::Data::new(handlebars.clone()))
+ .service(leaderboard),
+ )
+ .await;
+
+ let req = test::TestRequest::get().uri("/leaderboard").to_request();
+
+ let resp = test::call_service(&app, req).await;
+
+ assert!(resp.status().is_success());
+
+ let bytes = body::to_bytes(resp.into_body()).await.unwrap();
+ assert!(from_utf8(&bytes).unwrap().contains("client1"));
+ }
+
+ #[test]
+ async fn app_does_not_register_same_url_twice() {
+ let mut app_state = State::new();
+ let registration = Registration {
+ name: "foo".to_string(),
+ url: "http://1.2.3.4".to_string(),
+ };
+
+ app_state.register(&registration);
+ let result = app_state.register(&registration);
+
+ assert_eq!(
+ RegistrationResult::UrlAlreadyRegistered {
+ url: "http://1.2.3.4".to_string()
+ },
+ result
+ );
+ }
+
+ #[test]
+ async fn unregistering_registered_client_stops_tester_thread_from_sending_tests() {
+ let mut app_state = State::with_duration(Duration::from_millis(100));
+ let registration = Registration {
+ name: "foo".to_string(),
+ url: "http://1.2.3.4".to_string(),
+ };
+
+ let reg = app_state.register(&registration);
+ assert!(matches!(
+ reg,
+ RegistrationResult::RegistrationSuccess { .. }
+ ));
+
+ tokio::time::sleep(Duration::from_millis(500)).await;
+
+ app_state.unregister(&registration.url);
+
+ let grade_before = app_state.client_events(&registration.url);
+ tokio::time::sleep(Duration::from_millis(500)).await;
+ let grade_after = app_state.client_events(&registration.url);
+
+ assert_eq!(grade_before, grade_after);
+ }
+
+ fn client() -> Client {
+ Client::new(
+ "http://1.2.3.4".to_string(),
+ "foo".to_string(),
+ Duration::from_secs(10),
+ )
+ }
+
+ #[test]
+ async fn client_generates_constant_at_level_1() {
+ let mut client = client();
+
+ let (input, _) = client.generate_expr();
+
+ match parse(&input)[..] {
+ [Value::Num(_)] => (),
+ _ => panic!("Expected constant 3"),
+ }
+ }
+
+ #[test]
+ async fn client_generates_different_inputs_on_each_call() {
+ let mut client = client();
+
+ let (input1, _) = client.generate_expr();
+ let (input2, _) = client.generate_expr();
+
+ assert_ne!(input1, input2);
+ }
+
+ #[test]
+ async fn client_generates_ascii_variables_at_level_2() {
+ let mut client = client();
+ client.grade = 2;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::Sym(name)] => {
+ assert!(name.chars().all(|c| c.is_ascii_alphanumeric()));
+ }
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_unicode_variables_at_level_3() {
+ let mut client = client();
+ client.grade = 3;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::Sym(_)] => (),
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_binary_application_at_level_4() {
+ let mut client = client();
+ client.grade = 4;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::App(_, _)] => (),
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_nested_applications_and_constants_at_level_5() {
+ let mut client = client();
+ client.grade = 5;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::App(_, _)] => (),
+ [Value::Sym(_)] => (),
+ [Value::Num(_)] => (),
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_lambda_terms_at_level_6() {
+ let mut client = client();
+ client.grade = 6;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::Lam(_, _)] => (),
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_application_with_lambda_terms_at_level_7() {
+ let mut client = client();
+ client.grade = 7;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ match &parsed[..] {
+ [Value::App(t1, _)] if matches!(**t1, Value::Lam(_, _)) => (),
+ _ => panic!("Expected symbol, got {:?}", parsed),
+ }
+ }
+
+ #[test]
+ async fn client_generates_applications_with_more_than_2_terms_at_level_8() {
+ let mut client = client();
+ client.grade = 8;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+ if let [Value::App(_, _)] = &parsed[..] {
+ assert!(input.split(' ').count() >= 2)
+ }
+ }
+
+ #[test]
+ async fn client_generates_more_complex_terms_at_level_9() {
+ let mut client = client();
+ client.grade = 9;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+
+ assert!(!parsed.is_empty());
+ }
+
+ #[test]
+ async fn client_generates_multiple_terms_at_level_10() {
+ let mut client = client();
+ client.grade = 10;
+
+ let (input, _) = client.generate_expr();
+
+ let parsed = parse(&input);
+
+ assert!(!parsed.is_empty());
+ }
+
+ #[test]
+ async fn client_increases_grade_on_successful_test() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestSucceeded,
+ };
+
+ client.apply(&test);
+
+ assert_eq!(2, client.grade);
+ }
+
+ #[test]
+ async fn client_stores_test_results() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestSucceeded,
+ };
+
+ client.apply(&test);
+
+ assert_eq!(test, client.results.first().unwrap().clone());
+ }
+
+ #[test]
+ async fn client_returns_test_successful_if_result_match() {
+ let client = client();
+ let expected = "1".to_string();
+ let response = Ok("1".to_string());
+
+ let test = client.check_result(&expected, &response);
+
+ assert_eq!(TestResult::TestSucceeded, test.result);
+ }
+
+ #[test]
+ async fn client_returns_test_failed_given_result_do_not_match() {
+ let client = client();
+ let expected = "1".to_string();
+ let response = Ok("2".to_string());
+
+ let test = client.check_result(&expected, &response);
+
+ assert_eq!(TestResult::TestFailed("2".to_string()), test.result);
+ }
+
+ #[test]
+ async fn client_does_not_increase_grade_on_failed_test() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestFailed("2".to_string()),
+ };
+
+ client.apply(&test);
+
+ assert_eq!(1, client.grade);
+ }
+
+ #[test]
+ async fn client_starts_delay_to_next_test_at_10s() {
+ let client = client();
+
+ let delay = client.time_to_next_test();
+
+ assert_eq!(std::time::Duration::from_secs(10), delay);
+ }
+
+ #[test]
+ async fn client_increases_delay_to_next_upon_failed_test() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestFailed("2".to_string()),
+ };
+ let delay_before = client.time_to_next_test();
+
+ client.apply(&test);
+
+ assert!(delay_before < client.time_to_next_test());
+ }
+
+ #[test]
+ async fn client_increases_delay_to_maximum_of_30s() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestFailed("2".to_string()),
+ };
+
+ for _ in 0..100 {
+ client.apply(&test);
+ }
+
+ assert_eq!(Duration::from_secs(30), client.time_to_next_test());
+ }
+
+ #[test]
+ async fn client_score_cannot_go_beyond_255() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestSucceeded,
+ };
+
+ for _ in 0..256 {
+ client.apply(&test);
+ }
+
+ assert_eq!(255, client.grade);
+ }
+
+ #[test]
+ async fn client_decreases_delay_to_next_upon_successful_test() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestSucceeded,
+ };
+ let delay_before = client.time_to_next_test();
+
+ client.apply(&test);
+
+ assert!(delay_before > client.time_to_next_test());
+ }
+
+ #[test]
+ async fn client_decreases_delay_to_minimum_of_500ms() {
+ let mut client = client();
+ let test = Test {
+ timestamp: chrono::offset::Utc::now(),
+ result: TestResult::TestSucceeded,
+ };
+
+ for _ in 0..100 {
+ client.apply(&test);
+ }
+
+ assert_eq!(Duration::from_millis(500), client.time_to_next_test());
+ }
+}