diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81204ed2..355f50f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,9 @@ repos: args: [--all-targets] - id: clippy args: [--all-targets, --, -D, warnings] + - repo: local + hooks: + - id: check-config-schema + name: check config schema is up to date + entry: bash -c 'cargo run --bin generate-config-schema && git diff --exit-code schemas/codspeed.schema.json' + language: system diff --git a/Cargo.lock b/Cargo.lock index 578c22ca..e98b30fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,7 @@ dependencies = [ "rstest 0.25.0", "rstest_reuse", "runner-shared", + "schemars", "semver", "serde", "serde_json", @@ -691,6 +692,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.15.0" @@ -2661,6 +2668,30 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.111", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2730,6 +2761,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "serde_json" version = "1.0.145" diff --git a/Cargo.toml b/Cargo.toml index 3c8453fa..aae4d38d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,15 @@ version = "4.9.0" edition = "2024" repository = "https://github.com/CodSpeedHQ/codspeed" publish = false +default-run = "codspeed" [[bin]] name = "codspeed" path = "src/main.rs" +[[bin]] +name = "generate-config-schema" +path = "src/bin/generate_config_schema.rs" [dependencies] anyhow = { workspace = true } @@ -27,6 +31,7 @@ reqwest = { version = "0.11.22", features = [ ] } reqwest-middleware = "0.2.4" reqwest-retry = "0.3.0" +schemars = { workspace = true } serde = { workspace = true } serde_json = { workspace = true, features = ["preserve_order"] } url = "2.4.1" @@ -91,6 +96,7 @@ anyhow = "1.0" clap = { version = "4.5", features = ["derive", "env"] } libc = "0.2" log = "0.4.28" +schemars = "0.8" serde_json = "1.0" serde = { version = "1.0.228", features = ["derive"] } ipc-channel = "0.18" @@ -119,3 +125,5 @@ strip = true [package.metadata.dist] targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +binaries.aarch64-unknown-linux-musl = ["codspeed"] +binaries.x86_64-unknown-linux-musl = ["codspeed"] diff --git a/schemas/codspeed.schema.json b/schemas/codspeed.schema.json new file mode 100644 index 00000000..181180bf --- /dev/null +++ b/schemas/codspeed.schema.json @@ -0,0 +1,159 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProjectConfig", + "description": "Project-level configuration from codspeed.yaml file\n\nThis configuration provides default options for the run and exec commands. CLI arguments always take precedence over config file values.", + "type": "object", + "properties": { + "options": { + "description": "Default options to apply to all benchmark runs", + "anyOf": [ + { + "$ref": "#/definitions/ProjectOptions" + }, + { + "type": "null" + } + ] + }, + "targets": { + "description": "List of benchmark targets to execute", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + }, + "definitions": { + "ProjectOptions": { + "description": "Root-level options that apply to all benchmark runs unless overridden by CLI", + "type": "object", + "properties": { + "max-rounds": { + "description": "Maximum number of rounds", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "max-time": { + "description": "Maximum total execution time", + "type": [ + "string", + "null" + ] + }, + "min-rounds": { + "description": "Minimum number of rounds", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "min-time": { + "description": "Minimum total execution time", + "type": [ + "string", + "null" + ] + }, + "warmup-time": { + "description": "Duration of warmup phase (e.g., \"1s\", \"500ms\")", + "type": [ + "string", + "null" + ] + }, + "working-directory": { + "description": "Working directory where commands will be executed (relative to config file)", + "type": [ + "string", + "null" + ] + } + } + }, + "Target": { + "description": "A benchmark target to execute", + "type": "object", + "required": [ + "exec" + ], + "properties": { + "exec": { + "description": "Command to execute", + "type": "string" + }, + "name": { + "description": "Optional name for this target", + "type": [ + "string", + "null" + ] + }, + "options": { + "description": "Target-specific options", + "anyOf": [ + { + "$ref": "#/definitions/TargetOptions" + }, + { + "type": "null" + } + ] + } + } + }, + "TargetOptions": { + "description": "Walltime execution options matching WalltimeExecutionArgs structure", + "type": "object", + "properties": { + "max-rounds": { + "description": "Maximum number of rounds", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "max-time": { + "description": "Maximum total execution time", + "type": [ + "string", + "null" + ] + }, + "min-rounds": { + "description": "Minimum number of rounds", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "min-time": { + "description": "Minimum total execution time", + "type": [ + "string", + "null" + ] + }, + "warmup-time": { + "description": "Duration of warmup phase (e.g., \"1s\", \"500ms\")", + "type": [ + "string", + "null" + ] + } + } + } + } +} diff --git a/src/bin/generate_config_schema.rs b/src/bin/generate_config_schema.rs new file mode 100644 index 00000000..72996a08 --- /dev/null +++ b/src/bin/generate_config_schema.rs @@ -0,0 +1,23 @@ +//! Generates JSON Schema for codspeed.yaml configuration file +//! +//! Run with: +//! ``` +//! cargo run --bin generate-config-schema +//! ``` + +use std::fs; + +use codspeed_runner::ProjectConfig; +use schemars::schema_for; + +const OUTPUT_FILE: &str = "schemas/codspeed.schema.json"; + +fn main() { + let schema = schema_for!(ProjectConfig); + let schema_json = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema"); + let output_file_path = std::path::Path::new(OUTPUT_FILE); + fs::create_dir_all(output_file_path.parent().unwrap()) + .expect("Failed to create schemas directory"); + fs::write(OUTPUT_FILE, format!("{schema_json}\n")).expect("Failed to write schema file"); + println!("Schema written to {OUTPUT_FILE}"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..ad36f9c0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,38 @@ +//! CodSpeed Runner library + +mod api_client; +pub mod app; +mod auth; +mod binary_installer; +mod config; +mod exec; +mod executor; +mod instruments; +mod local_logger; +mod logger; +mod prelude; +mod project_config; +mod request_client; +mod run; +mod run_environment; +mod runner_mode; +mod setup; + +pub use local_logger::clean_logger; +pub use project_config::{ProjectConfig, ProjectOptions, Target, TargetOptions, WalltimeOptions}; +pub use runner_mode::RunnerMode; + +use lazy_static::lazy_static; +use semver::Version; + +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const MONGODB_TRACER_VERSION: &str = "cs-mongo-tracer-v0.2.0"; + +pub const VALGRIND_CODSPEED_VERSION: Version = Version::new(3, 26, 0); +pub const VALGRIND_CODSPEED_DEB_REVISION_SUFFIX: &str = "0codspeed0"; +lazy_static! { + pub static ref VALGRIND_CODSPEED_VERSION_STRING: String = + format!("{VALGRIND_CODSPEED_VERSION}.codspeed"); + pub static ref VALGRIND_CODSPEED_DEB_VERSION: String = + format!("{VALGRIND_CODSPEED_VERSION}-{VALGRIND_CODSPEED_DEB_REVISION_SUFFIX}"); +} diff --git a/src/main.rs b/src/main.rs index 8ff00e69..ace11d2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,55 +1,21 @@ -mod api_client; -mod app; -mod auth; -mod binary_installer; -mod config; -mod exec; -mod executor; -mod instruments; -mod local_logger; -mod logger; -mod prelude; -mod project_config; -mod request_client; -mod run; -mod run_environment; -mod runner_mode; -mod setup; - +use codspeed_runner::{app, clean_logger}; use console::style; -use lazy_static::lazy_static; -use local_logger::clean_logger; -use prelude::*; -use semver::Version; - use log::log_enabled; -pub const VERSION: &str = env!("CARGO_PKG_VERSION"); -pub const MONGODB_TRACER_VERSION: &str = "cs-mongo-tracer-v0.2.0"; - -pub const VALGRIND_CODSPEED_VERSION: Version = Version::new(3, 26, 0); -pub const VALGRIND_CODSPEED_DEB_REVISION_SUFFIX: &str = "0codspeed0"; -lazy_static! { - pub static ref VALGRIND_CODSPEED_VERSION_STRING: String = - format!("{VALGRIND_CODSPEED_VERSION}.codspeed"); - pub static ref VALGRIND_CODSPEED_DEB_VERSION: String = - format!("{VALGRIND_CODSPEED_VERSION}-{VALGRIND_CODSPEED_DEB_REVISION_SUFFIX}"); -} - #[tokio::main(flavor = "current_thread")] async fn main() { - let res = crate::app::run().await; + let res = app::run().await; if let Err(err) = res { for cause in err.chain() { if log_enabled!(log::Level::Error) { - error!("{} {}", style("Error:").bold().red(), style(cause).red()); + log::error!("{} {}", style("Error:").bold().red(), style(cause).red()); } else { eprintln!("Error: {cause}"); } } if log_enabled!(log::Level::Debug) { for e in err.chain().skip(1) { - debug!("Caused by: {e}"); + log::debug!("Caused by: {e}"); } } clean_logger(); diff --git a/src/project_config/interfaces.rs b/src/project_config/interfaces.rs index 6271a256..4418dcf6 100644 --- a/src/project_config/interfaces.rs +++ b/src/project_config/interfaces.rs @@ -1,11 +1,11 @@ -use crate::runner_mode::RunnerMode; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Project-level configuration from codspeed.yaml file /// /// This configuration provides default options for the run and exec commands. /// CLI arguments always take precedence over config file values. -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct ProjectConfig { /// Default options to apply to all benchmark runs @@ -15,7 +15,7 @@ pub struct ProjectConfig { } /// A benchmark target to execute -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct Target { /// Optional name for this target @@ -26,7 +26,7 @@ pub struct Target { pub options: Option, } -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct TargetOptions { #[serde(flatten)] @@ -34,20 +34,18 @@ pub struct TargetOptions { } /// Root-level options that apply to all benchmark runs unless overridden by CLI -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct ProjectOptions { /// Working directory where commands will be executed (relative to config file) pub working_directory: Option, - /// Runner mode (walltime, memory, or simulation) - pub mode: Option, /// Walltime execution configuration (flattened) #[serde(flatten)] pub walltime: Option, } /// Walltime execution options matching WalltimeExecutionArgs structure -#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Deserialize, Serialize, PartialEq, JsonSchema)] #[serde(rename_all = "kebab-case")] pub struct WalltimeOptions { /// Duration of warmup phase (e.g., "1s", "500ms") diff --git a/src/project_config/merger.rs b/src/project_config/merger.rs index d7b730aa..3f19bec2 100644 --- a/src/project_config/merger.rs +++ b/src/project_config/merger.rs @@ -173,7 +173,6 @@ mod tests { let config = ProjectOptions { walltime: None, working_directory: Some("./config-dir".to_string()), - mode: Some(RunnerMode::Simulation), }; let merged = ConfigMerger::merge_shared_args(&cli, Some(&config)); @@ -206,7 +205,6 @@ mod tests { let config = ProjectOptions { walltime: None, working_directory: Some("./config-dir".to_string()), - mode: Some(RunnerMode::Memory), }; let merged = ConfigMerger::merge_shared_args(&cli, Some(&config)); diff --git a/src/project_config/mod.rs b/src/project_config/mod.rs index d5ccc6d6..0cf6338a 100644 --- a/src/project_config/mod.rs +++ b/src/project_config/mod.rs @@ -173,7 +173,6 @@ impl ProjectConfig { #[cfg(test)] mod tests { use super::*; - use crate::runner_mode::RunnerMode; use tempfile::TempDir; #[test] @@ -202,7 +201,6 @@ options: max-rounds: 100 min-rounds: 10 working-directory: ./bench - mode: walltime "#; let config: ProjectConfig = serde_yaml::from_str(yaml).unwrap(); let options = config.options.unwrap(); @@ -214,7 +212,6 @@ options: assert_eq!(walltime.max_rounds, Some(100)); assert_eq!(walltime.min_rounds, Some(10)); assert_eq!(options.working_directory, Some("./bench".to_string())); - assert_eq!(options.mode, Some(RunnerMode::Walltime)); } #[test] @@ -236,7 +233,6 @@ options: min_rounds: None, }), working_directory: None, - mode: None, }), targets: None, }; @@ -263,7 +259,6 @@ options: min_rounds: Some(5), }), working_directory: None, - mode: None, }), targets: None, }; @@ -290,7 +285,6 @@ options: min_rounds: None, }), working_directory: Some("./bench".to_string()), - mode: Some(RunnerMode::Walltime), }), targets: None, };