Compare commits

...

5 commits

11 changed files with 152 additions and 91 deletions

View file

@ -45,7 +45,7 @@ uuid = { version = "1.4.1", features = ["v4", "serde"] }
rand = "0.8.5" rand = "0.8.5"
semver = { version = "1.0.18", features = ["serde"] } semver = { version = "1.0.18", features = ["serde"] }
toml = "0.7.6" toml = "0.7.6"
tokio = { version = "1.32.0", features = ["sync", "time"] } tokio = { version = "1.32.0", features = ["sync", "time", "process"] }
clap = { version = "4.4.6", features = ["derive"] } clap = { version = "4.4.6", features = ["derive"] }
actix-rt = "2.7.0" actix-rt = "2.7.0"
tera = { version = "1.19.1", default-features = false } tera = { version = "1.19.1", default-features = false }

View file

@ -10,7 +10,7 @@ use tokio::sync::mpsc::Sender;
use crate::db::*; use crate::db::*;
use crate::docker::Docker; use crate::docker::Docker;
use crate::docker::DockerLike; //use crate::docker::DockerLike;
use crate::settings::Settings; use crate::settings::Settings;
use super::complaince::result::Result as CResult; use super::complaince::result::Result as CResult;
@ -21,7 +21,7 @@ pub type ArcMinCtx = Arc<dyn MinAppContext>;
pub trait FullAppContext: std::marker::Send + std::marker::Sync + CloneFullAppCtx { pub trait FullAppContext: std::marker::Send + std::marker::Sync + CloneFullAppCtx {
fn settings(&self) -> &Settings; fn settings(&self) -> &Settings;
fn db(&self) -> &Database; fn db(&self) -> &Database;
fn docker(&self) -> Arc<dyn DockerLike>; fn docker(&self) -> Arc<Docker>;
fn results(&self) -> &ResultStore; fn results(&self) -> &ResultStore;
fn port(&self) -> u32; fn port(&self) -> u32;
} }
@ -46,13 +46,13 @@ impl Clone for Box<dyn FullAppContext> {
} }
pub trait MinAppContext: std::marker::Send + std::marker::Sync { pub trait MinAppContext: std::marker::Send + std::marker::Sync {
fn docker_(&self) -> Arc<dyn DockerLike>; fn docker_(&self) -> Arc<Docker>;
fn results_(&self) -> &ResultStore; fn results_(&self) -> &ResultStore;
fn port_(&self) -> u32; fn port_(&self) -> u32;
} }
impl MinAppContext for Arc<dyn FullAppContext> { impl MinAppContext for Arc<dyn FullAppContext> {
fn docker_(&self) -> Arc<dyn DockerLike> { fn docker_(&self) -> Arc<Docker> {
self.docker() self.docker()
} }
fn results_(&self) -> &ResultStore { fn results_(&self) -> &ResultStore {
@ -68,7 +68,7 @@ pub struct DaemonCtx {
settings: Settings, settings: Settings,
db: Database, db: Database,
results: ResultStore, results: ResultStore,
docker: Arc<dyn DockerLike>, docker: Arc<Docker>,
} }
impl FullAppContext for DaemonCtx { impl FullAppContext for DaemonCtx {
@ -78,7 +78,7 @@ impl FullAppContext for DaemonCtx {
fn db(&self) -> &Database { fn db(&self) -> &Database {
&self.db &self.db
} }
fn docker(&self) -> Arc<dyn DockerLike> { fn docker(&self) -> Arc<Docker> {
self.docker.clone() self.docker.clone()
} }
fn results(&self) -> &ResultStore { fn results(&self) -> &ResultStore {
@ -90,7 +90,7 @@ impl FullAppContext for DaemonCtx {
} }
impl MinAppContext for DaemonCtx { impl MinAppContext for DaemonCtx {
fn docker_(&self) -> Arc<dyn DockerLike> { fn docker_(&self) -> Arc<Docker> {
self.docker() self.docker()
} }
fn results_(&self) -> &ResultStore { fn results_(&self) -> &ResultStore {
@ -120,11 +120,11 @@ impl DaemonCtx {
#[derive(Clone)] #[derive(Clone)]
pub struct CliCtx { pub struct CliCtx {
results: ResultStore, results: ResultStore,
docker: Arc<dyn DockerLike>, docker: Arc<Docker>,
} }
impl MinAppContext for CliCtx { impl MinAppContext for CliCtx {
fn docker_(&self) -> Arc<dyn DockerLike> { fn docker_(&self) -> Arc<Docker> {
self.docker.clone() self.docker.clone()
} }
fn results_(&self) -> &ResultStore { fn results_(&self) -> &ResultStore {

View file

@ -3,7 +3,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap; use std::collections::HashMap;
use std::process::Command; use std::process::{Command, Stdio};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Eq, PartialEq)] #[derive(Default, Clone, Eq, PartialEq)]
pub struct Docker; pub struct Docker;
@ -14,24 +16,14 @@ impl Docker {
} }
} }
pub trait DockerLike: std::marker::Send + std::marker::Sync + CloneDockerLike { #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
fn version(&self) -> String; pub struct Log {
fn run_container( pub stdout: String,
&self, pub stderr: String,
name: &str,
img: &str,
detached: bool,
env: &HashMap<String, String>,
network: Option<String>,
pull: bool,
);
fn get_exit_status(&self, name: &str) -> isize;
fn get_logs(&self, name: &str) -> String;
fn rm_container(&self, name: &str, force: bool);
} }
impl DockerLike for Docker { impl Docker {
fn version(&self) -> String { pub fn version(&self) -> String {
let version = Command::new("docker") let version = Command::new("docker")
.arg("--version") .arg("--version")
.output() .output()
@ -41,7 +33,7 @@ impl DockerLike for Docker {
x.get(1).unwrap().trim().to_string() x.get(1).unwrap().trim().to_string()
} }
fn run_container( pub fn run_container(
&self, &self,
name: &str, name: &str,
img: &str, img: &str,
@ -74,12 +66,14 @@ impl DockerLike for Docker {
env_args.push(img.to_string()); env_args.push(img.to_string());
let mut child = Command::new("docker") let mut child = Command::new("docker")
.args(&env_args) .args(&env_args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn() .spawn()
.expect("unable to obtain Docker version"); .expect("unable to obtain Docker version");
child.wait().unwrap(); child.wait().unwrap();
} }
fn get_exit_status(&self, name: &str) -> isize { pub fn get_exit_status(&self, name: &str) -> isize {
let output = Command::new("docker") let output = Command::new("docker")
.args(["inspect", name, "--format={{.State.ExitCode}}"]) .args(["inspect", name, "--format={{.State.ExitCode}}"])
.output() .output()
@ -88,15 +82,40 @@ impl DockerLike for Docker {
let out = out.trim(); let out = out.trim();
out.parse::<isize>().unwrap() out.parse::<isize>().unwrap()
} }
fn get_logs(&self, name: &str) -> String { pub fn get_logs(&self, name: &str) -> Log {
let output = Command::new("docker") let output = Command::new("docker")
.args(["logs", name]) .args(["logs", name])
.output() .output()
.expect("unable to get logs"); .expect("unable to get logs");
String::from_utf8(output.stdout).unwrap() Log {
stderr: String::from_utf8(output.stderr).unwrap(),
stdout: String::from_utf8(output.stdout).unwrap(),
}
} }
fn rm_container(&self, name: &str, force: bool) { pub async fn block_till_container_exists(&self, name: &str, mut timeout: usize) -> bool {
let args = ["container", "inspect", name, "--format={{.State.Status}}"];
loop {
let out = tokio::process::Command::new("docker")
.args(args)
.output()
.await
.unwrap_or_else(|_| panic!("unable to run docker command on container {name}"));
let out = String::from_utf8(out.stdout).unwrap();
let out = out.trim();
if out == "exited" {
return true;
}
if timeout == 0 {
return false;
}
tokio::time::sleep(std::time::Duration::new(1, 0)).await;
timeout -= 1;
}
}
pub fn rm_container(&self, name: &str, force: bool) {
let args = if force { let args = if force {
vec!["rm", "--force", name] vec!["rm", "--force", name]
} else { } else {
@ -109,34 +128,14 @@ impl DockerLike for Docker {
} }
} }
pub trait CloneDockerLike {
/// clone DB
fn clone_docker_like(&self) -> Box<dyn DockerLike>;
}
impl<T> CloneDockerLike for T
where
T: DockerLike + Clone + 'static,
{
fn clone_docker_like(&self) -> Box<dyn DockerLike> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn DockerLike> {
fn clone(&self) -> Self {
(**self).clone_docker_like()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::utils::get_random; use crate::utils::get_random;
use super::*; use super::*;
#[test] #[actix_rt::test]
fn test_docker_util() { async fn test_docker_util() {
let d = Docker::new(); let d = Docker::new();
d.version(); d.version();
let name = format!("test_sleep__{}", get_random(4)); let name = format!("test_sleep__{}", get_random(4));
@ -157,12 +156,18 @@ mod tests {
.unwrap(); .unwrap();
let out = String::from_utf8(out.stdout).unwrap(); let out = String::from_utf8(out.stdout).unwrap();
assert!(out.contains("true")); assert!(out.contains("true"));
std::thread::sleep(std::time::Duration::new(10, 0));
loop {
if d.block_till_container_exists(&name, 13).await {
break;
}
}
let logs = d.get_logs(&name); let logs = d.get_logs(&name);
println!("{logs}"); println!("{:?}", logs);
assert!(logs.contains("running")); assert!(logs.stdout.contains("running"));
assert!(logs.contains("FOO=BAR")); assert!(logs.stdout.contains("FOO=BAR"));
assert!(logs.contains("BAZ=BOO")); assert!(logs.stdout.contains("BAZ=BOO"));
assert_eq!(d.get_exit_status(&name), 0); assert_eq!(d.get_exit_status(&name), 0);
d.rm_container(&name, true); d.rm_container(&name, true);
} }

View file

@ -9,7 +9,7 @@ use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JValue; use serde_json::Value as JValue;
use crate::docker::DockerLike; use crate::docker::{Docker, Log};
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] #[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Container { pub struct Container {
@ -20,11 +20,11 @@ pub struct Container {
pub struct DockerCompose { pub struct DockerCompose {
base_dir: PathBuf, base_dir: PathBuf,
docker: Arc<dyn DockerLike>, docker: Arc<Docker>,
} }
impl DockerCompose { impl DockerCompose {
pub fn new(base_dir: PathBuf, docker: Arc<dyn DockerLike>) -> Self { pub fn new(base_dir: PathBuf, docker: Arc<Docker>) -> Self {
Self { base_dir, docker } Self { base_dir, docker }
} }
pub fn version() -> String { pub fn version() -> String {
@ -72,7 +72,7 @@ impl DockerCompose {
child.wait().unwrap(); child.wait().unwrap();
} }
pub fn logs(&self, service: &str) -> String { pub fn logs(&self, service: &str) -> Log {
self.docker.get_logs(service) self.docker.get_logs(service)
} }
@ -111,7 +111,7 @@ mod tests {
assert_eq!(services.len(), 2); assert_eq!(services.len(), 2);
for service in services.iter() { for service in services.iter() {
let logs = cmp.logs(&service.name); let logs = cmp.logs(&service.name);
assert!(logs.contains(&format!("NAME={}", service.name))); assert!(logs.stdout.contains(&format!("NAME={}", service.name)));
} }
cmp.down(true, true); cmp.down(true, true);
assert!(cmp.services().is_empty()); assert!(cmp.services().is_empty());

View file

@ -108,12 +108,13 @@ async fn main() -> std::io::Result<()> {
} }
crate::runner::suite::SuiteRunnerState::run_proxy(ctx.as_ref()); crate::runner::suite::SuiteRunnerState::run_proxy(ctx.as_ref());
let (suite_results, init_containers) = let (suite_results, init_containers, specimen_logs) =
crate::runner::target::run_target(ctx.as_ref(), path.clone()).await; crate::runner::target::run_target(ctx.as_ref(), path.clone()).await;
let content = crate::runner::results::ArchivableResult { let content = crate::runner::results::ArchivableResult {
commit: "".into(), commit: "".into(),
suites: suite_results, suites: suite_results,
init_containers, init_containers,
specimen_logs,
}; };
let results_file = path.join("results.json"); let results_file = path.join("results.json");
println!("Writing results to: {:?}", path.canonicalize()); println!("Writing results to: {:?}", path.canonicalize());

View file

@ -104,7 +104,10 @@ mod tests {
let init = init.get(0).unwrap(); let init = init.get(0).unwrap();
assert!(init.success); assert!(init.success);
assert_eq!(init.exit_code, 0); assert_eq!(init.exit_code, 0);
assert!(init.container.logs.contains("All Good")); assert!(
init.container.logs.stdout.contains("All Good")
|| init.container.logs.stderr.contains("All Good")
);
compose.down(true, true); compose.down(true, true);
} }
} }

View file

@ -23,12 +23,13 @@ pub async fn run(ctx: AppFullCtx, commit: &str) {
Git::checkout_commit(commit, &control); Git::checkout_commit(commit, &control);
for entry in crate::runner::target::get_targets(&control).iter() { for entry in crate::runner::target::get_targets(&control).iter() {
let (suite_results, init_containers) = let (suite_results, init_containers, target_logs) =
crate::runner::target::run_target(ctx.as_ref(), entry.into()).await; crate::runner::target::run_target(ctx.as_ref(), entry.into()).await;
let content = ArchivableResult { let content = ArchivableResult {
commit: commit.to_string(), commit: commit.to_string(),
suites: suite_results, suites: suite_results,
init_containers, init_containers,
specimen_logs : target_logs,
}; };
let results_repo = base_dir.join("results"); let results_repo = base_dir.join("results");

View file

@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
use crate::complaince::suite::Suite; use crate::complaince::suite::Suite;
use crate::docker::Log;
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct ArchivableInitResult { pub struct ArchivableInitResult {
@ -27,9 +28,10 @@ pub struct ArchivableTestResult {
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct ArchivableContainer { pub struct ArchivableContainer {
pub logs: String, pub logs: Log,
pub name: String, pub name: String,
} }
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct ArchivableTest { pub struct ArchivableTest {
pub name: String, pub name: String,
@ -52,6 +54,7 @@ pub struct ArchivableResult {
pub commit: String, pub commit: String,
pub suites: Vec<ArchivableSuiteResult>, pub suites: Vec<ArchivableSuiteResult>,
pub init_containers: Option<Vec<ArchivableInitResult>>, pub init_containers: Option<Vec<ArchivableInitResult>>,
pub specimen_logs: Vec<ArchivableContainer>,
} }
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]

View file

@ -59,7 +59,7 @@ impl SuiteRunnerState {
"run", "run",
"-d", "-d",
"-p", "-p",
"9080:9000", "9080:80",
"--name", "--name",
FTEST_NGINX_PROXY, FTEST_NGINX_PROXY,
"--network", "--network",
@ -132,9 +132,14 @@ impl SuiteRunnerState {
} }
async fn collect_results(mut self, ctx: &dyn MinAppContext) -> ArchivableSuiteResult { async fn collect_results(mut self, ctx: &dyn MinAppContext) -> ArchivableSuiteResult {
const TIMEOUT: usize = 40;
let mut tests = Vec::with_capacity(self.tests.len()); let mut tests = Vec::with_capacity(self.tests.len());
for (_auth, mut r) in self.tests.drain() { for (_auth, mut r) in self.tests.drain() {
let result = r.rx.recv().await.unwrap(); let result = r.rx.recv().await.unwrap();
ctx.docker_()
.block_till_container_exists(&r.container_name, TIMEOUT)
.await;
let log = ctx.docker_().get_logs(&r.container_name); let log = ctx.docker_().get_logs(&r.container_name);
ctx.docker_().rm_container(&r.container_name, true); ctx.docker_().rm_container(&r.container_name, true);
let container = ArchivableContainer { let container = ArchivableContainer {
@ -307,19 +312,36 @@ mod tests {
archivable_test.result.logs, archivable_test.result.logs,
format!("{}{LOGS}", archivable_test.result.container.name) format!("{}{LOGS}", archivable_test.result.container.name)
); );
println!("{}", archivable_test.result.container.logs); 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 assert!(archivable_test
.result .result
.container .container
.logs .logs
.stdout
.contains("FTEST_AUTH"));
assert!(archivable_test
.result
.container
.logs
.stdout
.contains("FTEST_HOST"));
assert!(archivable_test
.result
.container
.logs
.stdout
.contains("FTEST_TARGET_HOST")); .contains("FTEST_TARGET_HOST"));
assert!(archivable_test.result.container.logs.contains("FTEST_USER"));
assert!(archivable_test assert!(archivable_test
.result .result
.container .container
.logs .logs
.stdout
.contains("FTEST_USER"));
assert!(archivable_test
.result
.container
.logs
.stdout
.contains(&format!("TEST_NAME={}", t.name))); .contains(&format!("TEST_NAME={}", t.name)));
} }
} }

View file

@ -21,10 +21,11 @@ pub async fn run_target(
) -> ( ) -> (
Vec<ArchivableSuiteResult>, Vec<ArchivableSuiteResult>,
Option<Vec<ArchivableInitResult>>, Option<Vec<ArchivableInitResult>>,
Vec<ArchivableContainer>,
) { ) {
let compose = DockerCompose::new(target.canonicalize().unwrap(), ctx.docker_().clone()); let compose = DockerCompose::new(target.canonicalize().unwrap(), ctx.docker_().clone());
compose.up(); compose.up();
let services = compose.services(); let mut services = compose.services();
// Read test suite // Read test suite
let ftest_path = target.join(FTEST_TARGET_FILE); let ftest_path = target.join(FTEST_TARGET_FILE);
@ -43,13 +44,17 @@ pub async fn run_target(
// shut down target instance // shut down target instance
let mut target_logs = Vec::with_capacity(services.len()); let mut target_logs = Vec::with_capacity(services.len());
for s in services.iter() { for s in services.drain(0..) {
target_logs.push(compose.logs(&s.service)); let logs = compose.logs(&s.service);
target_logs.push(ArchivableContainer {
logs,
name: s.service,
});
} }
compose.down(true, true); compose.down(true, true);
(suite_results, init_containers) (suite_results, init_containers, target_logs)
} }
pub fn get_targets(control: &Path) -> Vec<PathBuf> { pub fn get_targets(control: &Path) -> Vec<PathBuf> {

View file

@ -22,27 +22,48 @@
<h4>Logs</h4> <h4>Logs</h4>
<code>{{ test.result.logs | linebreaksbr }}</code> <code>{{ test.result.logs | linebreaksbr }}</code>
<h4>Container Logs: {{ test.result.container.name }}</h4> <h4>Container Logs: {{ test.result.container.name }}</h4>
<code>{{ test.result.container.logs | linebreaksbr }}</code> <h5>STDOUT</h5>
<code>{{ test.result.container.logs.stdout | linebreaksbr }}</code>
<h5>STDERR</h5>
<code>{{ test.result.container.logs.stderr | linebreaksbr }}</code>
</details> </details>
{% endfor %} {% endfor %} {% if payload.results.init_containers %} {% endfor %} {% endfor %} {% if payload.results.init_containers %}
<h2>Initialization Workflow</h2> <h2>Initialization Workflow</h2>
{% for init in payload.results.init_containers %} {% for init in payload.results.init_containers %}
<details>
<details> {% if init.success %}
{% if init.success %} <summary>
<summary> <h3 class="test__name">[OK] {{init.container.name}}</h3>
<h3 class="test__name">[OK] {{init.container.name}}</h3> </summary>
</summary> {% else %}
{% else %} <summary>
<summary> <h3 class="test__name">[FAILED] {{init.container.name}}</h3>
<h3 class="test__name">[FAILED] {{init.container.name}}</h3> </summary>
</summary> {% endif %}
{% endif %} <h4>STDOUT</h4>
<code>{{ init.container.logs | linebreaksbr }}</code> <code>{{ init.container.logs.stdout | linebreaksbr }}</code>
</details> <h4>STDERR</h4>
<code>{{ init.container.logs.stderr | linebreaksbr }}</code>
</details>
{% endfor %} {% endif %} {% endfor %} {% endif %}
<h2>Specimen Logs</h2>
{% for specimen in payload.results.specimen_logs %}
<details>
<summary>
<h3 class="test__name">{{specimen.name}}</h3>
</summary>
<h4>STDOUT</h4>
<code>{{ specimen.logs.stdout | linebreaksbr }}</code>
<h4>STDERR</h4>
<code>{{ specimen.logs.stderr | linebreaksbr }}</code>
</details>
{% endfor %}
</body> </body>
<style> <style>
.test__name { .test__name {