spin_doctor/wasm/
missing.rs1use 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#[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#[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}