1use anyhow::{Result, bail};
2
3use crate::machine::Machine;
4
5#[derive(Debug, PartialEq, Eq)]
7pub enum SystemdStatus {
8 Active,
9 Failed,
10 Inactive,
11}
12
13impl SystemdStatus {
14 pub fn parse(s: &str) -> Self {
15 match s {
16 "active" => Self::Active,
17 "failed" => Self::Failed,
18 _ => Self::Inactive,
19 }
20 }
21}
22
23#[allow(dead_code)]
24impl Machine {
25 pub async fn assert_service_active(&self, unit: &str) -> Result<()> {
26 let cmd = format!("systemctl --user is-active {unit}");
27 let output = self.exec(&cmd).await?;
28 let status = SystemdStatus::parse(output.stdout_trimmed());
29 if status != SystemdStatus::Active {
30 bail!(
31 "expected service {unit} to be active, got: {}",
32 output.stdout_trimmed()
33 );
34 }
35 Ok(())
36 }
37
38 pub async fn assert_service_inactive(&self, unit: &str) -> Result<()> {
39 let cmd = format!("systemctl --user is-active {unit} 2>/dev/null || echo inactive");
40 let output = self.exec(&cmd).await?;
41 let status = SystemdStatus::parse(output.stdout_trimmed());
42 if status == SystemdStatus::Active {
43 bail!("expected service {unit} to be inactive, but it is active");
44 }
45 Ok(())
46 }
47
48 pub async fn assert_curl(&self, url: &str, expected_status: u16) -> Result<()> {
49 let cmd = format!("curl -s -o /dev/null -w '%{{http_code}}' {url}");
50 let output = self.exec(&cmd).await?;
51 let code: u16 = output.stdout_trimmed().parse().map_err(|e| {
52 anyhow::anyhow!(
53 "failed to parse HTTP status from curl output '{}': {e}",
54 output.stdout_trimmed()
55 )
56 })?;
57 if code != expected_status {
58 bail!("expected HTTP {expected_status} from {url}, got {code}");
59 }
60 Ok(())
61 }
62
63 pub async fn assert_journal_clean(&self, unit: &str) -> Result<()> {
64 let cmd = format!("journalctl _SYSTEMD_USER_UNIT={unit} -p err -q --no-pager");
65 let output = self.exec(&cmd).await?;
66 let errors = output.stdout_trimmed();
67 if !errors.is_empty() {
68 bail!("found error-level journal entries for {unit}:\n{errors}");
69 }
70 Ok(())
71 }
72
73 pub async fn assert_file_exists(&self, path: &str) -> Result<()> {
74 self.exec(&format!("test -e {path}")).await?;
75 Ok(())
76 }
77
78 pub async fn assert_file_not_exists(&self, path: &str) -> Result<()> {
79 let result = self
80 .exec(&format!("test -e {path} && echo exists || echo missing"))
81 .await?;
82 if result.stdout_trimmed().contains("exists") {
83 bail!("expected {path} to not exist, but it does");
84 }
85 Ok(())
86 }
87
88 pub async fn wait_for_service(
89 &self,
90 unit: &str,
91 timeout: std::time::Duration,
92 prefix: &str,
93 ) -> Result<()> {
94 let mut progress = crate::progress::WaitProgress::new(unit, "systemctl is-active", timeout)
95 .with_prefix(prefix);
96 loop {
97 let cmd = format!(
98 "s=$(systemctl --user is-active {unit} 2>/dev/null); \
99 if [ \"$s\" = active ] || [ \"$s\" = failed ]; then echo $s; \
100 else echo inactive; fi"
101 );
102 if let Ok(output) = self.exec(&cmd).await {
103 match SystemdStatus::parse(output.stdout_trimmed()) {
104 SystemdStatus::Active => return Ok(()),
105 SystemdStatus::Failed => {
106 let diag_cmd = format!(
107 "systemctl --user status {unit} 2>&1 | head -15; echo '---'; journalctl --user -u {unit} --no-pager -n 10 2>&1"
108 );
109 let diag = self
110 .exec(&diag_cmd)
111 .await
112 .map(|o| o.stdout.trim().to_string())
113 .unwrap_or_default();
114 bail!("service {unit} failed to start:\n{diag}");
115 }
116 SystemdStatus::Inactive => {}
117 }
118 }
119
120 if progress.timed_out() {
121 bail!(
122 "timed out waiting for {unit} to become active after {}s",
123 timeout.as_secs()
124 );
125 }
126
127 progress.tick();
128 tokio::time::sleep(std::time::Duration::from_secs(1)).await;
129 }
130 }
131}