Skip to content

Commit e2b4098

Browse files
authored
Merge pull request #1927 from lann/v2-doctor
doctor: Unbreak `spin doctor` for Manifest V2
2 parents facd112 + 4107a4d commit e2b4098

25 files changed

+284
-160
lines changed

Cargo.lock

Lines changed: 6 additions & 37 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/doctor/Cargo.toml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ spin-common = { path = "../common" }
1313
spin-manifest = { path = "../manifest" }
1414
tempfile = "3.3.0"
1515
terminal = { path = "../terminal" }
16-
tokio = "1"
17-
toml = "0.7"
18-
toml_edit = "0.19"
16+
tokio = { version = "1", features = ["process"] }
17+
toml = "0.8.2"
18+
toml_edit = "0.20.2"
1919
tracing = { workspace = true }
2020

2121
[dev-dependencies]
22+
glob = "0.3.1"
2223
tempfile = "3"
24+
tokio = { version = "1", features = ["macros", "rt"] }
25+
ui-testing = { path = "../ui-testing" }
26+
27+
[[test]]
28+
name = "ui"
29+
path = "tests/ui.rs"
30+
harness = false

crates/doctor/src/lib.rs

Lines changed: 63 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
//! Spin doctor: check and automatically fix problems with Spin apps.
22
#![deny(missing_docs)]
33

4-
use std::{fmt::Debug, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc};
4+
use std::{collections::VecDeque, fmt::Debug, fs, path::PathBuf};
55

66
use anyhow::{ensure, Context, Result};
77
use async_trait::async_trait;
8-
use tokio::sync::Mutex;
98
use toml_edit::Document;
109

1110
/// Diagnoses for app manifest format problems.
@@ -19,87 +18,98 @@ pub mod wasm;
1918

2019
/// Configuration for an app to be checked for problems.
2120
pub struct Checkup {
22-
manifest_path: PathBuf,
23-
diagnostics: Vec<Box<dyn BoxingDiagnostic>>,
21+
patient: PatientApp,
22+
diagnostics: VecDeque<Box<dyn BoxingDiagnostic>>,
23+
unprocessed_diagnoses: VecDeque<Box<dyn Diagnosis>>,
2424
}
2525

2626
impl Checkup {
2727
/// Return a new checkup for the app manifest at the given path.
28-
pub fn new(manifest_path: impl Into<PathBuf>) -> Self {
28+
pub fn new(manifest_path: impl Into<PathBuf>) -> Result<Self> {
29+
let patient = PatientApp::new(manifest_path)?;
2930
let mut checkup = Self {
30-
manifest_path: manifest_path.into(),
31-
diagnostics: vec![],
31+
patient,
32+
diagnostics: Default::default(),
33+
unprocessed_diagnoses: Default::default(),
3234
};
33-
checkup.add_diagnostic::<manifest::version::VersionDiagnostic>();
34-
checkup.add_diagnostic::<manifest::trigger::TriggerDiagnostic>();
35-
checkup.add_diagnostic::<rustlang::target::TargetDiagnostic>(); // Do toolchain checks _before_ build checks
36-
checkup.add_diagnostic::<wasm::missing::WasmMissingDiagnostic>();
3735
checkup
36+
.add_diagnostic::<manifest::version::VersionDiagnostic>()
37+
.add_diagnostic::<manifest::trigger::TriggerDiagnostic>()
38+
.add_diagnostic::<rustlang::target::TargetDiagnostic>() // Do toolchain checks _before_ build check
39+
.add_diagnostic::<wasm::missing::WasmMissingDiagnostic>();
40+
Ok(checkup)
41+
}
42+
43+
/// Returns the [`PatientApp`] being checked.
44+
pub fn patient(&self) -> &PatientApp {
45+
&self.patient
3846
}
3947

4048
/// Add a detectable problem to this checkup.
4149
pub fn add_diagnostic<D: Diagnostic + Default + 'static>(&mut self) -> &mut Self {
42-
self.diagnostics.push(Box::<D>::default());
50+
self.diagnostics.push_back(Box::<D>::default());
4351
self
4452
}
4553

46-
fn patient(&self) -> Result<PatientApp> {
47-
let path = &self.manifest_path;
54+
/// Returns the next detected problem.
55+
pub async fn next_diagnosis(&mut self) -> Result<Option<PatientDiagnosis>> {
56+
while self.unprocessed_diagnoses.is_empty() {
57+
let Some(diagnostic) = self.diagnostics.pop_front() else {
58+
return Ok(None);
59+
};
60+
self.unprocessed_diagnoses = diagnostic
61+
.diagnose_boxed(&self.patient)
62+
.await
63+
.unwrap_or_else(|err| {
64+
tracing::debug!("Diagnose failed: {err:?}");
65+
vec![]
66+
})
67+
.into()
68+
}
69+
Ok(Some(PatientDiagnosis {
70+
patient: &mut self.patient,
71+
diagnosis: self.unprocessed_diagnoses.pop_front().unwrap(),
72+
}))
73+
}
74+
}
75+
76+
/// An app "patient" to be checked for problems.
77+
#[derive(Clone)]
78+
pub struct PatientApp {
79+
/// Path to an app manifest file.
80+
pub manifest_path: PathBuf,
81+
/// Parsed app manifest TOML document.
82+
pub manifest_doc: Document,
83+
}
84+
85+
impl PatientApp {
86+
fn new(manifest_path: impl Into<PathBuf>) -> Result<Self> {
87+
let path = manifest_path.into();
4888
ensure!(
4989
path.is_file(),
5090
"No Spin app manifest file found at {path:?}"
5191
);
5292

53-
let contents = fs::read_to_string(path)
93+
let contents = fs::read_to_string(&path)
5494
.with_context(|| format!("Couldn't read Spin app manifest file at {path:?}"))?;
5595

5696
let manifest_doc: Document = contents
5797
.parse()
5898
.with_context(|| format!("Couldn't parse manifest file at {path:?} as valid TOML"))?;
5999

60-
Ok(PatientApp {
61-
manifest_path: path.into(),
100+
Ok(Self {
101+
manifest_path: path,
62102
manifest_doc,
63103
})
64104
}
65-
66-
/// Find problems with the configured app, calling the given closure with
67-
/// each problem found.
68-
pub async fn for_each_diagnosis<F>(&self, mut f: F) -> Result<usize>
69-
where
70-
F: for<'a> FnMut(
71-
Box<dyn Diagnosis + 'static>,
72-
&'a mut PatientApp,
73-
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>,
74-
{
75-
let patient = Arc::new(Mutex::new(self.patient()?));
76-
let mut count = 0;
77-
for diagnostic in &self.diagnostics {
78-
let patient = patient.clone();
79-
let diags = diagnostic
80-
.diagnose_boxed(&*patient.lock().await)
81-
.await
82-
.unwrap_or_else(|err| {
83-
tracing::debug!("Diagnose failed: {err:?}");
84-
vec![]
85-
});
86-
count += diags.len();
87-
for diag in diags {
88-
let mut patient = patient.lock().await;
89-
f(diag, &mut patient).await?;
90-
}
91-
}
92-
Ok(count)
93-
}
94105
}
95106

96-
/// An app "patient" to be checked for problems.
97-
#[derive(Clone)]
98-
pub struct PatientApp {
99-
/// Path to an app manifest file.
100-
pub manifest_path: PathBuf,
101-
/// Parsed app manifest TOML document.
102-
pub manifest_doc: Document,
107+
/// A PatientDiagnosis bundles a [`Diagnosis`] with its (borrowed) [`PatientApp`].
108+
pub struct PatientDiagnosis<'a> {
109+
/// The diagnosis
110+
pub diagnosis: Box<dyn Diagnosis>,
111+
/// A reference to the patient this diagnosis applies to
112+
pub patient: &'a mut PatientApp,
103113
}
104114

105115
/// The Diagnose trait implements the detection of a particular Spin app problem.

crates/doctor/src/manifest/trigger.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ impl Diagnostic for TriggerDiagnostic {
1818
async fn diagnose(&self, patient: &PatientApp) -> Result<Vec<Self::Diagnosis>> {
1919
let manifest: toml::Value = toml_edit::de::from_document(patient.manifest_doc.clone())?;
2020

21+
if manifest.get("spin_manifest_version") == Some(&Value::Integer(2)) {
22+
// Not applicable to manifest V2
23+
return Ok(vec![]);
24+
}
25+
2126
let mut diags = vec![];
2227

2328
// Top-level trigger config

crates/doctor/src/manifest/version.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,28 @@ impl Diagnostic for VersionDiagnostic {
2424
let test: VersionProbe =
2525
from_document(doc.clone()).context("failed to decode VersionProbe")?;
2626

27-
if let Some(value) = test.spin_manifest_version.or(test.spin_version.clone()) {
28-
if value.as_str() != Some("1") {
27+
if let Some(value) = test.spin_manifest_version {
28+
if corrected_version(&value).is_some() {
2929
return Ok(vec![VersionDiagnosis::WrongValue(value)]);
30-
} else if test.spin_version.is_some() {
31-
return Ok(vec![VersionDiagnosis::OldVersionKey]);
3230
}
31+
} else if test.spin_version.is_some() {
32+
return Ok(vec![VersionDiagnosis::OldVersionKey]);
3333
} else {
3434
return Ok(vec![VersionDiagnosis::MissingVersion]);
3535
}
3636
Ok(vec![])
3737
}
3838
}
3939

40+
fn corrected_version(value: &Value) -> Option<toml_edit::Value> {
41+
match value {
42+
Value::String(s) if s == "1" => None,
43+
Value::Integer(2) => None,
44+
Value::Integer(1) => Some("1".into()),
45+
_ => Some(2.into()),
46+
}
47+
}
48+
4049
#[derive(Debug, Deserialize)]
4150
struct VersionProbe {
4251
spin_manifest_version: Option<Value>,
@@ -60,7 +69,7 @@ impl Diagnosis for VersionDiagnosis {
6069
Self::MissingVersion => "Manifest missing 'spin_manifest_version' key".into(),
6170
Self::OldVersionKey => "Manifest using old 'spin_version' key".into(),
6271
Self::WrongValue(val) => {
63-
format!(r#"Manifest 'spin_manifest_version' must be "1", not {val}"#)
72+
format!(r#"Manifest 'spin_manifest_version' must be "1" or 2, not {val}"#)
6473
}
6574
}
6675
}
@@ -78,16 +87,23 @@ impl Diagnosis for VersionDiagnosis {
7887
impl ManifestTreatment for VersionDiagnosis {
7988
fn summary(&self) -> String {
8089
match self {
81-
Self::MissingVersion => "Add spin_manifest_version to manifest",
82-
Self::OldVersionKey => "Replace 'spin_version' with 'spin_manifest_version'",
83-
Self::WrongValue(_) => r#"Set manifest version to "1""#,
90+
Self::MissingVersion => "Add spin_manifest_version to manifest".into(),
91+
Self::OldVersionKey => "Replace 'spin_version' with 'spin_manifest_version'".into(),
92+
Self::WrongValue(value) => format!(
93+
"Set manifest version to {}",
94+
corrected_version(value).unwrap()
95+
),
8496
}
85-
.into()
8697
}
8798

8899
async fn treat_manifest(&self, doc: &mut Document) -> anyhow::Result<()> {
89100
doc.remove(SPIN_VERSION);
90-
let item = Item::Value("1".into());
101+
102+
let item = Item::Value(match self {
103+
Self::MissingVersion => 2.into(),
104+
Self::OldVersionKey => "1".into(),
105+
Self::WrongValue(value) => corrected_version(value).unwrap(),
106+
});
91107
if let Some(existing) = doc.get_mut(SPIN_MANIFEST_VERSION) {
92108
*existing = item;
93109
} else {
@@ -115,12 +131,6 @@ mod tests {
115131
run_correct_test::<VersionDiagnostic>("manifest_version").await;
116132
}
117133

118-
#[tokio::test]
119-
async fn test_missing() {
120-
let diag = run_broken_test::<VersionDiagnostic>("manifest_version", "missing_key").await;
121-
assert!(matches!(diag, VersionDiagnosis::MissingVersion));
122-
}
123-
124134
#[tokio::test]
125135
async fn test_old_key() {
126136
let diag = run_broken_test::<VersionDiagnostic>("manifest_version", "old_key").await;

crates/doctor/src/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ pub struct TestPatient {
6868

6969
impl TestPatient {
7070
fn new(manifest_temp: TempPath) -> Result<Self> {
71-
let inner = Checkup::new(&manifest_temp).patient()?;
71+
let inner = PatientApp::new(&manifest_temp)?;
7272
Ok(Self {
7373
inner,
7474
_manifest_temp: manifest_temp,

crates/doctor/tests/data/manifest_version_missing_key.toml

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
spin_manifest_version = 2
1+
spin_manifest_version = 1
22

33
# comment preserved
44
name = "app-name"

0 commit comments

Comments
 (0)