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, 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, } 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, delay: std::time::Duration, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] struct Test { timestamp: DateTime, 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::>() .join("\n"); let expected = eval_all(&exprs); ( input, expected .iter() .map(|v| format!("{}", v)) .collect::>() .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) -> Test { let result = match response { Ok(expr) => { if let Ok(vals) = parse_total(expr) { let actual = eval_all(&vals) .iter() .map(|v| format!("{}", v)) .collect::>() .join("\n"); if actual == *expected { TestResult::TestSucceeded } else { TestResult::TestFailed(actual) } } else { TestResult::TestFailed("Could not parse response".to_string()) } } Err(res) => res.clone(), }; Test { result, timestamp: chrono::offset::Utc::now(), } } } #[derive(Debug)] struct State { base_duration: Duration, clients: HashMap>, 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(®istration.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>>, registration: web::Json, ) -> impl Responder { let result = app_state.lock().unwrap().register(®istration); 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::>() .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>>, hb: web::Data>, ) -> 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) .service(actix_files::Files::new("/", "./static").show_files_listing() ) }) .bind((options.host, options.port))? .run() .await } fn get_test(client_m: &Mutex) -> (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>) { 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, expected: String, response: Result) { let mut client = client_m.lock().unwrap(); let test = client.check_result(&expected, &response); client.apply(&test); } fn sleep_time(client_m: &Arc>) -> Duration { client_m.lock().unwrap().time_to_next_test() } async fn send_test(input: &String, url: &String, timeout: Duration) -> Result { 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::(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(®istration); 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(®istration); let result = app_state.register(®istration); 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(®istration); assert!(matches!( reg, RegistrationResult::RegistrationSuccess { .. } )); tokio::time::sleep(Duration::from_millis(500)).await; app_state.unregister(®istration.url); let grade_before = app_state.client_events(®istration.url); tokio::time::sleep(Duration::from_millis(500)).await; let grade_after = app_state.client_events(®istration.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()); } }