From 20041ddec5f3c2aab6c1b0108fb14c62e47b9748 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Wed, 4 Mar 2026 14:45:50 -0500 Subject: [PATCH] feat: allow using env phones --- src/config.rs | 27 ++++++++++++++-- src/main.rs | 87 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/src/config.rs b/src/config.rs index 69aaff1..90aff0f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -27,14 +27,37 @@ impl Config { } pub fn load() -> Result { + // If all required env vars are set, use them directly (for CI) + if let (Ok(token), Ok(git_url)) = ( + std::env::var("ECTF_TOKEN"), + std::env::var("ECTF_GIT_URL"), + ) { + let api_url = std::env::var("ECTF_API_URL") + .unwrap_or_else(|_| DEFAULT_API_URL.to_string()); + return Ok(Self { token, git_url, api_url }); + } + let path = Self::path()?; let contents = std::fs::read_to_string(&path).with_context(|| { format!( - "Config not found at {}. Run `ectf-tools config` first.", + "Config not found at {}. Run `ectf-tools config` first, or set ECTF_TOKEN and ECTF_GIT_URL env vars.", path.display() ) })?; - serde_yaml::from_str(&contents).context("Failed to parse config file") + let mut cfg: Self = serde_yaml::from_str(&contents).context("Failed to parse config file")?; + + // Allow env vars to override individual fields from the config file + if let Ok(token) = std::env::var("ECTF_TOKEN") { + cfg.token = token; + } + if let Ok(git_url) = std::env::var("ECTF_GIT_URL") { + cfg.git_url = git_url; + } + if let Ok(api_url) = std::env::var("ECTF_API_URL") { + cfg.api_url = api_url; + } + + Ok(cfg) } pub fn save(&self) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 9d6746b..be61e62 100644 --- a/src/main.rs +++ b/src/main.rs @@ -294,6 +294,9 @@ enum ToolsCmd { /// Skip tests requiring a second HSM #[arg(long)] no_transfer: bool, + /// Output results as JSON for CI + #[arg(long)] + json: bool, }, } @@ -768,6 +771,7 @@ fn run_tools(port: &str, cmd: ToolsCmd) -> Result<()> { gid, transfer_port, no_transfer, + json, } => { validate_pin(&pin)?; let gid = parse_gid(&gid)?; @@ -783,7 +787,7 @@ fn run_tools(port: &str, cmd: ToolsCmd) -> Result<()> { _ => None, }; - return run_test(&mut hsm, hsm2.as_mut(), &pin, gid, no_transfer); + return run_test(&mut hsm, hsm2.as_mut(), &pin, gid, no_transfer, json); } } @@ -831,28 +835,47 @@ fn run_test( pin: &str, gid: u16, no_transfer: bool, + json: bool, ) -> Result<()> { struct TestResult { name: &'static str, passed: bool, + duration_secs: f64, + error: Option, } let mut results: Vec = Vec::new(); macro_rules! run_test { ($name:expr, $body:expr) => {{ - log::info(&format!("Running: {}", $name)); + if !json { + log::info(&format!("Running: {}", $name)); + } let start = std::time::Instant::now(); match (|| -> Result<()> { $body })() { Ok(()) => { let elapsed = start.elapsed(); - log::success(&format!("{} passed ({:.1}s)", $name, elapsed.as_secs_f64())); - results.push(TestResult { name: $name, passed: true }); + if !json { + log::success(&format!("{} passed ({:.1}s)", $name, elapsed.as_secs_f64())); + } + results.push(TestResult { + name: $name, + passed: true, + duration_secs: elapsed.as_secs_f64(), + error: None, + }); } Err(e) => { let elapsed = start.elapsed(); - log::error(&format!("{} FAILED ({:.1}s): {e}", $name, elapsed.as_secs_f64())); - results.push(TestResult { name: $name, passed: false }); + if !json { + log::error(&format!("{} FAILED ({:.1}s): {e}", $name, elapsed.as_secs_f64())); + } + results.push(TestResult { + name: $name, + passed: false, + duration_secs: elapsed.as_secs_f64(), + error: Some(e.to_string()), + }); } } }}; @@ -1305,27 +1328,57 @@ As we have seen him in the Capitol, Being cross"; // ── Summary ── - println!(); let total = results.len(); let passed = results.iter().filter(|r| r.passed).count(); let failed = total - passed; - for r in &results { - if r.passed { - log::success(&format!(" PASS {}", r.name)); + if json { + let tests: Vec = results + .iter() + .map(|r| { + let mut obj = serde_json::Map::new(); + obj.insert("name".into(), serde_json::Value::String(r.name.into())); + obj.insert("passed".into(), serde_json::Value::Bool(r.passed)); + obj.insert( + "duration_secs".into(), + serde_json::Value::Number( + serde_json::Number::from_f64(r.duration_secs).unwrap(), + ), + ); + if let Some(e) = &r.error { + obj.insert("error".into(), serde_json::Value::String(e.clone())); + } + serde_json::Value::Object(obj) + }) + .collect(); + let output = serde_json::json!({ + "total": total, + "passed": passed, + "failed": failed, + "tests": tests, + }); + println!("{}", serde_json::to_string(&output).unwrap()); + } else { + println!(); + for r in &results { + if r.passed { + log::success(&format!(" PASS {}", r.name)); + } else { + log::error(&format!(" FAIL {}", r.name)); + } + } + println!(); + if failed == 0 { + log::success(&format!("All {total} tests passed")); } else { - log::error(&format!(" FAIL {}", r.name)); + log::error(&format!("{failed}/{total} tests failed")); } } - println!(); - if failed == 0 { - log::success(&format!("All {total} tests passed")); - Ok(()) - } else { - log::error(&format!("{failed}/{total} tests failed")); + if failed > 0 { bail!("{failed} test(s) failed"); } + Ok(()) } // ─── HW commands ───