Skip to content

Commit 205c0b5

Browse files
committed
feat: add go-compatible CLI
1 parent 0808aef commit 205c0b5

File tree

4 files changed

+278
-0
lines changed

4 files changed

+278
-0
lines changed

go-runner/src/builder/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod discovery;
2+
pub mod patcher;
3+
pub mod templater;
4+
pub mod verifier;
5+
6+
pub use discovery::*;

go-runner/src/cli.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#[derive(Debug)]
2+
pub enum CliExit {
3+
Help,
4+
Version,
5+
MissingArgument,
6+
UnknownFlag,
7+
}
8+
9+
#[derive(Debug)]
10+
pub struct Cli {
11+
/// Run only benchmarks matching regexp
12+
pub bench: String,
13+
}
14+
15+
impl Cli {
16+
pub fn parse() -> Self {
17+
match Self::parse_args(std::env::args().skip(1)) {
18+
Ok(cli) => cli,
19+
Err(CliExit::Help) => std::process::exit(0),
20+
Err(CliExit::Version) => std::process::exit(0),
21+
Err(CliExit::MissingArgument) => std::process::exit(2),
22+
Err(CliExit::UnknownFlag) => std::process::exit(1),
23+
}
24+
}
25+
26+
fn parse_args(mut args: impl Iterator<Item = String>) -> Result<Self, CliExit> {
27+
let mut bench = ".".to_string();
28+
29+
// We currently only support the `test` subcommand.
30+
let cmd = args.next();
31+
assert!(
32+
cmd == Some("test".to_string()),
33+
"Expected 'test' as the first argument, got {cmd:?}",
34+
);
35+
36+
while let Some(arg) = args.next() {
37+
match arg.as_str() {
38+
"-h" | "--help" => {
39+
println!(
40+
"\
41+
The Codspeed Go Benchmark Runner
42+
43+
USAGE:
44+
go-runner test [OPTIONS]
45+
46+
OPTIONS:
47+
-bench <pattern> Run only benchmarks matching regexp (defaults to '.')
48+
-h, --help Print help information
49+
-V, --version Print version information"
50+
);
51+
return Err(CliExit::Help);
52+
}
53+
"-V" | "--version" => {
54+
println!("{}", env!("CARGO_PKG_VERSION"));
55+
return Err(CliExit::Version);
56+
}
57+
"-bench" => {
58+
bench = args.next().ok_or_else(|| {
59+
eprintln!("error: `-bench` requires a pattern");
60+
CliExit::MissingArgument
61+
})?;
62+
}
63+
s if s.starts_with("-bench=") => {
64+
bench = s.split_once('=').unwrap().1.to_string();
65+
}
66+
67+
s if s.starts_with('-') => {
68+
eprintln!("Unknown flag: {s}");
69+
return Err(CliExit::UnknownFlag);
70+
}
71+
72+
_ => {
73+
eprintln!(
74+
"warning: package arguments are not currently supported, ignoring '{arg}'"
75+
);
76+
// Consume and ignore all remaining arguments
77+
for remaining_arg in args {
78+
eprintln!(
79+
"warning: package arguments are not currently supported, ignoring '{remaining_arg}'"
80+
);
81+
}
82+
break;
83+
}
84+
}
85+
}
86+
Ok(Self { bench })
87+
}
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use super::*;
93+
94+
fn str_to_iter(cmd: &str) -> Result<Cli, CliExit> {
95+
let args: Vec<String> = if cmd.trim().is_empty() {
96+
Vec::new()
97+
} else {
98+
cmd.split_whitespace()
99+
.map(|s| s.to_string())
100+
.skip(1)
101+
.collect()
102+
};
103+
Cli::parse_args(args.into_iter())
104+
}
105+
106+
#[test]
107+
fn test_cli_parse_defaults() {
108+
let cli = str_to_iter("go-runner test").unwrap();
109+
assert_eq!(cli.bench, ".");
110+
}
111+
112+
#[test]
113+
fn test_cli_parse_with_bench_flag() {
114+
let cli = str_to_iter("go-runner test -bench Test").unwrap();
115+
assert_eq!(cli.bench, "Test");
116+
117+
let cli = str_to_iter("go-runner test -bench=BenchmarkFoo").unwrap();
118+
assert_eq!(cli.bench, "BenchmarkFoo");
119+
}
120+
121+
#[test]
122+
fn test_cli_parse_ignores_packages() {
123+
let cli = str_to_iter("go-runner test package1 package2").unwrap();
124+
assert_eq!(cli.bench, ".");
125+
}
126+
127+
#[test]
128+
fn test_cli_parse_help_flag() {
129+
let result = str_to_iter("go-runner test -h");
130+
assert!(matches!(result, Err(CliExit::Help)));
131+
132+
let result = str_to_iter("go-runner test --help");
133+
assert!(matches!(result, Err(CliExit::Help)));
134+
}
135+
136+
#[test]
137+
fn test_cli_parse_version_flag() {
138+
let result = str_to_iter("go-runner test -V");
139+
assert!(matches!(result, Err(CliExit::Version)));
140+
141+
let result = str_to_iter("go-runner test --version");
142+
assert!(matches!(result, Err(CliExit::Version)));
143+
}
144+
145+
#[test]
146+
fn test_cli_parse_invalid() {
147+
let result = str_to_iter("go-runner test -bench");
148+
assert!(matches!(result, Err(CliExit::MissingArgument)));
149+
150+
let result = str_to_iter("go-runner test -unknown");
151+
assert!(matches!(result, Err(CliExit::UnknownFlag)));
152+
}
153+
}

go-runner/src/lib.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use crate::{builder::BenchmarkPackage, prelude::*};
2+
use std::{
3+
collections::HashMap,
4+
path::{Path, PathBuf},
5+
};
6+
7+
mod builder;
8+
pub mod cli;
9+
pub mod prelude;
10+
mod results;
11+
mod runner;
12+
pub(crate) mod utils;
13+
14+
#[cfg(test)]
15+
mod integration_tests;
16+
17+
/// Builds and runs the specified Go project benchmarks, writing results to the .codspeed folder.
18+
pub fn run_benchmarks(project_dir: &Path, bench: &str) -> anyhow::Result<()> {
19+
let profile_dir = std::env::var("CODSPEED_PROFILE_FOLDER")
20+
.context("CODSPEED_PROFILE_FOLDER env var not set")?;
21+
std::fs::remove_dir_all(&profile_dir).ok();
22+
23+
// 1. Build phase - Benchmark and package discovery
24+
let packages = BenchmarkPackage::from_project(project_dir)?;
25+
info!("Discovered {} packages", packages.len());
26+
27+
let mut bench_name_to_path = HashMap::new();
28+
for package in &packages {
29+
for benchmark in &package.benchmarks {
30+
bench_name_to_path.insert(benchmark.name.clone(), benchmark.file_path.clone());
31+
}
32+
}
33+
34+
let total_benchmarks: usize = packages.iter().map(|p| p.benchmarks.len()).sum();
35+
info!("Total benchmarks discovered: {total_benchmarks}");
36+
for (name, path) in &bench_name_to_path {
37+
info!("Found {name:30} in {path:?}");
38+
}
39+
40+
// 2. Generate codspeed runners and execute them
41+
for package in &packages {
42+
info!("Generating custom runner for package: {}", package.name);
43+
let (_target_dir, runner_path) = builder::templater::run(package)?;
44+
45+
let args = [
46+
"-test.bench",
47+
bench,
48+
// Use a single iteration in tests to speed up execution, otherwise use 5 seconds
49+
"-test.benchtime",
50+
if cfg!(test) || std::env::var("CODSPEED_ENV").is_err() {
51+
"1x"
52+
} else {
53+
"5s"
54+
},
55+
];
56+
57+
info!("Running benchmarks for package: {}", package.name);
58+
runner::run(&runner_path, &args)?;
59+
}
60+
61+
// 3. Collect the results
62+
collect_walltime_results(bench_name_to_path)?;
63+
64+
Ok(())
65+
}
66+
67+
// TODO: This should be merged with codspeed-rust/codspeed/walltime_results.rs
68+
fn collect_walltime_results(bench_name_to_path: HashMap<String, PathBuf>) -> anyhow::Result<()> {
69+
let profile_dir = std::env::var("CODSPEED_PROFILE_FOLDER")
70+
.context("CODSPEED_PROFILE_FOLDER env var not set")?;
71+
let profile_dir = PathBuf::from(&profile_dir);
72+
let raw_results = results::raw_result::RawResult::parse_folder(&profile_dir)?;
73+
info!("Parsed {} raw results", raw_results.len());
74+
75+
let mut benchmarks_by_pid: HashMap<u32, Vec<results::walltime_results::WalltimeBenchmark>> =
76+
HashMap::new();
77+
for raw in raw_results {
78+
let file_path = bench_name_to_path
79+
.get(&raw.benchmark_name)
80+
.map(|p| p.to_string_lossy().to_string());
81+
benchmarks_by_pid
82+
.entry(raw.pid)
83+
.or_default()
84+
.push(raw.into_walltime_benchmark(file_path));
85+
}
86+
87+
for (pid, walltime_benchmarks) in benchmarks_by_pid {
88+
let creator = results::walltime_results::Creator {
89+
name: "codspeed-go".into(),
90+
version: env!("CARGO_PKG_VERSION").into(),
91+
pid,
92+
};
93+
let results_dir = profile_dir.join("results");
94+
std::fs::create_dir_all(&results_dir)?;
95+
96+
let results_file = results_dir.join(format!("{pid}.json"));
97+
let walltime_results =
98+
results::walltime_results::WalltimeResults::new(walltime_benchmarks, creator)?;
99+
std::fs::write(&results_file, serde_json::to_string(&walltime_results)?)?;
100+
info!("Results written to {results_file:?}");
101+
}
102+
103+
Ok(())
104+
}

go-runner/src/main.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use go_runner::cli::Cli;
2+
use std::path::Path;
3+
4+
fn main() -> anyhow::Result<()> {
5+
env_logger::builder()
6+
.parse_env("CODSPEED_LOG")
7+
.filter_module("handlebars", log::LevelFilter::Off)
8+
.format_timestamp(None)
9+
.init();
10+
11+
let cli = Cli::parse();
12+
go_runner::run_benchmarks(Path::new("."), &cli.bench)?;
13+
14+
Ok(())
15+
}

0 commit comments

Comments
 (0)