use crate::Problem;
pub type ProblemFromJsonFn = fn(&serde_json::Value) -> Result<Box<dyn Problem>, serde_json::Error>;
pub struct ProblemDeserializer {
pub kind: &'static str,
pub from_json: ProblemFromJsonFn,
}
inventory::collect!(ProblemDeserializer);
pub fn problem_from_json(kind: &str, details: &serde_json::Value) -> Option<Box<dyn Problem>> {
for entry in inventory::iter::<ProblemDeserializer> {
if entry.kind == kind {
match (entry.from_json)(details) {
Ok(p) => return Some(p),
Err(e) => {
log::warn!(
"buildlog-consultant: failed to deserialize problem of kind {:?}: {}",
kind,
e
);
return None;
}
}
}
}
None
}
#[macro_export]
macro_rules! register_problem_de {
($ty:ty, $kind:expr) => {
::inventory::submit! {
$crate::ProblemDeserializer {
kind: $kind,
from_json: |v| {
::serde_json::from_value::<$ty>(v.clone())
.map(|p| Box::new(p) as Box<dyn $crate::Problem>)
},
}
}
};
}
#[macro_export]
macro_rules! register_problem_de_fn {
($kind:expr, $fn:expr) => {
::inventory::submit! {
$crate::ProblemDeserializer {
kind: $kind,
from_json: $fn,
}
}
};
}
#[cfg(test)]
mod tests {
use crate::Problem;
fn roundtrip<P: Problem + 'static>(p: &P) {
let kind = p.kind().to_string();
let json = p.json();
let back = crate::problem_from_json(&kind, &json)
.unwrap_or_else(|| panic!("no deserializer for kind {:?}", kind));
assert_eq!(back.kind(), kind);
assert_eq!(back.json(), json, "round-trip mismatch for kind {}", kind);
}
#[test]
fn roundtrip_missing_file() {
roundtrip(&crate::problems::common::MissingFile::new(
"/usr/bin/foo".into(),
));
}
#[test]
fn roundtrip_missing_command() {
roundtrip(&crate::problems::common::MissingCommand("foo".into()));
}
#[test]
fn roundtrip_vcs_control_directory_needed() {
roundtrip(&crate::problems::common::VcsControlDirectoryNeeded::new(
vec!["git", "bzr"],
));
}
#[test]
fn roundtrip_missing_python_module() {
roundtrip(&crate::problems::common::MissingPythonModule::simple(
"numpy".to_string(),
));
}
#[test]
fn roundtrip_missing_python_distribution() {
roundtrip(&crate::problems::common::MissingPythonDistribution {
distribution: "twisted".to_string(),
python_version: Some(3),
minimum_version: Some("18.0".to_string()),
});
}
#[test]
fn roundtrip_missing_c_header() {
roundtrip(&crate::problems::common::MissingCHeader::new(
"stdio.h".into(),
));
}
#[test]
fn roundtrip_missing_go_package() {
roundtrip(&crate::problems::common::MissingGoPackage {
package: "github.com/foo/bar".into(),
});
}
#[test]
fn roundtrip_missing_node_module() {
roundtrip(&crate::problems::common::MissingNodeModule("foo".into()));
}
#[test]
fn roundtrip_missing_node_package() {
roundtrip(&crate::problems::common::MissingNodePackage("foo".into()));
}
#[test]
fn roundtrip_missing_vala_package() {
roundtrip(&crate::problems::common::MissingValaPackage(
"foo-1.0".into(),
));
}
#[test]
fn roundtrip_missing_library() {
roundtrip(&crate::problems::common::MissingLibrary("foo".into()));
}
#[test]
fn roundtrip_missing_haskell_module() {
roundtrip(&crate::problems::common::MissingHaskellModule::new(
"Foo.Bar".into(),
));
}
#[test]
fn roundtrip_missing_pkg_config() {
roundtrip(&crate::problems::common::MissingPkgConfig {
module: "foo".into(),
minimum_version: Some("1.0".into()),
});
}
#[test]
fn roundtrip_missing_python_module_with_versions() {
roundtrip(&crate::problems::common::MissingPythonModule {
module: "django".into(),
python_version: Some(3),
minimum_version: Some("4.2".into()),
});
}
#[test]
fn roundtrip_missing_python_distribution_simple() {
roundtrip(&crate::problems::common::MissingPythonDistribution {
distribution: "twisted".into(),
python_version: None,
minimum_version: None,
});
}
#[test]
fn roundtrip_missing_pkg_config_no_version() {
roundtrip(&crate::problems::common::MissingPkgConfig {
module: "glib-2.0".into(),
minimum_version: None,
});
}
#[test]
fn roundtrip_missing_haskell_dependencies() {
roundtrip(&crate::problems::common::MissingHaskellDependencies(vec![
"aeson".into(),
"text".into(),
]));
}
#[test]
fn roundtrip_missing_autoconf_macro_default() {
roundtrip(&crate::problems::common::MissingAutoconfMacro::new(
"AC_PROG_CC".into(),
));
}
#[test]
fn roundtrip_missing_autoconf_macro_need_rebuild() {
let mut p = crate::problems::common::MissingAutoconfMacro::new("PKG_CHECK_MODULES".into());
p.need_rebuild = true;
roundtrip(&p);
}
#[test]
fn roundtrip_unsatisfied_apt_dependencies() {
roundtrip(&crate::problems::debian::UnsatisfiedAptDependencies(
"libssl-dev (>= 1.1)".into(),
));
}
#[test]
fn roundtrip_apt_package_unknown() {
roundtrip(&crate::problems::debian::AptPackageUnknown(
"nonexistent-pkg".into(),
));
}
#[test]
fn missing_haskell_dependencies_accepts_bare_array() {
let v = serde_json::json!(["aeson", "text"]);
let p = crate::problem_from_json("missing-haskell-dependencies", &v)
.expect("bare array should deserialize");
assert_eq!(p.json(), serde_json::json!({"deps": ["aeson", "text"]}),);
}
#[test]
fn apt_package_unknown_accepts_bare_string() {
let v = serde_json::json!("foo-pkg");
let p = crate::problem_from_json("apt-package-unknown", &v)
.expect("bare string should deserialize");
assert_eq!(p.json(), serde_json::json!({"package": "foo-pkg"}));
}
#[test]
fn missing_file_missing_path_returns_none() {
let v = serde_json::json!({});
assert!(crate::problem_from_json("missing-file", &v).is_none());
}
#[test]
fn missing_file_wrong_type_returns_none() {
let v = serde_json::json!({"path": 42});
assert!(crate::problem_from_json("missing-file", &v).is_none());
}
#[test]
fn missing_python_module_only_required_field() {
let v = serde_json::json!({"module": "numpy"});
let p = crate::problem_from_json("missing-python-module", &v)
.expect("optional fields should be optional");
assert_eq!(p.kind(), "missing-python-module");
assert_eq!(
p.json(),
serde_json::json!({
"module": "numpy",
"python_version": null,
"minimum_version": null,
}),
);
}
#[test]
fn missing_autoconf_macro_omits_need_rebuild() {
let v = serde_json::json!({"macro": "AC_PROG_CC"});
let p = crate::problem_from_json("missing-autoconf-macro", &v)
.expect("need_rebuild should be optional");
assert_eq!(
p.json(),
serde_json::json!({"macro": "AC_PROG_CC", "need_rebuild": false}),
);
}
#[test]
fn unknown_kind_returns_none() {
let v = serde_json::json!({"foo": "bar"});
assert!(crate::problem_from_json("not-a-real-kind", &v).is_none());
}
}