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