padded_cell 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/Cargo.lock +2 -2
- data/README.md +148 -7
- data/ext/padded_cell_native/core/Cargo.toml +1 -1
- data/ext/padded_cell_native/core/src/chart/directives/tests.rs +56 -0
- data/ext/padded_cell_native/core/src/chart/directives.rs +144 -0
- data/ext/padded_cell_native/core/src/chart/json/tests.rs +46 -0
- data/ext/padded_cell_native/core/src/chart/json.rs +175 -0
- data/ext/padded_cell_native/core/src/chart/lcov.rs +50 -0
- data/ext/padded_cell_native/core/src/chart/markdown/tests.rs +93 -0
- data/ext/padded_cell_native/core/src/chart/markdown.rs +201 -0
- data/ext/padded_cell_native/core/src/chart/methods/tests.rs +38 -0
- data/ext/padded_cell_native/core/src/chart/methods.rs +62 -0
- data/ext/padded_cell_native/core/src/chart/output/tests.rs +45 -0
- data/ext/padded_cell_native/core/src/chart/output.rs +103 -0
- data/ext/padded_cell_native/core/src/chart/results/tests.rs +96 -0
- data/ext/padded_cell_native/core/src/chart/results.rs +379 -0
- data/ext/padded_cell_native/core/src/chart/source/tests.rs +64 -0
- data/ext/padded_cell_native/core/src/chart/source.rs +153 -0
- data/ext/padded_cell_native/core/src/chart/stats/tests.rs +52 -0
- data/ext/padded_cell_native/core/src/chart/stats.rs +218 -0
- data/ext/padded_cell_native/core/src/chart/test_support.rs +93 -0
- data/ext/padded_cell_native/core/src/chart/tests.rs +97 -0
- data/ext/padded_cell_native/core/src/chart/thresholds/tests.rs +36 -0
- data/ext/padded_cell_native/core/src/chart/thresholds.rs +116 -0
- data/ext/padded_cell_native/core/src/chart.rs +302 -0
- data/ext/padded_cell_native/core/src/discover/tests.rs +2 -0
- data/ext/padded_cell_native/core/src/discover.rs +7 -53
- data/ext/padded_cell_native/core/src/error.rs +24 -0
- data/ext/padded_cell_native/core/src/lib.rs +5 -1
- data/ext/padded_cell_native/core/src/parallel.rs +43 -0
- data/ext/padded_cell_native/ffi/Cargo.toml +1 -1
- data/ext/padded_cell_native/ffi/src/chart.rs +205 -0
- data/ext/padded_cell_native/ffi/src/lib.rs +3 -0
- data/lib/padded_cell/asylum.rb +91 -0
- data/lib/padded_cell/cell.rb +14 -8
- data/lib/padded_cell/chart/accumulator.rb +103 -0
- data/lib/padded_cell/chart/config.rb +129 -0
- data/lib/padded_cell/chart/delta.rb +148 -0
- data/lib/padded_cell/chart/document.rb +64 -0
- data/lib/padded_cell/chart/filter.rb +55 -0
- data/lib/padded_cell/chart/merge.rb +63 -0
- data/lib/padded_cell/chart/probe.rb +70 -0
- data/lib/padded_cell/chart/raw.rb +48 -0
- data/lib/padded_cell/chart/report.rb +128 -0
- data/lib/padded_cell/chart/source_files.rb +90 -0
- data/lib/padded_cell/chart.rb +38 -0
- data/lib/padded_cell/cli.rb +59 -4
- data/lib/padded_cell/loader.rb +5 -1
- data/lib/padded_cell/pool.rb +19 -42
- data/lib/padded_cell/reporters.rb +20 -3
- data/lib/padded_cell/results.rb +13 -0
- data/lib/padded_cell/runner.rb +125 -22
- data/lib/padded_cell/version.rb +1 -1
- data/lib/padded_cell/worker.rb +63 -0
- data/lib/padded_cell.rb +8 -0
- metadata +39 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
use super::stats::{Criterion, Totals, floor_digits};
|
|
2
|
+
use super::{FileReport, floor2};
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
/// A limit per criterion: line, branch, method.
|
|
8
|
+
pub type Limits = [Option<f64>; 3];
|
|
9
|
+
|
|
10
|
+
pub const CRITERIA: [Criterion; 3] = [Criterion::Line, Criterion::Branch, Criterion::Method];
|
|
11
|
+
|
|
12
|
+
/// What a run must reach.
|
|
13
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
14
|
+
pub struct Thresholds {
|
|
15
|
+
/// The whole run.
|
|
16
|
+
pub minimum: Limits,
|
|
17
|
+
/// Every file.
|
|
18
|
+
pub minimum_by_file: Limits,
|
|
19
|
+
/// Groups, by name.
|
|
20
|
+
pub minimum_by_group: Vec<(String, Limits)>,
|
|
21
|
+
/// How far coverage may fall since the previous run (`.last_run.json`).
|
|
22
|
+
pub maximum_drop: Limits,
|
|
23
|
+
/// The previous run's percents.
|
|
24
|
+
pub last_run: Limits,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Where a threshold was missed.
|
|
28
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
29
|
+
pub enum Scope {
|
|
30
|
+
Total,
|
|
31
|
+
File(String),
|
|
32
|
+
Group(String),
|
|
33
|
+
/// Coverage fell since the previous run.
|
|
34
|
+
Drop,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A threshold missed. For a drop, `limit` is the maximum and `actual` the
|
|
38
|
+
/// drop; otherwise `limit` is the minimum and `actual` the percent.
|
|
39
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
40
|
+
pub struct Violation {
|
|
41
|
+
pub scope: Scope,
|
|
42
|
+
pub criterion: Criterion,
|
|
43
|
+
pub limit: f64,
|
|
44
|
+
pub actual: f64,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Every threshold `totals`, `files` and `groups` miss, for the criteria
|
|
48
|
+
/// `measured`.
|
|
49
|
+
#[must_use]
|
|
50
|
+
pub fn violations(
|
|
51
|
+
thresholds: &Thresholds,
|
|
52
|
+
measured: &[Criterion],
|
|
53
|
+
totals: &Totals,
|
|
54
|
+
files: &[FileReport<'_>],
|
|
55
|
+
groups: &[(String, Totals)],
|
|
56
|
+
) -> Vec<Violation> {
|
|
57
|
+
let mut found = Vec::new();
|
|
58
|
+
let total = |criterion| totals.get(criterion).percent();
|
|
59
|
+
below(&mut found, || Scope::Total, &thresholds.minimum, measured, total);
|
|
60
|
+
for file in files {
|
|
61
|
+
let scope = || Scope::File(file.file.name.clone());
|
|
62
|
+
below(&mut found, scope, &thresholds.minimum_by_file, measured, |c| file.stats.percent(c));
|
|
63
|
+
}
|
|
64
|
+
for (name, limits) in &thresholds.minimum_by_group {
|
|
65
|
+
if let Some((_, group)) = groups.iter().find(|(group, _)| group == name) {
|
|
66
|
+
let scope = || Scope::Group(name.clone());
|
|
67
|
+
below(&mut found, scope, limits, measured, |criterion| group.get(criterion).percent());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (criterion, maximum) in limits_of(&thresholds.maximum_drop, measured) {
|
|
71
|
+
let Some(previous) = thresholds.last_run[index(criterion)] else {
|
|
72
|
+
continue;
|
|
73
|
+
};
|
|
74
|
+
let drop = floor_digits(previous - floor2(totals.get(criterion).percent()), 10);
|
|
75
|
+
if drop > maximum {
|
|
76
|
+
found.push(Violation { scope: Scope::Drop, criterion, limit: maximum, actual: drop });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
found
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The percents, floored, under their minimum.
|
|
83
|
+
fn below(
|
|
84
|
+
found: &mut Vec<Violation>,
|
|
85
|
+
scope: impl Fn() -> Scope,
|
|
86
|
+
limits: &Limits,
|
|
87
|
+
measured: &[Criterion],
|
|
88
|
+
percent: impl Fn(Criterion) -> f64,
|
|
89
|
+
) {
|
|
90
|
+
for (criterion, limit) in limits_of(limits, measured) {
|
|
91
|
+
let actual = floor2(percent(criterion));
|
|
92
|
+
if actual < limit {
|
|
93
|
+
found.push(Violation { scope: scope(), criterion, limit, actual });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The limits set, for the criteria measured.
|
|
99
|
+
fn limits_of<'l>(
|
|
100
|
+
limits: &'l Limits,
|
|
101
|
+
measured: &'l [Criterion],
|
|
102
|
+
) -> impl Iterator<Item = (Criterion, f64)> + 'l {
|
|
103
|
+
CRITERIA
|
|
104
|
+
.into_iter()
|
|
105
|
+
.zip(limits)
|
|
106
|
+
.filter(|(criterion, _)| measured.contains(criterion))
|
|
107
|
+
.filter_map(|(criterion, limit)| limit.map(|limit| (criterion, limit)))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const fn index(criterion: Criterion) -> usize {
|
|
111
|
+
match criterion {
|
|
112
|
+
Criterion::Line => 0,
|
|
113
|
+
Criterion::Branch => 1,
|
|
114
|
+
Criterion::Method => 2,
|
|
115
|
+
}
|
|
116
|
+
}
|