Skip to main content

ryra_vm/
assert.rs

1use anyhow::{Result, bail};
2
3use crate::machine::Machine;
4
5/// Parsed systemd unit status — avoids raw string comparisons.
6#[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(&self, unit: &str, timeout: std::time::Duration) -> Result<()> {
89        let start = std::time::Instant::now();
90        loop {
91            let cmd = format!(
92                "s=$(systemctl --user is-active {unit} 2>/dev/null); \
93                 if [ \"$s\" = active ] || [ \"$s\" = failed ]; then echo $s; \
94                 else echo inactive; fi"
95            );
96            if let Ok(output) = self.exec(&cmd).await {
97                match SystemdStatus::parse(output.stdout_trimmed()) {
98                    SystemdStatus::Active => return Ok(()),
99                    SystemdStatus::Failed => {
100                        let diag_cmd = format!(
101                            "systemctl --user status {unit} 2>&1 | head -15; echo '---'; journalctl --user -u {unit} --no-pager -n 10 2>&1"
102                        );
103                        let diag = self
104                            .exec(&diag_cmd)
105                            .await
106                            .map(|o| o.stdout.trim().to_string())
107                            .unwrap_or_default();
108                        bail!("service {unit} failed to start:\n{diag}");
109                    }
110                    SystemdStatus::Inactive => {}
111                }
112            }
113
114            if start.elapsed() > timeout {
115                bail!(
116                    "timed out waiting for {unit} to become active after {}s",
117                    timeout.as_secs()
118                );
119            }
120
121            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
122        }
123    }
124}