buildlog_consultant/
problem.rs1use crate::Problem;
18
19pub type ProblemFromJsonFn = fn(&serde_json::Value) -> Result<Box<dyn Problem>, serde_json::Error>;
26
27pub struct ProblemDeserializer {
37 pub kind: &'static str,
39 pub from_json: ProblemFromJsonFn,
43}
44
45inventory::collect!(ProblemDeserializer);
46
47pub fn problem_from_json(kind: &str, details: &serde_json::Value) -> Option<Box<dyn Problem>> {
54 for entry in inventory::iter::<ProblemDeserializer> {
55 if entry.kind == kind {
56 match (entry.from_json)(details) {
57 Ok(p) => return Some(p),
58 Err(e) => {
59 log::warn!(
60 "buildlog-consultant: failed to deserialize problem of kind {:?}: {}",
61 kind,
62 e
63 );
64 return None;
65 }
66 }
67 }
68 }
69 None
70}
71
72#[macro_export]
83macro_rules! register_problem_de {
84 ($ty:ty, $kind:expr) => {
85 ::inventory::submit! {
86 $crate::ProblemDeserializer {
87 kind: $kind,
88 from_json: |v| {
89 ::serde_json::from_value::<$ty>(v.clone())
90 .map(|p| Box::new(p) as Box<dyn $crate::Problem>)
91 },
92 }
93 }
94 };
95}
96
97#[macro_export]
102macro_rules! register_problem_de_fn {
103 ($kind:expr, $fn:expr) => {
104 ::inventory::submit! {
105 $crate::ProblemDeserializer {
106 kind: $kind,
107 from_json: $fn,
108 }
109 }
110 };
111}
112
113#[cfg(test)]
114mod tests {
115 use crate::Problem;
116
117 fn roundtrip<P: Problem + 'static>(p: &P) {
123 let kind = p.kind().to_string();
124 let json = p.json();
125 let back = crate::problem_from_json(&kind, &json)
126 .unwrap_or_else(|| panic!("no deserializer for kind {:?}", kind));
127 assert_eq!(back.kind(), kind);
128 assert_eq!(back.json(), json, "round-trip mismatch for kind {}", kind);
129 }
130
131 #[test]
132 fn roundtrip_missing_file() {
133 roundtrip(&crate::problems::common::MissingFile::new(
134 "/usr/bin/foo".into(),
135 ));
136 }
137
138 #[test]
139 fn roundtrip_missing_command() {
140 roundtrip(&crate::problems::common::MissingCommand("foo".into()));
141 }
142
143 #[test]
144 fn roundtrip_vcs_control_directory_needed() {
145 roundtrip(&crate::problems::common::VcsControlDirectoryNeeded::new(
146 vec!["git", "bzr"],
147 ));
148 }
149
150 #[test]
151 fn roundtrip_missing_python_module() {
152 roundtrip(&crate::problems::common::MissingPythonModule::simple(
153 "numpy".to_string(),
154 ));
155 }
156
157 #[test]
158 fn roundtrip_missing_python_distribution() {
159 roundtrip(&crate::problems::common::MissingPythonDistribution {
160 distribution: "twisted".to_string(),
161 python_version: Some(3),
162 minimum_version: Some("18.0".to_string()),
163 });
164 }
165
166 #[test]
167 fn roundtrip_missing_c_header() {
168 roundtrip(&crate::problems::common::MissingCHeader::new(
169 "stdio.h".into(),
170 ));
171 }
172
173 #[test]
174 fn roundtrip_missing_go_package() {
175 roundtrip(&crate::problems::common::MissingGoPackage {
176 package: "github.com/foo/bar".into(),
177 });
178 }
179
180 #[test]
181 fn roundtrip_missing_node_module() {
182 roundtrip(&crate::problems::common::MissingNodeModule("foo".into()));
183 }
184
185 #[test]
186 fn roundtrip_missing_node_package() {
187 roundtrip(&crate::problems::common::MissingNodePackage("foo".into()));
188 }
189
190 #[test]
191 fn roundtrip_missing_vala_package() {
192 roundtrip(&crate::problems::common::MissingValaPackage(
193 "foo-1.0".into(),
194 ));
195 }
196
197 #[test]
198 fn roundtrip_missing_library() {
199 roundtrip(&crate::problems::common::MissingLibrary("foo".into()));
200 }
201
202 #[test]
203 fn roundtrip_missing_haskell_module() {
204 roundtrip(&crate::problems::common::MissingHaskellModule::new(
205 "Foo.Bar".into(),
206 ));
207 }
208
209 #[test]
210 fn roundtrip_missing_pkg_config() {
211 roundtrip(&crate::problems::common::MissingPkgConfig {
212 module: "foo".into(),
213 minimum_version: Some("1.0".into()),
214 });
215 }
216
217 #[test]
218 fn roundtrip_missing_python_module_with_versions() {
219 roundtrip(&crate::problems::common::MissingPythonModule {
220 module: "django".into(),
221 python_version: Some(3),
222 minimum_version: Some("4.2".into()),
223 });
224 }
225
226 #[test]
227 fn roundtrip_missing_python_distribution_simple() {
228 roundtrip(&crate::problems::common::MissingPythonDistribution {
229 distribution: "twisted".into(),
230 python_version: None,
231 minimum_version: None,
232 });
233 }
234
235 #[test]
236 fn roundtrip_missing_pkg_config_no_version() {
237 roundtrip(&crate::problems::common::MissingPkgConfig {
238 module: "glib-2.0".into(),
239 minimum_version: None,
240 });
241 }
242
243 #[test]
244 fn roundtrip_missing_haskell_dependencies() {
245 roundtrip(&crate::problems::common::MissingHaskellDependencies(vec![
246 "aeson".into(),
247 "text".into(),
248 ]));
249 }
250
251 #[test]
252 fn roundtrip_missing_autoconf_macro_default() {
253 roundtrip(&crate::problems::common::MissingAutoconfMacro::new(
254 "AC_PROG_CC".into(),
255 ));
256 }
257
258 #[test]
259 fn roundtrip_missing_autoconf_macro_need_rebuild() {
260 let mut p = crate::problems::common::MissingAutoconfMacro::new("PKG_CHECK_MODULES".into());
261 p.need_rebuild = true;
262 roundtrip(&p);
263 }
264
265 #[test]
266 fn roundtrip_unsatisfied_apt_dependencies() {
267 roundtrip(&crate::problems::debian::UnsatisfiedAptDependencies(
268 "libssl-dev (>= 1.1)".into(),
269 ));
270 }
271
272 #[test]
273 fn roundtrip_apt_package_unknown() {
274 roundtrip(&crate::problems::debian::AptPackageUnknown(
275 "nonexistent-pkg".into(),
276 ));
277 }
278
279 #[test]
280 fn missing_haskell_dependencies_accepts_bare_array() {
281 let v = serde_json::json!(["aeson", "text"]);
284 let p = crate::problem_from_json("missing-haskell-dependencies", &v)
285 .expect("bare array should deserialize");
286 assert_eq!(p.json(), serde_json::json!({"deps": ["aeson", "text"]}),);
287 }
288
289 #[test]
290 fn apt_package_unknown_accepts_bare_string() {
291 let v = serde_json::json!("foo-pkg");
294 let p = crate::problem_from_json("apt-package-unknown", &v)
295 .expect("bare string should deserialize");
296 assert_eq!(p.json(), serde_json::json!({"package": "foo-pkg"}));
297 }
298
299 #[test]
300 fn missing_file_missing_path_returns_none() {
301 let v = serde_json::json!({});
304 assert!(crate::problem_from_json("missing-file", &v).is_none());
305 }
306
307 #[test]
308 fn missing_file_wrong_type_returns_none() {
309 let v = serde_json::json!({"path": 42});
311 assert!(crate::problem_from_json("missing-file", &v).is_none());
312 }
313
314 #[test]
315 fn missing_python_module_only_required_field() {
316 let v = serde_json::json!({"module": "numpy"});
319 let p = crate::problem_from_json("missing-python-module", &v)
320 .expect("optional fields should be optional");
321 assert_eq!(p.kind(), "missing-python-module");
322 assert_eq!(
323 p.json(),
324 serde_json::json!({
325 "module": "numpy",
326 "python_version": null,
327 "minimum_version": null,
328 }),
329 );
330 }
331
332 #[test]
333 fn missing_autoconf_macro_omits_need_rebuild() {
334 let v = serde_json::json!({"macro": "AC_PROG_CC"});
336 let p = crate::problem_from_json("missing-autoconf-macro", &v)
337 .expect("need_rebuild should be optional");
338 assert_eq!(
339 p.json(),
340 serde_json::json!({"macro": "AC_PROG_CC", "need_rebuild": false}),
341 );
342 }
343
344 #[test]
345 fn unknown_kind_returns_none() {
346 let v = serde_json::json!({"foo": "bar"});
347 assert!(crate::problem_from_json("not-a-real-kind", &v).is_none());
348 }
349}