diff --git a/Cargo.lock b/Cargo.lock index 0328383..0083ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored_json" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35980a1b846f8e3e359fd18099172a0857140ba9230affc4f71348081e039b6" +dependencies = [ + "serde", + "serde_json", + "yansi", +] + [[package]] name = "console" version = "0.15.11" @@ -480,6 +491,7 @@ dependencies = [ "assert_cmd", "cc", "clap", + "colored_json", "env_logger", "fs_extra", "indicatif", @@ -519,6 +531,7 @@ dependencies = [ "cfg-if", "miette-derive", "owo-colors", + "serde", "supports-color", "supports-hyperlinks", "supports-unicode", @@ -1245,6 +1258,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.24" diff --git a/Cargo.toml b/Cargo.toml index 55b8e89..35d1ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.36", features = ["derive"] } +colored_json = "5.0.0" indicatif = "0.18.0" itertools = "0.14.0" regex = "1.11.1" @@ -19,7 +20,7 @@ tree-sitter-rust-orchard = "0.12.0" tree-sitter-java = "0.23.5" tree-sitter-python = "0.25.0" rayon = "1.11.0" -miette = { version = "7.6.0", features = ["fancy"] } +miette = { version = "7.6.0", features = ["fancy", "serde"] } [build-dependencies] cc="*" diff --git a/editors/code/package.json b/editors/code/package.json index 8436951..482efe8 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -10,7 +10,7 @@ "engines": { "vscode": "^1.83.0", "node": ">=16.0.0", - "pnpm": "10.8.0" + "pnpm": ">=10.8.0" }, "categories": [ "Debuggers" diff --git a/editors/code/src/debugAdapter.ts b/editors/code/src/debugAdapter.ts index 80630cd..9fbb020 100644 --- a/editors/code/src/debugAdapter.ts +++ b/editors/code/src/debugAdapter.ts @@ -270,7 +270,6 @@ export class DebugSession extends LoggingDebugSession { const log2srcPath = path.resolve(__dirname, this._binaryPath); const execFile = require('child_process').execFileSync; const start = this._line - 1; - const end = this._line; const editors = this.findEditors(); if (editors.length > 0) { @@ -280,7 +279,7 @@ export class DebugSession extends LoggingDebugSession { let l2sArgs = ['-d', this._launchArgs.source, '--log', this._launchArgs.log, '--start', start, - '--end', end] + '--count', 1] if (this._launchArgs.log_format !== undefined && this._launchArgs.log_format !== "") { l2sArgs.push("-f"); l2sArgs.push(this._launchArgs.log_format); diff --git a/src/lib.rs b/src/lib.rs index 3fe4512..5c63a7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::ffi::OsStr; use std::fs::File; use std::io; -use std::ops::{Deref, RangeBounds}; +use std::ops::Deref; use std::path::{Path, PathBuf}; use std::sync::{Arc, LazyLock}; use thiserror::Error; @@ -35,6 +35,8 @@ pub use source_ref::SourceRef; #[derive(Error, Debug, Diagnostic, Clone)] pub enum LogError { + #[error("unable to read line {line}")] + UnableToReadLine { line: usize, source: Arc }, #[error("invalid log format regular expression")] InvalidFormatRegex { source: regex::Error }, #[error("unknown capture in log format: {name}")] @@ -53,6 +55,11 @@ pub enum LogError { path: PathBuf, source: Arc, }, + #[error("cannot read log file \"{path}\"")] + CannotReadLogFile { + path: PathBuf, + source: Arc, + }, #[error("no log statements found")] #[diagnostic(help( "\ @@ -68,6 +75,9 @@ pub enum LogError { }, #[error("unsupported file type \"{name}\"")] UnsupportedFileType { name: String }, + #[error("no log messages found in input")] + #[diagnostic(help("Make sure the log format matches the input"))] + NoLogMessages, } /// Collection of log statements in a single source file @@ -299,6 +309,65 @@ static PYTHON_PLACEHOLDER_REGEX: LazyLock = LazyLock::new(|| { Regex::new(r#"%[-+ #0]*\d*(?:\.\d+)?[hlLzjt]*[diuoxXfFeEgGaAcspn%]"#).unwrap() }); +static BACKTRACE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r#"(?smx) + (? + # Match the initial 'Traceback' line + ^Traceback\s+\(most\s+recent\s+call\s+last\):\s*$\n? + + # Match all stack frames + (?: + # File line: ' File "path", line N, in function' + ^\s{2}File\s+\"[^\"]*\",\s+line\s+\d+,\s+in\s+\S+\s*$\n? + + # Code line (optional): ' code_here' + (?:^\s{4}.*$\n?)? + )+ + + # Match the final exception line + ^[a-zA-Z_][a-zA-Z0-9_.]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*:.*$ + ) + | + (? + # Match exception header(s) + (?:^\S*?(?:Exception|Error)(?::\s*.*?)?$\n?)+ + + # Match all stack trace components + (?: + # Stack frame: at package.Class.method(Source.java:123) + (?:^\s*at\s+ + (?:[a-zA-Z_$][a-zA-Z0-9_$]*\.)* # Package names + [a-zA-Z_$][a-zA-Z0-9_$]* # Class name + (?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)? # Method name + (?:\([^)]*\))? # Source info + (?:\s*~\[[^\]]+\])? # Module info + (?:\s*@[a-fA-F0-9]+)?$\n? # Memory address + ) + | + # Suppressed frames: ... N more + (?:^\s*\.{3}\s*\d+\s+ + (?:more|common\s+frames?\s+omitted)$\n? + ) + | + # Caused by chain + (?:^\s*Caused\s+by:\s* + [a-zA-Z_$][a-zA-Z0-9_$.]* # Exception class + (?::\s*.*?)?$\n? # Optional message + ) + | + # Suppressed exceptions + (?:^\s*Suppressed:\s* + [a-zA-Z_$][a-zA-Z0-9_$.]* # Exception class + (?::\s*.*?)?$\n? # Optional message + ) + )* + ) +"#, + ) + .unwrap() +}); + impl SourceLanguage { pub fn as_str(&self) -> &'static str { match self { @@ -351,7 +420,7 @@ impl SourceLanguage { (argument_list . (string_literal) @arguments) ] (#match? @object-name "log(ger)?|LOG(GER)?") - (#match? @method-name "fine|debug|info|warn|trace") + (#match? @method-name "fine|debug|info|warn|trace|error") ) "# } @@ -442,43 +511,127 @@ pub struct LogMapping<'a> { pub struct LogRef<'a> { #[serde(skip_serializing)] pub line: &'a str, + #[serde(skip_serializing_if = "is_only_body")] pub details: Option>, } +fn is_only_body(details: &Option) -> bool { + if let Some(details) = details { + details.thread.is_none() + && details.file.is_none() + && details.lineno.is_none() + && details.trace.is_none() + } else { + true + } +} + #[derive(Copy, Clone, Debug, PartialEq, Serialize)] +pub struct StackTrace<'a> { + pub language: SourceLanguage, + pub content: &'a str, +} + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Default)] pub struct LogDetails<'a> { #[serde(skip_serializing_if = "Option::is_none")] pub thread: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub file: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - pub lineno: Option, + pub lineno: Option, #[serde(skip_serializing)] pub body: Option<&'a str>, + #[serde(skip_serializing_if = "Option::is_none")] + pub trace: Option>, } -impl<'a> LogRef<'a> { - pub fn new(line: &'a str) -> Self { - Self { - line, - details: None, - } +impl<'a> LogDetails<'a> { + fn is_empty(&self) -> bool { + self.thread.is_none() + && self.file.is_none() + && self.lineno.is_none() + && self.body.is_none() + && self.trace.is_none() } +} - pub fn with_format(line: &'a str, log_format: LogFormat) -> Self { - let captures = log_format.captures(line); - let thread = captures.name("thread").map(|thread_match| thread_match.as_str()); - let file = captures.name("file").map(|file_match| file_match.as_str()); - let lineno = captures - .name("line") - .and_then(|lineno| lineno.as_str().parse::().ok()); - let body = captures.name("body").map(|body| body.as_str()); +pub struct LogRefBuilder<'a> { + details: LogDetails<'a>, +} + +impl<'a> LogRefBuilder<'a> { + pub fn new() -> Self { Self { - line, - details: Some(LogDetails { thread, file, lineno, body }), + details: Default::default(), } } + pub fn build_from_captures(self, captures: Captures<'a>, content: &'a str) -> LogRef<'a> { + self.with_file(captures.name("file").map(|m| m.as_str())) + .with_lineno( + captures + .name("line") + .map(|m| m.as_str().parse::().unwrap_or_default()), + ) + .with_thread(captures.name("thread").map(|m| m.as_str())) + .with_body(captures.name("body").map(|m| m.as_str())) + .build(content) + } + + pub fn with_thread(mut self, thread: Option<&'a str>) -> Self { + self.details.thread = thread; + self + } + pub fn with_file(mut self, file: Option<&'a str>) -> Self { + self.details.file = file; + self + } + pub fn with_lineno(mut self, lineno: Option) -> Self { + self.details.lineno = lineno; + self + } + + pub fn with_body(mut self, body: Option<&'a str>) -> Self { + let (body, trace) = if let Some(body) = body { + if let Some(trace) = BACKTRACE_REGEX.captures(body) { + let language = if trace.name("python").is_some() { + SourceLanguage::Python + } else if trace.name("java").is_some() { + SourceLanguage::Java + } else { + unreachable!(); + }; + let cap0 = trace.get(0).unwrap(); + ( + Some(*&body[0..cap0.range().start].trim_end()), + Some(StackTrace { + language, + content: cap0.as_str(), + }), + ) + } else { + (Some(body), None) + } + } else { + (None, None) + }; + self.details.body = body; + self.details.trace = trace; + self + } + + pub fn build(self, line: &'a str) -> LogRef<'a> { + let details = if self.details.is_empty() { + None + } else { + Some(self.details) + }; + LogRef { line, details } + } +} + +impl<'a> LogRef<'a> { pub fn body(self) -> &'a str { if let Some(LogDetails { body: Some(s), .. }) = self.details { s @@ -500,17 +653,20 @@ pub fn lookup_source<'a>( log_format: &LogFormat, src_refs: &'a [SourceRef], ) -> Option<&'a SourceRef> { - let captures = log_format.captures(log_ref.body()); - let file_name = captures.name("file").map_or("", |m| m.as_str()); - let line_no: usize = captures - .name("line") - .map_or(0, |m| m.as_str().parse::().unwrap_or_default()); - // println!("{:?} {:?}", file_name, line_no); - - src_refs.iter().find(|&source_ref| { - // println!("source_ref.source_path = {} line_no = {}", source_ref.source_path, source_ref.line_no); - source_ref.source_path.contains(file_name) && source_ref.line_no == line_no - }) + if let Some(captures) = log_format.captures(log_ref.body()) { + let file_name = captures.name("file").map_or("", |m| m.as_str()); + let line_no: usize = captures + .name("line") + .map_or(0, |m| m.as_str().parse::().unwrap_or_default()); + // println!("{:?} {:?}", file_name, line_no); + + src_refs.iter().find(|&source_ref| { + // println!("source_ref.source_path = {} line_no = {}", source_ref.source_path, source_ref.line_no); + source_ref.source_path.contains(file_name) && source_ref.line_no == line_no + }) + } else { + None + } } pub fn extract_variables<'a>(log_ref: &LogRef<'a>, src_ref: &'a SourceRef) -> Vec { @@ -547,26 +703,6 @@ pub fn extract_variables<'a>(log_ref: &LogRef<'a>, src_ref: &'a SourceRef) -> Ve variables } -pub fn filter_log(buffer: &str, filter: R, log_format: Option) -> Vec> -where - R: RangeBounds, -{ - buffer - .lines() - .enumerate() - .filter_map(|(line_no, line)| { - if filter.contains(&line_no) { - match &log_format { - Some(format) => Some(LogRef::with_format(line, format.clone())), - None => Some(LogRef::new(line)), - } - } else { - None - } - }) - .collect() -} - pub fn extract_logging_guarded(sources: &[CodeSource], guard: &WorkGuard) -> Vec { sources .par_iter() @@ -633,50 +769,36 @@ pub fn extract_logging(sources: &[CodeSource], tracker: &ProgressTracker) -> Vec #[cfg(test)] mod tests { use super::*; - use insta::assert_yaml_snapshot; + use insta::{assert_snapshot, assert_yaml_snapshot}; use std::ptr; - #[test] - fn test_filter_log_defaults() { - let buffer = String::from("hello\nwarning\nerror\nboom"); - let result = filter_log(&buffer, .., None); - assert_eq!( - result, - vec![ - LogRef::new("hello"), - LogRef::new("warning"), - LogRef::new("error"), - LogRef::new("boom"), - ] - ); + fn from_log_format_and_line<'a>(buffer: &'a str, log_format: LogFormat) -> LogRef<'a> { + let captures = log_format.captures(&buffer).unwrap(); + LogRefBuilder::new().build_from_captures(captures, &buffer) } #[test] - fn test_filter_log_with_filter() { - let buffer = String::from("hello\nwarning\nerror\nboom"); - let result = filter_log(&buffer, 1..2, None); - assert_eq!(result, vec![LogRef::new("warning")]); - } - - #[test] - fn test_filter_log_with_format() { + fn test_log_ref_builder() { let buffer = String::from( "2025-04-10 22:12:52 INFO JvmPauseMonitor:146 - JvmPauseMonitor-n0: Started", ); let regex = r"^(?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (?\w+)\s+ (?[\w$.]+):(?\d+) - (?.*)$"; - let result = filter_log(&buffer, .., Some(regex.try_into().unwrap())); + let log_format: LogFormat = regex.try_into().unwrap(); + let captures = log_format.captures(&buffer).unwrap(); + let result = LogRefBuilder::new().build_from_captures(captures, &buffer); let details = Some(LogDetails { thread: None, file: Some("JvmPauseMonitor"), lineno: Some(146), body: Some("JvmPauseMonitor-n0: Started"), + trace: None, }); assert_eq!( result, - vec![LogRef { + LogRef { line: "2025-04-10 22:12:52 INFO JvmPauseMonitor:146 - JvmPauseMonitor-n0: Started", details - }] + } ); } @@ -729,7 +851,7 @@ fn namedarg2(salutation: &str, name: &str) { let lf = r#"^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \w+ \w+\]\s+(?.*)"# .try_into() .unwrap(); - let log_ref = LogRef::with_format( + let log_ref = from_log_format_and_line( "[2024-05-09T19:58:53Z DEBUG main] you're only as funky as your last cut", lf, ); @@ -748,7 +870,8 @@ fn namedarg2(salutation: &str, name: &str) { let lf = r#"^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \w+ \w+\]\s+(?.*)"# .try_into() .unwrap(); - let log_ref = LogRef::with_format("[2024-05-09T19:58:53Z DEBUG main] Hello, Leander!", lf); + let log_ref = + from_log_format_and_line("[2024-05-09T19:58:53Z DEBUG main] Hello, Leander!", lf); let code = CodeSource::from_string(&Path::new("in-mem.rs"), TEST_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -773,7 +896,7 @@ fn main() { let lf = r#"^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \w+ \w+\]\s+(?.*)"# .try_into() .unwrap(); - let log_ref = LogRef::with_format( + let log_ref = from_log_format_and_line( "[2024-05-09T19:58:53Z DEBUG main] you're only as funky\n as your last cut", lf, ); @@ -797,7 +920,7 @@ fn main() { #[test] fn test_link_to_source_no_matches() { - let log_ref = LogRef::new("nope!"); + let log_ref = LogRefBuilder::new().build("nope!"); let code = CodeSource::from_string(&Path::new("in-mem.rs"), TEST_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -810,7 +933,7 @@ fn main() { #[test] fn test_extract_variables() { - let log_ref = LogRef::new("this won't match i=1; j=2"); + let log_ref = LogRefBuilder::new().build("this won't match i=1; j=2"); let code = CodeSource::from_string(&Path::new("in-mem.rs"), TEST_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -835,7 +958,7 @@ fn main() { #[test] fn test_extract_named() { - let log_ref = LogRef::new("Hello, Tim!"); + let log_ref = LogRefBuilder::new().build("Hello, Tim!"); let code = CodeSource::from_string(&Path::new("in-mem.rs"), TEST_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -868,7 +991,7 @@ fn main() { fn test_extract_var_punctuation() { let lf = r"^(?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (?\w+)\s+ (?[\w$.]+):(?\d+) - (?.*)$".try_into().unwrap(); - let log_ref = LogRef::with_format( + let log_ref = from_log_format_and_line( "2025-04-10 22:12:52 INFO JvmPauseMonitor:146 - JvmPauseMonitor-n0: Started", lf, ); @@ -898,7 +1021,7 @@ fn main() { #[test] fn test_basic_cpp() { - let log_ref = LogRef::new("Hello, Steve!"); + let log_ref = LogRefBuilder::new().build("Hello, Steve!"); let code = CodeSource::from_string(&Path::new("in-mem.cc"), CPP_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -926,7 +1049,7 @@ processing \started -- {args[0]}""") #[test] fn test_basic_python() { - let log_ref = LogRef::new("foo bar π"); + let log_ref = LogRefBuilder::new().build("foo bar π"); let code = CodeSource::from_string(&Path::new("in-mem.py"), PYTHON_SOURCE); let src_refs = extract_logging(&[code], &ProgressTracker::new()) .pop() @@ -942,4 +1065,27 @@ processing \started -- {args[0]}""") },] ); } + + const TRACE: &str = r#"JvmPauseMonitor-n0: Started +java.lang.IllegalStateException: simulated failure for demo + at org.example.Main.simulateError(Main.java:50) + at org.example.Main.main(Main.java:41) + at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279) + at java.base/java.lang.Thread.run(Thread.java:1447) +"#; + + #[test] + fn test_backtrace_re() { + let code = CodeSource::from_string(&PathBuf::from("in-mem.java"), TEST_PUNC_SRC); + let log_ref = LogRefBuilder::new().with_body(Some(TRACE)).build(TRACE); + assert_snapshot!(log_ref.line); + assert_yaml_snapshot!(log_ref); + let src_refs = extract_logging(&[code], &ProgressTracker::new()) + .pop() + .unwrap() + .log_statements; + assert_yaml_snapshot!(src_refs); + let vars = extract_variables(&log_ref, &src_refs[0]); + assert_yaml_snapshot!(vars); + } } diff --git a/src/log_format.rs b/src/log_format.rs index 1921c8f..53ebd71 100644 --- a/src/log_format.rs +++ b/src/log_format.rs @@ -1,6 +1,6 @@ use regex::{Captures, Regex, RegexBuilder}; -use crate::{LogError, LogRef}; +use crate::LogError; #[derive(Clone, Debug)] pub struct LogFormat { @@ -13,21 +13,12 @@ impl LogFormat { flatten.any(|name| name == "line") && flatten.any(|name| name == "file") } - pub fn build_src_filter(&self, log_refs: &Vec) -> Option> { - let mut results = Vec::new(); - for log_ref in log_refs { - let captures = self.captures(log_ref.line); - if let Some(file_match) = captures.name("file") { - results.push(file_match.as_str().to_string()); - } - } - (!results.is_empty()).then_some(results) + pub fn is_match(&self, line: &str) -> bool { + self.regex.is_match(line) } - pub fn captures<'a>(&self, line: &'a str) -> Captures<'a> { - self.regex - .captures(line) - .unwrap_or_else(|| panic!("Couldn't match `{}` with `{:?}`", line, self.regex)) + pub fn captures<'a>(&self, line: &'a str) -> Option> { + self.regex.captures(line) } } diff --git a/src/main.rs b/src/main.rs index aabd7de..dcd9c08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ use clap::Parser as ClapParser; +use colored_json::{ColoredFormatter, CompactFormatter, Styler}; use indicatif::{ProgressBar, ProgressStyle}; -use log2src::{filter_log, LogError, LogMapping, LogMatcher, ProgressTracker, ProgressUpdate}; +use log2src::{ + LogError, LogFormat, LogMapping, LogMatcher, LogRef, LogRefBuilder, ProgressTracker, + ProgressUpdate, +}; use miette::{IntoDiagnostic, Report}; -use serde_json::{self}; -use std::io::stdout; +use serde::Serialize; +use std::io::{stdout, BufRead, BufReader}; use std::sync::atomic::Ordering; use std::thread::sleep; use std::time::Duration; @@ -13,9 +17,9 @@ use std::{fs, io, path::PathBuf}; #[derive(ClapParser)] #[command(author, version, about, long_about)] struct Cli { - /// A source directory (or soon directoires) to map logs onto + /// The source directories to map logs onto #[arg(short = 'd', long, value_name = "SOURCES")] - sources: String, + sources: Vec, /// A log file to use, if not from stdin #[arg(short, long, value_name = "LOG")] @@ -25,19 +29,153 @@ struct Cli { #[arg(short, long, value_name = "FORMAT")] format: Option, - /// The line in the log to use (0 based) + /// The first line in the log to use (0 based) #[arg(short, long, value_name = "START")] start: Option, - /// The last line of the log to use (0 based) - #[arg(short, long, value_name = "END")] - end: Option, + /// The number of lines to process + #[arg(short, long, value_name = "COUNT")] + count: Option, /// Print progress information to standard error #[arg(short, long)] verbose: bool, } +fn get_colored_formatter() -> ColoredFormatter { + let compact_formatter = CompactFormatter {}; + + ColoredFormatter::with_styler(compact_formatter, Styler::default()) +} + +#[must_use] +struct MessageAccumulator { + log_matcher: LogMatcher, + log_format: Option, + content: String, + message_count: usize, +} + +impl MessageAccumulator { + fn new(log_matcher: LogMatcher, log_format: Option) -> Self { + Self { + log_matcher, + log_format, + content: String::new(), + message_count: 0, + } + } + + fn get_log_mapping<'a>(&self, log_ref: LogRef<'a>) -> LogMapping<'a> { + self.log_matcher + .match_log_statement(&log_ref) + .unwrap_or_else(move || LogMapping { + log_ref, + src_ref: None, + variables: vec![], + }) + } + + fn process_msg(&mut self) { + if let Some(captures) = self.log_format.as_ref().unwrap().captures(&self.content) { + self.message_count += 1; + let log_ref = LogRefBuilder::new().build_from_captures(captures, &self.content); + let log_mapping = self.get_log_mapping(log_ref); + let serialized = get_colored_formatter().to_colored_json_auto(&log_mapping); + println!("{}", serialized.unwrap()); + } + self.content.clear(); + } + + fn new_msg(&mut self, line: &str) { + if !self.content.is_empty() { + self.process_msg(); + } + + self.content.push_str(line); + } + + fn continued_line(&mut self, line: &str) { + if self.content.is_empty() { + return; + } + self.content.push('\n'); + self.content.push_str(line); + } + + fn process_bare_msg(&self, line: &str) { + let log_ref = LogRefBuilder::new().with_body(Some(line)).build(line); + let log_mapping = self.get_log_mapping(log_ref); + println!( + "{}", + get_colored_formatter() + .to_colored_json_auto(&log_mapping) + .unwrap() + ); + } + + fn consume_line(&mut self, line: &str) { + match &self.log_format { + Some(format) => { + if format.is_match(&line) { + self.new_msg(&line); + } else { + self.continued_line(&line); + } + } + None => { + self.process_bare_msg(&line); + } + } + } + + fn flush(&mut self) { + if !self.content.is_empty() { + self.process_msg(); + } + } + + fn eof(mut self) -> miette::Result<()> { + self.flush(); + + if self.log_format.is_some() && self.message_count == 0 { + Err(LogError::NoLogMessages.into()) + } else { + Ok(()) + } + } +} + +#[derive(Debug, Serialize)] +struct SerializableDiagnostic { + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + severity: Option, + #[serde(skip_serializing_if = "Option::is_none")] + source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + help: Option, +} + +impl From for SerializableDiagnostic { + fn from(value: Report) -> Self { + Self { + message: value.to_string(), + code: value.code().map(|c| c.to_string()), + severity: value.severity(), + source: value.source().map(|s| s.to_string()), + help: value.help().map(|h| h.to_string()), + } + } +} + +#[derive(Debug, Serialize)] +struct ErrorWrapper { + error: SerializableDiagnostic, +} + fn main() -> miette::Result<()> { let mut tracker = ProgressTracker::new(); @@ -71,33 +209,43 @@ fn main() -> miette::Result<()> { bar.set_position(info.completed.load(Ordering::Relaxed)); sleep(Duration::from_millis(33)); } + bar.finish_and_clear(); } } } }); } - let format_re = if let Some(format) = args.format { + let log_format: Option = if let Some(format) = args.format { Some(format.as_str().try_into()?) } else { None }; - let input = args.log; - let mut reader: Box = match input { + let reader: Box = match args.log { None => Box::new(io::stdin()), - Some(filename) => Box::new(fs::File::open(filename).expect("Can open file")), + Some(filename) => { + let path = PathBuf::from(filename); + match fs::File::open(&path) { + Ok(file) => Box::new(file), + Err(err) => { + return Err(LogError::CannotReadLogFile { + path, + source: err.into(), + } + .into()); + } + } + } }; - let mut buffer = String::new(); - reader.read_to_string(&mut buffer).into_diagnostic()?; - let filter = args.start.unwrap_or(0)..args.end.unwrap_or(usize::MAX); - - let filtered = filter_log(&buffer, filter, format_re); let mut log_matcher = LogMatcher::new(); - log_matcher - .add_root(&PathBuf::from(args.sources)) - .into_diagnostic()?; + for source in &args.sources { + log_matcher + .add_root(&PathBuf::from(source)) + .into_diagnostic()?; + } + log_matcher .discover_sources(&tracker) .into_iter() @@ -106,15 +254,36 @@ fn main() -> miette::Result<()> { if log_matcher.is_empty() { return Err(LogError::NoLogStatements.into()); } - let log_mappings = filtered - .iter() - .flat_map(|log_ref| log_matcher.match_log_statement(log_ref)) - .collect::>(); - - for mapping in log_mappings { - let serialized = serde_json::to_string(&mapping).unwrap(); - println!("{}", serialized); + let start = args.start.unwrap_or(0); + let desired_line_range = start..start.saturating_add(args.count.unwrap_or(usize::MAX)); + let mut accumulator = MessageAccumulator::new(log_matcher, log_format); + + let reader = BufReader::new(reader); + for (lineno, line_res) in reader.lines().enumerate() { + if !desired_line_range.contains(&lineno) { + continue; + } + match line_res { + Ok(line) => accumulator.consume_line(&line), + Err(err) => { + accumulator.flush(); + let report: Report = LogError::UnableToReadLine { + line: lineno, + source: err.into(), + } + .into(); + let wrapper = ErrorWrapper { + error: SerializableDiagnostic::from(report), + }; + println!( + "{}", + get_colored_formatter() + .to_colored_json_auto(&wrapper) + .unwrap() + ); + } + } } - Ok(()) + accumulator.eof() } diff --git a/src/snapshots/log2src__tests__backtrace_re-2.snap b/src/snapshots/log2src__tests__backtrace_re-2.snap new file mode 100644 index 0000000..42a03ae --- /dev/null +++ b/src/snapshots/log2src__tests__backtrace_re-2.snap @@ -0,0 +1,8 @@ +--- +source: src/lib.rs +expression: log_ref +--- +details: + trace: + language: Java + content: "java.lang.IllegalStateException: simulated failure for demo\n at org.example.Main.simulateError(Main.java:50)\n at org.example.Main.main(Main.java:41)\n at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279)\n" diff --git a/src/snapshots/log2src__tests__backtrace_re-3.snap b/src/snapshots/log2src__tests__backtrace_re-3.snap new file mode 100644 index 0000000..26aff0d --- /dev/null +++ b/src/snapshots/log2src__tests__backtrace_re-3.snap @@ -0,0 +1,30 @@ +--- +source: src/lib.rs +expression: src_refs +--- +- sourcePath: in-mem.java + language: Java + lineNumber: 3 + endLineNumber: 3 + column: 13 + name: run + text: "\"{}: Started\"" + quality: 8 + pattern: "(?s)^(.+): Started$" + args: + - Placeholder + vars: + - this +- sourcePath: in-mem.java + language: Java + lineNumber: 9 + endLineNumber: 9 + column: 15 + name: run + text: "\"{}: Stopped\"" + quality: 8 + pattern: "(?s)^(.+): Stopped$" + args: + - Placeholder + vars: + - this diff --git a/src/snapshots/log2src__tests__backtrace_re-4.snap b/src/snapshots/log2src__tests__backtrace_re-4.snap new file mode 100644 index 0000000..c744757 --- /dev/null +++ b/src/snapshots/log2src__tests__backtrace_re-4.snap @@ -0,0 +1,6 @@ +--- +source: src/lib.rs +expression: vars +--- +- expr: this + value: JvmPauseMonitor-n0 diff --git a/src/snapshots/log2src__tests__backtrace_re.snap b/src/snapshots/log2src__tests__backtrace_re.snap new file mode 100644 index 0000000..b1057ba --- /dev/null +++ b/src/snapshots/log2src__tests__backtrace_re.snap @@ -0,0 +1,10 @@ +--- +source: src/lib.rs +expression: log_ref.line +--- +JvmPauseMonitor-n0: Started +java.lang.IllegalStateException: simulated failure for demo + at org.example.Main.simulateError(Main.java:50) + at org.example.Main.main(Main.java:41) + at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:279) + at java.base/java.lang.Thread.run(Thread.java:1447) diff --git a/tests/resources/java/basic-invalid-utf.log b/tests/resources/java/basic-invalid-utf.log new file mode 100644 index 0000000..b9ab995 --- /dev/null +++ b/tests/resources/java/basic-invalid-utf.log @@ -0,0 +1,5 @@ +2024-05-08 14:46:47 FINE Basic main: Hello from main +2024-05-08 14:46:47 FINE Basic foo: Hello from foo i=0 +2024-05-08 14:46:47 INFO Basic invalid: Invalid UTF-8 sequence follows: <- invalid bytes +2024-05-08 14:46:47 FINE Basic foo: Hello from foo i=1 +2024-05-08 14:46:47 FINE Basic foo: Hello from foo i=2 diff --git a/tests/snapshots/test_java__basic.snap b/tests/snapshots/test_java__basic.snap index 6a8badc..82f4caa 100644 --- a/tests/snapshots/test_java__basic.snap +++ b/tests/snapshots/test_java__basic.snap @@ -13,9 +13,9 @@ info: success: true exit_code: 0 ----- stdout ----- -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":16,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":16,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} ----- stderr ----- diff --git a/tests/snapshots/test_java__basic_invalid_utf.snap b/tests/snapshots/test_java__basic_invalid_utf.snap new file mode 100644 index 0000000..e316d39 --- /dev/null +++ b/tests/snapshots/test_java__basic_invalid_utf.snap @@ -0,0 +1,22 @@ +--- +source: tests/test_java.rs +info: + program: log2src + args: + - "-d" + - tests/java/Basic.java + - "-l" + - tests/resources/java/basic-invalid-utf.log + - "-f" + - "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\w+ \\w+: (?.*)" +--- +success: true +exit_code: 0 +----- stdout ----- +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":16,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} +{"error":{"message":"unable to read line 2","source":"stream did not contain valid UTF-8"}} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} + +----- stderr ----- diff --git a/tests/snapshots/test_java__basic_range.snap b/tests/snapshots/test_java__basic_range.snap new file mode 100644 index 0000000..8a09c8f --- /dev/null +++ b/tests/snapshots/test_java__basic_range.snap @@ -0,0 +1,23 @@ +--- +source: tests/test_java.rs +info: + program: log2src + args: + - "-d" + - tests/java/Basic.java + - "-l" + - tests/resources/java/basic.log + - "-f" + - "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\w+ \\w+: (?.*)" + - "-s" + - "1" + - "-c" + - "2" +--- +success: true +exit_code: 0 +----- stdout ----- +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/Basic.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} + +----- stderr ----- diff --git a/tests/snapshots/test_java__basic_with_log.snap b/tests/snapshots/test_java__basic_with_log.snap index 93bb558..2300010 100644 --- a/tests/snapshots/test_java__basic_with_log.snap +++ b/tests/snapshots/test_java__basic_with_log.snap @@ -13,9 +13,9 @@ info: success: true exit_code: 0 ----- stdout ----- -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":13,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":13,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithLog.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":17,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} ----- stderr ----- diff --git a/tests/snapshots/test_java__basic_with_upper.snap b/tests/snapshots/test_java__basic_with_upper.snap index b141c30..e6893a7 100644 --- a/tests/snapshots/test_java__basic_with_upper.snap +++ b/tests/snapshots/test_java__basic_with_upper.snap @@ -13,9 +13,9 @@ info: success: true exit_code: 0 ----- stdout ----- -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":16,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":18,"endLineNumber":18,"column":16,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"0"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"1"}]} +{"logRef":{},"srcRef":{"sourcePath":"{java_dir}/BasicWithUpper.java","language":"Java","lineNumber":25,"endLineNumber":25,"column":20,"name":"foo","text":"\"Hello from foo i=\\{i}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":[{"Named":"i"}],"vars":[]},"variables":[{"expr":"i","value":"2"}]} ----- stderr ----- diff --git a/tests/snapshots/test_java__invalid_log_format.snap b/tests/snapshots/test_java__invalid_log_format.snap new file mode 100644 index 0000000..2d513b6 --- /dev/null +++ b/tests/snapshots/test_java__invalid_log_format.snap @@ -0,0 +1,19 @@ +--- +source: tests/test_java.rs +info: + program: log2src + args: + - "-d" + - tests/java/Basic.java + - "-l" + - tests/resources/java/basic.log + - "-f" + - "^-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\w+ \\w+: (?.*)" +--- +success: false +exit_code: 1 +----- stdout ----- + +----- stderr ----- +Error: × no log messages found in input + help: Make sure the log format matches the input diff --git a/tests/snapshots/test_java__invalid_log_path.snap b/tests/snapshots/test_java__invalid_log_path.snap new file mode 100644 index 0000000..9aa88b5 --- /dev/null +++ b/tests/snapshots/test_java__invalid_log_path.snap @@ -0,0 +1,19 @@ +--- +source: tests/test_java.rs +info: + program: log2src + args: + - "-d" + - tests/java/Basic.java + - "-l" + - badname.log + - "-f" + - "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2} \\w+ \\w+ \\w+: (?.*)" +--- +success: false +exit_code: 1 +----- stdout ----- + +----- stderr ----- +Error: × cannot read log file "badname.log" + ╰─▶ {errmsg} (os error 2) diff --git a/tests/snapshots/test_rust__basic.snap b/tests/snapshots/test_rust__basic.snap index 99b496a..789b36a 100644 --- a/tests/snapshots/test_rust__basic.snap +++ b/tests/snapshots/test_rust__basic.snap @@ -13,11 +13,11 @@ info: success: true exit_code: 0 ----- stdout ----- -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":6,"endLineNumber":6,"column":11,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"0"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"1"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"2"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":18,"endLineNumber":18,"column":24,"name":"bar","text":"\"Hello from bar j={j}\"","quality":14,"pattern":"(?s)^Hello from bar j=(.+)$","args":[{"Named":"j"}],"vars":[]},"variables":[{"expr":"j","value":"4"}]} -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":20,"endLineNumber":20,"column":32,"name":"baz","text":"\"Hello from baz i={1} j={0}\"","quality":16,"pattern":"(?s)^Hello from baz i=(.+) j=(.+)$","args":[{"Positional":1},{"Positional":0}],"vars":["j","i"]},"variables":[{"expr":"i","value":"5"},{"expr":"j","value":"6"}]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":6,"endLineNumber":6,"column":11,"name":"main","text":"\"Hello from main\"","quality":13,"pattern":"(?s)^Hello from main$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"0"}]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"1"}]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"foo","text":"\"Hello from foo i={}\"","quality":14,"pattern":"(?s)^Hello from foo i=(.+)$","args":["Placeholder"],"vars":["i"]},"variables":[{"expr":"i","value":"2"}]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":18,"endLineNumber":18,"column":24,"name":"bar","text":"\"Hello from bar j={j}\"","quality":14,"pattern":"(?s)^Hello from bar j=(.+)$","args":[{"Named":"j"}],"vars":[]},"variables":[{"expr":"j","value":"4"}]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/basic.rs","language":"Rust","lineNumber":20,"endLineNumber":20,"column":32,"name":"baz","text":"\"Hello from baz i={1} j={0}\"","quality":16,"pattern":"(?s)^Hello from baz i=(.+) j=(.+)$","args":[{"Positional":1},{"Positional":0}],"vars":["j","i"]},"variables":[{"expr":"i","value":"5"},{"expr":"j","value":"6"}]} ----- stderr ----- diff --git a/tests/snapshots/test_rust__stack.snap b/tests/snapshots/test_rust__stack.snap index b1ce012..533565c 100644 --- a/tests/snapshots/test_rust__stack.snap +++ b/tests/snapshots/test_rust__stack.snap @@ -15,6 +15,6 @@ info: success: true exit_code: 0 ----- stdout ----- -{"logRef":{"details":{}},"srcRef":{"sourcePath":"{example_dir}/stack.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"b","text":"\"Hello from b\"","quality":10,"pattern":"(?s)^Hello from b$","args":[],"vars":[]},"variables":[]} +{"logRef":{},"srcRef":{"sourcePath":"{example_dir}/stack.rs","language":"Rust","lineNumber":15,"endLineNumber":15,"column":11,"name":"b","text":"\"Hello from b\"","quality":10,"pattern":"(?s)^Hello from b$","args":[],"vars":[]},"variables":[]} ----- stderr ----- diff --git a/tests/test_java.rs b/tests/test_java.rs index 4b1a60a..96c97e4 100644 --- a/tests/test_java.rs +++ b/tests/test_java.rs @@ -4,6 +4,43 @@ use std::{path::Path, process::Command}; mod common_settings; +#[test] +fn invalid_log_path() -> Result<(), Box> { + let _guard = common_settings::enable_filters(); + let mut cmd = Command::cargo_bin("log2src")?; + let basic_source = Path::new("tests").join("java").join("Basic.java"); + let basic_log = Path::new("badname.log"); + cmd.arg("-d") + .arg(basic_source.to_str().expect("test case source code exists")) + .arg("-l") + .arg(basic_log.to_str().expect("test case log exists")) + .arg("-f") + .arg(r#"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \w+ \w+ \w+: (?.*)"#); + + assert_cmd_snapshot!(cmd); + Ok(()) +} + +#[test] +fn invalid_log_format() -> Result<(), Box> { + let _guard = common_settings::enable_filters(); + let mut cmd = Command::cargo_bin("log2src")?; + let basic_source = Path::new("tests").join("java").join("Basic.java"); + let basic_log = Path::new("tests") + .join("resources") + .join("java") + .join("basic.log"); + cmd.arg("-d") + .arg(basic_source.to_str().expect("test case source code exists")) + .arg("-l") + .arg(basic_log.to_str().expect("test case log exists")) + .arg("-f") + .arg(r#"^-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \w+ \w+ \w+: (?.*)"#); + + assert_cmd_snapshot!(cmd); + Ok(()) +} + #[test] fn basic() -> Result<(), Box> { let _guard = common_settings::enable_filters(); @@ -24,6 +61,50 @@ fn basic() -> Result<(), Box> { Ok(()) } +#[test] +fn basic_range() -> Result<(), Box> { + let _guard = common_settings::enable_filters(); + let mut cmd = Command::cargo_bin("log2src")?; + let basic_source = Path::new("tests").join("java").join("Basic.java"); + let basic_log = Path::new("tests") + .join("resources") + .join("java") + .join("basic.log"); + cmd.arg("-d") + .arg(basic_source.to_str().expect("test case source code exists")) + .arg("-l") + .arg(basic_log.to_str().expect("test case log exists")) + .arg("-f") + .arg(r#"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \w+ \w+ \w+: (?.*)"#) + .arg("-s") + .arg("1") + .arg("-c") + .arg("2"); + + assert_cmd_snapshot!(cmd); + Ok(()) +} + +#[test] +fn basic_invalid_utf() -> Result<(), Box> { + let _guard = common_settings::enable_filters(); + let mut cmd = Command::cargo_bin("log2src")?; + let basic_source = Path::new("tests").join("java").join("Basic.java"); + let basic_log = Path::new("tests") + .join("resources") + .join("java") + .join("basic-invalid-utf.log"); + cmd.arg("-d") + .arg(basic_source.to_str().expect("test case source code exists")) + .arg("-l") + .arg(basic_log.to_str().expect("test case log exists")) + .arg("-f") + .arg(r#"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \w+ \w+ \w+: (?.*)"#); + + assert_cmd_snapshot!(cmd); + Ok(()) +} + #[test] fn basic_with_log() -> Result<(), Box> { let _guard = common_settings::enable_filters();