diff --git a/third_party/move/tools/move-coverage/src/source_coverage.rs b/third_party/move/tools/move-coverage/src/source_coverage.rs index cba696461cda6..16d6e8511637f 100644 --- a/third_party/move/tools/move-coverage/src/source_coverage.rs +++ b/third_party/move/tools/move-coverage/src/source_coverage.rs @@ -398,34 +398,107 @@ impl SourceCoverage { } } -fn merge_spans(file_hash: FileHash, cov: FunctionSourceCoverage) -> Vec { +/// Merge overlapping spans. +pub fn merge_spans(file_hash: FileHash, cov: FunctionSourceCoverage) -> Vec { if cov.uncovered_locations.is_empty() { return vec![]; } - let mut covs: Vec<_> = cov + let mut spans = cov .uncovered_locations .iter() .filter(|loc| loc.file_hash() == file_hash) .map(|loc| Span::new(loc.start(), loc.end())) - .collect(); - if covs.is_empty() { + .collect::>(); + if spans.is_empty() { return vec![]; } - covs.sort(); + spans.sort(); - let mut unioned = Vec::new(); - let mut curr = covs.remove(0); + let mut unioned = Vec::with_capacity(spans.len()); + let mut curr = spans.remove(0); - for interval in covs { - if curr.disjoint(interval) { - unioned.push(curr); - curr = interval; + for span in spans { + if curr.end() >= span.start() { + curr = curr.merge(span); } else { - curr = curr.merge(interval); + unioned.push(curr); + curr = span; } } unioned.push(curr); unioned } + +#[cfg(test)] +mod tests { + use super::FunctionSourceCoverage; + use crate::source_coverage::merge_spans; + use codespan::Span; + use move_command_line_common::files::FileHash; + use move_ir_types::location::Loc; + + /// Ensure merging spans works as expected. + #[test] + fn test_merge_spans_works() { + let test_hash = FileHash([0; 32]); + let other_hash = FileHash([1; 32]); + + // Let's use unsorted array on purpose. + let uncovered_locations = vec![ + // Should be merged: + Loc::new(test_hash, 50, 51), + Loc::new(test_hash, 51, 52), + // Should be merged: + Loc::new(test_hash, 3, 5), + Loc::new(test_hash, 2, 10), + Loc::new(test_hash, 5, 11), + // Should stay the same: + Loc::new(test_hash, 15, 16), + // Should be merged: + Loc::new(test_hash, 20, 25), + Loc::new(test_hash, 24, 25), + Loc::new(test_hash, 21, 25), + // Should stay the same: + Loc::new(test_hash, 26, 29), + // Shouldn't intefere with other hashes. + Loc::new(other_hash, 2, 20), + ]; + let expected_spans = vec![ + Span::new(2, 11), + Span::new(15, 16), + Span::new(20, 25), + Span::new(26, 29), + Span::new(50, 52), + ]; + + let cov = FunctionSourceCoverage { + fn_is_native: false, + uncovered_locations, + }; + assert_eq!(expected_spans, merge_spans(test_hash, cov)); + } + + /// Ensure merging spans works fine when there is no spans to be merged. + #[test] + fn test_merge_spans_when_spans_are_empty() { + let test_hash = FileHash([0; 32]); + let other_hash = FileHash([1; 32]); + + // Check that it works when are files have full coverage. + let cov = FunctionSourceCoverage { + fn_is_native: false, + uncovered_locations: vec![], + }; + assert!(merge_spans(test_hash, cov).is_empty()); + + // Check that it works when the hash under test has the full coverage. + let uncovered_locations = vec![Loc::new(other_hash, 2, 20)]; + let cov = FunctionSourceCoverage { + fn_is_native: false, + uncovered_locations, + }; + assert!(merge_spans(test_hash, cov).is_empty()); + } +}