Skip to main content

spin_doctor/wasm/
missing.rs

1use std::process::Command;
2
3use anyhow::{Context, Result, ensure};
4use async_trait::async_trait;
5use itertools::Itertools;
6use spin_common::ui::quoted_path;
7
8use crate::{Diagnosis, PatientApp, Treatment};
9
10use super::{PatientWasm, WasmDiagnostic};
11
12/// WasmMissingDiagnostic detects missing Wasm sources.
13#[derive(Default)]
14pub struct WasmMissingDiagnostic;
15
16#[async_trait]
17impl WasmDiagnostic for WasmMissingDiagnostic {
18    type Diagnosis = WasmMissing;
19
20    async fn diagnose_wasm(
21        &self,
22        _app: &PatientApp,
23        wasm: PatientWasm,
24    ) -> anyhow::Result<Vec<Self::Diagnosis>> {
25        if let Some(abs_path) = wasm.abs_source_path()
26            && !abs_path.exists()
27        {
28            return Ok(vec![WasmMissing(wasm)]);
29        }
30        Ok(vec![])
31    }
32}
33
34/// WasmMissing represents a missing Wasm source.
35#[derive(Debug)]
36pub struct WasmMissing(PatientWasm);
37
38impl WasmMissing {
39    fn build_cmd(&self, patient: &PatientApp) -> Result<Command> {
40        let spin_bin = std::env::current_exe().context("Couldn't find spin executable")?;
41        let mut cmd = Command::new(spin_bin);
42        cmd.arg("build")
43            .arg("-f")
44            .arg(&patient.manifest_path)
45            .arg("--component-id")
46            .arg(self.0.component_id());
47        Ok(cmd)
48    }
49}
50
51impl Diagnosis for WasmMissing {
52    fn description(&self) -> String {
53        let id = self.0.component_id();
54        let Some(rel_path) = self.0.source_path() else {
55            unreachable!("unsupported source");
56        };
57        format!(
58            "Component {id:?} source {} is missing",
59            quoted_path(rel_path)
60        )
61    }
62
63    fn treatment(&self) -> Option<&dyn Treatment> {
64        self.0.has_build().then_some(self)
65    }
66}
67
68#[async_trait]
69impl Treatment for WasmMissing {
70    fn summary(&self) -> String {
71        "Run `spin build`".into()
72    }
73
74    async fn dry_run(&self, patient: &PatientApp) -> anyhow::Result<String> {
75        let args = self
76            .build_cmd(patient)?
77            .get_args()
78            .map(|arg| arg.to_string_lossy())
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::{TestPatient, assert_single_diagnosis};
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!(
122            diag.build_cmd(&patient)
123                .unwrap()
124                .get_args()
125                .any(|arg| arg == "missing-source")
126        );
127    }
128}