Skip to content

Commit 806e055

Browse files
authored
Merge pull request #79 from trydirect/copilot/analyze-test-coverage
Copilot/analyze test coverage
2 parents b159dfa + 8910642 commit 806e055

10 files changed

Lines changed: 972 additions & 0 deletions

File tree

src/commands/deploy.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,145 @@ pub async fn rollback_latest() -> Result<Option<RollbackEntry>> {
113113
save_manifest(&m).await?;
114114
Ok(Some(entry))
115115
}
116+
117+
#[cfg(test)]
118+
mod tests {
119+
use super::*;
120+
use crate::test_utils::EnvGuard;
121+
use std::sync::{Mutex, OnceLock};
122+
123+
fn env_lock() -> &'static Mutex<()> {
124+
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
125+
LOCK.get_or_init(|| Mutex::new(()))
126+
}
127+
128+
#[test]
129+
fn rollback_entry_serialization() {
130+
let entry = RollbackEntry {
131+
job_id: "job-1".to_string(),
132+
backup_path: "/backups/status.bak".to_string(),
133+
install_path: "/usr/bin/status".to_string(),
134+
timestamp: Utc::now(),
135+
};
136+
let json = serde_json::to_string(&entry).unwrap();
137+
let deserialized: RollbackEntry = serde_json::from_str(&json).unwrap();
138+
assert_eq!(deserialized.job_id, "job-1");
139+
assert_eq!(deserialized.backup_path, "/backups/status.bak");
140+
assert_eq!(deserialized.install_path, "/usr/bin/status");
141+
}
142+
143+
#[test]
144+
fn rollback_manifest_default_is_empty() {
145+
let manifest = RollbackManifest::default();
146+
assert!(manifest.entries.is_empty());
147+
}
148+
149+
#[test]
150+
fn rollback_manifest_serialization_roundtrip() {
151+
let manifest = RollbackManifest {
152+
entries: vec![
153+
RollbackEntry {
154+
job_id: "job-1".to_string(),
155+
backup_path: "/backups/a.bak".to_string(),
156+
install_path: "/usr/bin/status".to_string(),
157+
timestamp: Utc::now(),
158+
},
159+
RollbackEntry {
160+
job_id: "job-2".to_string(),
161+
backup_path: "/backups/b.bak".to_string(),
162+
install_path: "/usr/bin/status".to_string(),
163+
timestamp: Utc::now(),
164+
},
165+
],
166+
};
167+
let json = serde_json::to_string_pretty(&manifest).unwrap();
168+
let deserialized: RollbackManifest = serde_json::from_str(&json).unwrap();
169+
assert_eq!(deserialized.entries.len(), 2);
170+
assert_eq!(deserialized.entries[0].job_id, "job-1");
171+
assert_eq!(deserialized.entries[1].job_id, "job-2");
172+
}
173+
174+
#[tokio::test]
175+
async fn load_manifest_nonexistent_returns_default() {
176+
let _lock = env_lock().lock().expect("env lock poisoned");
177+
let dir = tempfile::tempdir().unwrap();
178+
let _env = EnvGuard::set("UPDATE_STORAGE_PATH", dir.path().to_str().unwrap());
179+
let manifest = load_manifest().await.unwrap();
180+
assert!(manifest.entries.is_empty());
181+
}
182+
183+
#[tokio::test]
184+
async fn save_and_load_manifest_roundtrip() {
185+
let _lock = env_lock().lock().expect("env lock poisoned");
186+
let dir = tempfile::tempdir().unwrap();
187+
let _env = EnvGuard::set("UPDATE_STORAGE_PATH", dir.path().to_str().unwrap());
188+
189+
let manifest = RollbackManifest {
190+
entries: vec![RollbackEntry {
191+
job_id: "test-job".to_string(),
192+
backup_path: "/backup/test.bak".to_string(),
193+
install_path: "/usr/bin/status".to_string(),
194+
timestamp: Utc::now(),
195+
}],
196+
};
197+
save_manifest(&manifest).await.unwrap();
198+
199+
let loaded = load_manifest().await.unwrap();
200+
assert_eq!(loaded.entries.len(), 1);
201+
assert_eq!(loaded.entries[0].job_id, "test-job");
202+
}
203+
204+
#[tokio::test]
205+
async fn record_rollback_appends_entry() {
206+
let _lock = env_lock().lock().expect("env lock poisoned");
207+
let dir = tempfile::tempdir().unwrap();
208+
let _env = EnvGuard::set("UPDATE_STORAGE_PATH", dir.path().to_str().unwrap());
209+
210+
// Save an initial empty manifest
211+
save_manifest(&RollbackManifest::default()).await.unwrap();
212+
213+
record_rollback("job-1", "/backup/1.bak", "/usr/bin/status")
214+
.await
215+
.unwrap();
216+
record_rollback("job-2", "/backup/2.bak", "/usr/bin/status")
217+
.await
218+
.unwrap();
219+
220+
let loaded = load_manifest().await.unwrap();
221+
assert_eq!(loaded.entries.len(), 2);
222+
assert_eq!(loaded.entries[0].job_id, "job-1");
223+
assert_eq!(loaded.entries[1].job_id, "job-2");
224+
}
225+
226+
#[tokio::test]
227+
async fn backup_current_binary_creates_file() {
228+
let _lock = env_lock().lock().expect("env lock poisoned");
229+
let dir = tempfile::tempdir().unwrap();
230+
let _env = EnvGuard::set("UPDATE_STORAGE_PATH", dir.path().to_str().unwrap());
231+
232+
// Create a fake binary to back up
233+
let src = dir.path().join("status");
234+
tokio::fs::write(&src, b"fake binary content")
235+
.await
236+
.unwrap();
237+
238+
let backup_path = backup_current_binary(src.to_str().unwrap(), "test-job")
239+
.await
240+
.unwrap();
241+
assert!(Path::new(&backup_path).exists());
242+
243+
let content = tokio::fs::read(&backup_path).await.unwrap();
244+
assert_eq!(content, b"fake binary content");
245+
}
246+
247+
#[tokio::test]
248+
async fn rollback_latest_with_empty_manifest_returns_none() {
249+
let _lock = env_lock().lock().expect("env lock poisoned");
250+
let dir = tempfile::tempdir().unwrap();
251+
let _env = EnvGuard::set("UPDATE_STORAGE_PATH", dir.path().to_str().unwrap());
252+
253+
save_manifest(&RollbackManifest::default()).await.unwrap();
254+
let result = rollback_latest().await.unwrap();
255+
assert!(result.is_none());
256+
}
257+
}

src/commands/version_check.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,62 @@ pub async fn check_remote_version() -> Result<Option<RemoteVersion>> {
3434
.context("parsing remote version response")?;
3535
Ok(Some(rv))
3636
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
use crate::test_utils::EnvGuard;
42+
use std::sync::{Mutex, OnceLock};
43+
44+
fn env_lock() -> &'static Mutex<()> {
45+
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
46+
LOCK.get_or_init(|| Mutex::new(()))
47+
}
48+
49+
#[test]
50+
fn remote_version_deserialize_with_checksum() {
51+
let json = r#"{"version": "1.2.3", "checksum": "abc123"}"#;
52+
let rv: RemoteVersion = serde_json::from_str(json).unwrap();
53+
assert_eq!(rv.version, "1.2.3");
54+
assert_eq!(rv.checksum, Some("abc123".to_string()));
55+
}
56+
57+
#[test]
58+
fn remote_version_deserialize_without_checksum() {
59+
let json = r#"{"version": "1.2.3"}"#;
60+
let rv: RemoteVersion = serde_json::from_str(json).unwrap();
61+
assert_eq!(rv.version, "1.2.3");
62+
assert_eq!(rv.checksum, None);
63+
}
64+
65+
#[test]
66+
fn remote_version_deserialize_null_checksum() {
67+
let json = r#"{"version": "0.1.0", "checksum": null}"#;
68+
let rv: RemoteVersion = serde_json::from_str(json).unwrap();
69+
assert_eq!(rv.version, "0.1.0");
70+
assert_eq!(rv.checksum, None);
71+
}
72+
73+
#[test]
74+
fn remote_version_deserialize_missing_version_fails() {
75+
let json = r#"{"checksum": "abc"}"#;
76+
let result: std::result::Result<RemoteVersion, _> = serde_json::from_str(json);
77+
assert!(result.is_err());
78+
}
79+
80+
#[tokio::test]
81+
async fn check_remote_version_no_env_returns_none() {
82+
let _lock = env_lock().lock().expect("env lock poisoned");
83+
let _env = EnvGuard::remove("UPDATE_SERVER_URL");
84+
let result = check_remote_version().await.unwrap();
85+
assert!(result.is_none());
86+
}
87+
88+
#[tokio::test]
89+
async fn check_remote_version_empty_env_returns_none() {
90+
let _lock = env_lock().lock().expect("env lock poisoned");
91+
let _env = EnvGuard::set("UPDATE_SERVER_URL", "");
92+
let result = check_remote_version().await.unwrap();
93+
assert!(result.is_none());
94+
}
95+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,8 @@ pub mod security;
77
pub mod transport;
88
pub mod utils;
99

10+
#[cfg(test)]
11+
pub(crate) mod test_utils;
12+
1013
// Crate version exposed for runtime queries
1114
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

src/monitoring/mod.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,143 @@ pub fn spawn_heartbeat(
222222
}
223223
})
224224
}
225+
226+
#[cfg(test)]
227+
mod tests {
228+
use super::*;
229+
230+
#[test]
231+
fn metrics_snapshot_default() {
232+
let snapshot = MetricsSnapshot::default();
233+
assert_eq!(snapshot.timestamp_ms, 0);
234+
assert_eq!(snapshot.cpu_usage_pct, 0.0);
235+
assert_eq!(snapshot.memory_total_bytes, 0);
236+
assert_eq!(snapshot.memory_used_bytes, 0);
237+
assert_eq!(snapshot.memory_used_pct, 0.0);
238+
assert_eq!(snapshot.disk_total_bytes, 0);
239+
assert_eq!(snapshot.disk_used_bytes, 0);
240+
assert_eq!(snapshot.disk_used_pct, 0.0);
241+
}
242+
243+
#[test]
244+
fn metrics_snapshot_serialization() {
245+
let snapshot = MetricsSnapshot {
246+
timestamp_ms: 1700000000000,
247+
cpu_usage_pct: 45.5,
248+
memory_total_bytes: 16_000_000_000,
249+
memory_used_bytes: 8_000_000_000,
250+
memory_used_pct: 50.0,
251+
disk_total_bytes: 500_000_000_000,
252+
disk_used_bytes: 250_000_000_000,
253+
disk_used_pct: 50.0,
254+
};
255+
let json = serde_json::to_string(&snapshot).unwrap();
256+
assert!(json.contains("\"cpu_usage_pct\":45.5"));
257+
assert!(json.contains("\"memory_total_bytes\":16000000000"));
258+
}
259+
260+
#[test]
261+
fn control_plane_display() {
262+
assert_eq!(ControlPlane::StatusPanel.to_string(), "status_panel");
263+
assert_eq!(ControlPlane::ComposeAgent.to_string(), "compose_agent");
264+
}
265+
266+
#[test]
267+
fn control_plane_serialization() {
268+
let json = serde_json::to_string(&ControlPlane::StatusPanel).unwrap();
269+
assert_eq!(json, "\"status_panel\"");
270+
let json = serde_json::to_string(&ControlPlane::ComposeAgent).unwrap();
271+
assert_eq!(json, "\"compose_agent\"");
272+
}
273+
274+
#[test]
275+
fn control_plane_equality() {
276+
assert_eq!(ControlPlane::StatusPanel, ControlPlane::StatusPanel);
277+
assert_ne!(ControlPlane::StatusPanel, ControlPlane::ComposeAgent);
278+
}
279+
280+
#[test]
281+
fn command_execution_metrics_default() {
282+
let metrics = CommandExecutionMetrics::default();
283+
assert_eq!(metrics.status_panel_count, 0);
284+
assert_eq!(metrics.compose_agent_count, 0);
285+
assert_eq!(metrics.total_count, 0);
286+
assert!(metrics.last_control_plane.is_none());
287+
assert_eq!(metrics.last_command_timestamp_ms, 0);
288+
}
289+
290+
#[test]
291+
fn record_status_panel_execution() {
292+
let mut metrics = CommandExecutionMetrics::default();
293+
metrics.record_execution(ControlPlane::StatusPanel);
294+
295+
assert_eq!(metrics.status_panel_count, 1);
296+
assert_eq!(metrics.compose_agent_count, 0);
297+
assert_eq!(metrics.total_count, 1);
298+
assert_eq!(metrics.last_control_plane, Some("status_panel".to_string()));
299+
assert!(metrics.last_command_timestamp_ms > 0);
300+
}
301+
302+
#[test]
303+
fn record_compose_agent_execution() {
304+
let mut metrics = CommandExecutionMetrics::default();
305+
metrics.record_execution(ControlPlane::ComposeAgent);
306+
307+
assert_eq!(metrics.status_panel_count, 0);
308+
assert_eq!(metrics.compose_agent_count, 1);
309+
assert_eq!(metrics.total_count, 1);
310+
assert_eq!(
311+
metrics.last_control_plane,
312+
Some("compose_agent".to_string())
313+
);
314+
}
315+
316+
#[test]
317+
fn record_multiple_executions() {
318+
let mut metrics = CommandExecutionMetrics::default();
319+
metrics.record_execution(ControlPlane::StatusPanel);
320+
metrics.record_execution(ControlPlane::StatusPanel);
321+
metrics.record_execution(ControlPlane::ComposeAgent);
322+
323+
assert_eq!(metrics.status_panel_count, 2);
324+
assert_eq!(metrics.compose_agent_count, 1);
325+
assert_eq!(metrics.total_count, 3);
326+
assert_eq!(
327+
metrics.last_control_plane,
328+
Some("compose_agent".to_string())
329+
);
330+
}
331+
332+
#[tokio::test]
333+
async fn metrics_collector_snapshot_returns_valid_data() {
334+
let collector = MetricsCollector::new();
335+
let snapshot = collector.snapshot().await;
336+
337+
assert!(snapshot.timestamp_ms > 0);
338+
// On any machine, total memory should be > 0
339+
assert!(snapshot.memory_total_bytes > 0);
340+
// Used memory should not exceed total
341+
assert!(snapshot.memory_used_bytes <= snapshot.memory_total_bytes);
342+
// Percentages should be 0-100 range
343+
assert!(snapshot.memory_used_pct >= 0.0 && snapshot.memory_used_pct <= 100.0);
344+
assert!(snapshot.disk_used_pct >= 0.0 && snapshot.disk_used_pct <= 100.0);
345+
}
346+
347+
#[test]
348+
fn command_execution_metrics_serialization() {
349+
let mut metrics = CommandExecutionMetrics::default();
350+
metrics.record_execution(ControlPlane::StatusPanel);
351+
352+
let json = serde_json::to_string(&metrics).unwrap();
353+
assert!(json.contains("\"status_panel_count\":1"));
354+
assert!(json.contains("\"compose_agent_count\":0"));
355+
assert!(json.contains("\"total_count\":1"));
356+
}
357+
358+
#[test]
359+
fn metrics_collector_default() {
360+
// Verify Default trait works
361+
let collector = MetricsCollector::default();
362+
let _ = format!("{:?}", collector);
363+
}
364+
}

0 commit comments

Comments
 (0)