1use anyhow::{Context, Result, bail};
13use serde::Deserialize;
14use std::collections::BTreeMap;
15use std::path::{Path, PathBuf};
16
17pub const FILE_NAME: &str = "tak.toml";
18
19pub const DEFAULT_RUNS: u32 = 20;
22pub const DEFAULT_WARMUP: u32 = 3;
23
24#[derive(Debug, Deserialize)]
25pub struct Config {
26 #[serde(default)]
29 pub bench: BTreeMap<String, Bench>,
30 #[serde(default)]
33 pub env: Option<EnvSection>,
34}
35
36#[derive(Debug, Deserialize, Default, Clone, PartialEq, Eq)]
43pub struct EnvSection {
44 pub allow: Option<Vec<String>>,
45 pub deny: Option<Vec<String>>,
46}
47
48#[derive(Debug, Deserialize)]
49pub struct Bench {
50 cmd: Cmd,
51 pub runs: Option<u32>,
52 pub warmup: Option<u32>,
53}
54
55#[derive(Debug, Deserialize)]
57#[serde(untagged)]
58enum Cmd {
59 Argv(Vec<String>),
60 Line(String),
61}
62
63impl Bench {
64 pub fn argv(&self) -> Result<Vec<String>> {
72 let v = match &self.cmd {
73 Cmd::Argv(v) => v.clone(),
74 Cmd::Line(s) => s.split_whitespace().map(str::to_string).collect(),
75 };
76 if v.is_empty() {
77 bail!("empty command");
78 }
79 Ok(v)
80 }
81
82 pub fn runs(&self) -> u32 {
83 self.runs.unwrap_or(DEFAULT_RUNS)
84 }
85
86 pub fn warmup(&self) -> u32 {
87 self.warmup.unwrap_or(DEFAULT_WARMUP)
88 }
89}
90
91impl Config {
92 pub fn parse(text: &str) -> Result<Self> {
93 let cfg: Config = toml::from_str(text).context("could not parse tak.toml")?;
94 for (name, b) in &cfg.bench {
97 b.argv()
98 .with_context(|| format!("benchmark `{name}` has no command"))?;
99 }
100 Ok(cfg)
101 }
102
103 pub fn find(start: &Path) -> Result<Option<(PathBuf, Self)>> {
108 for dir in start.ancestors() {
109 let path = dir.join(FILE_NAME);
110 if path.is_file() {
111 let text = std::fs::read_to_string(&path)
112 .with_context(|| format!("could not read {}", path.display()))?;
113 let cfg = Self::parse(&text).with_context(|| format!("in {}", path.display()))?;
114 return Ok(Some((path, cfg)));
115 }
116 }
117 Ok(None)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn a_command_may_be_a_list_or_a_string() {
127 let c = Config::parse(
128 r#"
129 [bench.a]
130 cmd = ["mycli", "--version"]
131 [bench.b]
132 cmd = "mycli --help"
133 "#,
134 )
135 .unwrap();
136 assert_eq!(c.bench["a"].argv().unwrap(), ["mycli", "--version"]);
137 assert_eq!(c.bench["b"].argv().unwrap(), ["mycli", "--help"]);
138 }
139
140 #[test]
143 fn a_string_command_gets_no_shell_semantics() {
144 let c = Config::parse(
145 r#"[bench.a]
146cmd = "mycli 'two words'""#,
147 )
148 .unwrap();
149 assert_eq!(c.bench["a"].argv().unwrap(), ["mycli", "'two", "words'"]);
150 }
151
152 #[test]
153 fn defaults_match_the_cli() {
154 let c = Config::parse("[bench.a]\ncmd = \"x\"").unwrap();
155 assert_eq!(c.bench["a"].runs(), DEFAULT_RUNS);
156 assert_eq!(c.bench["a"].warmup(), DEFAULT_WARMUP);
157 }
158
159 #[test]
160 fn per_benchmark_overrides_win() {
161 let c = Config::parse("[bench.a]\ncmd = \"x\"\nruns = 5\nwarmup = 1").unwrap();
162 assert_eq!(c.bench["a"].runs(), 5);
163 assert_eq!(c.bench["a"].warmup(), 1);
164 }
165
166 #[test]
169 fn an_empty_command_is_rejected_at_parse_time() {
170 let err = Config::parse("[bench.a]\ncmd = []").unwrap_err();
171 assert!(format!("{err:#}").contains('a'), "{err:#}");
172 }
173
174 #[test]
175 fn benchmarks_run_in_a_stable_order() {
176 let c = Config::parse("[bench.zebra]\ncmd = \"z\"\n[bench.alpha]\ncmd = \"a\"").unwrap();
177 assert_eq!(c.bench.keys().collect::<Vec<_>>(), ["alpha", "zebra"]);
178 }
179
180 #[test]
181 fn an_empty_file_declares_nothing() {
182 assert!(Config::parse("").unwrap().bench.is_empty());
183 }
184
185 #[test]
186 fn find_walks_up_from_a_subdirectory() {
187 let root = std::env::temp_dir().join(format!("tak-cfg-{}", std::process::id()));
188 let nested = root.join("a").join("b");
189 std::fs::create_dir_all(&nested).unwrap();
190 std::fs::write(root.join(FILE_NAME), "[bench.x]\ncmd = \"true\"").unwrap();
191
192 let (path, cfg) = Config::find(&nested).unwrap().expect("should find it");
193 assert_eq!(path, root.join(FILE_NAME));
194 assert!(cfg.bench.contains_key("x"));
195
196 std::fs::remove_dir_all(&root).ok();
197 }
198}