Skip to content

Commit 74985bd

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

3 files changed

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

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)