Skip to content

Commit c057edd

Browse files
authored
Allow group to have named depenencies (#50)
With this change, a group will be able to define a list of "needs" that are names of other groups. When executing, groups will run their dependencies first. In the event a dependency fails, the group will not run. When a "path" fails, if other paths don't depend on the failing group, then they will continue to run. This will allow as much work as possible to happen. The order that groups will execute will now be stable, and list (both root and doctor) let the user know the calculated run order. Closes #45 Closes #39 Closes #46
1 parent b478e01 commit c057edd

20 files changed

+709
-170
lines changed

Cargo.lock

+18-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/docs/models/ScopeDoctorGroup.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ metadata:
1717
name: node
1818
spec:
1919
description: Check node is ready.
20+
needs:
21+
- python
22+
- brew
2023
actions:
2124
- description: Node Version
2225
check:
@@ -65,6 +68,7 @@ To target a script relative to the group it must start with `.`, and giving a re
6568
## Schema
6669

6770
- `.spec.action` a series of steps to check and fix for the group.
71+
- `.spec.needs[]` an array of names required before this group can run.
6872
- `.spec.action[].required` default true, when true action failing check & fix will stop all execution.
6973
- `.spec.action[].check.paths` A list of globs to check for a change.
7074
- `.spec.action[].check.commands` A list of commands to execute, using the exit code to determine if changes are needed.

scope-doctor/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tokio = { version = "1", features = ["full"] }
1919
mockall = "0.12.1"
2020
educe = "0.5.11"
2121
derive_builder = "0.13.0"
22+
petgraph = "0.6.4"
2223

2324
scope-lib = { path = "../scope-lib" }
2425

scope-doctor/src/check.rs

+47-44
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl CacheResults {
5050
}
5151
}
5252

53-
#[derive(Debug, PartialEq)]
53+
#[derive(Debug, PartialEq, Clone)]
5454
#[allow(clippy::enum_variant_names)]
5555
pub enum ActionRunResult {
5656
CheckSucceeded,
@@ -76,10 +76,20 @@ impl ActionRunResult {
7676
}
7777
}
7878

79+
#[automock]
80+
#[async_trait::async_trait]
81+
pub trait DoctorActionRun: Send + Sync {
82+
async fn run_action(&self) -> Result<ActionRunResult>;
83+
fn required(&self) -> bool;
84+
fn name(&self) -> String;
85+
fn help_text(&self) -> Option<String>;
86+
fn help_url(&self) -> Option<String>;
87+
}
88+
7989
#[derive(Educe, Builder)]
8090
#[educe(Debug)]
8191
#[builder(setter(into))]
82-
pub struct DoctorActionRun {
92+
pub struct DefaultDoctorActionRun {
8393
pub model: ModelRoot<DoctorGroup>,
8494
pub action: DoctorGroupAction,
8595
pub working_dir: PathBuf,
@@ -91,9 +101,10 @@ pub struct DoctorActionRun {
91101
pub glob_walker: Arc<dyn GlobWalker>,
92102
}
93103

94-
impl DoctorActionRun {
104+
#[async_trait::async_trait]
105+
impl DoctorActionRun for DefaultDoctorActionRun {
95106
#[instrument(skip_all, fields(model.name = self.model.name(), action.name = self.action.name, action.description = self.action.description ))]
96-
pub async fn run_action(&self) -> Result<ActionRunResult> {
107+
async fn run_action(&self) -> Result<ActionRunResult> {
97108
let check_status = self.evaluate_checks().await?;
98109
if check_status == CacheResults::FixNotRequired {
99110
return Ok(ActionRunResult::CheckSucceeded);
@@ -124,6 +135,24 @@ impl DoctorActionRun {
124135
Ok(ActionRunResult::CheckFailedFixSucceedVerifySucceed)
125136
}
126137

138+
fn required(&self) -> bool {
139+
self.action.required
140+
}
141+
142+
fn name(&self) -> String {
143+
self.action.name.to_string()
144+
}
145+
146+
fn help_text(&self) -> Option<String> {
147+
self.action.fix.help_text.clone()
148+
}
149+
150+
fn help_url(&self) -> Option<String> {
151+
self.action.fix.help_url.clone()
152+
}
153+
}
154+
155+
impl DefaultDoctorActionRun {
127156
async fn update_caches(&self) {
128157
if let Some(cache_path) = &self.action.check.files {
129158
let result = self
@@ -268,7 +297,7 @@ impl DoctorActionRun {
268297

269298
#[automock]
270299
#[async_trait]
271-
pub trait GlobWalker {
300+
pub trait GlobWalker: Send + Sync {
272301
async fn have_globs_changed(
273302
&self,
274303
base_dir: &Path,
@@ -337,21 +366,18 @@ impl GlobWalker for DefaultGlobWalker {
337366
}
338367

339368
#[cfg(test)]
340-
mod tests {
341-
use crate::check::{ActionRunResult, DoctorActionRun, MockGlobWalker, RuntimeError};
369+
pub(crate) mod tests {
370+
use crate::check::{
371+
ActionRunResult, DefaultDoctorActionRun, DoctorActionRun, MockGlobWalker, RuntimeError,
372+
};
342373
use crate::file_cache::{FileCache, NoOpCache};
374+
use crate::tests::build_root_model;
343375
use anyhow::{anyhow, Result};
344-
use scope_lib::prelude::{
345-
DoctorGroup, DoctorGroupAction, DoctorGroupActionBuilder, DoctorGroupActionCheckBuilder,
346-
DoctorGroupActionCommand, DoctorGroupActionFixBuilder, DoctorGroupBuilder,
347-
DoctorGroupCachePath, MockExecutionProvider, ModelMetadataBuilder, ModelRoot,
348-
ModelRootBuilder, OutputCaptureBuilder,
349-
};
350-
use std::collections::BTreeMap;
376+
use scope_lib::prelude::*;
351377
use std::path::PathBuf;
352378
use std::sync::Arc;
353379

354-
fn build_run_fail_fix_succeed_action() -> DoctorGroupAction {
380+
pub fn build_run_fail_fix_succeed_action() -> DoctorGroupAction {
355381
DoctorGroupActionBuilder::default()
356382
.description("a test action")
357383
.name("action")
@@ -373,7 +399,7 @@ mod tests {
373399
.unwrap()
374400
}
375401

376-
fn build_file_fix_action() -> DoctorGroupAction {
402+
pub fn build_file_fix_action() -> DoctorGroupAction {
377403
DoctorGroupActionBuilder::default()
378404
.description("a test action")
379405
.name("action")
@@ -395,30 +421,7 @@ mod tests {
395421
.unwrap()
396422
}
397423

398-
fn make_model(actions: Vec<DoctorGroupAction>) -> ModelRoot<DoctorGroup> {
399-
let group = DoctorGroupBuilder::default()
400-
.description("a description")
401-
.actions(actions)
402-
.build()
403-
.unwrap();
404-
405-
ModelRootBuilder::default()
406-
.api_version("fake")
407-
.kind("fake-kind")
408-
.metadata(
409-
ModelMetadataBuilder::default()
410-
.name("fake-model")
411-
.annotations(BTreeMap::default())
412-
.labels(BTreeMap::default())
413-
.build()
414-
.unwrap(),
415-
)
416-
.spec(group)
417-
.build()
418-
.unwrap()
419-
}
420-
421-
fn command_result(
424+
pub fn command_result(
422425
mock: &mut MockExecutionProvider,
423426
command: &'static str,
424427
expected_results: Vec<i32>,
@@ -437,16 +440,16 @@ mod tests {
437440
});
438441
}
439442

440-
fn setup_test(
443+
pub fn setup_test(
441444
actions: Vec<DoctorGroupAction>,
442445
exec_runner: MockExecutionProvider,
443446
glob_walker: MockGlobWalker,
444-
) -> DoctorActionRun {
445-
let model = make_model(actions.clone());
447+
) -> DefaultDoctorActionRun {
448+
let model = build_root_model(actions.clone());
446449
let path = PathBuf::from("/tmp/foo");
447450
let file_cache: Arc<dyn FileCache> = Arc::<NoOpCache>::default();
448451

449-
DoctorActionRun {
452+
DefaultDoctorActionRun {
450453
model,
451454
action: actions[0].clone(),
452455
working_dir: path,

scope-doctor/src/commands/list.rs

+25-6
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
use crate::runner::compute_group_order;
12
use anyhow::Result;
23
use clap::Args;
3-
use colored::*;
4-
use scope_lib::prelude::{FoundConfig, HelpMetadata, ScopeModel};
4+
use scope_lib::prelude::{DoctorGroup, FoundConfig, ModelRoot};
5+
use scope_lib::print_details;
6+
use std::collections::{BTreeSet, VecDeque};
57
use tracing::info;
68

79
#[derive(Debug, Args)]
810
pub struct DoctorListArgs {}
911

1012
pub async fn doctor_list(found_config: &FoundConfig, _args: &DoctorListArgs) -> Result<()> {
1113
info!(target: "user", "Available checks that will run");
12-
info!(target: "user", "{:<20}{:<40}", "Name".white().bold(), "Description".white().bold());
13-
for check in found_config.doctor_group.values() {
14-
info!(target: "user", "{:<20}{:<40}", check.name().white().bold(), check.spec.description());
15-
}
14+
let order = generate_doctor_list(found_config);
15+
print_details(&found_config.working_dir, order);
1616
Ok(())
1717
}
18+
19+
pub fn generate_doctor_list(found_config: &FoundConfig) -> Vec<&ModelRoot<DoctorGroup>> {
20+
let all_keys = BTreeSet::from_iter(found_config.doctor_group.keys().map(|x| x.to_string()));
21+
let all_paths = compute_group_order(&found_config.doctor_group, all_keys);
22+
23+
let mut group_order = VecDeque::new();
24+
for path in all_paths {
25+
for group in path {
26+
if !group_order.contains(&group) {
27+
group_order.push_back(group);
28+
}
29+
}
30+
}
31+
32+
group_order
33+
.iter()
34+
.map(|name| found_config.doctor_group.get(name).unwrap())
35+
.collect()
36+
}

scope-doctor/src/commands/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ mod list;
33
mod run;
44

55
pub use init::{doctor_init, DoctorInitArgs};
6-
pub use list::{doctor_list, DoctorListArgs};
6+
pub use list::{doctor_list, generate_doctor_list, DoctorListArgs};
77
pub use run::{doctor_run, DoctorRunArgs};

0 commit comments

Comments
 (0)