joule_profiler_cli/config/
overrides.rs1use std::str::FromStr;
4
5use anyhow::{Result, anyhow, bail};
6use toml::{Value, map::Map};
7
8#[derive(Debug, Clone, PartialEq)]
11pub struct ConfigOverride {
12 path: Vec<String>,
13 value: Value,
14}
15
16impl ConfigOverride {
17 pub fn apply(&self, root: &mut Value) -> Result<()> {
21 let (key, parents) = self
22 .path
23 .split_last()
24 .expect("an override path always has at least one segment");
25
26 let key_path = self.path.join(".");
27
28 let mut table = root.as_table_mut().ok_or_else(|| {
29 anyhow!("cannot override `{key_path}`: the configuration is not a table.")
30 })?;
31
32 for (depth, segment) in parents.iter().enumerate() {
33 let node = table
34 .entry(segment.clone())
35 .or_insert_with(|| Value::Table(Map::new()));
36 let kind = node.type_str();
37
38 table = node.as_table_mut().ok_or_else(|| {
39 anyhow!(
40 "cannot override `{key_path}`: `{}` is a {kind}, not a table.",
41 self.path[..=depth].join("."),
42 )
43 })?;
44 }
45
46 table.insert(key.clone(), self.value.clone());
47
48 Ok(())
49 }
50}
51
52impl FromStr for ConfigOverride {
53 type Err = anyhow::Error;
54
55 fn from_str(s: &str) -> Result<Self> {
56 let Some((key, value)) = s.split_once('=') else {
57 bail!("expected KEY=VALUE, got `{s}`.");
58 };
59
60 let path: Vec<String> = key
61 .trim()
62 .split('.')
63 .map(|segment| segment.trim().to_owned())
64 .collect();
65
66 if path.iter().any(String::is_empty) {
67 bail!("`{key}` is not a valid configuration key.");
68 }
69
70 Ok(Self {
71 path,
72 value: parse_value(value),
73 })
74 }
75}
76
77fn parse_value(raw: &str) -> Value {
82 let raw = raw.trim();
83
84 toml::from_str::<Value>(&format!("value = {raw}"))
85 .ok()
86 .and_then(|mut parsed| parsed.as_table_mut()?.remove("value"))
87 .unwrap_or_else(|| Value::String(raw.to_owned()))
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 fn parse(s: &str) -> ConfigOverride {
95 s.parse::<ConfigOverride>().unwrap()
96 }
97
98 fn applied(overrides: &[&str]) -> Value {
99 let mut root = Value::Table(Map::new());
100 for raw in overrides {
101 parse(raw).apply(&mut root).unwrap();
102 }
103 root
104 }
105
106 #[test]
107 fn parses_a_dotted_key_path() {
108 let parsed = parse("sources.rapl.rapl_path=/sys/class/powercap");
109
110 assert_eq!(parsed.path, ["sources", "rapl", "rapl_path"]);
111 assert_eq!(
112 parsed.value,
113 Value::String("/sys/class/powercap".to_owned())
114 );
115 }
116
117 #[test]
118 fn parses_toml_typed_values() {
119 assert_eq!(parse("a=42").value, Value::Integer(42));
120 assert_eq!(parse("a=false").value, Value::Boolean(false));
121 assert_eq!(
122 parse("a=[0, 1]").value,
123 Value::Array(vec![Value::Integer(0), Value::Integer(1)])
124 );
125 }
126
127 #[test]
128 fn falls_back_to_a_string_value() {
129 assert_eq!(parse("a=10ms").value, Value::String("10ms".to_owned()));
130 assert_eq!(
131 parse("a=__[A-Z]+__").value,
132 Value::String("__[A-Z]+__".to_owned())
133 );
134 }
135
136 #[test]
137 fn quoting_forces_a_string_value() {
138 assert_eq!(parse("a=\"42\"").value, Value::String("42".to_owned()));
139 }
140
141 #[test]
142 fn keeps_equal_signs_of_the_value() {
143 assert_eq!(parse("a=b=c").value, Value::String("b=c".to_owned()));
144 }
145
146 #[test]
147 fn rejects_a_value_less_override() {
148 assert!("profiler.use_root".parse::<ConfigOverride>().is_err());
149 }
150
151 #[test]
152 fn rejects_an_empty_key_segment() {
153 assert!("sources..rapl=1".parse::<ConfigOverride>().is_err());
154 assert!("=1".parse::<ConfigOverride>().is_err());
155 }
156
157 #[test]
158 fn apply_creates_the_intermediate_tables() {
159 let root = applied(&["sources.cgroup.create_cgroup=false"]);
160
161 assert_eq!(
162 root["sources"]["cgroup"]["create_cgroup"],
163 Value::Boolean(false)
164 );
165 }
166
167 #[test]
168 fn apply_keeps_the_neighbouring_keys() {
169 let root = applied(&[
170 "sources.cgroup.cgroup_name=first",
171 "sources.cgroup.create_cgroup=false",
172 "sources.rapl.sockets_spec=[0]",
173 "profiler.use_root=true",
174 ]);
175
176 assert_eq!(
177 root["sources"]["cgroup"]["cgroup_name"],
178 Value::String("first".to_owned())
179 );
180 assert_eq!(
181 root["sources"]["cgroup"]["create_cgroup"],
182 Value::Boolean(false)
183 );
184 assert_eq!(
185 root["sources"]["rapl"]["sockets_spec"][0],
186 Value::Integer(0)
187 );
188 assert_eq!(root["profiler"]["use_root"], Value::Boolean(true));
189 }
190
191 #[test]
192 fn apply_replaces_an_existing_value() {
193 let root = applied(&["profiler.output_format=csv", "profiler.output_format=json"]);
194
195 assert_eq!(
196 root["profiler"]["output_format"],
197 Value::String("json".to_owned())
198 );
199 }
200
201 #[test]
202 fn apply_rejects_a_path_through_a_non_table() {
203 let mut root = Value::Table(Map::new());
204 parse("profiler.use_root=true").apply(&mut root).unwrap();
205
206 let err = parse("profiler.use_root.nested=1")
207 .apply(&mut root)
208 .unwrap_err();
209
210 assert!(err.to_string().contains("profiler.use_root"));
211 }
212}