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
13 changes: 13 additions & 0 deletions examples/v1alpha/KnownErrorWithCapture.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: scope.github.com/v1alpha
kind: ScopeKnownError
metadata:
name: not-executable
description: Detects a permission-denied error and makes the named file executable
spec:
pattern: "permission denied: \\./(?<file>.*)"
help: "The file '{{ file }}' is not executable. Run `chmod +x {{ file }}` and try again."
fix:
prompt:
text: "Make '{{ file }}' executable?"
commands:
- chmod +x {{ file }}
6 changes: 3 additions & 3 deletions schema/merged.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@
]
},
"KnownErrorPattern": {
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.",
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.\n\nCapture groups in a matched pattern are available in `help` and in `fix`'s `commands` and\n`prompt`: a positional group like `(.*)` is substituted with `{{ captures[1] }}` (`captures[0]`\nis the whole match), and a named group like `(?<file>.*)` is substituted with `{{ file }}`.\nWhen multiple patterns are given, the first one that matches supplies the captures.",
"anyOf": [
{
"description": "A single regex pattern.",
Expand All @@ -238,7 +238,7 @@
"type": "object",
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"description": "An optional fix the user will be prompted to run. Its `commands` and `prompt` may\nreference `pattern`'s capture groups, e.g. `{{ captures[1] }}` or `{{ name }}` for a named\ngroup.",
"anyOf": [
{
"$ref": "#/$defs/DoctorFixSpec"
Expand All @@ -249,7 +249,7 @@
]
},
"help": {
"description": "Text that the user can use to fix the issue",
"description": "Text that the user can use to fix the issue. May reference `pattern`'s capture groups,\ne.g. `{{ captures[1] }}` or `{{ name }}` for a named group.",
"type": "string"
},
"pattern": {
Expand Down
6 changes: 3 additions & 3 deletions schema/v1alpha.com.github.scope.ScopeDoctorGroup.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
]
},
"KnownErrorPattern": {
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.",
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.\n\nCapture groups in a matched pattern are available in `help` and in `fix`'s `commands` and\n`prompt`: a positional group like `(.*)` is substituted with `{{ captures[1] }}` (`captures[0]`\nis the whole match), and a named group like `(?<file>.*)` is substituted with `{{ file }}`.\nWhen multiple patterns are given, the first one that matches supplies the captures.",
"anyOf": [
{
"description": "A single regex pattern.",
Expand All @@ -254,7 +254,7 @@
"type": "object",
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"description": "An optional fix the user will be prompted to run. Its `commands` and `prompt` may\nreference `pattern`'s capture groups, e.g. `{{ captures[1] }}` or `{{ name }}` for a named\ngroup.",
"anyOf": [
{
"$ref": "#/$defs/DoctorFixSpec"
Expand All @@ -265,7 +265,7 @@
]
},
"help": {
"description": "Text that the user can use to fix the issue",
"description": "Text that the user can use to fix the issue. May reference `pattern`'s capture groups,\ne.g. `{{ captures[1] }}` or `{{ name }}` for a named group.",
"type": "string"
},
"pattern": {
Expand Down
6 changes: 3 additions & 3 deletions schema/v1alpha.com.github.scope.ScopeKnownError.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
]
},
"KnownErrorPattern": {
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.",
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.\n\nCapture groups in a matched pattern are available in `help` and in `fix`'s `commands` and\n`prompt`: a positional group like `(.*)` is substituted with `{{ captures[1] }}` (`captures[0]`\nis the whole match), and a named group like `(?<file>.*)` is substituted with `{{ file }}`.\nWhen multiple patterns are given, the first one that matches supplies the captures.",
"anyOf": [
{
"description": "A single regex pattern.",
Expand All @@ -254,7 +254,7 @@
"type": "object",
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"description": "An optional fix the user will be prompted to run. Its `commands` and `prompt` may\nreference `pattern`'s capture groups, e.g. `{{ captures[1] }}` or `{{ name }}` for a named\ngroup.",
"anyOf": [
{
"$ref": "#/$defs/DoctorFixSpec"
Expand All @@ -265,7 +265,7 @@
]
},
"help": {
"description": "Text that the user can use to fix the issue",
"description": "Text that the user can use to fix the issue. May reference `pattern`'s capture groups,\ne.g. `{{ captures[1] }}` or `{{ name }}` for a named group.",
"type": "string"
},
"pattern": {
Expand Down
6 changes: 3 additions & 3 deletions schema/v1alpha.com.github.scope.ScopeReportLocation.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@
]
},
"KnownErrorPattern": {
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.",
"description": "One or more regexes that determine whether a log line matches this known error.\nAccepts either a single pattern string or a list of pattern strings; a line matching\nany one of the patterns triggers the error.\n\nCapture groups in a matched pattern are available in `help` and in `fix`'s `commands` and\n`prompt`: a positional group like `(.*)` is substituted with `{{ captures[1] }}` (`captures[0]`\nis the whole match), and a named group like `(?<file>.*)` is substituted with `{{ file }}`.\nWhen multiple patterns are given, the first one that matches supplies the captures.",
"anyOf": [
{
"description": "A single regex pattern.",
Expand All @@ -254,7 +254,7 @@
"type": "object",
"properties": {
"fix": {
"description": "An optional fix the user will be prompted to run.",
"description": "An optional fix the user will be prompted to run. Its `commands` and `prompt` may\nreference `pattern`'s capture groups, e.g. `{{ captures[1] }}` or `{{ name }}` for a named\ngroup.",
"anyOf": [
{
"$ref": "#/$defs/DoctorFixSpec"
Expand All @@ -265,7 +265,7 @@
]
},
"help": {
"description": "Text that the user can use to fix the issue",
"description": "Text that the user can use to fix the issue. May reference `pattern`'s capture groups,\ne.g. `{{ captures[1] }}` or `{{ name }}` for a named group.",
"type": "string"
},
"pattern": {
Expand Down
33 changes: 28 additions & 5 deletions src/doctor/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1571,14 +1571,14 @@ pub(crate) mod tests {

mod analyze_known_errors_spec {
use super::*;
use crate::models::prelude::{ModelMetadata, ModelMetadataAnnotations};
use crate::models::prelude::{DoctorFixSpec, ModelMetadata, ModelMetadataAnnotations};
use crate::shared::analyze::AnalyzeStatus;
use regex::RegexSet;
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"])),
Some(DoctorFixSpec {
commands: vec!["true".to_string()],
help_text: None,
help_url: None,
prompt: None,
Expand All @@ -1601,7 +1601,7 @@ pub(crate) mod tests {
},
labels: BTreeMap::new(),
},
regexes: RegexSet::new([pattern]).unwrap(),
regexes: vec![Regex::new(pattern).unwrap()],
help_text: "test help".to_string(),
fix,
}
Expand Down Expand Up @@ -1694,5 +1694,28 @@ pub(crate) mod tests {
assert!(matches!(status, AnalyzeStatus::KnownErrorFoundNoFixFound));
Ok(())
}

// help_text predates capture substitution and may contain brace-like literal text that
// isn't valid template syntax; that must not abort the run.
#[tokio::test]
async fn malformed_help_text_template_falls_back_to_raw_text() -> 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;
let mut known_error = make_known_error("error-pattern", true);
known_error.help_text = "Error code {500} occurred, 50% done, {% if".to_string();
run.known_errors
.insert("ScopeKnownError/test-error".to_string(), known_error);

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

assert!(matches!(status, AnalyzeStatus::KnownErrorFoundFixSucceeded));
Ok(())
}
}
}
13 changes: 11 additions & 2 deletions src/models/v1alpha/known_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ fn non_empty_string_list(generator: &mut schemars::generate::SchemaGenerator) ->
/// One or more regexes that determine whether a log line matches this known error.
/// Accepts either a single pattern string or a list of pattern strings; a line matching
/// any one of the patterns triggers the error.
///
/// Capture groups in a matched pattern are available in `help` and in `fix`'s `commands` and
/// `prompt`: a positional group like `(.*)` is substituted with `{{ captures[1] }}` (`captures[0]`
/// is the whole match), and a named group like `(?<file>.*)` is substituted with `{{ file }}`.
/// When multiple patterns are given, the first one that matches supplies the captures.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema)]
#[serde(untagged)]
pub enum KnownErrorPattern {
Expand All @@ -46,15 +51,18 @@ impl KnownErrorPattern {
#[serde(rename_all = "camelCase")]
#[schemars(deny_unknown_fields)]
pub struct KnownErrorSpec {
/// Text that the user can use to fix the issue
/// Text that the user can use to fix the issue. May reference `pattern`'s capture groups,
/// e.g. `{{ captures[1] }}` or `{{ name }}` for a named group.
pub help: String,

/// A regex (or list of regexes) used to determine if the line is an error.
/// A single string or a list of strings are both accepted; the known error fires when any
/// pattern matches.
pub pattern: KnownErrorPattern,

/// An optional fix the user will be prompted to run.
/// An optional fix the user will be prompted to run. Its `commands` and `prompt` may
/// reference `pattern`'s capture groups, e.g. `{{ captures[1] }}` or `{{ name }}` for a named
/// group.
pub fix: Option<DoctorFixSpec>,
}

Expand Down Expand Up @@ -121,6 +129,7 @@ impl InternalScopeModel<KnownErrorSpec, V1AlphaKnownError> for V1AlphaKnownError
vec![
"v1alpha/KnownError.yaml".to_string(),
"v1alpha/KnownErrorMultiPattern.yaml".to_string(),
"v1alpha/KnownErrorWithCapture.yaml".to_string(),
]
}
}
Expand Down
41 changes: 35 additions & 6 deletions src/shared/analyze/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::models::HelpMetadata;
use crate::prelude::{
CaptureOpts, DefaultExecutionProvider, DoctorFix, ExecutionProvider, KnownError, OutputCapture,
OutputDisplay, generate_env_vars,
OutputDisplay, generate_env_vars, substitute_templates_with_captures,
};
use anyhow::Result;
use inquire::InquireError;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tokio::io::{AsyncBufReadExt, AsyncRead};
use tracing::{debug, error, info, warn};

Expand Down Expand Up @@ -34,17 +34,46 @@ where
let mut known_errors_to_remove = Vec::new();
for (name, ke) in &known_errors {
debug!("Checking known error {}", ke.name());
if ke.regexes.is_match(&line) {
if let Some(captures) = ke.find_match(&line) {
warn!(target: "always", "Known error '{}' found on line {}", ke.name(), line_number);
info!(target: "always", "\t==> {}", ke.help_text);

let ke_working_dir = ke
.metadata
.annotations
.working_dir
.as_ref()
.expect("known error metadata should have a working_dir");
// Help text predates capture substitution and may contain brace-like literal
// text that isn't valid template syntax; fall back to the raw text rather than
// aborting the run over a cosmetic rendering failure.
let help_text =
substitute_templates_with_captures(ke_working_dir, &ke.help_text, &captures)
.unwrap_or_else(|err| {
debug!(
"Failed to render help text for known error '{}': {}",
ke.name(),
err
);
ke.help_text.clone()
});
info!(target: "always", "\t==> {}", help_text);

result = match &ke.fix {
Some(fix) => {
Some(fix_spec) => {
info!(target: "always", "found a fix!");

let containing_dir_string = ke.metadata.containing_dir();
let containing_dir = Path::new(&containing_dir_string);
let fix = DoctorFix::from_spec_with_captures(
containing_dir,
ke_working_dir,
fix_spec.clone(),
&captures,
)?;

tracing_indicatif::suspend_tracing_indicatif(|| {
let exec_path = ke.metadata.exec_path();
prompt_and_run_fix(working_dir, exec_path, fix, yolo)
prompt_and_run_fix(working_dir, exec_path, &fix, yolo)
})
.await?
}
Expand Down
40 changes: 37 additions & 3 deletions src/shared/models/internal/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use derive_builder::Builder;
use std::path::Path;

use super::{extract_command_path, substitute_templates};
use super::{RegexCaptures, extract_command_path, substitute_templates_with_captures};

#[derive(Debug, PartialEq, Clone, Builder)]
pub struct DoctorCommand {
Expand All @@ -11,7 +11,23 @@ pub struct DoctorCommand {

impl DoctorCommand {
pub fn try_new(containing_dir: &Path, working_dir: &str, command: &str) -> Result<Self> {
let cmd = substitute_templates(working_dir, command)?;
Self::try_new_with_captures(
containing_dir,
working_dir,
command,
&RegexCaptures::default(),
)
}

/// Like `try_new`, but also substitutes regex capture groups (e.g. `{{ captures[1] }}` or
/// `{{ name }}` for a named group) matched from a `ScopeKnownError` pattern.
pub fn try_new_with_captures(
containing_dir: &Path,
working_dir: &str,
command: &str,
captures: &RegexCaptures,
) -> Result<Self> {
let cmd = substitute_templates_with_captures(working_dir, command, captures)?;
let cmd = extract_command_path(containing_dir, &cmd);
let cmd = Self::expand_command(&cmd);
Ok(DoctorCommand::from_str(&cmd))
Expand Down Expand Up @@ -56,10 +72,28 @@ impl DoctorCommands {
containing_dir: &Path,
working_dir: &str,
commands: &[String],
) -> Result<DoctorCommands> {
Self::from_commands_with_captures(
containing_dir,
working_dir,
commands,
&RegexCaptures::default(),
)
}

/// Like `from_commands`, but also substitutes regex capture groups matched from a
/// `ScopeKnownError` pattern into each command.
pub fn from_commands_with_captures(
containing_dir: &Path,
working_dir: &str,
commands: &[String],
captures: &RegexCaptures,
) -> Result<DoctorCommands> {
commands
.iter()
.map(|command| DoctorCommand::try_new(containing_dir, working_dir, command))
.map(|command| {
DoctorCommand::try_new_with_captures(containing_dir, working_dir, command, captures)
})
.collect::<Result<Vec<_>>>()
.map(|commands| DoctorCommands { commands })
}
Expand Down
Loading
Loading