diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-15 21:27:00 +0100 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2026-02-15 21:27:00 +0100 |
| commit | ce0dbf6b249956700c6a1705bf4ad85a09d53e8c (patch) | |
| tree | d7c3236807cfbf75d7f3a355eb5df5a5e2cc4ad7 /tests/integration/cli_status.rs | |
| parent | 064a1d01c5c14f5ecc032fa9b8346a4a88b893f6 (diff) | |
Switch, cleanup, and status CLI commands. Persistent build state via
state.json. Post-deploy hooks on success and failure with
WITRYNA_BUILD_STATUS. Dependency diet (axum→tiny_http, clap→argh,
tracing→log). Drop built-in rate limiting. Nix flake with NixOS module.
Arch Linux PKGBUILD. Centralized version management.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'tests/integration/cli_status.rs')
| -rw-r--r-- | tests/integration/cli_status.rs | 544 |
1 files changed, 421 insertions, 123 deletions
diff --git a/tests/integration/cli_status.rs b/tests/integration/cli_status.rs index 25135fb..4eb50a4 100644 --- a/tests/integration/cli_status.rs +++ b/tests/integration/cli_status.rs @@ -1,3 +1,4 @@ +use std::fmt::Write as _; use std::process::Stdio; use tempfile::TempDir; use tokio::process::Command; @@ -12,17 +13,16 @@ fn witryna_bin() -> std::path::PathBuf { } /// Write a minimal witryna.toml config for status tests. -async fn write_status_config( - dir: &std::path::Path, - sites: &[&str], - log_dir: &std::path::Path, -) -> std::path::PathBuf { +async fn write_status_config(dir: &std::path::Path, sites: &[&str]) -> std::path::PathBuf { let base_dir = dir.join("data"); + let log_dir = dir.join("logs"); tokio::fs::create_dir_all(&base_dir).await.unwrap(); + tokio::fs::create_dir_all(&log_dir).await.unwrap(); let mut sites_toml = String::new(); for name in sites { - sites_toml.push_str(&format!( + write!( + sites_toml, r#" [[sites]] name = "{name}" @@ -30,7 +30,8 @@ repo_url = "https://example.com/{name}.git" branch = "main" webhook_token = "unused" "# - )); + ) + .unwrap(); } let config_path = dir.join("witryna.toml"); @@ -48,63 +49,43 @@ log_level = "info" config_path } -/// Write a fake build log with a valid header. -async fn write_test_build_log( - log_dir: &std::path::Path, +/// Write a state.json for a site with the new format. +async fn write_state_json( + base_dir: &std::path::Path, site_name: &str, - timestamp: &str, - status: &str, - commit: &str, - image: &str, - duration: &str, + current: &str, + builds: &[serde_json::Value], ) { - let site_log_dir = log_dir.join(site_name); - tokio::fs::create_dir_all(&site_log_dir).await.unwrap(); - - let content = format!( - "=== BUILD LOG ===\n\ - Site: {site_name}\n\ - Timestamp: {timestamp}\n\ - Git Commit: {commit}\n\ - Image: {image}\n\ - Duration: {duration}\n\ - Status: {status}\n\ - \n\ - === STDOUT ===\n\ - build output\n\ - \n\ - === STDERR ===\n" - ); + let state_dir = base_dir.join("builds").join(site_name); + tokio::fs::create_dir_all(&state_dir).await.unwrap(); + + let content = serde_json::json!({ + "current": current, + "builds": builds, + }); - let log_file = site_log_dir.join(format!("{timestamp}.log")); - tokio::fs::write(&log_file, content).await.unwrap(); + let state_path = state_dir.join("state.json"); + tokio::fs::write(&state_path, content.to_string()) + .await + .unwrap(); } -/// Write a fake hook log with a valid header. -async fn write_test_hook_log( - log_dir: &std::path::Path, - site_name: &str, - timestamp: &str, +/// Create a build entry JSON value. +fn build_entry( status: &str, -) { - let site_log_dir = log_dir.join(site_name); - tokio::fs::create_dir_all(&site_log_dir).await.unwrap(); - - let content = format!( - "=== HOOK LOG ===\n\ - Site: {site_name}\n\ - Timestamp: {timestamp}\n\ - Command: hook-cmd\n\ - Duration: 1s\n\ - Status: {status}\n\ - \n\ - === STDOUT ===\n\ - \n\ - === STDERR ===\n" - ); - - let log_file = site_log_dir.join(format!("{timestamp}-hook.log")); - tokio::fs::write(&log_file, content).await.unwrap(); + timestamp: &str, + git_commit: &str, + duration: &str, + log: &str, +) -> serde_json::Value { + serde_json::json!({ + "status": status, + "timestamp": timestamp, + "started_at": "2026-02-10T12:00:00Z", + "git_commit": git_commit, + "duration": duration, + "log": log, + }) } // --------------------------------------------------------------------------- @@ -114,13 +95,10 @@ async fn write_test_hook_log( #[tokio::test] async fn cli_status_no_builds() { let tempdir = TempDir::new().unwrap(); - let log_dir = tempdir.path().join("logs"); - tokio::fs::create_dir_all(&log_dir).await.unwrap(); - - let config_path = write_status_config(tempdir.path(), &["empty-site"], &log_dir).await; + let config_path = write_status_config(tempdir.path(), &["empty-site"]).await; let output = Command::new(witryna_bin()) - .args(["--config", config_path.to_str().unwrap(), "status"]) + .args(["status", "--config", config_path.to_str().unwrap()]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -139,24 +117,25 @@ async fn cli_status_no_builds() { #[tokio::test] async fn cli_status_single_build() { let tempdir = TempDir::new().unwrap(); - let log_dir = tempdir.path().join("logs"); - tokio::fs::create_dir_all(&log_dir).await.unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["my-site"]).await; - write_test_build_log( - &log_dir, + write_state_json( + &base_dir, "my-site", "20260126-143000-123456", - "success", - "abc123d", - "node:20-alpine", - "45s", + &[build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/my-site/20260126-143000-123456.log", + )], ) .await; - let config_path = write_status_config(tempdir.path(), &["my-site"], &log_dir).await; - let output = Command::new(witryna_bin()) - .args(["--config", config_path.to_str().unwrap(), "status"]) + .args(["status", "--config", config_path.to_str().unwrap()]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -174,28 +153,29 @@ async fn cli_status_single_build() { #[tokio::test] async fn cli_status_json_output() { let tempdir = TempDir::new().unwrap(); - let log_dir = tempdir.path().join("logs"); - tokio::fs::create_dir_all(&log_dir).await.unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["json-site"]).await; - write_test_build_log( - &log_dir, + write_state_json( + &base_dir, "json-site", "20260126-143000-123456", - "success", - "abc123d", - "node:20-alpine", - "45s", + &[build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/json-site/20260126-143000-123456.log", + )], ) .await; - let config_path = write_status_config(tempdir.path(), &["json-site"], &log_dir).await; - let output = Command::new(witryna_bin()) .args([ - "--config", - config_path.to_str().unwrap(), "status", "--json", + "--config", + config_path.to_str().unwrap(), ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -217,41 +197,43 @@ async fn cli_status_json_output() { #[tokio::test] async fn cli_status_site_filter() { let tempdir = TempDir::new().unwrap(); - let log_dir = tempdir.path().join("logs"); - tokio::fs::create_dir_all(&log_dir).await.unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["site-a", "site-b"]).await; - // Create logs for two sites - write_test_build_log( - &log_dir, + write_state_json( + &base_dir, "site-a", "20260126-143000-000000", - "success", - "aaa1111", - "alpine:latest", - "10s", + &[build_entry( + "success", + "20260126-143000-000000", + "aaa1111", + "10s", + "/logs/a.log", + )], ) .await; - write_test_build_log( - &log_dir, + write_state_json( + &base_dir, "site-b", "20260126-150000-000000", - "success", - "bbb2222", - "alpine:latest", - "20s", + &[build_entry( + "success", + "20260126-150000-000000", + "bbb2222", + "20s", + "/logs/b.log", + )], ) .await; - let config_path = write_status_config(tempdir.path(), &["site-a", "site-b"], &log_dir).await; - let output = Command::new(witryna_bin()) .args([ - "--config", - config_path.to_str().unwrap(), "status", - "--site", "site-a", + "--config", + config_path.to_str().unwrap(), ]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -271,33 +253,340 @@ async fn cli_status_site_filter() { #[tokio::test] async fn cli_status_hook_failed() { let tempdir = TempDir::new().unwrap(); - let log_dir = tempdir.path().join("logs"); - tokio::fs::create_dir_all(&log_dir).await.unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["hook-site"]).await; - // Build succeeded, but hook failed - write_test_build_log( - &log_dir, + write_state_json( + &base_dir, "hook-site", "20260126-143000-123456", - "success", - "abc123d", - "alpine:latest", - "12s", + &[build_entry( + "hook failed", + "20260126-143000-123456", + "abc123d", + "12s", + "/logs/hook-site/20260126-143000-123456.log", + )], ) .await; - write_test_hook_log( - &log_dir, - "hook-site", + let output = Command::new(witryna_bin()) + .args(["status", "--config", config_path.to_str().unwrap()]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("hook failed"), + "should show 'hook failed', got: {stdout}" + ); +} + +#[tokio::test] +async fn cli_status_building_shows_in_progress() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["building-site"]).await; + + let started_at = witryna::time::format_rfc3339(std::time::SystemTime::now()); + + let state_dir = base_dir.join("builds").join("building-site"); + tokio::fs::create_dir_all(&state_dir).await.unwrap(); + let content = serde_json::json!({ + "current": "", + "builds": [{ + "status": "building", + "timestamp": "20260210-120000-000000", + "started_at": started_at, + "git_commit": "", + "duration": "", + "log": "/logs/building-site/20260210-120000-000000.log", + }], + }); + tokio::fs::write(state_dir.join("state.json"), content.to_string()) + .await + .unwrap(); + + let output = Command::new(witryna_bin()) + .args(["status", "--config", config_path.to_str().unwrap()]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("building"), + "should show 'building' status, got: {stdout}" + ); +} + +#[tokio::test] +async fn cli_status_json_includes_building_state() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["json-building"]).await; + + let started_at = witryna::time::format_rfc3339(std::time::SystemTime::now()); + + let state_dir = base_dir.join("builds").join("json-building"); + tokio::fs::create_dir_all(&state_dir).await.unwrap(); + let content = serde_json::json!({ + "current": "", + "builds": [{ + "status": "building", + "timestamp": "20260210-120000-000000", + "started_at": started_at, + "git_commit": "", + "duration": "", + "log": "/logs/json-building/20260210-120000-000000.log", + }], + }); + tokio::fs::write(state_dir.join("state.json"), content.to_string()) + .await + .unwrap(); + + let output = Command::new(witryna_bin()) + .args([ + "status", + "--json", + "--config", + config_path.to_str().unwrap(), + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let arr = parsed.as_array().unwrap(); + assert_eq!(arr.len(), 1); + assert_eq!(arr[0]["site_name"], "json-building"); + assert_eq!(arr[0]["status"], "building"); +} + +// --------------------------------------------------------------------------- +// current_build / "+" marker tests +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn cli_status_marks_current_build() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["my-site"]).await; + + write_state_json( + &base_dir, + "my-site", + "20260126-143000-123456", + &[build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/my-site/20260126-143000-123456.log", + )], + ) + .await; + + let output = Command::new(witryna_bin()) + .args(["status", "--config", config_path.to_str().unwrap()]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + + let data_line = stdout + .lines() + .find(|l| l.contains("my-site")) + .expect("should have my-site row"); + assert!( + data_line.starts_with('+'), + "current build row should start with '+', got: {data_line}" + ); +} + +#[tokio::test] +async fn cli_status_no_current_no_marker() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["my-site"]).await; + + write_state_json( + &base_dir, + "my-site", + "", // no current + &[build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/my-site/20260126-143000-123456.log", + )], + ) + .await; + + let output = Command::new(witryna_bin()) + .args(["status", "--config", config_path.to_str().unwrap()]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + + for line in stdout.lines().skip(1) { + assert!( + !line.starts_with('+'), + "no row should have '+' without current, got: {line}" + ); + } +} + +#[tokio::test] +async fn cli_status_json_includes_current_build() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["json-site"]).await; + + write_state_json( + &base_dir, + "json-site", + "20260126-143000-123456", + &[build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/json-site/20260126-143000-123456.log", + )], + ) + .await; + + let output = Command::new(witryna_bin()) + .args([ + "status", + "--json", + "--config", + config_path.to_str().unwrap(), + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + let arr = parsed.as_array().unwrap(); + assert_eq!(arr.len(), 1); + assert_eq!( + arr[0]["current_build"], "20260126-143000-123456", + "JSON should include current_build field" + ); +} + +#[tokio::test] +async fn cli_status_single_site_shows_all_builds() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["my-site"]).await; + + write_state_json( + &base_dir, + "my-site", "20260126-143000-123456", - "failed (exit code 1)", + &[ + build_entry( + "failed", + "20260126-150000-000000", + "def4567", + "30s", + "/logs/2.log", + ), + build_entry( + "success", + "20260126-143000-123456", + "abc123d", + "45s", + "/logs/1.log", + ), + ], ) .await; - let config_path = write_status_config(tempdir.path(), &["hook-site"], &log_dir).await; + // Single-site view + let output = Command::new(witryna_bin()) + .args([ + "status", + "my-site", + "--config", + config_path.to_str().unwrap(), + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .unwrap(); + + assert!(output.status.success(), "should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + + let data_lines: Vec<&str> = stdout.lines().skip(1).collect(); + assert_eq!(data_lines.len(), 2, "should have 2 build rows"); + + // First row: failed (no marker — current points to a different timestamp) + assert!( + !data_lines[0].starts_with('+'), + "failed build should not have '+': {}", + data_lines[0] + ); + // Second row: success (has marker — matches current) + assert!( + data_lines[1].starts_with('+'), + "current build should have '+': {}", + data_lines[1] + ); +} + +#[tokio::test] +async fn cli_status_failed_build_shows_failed() { + let tempdir = TempDir::new().unwrap(); + let base_dir = tempdir.path().join("data"); + let config_path = write_status_config(tempdir.path(), &["fail-site"]).await; + + write_state_json( + &base_dir, + "fail-site", + "", + &[build_entry( + "failed", + "20260126-160000-000000", + "def4567", + "2m 0s", + "/logs/fail-site/20260126-160000-000000.log", + )], + ) + .await; let output = Command::new(witryna_bin()) - .args(["--config", config_path.to_str().unwrap(), "status"]) + .args(["status", "--config", config_path.to_str().unwrap()]) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output() @@ -307,7 +596,16 @@ async fn cli_status_hook_failed() { assert!(output.status.success(), "should exit 0"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( - stdout.contains("hook failed"), - "should show 'hook failed', got: {stdout}" + stdout.contains("failed"), + "should show 'failed' (not a long error), got: {stdout}" + ); + // Verify that status column is clean (no long error string breaking the table) + let data_line = stdout + .lines() + .find(|l| l.contains("fail-site")) + .expect("should have fail-site row"); + assert!( + data_line.contains("def4567"), + "should show commit in correct column, got: {data_line}" ); } |
