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,302 @@
|
|
|
1
|
+
mod directives;
|
|
2
|
+
mod json;
|
|
3
|
+
mod lcov;
|
|
4
|
+
mod markdown;
|
|
5
|
+
mod methods;
|
|
6
|
+
mod output;
|
|
7
|
+
mod results;
|
|
8
|
+
mod source;
|
|
9
|
+
mod stats;
|
|
10
|
+
mod thresholds;
|
|
11
|
+
|
|
12
|
+
#[cfg(test)]
|
|
13
|
+
mod test_support;
|
|
14
|
+
#[cfg(test)]
|
|
15
|
+
mod tests;
|
|
16
|
+
|
|
17
|
+
use std::borrow::Cow;
|
|
18
|
+
use std::ops::RangeInclusive;
|
|
19
|
+
use std::path::PathBuf;
|
|
20
|
+
|
|
21
|
+
pub use directives::{Blocks, Skipped};
|
|
22
|
+
pub use json::Json;
|
|
23
|
+
pub use lcov::write_lcov;
|
|
24
|
+
pub use markdown::write_markdown;
|
|
25
|
+
pub use methods::static_methods;
|
|
26
|
+
pub use output::{Format, UnknownFormat, write, write_last_run_file};
|
|
27
|
+
pub use results::{Meta, Summary, write_coverage_json, write_last_run, write_resultset};
|
|
28
|
+
pub use source::{Source, chomp};
|
|
29
|
+
pub use stats::{Criterion, FileStats, Stats, Totals, floor_digits, floor2};
|
|
30
|
+
pub use thresholds::{CRITERIA, Limits, Scope, Thresholds, Violation, violations};
|
|
31
|
+
|
|
32
|
+
use crate::FileError;
|
|
33
|
+
use crate::parallel::fill;
|
|
34
|
+
|
|
35
|
+
/// A range of source: 1-based lines, 0-based columns, as Ruby reports them.
|
|
36
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
37
|
+
pub struct Span {
|
|
38
|
+
pub start_line: u32,
|
|
39
|
+
pub start_column: u32,
|
|
40
|
+
pub end_line: u32,
|
|
41
|
+
pub end_column: u32,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl Span {
|
|
45
|
+
const fn overlaps(&self, lines: &RangeInclusive<u32>) -> bool {
|
|
46
|
+
self.start_line <= *lines.end() && self.end_line >= *lines.start()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// One way through a [`Condition`]: `then`, `else`, `when`, `in`, `body`...
|
|
51
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
52
|
+
pub struct Arm {
|
|
53
|
+
pub kind: String,
|
|
54
|
+
pub span: Span,
|
|
55
|
+
pub hits: u64,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// A branching construct (`if`, `unless`, `case`, `while`, `&.`...) and its
|
|
59
|
+
/// arms.
|
|
60
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
61
|
+
pub struct Condition {
|
|
62
|
+
pub kind: String,
|
|
63
|
+
pub span: Span,
|
|
64
|
+
pub arms: Vec<Arm>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// A method definition, and how many times it was called.
|
|
68
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
69
|
+
pub struct Method {
|
|
70
|
+
/// The class or module it was defined in, as Ruby names it.
|
|
71
|
+
pub owner: String,
|
|
72
|
+
pub name: String,
|
|
73
|
+
pub span: Span,
|
|
74
|
+
pub hits: u64,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// What was measured for one file.
|
|
78
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
79
|
+
pub struct FileCoverage {
|
|
80
|
+
/// Where to read the source.
|
|
81
|
+
pub path: PathBuf,
|
|
82
|
+
/// The path reports show: relative to the project root.
|
|
83
|
+
pub name: String,
|
|
84
|
+
/// Hits by line, index 0 for line 1; `None` where there is no code.
|
|
85
|
+
pub lines: Vec<Option<u64>>,
|
|
86
|
+
/// `None` when branches were not measured for this file.
|
|
87
|
+
pub branches: Option<Vec<Condition>>,
|
|
88
|
+
/// `None` when methods were not measured for this file.
|
|
89
|
+
pub methods: Option<Vec<Method>>,
|
|
90
|
+
/// False for a tracked file the suite never loaded: Ruby compiled it
|
|
91
|
+
/// without running it. Blank and comment lines are dropped from its
|
|
92
|
+
/// lines, and its methods (which only exist once run) are found in its
|
|
93
|
+
/// source.
|
|
94
|
+
pub loaded: bool,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// How sources are read.
|
|
98
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
99
|
+
pub struct Options {
|
|
100
|
+
/// The word between colons that opens and closes a block of lines
|
|
101
|
+
/// coverage ignores: `# :nocov:`.
|
|
102
|
+
pub nocov_token: String,
|
|
103
|
+
/// Methods were measured: files never loaded get theirs from the source.
|
|
104
|
+
pub methods: bool,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl Default for Options {
|
|
108
|
+
fn default() -> Self {
|
|
109
|
+
Self { nocov_token: "nocov".to_owned(), methods: false }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// A file, read and classified.
|
|
114
|
+
#[derive(Debug)]
|
|
115
|
+
pub struct FileReport<'a> {
|
|
116
|
+
pub file: &'a FileCoverage,
|
|
117
|
+
pub source: Source,
|
|
118
|
+
pub stats: FileStats,
|
|
119
|
+
lines: Cow<'a, [Option<u64>]>,
|
|
120
|
+
methods: Option<Cow<'a, [Method]>>,
|
|
121
|
+
skipped: Skipped,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// A source line and its coverage.
|
|
125
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
126
|
+
pub struct Line<'r> {
|
|
127
|
+
pub number: u32,
|
|
128
|
+
pub text: &'r [u8],
|
|
129
|
+
/// `None` where there is no code. A skipped line keeps its hits.
|
|
130
|
+
pub hits: Option<u64>,
|
|
131
|
+
pub skipped: bool,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl Line<'_> {
|
|
135
|
+
/// Not code.
|
|
136
|
+
#[must_use]
|
|
137
|
+
pub const fn never(&self) -> bool {
|
|
138
|
+
!self.skipped && self.hits.is_none()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[must_use]
|
|
142
|
+
pub const fn covered(&self) -> bool {
|
|
143
|
+
!self.skipped && matches!(self.hits, Some(hits) if hits > 0)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Code that never ran.
|
|
147
|
+
#[must_use]
|
|
148
|
+
pub const fn missed(&self) -> bool {
|
|
149
|
+
!self.skipped && matches!(self.hits, Some(0))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// An arm of a condition, as reports show it.
|
|
154
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
155
|
+
pub struct Branch<'r> {
|
|
156
|
+
pub kind: &'r str,
|
|
157
|
+
pub start_line: u32,
|
|
158
|
+
pub end_line: u32,
|
|
159
|
+
pub hits: u64,
|
|
160
|
+
/// The arm starts on the line of its condition (`x ? a : b`).
|
|
161
|
+
pub inline: bool,
|
|
162
|
+
pub skipped: bool,
|
|
163
|
+
/// Where its condition starts.
|
|
164
|
+
pub condition_line: u32,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
impl Branch<'_> {
|
|
168
|
+
/// The line a branch is shown on: its own when inline, else the line
|
|
169
|
+
/// before it (the `if` or `else` above a block).
|
|
170
|
+
#[must_use]
|
|
171
|
+
pub const fn report_line(&self) -> u32 {
|
|
172
|
+
if self.inline { self.start_line } else { self.start_line.saturating_sub(1) }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[must_use]
|
|
176
|
+
pub const fn covered(&self) -> bool {
|
|
177
|
+
!self.skipped && self.hits > 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[must_use]
|
|
181
|
+
pub const fn missed(&self) -> bool {
|
|
182
|
+
!self.skipped && self.hits == 0
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// A method and its coverage.
|
|
187
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
188
|
+
pub struct MethodReport<'r> {
|
|
189
|
+
pub method: &'r Method,
|
|
190
|
+
pub skipped: bool,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl MethodReport<'_> {
|
|
194
|
+
#[must_use]
|
|
195
|
+
pub const fn covered(&self) -> bool {
|
|
196
|
+
!self.skipped && self.method.hits > 0
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[must_use]
|
|
200
|
+
pub const fn missed(&self) -> bool {
|
|
201
|
+
!self.skipped && self.method.hits == 0
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Reads `file`'s source and classifies what was measured.
|
|
206
|
+
///
|
|
207
|
+
/// # Errors
|
|
208
|
+
///
|
|
209
|
+
/// [`FileError`] when the source cannot be read.
|
|
210
|
+
pub fn analyze<'a>(file: &'a FileCoverage, options: &Options) -> Result<FileReport<'a>, FileError> {
|
|
211
|
+
let source = Source::read(&file.path)?;
|
|
212
|
+
let skipped = Skipped::of(&source, &options.nocov_token);
|
|
213
|
+
let lines = if file.loaded {
|
|
214
|
+
Cow::Borrowed(file.lines.as_slice())
|
|
215
|
+
} else {
|
|
216
|
+
Cow::Owned(source.relevant(&file.lines, &skipped.line))
|
|
217
|
+
};
|
|
218
|
+
let methods = match &file.methods {
|
|
219
|
+
Some(methods) => Some(Cow::Borrowed(methods.as_slice())),
|
|
220
|
+
None if !file.loaded && options.methods => static_methods(&source).map(Cow::Owned),
|
|
221
|
+
None => None,
|
|
222
|
+
};
|
|
223
|
+
let mut report =
|
|
224
|
+
FileReport { file, source, stats: FileStats::default(), lines, methods, skipped };
|
|
225
|
+
report.stats = FileStats::of(&report);
|
|
226
|
+
Ok(report)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// [`analyze`] for every file, spread over the available cores. Reports keep
|
|
230
|
+
/// the order of `files`.
|
|
231
|
+
///
|
|
232
|
+
/// # Errors
|
|
233
|
+
///
|
|
234
|
+
/// The [`FileError`] of the first unreadable source, in input order.
|
|
235
|
+
pub fn analyze_all<'a>(
|
|
236
|
+
files: &'a [FileCoverage],
|
|
237
|
+
options: &Options,
|
|
238
|
+
) -> Result<Vec<FileReport<'a>>, FileError> {
|
|
239
|
+
let mut reports: Vec<Option<FileReport<'a>>> = files.iter().map(|_| None).collect();
|
|
240
|
+
fill(files, &mut reports, |file| analyze(file, options).map(Some))?;
|
|
241
|
+
Ok(reports.into_iter().flatten().collect())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
impl<'a> FileReport<'a> {
|
|
245
|
+
/// Hits by line, as measured (index 0 for line 1): for a file never
|
|
246
|
+
/// loaded, its code lines only.
|
|
247
|
+
#[must_use]
|
|
248
|
+
pub fn raw_lines(&self) -> &[Option<u64>] {
|
|
249
|
+
&self.lines
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Every line of the source, in order.
|
|
253
|
+
pub fn lines(&self) -> impl Iterator<Item = Line<'_>> {
|
|
254
|
+
self.source.lines().zip(1..).enumerate().map(|(index, (text, number))| Line {
|
|
255
|
+
number,
|
|
256
|
+
text,
|
|
257
|
+
hits: self.lines.get(index).copied().flatten(),
|
|
258
|
+
skipped: self.skipped.line.iter().any(|block| block.contains(&number)),
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// Every arm of every condition, in Ruby's order. Empty when branches
|
|
263
|
+
/// were not measured.
|
|
264
|
+
pub fn branches(&self) -> impl Iterator<Item = Branch<'a>> + '_ {
|
|
265
|
+
self.file.branches.iter().flatten().flat_map(move |condition| {
|
|
266
|
+
condition.arms.iter().map(move |arm| {
|
|
267
|
+
let inline = arm.span.start_line == condition.span.start_line;
|
|
268
|
+
let mut branch = Branch {
|
|
269
|
+
kind: &arm.kind,
|
|
270
|
+
start_line: arm.span.start_line,
|
|
271
|
+
end_line: arm.span.end_line,
|
|
272
|
+
hits: arm.hits,
|
|
273
|
+
inline,
|
|
274
|
+
skipped: false,
|
|
275
|
+
condition_line: condition.span.start_line,
|
|
276
|
+
};
|
|
277
|
+
branch.skipped =
|
|
278
|
+
self.skipped.branch.iter().any(|block| {
|
|
279
|
+
arm.span.overlaps(block) || block.contains(&branch.report_line())
|
|
280
|
+
});
|
|
281
|
+
branch
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/// The branches outside skipped blocks: what reports show.
|
|
287
|
+
pub fn reported_branches(&self) -> impl Iterator<Item = Branch<'a>> + '_ {
|
|
288
|
+
self.branches().filter(|branch| !branch.skipped)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// Every method, in the order measured. Empty when methods were not
|
|
292
|
+
/// measured.
|
|
293
|
+
pub fn methods(&self) -> impl Iterator<Item = MethodReport<'_>> {
|
|
294
|
+
let lines = self.source.len();
|
|
295
|
+
self.methods.iter().flat_map(|methods| methods.iter()).map(move |method| MethodReport {
|
|
296
|
+
method,
|
|
297
|
+
// A method past the end of the source has no lines to show.
|
|
298
|
+
skipped: usize::try_from(method.span.start_line).is_ok_and(|start| start > lines)
|
|
299
|
+
|| self.skipped.method.iter().any(|block| method.span.overlaps(block)),
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
//! `test "name"` declarations. Nothing is evaluated.
|
|
7
7
|
|
|
8
8
|
use std::iter::FusedIterator;
|
|
9
|
-
use std::
|
|
10
|
-
use std::
|
|
11
|
-
use std::{fs, io, slice, thread};
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
use std::slice;
|
|
12
11
|
|
|
13
12
|
use ruby_prism::{
|
|
14
13
|
CallNode, ClassNode, DefNode, Location, ModuleNode, Node, SingletonClassNode, Visit,
|
|
15
14
|
};
|
|
16
15
|
|
|
16
|
+
use crate::FileError;
|
|
17
|
+
use crate::parallel::fill;
|
|
18
|
+
|
|
17
19
|
#[cfg(test)]
|
|
18
20
|
mod tests;
|
|
19
21
|
|
|
@@ -121,17 +123,6 @@ impl ExactSizeIterator for Iter<'_> {}
|
|
|
121
123
|
|
|
122
124
|
impl FusedIterator for Iter<'_> {}
|
|
123
125
|
|
|
124
|
-
/// A test file that could not be read.
|
|
125
|
-
#[derive(Debug, thiserror::Error)]
|
|
126
|
-
#[error("{}: {source}", path.display())]
|
|
127
|
-
pub struct FileError {
|
|
128
|
-
/// The file.
|
|
129
|
-
pub path: PathBuf,
|
|
130
|
-
/// Why it could not be read.
|
|
131
|
-
#[source]
|
|
132
|
-
pub source: io::Error,
|
|
133
|
-
}
|
|
134
|
-
|
|
135
126
|
/// The tests defined in `source`, in source order. Syntax errors do not
|
|
136
127
|
/// stop discovery: what Prism recovers is reported.
|
|
137
128
|
#[must_use]
|
|
@@ -160,7 +151,7 @@ pub fn discover_many<S: AsRef<[u8]> + Sync>(sources: &[S]) -> Vec<Discovery> {
|
|
|
160
151
|
/// [`FileError`] when the file cannot be read.
|
|
161
152
|
pub fn discover_file(path: impl AsRef<Path>) -> Result<Discovery, FileError> {
|
|
162
153
|
let path = path.as_ref();
|
|
163
|
-
let source =
|
|
154
|
+
let source = FileError::read(path)?;
|
|
164
155
|
Ok(discover(&source))
|
|
165
156
|
}
|
|
166
157
|
|
|
@@ -172,47 +163,10 @@ pub fn discover_file(path: impl AsRef<Path>) -> Result<Discovery, FileError> {
|
|
|
172
163
|
/// The [`FileError`] of the first unreadable file, in input order.
|
|
173
164
|
pub fn discover_files<P: AsRef<Path> + Sync>(paths: &[P]) -> Result<Vec<Discovery>, FileError> {
|
|
174
165
|
let mut found = vec![Discovery::default(); paths.len()];
|
|
175
|
-
fill(paths, &mut found,
|
|
166
|
+
fill(paths, &mut found, discover_file)?;
|
|
176
167
|
Ok(found)
|
|
177
168
|
}
|
|
178
169
|
|
|
179
|
-
/// Runs `f` over `items` on scoped threads, each writing its own slots of
|
|
180
|
-
/// `out`. Returns the first error in input order.
|
|
181
|
-
///
|
|
182
|
-
/// Scoped threads: none outlives the call, so a process may fork right after
|
|
183
|
-
/// (a pool such as rayon's would leave its threads behind in the parent and
|
|
184
|
-
/// a broken pool in the child).
|
|
185
|
-
fn fill<T, R, E>(items: &[T], out: &mut [R], f: impl Fn(&T) -> Result<R, E> + Sync) -> Result<(), E>
|
|
186
|
-
where
|
|
187
|
-
T: Sync,
|
|
188
|
-
R: Send,
|
|
189
|
-
E: Send,
|
|
190
|
-
{
|
|
191
|
-
let run = |items: &[T], out: &mut [R]| -> Result<(), E> {
|
|
192
|
-
for (item, slot) in items.iter().zip(out) {
|
|
193
|
-
*slot = f(item)?;
|
|
194
|
-
}
|
|
195
|
-
Ok(())
|
|
196
|
-
};
|
|
197
|
-
let run = &run;
|
|
198
|
-
let threads = thread::available_parallelism().map_or(1, NonZeroUsize::get).min(items.len());
|
|
199
|
-
if threads <= 1 {
|
|
200
|
-
return run(items, out);
|
|
201
|
-
}
|
|
202
|
-
let chunk = items.len().div_ceil(threads);
|
|
203
|
-
thread::scope(|scope| {
|
|
204
|
-
let workers: Vec<_> = items
|
|
205
|
-
.chunks(chunk)
|
|
206
|
-
.zip(out.chunks_mut(chunk))
|
|
207
|
-
.map(|(items, out)| scope.spawn(move || run(items, out)))
|
|
208
|
-
.collect();
|
|
209
|
-
// Chunks are in input order and each stops at its first error.
|
|
210
|
-
workers.into_iter().try_for_each(|worker| {
|
|
211
|
-
worker.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic))
|
|
212
|
-
})
|
|
213
|
-
})
|
|
214
|
-
}
|
|
215
|
-
|
|
216
170
|
/// Walks the tree. Names are borrowed from the parse result until a test
|
|
217
171
|
/// class needs its full name.
|
|
218
172
|
struct Visitor<'pr> {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
use std::{fs, io};
|
|
3
|
+
|
|
4
|
+
/// A file that could not be read or written.
|
|
5
|
+
#[derive(Debug, thiserror::Error)]
|
|
6
|
+
#[error("{}: {source}", path.display())]
|
|
7
|
+
pub struct FileError {
|
|
8
|
+
/// The file.
|
|
9
|
+
pub path: PathBuf,
|
|
10
|
+
/// Why it failed.
|
|
11
|
+
#[source]
|
|
12
|
+
pub source: io::Error,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl FileError {
|
|
16
|
+
/// The bytes of `path`.
|
|
17
|
+
///
|
|
18
|
+
/// # Errors
|
|
19
|
+
///
|
|
20
|
+
/// A [`FileError`] for `path` when it cannot be read.
|
|
21
|
+
pub fn read(path: &Path) -> Result<Vec<u8>, Self> {
|
|
22
|
+
fs::read(path).map_err(|source| Self { path: path.to_owned(), source })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -13,14 +13,18 @@
|
|
|
13
13
|
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
14
14
|
//! ```
|
|
15
15
|
|
|
16
|
+
pub mod chart;
|
|
16
17
|
mod discover;
|
|
18
|
+
mod error;
|
|
17
19
|
mod hash;
|
|
20
|
+
mod parallel;
|
|
18
21
|
mod plan;
|
|
19
22
|
mod rng;
|
|
20
23
|
|
|
21
24
|
pub use discover::{
|
|
22
|
-
Discovery,
|
|
25
|
+
Discovery, Iter, TestDef, discover, discover_file, discover_files, discover_many,
|
|
23
26
|
};
|
|
27
|
+
pub use error::FileError;
|
|
24
28
|
pub use hash::fnv1a64;
|
|
25
29
|
pub use plan::plan;
|
|
26
30
|
pub use rng::SplitMix64;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use std::num::NonZeroUsize;
|
|
2
|
+
use std::thread;
|
|
3
|
+
|
|
4
|
+
/// Runs `f` over `items` on scoped threads, each writing its own slots of
|
|
5
|
+
/// `out`. Returns the first error in input order.
|
|
6
|
+
///
|
|
7
|
+
/// Scoped threads: none outlives the call, so a process may fork right after
|
|
8
|
+
/// (a pool such as rayon's would leave its threads behind in the parent and
|
|
9
|
+
/// a broken pool in the child).
|
|
10
|
+
pub fn fill<'a, T, R, E>(
|
|
11
|
+
items: &'a [T],
|
|
12
|
+
out: &mut [R],
|
|
13
|
+
f: impl Fn(&'a T) -> Result<R, E> + Sync,
|
|
14
|
+
) -> Result<(), E>
|
|
15
|
+
where
|
|
16
|
+
T: Sync,
|
|
17
|
+
R: Send,
|
|
18
|
+
E: Send,
|
|
19
|
+
{
|
|
20
|
+
let run = |items: &'a [T], out: &mut [R]| -> Result<(), E> {
|
|
21
|
+
for (item, slot) in items.iter().zip(out) {
|
|
22
|
+
*slot = f(item)?;
|
|
23
|
+
}
|
|
24
|
+
Ok(())
|
|
25
|
+
};
|
|
26
|
+
let run = &run;
|
|
27
|
+
let threads = thread::available_parallelism().map_or(1, NonZeroUsize::get).min(items.len());
|
|
28
|
+
if threads <= 1 {
|
|
29
|
+
return run(items, out);
|
|
30
|
+
}
|
|
31
|
+
let chunk = items.len().div_ceil(threads);
|
|
32
|
+
thread::scope(|scope| {
|
|
33
|
+
let workers: Vec<_> = items
|
|
34
|
+
.chunks(chunk)
|
|
35
|
+
.zip(out.chunks_mut(chunk))
|
|
36
|
+
.map(|(items, out)| scope.spawn(move || run(items, out)))
|
|
37
|
+
.collect();
|
|
38
|
+
// Chunks are in input order and each stops at its first error.
|
|
39
|
+
workers.into_iter().try_for_each(|worker| {
|
|
40
|
+
worker.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic))
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|