spin_doctor/wasm/
missing.rs

1use std::process::Command;
2
3use anyhow::{ensure, Context, Result};
4use async_trait::async_trait;
5use spin_common::ui::quoted_path;
6
7use crate::{Diagnosis, PatientApp, Treatment};
8
9use super::{PatientWasm, WasmDiagnostic};
10
11/// WasmMissingDiagnostic detects missing Wasm sources.
12#[derive(Default)]
13pub struct WasmMissingDiagnostic;
14
15#[async_trait]
16impl WasmDiagnostic for WasmMissingDiagnostic {
17    type Diagnosis = WasmMissing;
18
19    async fn diagnose_wasm(
20        &self,
21        _app: &PatientApp,
22        wasm: PatientWasm,
23    ) -> anyhow::Result<Vec<Self::Diagnosis>> {
24        if let Some(abs_path) = wasm.abs_source_path() {
25            if !abs_path.exists() {
26                return Ok(vec![WasmMissing(wasm)]);
27            }
28        }
29        Ok(vec![])
30    }
31}
32
33/// WasmMissing represents a missing Wasm source.
34#[derive(Debug)]
35pub struct WasmMissing(PatientWasm);
36
37impl WasmMissing {
38    fn build_cmd(&self, patient: &PatientApp) -> Result<Command> {
39        let spin_bin = std::env::current_exe().context("Couldn't find spin executable")?;
40        let mut cmd = Command::new(spin_bin);
41        cmd.arg("build")
42            .arg("-f")
43            .arg(&patient.manifest_path)
44            .arg("--component-id")
45            .arg(self.0.component_id());
46        Ok(cmd)
47    }
48}
49
50impl Diagnosis for WasmMissing {
51    fn description(&self) -> String {
52        let id = self.0.component_id();
53        let Some(rel_path) = self.0.source_path() else {
54            unreachable!("unsupported source");
55        };
56        format!(
57            "Component {id:?} source {} is missing",
58            quoted_path(rel_path)
59        )
60    }
61
62    fn treatment(&self) -> Option<&dyn Treatment> {
63        self.0.has_build().then_some(self)
64    }
65}
66
67#[async_trait]
68impl Treatment for WasmMissing {
69    fn summary(&self) -> String {
70        "Run `spin build`".into()
71    }
72
73    async fn dry_run(&self, patient: &PatientApp) -> anyhow::Result<String> {
74        let args = self
75            .build_cmd(patient)?
76            .get_args()
77            .map(|arg| arg.to_string_lossy())
78            .collect::<Vec<_>>()
79            .join(" ");
80        Ok(format!("Run `spin {args}`"))
81    }
82
83    async fn treat(&self, patient: &mut PatientApp) -> anyhow::Result<()> {
84        let mut cmd = self.build_cmd(patient)?;
85        let status = cmd.status()?;
86        ensure!(status.success(), "Build command {cmd:?} failed: {status:?}");
87        Ok(())
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use crate::test::{assert_single_diagnosis, TestPatient};
94
95    use super::*;
96
97    const MINIMUM_VIABLE_MANIFEST: &str = r#"
98            spin_manifest_version = "1"
99            name = "wasm-missing-test"
100            version = "0.0.0"
101            trigger = { type = "test" }
102            [[component]]
103            id = "missing-source"
104            source = "does-not-exist.wasm"
105            trigger = {}
106        "#;
107
108    #[tokio::test]
109    async fn test_without_build() {
110        let patient = TestPatient::from_toml_str(MINIMUM_VIABLE_MANIFEST);
111        let diag = assert_single_diagnosis::<WasmMissingDiagnostic>(&patient).await;
112        assert!(diag.treatment().is_none());
113    }
114
115    #[tokio::test]
116    async fn test_with_build() {
117        let manifest = format!("{MINIMUM_VIABLE_MANIFEST}\nbuild.command = 'true'");
118        let patient = TestPatient::from_toml_str(manifest);
119        let diag = assert_single_diagnosis::<WasmMissingDiagnostic>(&patient).await;
120        assert!(diag.treatment().is_some());
121        assert!(diag
122            .build_cmd(&patient)
123            .unwrap()
124            .get_args()
125            .any(|arg| arg == "missing-source"));
126    }
127}