harness_cli/configs/
harness.rs

1//! The harness benchmarking configs
2//!
3//! This should be placed in the `[package.metadata.harness]` section of the `Cargo.toml` file.
4//!
5//! If the `harness` section is not present, a default config will be created, which contains
6//! a default profile, with two builds: `HEAD` pointing to the current commit, and `HEAD~1` pointing to the previous commit.
7//!
8//! # Example:
9//!
10//! The following example defines a `default` profile.
11//!
12//! Note that the `default` profile will be used by the runner by default,
13//! if no profile name is specified when running `cargo harness run`.
14//!
15//! ```toml
16//! [package.metadata.harness.profiles.default]
17//! iterations = 3 # Optional. Default to 5
18//! invocations = 40 # Optional. Default to 10
19//! # Additional environment variables to set for all builds and benchmarks
20//! # Optional. Default to no additional environment variables
21//! env = { BAR = "BAZ" }
22//!
23//! # The list of builds to evaluate.
24//! # If not specified, two builds `HEAD` and `HEAD~1` will be evaluated by default.
25//! [package.metadata.harness.profiles.default.builds]
26//! # No extra build configurations.
27//! # Default cargo features and the current git commit will be used to produce the build.
28//! # No extra environment variables will be set.
29//! foo = {}
30//! # Another build with extra cargo features, and disabled default features
31//! bar = { features = ["unstable"], default-features = false }
32//! # Extra environment variables only for this build.
33//! baz = { env = { "FOO" = "BAR" } }
34//! # Compile this build with a specific git commit.
35//! qux = { commit = "a1b2c3d4e5f6" }
36//! ````
37use std::{collections::HashMap, path::PathBuf};
38
39use serde::{Deserialize, Serialize};
40use toml::Table;
41
42/// The information we care in a Cargo.toml
43#[derive(Deserialize)]
44pub(crate) struct CargoConfig {
45    /// The package section of the Cargo.toml
46    package: CargoConfigPackage,
47    /// The bench list of the Cargo.toml
48    #[serde(default)]
49    bench: Vec<CargoBenchConfig>,
50    /// Other fields
51    #[serde(flatten)]
52    _others: HashMap<String, toml::Value>,
53}
54
55impl CargoConfig {
56    /// Load the Cargo.toml file
57    fn load_cargo_toml() -> anyhow::Result<CargoConfig> {
58        if !PathBuf::from("./Cargo.toml").is_file() {
59            anyhow::bail!("Failed to load ./Cargo.toml");
60        }
61        let s = std::fs::read_to_string("./Cargo.toml")?;
62        Ok(toml::from_str::<CargoConfig>(&s)?)
63    }
64
65    pub(crate) fn load_benches() -> anyhow::Result<Vec<String>> {
66        Ok(Self::load_cargo_toml()?
67            .bench
68            .iter()
69            .filter_map(|b| {
70                if !b.harness {
71                    Some(b.name.clone())
72                } else {
73                    None
74                }
75            })
76            .collect())
77    }
78}
79
80/// The package section of the Cargo.toml
81#[derive(Deserialize)]
82struct CargoConfigPackage {
83    /// The custom metadata section of the Cargo.toml
84    metadata: Option<CargoConfigPackageMetadata>,
85    /// Other fields
86    #[serde(flatten)]
87    _others: HashMap<String, toml::Value>,
88}
89
90/// The bench item of the bench list in the Cargo.toml
91#[derive(Deserialize)]
92struct CargoBenchConfig {
93    /// bench name
94    name: String,
95    /// we only care about benches with `harness=false`
96    #[serde(default)]
97    harness: bool,
98    /// Other fields
99    #[serde(flatten)]
100    _others: HashMap<String, toml::Value>,
101}
102
103/// The custom metadata section of the Cargo.toml
104#[derive(Deserialize)]
105struct CargoConfigPackageMetadata {
106    /// The harness config
107    harness: Option<HarnessConfig>,
108    #[serde(flatten)]
109    _others: HashMap<String, toml::Value>,
110}
111
112/// The harness configuration.
113///
114/// This should be placed in the `[package.metadata.harness]` section of the `Cargo.toml` file.
115///
116#[derive(Serialize, Deserialize, Debug)]
117pub struct HarnessConfig {
118    /// Custom project name. Default to the crate name.
119    pub project: Option<String>,
120    /// Evaluation profiles
121    pub profiles: HashMap<String, Profile>,
122}
123
124impl HarnessConfig {
125    /// Load the harness configuration from the `Cargo.toml` file
126    /// If the `harness` section is not present, a default config with a default profile is returned.
127    pub fn load_from_cargo_toml() -> anyhow::Result<HarnessConfig> {
128        if !PathBuf::from("./Cargo.toml").is_file() {
129            anyhow::bail!("Failed to load ./Cargo.toml");
130        }
131        let s = std::fs::read_to_string("./Cargo.toml")?;
132        let mut harness = toml::from_str::<CargoConfig>(&s)?
133            .package
134            .metadata
135            .and_then(|m| m.harness)
136            .unwrap_or_default();
137        if harness.profiles.is_empty() {
138            harness
139                .profiles
140                .insert("default".to_owned(), Default::default());
141        }
142        Ok(harness)
143    }
144}
145
146impl Default for HarnessConfig {
147    fn default() -> Self {
148        Self {
149            project: None,
150            profiles: [("default".to_owned(), Default::default())]
151                .into_iter()
152                .collect(),
153        }
154    }
155}
156
157fn default_iterations() -> usize {
158    5
159}
160
161fn default_invocations() -> usize {
162    10
163}
164
165/// The benchmarking profile.
166///
167/// A harness config can contain multiple profiles, each with a unique name.
168///
169/// The `default` profile will be used by the runner by default.
170#[derive(Serialize, Deserialize, Debug, Clone)]
171pub struct Profile {
172    /// Enabled probes and their configurations. The configuration must be a TOML table (e.g. `example_probe = { param = "42" }`).
173    #[serde(default)]
174    pub probes: HashMap<String, Table>,
175    /// Environment variables to set to all builds and benchmarks
176    #[serde(default)]
177    pub env: HashMap<String, String>,
178    /// Builds to evaluate
179    #[serde(default)]
180    pub builds: HashMap<String, BuildConfig>,
181    /// Number of iterations. Default is 5
182    #[serde(default = "default_iterations")]
183    pub iterations: usize,
184    /// Number of invocations. Default is 10
185    #[serde(default = "default_invocations")]
186    pub invocations: usize,
187}
188
189impl Default for Profile {
190    fn default() -> Self {
191        Self {
192            probes: HashMap::new(),
193            env: HashMap::new(),
194            builds: HashMap::new(),
195            iterations: default_iterations(),
196            invocations: default_invocations(),
197        }
198    }
199}
200
201fn default_true() -> bool {
202    true
203}
204
205/// The build configuration used for evaluation
206#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
207pub struct BuildConfig {
208    /// Extra cargo features used for compilation. Default to no extra features.
209    #[serde(default)]
210    pub features: Vec<String>,
211    /// Whether to use default features. Default to `true`
212    #[serde(default = "default_true", rename = "default-features")]
213    pub default_features: bool,
214    /// Environment variables to set. Default to no extra environment variables.
215    #[serde(default)]
216    pub env: HashMap<String, String>,
217    /// The commit used to produce the build. Default to the current commit.
218    #[serde(default)]
219    pub commit: Option<String>,
220}
221
222impl Default for BuildConfig {
223    fn default() -> Self {
224        Self {
225            features: Vec::new(),
226            default_features: true,
227            env: HashMap::new(),
228            commit: None,
229        }
230    }
231}