Skip to content

Commit a5a74e9

Browse files
feat: validate walltime results before uploading
1 parent 5f7dffc commit a5a74e9

3 files changed

Lines changed: 275 additions & 0 deletions

File tree

src/run/runner/wall_time/executor.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::helpers::validate_walltime_results;
12
use super::perf::PerfRunner;
23
use crate::prelude::*;
34
use crate::run::RunnerMode;
@@ -224,6 +225,8 @@ impl Executor for WallTimeExecutor {
224225
perf.save_files_to(&run_data.profile_folder).await?;
225226
}
226227

228+
validate_walltime_results(&run_data.profile_folder)?;
229+
227230
Ok(())
228231
}
229232
}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
use crate::prelude::*;
2+
use runner_shared::walltime_results::WalltimeResults;
3+
use std::path::Path;
4+
5+
fn add_empty_result_error_explanation(error_details: &str) -> String {
6+
format!(
7+
"The command did not produce any CodSpeed result to process, did you run your benchmarks using a compatible CodSpeed integration?\n\
8+
Check out https://codspeed.io/docs/benchmarks/overview for more information.\n\n\
9+
Details: {error_details}"
10+
)
11+
}
12+
13+
/// Validates that walltime results exist and contain at least one benchmark.
14+
pub fn validate_walltime_results(profile_folder: &Path) -> Result<()> {
15+
let results_dir = profile_folder.join("results");
16+
17+
if !results_dir.exists() {
18+
bail!(add_empty_result_error_explanation(&format!(
19+
"No walltime results found in profile folder: {results_dir:?}."
20+
)));
21+
}
22+
23+
debug!("Validating walltime results in {results_dir:?}");
24+
25+
let mut found_valid_results = false;
26+
27+
for entry in std::fs::read_dir(&results_dir)? {
28+
let entry = entry?;
29+
let path = entry.path();
30+
31+
// Only process JSON files
32+
if path.extension().and_then(|s| s.to_str()) != Some("json") {
33+
continue;
34+
}
35+
36+
debug!("Parsing walltime results from {path:?}");
37+
let file = std::fs::File::open(&path)
38+
.with_context(|| format!("Failed to open walltime results file: {path:?}"))?;
39+
40+
let results: WalltimeResults = serde_json::from_reader(&file)
41+
.with_context(|| format!("Failed to parse walltime results from: {path:?}"))?;
42+
43+
if results.benchmarks.is_empty() {
44+
bail!(add_empty_result_error_explanation(&format!(
45+
"No benchmarks found in walltime results file: {path:?}."
46+
)));
47+
}
48+
49+
found_valid_results = true;
50+
debug!(
51+
"Found {} benchmark(s) in {path:?}",
52+
results.benchmarks.len()
53+
);
54+
}
55+
56+
if !found_valid_results {
57+
bail!(add_empty_result_error_explanation(&format!(
58+
"No JSON result files found in: {results_dir:?}."
59+
)));
60+
}
61+
62+
Ok(())
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use super::*;
68+
use std::fs;
69+
use std::io::Write;
70+
use tempfile::TempDir;
71+
72+
// Test helpers
73+
struct TestProfileFolder {
74+
temp_dir: TempDir,
75+
}
76+
77+
impl TestProfileFolder {
78+
fn new() -> Self {
79+
Self {
80+
temp_dir: TempDir::new().unwrap(),
81+
}
82+
}
83+
84+
fn path(&self) -> &Path {
85+
self.temp_dir.path()
86+
}
87+
88+
fn results_dir(&self) -> std::path::PathBuf {
89+
self.path().join("results")
90+
}
91+
92+
fn create_results_dir(&self) {
93+
fs::create_dir_all(self.results_dir()).unwrap();
94+
}
95+
96+
fn write_json_file(&self, filename: &str, content: &str) {
97+
self.create_results_dir();
98+
let file_path = self.results_dir().join(filename);
99+
let mut file = fs::File::create(file_path).unwrap();
100+
file.write_all(content.as_bytes()).unwrap();
101+
}
102+
103+
fn write_text_file(&self, filename: &str, content: &str) {
104+
self.create_results_dir();
105+
let file_path = self.results_dir().join(filename);
106+
let mut file = fs::File::create(file_path).unwrap();
107+
file.write_all(content.as_bytes()).unwrap();
108+
}
109+
}
110+
111+
fn valid_walltime_results_json(benchmark_count: usize) -> String {
112+
let benchmarks: Vec<String> = (0..benchmark_count)
113+
.map(|i| {
114+
format!(
115+
r#"{{
116+
"name": "bench_{i}",
117+
"uri": "test.rs::bench_{i}",
118+
"config": {{}},
119+
"stats": {{
120+
"min_ns": 100.0,
121+
"max_ns": 200.0,
122+
"mean_ns": 150.0,
123+
"stdev_ns": 10.0,
124+
"q1_ns": 140.0,
125+
"median_ns": 150.0,
126+
"q3_ns": 160.0,
127+
"rounds": 100,
128+
"total_time": 15000.0,
129+
"iqr_outlier_rounds": 0,
130+
"stdev_outlier_rounds": 0,
131+
"iter_per_round": 1,
132+
"warmup_iters": 10
133+
}}
134+
}}"#
135+
)
136+
})
137+
.collect();
138+
139+
format!(
140+
r#"{{
141+
"creator": {{
142+
"name": "test",
143+
"version": "1.0.0",
144+
"pid": 12345
145+
}},
146+
"instrument": {{
147+
"type": "walltime"
148+
}},
149+
"benchmarks": [{}]
150+
}}"#,
151+
benchmarks.join(",")
152+
)
153+
}
154+
155+
fn empty_benchmarks_json() -> String {
156+
r#"{
157+
"creator": {
158+
"name": "test",
159+
"version": "1.0.0",
160+
"pid": 12345
161+
},
162+
"instrument": {
163+
"type": "walltime"
164+
},
165+
"benchmarks": []
166+
}"#
167+
.to_string()
168+
}
169+
170+
// Success cases
171+
172+
#[test]
173+
fn test_valid_single_result_file() {
174+
let profile = TestProfileFolder::new();
175+
profile.write_json_file("results.json", &valid_walltime_results_json(1));
176+
177+
let result = validate_walltime_results(profile.path());
178+
assert!(result.is_ok());
179+
}
180+
181+
#[test]
182+
fn test_valid_multiple_result_files() {
183+
let profile = TestProfileFolder::new();
184+
profile.write_json_file("results1.json", &valid_walltime_results_json(2));
185+
profile.write_json_file("results2.json", &valid_walltime_results_json(3));
186+
187+
let result = validate_walltime_results(profile.path());
188+
assert!(result.is_ok());
189+
}
190+
191+
#[test]
192+
fn test_ignores_non_json_files() {
193+
let profile = TestProfileFolder::new();
194+
profile.write_json_file("results.json", &valid_walltime_results_json(1));
195+
profile.write_text_file("readme.txt", "This is a text file");
196+
profile.write_text_file("data.csv", "col1,col2");
197+
198+
let result = validate_walltime_results(profile.path());
199+
assert!(result.is_ok());
200+
}
201+
202+
// Failure cases
203+
204+
#[test]
205+
fn test_missing_results_directory() {
206+
let profile = TestProfileFolder::new();
207+
// Don't create results directory
208+
209+
let result = validate_walltime_results(profile.path());
210+
assert!(result.is_err());
211+
let error = result.unwrap_err().to_string();
212+
assert!(error.contains("No walltime results found in profile folder"));
213+
}
214+
215+
#[test]
216+
fn test_empty_results_directory() {
217+
let profile = TestProfileFolder::new();
218+
profile.create_results_dir();
219+
220+
let result = validate_walltime_results(profile.path());
221+
assert!(result.is_err());
222+
let error = result.unwrap_err().to_string();
223+
assert!(error.contains("No JSON result files found in"));
224+
}
225+
226+
#[test]
227+
fn test_no_json_files_in_directory() {
228+
let profile = TestProfileFolder::new();
229+
profile.write_text_file("readme.txt", "some text");
230+
profile.write_text_file("data.csv", "col1,col2");
231+
232+
let result = validate_walltime_results(profile.path());
233+
assert!(result.is_err());
234+
let error = result.unwrap_err().to_string();
235+
assert!(error.contains("No JSON result files found in"));
236+
}
237+
238+
#[test]
239+
fn test_empty_benchmarks_array() {
240+
let profile = TestProfileFolder::new();
241+
profile.write_json_file("results.json", &empty_benchmarks_json());
242+
243+
let result = validate_walltime_results(profile.path());
244+
assert!(result.is_err());
245+
let error = result.unwrap_err().to_string();
246+
assert!(error.contains("No benchmarks found in walltime results file"));
247+
}
248+
249+
#[test]
250+
fn test_invalid_json_format() {
251+
let profile = TestProfileFolder::new();
252+
profile.write_json_file("results.json", "{ invalid json }");
253+
254+
let result = validate_walltime_results(profile.path());
255+
assert!(result.is_err());
256+
let error = result.unwrap_err().to_string();
257+
assert!(error.contains("Failed to parse walltime results from"));
258+
}
259+
260+
#[test]
261+
fn test_multiple_files_one_empty() {
262+
let profile = TestProfileFolder::new();
263+
profile.write_json_file("results1.json", &valid_walltime_results_json(2));
264+
profile.write_json_file("results2.json", &empty_benchmarks_json());
265+
266+
let result = validate_walltime_results(profile.path());
267+
assert!(result.is_err());
268+
let error = result.unwrap_err().to_string();
269+
assert!(error.contains("No benchmarks found in walltime results file"));
270+
}
271+
}

src/run/runner/wall_time/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod executor;
2+
pub mod helpers;
23
pub mod perf;

0 commit comments

Comments
 (0)