padded_cell 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Cargo.lock +16 -2
  4. data/Cargo.toml +2 -0
  5. data/README.md +170 -8
  6. data/ext/padded_cell_native/core/Cargo.toml +2 -1
  7. data/ext/padded_cell_native/core/src/chart/directives/tests.rs +56 -0
  8. data/ext/padded_cell_native/core/src/chart/directives.rs +144 -0
  9. data/ext/padded_cell_native/core/src/chart/json/tests.rs +46 -0
  10. data/ext/padded_cell_native/core/src/chart/json.rs +175 -0
  11. data/ext/padded_cell_native/core/src/chart/lcov.rs +50 -0
  12. data/ext/padded_cell_native/core/src/chart/markdown/tests.rs +93 -0
  13. data/ext/padded_cell_native/core/src/chart/markdown.rs +201 -0
  14. data/ext/padded_cell_native/core/src/chart/methods/tests.rs +38 -0
  15. data/ext/padded_cell_native/core/src/chart/methods.rs +62 -0
  16. data/ext/padded_cell_native/core/src/chart/output/tests.rs +45 -0
  17. data/ext/padded_cell_native/core/src/chart/output.rs +103 -0
  18. data/ext/padded_cell_native/core/src/chart/results/tests.rs +96 -0
  19. data/ext/padded_cell_native/core/src/chart/results.rs +379 -0
  20. data/ext/padded_cell_native/core/src/chart/source/tests.rs +64 -0
  21. data/ext/padded_cell_native/core/src/chart/source.rs +153 -0
  22. data/ext/padded_cell_native/core/src/chart/stats/tests.rs +52 -0
  23. data/ext/padded_cell_native/core/src/chart/stats.rs +218 -0
  24. data/ext/padded_cell_native/core/src/chart/test_support.rs +93 -0
  25. data/ext/padded_cell_native/core/src/chart/tests.rs +97 -0
  26. data/ext/padded_cell_native/core/src/chart/thresholds/tests.rs +36 -0
  27. data/ext/padded_cell_native/core/src/chart/thresholds.rs +116 -0
  28. data/ext/padded_cell_native/core/src/chart.rs +302 -0
  29. data/ext/padded_cell_native/core/src/discover/tests.rs +2 -0
  30. data/ext/padded_cell_native/core/src/discover.rs +7 -53
  31. data/ext/padded_cell_native/core/src/error.rs +24 -0
  32. data/ext/padded_cell_native/core/src/lib.rs +10 -3
  33. data/ext/padded_cell_native/core/src/parallel.rs +43 -0
  34. data/ext/padded_cell_native/core/src/restarts/tests.rs +29 -0
  35. data/ext/padded_cell_native/core/src/restarts.rs +64 -0
  36. data/ext/padded_cell_native/ffi/Cargo.toml +2 -1
  37. data/ext/padded_cell_native/ffi/src/chart.rs +205 -0
  38. data/ext/padded_cell_native/ffi/src/lib.rs +5 -0
  39. data/ext/padded_cell_native/ffi/src/limits.rs +83 -0
  40. data/lib/padded_cell/asylum.rb +93 -0
  41. data/lib/padded_cell/cell.rb +23 -14
  42. data/lib/padded_cell/chart/accumulator.rb +103 -0
  43. data/lib/padded_cell/chart/config.rb +129 -0
  44. data/lib/padded_cell/chart/delta.rb +148 -0
  45. data/lib/padded_cell/chart/document.rb +64 -0
  46. data/lib/padded_cell/chart/filter.rb +55 -0
  47. data/lib/padded_cell/chart/merge.rb +63 -0
  48. data/lib/padded_cell/chart/probe.rb +70 -0
  49. data/lib/padded_cell/chart/raw.rb +48 -0
  50. data/lib/padded_cell/chart/report.rb +128 -0
  51. data/lib/padded_cell/chart/source_files.rb +90 -0
  52. data/lib/padded_cell/chart.rb +38 -0
  53. data/lib/padded_cell/cli.rb +70 -4
  54. data/lib/padded_cell/limits.rb +45 -0
  55. data/lib/padded_cell/loader.rb +5 -1
  56. data/lib/padded_cell/pool.rb +82 -58
  57. data/lib/padded_cell/reporters.rb +20 -3
  58. data/lib/padded_cell/results.rb +13 -0
  59. data/lib/padded_cell/runner.rb +150 -22
  60. data/lib/padded_cell/version.rb +1 -1
  61. data/lib/padded_cell/worker.rb +63 -0
  62. data/lib/padded_cell.rb +8 -0
  63. metadata +43 -1
@@ -0,0 +1,153 @@
1
+ use std::ops::RangeInclusive;
2
+ use std::path::Path;
3
+
4
+ use crate::FileError;
5
+
6
+ #[cfg(test)]
7
+ mod tests;
8
+
9
+ /// A source file split into lines the way Ruby's `readlines` splits it:
10
+ /// after each newline, and a last line without one.
11
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
12
+ pub struct Source {
13
+ bytes: Vec<u8>,
14
+ /// Where each line ends, its newline included.
15
+ ends: Vec<usize>,
16
+ }
17
+
18
+ impl Source {
19
+ /// # Errors
20
+ ///
21
+ /// [`FileError`] when the file cannot be read.
22
+ pub fn read(path: &Path) -> Result<Self, FileError> {
23
+ Ok(Self::new(FileError::read(path)?))
24
+ }
25
+
26
+ #[must_use]
27
+ pub fn new(bytes: Vec<u8>) -> Self {
28
+ let mut ends: Vec<usize> = newline_ends(&bytes);
29
+ if bytes.last().is_some_and(|&last| last != b'\n') {
30
+ ends.push(bytes.len());
31
+ }
32
+ Self { bytes, ends }
33
+ }
34
+
35
+ /// The number of lines.
36
+ #[must_use]
37
+ pub const fn len(&self) -> usize {
38
+ self.ends.len()
39
+ }
40
+
41
+ #[must_use]
42
+ pub const fn is_empty(&self) -> bool {
43
+ self.ends.is_empty()
44
+ }
45
+
46
+ #[must_use]
47
+ pub fn bytes(&self) -> &[u8] {
48
+ &self.bytes
49
+ }
50
+
51
+ /// The 1-based line and the 0-based byte column of `offset`.
52
+ #[must_use]
53
+ pub fn position(&self, offset: usize) -> (u32, u32) {
54
+ let line = self.ends.partition_point(|&end| end <= offset);
55
+ let number = |value: usize| u32::try_from(value).unwrap_or(u32::MAX);
56
+ (number(line.strict_add(1)), number(offset.strict_sub(self.line_start(offset))))
57
+ }
58
+
59
+ /// Where the line holding `offset` starts.
60
+ #[must_use]
61
+ pub fn line_start(&self, offset: usize) -> usize {
62
+ let line = self.ends.partition_point(|&end| end <= offset);
63
+ line.checked_sub(1).map_or(0, |previous| self.ends[previous])
64
+ }
65
+
66
+ /// Each line, its newline included.
67
+ pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
68
+ let starts = std::iter::once(0).chain(self.ends.iter().copied());
69
+ starts.zip(&self.ends).map(|(start, &end)| &self.bytes[start..end])
70
+ }
71
+
72
+ /// The `# :token:` blocks: from a marker comment to the next, both
73
+ /// included. A block left open runs to the last line.
74
+ #[must_use]
75
+ pub fn nocov(&self, token: &str) -> Vec<RangeInclusive<u32>> {
76
+ let mut markers: Vec<u32> = self
77
+ .lines()
78
+ .zip(1..)
79
+ .filter(|(line, _)| nocov_marker(line, token))
80
+ .map(|(_, n)| n)
81
+ .collect();
82
+ if markers.len() % 2 == 1 {
83
+ markers.push(u32::try_from(self.len()).unwrap_or(u32::MAX));
84
+ }
85
+ markers.as_chunks::<2>().0.iter().map(|&[start, end]| start..=end).collect()
86
+ }
87
+
88
+ /// The lines of a file that never loaded, from `Coverage.line_stub`:
89
+ /// blank and comment lines, and lines in `skipped` blocks, are not code.
90
+ #[must_use]
91
+ pub fn relevant(
92
+ &self,
93
+ stub: &[Option<u64>],
94
+ skipped: &[RangeInclusive<u32>],
95
+ ) -> Vec<Option<u64>> {
96
+ let mut lines = self.lines();
97
+ stub.iter()
98
+ .zip(1..)
99
+ .map(|(&hits, number)| {
100
+ let text = lines.next()?;
101
+ let code =
102
+ !blank_or_comment(text) && !skipped.iter().any(|block| block.contains(&number));
103
+ if code { hits } else { None }
104
+ })
105
+ .collect()
106
+ }
107
+ }
108
+
109
+ /// Without its line ending, as Ruby's `chomp`.
110
+ #[must_use]
111
+ pub fn chomp(line: &[u8]) -> &[u8] {
112
+ let line = line.strip_suffix(b"\n").unwrap_or(line);
113
+ line.strip_suffix(b"\r").unwrap_or(line)
114
+ }
115
+
116
+ /// Offsets just past each newline.
117
+ fn newline_ends(bytes: &[u8]) -> Vec<usize> {
118
+ bytes
119
+ .iter()
120
+ .enumerate()
121
+ .filter(|&(_, &byte)| byte == b'\n')
122
+ .map(|(index, _)| index.strict_add(1))
123
+ .collect()
124
+ }
125
+
126
+ /// Ruby's `\s`: ASCII whitespace only.
127
+ pub const fn ruby_space(byte: u8) -> bool {
128
+ matches!(byte, b' ' | b'\t' | b'\n' | b'\x0b' | b'\x0c' | b'\r')
129
+ }
130
+
131
+ /// Without its leading `\s`.
132
+ pub fn trim_start(line: &[u8]) -> &[u8] {
133
+ let start = line.iter().position(|&byte| !ruby_space(byte)).unwrap_or(line.len());
134
+ &line[start..]
135
+ }
136
+
137
+ /// `/^\s*$/` or `/^\s*#/`.
138
+ #[must_use]
139
+ pub fn blank_or_comment(line: &[u8]) -> bool {
140
+ matches!(trim_start(line).first(), None | Some(b'#'))
141
+ }
142
+
143
+ /// `/^(\s*)#(\s*)(:token:)/`.
144
+ fn nocov_marker(line: &[u8], token: &str) -> bool {
145
+ let Some(comment) = trim_start(line).strip_prefix(b"#") else {
146
+ return false;
147
+ };
148
+ let marker = trim_start(comment);
149
+ marker
150
+ .strip_prefix(b":")
151
+ .and_then(|rest| rest.strip_prefix(token.as_bytes()))
152
+ .is_some_and(|rest| rest.starts_with(b":"))
153
+ }
@@ -0,0 +1,52 @@
1
+ use super::*;
2
+
3
+ const fn stats(covered: u64, missed: u64, hits: u64) -> Stats {
4
+ Stats { covered, missed, omitted: 0, hits }
5
+ }
6
+
7
+ #[test]
8
+ fn percent_and_strength() {
9
+ const CASES: &[(Stats, f64, f64)] = &[
10
+ (stats(0, 0, 0), 100.0, 0.0),
11
+ (stats(3, 0, 6), 100.0, 2.0),
12
+ (stats(0, 4, 0), 0.0, 0.0),
13
+ (stats(2, 1, 9), 200.0 / 3.0, 3.0),
14
+ ];
15
+ for (stats, percent, strength) in CASES {
16
+ assert_eq!(stats.percent().to_bits(), percent.to_bits(), "{stats:?}");
17
+ assert_eq!(stats.strength().to_bits(), strength.to_bits(), "{stats:?}");
18
+ }
19
+ }
20
+
21
+ #[test]
22
+ fn stats_sum() {
23
+ let total: Stats = [stats(1, 2, 3), stats(4, 5, 6)].into_iter().sum();
24
+ assert_eq!(total, stats(5, 7, 9));
25
+ assert_eq!(total.total(), 12);
26
+ }
27
+
28
+ /// Values from Ruby's `Float#floor(2)`.
29
+ #[test]
30
+ fn floor2_is_rubys_float_floor() {
31
+ const CASES: &[(f64, f64)] = &[
32
+ (0.29, 0.29),
33
+ (89.999, 89.99),
34
+ (100.0, 100.0),
35
+ (0.0, 0.0),
36
+ (200.0 / 3.0, 66.66),
37
+ (0.001, 0.0),
38
+ (57.142_857_142_857_14, 57.14),
39
+ ];
40
+ for (number, expected) in CASES {
41
+ assert_eq!(floor2(*number).to_bits(), expected.to_bits(), "number: {number}");
42
+ }
43
+ }
44
+
45
+ #[test]
46
+ fn unmeasured_files_are_at_zero_until_covered() {
47
+ let mut file = FileStats { unmeasured_branches: true, ..FileStats::default() };
48
+ assert_eq!(file.percent(Criterion::Branch).to_bits(), 0.0_f64.to_bits());
49
+ assert_eq!(file.percent(Criterion::Line).to_bits(), 100.0_f64.to_bits());
50
+ file.unmeasured_branches = false;
51
+ assert_eq!(file.percent(Criterion::Branch).to_bits(), 100.0_f64.to_bits());
52
+ }
@@ -0,0 +1,218 @@
1
+ use std::iter::Sum;
2
+ use std::ops::{Add, AddAssign};
3
+
4
+ use super::FileReport;
5
+
6
+ #[cfg(test)]
7
+ mod tests;
8
+
9
+ /// What coverage counts.
10
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
11
+ pub enum Criterion {
12
+ Line,
13
+ Branch,
14
+ Method,
15
+ }
16
+
17
+ impl Criterion {
18
+ /// `line`: its name in `.last_run.json` and in meta keys.
19
+ #[must_use]
20
+ pub const fn singular(self) -> &'static str {
21
+ match self {
22
+ Self::Line => "line",
23
+ Self::Branch => "branch",
24
+ Self::Method => "method",
25
+ }
26
+ }
27
+
28
+ /// `lines`: its name in `coverage.json`.
29
+ #[must_use]
30
+ pub const fn plural(self) -> &'static str {
31
+ match self {
32
+ Self::Line => "lines",
33
+ Self::Branch => "branches",
34
+ Self::Method => "methods",
35
+ }
36
+ }
37
+ }
38
+
39
+ /// The counts of one criterion, for a file, a group or a whole run.
40
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41
+ pub struct Stats {
42
+ pub covered: u64,
43
+ pub missed: u64,
44
+ /// Lines that are not code (lines only).
45
+ pub omitted: u64,
46
+ /// The hits of the covered items, summed.
47
+ pub hits: u64,
48
+ }
49
+
50
+ impl Stats {
51
+ /// What can be covered.
52
+ #[must_use]
53
+ pub const fn total(&self) -> u64 {
54
+ self.covered.strict_add(self.missed)
55
+ }
56
+
57
+ /// Covered, in percent. 100 when nothing was missed, nothing to cover
58
+ /// included.
59
+ #[must_use]
60
+ pub fn percent(&self) -> f64 {
61
+ if self.missed == 0 { 100.0 } else { float(self.covered) * 100.0 / float(self.total()) }
62
+ }
63
+
64
+ /// Hits per item that can be covered.
65
+ #[must_use]
66
+ pub fn strength(&self) -> f64 {
67
+ if self.total() == 0 { 0.0 } else { float(self.hits) / float(self.total()) }
68
+ }
69
+ }
70
+
71
+ impl Add for Stats {
72
+ type Output = Self;
73
+
74
+ fn add(self, other: Self) -> Self {
75
+ Self {
76
+ covered: self.covered.strict_add(other.covered),
77
+ missed: self.missed.strict_add(other.missed),
78
+ omitted: self.omitted.strict_add(other.omitted),
79
+ hits: self.hits.strict_add(other.hits),
80
+ }
81
+ }
82
+ }
83
+
84
+ impl AddAssign for Stats {
85
+ fn add_assign(&mut self, other: Self) {
86
+ *self = *self + other;
87
+ }
88
+ }
89
+
90
+ impl Sum for Stats {
91
+ fn sum<I: Iterator<Item = Self>>(stats: I) -> Self {
92
+ stats.fold(Self::default(), Add::add)
93
+ }
94
+ }
95
+
96
+ /// The counts of several files: a group, or a whole run. Its percents are
97
+ /// computed from the summed counts.
98
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
99
+ pub struct Totals {
100
+ pub line: Stats,
101
+ pub branch: Stats,
102
+ pub method: Stats,
103
+ }
104
+
105
+ impl Totals {
106
+ /// The totals of `files`.
107
+ #[must_use]
108
+ pub fn of_files<'r>(files: impl IntoIterator<Item = &'r FileReport<'r>>) -> Self {
109
+ Self::of(files.into_iter().map(|file| &file.stats.counts))
110
+ }
111
+
112
+ #[must_use]
113
+ pub fn of<'r>(files: impl IntoIterator<Item = &'r Self>) -> Self {
114
+ files.into_iter().fold(Self::default(), |totals, file| Self {
115
+ line: totals.line + file.line,
116
+ branch: totals.branch + file.branch,
117
+ method: totals.method + file.method,
118
+ })
119
+ }
120
+
121
+ #[must_use]
122
+ pub const fn get(&self, criterion: Criterion) -> Stats {
123
+ match criterion {
124
+ Criterion::Line => self.line,
125
+ Criterion::Branch => self.branch,
126
+ Criterion::Method => self.method,
127
+ }
128
+ }
129
+ }
130
+
131
+ /// The counts of a file.
132
+ #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
133
+ pub struct FileStats {
134
+ pub counts: Totals,
135
+ /// A file never loaded, without branch data: its branch percent is 0
136
+ /// rather than 100.
137
+ unmeasured_branches: bool,
138
+ unmeasured_methods: bool,
139
+ }
140
+
141
+ impl FileStats {
142
+ pub(super) fn of(report: &FileReport<'_>) -> Self {
143
+ let mut stats = Self {
144
+ unmeasured_branches: !report.file.loaded && report.file.branches.is_none(),
145
+ unmeasured_methods: !report.file.loaded && report.methods.is_none(),
146
+ ..Self::default()
147
+ };
148
+ let counts = &mut stats.counts;
149
+ for line in report.lines() {
150
+ let hits = line.hits.unwrap_or(0);
151
+ count(&mut counts.line, line.covered(), line.missed(), hits);
152
+ counts.line.omitted = counts.line.omitted.strict_add(u64::from(line.never()));
153
+ }
154
+ for branch in report.branches() {
155
+ count(&mut counts.branch, branch.covered(), branch.missed(), branch.hits);
156
+ }
157
+ for method in report.methods() {
158
+ count(&mut counts.method, method.covered(), method.missed(), method.method.hits);
159
+ }
160
+ stats
161
+ }
162
+
163
+ #[must_use]
164
+ pub const fn get(&self, criterion: Criterion) -> Stats {
165
+ self.counts.get(criterion)
166
+ }
167
+
168
+ /// The counts of `criterion`, and the file's percent.
169
+ #[must_use]
170
+ pub fn with_percent(&self, criterion: Criterion) -> (Stats, f64) {
171
+ (self.get(criterion), self.percent(criterion))
172
+ }
173
+
174
+ /// The file's percent.
175
+ #[must_use]
176
+ pub fn percent(&self, criterion: Criterion) -> f64 {
177
+ let unmeasured = match criterion {
178
+ Criterion::Line => false,
179
+ Criterion::Branch => self.unmeasured_branches,
180
+ Criterion::Method => self.unmeasured_methods,
181
+ };
182
+ let stats = self.get(criterion);
183
+ if unmeasured && stats.covered == 0 { 0.0 } else { stats.percent() }
184
+ }
185
+ }
186
+
187
+ const fn count(stats: &mut Stats, covered: bool, missed: bool, hits: u64) {
188
+ if covered {
189
+ stats.covered = stats.covered.strict_add(1);
190
+ stats.hits = stats.hits.strict_add(hits);
191
+ } else if missed {
192
+ stats.missed = stats.missed.strict_add(1);
193
+ }
194
+ }
195
+
196
+ /// Ruby's `Float#floor(2)`, which thresholds compare with:
197
+ /// 89.999 is 89.99, and 0.29 stays 0.29 although `0.29 * 100.0` is
198
+ /// 28.999999999999996.
199
+ #[must_use]
200
+ pub fn floor2(number: f64) -> f64 {
201
+ floor_digits(number, 2)
202
+ }
203
+
204
+ /// Ruby's `Float#floor(digits)`: floors `number * 10^digits`, then takes
205
+ /// the next step up when it does not exceed `number` (the product lost it).
206
+ #[must_use]
207
+ pub fn floor_digits(number: f64, digits: i32) -> f64 {
208
+ let scale = 10_f64.powi(digits);
209
+ let floored = (number * scale).floor();
210
+ let up = (floored + 1.0) / scale;
211
+ if up > number { floored / scale } else { up }
212
+ }
213
+
214
+ /// Counts are far below 2^53: the conversion is exact.
215
+ #[expect(clippy::cast_precision_loss, reason = "counts stay far below 2^53")]
216
+ const fn float(count: u64) -> f64 {
217
+ count as f64
218
+ }
@@ -0,0 +1,93 @@
1
+ use std::fs;
2
+
3
+ use tempfile::TempDir;
4
+
5
+ use super::stats::Criterion;
6
+ use super::{
7
+ Arm, Condition, FileCoverage, FileReport, Meta, Method, Options, Span, Summary, Totals,
8
+ analyze_all,
9
+ };
10
+
11
+ const CALC: &str = "# frozen_string_literal: true
12
+
13
+ class Calc
14
+ def sign(number)
15
+ if number.positive?
16
+ :positive
17
+ else
18
+ :other
19
+ end
20
+ end
21
+
22
+ # :nocov:
23
+ def debug = puts(1)
24
+ # :nocov:
25
+
26
+ def maybe(flag) = flag ? :yes : :no
27
+ end
28
+ ";
29
+
30
+ pub const fn span(start_line: u32, start_column: u32, end_line: u32, end_column: u32) -> Span {
31
+ Span { start_line, start_column, end_line, end_column }
32
+ }
33
+
34
+ pub fn arm(kind: &str, span: Span, hits: u64) -> Arm {
35
+ Arm { kind: kind.to_owned(), span, hits }
36
+ }
37
+
38
+ pub fn method(name: &str, span: Span, hits: u64) -> Method {
39
+ Method { owner: "Calc".to_owned(), name: name.to_owned(), span, hits }
40
+ }
41
+
42
+ /// Calc, run once with a positive number and once with `maybe(true)`.
43
+ pub fn calc(dir: &TempDir) -> FileCoverage {
44
+ let path = dir.path().join("calc.rb");
45
+ fs::write(&path, CALC).unwrap();
46
+ let mut lines = vec![None; 17];
47
+ for (line, hits) in [(3, 1), (4, 1), (5, 2), (6, 1), (8, 1), (13, 1), (16, 1)] {
48
+ lines[line - 1] = Some(hits);
49
+ }
50
+ FileCoverage {
51
+ path,
52
+ name: "lib/calc.rb".to_owned(),
53
+ lines,
54
+ branches: Some(vec![
55
+ Condition {
56
+ kind: "if".to_owned(),
57
+ span: span(5, 4, 9, 7),
58
+ arms: vec![arm("then", span(6, 6, 6, 15), 1), arm("else", span(8, 6, 8, 12), 0)],
59
+ },
60
+ Condition {
61
+ kind: "if".to_owned(),
62
+ span: span(16, 20, 16, 37),
63
+ arms: vec![
64
+ arm("then", span(16, 27, 16, 31), 1),
65
+ arm("else", span(16, 34, 16, 37), 0),
66
+ ],
67
+ },
68
+ ]),
69
+ methods: Some(vec![
70
+ method("sign", span(4, 2, 10, 5), 2),
71
+ method("debug", span(13, 2, 13, 21), 0),
72
+ method("maybe", span(16, 2, 16, 37), 1),
73
+ ]),
74
+ loaded: true,
75
+ }
76
+ }
77
+
78
+ /// `files`, analyzed with the default options.
79
+ pub fn reports(files: &[FileCoverage]) -> Vec<FileReport<'_>> {
80
+ analyze_all(files, &Options::default()).unwrap()
81
+ }
82
+
83
+ /// A run of `reports`, measuring `measured`, without groups or violations.
84
+ pub fn summary<'a>(reports: &'a [FileReport<'a>], measured: &[Criterion]) -> Summary<'a> {
85
+ Summary {
86
+ files: reports,
87
+ measured: measured.to_vec(),
88
+ totals: Totals::of_files(reports),
89
+ groups: Vec::new(),
90
+ violations: Vec::new(),
91
+ meta: Meta { generator: "padded_cell 0.1.0".to_owned(), ..Meta::default() },
92
+ }
93
+ }
@@ -0,0 +1,97 @@
1
+ use std::fs;
2
+
3
+ use tempfile::TempDir;
4
+
5
+ use super::test_support::calc;
6
+ use super::*;
7
+
8
+ #[test]
9
+ fn lines_branches_and_methods_classified() {
10
+ let dir = TempDir::new().unwrap();
11
+ let file = calc(&dir);
12
+ let report = analyze(&file, &Options::default()).unwrap();
13
+
14
+ let skipped: Vec<u32> =
15
+ report.lines().filter(|line| line.skipped).map(|line| line.number).collect();
16
+ assert_eq!(skipped, [12, 13, 14]);
17
+ let branches: Vec<(u32, bool, u64)> = report
18
+ .branches()
19
+ .map(|branch| (branch.report_line(), branch.inline, branch.hits))
20
+ .collect();
21
+ assert_eq!(branches, [(5, false, 1), (7, false, 0), (16, true, 1), (16, true, 0)]);
22
+ let methods: Vec<(&str, bool)> =
23
+ report.methods().map(|method| (method.method.name.as_str(), method.skipped)).collect();
24
+ assert_eq!(methods, [("sign", false), ("debug", true), ("maybe", false)]);
25
+
26
+ let stats = report.stats.counts;
27
+ assert_eq!(stats.line, Stats { covered: 6, missed: 0, omitted: 8, hits: 7 });
28
+ assert_eq!(stats.branch, Stats { covered: 2, missed: 2, omitted: 0, hits: 2 });
29
+ assert_eq!(stats.method, Stats { covered: 2, missed: 0, omitted: 0, hits: 3 });
30
+ }
31
+
32
+ #[test]
33
+ fn lcov_tracefile_byte_for_byte() {
34
+ let dir = TempDir::new().unwrap();
35
+ let empty = dir.path().join("empty.rb");
36
+ fs::write(&empty, "# nothing\n").unwrap();
37
+ let files = [
38
+ calc(&dir),
39
+ FileCoverage {
40
+ path: empty,
41
+ name: "lib/empty.rb".to_owned(),
42
+ lines: vec![None],
43
+ branches: Some(vec![]),
44
+ methods: None,
45
+ loaded: true,
46
+ },
47
+ ];
48
+ let reports = analyze_all(&files, &Options::default()).unwrap();
49
+ let mut out = Vec::new();
50
+ write_lcov(&reports, &mut out).unwrap();
51
+
52
+ let expected = "SF:./lib/calc.rb
53
+ DA:3,1
54
+ DA:4,1
55
+ DA:5,2
56
+ DA:6,1
57
+ DA:8,1
58
+ DA:16,1
59
+ LF:7
60
+ LH:6
61
+ BRDA:5,0,1,1
62
+ BRDA:7,0,2,-
63
+ BRDA:16,0,3,1
64
+ BRDA:16,0,4,-
65
+ end_of_record
66
+ SF:./lib/empty.rb
67
+
68
+ LF:0
69
+ LH:0
70
+
71
+ end_of_record
72
+ ";
73
+ assert_eq!(String::from_utf8(out).unwrap(), expected);
74
+ }
75
+
76
+ #[test]
77
+ fn files_never_loaded_count_their_code_as_missed() {
78
+ let dir = TempDir::new().unwrap();
79
+ let mut file = calc(&dir);
80
+ file.loaded = false;
81
+ file.lines = vec![Some(0); 17];
82
+ file.branches = None;
83
+ file.methods = None;
84
+ let report = analyze(&file, &Options::default()).unwrap();
85
+
86
+ assert_eq!(report.stats.counts.line, Stats { covered: 0, missed: 10, omitted: 4, hits: 0 });
87
+ assert_eq!(report.stats.percent(Criterion::Branch).to_bits(), 0.0_f64.to_bits());
88
+ }
89
+
90
+ #[test]
91
+ fn unreadable_sources_are_file_errors() {
92
+ let dir = TempDir::new().unwrap();
93
+ let mut file = calc(&dir);
94
+ file.path = dir.path().join("missing.rb");
95
+ let error = analyze(&file, &Options::default()).unwrap_err();
96
+ assert_eq!(error.path, file.path);
97
+ }
@@ -0,0 +1,36 @@
1
+ use tempfile::TempDir;
2
+
3
+ use super::super::test_support::{calc, reports};
4
+ use super::*;
5
+
6
+ /// Calc: lines 100% (6/6), branches 50% (2/4).
7
+ #[test]
8
+ fn every_kind_of_threshold() {
9
+ let dir = TempDir::new().unwrap();
10
+ let files = [calc(&dir)];
11
+ let reports = reports(&files);
12
+ let totals = Totals::of_files(&reports);
13
+ let thresholds = Thresholds {
14
+ minimum: [Some(100.0), Some(60.0), Some(100.0)],
15
+ minimum_by_file: [None, Some(50.01), None],
16
+ minimum_by_group: vec![("Core".to_owned(), [Some(100.0), Some(49.99), None])],
17
+ maximum_drop: [Some(0.0), Some(5.0), None],
18
+ last_run: [Some(100.0), Some(60.0), None],
19
+ };
20
+ let groups = [("Core".to_owned(), totals), ("Other".to_owned(), Totals::default())];
21
+ let found =
22
+ violations(&thresholds, &[Criterion::Line, Criterion::Branch], &totals, &reports, &groups);
23
+
24
+ let violation =
25
+ |scope, limit, actual| Violation { scope, criterion: Criterion::Branch, limit, actual };
26
+ assert_eq!(
27
+ found,
28
+ [
29
+ violation(Scope::Total, 60.0, 50.0),
30
+ violation(Scope::File("lib/calc.rb".to_owned()), 50.01, 50.0),
31
+ // Dropped from 60% to 50%: more than 5 points. Methods were not
32
+ // measured: their minimum is not checked.
33
+ violation(Scope::Drop, 5.0, 10.0),
34
+ ]
35
+ );
36
+ }