diff options
| author | Arnaud Bailly <arnaud.bailly@iohk.io> | 2025-01-25 10:45:41 +0100 |
|---|---|---|
| committer | Arnaud Bailly <arnaud.bailly@iohk.io> | 2025-01-25 10:45:41 +0100 |
| commit | 7752d73216578d5961751b5d0535088d384b4aa6 (patch) | |
| tree | 786e46fe1276e93ade0a48398cd4c9ac13081707 /rust/src/web.rs | |
| parent | d6f68e919db51d366c8ca3c1509bea12aa81d692 (diff) | |
| download | lambda-nantes-7752d73216578d5961751b5d0535088d384b4aa6.tar.gz | |
Move λ-calcul workshop code to subdirectory
Diffstat (limited to 'rust/src/web.rs')
| -rw-r--r-- | rust/src/web.rs | 899 |
1 files changed, 0 insertions, 899 deletions
diff --git a/rust/src/web.rs b/rust/src/web.rs deleted file mode 100644 index 3f8f056..0000000 --- a/rust/src/web.rs +++ /dev/null @@ -1,899 +0,0 @@ -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()); - } -} |
