joule_profiler_cli/config/
mod.rs1use std::{collections::HashMap, fs, path::Path, time::Duration};
2
3use anyhow::{Context, Result};
4use serde::Deserialize;
5
6#[cfg(feature = "_rapl")]
7use crate::RaplBackend;
8use crate::{config::overrides::ConfigOverride, output::formats::OutputFormat};
9
10pub mod overrides;
11pub mod source;
12pub mod table;
13
14const DEFAULT_INIT_TIMEOUT: Duration = Duration::from_secs(1);
15const DEFAULT_TOKEN_PATTERN: &str = "__[A-Z0-9_]+__";
16
17fn default_timeout() -> Duration {
18 DEFAULT_INIT_TIMEOUT
19}
20
21fn default_token_pattern() -> String {
22 DEFAULT_TOKEN_PATTERN.to_owned()
23}
24
25#[derive(Debug, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct ProfilerConfig {
30 pub stdout_file: Option<String>,
32
33 #[serde(default = "default_token_pattern")]
35 pub token_pattern: String,
36
37 #[serde(default)]
39 pub use_root: bool,
40
41 pub output_file: Option<String>,
43
44 #[serde(default)]
46 pub output_format: OutputFormat,
47
48 #[serde(default = "default_timeout", with = "humantime_serde")]
50 pub init_timeout: Duration,
51
52 #[cfg(feature = "_rapl")]
53 #[serde(default)]
54 pub rapl_backend: RaplBackend,
55}
56
57impl Default for ProfilerConfig {
58 fn default() -> Self {
59 Self {
60 token_pattern: default_token_pattern(),
61 init_timeout: default_timeout(),
62 output_format: OutputFormat::Terminal,
63 #[cfg(feature = "_rapl")]
64 rapl_backend: RaplBackend::default(),
65 use_root: false,
66 stdout_file: None,
67 output_file: None,
68 }
69 }
70}
71
72#[derive(Debug, Default, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct GlobalConfig {
75 #[serde(default)]
77 pub profiler: ProfilerConfig,
78
79 #[serde(default)]
81 pub sources: HashMap<String, toml::Value>,
82}
83
84pub fn load_global_config(
91 config_file: Option<&Path>,
92 overrides: &[ConfigOverride],
93) -> Result<GlobalConfig> {
94 let mut value = match config_file {
95 Some(path) => {
96 let content = fs::read_to_string(path)
97 .with_context(|| format!("configuration file error on `{}`", path.display()))?;
98 toml::from_str(&content).context("error parsing configuration file")?
99 }
100 None => toml::Value::Table(toml::map::Map::new()),
101 };
102
103 for config_override in overrides {
104 config_override.apply(&mut value)?;
105 }
106
107 value
108 .try_into()
109 .context("error applying the configuration. Check the `-D` overrides")
110}
111
112#[cfg(test)]
113mod tests {
114 use std::io::Write;
115
116 use super::*;
117
118 fn overrides(raw: &[&str]) -> Vec<ConfigOverride> {
119 raw.iter().map(|s| s.parse().unwrap()).collect()
120 }
121
122 fn config_file(content: &str) -> tempfile::NamedTempFile {
123 let mut file = tempfile::NamedTempFile::new().unwrap();
124 write!(file, "{content}").unwrap();
125 file
126 }
127
128 #[test]
129 fn load_without_a_file_nor_overrides_is_the_default_config() {
130 let config = load_global_config(None, &[]).unwrap();
131
132 assert_eq!(config.profiler.token_pattern, DEFAULT_TOKEN_PATTERN);
133 assert!(config.sources.is_empty());
134 }
135
136 #[test]
137 fn load_applies_overrides_without_a_config_file() {
138 let config = load_global_config(
139 None,
140 &overrides(&[
141 "profiler.token_pattern=__CLI__",
142 "sources.cgroup.create_cgroup=false",
143 ]),
144 )
145 .unwrap();
146
147 assert_eq!(config.profiler.token_pattern, "__CLI__");
148 assert_eq!(
149 config.sources["cgroup"]["create_cgroup"],
150 toml::Value::Boolean(false)
151 );
152 }
153
154 #[test]
155 fn load_overrides_take_precedence_over_the_file() {
156 let file = config_file(
157 "[profiler]\ntoken_pattern = \"__FILE__\"\nuse_root = true\n\
158 [sources.cgroup]\ncgroup_name = \"from-file\"\n",
159 );
160
161 let config = load_global_config(
162 Some(file.path()),
163 &overrides(&["profiler.token_pattern=__CLI__"]),
164 )
165 .unwrap();
166
167 assert_eq!(config.profiler.token_pattern, "__CLI__");
168 assert!(config.profiler.use_root);
169 assert_eq!(
170 config.sources["cgroup"]["cgroup_name"],
171 toml::Value::String("from-file".to_owned())
172 );
173 }
174
175 #[test]
176 fn load_overrides_a_source_absent_from_the_file() {
177 let file = config_file("[sources.rapl]\nsockets_spec = [0]\n");
178
179 let config = load_global_config(
180 Some(file.path()),
181 &overrides(&["sources.cgroup.poll_interval=20ms"]),
182 )
183 .unwrap();
184
185 assert_eq!(
186 config.sources["cgroup"]["poll_interval"],
187 toml::Value::String("20ms".to_owned())
188 );
189 assert_eq!(
190 config.sources["rapl"]["sockets_spec"][0],
191 toml::Value::Integer(0)
192 );
193 }
194
195 #[test]
196 fn load_reports_a_missing_config_file() {
197 let err = load_global_config(Some(Path::new("/does/not/exist.toml")), &[]).unwrap_err();
198
199 assert!(err.to_string().contains("/does/not/exist.toml"));
200 }
201
202 #[test]
203 fn load_reports_an_override_of_the_wrong_type() {
204 let err = load_global_config(None, &overrides(&["profiler.use_root=42"])).unwrap_err();
205
206 assert!(err.to_string().contains("-D"));
207 }
208
209 #[test]
210 fn load_rejects_an_unknown_profiler_key() {
211 let err =
212 load_global_config(None, &overrides(&["profiler.output_forrmat=json"])).unwrap_err();
213
214 assert!(format!("{err:#}").contains("output_forrmat"));
215 }
216
217 #[test]
218 fn load_rejects_an_unknown_top_level_key() {
219 let err = load_global_config(None, &overrides(&["profilr.use_root=true"])).unwrap_err();
220
221 assert!(format!("{err:#}").contains("profilr"));
222 }
223
224 #[test]
225 fn load_rejects_an_unknown_key_of_a_config_file() {
226 let file = config_file("[profiler]\nuse_rooot = true\n");
227
228 let err = load_global_config(Some(file.path()), &[]).unwrap_err();
229
230 assert!(format!("{err:#}").contains("use_rooot"));
231 }
232}