corescout_substrate/observation/
source.rs1use std::path::Path;
16
17use corescout_mirror::schema::Availability;
18use corescout_mirror::state::ChannelId;
19
20use crate::observation::{SensorOutcome, StateWriter};
21
22pub fn string(path: impl AsRef<Path>) -> Option<String> {
24 std::fs::read_to_string(path)
25 .ok()
26 .map(|s| s.trim_end().to_string())
27}
28
29pub fn u64(path: impl AsRef<Path>) -> Option<u64> {
31 string(path)?.trim().parse().ok()
32}
33
34pub fn f64(path: impl AsRef<Path>) -> Option<f64> {
36 u64(path).map(|v| v as f64)
37}
38
39pub fn numbered_children(dir: impl AsRef<Path>, prefix: &str) -> Vec<(u32, std::path::PathBuf)> {
45 let Ok(entries) = std::fs::read_dir(dir.as_ref()) else {
46 return Vec::new();
47 };
48 let mut found: Vec<(u32, std::path::PathBuf)> = entries
49 .flatten()
50 .filter_map(|entry| {
51 let name = entry.file_name();
52 let name = name.to_string_lossy();
53 let index = name.strip_prefix(prefix)?.parse::<u32>().ok()?;
54 Some((index, entry.path()))
55 })
56 .collect();
57 found.sort_by_key(|(index, _)| *index);
58 found
59}
60
61pub fn named_children(dir: impl AsRef<Path>, prefix: &str) -> Vec<(String, std::path::PathBuf)> {
66 let Ok(entries) = std::fs::read_dir(dir.as_ref()) else {
67 return Vec::new();
68 };
69 let mut found: Vec<(String, std::path::PathBuf)> = entries
70 .flatten()
71 .filter_map(|entry| {
72 let name = entry.file_name().to_string_lossy().into_owned();
73 if name.starts_with(prefix) {
74 Some((name, entry.path()))
75 } else {
76 None
77 }
78 })
79 .collect();
80 found.sort_by(|a, b| a.0.cmp(&b.0));
81 found
82}
83
84pub fn clock_ticks_per_second() -> u64 {
91 #[cfg(target_os = "linux")]
92 {
93 let value = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
95 if value > 0 {
96 return value as u64;
97 }
98 }
99 100
100}
101
102pub fn sample(
110 out: &mut StateWriter<'_>,
111 outcome: &mut SensorOutcome,
112 row: u32,
113 channel: Option<ChannelId>,
114 path: impl AsRef<Path>,
115 required: bool,
116) {
117 let Some(channel) = channel else { return };
118 let path = path.as_ref();
119 match self::f64(path) {
120 Some(value) => {
121 out.set(row, channel, value);
122 outcome.sample();
123 }
124 None => {
125 let why = match std::fs::metadata(path) {
128 Ok(_) => Availability::PermissionDenied,
129 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
130 Availability::PermissionDenied
131 }
132 Err(_) if required => Availability::Unavailable,
133 Err(_) => Availability::Unsupported,
134 };
135 out.missing(row, channel, why);
136 if required {
137 outcome.error();
138 }
139 }
140 }
141}
142
143pub fn emit(
145 out: &mut StateWriter<'_>,
146 outcome: &mut SensorOutcome,
147 row: u32,
148 channel: Option<ChannelId>,
149 value: f64,
150) {
151 let Some(channel) = channel else { return };
152 out.set(row, channel, value);
153 outcome.sample();
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn temp_dir(tag: &str) -> std::path::PathBuf {
161 let dir =
162 std::env::temp_dir().join(format!("corescout-source-{tag}-{}", std::process::id()));
163 let _ = std::fs::remove_dir_all(&dir);
164 std::fs::create_dir_all(&dir).unwrap();
165 dir
166 }
167
168 #[test]
169 fn missing_files_are_absent_not_errors() {
170 assert_eq!(string("/definitely/not/here"), None);
171 assert_eq!(u64("/definitely/not/here"), None);
172 assert_eq!(f64("/definitely/not/here"), None);
173 }
174
175 #[test]
176 fn values_are_trimmed_of_the_kernel_trailing_newline() {
177 let dir = temp_dir("trim");
178 std::fs::write(dir.join("value"), "3600000\n").unwrap();
179 assert_eq!(string(dir.join("value")).as_deref(), Some("3600000"));
180 assert_eq!(u64(dir.join("value")), Some(3_600_000));
181 assert_eq!(f64(dir.join("value")), Some(3_600_000.0));
182 }
183
184 #[test]
185 fn non_numeric_content_is_absent_rather_than_zero() {
186 let dir = temp_dir("garbage");
187 std::fs::write(dir.join("value"), "not a number\n").unwrap();
188 assert_eq!(u64(dir.join("value")), None);
189 }
190
191 #[test]
192 fn numbered_children_are_sorted_numerically() {
193 let dir = temp_dir("numbered");
194 for index in [10u32, 2, 1, 20] {
195 std::fs::create_dir_all(dir.join(format!("state{index}"))).unwrap();
196 }
197 std::fs::create_dir_all(dir.join("other")).unwrap();
198 let found = numbered_children(&dir, "state");
199 let indices: Vec<u32> = found.iter().map(|(i, _)| *i).collect();
200 assert_eq!(
201 indices,
202 vec![1, 2, 10, 20],
203 "lexicographic ordering would put state10 before state2"
204 );
205 }
206
207 #[test]
208 fn named_children_are_sorted_and_filtered() {
209 let dir = temp_dir("named");
214 for name in [
215 "intel-rapl-1",
216 "intel-rapl-0",
217 "intel-rapl-0-0",
218 "unrelated",
219 ] {
220 std::fs::create_dir_all(dir.join(name)).unwrap();
221 }
222 let found = named_children(&dir, "intel-rapl-");
223 let names: Vec<&str> = found.iter().map(|(n, _)| n.as_str()).collect();
224 assert_eq!(
225 names,
226 vec!["intel-rapl-0", "intel-rapl-0-0", "intel-rapl-1"]
227 );
228 }
229
230 #[test]
231 fn missing_directories_yield_nothing() {
232 assert!(numbered_children("/definitely/not/here", "x").is_empty());
233 assert!(named_children("/definitely/not/here", "x").is_empty());
234 }
235
236 #[test]
237 fn clock_ticks_are_plausible() {
238 let ticks = clock_ticks_per_second();
239 assert!((1..=10_000).contains(&ticks), "implausible CLK_TCK {ticks}");
240 }
241}