diff options
Diffstat (limited to 'lambda-calcul/rust/src/web.rs')
| -rw-r--r-- | lambda-calcul/rust/src/web.rs | 899 |
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(®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<Arc<Mutex<State>>>, + registration: web::Json<Registration>, +) -> 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::<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(®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()); + } +} |
