1
1
//! Finds all the benchmarks and packages in a given Go project.
2
2
3
3
use std:: {
4
- collections :: HashMap ,
4
+ ops :: Deref ,
5
5
path:: { Path , PathBuf } ,
6
6
process:: Command ,
7
7
} ;
@@ -11,7 +11,7 @@ use crate::prelude::*;
11
11
use serde:: { Deserialize , Serialize } ;
12
12
13
13
#[ derive( Debug , Clone , Serialize , Deserialize ) ]
14
- pub struct Module {
14
+ pub struct GoModule {
15
15
/// The module path (e.g., "local.dev/example-complex").
16
16
#[ serde( rename = "Path" ) ]
17
17
pub path : String ,
@@ -58,7 +58,7 @@ pub struct GoPackage {
58
58
pub compiled_go_files : Option < Vec < String > > ,
59
59
60
60
#[ serde( rename = "Module" ) ]
61
- pub module : Module ,
61
+ pub module : GoModule ,
62
62
}
63
63
64
64
impl GoPackage {
@@ -70,15 +70,7 @@ impl GoPackage {
70
70
serde_json:: from_str ( & format ! ( "[{output}]" ) ) . context ( "Failed to parse Go list output" )
71
71
}
72
72
73
- pub fn name ( & self ) -> & str {
74
- & self . name
75
- }
76
-
77
- pub fn test_go_files ( & self ) -> Option < & Vec < String > > {
78
- self . test_go_files . as_ref ( )
79
- }
80
-
81
- pub fn benchmarks ( & self ) -> anyhow:: Result < Vec < BenchmarkData > > {
73
+ fn benchmarks ( & self ) -> anyhow:: Result < Vec < GoBenchmark > > {
82
74
let Some ( test_go_files) = & self . test_go_files else {
83
75
bail ! ( "No test files found for package: {}" , self . name) ;
84
76
} ;
@@ -100,25 +92,25 @@ impl GoPackage {
100
92
} ;
101
93
102
94
// We don't support files with benchmarks that also contains github.com/frankban/quicktest
103
- if file
104
- . imports
105
- . iter ( )
106
- . any ( |import| import. path . value . contains ( "github.com/frankban/quicktest" ) )
107
- {
108
- warn ! ( "Skipping file with quicktest import: {file_path:?}" ) ;
109
- continue ;
110
- }
111
-
112
- // We don't support testify yet
113
- // FIXME: We shouldn't skip packages where benchmarks and tests are separated -> opentelemetry-go
114
- if file
115
- . imports
116
- . iter ( )
117
- . any ( |import| import. path . value . contains ( "github.com/stretchr/testify" ) )
118
- {
119
- warn ! ( "Skipping file with testify import: {file_path:?}" ) ;
120
- continue ;
121
- }
95
+ // if file
96
+ // .imports
97
+ // .iter()
98
+ // .any(|import| import.path.value.contains("github.com/frankban/quicktest"))
99
+ // {
100
+ // warn!("Skipping file with quicktest import: {file_path:?}");
101
+ // continue;
102
+ // }
103
+
104
+ // // We don't support testify yet
105
+ // // FIXME: We shouldn't skip packages where benchmarks and tests are separated -> opentelemetry-go
106
+ // if file
107
+ // .imports
108
+ // .iter()
109
+ // .any(|import| import.path.value.contains("github.com/stretchr/testify"))
110
+ // {
111
+ // warn!("Skipping file with testify import: {file_path:?}");
112
+ // continue;
113
+ // }
122
114
123
115
// We can't import packages that are declared as `main`
124
116
if file. pkg_name . name == "main" {
@@ -159,8 +151,26 @@ impl GoPackage {
159
151
160
152
let valid_benchmarks =
161
153
verifier:: FuncVisitor :: verify_source_code ( & content, & found_benchmarks) ?;
154
+ if valid_benchmarks. len ( ) != found_benchmarks. len ( ) {
155
+ warn ! (
156
+ "Only {} out of {} are valid, skipping file" ,
157
+ valid_benchmarks. len( ) ,
158
+ found_benchmarks. len( )
159
+ ) ;
160
+ warn ! ( "Valid benchmarks: {valid_benchmarks:?}" ) ;
161
+ warn ! (
162
+ "Invalid benchmarks: {:?}" ,
163
+ found_benchmarks
164
+ . iter( )
165
+ . filter( |f| !valid_benchmarks. contains( f) )
166
+ . collect:: <Vec <_>>( )
167
+ ) ;
168
+
169
+ continue ;
170
+ }
171
+
162
172
for func in valid_benchmarks {
163
- benchmarks. push ( BenchmarkData :: new (
173
+ benchmarks. push ( GoBenchmark :: new (
164
174
package_import_path. clone ( ) ,
165
175
func,
166
176
root_relative_file_path. to_path_buf ( ) ,
@@ -173,7 +183,7 @@ impl GoPackage {
173
183
}
174
184
175
185
#[ derive( Debug , Clone , Serialize , Deserialize ) ]
176
- pub struct BenchmarkData {
186
+ pub struct GoBenchmark {
177
187
/// The name of the benchmark (e.g. `BenchmarkFoo`).
178
188
pub name : String ,
179
189
@@ -190,7 +200,7 @@ pub struct BenchmarkData {
190
200
pub file_path : PathBuf ,
191
201
}
192
202
193
- impl BenchmarkData {
203
+ impl GoBenchmark {
194
204
pub fn new ( package_import_path : String , name : String , file_path : PathBuf ) -> Self {
195
205
#[ cfg( test) ]
196
206
let import_alias = {
@@ -213,143 +223,129 @@ impl BenchmarkData {
213
223
}
214
224
}
215
225
216
- fn run_go_list ( go_project_path : & Path ) -> anyhow:: Result < Vec < GoPackage > > {
217
- // Execute 'go list -test -compiled -json ./...' to get package information
218
- let output = Command :: new ( "go" )
219
- . args ( [ "list" , "-test" , "-compiled" , "-json" , "./..." ] )
220
- . current_dir ( go_project_path)
221
- . output ( ) ?;
222
-
223
- if !output. status . success ( ) {
224
- bail ! (
225
- "Failed to execute 'go list': {}" ,
226
- String :: from_utf8_lossy( & output. stderr)
227
- ) ;
226
+ /// Represents a package with its benchmarks.
227
+ #[ derive( Debug , Clone , Serialize ) ]
228
+ pub struct BenchmarkPackage {
229
+ raw_package : GoPackage ,
230
+ pub benchmarks : Vec < GoBenchmark > ,
231
+ }
232
+
233
+ impl BenchmarkPackage {
234
+ fn new ( package : GoPackage , benchmarks : Vec < GoBenchmark > ) -> Self {
235
+ Self {
236
+ raw_package : package,
237
+ benchmarks,
238
+ }
228
239
}
229
240
230
- // Wrap it in '[{output}]' and parse it with serde_json
231
- let output_str = String :: from_utf8 ( output. stdout ) ?;
232
- trace ! ( "Go list output: {output_str}" ) ;
241
+ pub fn from_project ( go_project_path : & Path ) -> anyhow:: Result < Vec < BenchmarkPackage > > {
242
+ let raw_packages = Self :: run_go_list ( go_project_path) ?;
243
+ let has_test_files =
244
+ |files : & Vec < String > | files. iter ( ) . any ( |name| name. ends_with ( "_test.go" ) ) ;
245
+ let has_test_imports = |imports : & Vec < String > | {
246
+ imports. iter ( ) . any ( |import| {
247
+ // import "testing"
248
+ import. contains ( "testing" )
249
+ } )
250
+ } ;
233
251
234
- GoPackage :: from_go_list_output ( & output_str)
235
- }
252
+ let mut packages = Vec :: new ( ) ;
253
+ for package in raw_packages {
254
+ // Skip packages without test files
255
+ let has_tests = package
256
+ . test_go_files
257
+ . as_ref ( )
258
+ . map ( has_test_files)
259
+ . unwrap_or_default ( ) ;
260
+ if !has_tests {
261
+ debug ! ( "Skipping package without test files: {}" , package. name) ;
262
+ continue ;
263
+ }
236
264
237
- pub fn run ( go_project_path : & Path ) -> anyhow:: Result < HashMap < String , GoPackage > > {
238
- let packages = run_go_list ( go_project_path) ?;
265
+ // Skip packages without test imports
266
+ let has_test_imports = package
267
+ . test_imports
268
+ . as_ref ( )
269
+ . map ( has_test_imports)
270
+ . unwrap_or_default ( ) ;
271
+ if !has_test_imports {
272
+ debug ! ( "Skipping package without test imports: {}" , package. name) ;
273
+ continue ;
274
+ }
239
275
240
- discover_benchmarks ( packages)
241
- }
276
+ // Only include test executables, since we want to generate them manually.
277
+ // Example format: `local.dev/example-complex [local.dev/example-complex.test]`
278
+ if !package. import_path . ends_with ( ".test]" ) {
279
+ debug ! ( "Skipping package without test executable: {}" , package. name) ;
280
+ continue ;
281
+ }
242
282
243
- fn discover_benchmarks ( packages : Vec < GoPackage > ) -> anyhow:: Result < HashMap < String , GoPackage > > {
244
- let has_test_files = |files : & Vec < String > | files. iter ( ) . any ( |name| name. ends_with ( "_test.go" ) ) ;
245
- let has_test_imports = |imports : & Vec < String > | {
246
- imports. iter ( ) . any ( |import| {
247
- // import "testing"
248
- import. contains ( "testing" )
249
- } )
250
- } ;
251
-
252
- let mut benchmark_packages: HashMap < String , GoPackage > = HashMap :: new ( ) ;
253
- for package in packages {
254
- // Skip packages without test files
255
- let has_tests = package
256
- . test_go_files
257
- . as_ref ( )
258
- . map ( has_test_files)
259
- . unwrap_or_default ( ) ;
260
- if !has_tests {
261
- debug ! ( "Skipping package without test files: {}" , package. name) ;
262
- continue ;
263
- }
283
+ // Skip packages that don't have benchmarks
284
+ let benchmarks = match package. benchmarks ( ) {
285
+ Ok ( benchmarks) => benchmarks,
286
+ Err ( e) => {
287
+ warn ! (
288
+ "Failed to get benchmarks for package {}: {}" ,
289
+ package. name, e
290
+ ) ;
291
+ continue ;
292
+ }
293
+ } ;
294
+ if benchmarks. is_empty ( ) {
295
+ debug ! ( "Skipping package without benchmarks: {}" , package. name) ;
296
+ continue ;
297
+ }
264
298
265
- // Skip packages without test imports
266
- let has_test_imports = package
267
- . test_imports
268
- . as_ref ( )
269
- . map ( has_test_imports)
270
- . unwrap_or_default ( ) ;
271
- if !has_test_imports {
272
- debug ! ( "Skipping package without test imports: {}" , package. name) ;
273
- continue ;
299
+ packages. push ( BenchmarkPackage :: new ( package, benchmarks) ) ;
274
300
}
275
301
276
- // Only include test executables, since we want to generate them manually.
277
- // Example format: `local.dev/example-complex [local.dev/example-complex.test]`
278
- if !package. import_path . ends_with ( ".test]" ) {
279
- debug ! ( "Skipping package without test executable: {}" , package. name) ;
280
- continue ;
281
- }
302
+ Ok ( packages)
303
+ }
282
304
283
- // Skip packages that don't have benchmarks
284
- let benchmarks = match package. benchmarks ( ) {
285
- Ok ( benchmarks) => benchmarks,
286
- Err ( e) => {
287
- warn ! (
288
- "Failed to get benchmarks for package {}: {}" ,
289
- package. name, e
290
- ) ;
291
- continue ;
292
- }
293
- } ;
294
- if benchmarks. is_empty ( ) {
295
- debug ! ( "Skipping package without benchmarks: {}" , package. name) ;
296
- continue ;
305
+ fn run_go_list ( go_project_path : & Path ) -> anyhow:: Result < Vec < GoPackage > > {
306
+ // Execute 'go list -test -compiled -json ./...' to get package information
307
+ let output = Command :: new ( "go" )
308
+ . args ( [ "list" , "-test" , "-compiled" , "-json" , "./..." ] )
309
+ . current_dir ( go_project_path)
310
+ . output ( ) ?;
311
+
312
+ if !output. status . success ( ) {
313
+ bail ! (
314
+ "Failed to execute 'go list': {}" ,
315
+ String :: from_utf8_lossy( & output. stderr)
316
+ ) ;
297
317
}
298
318
299
- assert ! ( !benchmark_packages. contains_key( & package. import_path) ) ;
300
- benchmark_packages. insert ( package. import_path . clone ( ) , package) ;
301
- // // FIXME: Assert that we didn't already add this package
302
- // benchmark_packages
303
- // .entry(package.import_path.clone())
304
- // .insert_entry(package);
319
+ // Wrap it in '[{output}]' and parse it with serde_json
320
+ let output_str = String :: from_utf8 ( output. stdout ) ?;
321
+ trace ! ( "Go list output: {output_str}" ) ;
322
+
323
+ GoPackage :: from_go_list_output ( & output_str)
305
324
}
325
+ }
306
326
307
- Ok ( benchmark_packages)
327
+ impl Deref for BenchmarkPackage {
328
+ type Target = GoPackage ;
329
+
330
+ fn deref ( & self ) -> & Self :: Target {
331
+ & self . raw_package
332
+ }
308
333
}
309
334
310
335
#[ cfg( test) ]
311
336
mod tests {
312
337
use super :: * ;
313
338
314
- #[ test]
315
- fn test_parse_go_list_output ( ) {
316
- let packages = run_go_list ( Path :: new ( "testdata/projects/golang-benchmarks" ) ) . unwrap ( ) ;
317
-
318
- insta:: with_settings!( { sort_maps => true } , {
319
- insta:: assert_json_snapshot!( packages, {
320
- ".**[\" Dir\" ]" => "[package_dir]" ,
321
- ".**[\" Module\" ][\" Dir\" ]" => "[module_dir]" ,
322
- ".**[\" Module\" ][\" GoMod\" ]" => "[go_mod_path]"
323
- } ) ;
324
- } ) ;
325
- }
326
-
327
339
#[ test]
328
340
fn test_discover_benchmarks ( ) {
329
- let packages = run_go_list ( Path :: new ( "testdata/projects/golang-benchmarks" ) ) . unwrap ( ) ;
330
- let packages = discover_benchmarks ( packages) . unwrap ( ) ;
331
-
332
- insta:: with_settings!( { sort_maps => true } , {
333
- insta:: assert_json_snapshot!( packages, {
334
- ".**[\" Dir\" ]" => "[package_dir]" ,
335
- ".**[\" Module\" ][\" Dir\" ]" => "[module_dir]" ,
336
- ".**[\" Module\" ][\" GoMod\" ]" => "[go_mod_path]"
337
- } ) ;
341
+ let packages =
342
+ BenchmarkPackage :: from_project ( Path :: new ( "testdata/projects/golang-benchmarks" ) )
343
+ . unwrap ( ) ;
344
+
345
+ insta:: assert_json_snapshot!( packages, {
346
+ ".**[\" Dir\" ]" => "[package_dir]" ,
347
+ ".**[\" Module\" ][\" Dir\" ]" => "[module_dir]" ,
348
+ ".**[\" Module\" ][\" GoMod\" ]" => "[go_mod_path]"
338
349
} ) ;
339
350
}
340
-
341
- #[ test]
342
- fn test_benchmarks_for_package ( ) {
343
- let packages = run_go_list ( Path :: new ( "testdata/projects/golang-benchmarks" ) ) . unwrap ( ) ;
344
- let benchmark_packages = discover_benchmarks ( packages) . unwrap ( ) ;
345
-
346
- let package = benchmark_packages
347
- . get ( "github.com/SimonWaldherr/golang-benchmarks/base64 [github.com/SimonWaldherr/golang-benchmarks/base64.test]" )
348
- . unwrap ( ) ;
349
-
350
- let mut benches = package. benchmarks ( ) . unwrap ( ) ;
351
- benches. sort_by_cached_key ( |b| b. name . clone ( ) ) ;
352
-
353
- insta:: assert_json_snapshot!( benches) ;
354
- }
355
351
}
0 commit comments