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(
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}