From 8d6e844873e6f04989e6c00d01a57c0d81a05f3e Mon Sep 17 00:00:00 2001 From: Alexis Langlet Date: Mon, 30 Oct 2023 15:37:10 +0100 Subject: [PATCH 1/5] test(api): test parse_response --- api/src/api/mod.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/api/src/api/mod.rs b/api/src/api/mod.rs index 1f902fc..ff1cca2 100644 --- a/api/src/api/mod.rs +++ b/api/src/api/mod.rs @@ -72,3 +72,63 @@ fn parse_response(response: ExecuteResponse) -> RunResponse { stderr, } } + +#[cfg(test)] +mod test{ + use crate::{vm_manager::grpc_definitions::{ExecuteResponse, ExecuteResponseStep}, api::parse_response}; + + + #[test] + fn test_parse_response_stdout() { + let response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "World".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }; + + let parsed = parse_response(response); + + assert_eq!(parsed.stdout, "HelloWorld"); + assert_eq!(parsed.stderr, ""); + assert_eq!(parsed.status, 0); + } + + #[test] + fn test_parse_response_with_error() { + let response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "".to_string(), + stderr: "Error".to_string(), + exit_code: 1, + }, + ], + }; + + let parsed = parse_response(response); + + assert_eq!(parsed.stdout, "Hello"); + assert_eq!(parsed.stderr, "Error"); + assert_eq!(parsed.status, 1); + } +} \ No newline at end of file From 1918de9f2e8d1b14ce063f2e0a7e0646bd0137db Mon Sep 17 00:00:00 2001 From: Alexis Langlet Date: Tue, 31 Oct 2023 09:07:59 +0100 Subject: [PATCH 2/5] test(api): test generate_steps and find language --- api/src/api/service.rs | 128 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/api/src/api/service.rs b/api/src/api/service.rs index c35cd87..f76fe60 100644 --- a/api/src/api/service.rs +++ b/api/src/api/service.rs @@ -33,7 +33,7 @@ impl LambdoApiService { let entrypoint = request.code[0].filename.clone(); let language_settings = self.find_language(&request.language).unwrap(); - let steps = self.generate_steps(&language_settings, &entrypoint); + let steps = Self::generate_steps(&language_settings, &entrypoint); let file = FileModel { filename: entrypoint.to_string(), content: request.code[0].content.clone(), @@ -75,7 +75,6 @@ impl LambdoApiService { } fn generate_steps( - &self, language_settings: &LambdoLanguageConfig, entrypoint: &str, ) -> Vec { @@ -91,3 +90,128 @@ impl LambdoApiService { steps } } + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use tokio::sync::Mutex; + + use super::LambdoApiService; + use crate::{ + config::{ + LambdoAgentConfig, LambdoApiConfig, LambdoConfig, LambdoLanguageConfig, + LambdoLanguageStepConfig, LambdoLanguageStepOutputConfig, LambdoVMMConfig, + }, + vm_manager::{state::LambdoState, VMManager}, + }; + + fn generate_lambdo_test_config() -> LambdoConfig { + LambdoConfig { + apiVersion: "lambdo.io/v1alpha1".to_string(), + kind: "Config".to_string(), + api: LambdoApiConfig { + web_host: "0.0.0.0".to_string(), + web_port: 3000, + grpc_host: "0.0.0.0".to_string(), + gprc_port: 50051, + bridge: "lambdo0".to_string(), + bridge_address: "0.0.0.0".to_string(), + }, + vmm: LambdoVMMConfig { + kernel: "/var/lib/lambdo/kernel/vmlinux.bin".to_string(), + }, + agent: LambdoAgentConfig { + path: "/usr/local/bin/lambdo-agent".to_string(), + config: "/etc/lambdo/agent.yaml".to_string(), + }, + languages: vec![ + LambdoLanguageConfig { + name: "NODE".to_string(), + version: "1.0".to_string(), + initramfs: "test".to_string(), + steps: vec![ + LambdoLanguageStepConfig { + name: Some("step 1".to_string()), + command: "echo {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + LambdoLanguageStepConfig { + name: Some("step 2".to_string()), + command: "echo hello".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + LambdoLanguageStepConfig { + name: Some("step 3".to_string()), + command: "cat {{filename}} > {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }, + ], + }, + LambdoLanguageConfig { + name: "PYTHON".to_string(), + version: "3.0".to_string(), + initramfs: "test".to_string(), + steps: vec![LambdoLanguageStepConfig { + name: Some("step".to_string()), + command: "echo {{filename}}".to_string(), + output: LambdoLanguageStepOutputConfig { + enabled: true, + debug: false, + }, + }], + }, + ], + } + } + + #[test] + fn test_generate_steps() { + let language_settings = LambdoLanguageConfig { + name: "NODE".to_string(), + version: "1.0".to_string(), + initramfs: "test".to_string(), + steps: generate_lambdo_test_config().languages[0].steps.clone(), + }; + let entrypoint = "index.js"; + + let expected_steps = vec![ + "echo index.js".to_string(), + "echo hello".to_string(), + "cat index.js > index.js".to_string(), + ]; + + let steps = LambdoApiService::generate_steps(&language_settings, &entrypoint); + + assert_eq!(steps.len(), 3); + for (i, step) in steps.iter().enumerate() { + assert_eq!(step.command, expected_steps[i]); + } + } + + #[test] + fn test_find_language() { + let config = generate_lambdo_test_config(); + let service = LambdoApiService { + config: config.clone(), + vm_manager: VMManager { + state: Arc::new(Mutex::new(LambdoState::new(config))), + }, + }; + + let language = "NODE".to_string(); + let language_settings = service.find_language(&language).unwrap(); + + assert_eq!(language_settings.name, language); + assert_eq!(language_settings.steps[0].name, Some("step 1".to_string())); + } +} From ba54bec2240d1cae21c168da5a09ba4743c5dfbb Mon Sep 17 00:00:00 2001 From: Alexis Langlet Date: Tue, 31 Oct 2023 09:10:40 +0100 Subject: [PATCH 3/5] test(api): add mockall and automock VMManagerTrait Define a trait that is implemented by VMManger for testing purpose also refactor part of vm_manager in the meantime --- api/Cargo.toml | 2 ++ api/src/api/service.rs | 25 ++++++++++++++++--------- api/src/vm_manager/mod.rs | 24 +++++++++++++++++++++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/api/Cargo.toml b/api/Cargo.toml index 9c6f1de..5ba95db 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -23,6 +23,8 @@ rand = "0.8.4" tokio = { version = "1.0", features = ["rt-multi-thread", "macros", "process"] } tonic = { version = "0.10.2", features = ["transport"] } prost = "0.12.1" +async-trait = "0.1.74" +mockall = "0.11.4" [build-dependencies] tonic-build = { version = "0.10.2", features = ["prost"] } diff --git a/api/src/api/service.rs b/api/src/api/service.rs index f76fe60..39f0ad8 100644 --- a/api/src/api/service.rs +++ b/api/src/api/service.rs @@ -1,7 +1,8 @@ use crate::{ config::{LambdoConfig, LambdoLanguageConfig}, - vm_manager::grpc_definitions::{ - ExecuteRequest, ExecuteRequestStep, ExecuteResponse, FileModel, + vm_manager::{ + grpc_definitions::{ExecuteRequest, ExecuteRequestStep, ExecuteResponse, FileModel}, + VMManagerTrait, }, vm_manager::{state::LambdoStateRef, Error, VMManager}, }; @@ -12,21 +13,27 @@ use crate::model::RunRequest; pub struct LambdoApiService { pub config: LambdoConfig, - pub vm_manager: VMManager, + pub vm_manager: Box, } impl LambdoApiService { pub async fn new(config: LambdoConfig) -> Result { let state = crate::vm_manager::state::LambdoState::new(config.clone()); let vm_manager = - VMManager::new(std::sync::Arc::new(tokio::sync::Mutex::new(state))).await?; - Ok(LambdoApiService { config, vm_manager }) + VMManager::from_state(std::sync::Arc::new(tokio::sync::Mutex::new(state))).await?; + Ok(LambdoApiService { + config, + vm_manager: Box::new(vm_manager), + }) } pub async fn new_with_state(state: LambdoStateRef) -> Result { let config = state.lock().await.config.clone(); - let vm_manager = VMManager::new(state).await?; - Ok(LambdoApiService { config, vm_manager }) + let vm_manager = VMManager::from_state(state).await?; + Ok(LambdoApiService { + config, + vm_manager: Box::new(vm_manager), + }) } pub async fn run_code(&self, request: RunRequest) -> Result { @@ -203,9 +210,9 @@ mod test { let config = generate_lambdo_test_config(); let service = LambdoApiService { config: config.clone(), - vm_manager: VMManager { + vm_manager: Box::new(VMManager { state: Arc::new(Mutex::new(LambdoState::new(config))), - }, + }), }; let language = "NODE".to_string(); diff --git a/api/src/vm_manager/mod.rs b/api/src/vm_manager/mod.rs index c66fd97..c24de4b 100644 --- a/api/src/vm_manager/mod.rs +++ b/api/src/vm_manager/mod.rs @@ -1,4 +1,5 @@ pub mod state; +use mockall::automock; use network_interface::{NetworkInterface, NetworkInterfaceConfig}; use tokio::process::Command; @@ -21,12 +22,27 @@ use self::{ mod vmm; +#[automock] +#[async_trait::async_trait] +pub trait VMManagerTrait: Sync + Send { + async fn from_state(state: LambdoStateRef) -> Result + where + Self: Sized; + + async fn run_code( + &self, + request: ExecuteRequest, + language_settings: LanguageSettings, + ) -> Result; +} + pub struct VMManager { pub state: LambdoStateRef, } -impl VMManager { - pub async fn new(state: LambdoStateRef) -> Result { +#[async_trait::async_trait] +impl VMManagerTrait for VMManager { + async fn from_state(state: LambdoStateRef) -> Result { let mut vmm_manager = VMManager { state }; { @@ -51,7 +67,7 @@ impl VMManager { Ok(vmm_manager) } - pub async fn run_code( + async fn run_code( &self, request: ExecuteRequest, language_settings: LanguageSettings, @@ -112,7 +128,9 @@ impl VMManager { Ok(response) } +} +impl VMManager { pub async fn event_listener(&mut self) { let mut receiver = self.state.lock().await.channel.1.resubscribe(); let state = self.state.clone(); From 0b1964dd246e8a73f102958574d8fd78fd71fc88 Mon Sep 17 00:00:00 2001 From: Alexis Langlet Date: Wed, 1 Nov 2023 11:43:53 +0100 Subject: [PATCH 4/5] test(api): test run_code in service --- api/src/api/service.rs | 78 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/api/src/api/service.rs b/api/src/api/service.rs index 39f0ad8..d4f5d5b 100644 --- a/api/src/api/service.rs +++ b/api/src/api/service.rs @@ -102,6 +102,7 @@ impl LambdoApiService { mod test { use std::sync::Arc; + use mockall::predicate; use tokio::sync::Mutex; use super::LambdoApiService; @@ -110,7 +111,12 @@ mod test { LambdoAgentConfig, LambdoApiConfig, LambdoConfig, LambdoLanguageConfig, LambdoLanguageStepConfig, LambdoLanguageStepOutputConfig, LambdoVMMConfig, }, - vm_manager::{state::LambdoState, VMManager}, + model::{LanguageSettings, RunRequest}, + vm_manager::{ + grpc_definitions::{ExecuteRequest, ExecuteResponse, ExecuteResponseStep, FileModel}, + state::LambdoState, + MockVMManagerTrait, VMManager, + }, }; fn generate_lambdo_test_config() -> LambdoConfig { @@ -221,4 +227,74 @@ mod test { assert_eq!(language_settings.name, language); assert_eq!(language_settings.steps[0].name, Some("step 1".to_string())); } + + #[tokio::test] + async fn test_run_code() { + let config = generate_lambdo_test_config(); + + let language = "NODE".to_string(); + let code = vec![FileModel { + filename: "index.js".to_string(), + content: "console.log('hello world')".to_string(), + }]; + let input = "hello".to_string(); + + let request = RunRequest { + version: "1.0".to_string(), + language: language.clone(), + code, + input, + }; + + let expected_language_settings = config.languages[0].clone(); + assert_eq!(expected_language_settings.name, language.clone()); + + let expected_response = ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo index.js".to_string(), + stdout: "index.js\n".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo hello".to_string(), + stdout: "hello\n".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "cat index.js > index.js".to_string(), + stdout: "".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }; + + let response = expected_response.clone(); + let mut mock_vm_manager = MockVMManagerTrait::new(); + mock_vm_manager + .expect_run_code() + .with( + predicate::function(|req: &ExecuteRequest| { + req.files[0].filename == "index.js" && req.steps[0].command == "echo index.js" + }), + predicate::function(move |lang: &LanguageSettings| { + lang.name == language && lang.version == expected_language_settings.version + }), + ) + .times(1) + .returning(move |_, _| Ok(response.clone())); + + let service = LambdoApiService { + config: config.clone(), + vm_manager: Box::new(mock_vm_manager), + }; + + let response = service.run_code(request).await.unwrap(); + + assert_eq!(response, expected_response); + } } From 600982f4a27eb5406c384ed0c650ae152c465c7a Mon Sep 17 00:00:00 2001 From: Alexis Langlet Date: Fri, 24 Nov 2023 12:51:05 +0100 Subject: [PATCH 5/5] test(api): test run_code in mod --- api/src/api/mod.rs | 127 +++++++++++++++++++++++++++++++++-------- api/src/api/service.rs | 71 +++++++++++++---------- api/src/main.rs | 14 +++-- 3 files changed, 154 insertions(+), 58 deletions(-) diff --git a/api/src/api/mod.rs b/api/src/api/mod.rs index ff1cca2..4e3d19c 100644 --- a/api/src/api/mod.rs +++ b/api/src/api/mod.rs @@ -4,40 +4,29 @@ use actix_web::{post, web, Responder}; use log::{debug, error, info, trace, warn}; use crate::{ - api::service::LambdoApiService, + api::service::{LambdoApiService, LambdoApiServiceTrait}, model::{RunRequest, RunResponse}, vm_manager::{self, grpc_definitions::ExecuteResponse}, }; use std::error::Error; -#[post("/run")] -async fn run( - run_body: web::Json, - service: web::Data, -) -> Result> { - debug!( - "Received code execution request from http (language: {}, version: {})", - run_body.language, run_body.version - ); - trace!("Request body: {:?}", run_body); +async fn run_code(run_resquest: RunRequest, service: &dyn LambdoApiServiceTrait) -> RunResponse { + let response = service.run_code(run_resquest).await; - let response = service.run_code(run_body.into_inner()).await; - - let response = match response { + match response { Ok(response) => { info!("Execution ended for {:?}", response.id); trace!("Response: {:?}", response); parse_response(response) } - // for the moment just signal an internal server error Err(e) => match e { vm_manager::Error::Timeout => { warn!("Timeout while executing code"); - return Ok(web::Json(RunResponse { + RunResponse { status: 128, stdout: "".to_string(), stderr: "Timeout".to_string(), - })); + } } _ => { error!("Error while executing code: {:?}", e); @@ -48,12 +37,35 @@ async fn run( } } }, - }; + } +} + +#[post("/run")] +pub async fn post_run_route( + run_body: web::Json, + api_service: web::Data, +) -> Result> { + debug!( + "Received code execution request from http (language: {}, version: {})", + run_body.language, run_body.version + ); + trace!("Request body: {:?}", run_body); + + let service = api_service.get_ref(); + let result = run_code(run_body.into_inner(), service); - Ok(web::Json(response)) + Ok(web::Json(result.await)) } fn parse_response(response: ExecuteResponse) -> RunResponse { + if response.steps.is_empty() { + return RunResponse { + status: 1, + stdout: "".to_string(), + stderr: "Nothing was run".to_string(), + }; + } + let mut stdout = String::new(); let mut stderr = String::new(); for step in response.steps.as_slice() { @@ -67,16 +79,23 @@ fn parse_response(response: ExecuteResponse) -> RunResponse { status: response.steps[response.steps.len() - 1] .exit_code .try_into() - .unwrap(), + .unwrap_or(1), stdout, stderr, } } #[cfg(test)] -mod test{ - use crate::{vm_manager::grpc_definitions::{ExecuteResponse, ExecuteResponseStep}, api::parse_response}; +mod test { + use std::vec; + + use crate::{ + api::{parse_response, run_code}, + model::RunRequest, + vm_manager::grpc_definitions::{ExecuteResponse, ExecuteResponseStep, FileModel}, + }; + use super::service::MockLambdoApiServiceTrait; #[test] fn test_parse_response_stdout() { @@ -131,4 +150,66 @@ mod test{ assert_eq!(parsed.stderr, "Error"); assert_eq!(parsed.status, 1); } -} \ No newline at end of file + + #[tokio::test] + async fn test_run_code_with_no_steps() { + let mut mock_service = MockLambdoApiServiceTrait::new(); + mock_service.expect_run_code().once().returning(|_| { + Ok(ExecuteResponse { + id: "test".to_string(), + steps: vec![], + }) + }); + + let run_request = RunRequest { + language: "Node".to_string(), + version: "1".to_string(), + code: vec![], + input: "".to_string(), + }; + + let response = run_code(run_request, &mock_service).await; + assert_eq!(response.status, 1); + assert_eq!(response.stdout, ""); + assert_eq!(response.stderr, "Nothing was run"); + } + + #[tokio::test] + async fn test_run_with_steps() { + let mut mock_service = MockLambdoApiServiceTrait::new(); + mock_service.expect_run_code().once().returning(|_| { + Ok(ExecuteResponse { + id: "test".to_string(), + steps: vec![ + ExecuteResponseStep { + command: "echo Hello".to_string(), + stdout: "Hello".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ExecuteResponseStep { + command: "echo World".to_string(), + stdout: "World".to_string(), + stderr: "".to_string(), + exit_code: 0, + }, + ], + }) + }); + + let run_request = RunRequest { + language: "Node".to_string(), + version: "1".to_string(), + code: vec![FileModel { + filename: "test.js".to_string(), + content: "console.log('Hello World')".to_string(), + }], + input: "test.js".to_string(), + }; + + let response = run_code(run_request, &mock_service).await; + assert_eq!(response.status, 0); + assert_eq!(response.stdout, "HelloWorld"); + assert_eq!(response.stderr, ""); + } +} diff --git a/api/src/api/service.rs b/api/src/api/service.rs index d4f5d5b..429b2f6 100644 --- a/api/src/api/service.rs +++ b/api/src/api/service.rs @@ -7,10 +7,17 @@ use crate::{ vm_manager::{state::LambdoStateRef, Error, VMManager}, }; use log::{debug, trace}; +use mockall::automock; use uuid::Uuid; use crate::model::RunRequest; +#[automock] +#[async_trait::async_trait] +pub trait LambdoApiServiceTrait: Send + Sync { + async fn run_code(&self, request: RunRequest) -> Result; +} + pub struct LambdoApiService { pub config: LambdoConfig, pub vm_manager: Box, @@ -36,7 +43,39 @@ impl LambdoApiService { }) } - pub async fn run_code(&self, request: RunRequest) -> Result { + fn find_language( + &self, + language: &String, + ) -> Result> { + let language_list = &self.config.languages; + for lang in language_list { + if &*lang.name == language { + return Ok(lang.clone()); + } + } + Err("Language not found".into()) + } + + fn generate_steps( + language_settings: &LambdoLanguageConfig, + entrypoint: &str, + ) -> Vec { + let mut steps: Vec = Vec::new(); + for step in &language_settings.steps { + let command = step.command.replace("{{filename}}", entrypoint); + + steps.push(ExecuteRequestStep { + command, + enable_output: step.output.enabled, + }); + } + steps + } +} + +#[async_trait::async_trait] +impl LambdoApiServiceTrait for LambdoApiService { + async fn run_code(&self, request: RunRequest) -> Result { let entrypoint = request.code[0].filename.clone(); let language_settings = self.find_language(&request.language).unwrap(); @@ -67,35 +106,6 @@ impl LambdoApiService { response } - - fn find_language( - &self, - language: &String, - ) -> Result> { - let language_list = &self.config.languages; - for lang in language_list { - if &*lang.name == language { - return Ok(lang.clone()); - } - } - Err("Language not found".into()) - } - - fn generate_steps( - language_settings: &LambdoLanguageConfig, - entrypoint: &str, - ) -> Vec { - let mut steps: Vec = Vec::new(); - for step in &language_settings.steps { - let command = step.command.replace("{{filename}}", entrypoint); - - steps.push(ExecuteRequestStep { - command, - enable_output: step.output.enabled, - }); - } - steps - } } #[cfg(test)] @@ -107,6 +117,7 @@ mod test { use super::LambdoApiService; use crate::{ + api::service::LambdoApiServiceTrait, config::{ LambdoAgentConfig, LambdoApiConfig, LambdoConfig, LambdoLanguageConfig, LambdoLanguageStepConfig, LambdoLanguageStepOutputConfig, LambdoVMMConfig, diff --git a/api/src/main.rs b/api/src/main.rs index d01a9aa..caf0c9e 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -9,7 +9,7 @@ use config::LambdoConfig; use thiserror::Error; use crate::{ - api::{run, service::LambdoApiService}, + api::{post_run_route, service::LambdoApiService}, vm_manager::grpc_definitions::lambdo_api_service_server::LambdoApiServiceServer, vm_manager::state::LambdoState, vm_manager::VMListener, @@ -87,8 +87,12 @@ async fn main() -> std::io::Result<()> { let http_port = config.api.web_port; let app_state = web::Data::new(api_service); info!("Starting web server on {}:{}", http_host, http_port); - HttpServer::new(move || App::new().app_data(app_state.clone()).service(run)) - .bind((http_host.clone(), http_port))? - .run() - .await + HttpServer::new(move || { + App::new() + .app_data(app_state.clone()) + .service(post_run_route) + }) + .bind((http_host.clone(), http_port))? + .run() + .await }