diff --git a/src/output_results/mod.rs b/src/output_results/mod.rs index dd72fe1..81284c3 100644 --- a/src/output_results/mod.rs +++ b/src/output_results/mod.rs @@ -120,11 +120,13 @@ pub fn output_results( continue; } - let partials: Result>>> = ranges + type Partial = (Vec>, Vec<(DateTime, String)>); + let partials: Result> = ranges .par_iter() - .map(|range| -> Result>> { + .map(|range| -> Result { let mut local_aggregators: Vec> = aggregators.iter().map(|a| a.boxed_clone()).collect(); + let mut printed: Vec<(DateTime, String)> = Vec::new(); let slice = &bytes[range.clone()]; @@ -142,6 +144,7 @@ pub fn output_results( &filter_container, &mut local_aggregators, converted_args.print_details, + &mut printed, )?; record_start = offset; } @@ -156,18 +159,28 @@ pub fn output_results( &filter_container, &mut local_aggregators, converted_args.print_details, + &mut printed, )?; } - Ok(local_aggregators) + Ok((local_aggregators, printed)) }) .collect(); debug!("Finished output in: {:?}", timing.elapsed()); let partials = partials?; - for partial in partials { - for (i, aggregator) in partial.into_iter().enumerate() { + let mut printed: Vec<(DateTime, String)> = Vec::new(); + for (partial_aggregators, partial_printed) in partials { + for (i, aggregator) in partial_aggregators.into_iter().enumerate() { aggregators[i].merge_box(aggregator.as_ref()); } + printed.extend(partial_printed); + } + if converted_args.print_details { + // Stable sort keeps records with equal timestamps in file order. + printed.sort_by_key(|entry| entry.0); + for (_, text) in printed { + println!("{text}"); + } } for agg in &mut *aggregators { agg.print(); @@ -192,6 +205,7 @@ fn filter_record( filters: &FilterContainer, local_aggregators: &mut Vec>, print: bool, + printed: &mut Vec<(DateTime, String)>, ) -> Result<()> { for filter in &filters.filters { if !filter.matches(record, &filters.format) { @@ -238,7 +252,7 @@ fn filter_record( )?; if print { - println!("{text}"); + printed.push((log_time_local, text.to_string())); } Ok(()) } diff --git a/tests/errors.rs b/tests/errors.rs index adc3815..62e7c81 100644 --- a/tests/errors.rs +++ b/tests/errors.rs @@ -191,6 +191,44 @@ fn simple_filter_with_hist_subcommand() -> Result<(), Box Ok(()) } +#[test] +fn error_output_sorted_by_timestamp() -> Result<(), Box> { + let mut cmd = Command::new(cargo::cargo_bin!("pgweasel")); + + // The records in debian_default2.log are deliberately out of order in the + // file, so a correct sort must reorder them by leading timestamp. + let output = cmd + .args(["err", "./tests/files/debian_default2.log"]) + .assert() + .success() + .get_output() + .stdout + .clone(); + let stdout = String::from_utf8(output)?; + + // Collect the leading "YYYY-MM-DD HH:MM:SS.mmm" timestamp of every record. + let timestamps: Vec<&str> = stdout + .lines() + .filter(|line| line.len() >= 23 && line.as_bytes()[4] == b'-') + .map(|line| &line[..23]) + .collect(); + + assert!( + timestamps.len() > 1, + "expected multiple error records, got {}", + timestamps.len() + ); + + let mut sorted = timestamps.clone(); + sorted.sort(); + assert_eq!( + timestamps, sorted, + "error output was not sorted by timestamp" + ); + + Ok(()) +} + #[test] fn non_existing_file() -> Result<(), Box> { let mut cmd = Command::new(cargo::cargo_bin!("pgweasel"));