// SPDX-FileCopyrightText: 2023 Aravinth Manivannan // // SPDX-License-Identifier: AGPL-3.0-or-later use std::collections::HashMap; use tokio::sync::mpsc::{self, Receiver}; use url::Url; use super::results::*; use crate::complaince::result::Result as CResult; use crate::complaince::suite::Suite; use crate::complaince::suite::Test; use crate::utils::get_random; pub struct SuiteRunnerState { suite: Suite, tests: HashMap, } #[derive(Debug)] pub struct TestRunnerState { rx: Receiver, container_name: String, test: Test, } const FTEST_NGINX_PROXY: &str = "ftest"; impl SuiteRunnerState { pub async fn run( container_host: &Url, suite: &crate::complaince::suite::Suite, ctx: &crate::ctx::ArcCtx, ) -> ArchivableSuiteResult { let state = Self::launch_suite(container_host, suite, ctx); state.collect_results(ctx).await } pub fn run_proxy(ctx: &crate::ctx::ArcCtx) { Self::stop_proxy(ctx); let mut default = ""; let nets = std::process::Command::new("/sbin/ip") .arg("route") .output() .unwrap(); let nets = String::from_utf8(nets.stdout).unwrap(); for l in nets.lines() { let f = l.split(' ').collect::>(); if let Some("default") = f.get(0).map(|s| *s) { default = f.get(8).unwrap(); log::info!("Found default net: {default}"); } } let args = [ "run", "-d", "-p", "9080:9000", "--name", FTEST_NGINX_PROXY, "--network", "ftest", "--add-host", &format!("ftest_backend:{default}"), "forgeflux/ftest-nginx-proxy", ]; let mut child = std::process::Command::new("docker") .args(&args) .spawn() .expect("unable to obtain Docker version"); child.wait().unwrap(); log::info!("Started {FTEST_NGINX_PROXY} nginx proxy"); } pub fn stop_proxy(ctx: &crate::ctx::ArcCtx) { ctx.docker.rm_container(FTEST_NGINX_PROXY, true); log::info!("Stopped {FTEST_NGINX_PROXY} nginx proxy"); } fn launch_suite( container_host: &Url, suite: &crate::complaince::suite::Suite, ctx: &crate::ctx::ArcCtx, ) -> Self { let mut tests = HashMap::with_capacity(suite.tests.len()); for test in suite.tests.iter() { let auth = get_random(32); println!("starting test {}", test.name); let mut env = HashMap::new(); env.insert("FTEST_AUTH".into(), auth.to_owned()); env.insert( "FTEST_HOST".into(), format!("http://ftest:{}", ctx.settings.server.port), ); env.insert("FTEST_TARGET_HOST".into(), container_host.to_string()); env.insert("FTEST_USER".into(), "alice".into()); if let Some(custom_vars) = test.env_vars.clone() { env.extend(custom_vars); } let name = format!("{}---{}--{}", suite.name, test.name, &auth[0..5]); tracing::info!("Starting {name} with env vars {:?}", &env); ctx.docker.run_container( &name, &test.container, true, &env, Some("ftest".to_string()), true, ); let (tx, rx) = mpsc::channel(1); { let mut w = ctx.results.write().unwrap(); w.insert(auth.to_owned(), tx); } tests.insert( auth.to_owned(), TestRunnerState { container_name: name, rx, test: test.clone(), }, ); } println!("{:?}", tests); SuiteRunnerState { suite: suite.clone(), tests, } } async fn collect_results(mut self, ctx: &crate::ctx::ArcCtx) -> ArchivableSuiteResult { let mut tests = Vec::with_capacity(self.tests.len()); for (_auth, mut r) in self.tests.drain() { let result = r.rx.recv().await.unwrap(); let log = ctx.docker.get_logs(&r.container_name); ctx.docker.rm_container(&r.container_name, true); let container = ArchivableContainer { name: r.container_name, logs: log, }; let res = ArchivableTestResult { success: result.success, logs: result.logs, container, }; let s = ArchivableTest { name: r.test.name, url: r.test.url, version: r.test.version, container: r.test.container, env_vars: r.test.env_vars, result: res, }; tests.push(s); } ArchivableSuiteResult { suite: self.suite.clone(), tests, } } } #[cfg(test)] mod tests { use super::*; use crate::complaince::result::Result as CResult; use crate::complaince::suite::Test; use crate::{AppCtx, Ctx, Settings}; use std::sync::Arc; use url::Url; // use std::sync::RwLock; // #[derive(Clone, Eq, PartialEq, Debug)] // enum ContainerState { // NoContainer, // Running(String), // Stopped, // Removed, // } // // #[derive(Clone)] // struct TestDocker { // state: Arc>, // } // // impl TestDocker { // pub fn new() -> Self { // Self { // state: Arc::new(RwLock::new(ContainerState::NoContainer)), // } // } // } // impl DockerLike for TestDocker { // fn version(&self) -> String { // unimplemented!(); // } // fn run_container( // &self, // name: &str, // img: &str, // detached: bool, // env: &HashMap, // network: Option, // pull: bool, // ) { // let mut w = self.state.write().unwrap(); // if *w == ContainerState::NoContainer { // *w = ContainerState::Running(name.to_string()); // } else { // panic!("Container is {:?}", w); // } // } // fn get_exit_status(&self, name: &str) -> isize { // 0 // } // fn get_logs(&self, name: &str) -> String { // "".into() // } // fn rm_container(&self, name: &str, force: bool) { // let mut w = self.state.write().unwrap(); // if *w == ContainerState::Stopped { // *w = ContainerState::Removed; // } else { // panic!("Container is {:?}", w); // } // } // } // #[actix_rt::test] async fn suite_runner_works() { const LOGS: &str = "SUITE RUNNER LOG STRING"; let settings = Settings::new().unwrap(); let ctx = Ctx::new(settings.clone()).await; // ctx.docker = Arc::new(TestDocker::new()); let ctx = AppCtx::new(Arc::new(ctx)); let mut dummy_test = Test { name: "suite_runner_works".into(), url: Url::parse("https://git.batsense.net/ForgeFlux/ftest").unwrap(), version: semver::Version::parse("1.0.1").unwrap(), container: "forgeflux/ftest-docker-cmd-tester".into(), env_vars: None, }; let mut env = HashMap::new(); env.insert("TEST_NAME".into(), dummy_test.name.clone()); dummy_test.env_vars = Some(env); let mut dummy_test2 = dummy_test.clone(); dummy_test2.name = "suite_runner_works2".into(); let mut env = HashMap::new(); env.insert("TEST_NAME".into(), dummy_test2.name.clone()); dummy_test2.env_vars = Some(env); let suite = Suite { name: "suite_runner_works".into(), description: "testing suite runner".into(), version: semver::Version::parse("1.0.1").unwrap(), tests: vec![dummy_test.clone(), dummy_test2.clone()], }; let state = SuiteRunnerState::launch_suite( &Url::parse("http://suite_runner_works.service").unwrap(), &suite, &ctx, ); assert_eq!(state.tests.len(), 2); std::thread::sleep(std::time::Duration::new(13, 0)); for (k, v) in state.tests.iter() { assert_eq!(ctx.docker.get_exit_status(&v.container_name), 0); let tx = { let r = ctx.results.read().unwrap(); r.get(k).unwrap().to_owned() }; let tx_result = CResult { success: true, // sent by the app logs: format!("{}{LOGS}", v.container_name), }; tx.send(tx_result).await.unwrap(); } let results = state.collect_results(&ctx).await; assert_eq!(results.tests.len(), 2); for archivable_test in results.tests.iter() { //let archivable_test = results.tests.get(0).unwrap(); let t = if archivable_test.name == dummy_test.name.as_ref() { dummy_test.clone() } else { dummy_test2.clone() }; assert_eq!(archivable_test.name, t.name); assert!(archivable_test.result.success); assert!(archivable_test.result.container.name.contains(&t.name)); assert_eq!( archivable_test.result.logs, format!("{}{LOGS}", archivable_test.result.container.name) ); println!("{}", archivable_test.result.container.logs); assert!(archivable_test.result.container.logs.contains("FTEST_AUTH")); assert!(archivable_test.result.container.logs.contains("FTEST_HOST")); assert!(archivable_test .result .container .logs .contains("FTEST_TARGET_HOST")); assert!(archivable_test.result.container.logs.contains("FTEST_USER")); assert!(archivable_test .result .container .logs .contains(&format!("TEST_NAME={}", t.name))); } } }