benchset 0.0.3__tar.gz → 0.0.4__tar.gz
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.
- {benchset-0.0.3 → benchset-0.0.4}/Cargo.lock +1 -1
- {benchset-0.0.3 → benchset-0.0.4}/Cargo.toml +1 -1
- {benchset-0.0.3 → benchset-0.0.4}/PKG-INFO +3 -3
- {benchset-0.0.3 → benchset-0.0.4}/Package.swift +2 -2
- {benchset-0.0.3 → benchset-0.0.4}/README.md +2 -2
- {benchset-0.0.3 → benchset-0.0.4}/benchset/cli.py +1 -1
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/PKG-INFO +3 -3
- {benchset-0.0.3 → benchset-0.0.4}/native/src/kernel_tests.rs +52 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/lib.rs +38 -23
- {benchset-0.0.3 → benchset-0.0.4}/native/src/runtime_tests.rs +183 -0
- {benchset-0.0.3 → benchset-0.0.4}/pyproject.toml +1 -1
- {benchset-0.0.3 → benchset-0.0.4}/tests/swift/BenchsetTests.swift +53 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_cli.py +124 -15
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_semantics.py +11 -6
- {benchset-0.0.3 → benchset-0.0.4}/LICENSE +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/MANIFEST.in +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/Sources/Benchset/Benchset.swift +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/__init__.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/__main__.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/_native.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/api.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/models.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/py.typed +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset/reference.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/SOURCES.txt +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/dependency_links.txt +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/entry_points.txt +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/requires.txt +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/benchset.egg-info/top_level.txt +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/examples/native/c.c +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/examples/native/rust.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/examples/native/swift.swift +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/include/benchset.h +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/include/module.modulemap +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/reference-calibration.json +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/ffi.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/ffi_reference_tests.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/ffi_tests.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/kernel.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/reference.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/native/src/reference_tests.rs +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/rust-toolchain.toml +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/__init__.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/build_backend.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/build_native.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/check_native_coverage.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/check_source_install.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/package_native.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/release.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/scripts/version.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/setup.cfg +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/setup.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/__init__.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/helpers.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/swift/ReferenceTests.swift +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_benchset.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_build.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_errors.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_reference.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_reference_artifacts.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_reference_native.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_release.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_resources.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_signals.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/test_version.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/typing/__init__.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/typing/reference_negative.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/typing/reference_positive.py +0 -0
- {benchset-0.0.3 → benchset-0.0.4}/tests/typing/test_reference_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: benchset
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Sustained scalar CPU benchmarks with an embeddable Rust core
|
|
5
5
|
Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
|
|
6
6
|
Project-URL: Source, https://github.com/mutating/benchset
|
|
@@ -356,7 +356,7 @@ settings are comparable.
|
|
|
356
356
|
|
|
357
357
|
```toml
|
|
358
358
|
[dependencies]
|
|
359
|
-
benchset = "0.0.
|
|
359
|
+
benchset = "0.0.4"
|
|
360
360
|
# For changes from a local source checkout, use:
|
|
361
361
|
# benchset = { path = "../benchset" }
|
|
362
362
|
```
|
|
@@ -471,7 +471,7 @@ the same repository. Rust is not required by the consumer.
|
|
|
471
471
|
For a Swift manifest, add:
|
|
472
472
|
|
|
473
473
|
```swift
|
|
474
|
-
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.
|
|
474
|
+
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.4"))
|
|
475
475
|
```
|
|
476
476
|
|
|
477
477
|
Add `.product(name: "Benchset", package: "benchset")` to your target's
|
|
@@ -9,8 +9,8 @@ let package = Package(
|
|
|
9
9
|
products: [.library(name: "Benchset", targets: ["Benchset"])],
|
|
10
10
|
targets: [
|
|
11
11
|
.binaryTarget(name: "CBenchset",
|
|
12
|
-
url: "https://github.com/mutating/benchset/releases/download/v0.0.
|
|
13
|
-
checksum: "
|
|
12
|
+
url: "https://github.com/mutating/benchset/releases/download/v0.0.4/Benchset.xcframework.zip",
|
|
13
|
+
checksum: "0e6643b1daa58773a93aec6b4228b2d81580f058dc5348609457363a9b5ec4ae"),
|
|
14
14
|
.target(name: "Benchset", dependencies: ["CBenchset"]),
|
|
15
15
|
.testTarget(name: "BenchsetTests", dependencies: ["Benchset"], path: "tests/swift")
|
|
16
16
|
]
|
|
@@ -322,7 +322,7 @@ settings are comparable.
|
|
|
322
322
|
|
|
323
323
|
```toml
|
|
324
324
|
[dependencies]
|
|
325
|
-
benchset = "0.0.
|
|
325
|
+
benchset = "0.0.4"
|
|
326
326
|
# For changes from a local source checkout, use:
|
|
327
327
|
# benchset = { path = "../benchset" }
|
|
328
328
|
```
|
|
@@ -437,7 +437,7 @@ the same repository. Rust is not required by the consumer.
|
|
|
437
437
|
For a Swift manifest, add:
|
|
438
438
|
|
|
439
439
|
```swift
|
|
440
|
-
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.
|
|
440
|
+
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.4"))
|
|
441
441
|
```
|
|
442
442
|
|
|
443
443
|
Add `.product(name: "Benchset", package: "benchset")` to your target's
|
|
@@ -53,7 +53,7 @@ class _Parser(argparse.ArgumentParser):
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def settings(argv: List[str]) -> Settings:
|
|
56
|
-
source = _Arguments(named_arguments=[
|
|
56
|
+
source = _Arguments(named_arguments=[])
|
|
57
57
|
source.parser = _Parser(
|
|
58
58
|
prog='benchset', argument_default=argparse.SUPPRESS,
|
|
59
59
|
description='Sustained scalar CPU throughput and speed relative to Raspberry Pi 5.',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: benchset
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Sustained scalar CPU benchmarks with an embeddable Rust core
|
|
5
5
|
Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
|
|
6
6
|
Project-URL: Source, https://github.com/mutating/benchset
|
|
@@ -356,7 +356,7 @@ settings are comparable.
|
|
|
356
356
|
|
|
357
357
|
```toml
|
|
358
358
|
[dependencies]
|
|
359
|
-
benchset = "0.0.
|
|
359
|
+
benchset = "0.0.4"
|
|
360
360
|
# For changes from a local source checkout, use:
|
|
361
361
|
# benchset = { path = "../benchset" }
|
|
362
362
|
```
|
|
@@ -471,7 +471,7 @@ the same repository. Rust is not required by the consumer.
|
|
|
471
471
|
For a Swift manifest, add:
|
|
472
472
|
|
|
473
473
|
```swift
|
|
474
|
-
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.
|
|
474
|
+
.package(url: "https://github.com/mutating/benchset.git", .exact("0.0.4"))
|
|
475
475
|
```
|
|
476
476
|
|
|
477
477
|
Add `.product(name: "Benchset", package: "benchset")` to your target's
|
|
@@ -45,6 +45,22 @@ fn golden_workload_vectors() {
|
|
|
45
45
|
8459165180143766608,
|
|
46
46
|
],
|
|
47
47
|
),
|
|
48
|
+
(
|
|
49
|
+
2,
|
|
50
|
+
[
|
|
51
|
+
6438600776746354456,
|
|
52
|
+
496348541366349225,
|
|
53
|
+
10204253244531584854,
|
|
54
|
+
],
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
3,
|
|
58
|
+
[
|
|
59
|
+
8423339373471318280,
|
|
60
|
+
5810453192619338186,
|
|
61
|
+
1329735283187018493,
|
|
62
|
+
],
|
|
63
|
+
),
|
|
48
64
|
(
|
|
49
65
|
usize::MAX,
|
|
50
66
|
[
|
|
@@ -78,6 +94,42 @@ fn crafted_parser_inputs_and_wrapping_states() {
|
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
#[test]
|
|
98
|
+
fn parser_preserves_initial_state_before_any_separator() {
|
|
99
|
+
// A leading separator resets parser state. Exercise prefixes before that
|
|
100
|
+
// reset, including invalid tokens, while preserving the calibrated workload.
|
|
101
|
+
for (state, expected) in [
|
|
102
|
+
(
|
|
103
|
+
0,
|
|
104
|
+
[412704983584011650, 9308271035056677818, 3612450324633344040],
|
|
105
|
+
),
|
|
106
|
+
(
|
|
107
|
+
1,
|
|
108
|
+
[
|
|
109
|
+
9009395174570800451,
|
|
110
|
+
17241420472984297642,
|
|
111
|
+
8483684467978543156,
|
|
112
|
+
],
|
|
113
|
+
),
|
|
114
|
+
(
|
|
115
|
+
u64::MAX,
|
|
116
|
+
[
|
|
117
|
+
5890298480000480609,
|
|
118
|
+
1796841914283376693,
|
|
119
|
+
15172682340896178227,
|
|
120
|
+
],
|
|
121
|
+
),
|
|
122
|
+
] {
|
|
123
|
+
for (prefix, checksum) in b"+.e".iter().zip(expected) {
|
|
124
|
+
let mut kernel = Kernel::new(0).unwrap();
|
|
125
|
+
kernel.state = state;
|
|
126
|
+
kernel.input.fill(b' ');
|
|
127
|
+
kernel.input[0] = *prefix;
|
|
128
|
+
assert_eq!(kernel.step(), checksum);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
81
133
|
#[test]
|
|
82
134
|
fn capacity_failure_is_reported_without_real_oom() {
|
|
83
135
|
assert!(allocate_table(usize::MAX).unwrap_err().contains("capacity"));
|
|
@@ -169,6 +169,20 @@ struct Timing {
|
|
|
169
169
|
end: Instant,
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
impl Timing {
|
|
173
|
+
fn is_warmup(self, now: Instant) -> bool {
|
|
174
|
+
now < self.warm_end
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn sample_elapsed(self, now: Instant, last_elapsed: f64) -> Option<f64> {
|
|
178
|
+
if self.is_warmup(now) || now >= self.end {
|
|
179
|
+
return None;
|
|
180
|
+
}
|
|
181
|
+
let elapsed = now.duration_since(self.warm_end).as_secs_f64();
|
|
182
|
+
(elapsed - last_elapsed >= 1.0).then_some(elapsed)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
struct Workers {
|
|
173
187
|
handles: Vec<JoinHandle<()>>,
|
|
174
188
|
stop: Arc<AtomicBool>,
|
|
@@ -319,7 +333,7 @@ fn run_with(
|
|
|
319
333
|
{
|
|
320
334
|
break;
|
|
321
335
|
}
|
|
322
|
-
let warmup =
|
|
336
|
+
let warmup = times.is_warmup(now);
|
|
323
337
|
// Publish once per eight iterations, never contend on a shared counter.
|
|
324
338
|
for _ in 0..8 {
|
|
325
339
|
checksum = kernel.step();
|
|
@@ -387,24 +401,21 @@ fn run_with(
|
|
|
387
401
|
let mut last_sample_iterations = 0;
|
|
388
402
|
loop {
|
|
389
403
|
let now = Instant::now();
|
|
390
|
-
let warmup =
|
|
404
|
+
let warmup = times.is_warmup(now);
|
|
391
405
|
let (phase, phase_start, phase_duration) = if warmup {
|
|
392
406
|
(Phase::Warmup, times.start, options.warmup)
|
|
393
407
|
} else {
|
|
394
408
|
(Phase::Measurement, times.warm_end, options.measurement)
|
|
395
409
|
};
|
|
396
410
|
let iterations = sum(&counters, warmup);
|
|
397
|
-
if
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
&mut last_sample_iterations,
|
|
406
|
-
);
|
|
407
|
-
}
|
|
411
|
+
if let Some(elapsed) = times.sample_elapsed(now, last_sample_elapsed) {
|
|
412
|
+
append_sample(
|
|
413
|
+
&mut result.samples,
|
|
414
|
+
elapsed,
|
|
415
|
+
iterations,
|
|
416
|
+
&mut last_sample_elapsed,
|
|
417
|
+
&mut last_sample_iterations,
|
|
418
|
+
);
|
|
408
419
|
}
|
|
409
420
|
if last_phase != phase || now.duration_since(last_progress) >= Duration::from_millis(100) {
|
|
410
421
|
on_progress(Progress {
|
|
@@ -433,16 +444,7 @@ fn run_with(
|
|
|
433
444
|
for handle in workers.handles.drain(..) {
|
|
434
445
|
panicked |= handle.join().is_err();
|
|
435
446
|
}
|
|
436
|
-
result
|
|
437
|
-
result.elapsed_seconds = counters
|
|
438
|
-
.iter()
|
|
439
|
-
.map(|c| c.finish_ns.load(Ordering::Relaxed))
|
|
440
|
-
.max()
|
|
441
|
-
.unwrap_or(0) as f64
|
|
442
|
-
/ 1e9;
|
|
443
|
-
result.checksum = counters
|
|
444
|
-
.iter()
|
|
445
|
-
.fold(0, |value, c| value ^ c.checksum.load(Ordering::Relaxed));
|
|
447
|
+
collect_counters(&mut result, &counters);
|
|
446
448
|
finish_samples(&mut result);
|
|
447
449
|
finish_result(&mut result, panicked, cancellation.is_cancelled());
|
|
448
450
|
on_progress(Progress {
|
|
@@ -469,6 +471,19 @@ fn release(gate: &(std::sync::Mutex<bool>, std::sync::Condvar)) {
|
|
|
469
471
|
gate.1.notify_all();
|
|
470
472
|
}
|
|
471
473
|
|
|
474
|
+
fn collect_counters(result: &mut BenchmarkResult, counters: &[Arc<Counters>]) {
|
|
475
|
+
result.iterations = sum(counters, false);
|
|
476
|
+
result.elapsed_seconds = counters
|
|
477
|
+
.iter()
|
|
478
|
+
.map(|c| c.finish_ns.load(Ordering::Relaxed))
|
|
479
|
+
.max()
|
|
480
|
+
.unwrap_or(0) as f64
|
|
481
|
+
/ 1e9;
|
|
482
|
+
result.checksum = counters
|
|
483
|
+
.iter()
|
|
484
|
+
.fold(0, |value, c| value ^ c.checksum.load(Ordering::Relaxed));
|
|
485
|
+
}
|
|
486
|
+
|
|
472
487
|
fn sum(counters: &[Arc<Counters>], warmup: bool) -> u64 {
|
|
473
488
|
counters
|
|
474
489
|
.iter()
|
|
@@ -49,6 +49,8 @@ fn assert_recovered() {
|
|
|
49
49
|
|
|
50
50
|
#[test]
|
|
51
51
|
fn public_defaults_names_and_shared_cancellation() {
|
|
52
|
+
assert_eq!(DEFAULT_WARMUP_SECONDS, 120.0);
|
|
53
|
+
assert_eq!(DEFAULT_MEASUREMENT_SECONDS, 60.0);
|
|
52
54
|
assert_eq!(Benchmark::SingleCore.name(), "single_core");
|
|
53
55
|
assert_eq!(Benchmark::MultiCore.name(), "multi_core");
|
|
54
56
|
assert_eq!(
|
|
@@ -141,6 +143,13 @@ fn real_runs_account_for_work_and_progress_on_caller() {
|
|
|
141
143
|
assert!((0.0..=1.0).contains(&p.fraction));
|
|
142
144
|
if matches!(p.phase, Phase::Warmup | Phase::Measurement) {
|
|
143
145
|
assert!(p.phase_elapsed_seconds <= p.phase_duration_seconds);
|
|
146
|
+
let elapsed = p.phase_elapsed_seconds
|
|
147
|
+
+ if p.phase == Phase::Measurement {
|
|
148
|
+
0.02
|
|
149
|
+
} else {
|
|
150
|
+
0.0
|
|
151
|
+
};
|
|
152
|
+
assert!((p.fraction - (elapsed / 0.06).min(1.0)).abs() < 1e-12);
|
|
144
153
|
} else {
|
|
145
154
|
assert_eq!(p.phase_elapsed_seconds, result.elapsed_seconds);
|
|
146
155
|
}
|
|
@@ -807,3 +816,177 @@ fn final_sample_repairs_snapshot_after_worker_finish() {
|
|
|
807
816
|
}
|
|
808
817
|
}
|
|
809
818
|
}
|
|
819
|
+
|
|
820
|
+
#[test]
|
|
821
|
+
fn timing_uses_half_open_phases_and_periodic_measurement_samples() {
|
|
822
|
+
let start = Instant::now();
|
|
823
|
+
let times = Timing {
|
|
824
|
+
start,
|
|
825
|
+
warm_end: start + Duration::from_secs(1),
|
|
826
|
+
end: start + Duration::from_secs(5),
|
|
827
|
+
};
|
|
828
|
+
assert!(times.is_warmup(times.warm_end - Duration::from_nanos(1)));
|
|
829
|
+
assert!(!times.is_warmup(times.warm_end));
|
|
830
|
+
assert!(!times.is_warmup(times.end));
|
|
831
|
+
assert!(!Timing {
|
|
832
|
+
warm_end: start,
|
|
833
|
+
..times
|
|
834
|
+
}
|
|
835
|
+
.is_warmup(start));
|
|
836
|
+
for (offset_ns, last_elapsed, expected) in [
|
|
837
|
+
(0, 0.0, None),
|
|
838
|
+
(1_000_000_000, 0.0, None),
|
|
839
|
+
(1_999_999_999, 0.0, None),
|
|
840
|
+
(2_000_000_000, 0.0, Some(1.0)),
|
|
841
|
+
(2_100_000_000, 1.0, None),
|
|
842
|
+
(2_500_000_000, 0.5, Some(1.5)),
|
|
843
|
+
(3_000_000_000, 1.0, Some(2.0)),
|
|
844
|
+
(5_000_000_000, 0.0, None),
|
|
845
|
+
(5_000_000_001, 0.0, None),
|
|
846
|
+
] {
|
|
847
|
+
assert_eq!(
|
|
848
|
+
times.sample_elapsed(start + Duration::from_nanos(offset_ns), last_elapsed),
|
|
849
|
+
expected
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
#[test]
|
|
855
|
+
fn cancelled_warmup_excludes_work_and_reports_elapsed_fraction() {
|
|
856
|
+
let _serial = TEST_LOCK.lock().unwrap();
|
|
857
|
+
let token = Cancellation::default();
|
|
858
|
+
let before = Instant::now();
|
|
859
|
+
let mut events = Vec::new();
|
|
860
|
+
let mut observed_work = false;
|
|
861
|
+
let result = run(
|
|
862
|
+
Benchmark::SingleCore,
|
|
863
|
+
Options {
|
|
864
|
+
warmup: Duration::from_secs(10),
|
|
865
|
+
measurement: Duration::from_secs(20),
|
|
866
|
+
},
|
|
867
|
+
&token,
|
|
868
|
+
|p| {
|
|
869
|
+
events.push(p);
|
|
870
|
+
if p.phase == Phase::Warmup && p.iterations > 0 {
|
|
871
|
+
observed_work = true;
|
|
872
|
+
token.cancel();
|
|
873
|
+
} else if p.phase == Phase::Measurement {
|
|
874
|
+
token.cancel();
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
);
|
|
878
|
+
assert!(observed_work, "warmup must publish its completed work");
|
|
879
|
+
assert_eq!(result.status, Status::Cancelled);
|
|
880
|
+
assert_eq!(
|
|
881
|
+
result.iterations, 0,
|
|
882
|
+
"warmup work must not enter measurement"
|
|
883
|
+
);
|
|
884
|
+
assert_eq!(result.elapsed_seconds, 0.0);
|
|
885
|
+
let final_event = events.last().unwrap();
|
|
886
|
+
assert_eq!(final_event.phase, Phase::Cancelled);
|
|
887
|
+
assert!(final_event.fraction > 0.0);
|
|
888
|
+
assert!(final_event.fraction <= before.elapsed().as_secs_f64() / 30.0 + 1e-12);
|
|
889
|
+
assert!(result.score.is_none() && result.pi_equivalent.is_none());
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
#[test]
|
|
893
|
+
fn observed_measurement_time_produces_periodic_samples_before_the_tail() {
|
|
894
|
+
let _serial = TEST_LOCK.lock().unwrap();
|
|
895
|
+
let token = Cancellation::default();
|
|
896
|
+
let mut observed_snapshot = None;
|
|
897
|
+
let mut previous_progress = None;
|
|
898
|
+
let result = run(
|
|
899
|
+
Benchmark::SingleCore,
|
|
900
|
+
Options {
|
|
901
|
+
warmup: Duration::ZERO,
|
|
902
|
+
measurement: Duration::from_secs(10),
|
|
903
|
+
},
|
|
904
|
+
&token,
|
|
905
|
+
|p| {
|
|
906
|
+
if p.phase == Phase::Measurement {
|
|
907
|
+
if let Some(previous) = previous_progress {
|
|
908
|
+
assert!(
|
|
909
|
+
p.phase_elapsed_seconds - previous >= 0.1 - 1e-9,
|
|
910
|
+
"same-phase progress must be throttled to 10 Hz"
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
previous_progress = Some(p.phase_elapsed_seconds);
|
|
914
|
+
}
|
|
915
|
+
if p.phase == Phase::Measurement && p.phase_elapsed_seconds >= 1.0 {
|
|
916
|
+
let first = *observed_snapshot.get_or_insert(p.phase_elapsed_seconds);
|
|
917
|
+
if p.phase_elapsed_seconds - first >= 0.2 {
|
|
918
|
+
token.cancel();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
},
|
|
922
|
+
);
|
|
923
|
+
assert_eq!(result.status, Status::Cancelled);
|
|
924
|
+
assert!(
|
|
925
|
+
result.samples.len() >= 2,
|
|
926
|
+
"an observed snapshot must precede the cancellation tail"
|
|
927
|
+
);
|
|
928
|
+
assert!(result.samples[0].elapsed_seconds <= observed_snapshot.unwrap());
|
|
929
|
+
assert!(result.samples[..result.samples.len() - 1]
|
|
930
|
+
.iter()
|
|
931
|
+
.all(|s| s.duration_seconds >= 1.0));
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
#[test]
|
|
935
|
+
fn callback_panic_stops_already_running_workers_before_the_deadline() {
|
|
936
|
+
let _serial = TEST_LOCK.lock().unwrap();
|
|
937
|
+
let token = Cancellation::default();
|
|
938
|
+
let cleanup = token.clone();
|
|
939
|
+
let (done_tx, done_rx) = mpsc::channel();
|
|
940
|
+
let worker = thread::spawn(move || {
|
|
941
|
+
let outcome = std::panic::catch_unwind(|| {
|
|
942
|
+
run(
|
|
943
|
+
Benchmark::SingleCore,
|
|
944
|
+
Options {
|
|
945
|
+
warmup: Duration::from_secs(60),
|
|
946
|
+
measurement: Duration::from_secs(60),
|
|
947
|
+
},
|
|
948
|
+
&token,
|
|
949
|
+
|p| {
|
|
950
|
+
if p.phase == Phase::Warmup && p.iterations > 0 {
|
|
951
|
+
panic!("callback fault after observed work");
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
)
|
|
955
|
+
});
|
|
956
|
+
done_tx.send(outcome.is_err()).unwrap();
|
|
957
|
+
});
|
|
958
|
+
let stopped = done_rx.recv_timeout(Duration::from_secs(5));
|
|
959
|
+
// Always unblock a broken worker before asserting, including on a mutant.
|
|
960
|
+
cleanup.cancel();
|
|
961
|
+
worker.join().unwrap();
|
|
962
|
+
assert!(stopped.unwrap());
|
|
963
|
+
assert_recovered();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
#[test]
|
|
967
|
+
fn counter_aggregation_preserves_measurement_work_max_finish_and_xor() {
|
|
968
|
+
let counters: Vec<_> = [
|
|
969
|
+
(50, 8, 2_000_000_000, 0b1100),
|
|
970
|
+
(500, 16, 1_500_000_000, 0b1010),
|
|
971
|
+
]
|
|
972
|
+
.into_iter()
|
|
973
|
+
.map(|(warmup, measurement, finish, checksum)| {
|
|
974
|
+
Arc::new(Counters {
|
|
975
|
+
warmup: AtomicU64::new(warmup),
|
|
976
|
+
measurement: AtomicU64::new(measurement),
|
|
977
|
+
finish_ns: AtomicU64::new(finish),
|
|
978
|
+
checksum: AtomicU64::new(checksum),
|
|
979
|
+
})
|
|
980
|
+
})
|
|
981
|
+
.collect();
|
|
982
|
+
let mut result = fresh_result();
|
|
983
|
+
collect_counters(&mut result, &counters);
|
|
984
|
+
assert_eq!(result.iterations, 24);
|
|
985
|
+
assert_eq!(result.elapsed_seconds, 2.0);
|
|
986
|
+
assert_eq!(result.checksum, 0b0110);
|
|
987
|
+
collect_counters(&mut result, &[]);
|
|
988
|
+
assert_eq!(
|
|
989
|
+
(result.iterations, result.elapsed_seconds, result.checksum),
|
|
990
|
+
(0, 0.0, 0)
|
|
991
|
+
);
|
|
992
|
+
}
|
|
@@ -371,6 +371,59 @@ struct BenchsetTests {
|
|
|
371
371
|
#expect(stub.creates == 2 && stub.frees == 2 && stub.cancels == 0)
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
@Test func cleanupWaitsForAnInFlightNativeCancellation() throws {
|
|
375
|
+
let stub = NativeStub()
|
|
376
|
+
let running = DispatchSemaphore(value: 0)
|
|
377
|
+
let releaseRun = DispatchSemaphore(value: 0)
|
|
378
|
+
let cancelling = DispatchSemaphore(value: 0)
|
|
379
|
+
let releaseCancel = DispatchSemaphore(value: 0)
|
|
380
|
+
let freed = DispatchSemaphore(value: 0)
|
|
381
|
+
let finished = DispatchSemaphore(value: 0)
|
|
382
|
+
let cancelDone = DispatchSemaphore(value: 0)
|
|
383
|
+
let cancelInFlight = LockedBox(false)
|
|
384
|
+
let output = LockedBox<Result<BenchmarkResult, Error>?>(nil)
|
|
385
|
+
stub.action = { _, _ in
|
|
386
|
+
running.signal()
|
|
387
|
+
#expect(releaseRun.wait(timeout: .now() + 5) == .success)
|
|
388
|
+
}
|
|
389
|
+
var native = stub.api
|
|
390
|
+
native.cancel = { handle in
|
|
391
|
+
#expect(handle == stub.pointer)
|
|
392
|
+
cancelInFlight.value = true
|
|
393
|
+
cancelling.signal()
|
|
394
|
+
#expect(releaseCancel.wait(timeout: .now() + 5) == .success)
|
|
395
|
+
cancelInFlight.value = false
|
|
396
|
+
}
|
|
397
|
+
native.free = { handle in
|
|
398
|
+
#expect(handle == stub.pointer)
|
|
399
|
+
#expect(!cancelInFlight.value, "free must not overlap a native call using the handle")
|
|
400
|
+
stub.frees += 1
|
|
401
|
+
freed.signal()
|
|
402
|
+
}
|
|
403
|
+
let runner = BenchmarkRunner(native: native)
|
|
404
|
+
defer { releaseRun.signal(); releaseCancel.signal() }
|
|
405
|
+
DispatchQueue.global().async {
|
|
406
|
+
output.value = Result { try runner.run(.singleCore) }
|
|
407
|
+
finished.signal()
|
|
408
|
+
}
|
|
409
|
+
#expect(running.wait(timeout: .now() + 5) == .success)
|
|
410
|
+
DispatchQueue.global().async {
|
|
411
|
+
runner.cancel()
|
|
412
|
+
cancelDone.signal()
|
|
413
|
+
}
|
|
414
|
+
#expect(cancelling.wait(timeout: .now() + 5) == .success)
|
|
415
|
+
releaseRun.signal()
|
|
416
|
+
// Give cleanup a chance to attempt free while cancellation owns the
|
|
417
|
+
// handle. The assertion in free checks actual overlap, not elapsed time.
|
|
418
|
+
_ = freed.wait(timeout: .now() + 0.2)
|
|
419
|
+
releaseCancel.signal()
|
|
420
|
+
#expect(cancelDone.wait(timeout: .now() + 5) == .success)
|
|
421
|
+
#expect(finished.wait(timeout: .now() + 5) == .success)
|
|
422
|
+
let result = try #require(output.value)
|
|
423
|
+
#expect(try result.get().success)
|
|
424
|
+
#expect(stub.frees == 1)
|
|
425
|
+
}
|
|
426
|
+
|
|
374
427
|
@Test func terminalCancellationKeepsCompletedResult() throws {
|
|
375
428
|
let runner = BenchmarkRunner()
|
|
376
429
|
let result = try runner.run(.singleCore, warmupSeconds: 0, measurementSeconds: 0.03) {
|
|
@@ -26,6 +26,10 @@ from benchset import (
|
|
|
26
26
|
cli,
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
# Content checks allow slow interpreter startup on shared runners. Cancellation
|
|
30
|
+
# and terminal-disconnect responsiveness retain their separate 5-second bounds.
|
|
31
|
+
CLI_PROCESS_TIMEOUT = 30
|
|
32
|
+
|
|
29
33
|
|
|
30
34
|
def assert_short_cli_completed_or_no_work(
|
|
31
35
|
result: subprocess.CompletedProcess, names: List[str], warmup: float, encoding: str = 'utf-8',
|
|
@@ -57,6 +61,7 @@ def assert_short_cli_completed_or_no_work(
|
|
|
57
61
|
assert float(fields['Measurement seconds']) == 0.03
|
|
58
62
|
assert fields['Unit'] == 'iterations/s'
|
|
59
63
|
if index < len(completed):
|
|
64
|
+
assert fields['Reason'] in ('—', '-', '\\u2014')
|
|
60
65
|
assert fields['Status'] == 'completed'
|
|
61
66
|
number, unit = fields['Pi equivalent'].split(' \u00d7 ' if encoding == 'utf-8' else ' x ')
|
|
62
67
|
assert float(number) > 0
|
|
@@ -163,10 +168,8 @@ def test_information_is_complete_and_has_no_stderr(
|
|
|
163
168
|
invocation: Tuple[List[str], Dict[str, str]], option: str,
|
|
164
169
|
) -> None:
|
|
165
170
|
command, environment = invocation
|
|
166
|
-
# Shared CI runners can delay interpreter startup; this checks content,
|
|
167
|
-
# while cancellation responsiveness has its own stricter deadlines.
|
|
168
171
|
result = subprocess.run([*command, option], env=environment, capture_output=True,
|
|
169
|
-
encoding='utf-8', check=False, timeout=
|
|
172
|
+
encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
170
173
|
assert result.returncode == 0, result.stderr
|
|
171
174
|
assert not result.stderr
|
|
172
175
|
if option == '--version':
|
|
@@ -200,7 +203,7 @@ def test_invalid_arguments_are_actionable_without_a_traceback(
|
|
|
200
203
|
) -> None:
|
|
201
204
|
command, environment = invocation
|
|
202
205
|
result = subprocess.run([*command, *arguments], env=environment, capture_output=True,
|
|
203
|
-
encoding='utf-8', check=False, timeout=
|
|
206
|
+
encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
204
207
|
assert result.returncode == 2
|
|
205
208
|
assert not result.stdout
|
|
206
209
|
assert 'usage: benchset' in result.stderr
|
|
@@ -223,13 +226,13 @@ def test_real_entrypoints_accept_selection_order_numeric_forms_and_redirected_ou
|
|
|
223
226
|
[f'--{name}={value}'] if equals else ['--' + name, value]
|
|
224
227
|
)]
|
|
225
228
|
result = subprocess.run([*command, *arguments], env=environment, capture_output=True,
|
|
226
|
-
check=False, timeout=
|
|
229
|
+
check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
227
230
|
names = ['single_core', 'multi_core'] if benchmark is None or benchmark == 'all' else [benchmark]
|
|
228
231
|
assert_short_cli_completed_or_no_work(result, names, float(warmup))
|
|
229
232
|
|
|
230
233
|
|
|
231
234
|
def test_defaults_repeated_options_and_environment_independence(
|
|
232
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
235
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str],
|
|
233
236
|
) -> None:
|
|
234
237
|
calls: List[Tuple[str, float, float]] = []
|
|
235
238
|
|
|
@@ -243,12 +246,94 @@ def test_defaults_repeated_options_and_environment_independence(
|
|
|
243
246
|
monkeypatch.setenv('BENCHSET_DURATION', '0')
|
|
244
247
|
assert cli.main([]) == 0
|
|
245
248
|
assert calls == [('single_core', 120, 60), ('multi_core', 120, 60)]
|
|
249
|
+
output = capsys.readouterr()
|
|
250
|
+
assert not output.err
|
|
251
|
+
sections = output.out.rstrip('\n').split('\n\n')
|
|
252
|
+
assert len(sections) == 2
|
|
253
|
+
assert sections[0].startswith('Benchmark')
|
|
254
|
+
assert sections[1].startswith('Benchmark')
|
|
255
|
+
assert output.out.endswith(' —\n')
|
|
246
256
|
calls.clear()
|
|
247
257
|
assert cli.main(['--bench=all', '--bench', 'multi_core', '--warmup=0.5', '--warmup=-0.0',
|
|
248
258
|
'--duration=2', '--duration', '+.03']) == 0
|
|
249
259
|
assert calls == [('multi_core', -0.0, 0.03)]
|
|
250
260
|
|
|
251
261
|
|
|
262
|
+
@pytest.mark.parametrize(('arguments', 'message'), [
|
|
263
|
+
(['--warmup=oops'], 'argument --warmup: must be a number of seconds'),
|
|
264
|
+
(['--duration=oops'], 'argument --duration: must be a number of seconds'),
|
|
265
|
+
(['--warmup', '-1e-3'], '--warmup must be finite and nonnegative'),
|
|
266
|
+
(['--warmup=nan'], '--warmup must be finite and nonnegative'),
|
|
267
|
+
(['--duration=0'], '--duration must be finite and positive'),
|
|
268
|
+
(['--duration', '-NaN'], '--duration must be finite and positive'),
|
|
269
|
+
])
|
|
270
|
+
def test_programmatic_cli_rejects_invalid_durations_with_option_diagnostic(
|
|
271
|
+
arguments: List[str], message: str, capsys: pytest.CaptureFixture[str],
|
|
272
|
+
) -> None:
|
|
273
|
+
assert cli.main(arguments) == 2
|
|
274
|
+
captured = capsys.readouterr()
|
|
275
|
+
assert not captured.out
|
|
276
|
+
assert captured.err.startswith('usage: benchset ')
|
|
277
|
+
assert captured.err.splitlines()[-1] == 'benchset: error: ' + message
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_cli_help_keeps_explanations_and_copyable_examples_on_separate_lines(
|
|
281
|
+
capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch,
|
|
282
|
+
) -> None:
|
|
283
|
+
monkeypatch.setenv('COLUMNS', '80')
|
|
284
|
+
# A calling application's argv must not change the CLI's displayed name.
|
|
285
|
+
monkeypatch.setattr(sys, 'argv', ['embedding-application'])
|
|
286
|
+
assert cli.main(['--help']) == 0
|
|
287
|
+
captured = capsys.readouterr()
|
|
288
|
+
assert not captured.err
|
|
289
|
+
paragraphs = captured.out.strip().split('\n\n')
|
|
290
|
+
assert paragraphs[0].startswith('usage: benchset ')
|
|
291
|
+
assert paragraphs[1] == 'Sustained scalar CPU throughput and speed relative to Raspberry Pi 5.'
|
|
292
|
+
assert paragraphs[-2].splitlines() == [
|
|
293
|
+
'Higher iterations/s is faster. Pi equivalent: 0.5 = half the Pi speed; 2 = twice.',
|
|
294
|
+
'With no options, runs single_core then multi_core (about 6 minutes total).',
|
|
295
|
+
'Ctrl+C cancels the run.',
|
|
296
|
+
'Exit codes: 0 success, 1 failed, 2 invalid arguments, 130 cancelled.',
|
|
297
|
+
]
|
|
298
|
+
assert paragraphs[-1].splitlines() == [
|
|
299
|
+
'Examples:',
|
|
300
|
+
' benchset --bench single_core',
|
|
301
|
+
' benchset --bench multi_core --warmup 120 --duration 60',
|
|
302
|
+
' benchset --warmup 0 --duration 1 # quick smoke test',
|
|
303
|
+
]
|
|
304
|
+
options = ' '.join(paragraphs[2].split())
|
|
305
|
+
for entry in (
|
|
306
|
+
'--bench {all,single_core,multi_core} Benchmark to run (default: all).',
|
|
307
|
+
'--warmup SECONDS Warmup excluded from the score (default: 120; zero allowed).',
|
|
308
|
+
'--duration SECONDS Measurement duration, must be positive (default: 60).',
|
|
309
|
+
):
|
|
310
|
+
assert entry in options
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@pytest.mark.parametrize('benchmark', ['single_core', 'multi_core'])
|
|
314
|
+
def test_cli_worker_exception_retains_benchmark_and_stops_the_suite(
|
|
315
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], benchmark: str,
|
|
316
|
+
) -> None:
|
|
317
|
+
calls: List[str] = []
|
|
318
|
+
|
|
319
|
+
def fail(_runner: BenchmarkRunner, name: str, **_options: object) -> BenchmarkResult:
|
|
320
|
+
calls.append(name)
|
|
321
|
+
raise RuntimeError('backend unavailable')
|
|
322
|
+
|
|
323
|
+
monkeypatch.setattr(BenchmarkRunner, 'run', fail)
|
|
324
|
+
assert cli.main(['--bench', benchmark]) == 1
|
|
325
|
+
captured = capsys.readouterr()
|
|
326
|
+
assert not captured.out
|
|
327
|
+
fields = {key.strip(): value for key, value in (
|
|
328
|
+
line.split(' : ', 1) for line in captured.err.split('Samples:\n')[0].splitlines()
|
|
329
|
+
)}
|
|
330
|
+
assert fields['Benchmark'] == benchmark
|
|
331
|
+
assert fields['Status'] == 'failed'
|
|
332
|
+
assert fields['Reason'] == 'backend unavailable'
|
|
333
|
+
assert fields['Score'] == fields['Pi equivalent'] == '—'
|
|
334
|
+
assert calls == [benchmark]
|
|
335
|
+
|
|
336
|
+
|
|
252
337
|
@pytest.mark.parametrize('arguments', [
|
|
253
338
|
['--warmup', '.0', '--duration', ' .03 '],
|
|
254
339
|
['--war=0', '--dur=3e-2', '--ben=single_core'],
|
|
@@ -268,7 +353,7 @@ def test_negative_zero_has_equivalent_cli_spellings(
|
|
|
268
353
|
command, environment = invocation
|
|
269
354
|
option = ['--warmup=' + value] if equals else ['--warmup', value]
|
|
270
355
|
result = subprocess.run([*command, '--bench=single_core', '--duration=.03', *option],
|
|
271
|
-
env=environment, capture_output=True, encoding='utf-8', check=False, timeout=
|
|
356
|
+
env=environment, capture_output=True, encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
272
357
|
assert_short_cli_completed_or_no_work(result, ['single_core'], float(value))
|
|
273
358
|
|
|
274
359
|
|
|
@@ -281,7 +366,7 @@ def test_negative_numeric_values_reach_their_option_validator(
|
|
|
281
366
|
) -> None:
|
|
282
367
|
command, environment = invocation
|
|
283
368
|
result = subprocess.run([*command, option, value], env=environment, capture_output=True,
|
|
284
|
-
encoding='utf-8', check=False, timeout=
|
|
369
|
+
encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
285
370
|
assert result.returncode == 2
|
|
286
371
|
assert not result.stdout
|
|
287
372
|
assert option in result.stderr
|
|
@@ -295,7 +380,7 @@ def test_real_redirected_output_supports_non_unicode_encodings(
|
|
|
295
380
|
command, environment = invocation
|
|
296
381
|
environment['PYTHONIOENCODING'] = encoding + ':strict'
|
|
297
382
|
result = subprocess.run([*command, '--bench=single_core', '--warmup=0', '--duration=.03'],
|
|
298
|
-
env=environment, capture_output=True, check=False, timeout=
|
|
383
|
+
env=environment, capture_output=True, check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
299
384
|
assert_short_cli_completed_or_no_work(result, ['single_core'], 0, encoding)
|
|
300
385
|
|
|
301
386
|
|
|
@@ -307,7 +392,7 @@ def test_native_duration_limits_report_failure_and_stop_the_suite(
|
|
|
307
392
|
) -> None:
|
|
308
393
|
command, environment = invocation
|
|
309
394
|
result = subprocess.run([*command, '--warmup=0', '--duration=.03', option, value],
|
|
310
|
-
env=environment, capture_output=True, encoding='utf-8', check=False, timeout=
|
|
395
|
+
env=environment, capture_output=True, encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
311
396
|
assert result.returncode == 1
|
|
312
397
|
assert not result.stdout
|
|
313
398
|
assert 'failed' in result.stderr
|
|
@@ -358,6 +443,12 @@ def test_result_calibration_links_keep_cli_output_compact(status: str) -> None:
|
|
|
358
443
|
assert text.count('Reference machine') == 1
|
|
359
444
|
assert f'{result.reference_machine.name} ({result.reference_machine.short_name})' in text
|
|
360
445
|
assert 'Pi equivalent' in text
|
|
446
|
+
fields = {key.strip(): value for key, value in (
|
|
447
|
+
line.split(' : ', 1) for line in text.split('Samples:\n')[0].splitlines()
|
|
448
|
+
)}
|
|
449
|
+
for name in ('Reason', 'Score', 'Pi equivalent', 'Variation percent'):
|
|
450
|
+
assert fields[name] == '—'
|
|
451
|
+
assert text.endswith('Samples:\n —\n')
|
|
361
452
|
for nested_metadata in ('reference_machine', 'reference_run', 'ReferenceMachine(', 'ReferenceRun(',
|
|
362
453
|
'started_at_unix_seconds', 'start_temperature_c', 'warmup_seconds=1248'):
|
|
363
454
|
assert nested_metadata not in text
|
|
@@ -503,7 +594,7 @@ def test_real_terminal_disconnect_has_execution_failure_exit_without_traceback(
|
|
|
503
594
|
os.close(slave)
|
|
504
595
|
try:
|
|
505
596
|
observed = b''
|
|
506
|
-
deadline = time.monotonic() +
|
|
597
|
+
deadline = time.monotonic() + CLI_PROCESS_TIMEOUT
|
|
507
598
|
while b'\rsingle_core: measurement' not in observed and time.monotonic() < deadline:
|
|
508
599
|
readable, _writeable, _errors = select.select([master], [], [], 0.1)
|
|
509
600
|
if readable:
|
|
@@ -552,7 +643,7 @@ def test_closed_output_pipe_exits_without_traceback_or_shutdown_warning(
|
|
|
552
643
|
stdout=writer if destination != 'stderr' else subprocess.PIPE,
|
|
553
644
|
stderr=writer if destination == 'stderr' else (
|
|
554
645
|
subprocess.STDOUT if destination == 'merged' else subprocess.PIPE
|
|
555
|
-
), check=False, timeout=
|
|
646
|
+
), check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
556
647
|
finally:
|
|
557
648
|
os.close(writer)
|
|
558
649
|
assert result.returncode == 1
|
|
@@ -610,7 +701,7 @@ else:
|
|
|
610
701
|
command = [sys.executable, '-c', program, ','.join(map(str, closed)),
|
|
611
702
|
'module' if len(command) > 1 else command[0], str(completed_fixture)]
|
|
612
703
|
result = subprocess.run([*command, *arguments], env=environment, capture_output=True,
|
|
613
|
-
encoding='utf-8', check=False, timeout=
|
|
704
|
+
encoding='utf-8', check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
614
705
|
assert result.returncode == status, (result.returncode, result.stdout, result.stderr)
|
|
615
706
|
assert 'Traceback' not in result.stderr
|
|
616
707
|
assert 'Exception ignored' not in result.stderr
|
|
@@ -697,7 +788,7 @@ raise SystemExit(main(sys.argv[1:]))
|
|
|
697
788
|
'--unknown' if destination == 'stderr' else '--version'],
|
|
698
789
|
env=environment, stdout=sink if destination != 'stderr' else subprocess.PIPE,
|
|
699
790
|
stderr=sink if destination != 'stdout' else subprocess.PIPE,
|
|
700
|
-
text=True, check=False, timeout=
|
|
791
|
+
text=True, check=False, timeout=CLI_PROCESS_TIMEOUT)
|
|
701
792
|
assert result.returncode == 1
|
|
702
793
|
assert 'Traceback' not in (result.stderr or '')
|
|
703
794
|
assert 'Exception ignored' not in (result.stderr or '')
|
|
@@ -716,6 +807,24 @@ def test_error_report_write_failure_does_not_escape(monkeypatch: pytest.MonkeyPa
|
|
|
716
807
|
assert cli.main([]) == 1
|
|
717
808
|
|
|
718
809
|
|
|
810
|
+
def test_missing_stdout_still_redirects_a_broken_stderr_at_shutdown(
|
|
811
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
812
|
+
) -> None:
|
|
813
|
+
class Broken(io.StringIO):
|
|
814
|
+
def flush(self) -> None:
|
|
815
|
+
raise OSError('destination is unavailable')
|
|
816
|
+
|
|
817
|
+
def fileno(self) -> int:
|
|
818
|
+
return 42
|
|
819
|
+
|
|
820
|
+
redirects: List[int] = []
|
|
821
|
+
monkeypatch.setattr(cli.sys, 'stdout', None)
|
|
822
|
+
monkeypatch.setattr(cli.sys, 'stderr', Broken())
|
|
823
|
+
monkeypatch.setattr(cli.os, 'dup2', lambda _source, target: redirects.append(target))
|
|
824
|
+
assert cli.main([]) == 1
|
|
825
|
+
assert redirects == [42]
|
|
826
|
+
|
|
827
|
+
|
|
719
828
|
@pytest.mark.skipif(os.name == 'nt', reason='POSIX terminal signals; Windows cancellation has direct tests')
|
|
720
829
|
@pytest.mark.parametrize('phase', ['warmup', 'measurement'])
|
|
721
830
|
def test_real_ctrl_c_cancels_each_phase_and_stops_the_suite(
|
|
@@ -733,7 +842,7 @@ def test_real_ctrl_c_cancels_each_phase_and_stops_the_suite(
|
|
|
733
842
|
os.close(slave)
|
|
734
843
|
try:
|
|
735
844
|
observed = b''
|
|
736
|
-
deadline = time.monotonic() +
|
|
845
|
+
deadline = time.monotonic() + CLI_PROCESS_TIMEOUT
|
|
737
846
|
while phase.encode() not in observed and time.monotonic() < deadline:
|
|
738
847
|
readable, _writeable, _errors = select.select([master], [], [], 0.1)
|
|
739
848
|
if readable:
|
|
@@ -198,23 +198,28 @@ class ManagedNative(Native):
|
|
|
198
198
|
|
|
199
199
|
|
|
200
200
|
@pytest.mark.parametrize('entrypoint', ['runner', 'run', 'run_all'])
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
@pytest.mark.parametrize('durations', [None, (1.2345678901234567, 9.876543210987654)])
|
|
202
|
+
def test_public_api_forwards_durations_and_callback_without_running_long_benchmarks(
|
|
203
|
+
monkeypatch: pytest.MonkeyPatch, entrypoint: str, durations: Optional[Tuple[float, float]],
|
|
203
204
|
) -> None:
|
|
204
205
|
backend = ManagedNative()
|
|
205
206
|
monkeypatch.setattr(api, 'Native', lambda: backend)
|
|
207
|
+
options = {} if durations is None else {'warmup_seconds': durations[0], 'measurement_seconds': durations[1]}
|
|
208
|
+
expected_durations = (120.0, 60.0) if durations is None else durations
|
|
209
|
+
events: List[Progress] = []
|
|
206
210
|
results: Tuple[BenchmarkResult, ...]
|
|
207
211
|
if entrypoint == 'runner':
|
|
208
|
-
results = (BenchmarkRunner().run('single_core'),)
|
|
212
|
+
results = (BenchmarkRunner().run('single_core', on_progress=events.append, **options),)
|
|
209
213
|
elif entrypoint == 'run':
|
|
210
|
-
results = (run('single_core'),)
|
|
214
|
+
results = (run('single_core', on_progress=events.append, **options),)
|
|
211
215
|
else:
|
|
212
|
-
results = run_all()
|
|
216
|
+
results = run_all(on_progress=events.append, **options)
|
|
213
217
|
for result in results:
|
|
214
218
|
assert_result_references(result)
|
|
215
219
|
benchmarks = ['single_core', 'multi_core'] if entrypoint == 'run_all' else ['single_core']
|
|
216
220
|
assert [(result.benchmark, result.warmup_seconds, result.measurement_seconds)
|
|
217
|
-
for result in results] == [(benchmark,
|
|
221
|
+
for result in results] == [(benchmark, *expected_durations) for benchmark in benchmarks]
|
|
222
|
+
assert [event.benchmark for event in events] == benchmarks
|
|
218
223
|
assert all(result.success for result in results)
|
|
219
224
|
assert backend.calls == ['create', 'execute', 'free'] * len(benchmarks)
|
|
220
225
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|