Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/analyze/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ struct AnalyzeCommandArgs {
command: Vec<String>,
}

// `scope analyze` is interactive by design — users review and approve each fix,
// so auto-approving (yolo) isn't implemented here.
pub async fn analyze_root(found_config: &FoundConfig, args: &AnalyzeArgs) -> Result<i32> {
match &args.command {
AnalyzeCommands::Logs(args) => analyze_logs(found_config, args).await,
Expand All @@ -56,6 +58,7 @@ async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Res
&found_config.known_error,
&found_config.working_dir,
read_from_stdin().await?,
false,
)
.await?
}
Expand All @@ -64,6 +67,7 @@ async fn analyze_logs(found_config: &FoundConfig, args: &AnalyzeLogsArgs) -> Res
&found_config.known_error,
&found_config.working_dir,
read_from_file(file_path).await?,
false,
)
.await?
}
Expand Down Expand Up @@ -91,6 +95,7 @@ async fn analyze_command(found_config: &FoundConfig, args: &AnalyzeCommandArgs)
&found_config.known_error,
&found_config.working_dir,
read_from_command(&exec_runner, capture_opts).await?,
false,
)
.await?;

Expand Down
66 changes: 52 additions & 14 deletions src/bin/scope-intercept.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use clap::Parser;
use dev_scope::prelude::*;
use dev_scope::shared::analyze;
use dev_scope::shared::analyze::AnalyzeStatus;
use human_panic::setup_panic;
use std::env;
use std::io::Cursor;
use std::sync::Arc;
use tracing::{Level, debug, enabled, error, info, warn};
use tokio::io::BufReader;
use tracing::{Level, enabled, error, info, warn};

/// A wrapper CLI that can be used to capture output from a program, check if there are known errors
/// and let the user know.
Expand All @@ -25,6 +29,10 @@ struct Cli {
#[clap(flatten)]
config_options: ConfigOptions,

/// Automatically approve all fix prompts without asking
#[arg(long, short = 'y', default_value = "false")]
yolo: bool,

/// Command to execute withing scope-intercept.
#[arg(required = true)]
utility: String,
Expand Down Expand Up @@ -62,6 +70,7 @@ async fn main() -> anyhow::Result<()> {
}

async fn run_command(opts: Cli) -> anyhow::Result<i32> {
let yolo = opts.yolo;
let mut command = vec![opts.utility];
command.extend(opts.args);
let current_dir = std::env::current_dir()?;
Expand Down Expand Up @@ -90,20 +99,49 @@ async fn run_command(opts: Cli) -> anyhow::Result<i32> {
FoundConfig::empty(env::current_dir().unwrap())
});

let command_output = capture.generate_output();
let analyze_status = analyze::process_lines(
&found_config.known_error,
&found_config.working_dir,
BufReader::new(Cursor::new(capture.generate_user_output())),
yolo,
)
.await?;

for known_error in found_config.known_error.values() {
debug!("Checking known error {}", known_error.name());
if known_error.regex.is_match(&command_output) {
info!(target: "always", "Known error '{}' found", known_error.name());
info!(target: "always", "\t==> {}", known_error.help_text);
}
}
analyze::report_result(&analyze_status);

let (capture, exit_code) =
if matches!(analyze_status, AnalyzeStatus::KnownErrorFoundFixSucceeded) {
info!(target: "always", "Fix succeeded, retrying command");
let retry_capture = OutputCapture::capture_output(CaptureOpts {
working_dir: &current_dir,
args: &command,
output_dest: OutputDisplay::Visible,
path: &path,
env_vars: Default::default(),
})
.await?;

let retry_exit_code = retry_capture.exit_code.unwrap_or(-1);
if accepted_exit_codes.contains(&retry_exit_code) {
return Ok(retry_exit_code);
}

if found_config.report_upload.is_empty() {
return Ok(exit_code);
(retry_capture, retry_exit_code)
} else {
(capture, exit_code)
};

if !found_config.report_upload.is_empty() {
offer_bug_report(&found_config, &command, &capture).await?;
}
Ok(exit_code)
}

async fn offer_bug_report(
found_config: &FoundConfig,
command: &[String],
capture: &OutputCapture,
) -> anyhow::Result<()> {
let ans = inquire::Confirm::new("Do you want to upload a bug report?")
.with_default(false)
.with_help_message(
Expand All @@ -115,13 +153,13 @@ async fn run_command(opts: Cli) -> anyhow::Result<i32> {
let entrypoint = command.join(" ");
let exec_runner = Arc::new(DefaultExecutionProvider::default());

let builder = DefaultUnstructuredReportBuilder::new(&entrypoint, &capture);
let builder = DefaultUnstructuredReportBuilder::new(&entrypoint, capture);

for location in found_config.report_upload.values() {
let mut builder = builder.clone();
builder
.run_and_append_additional_data(
&found_config,
found_config,
exec_runner.clone(),
&location.additional_data,
)
Expand All @@ -140,5 +178,5 @@ async fn run_command(opts: Cli) -> anyhow::Result<i32> {
}
}
}
Ok(exit_code)
Ok(())
}
132 changes: 131 additions & 1 deletion src/doctor/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ pub struct DefaultDoctorActionRun {
pub working_dir: PathBuf,
pub file_cache: Arc<dyn FileCache>,
pub run_fix: bool,
pub yolo: bool,
#[educe(Debug(ignore))]
pub exec_runner: Arc<dyn ExecutionProvider>,
#[educe(Debug(ignore))]
Expand Down Expand Up @@ -672,7 +673,7 @@ impl DefaultDoctorActionRun {
.collect::<Vec<String>>();

let buffer = BufReader::new(StringVecReader::new(&lines));
analyze::process_lines(&self.known_errors, &self.working_dir, buffer).await
analyze::process_lines(&self.known_errors, &self.working_dir, buffer, self.yolo).await
}
}

Expand Down Expand Up @@ -920,6 +921,7 @@ pub(crate) mod tests {
working_dir: path,
file_cache,
run_fix: true,
yolo: false,
exec_runner: Arc::new(exec_runner),
glob_walker: Arc::new(glob_walker),
known_errors: BTreeMap::new(),
Expand Down Expand Up @@ -1566,4 +1568,132 @@ pub(crate) mod tests {
assert_eq!(home_dir.join("foo.txt").display().to_string(), actual);
}
}

mod analyze_known_errors_spec {
use super::*;
use crate::models::prelude::{ModelMetadata, ModelMetadataAnnotations};
use crate::shared::analyze::AnalyzeStatus;
use regex::Regex;

fn make_known_error(pattern: &str, with_fix: bool) -> KnownError {
let fix = if with_fix {
Some(DoctorFix {
command: Some(DoctorCommands::from(vec!["true"])),
help_text: None,
help_url: None,
prompt: None,
})
} else {
None
};

KnownError {
full_name: "ScopeKnownError/test-error".to_string(),
metadata: ModelMetadata {
name: "test-error".to_string(),
description: "a test known error".to_string(),
annotations: ModelMetadataAnnotations {
file_path: None,
file_dir: None,
working_dir: Some("/tmp".to_string()),
bin_path: None,
extra: BTreeMap::new(),
},
labels: BTreeMap::new(),
},
pattern: pattern.to_string(),
regex: Regex::new(pattern).unwrap(),
help_text: "test help".to_string(),
fix,
}
}

fn task_report(output: &str) -> ActionTaskReport {
ActionTaskReportBuilder::default()
.output(Some(output.to_string()))
.exit_code(Some(1))
.command("test".to_string())
.build()
.unwrap()
}

#[tokio::test]
async fn yolo_auto_approves_known_error_fix() -> Result<()> {
let action = build_run_fail_fix_succeed_action();
let exec_runner = MockExecutionProvider::new();
let glob_walker = MockGlobWalker::new();
let mut run = setup_test(vec![action], exec_runner, glob_walker);

run.working_dir = PathBuf::from("/tmp");
run.yolo = true;
run.known_errors.insert(
"ScopeKnownError/test-error".to_string(),
make_known_error("error-pattern", true),
);

let report = task_report("error-pattern found in output");
let status = run.analyze_known_errors(&[report]).await?;

assert!(matches!(status, AnalyzeStatus::KnownErrorFoundFixSucceeded));
Ok(())
}

#[tokio::test]
async fn no_tty_without_yolo_returns_user_denied() -> Result<()> {
let action = build_run_fail_fix_succeed_action();
let exec_runner = MockExecutionProvider::new();
let glob_walker = MockGlobWalker::new();
let mut run = setup_test(vec![action], exec_runner, glob_walker);

run.yolo = false;
run.known_errors.insert(
"ScopeKnownError/test-error".to_string(),
make_known_error("error-pattern", true),
);

let report = task_report("error-pattern found in output");
let status = run.analyze_known_errors(&[report]).await?;

assert!(matches!(status, AnalyzeStatus::KnownErrorFoundUserDenied));
Ok(())
}

#[tokio::test]
async fn no_pattern_match_returns_no_known_errors() -> Result<()> {
let action = build_run_fail_fix_succeed_action();
let exec_runner = MockExecutionProvider::new();
let glob_walker = MockGlobWalker::new();
let mut run = setup_test(vec![action], exec_runner, glob_walker);

run.known_errors.insert(
"ScopeKnownError/test-error".to_string(),
make_known_error("error-pattern", true),
);

let report = task_report("totally unrelated output");
let status = run.analyze_known_errors(&[report]).await?;

assert!(matches!(status, AnalyzeStatus::NoKnownErrorsFound));
Ok(())
}

#[tokio::test]
async fn pattern_match_without_fix_returns_no_fix_found() -> Result<()> {
let action = build_run_fail_fix_succeed_action();
let exec_runner = MockExecutionProvider::new();
let glob_walker = MockGlobWalker::new();
let mut run = setup_test(vec![action], exec_runner, glob_walker);

run.known_errors.insert(
"ScopeKnownError/test-error".to_string(),
make_known_error("error-pattern", false),
);

let report = task_report("error-pattern found in output");
let status = run.analyze_known_errors(&[report]).await?;

assert!(matches!(status, AnalyzeStatus::KnownErrorFoundNoFixFound));
Ok(())
}
}
}
1 change: 1 addition & 0 deletions src/doctor/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ fn transform_inputs(found_config: &FoundConfig, args: &DoctorRunArgs) -> RunTran
working_dir: found_config.working_dir.clone(),
file_cache: file_cache.clone(),
run_fix: args.fix.unwrap_or(true),
yolo: args.yolo,
exec_runner: exec_runner.clone(),
glob_walker: glob_walker.clone(),
known_errors: found_config.known_error.clone(),
Expand Down
Loading
Loading