use crate::FixerError;
use debian_workspace::fs_workspace::FsWorkspace;
use debian_workspace::{Trigger, Workspace};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum DetectorCost {
Cheap,
Filesystem,
Subprocess,
Network,
}
pub trait Detector: Send + Sync {
fn name(&self) -> &'static str;
fn lintian_tags(&self) -> &'static [&'static str];
fn triggers(&self) -> &'static [Trigger] {
&[]
}
fn cost(&self) -> DetectorCost {
DetectorCost::Cheap
}
fn detect(
&self,
ws: &dyn Workspace,
preferences: &crate::FixerPreferences,
) -> Result<Vec<crate::diagnostic::Diagnostic>, FixerError>;
fn describe(
&self,
fixed: &[(crate::diagnostic::Diagnostic, crate::diagnostic::ActionPlan)],
actions: &[crate::diagnostic::Action],
) -> String {
crate::builtin_fixers::default_describe(fixed, actions)
}
fn apply(
&self,
workspace: &FsWorkspace,
preferences: &crate::FixerPreferences,
) -> Result<crate::FixerResult, FixerError> {
let diagnostics = self.detect(workspace, preferences)?;
crate::builtin_fixers::apply_diagnostics_with(
workspace.base_path(),
&diagnostics,
preferences,
&|fixed, actions| self.describe(fixed, actions),
)
}
}
fn catch_panic<T>(f: impl FnOnce() -> Result<T, FixerError>) -> Result<T, FixerError> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(r) => r,
Err(panic_payload) => {
let message = if let Some(s) = panic_payload.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_payload.downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic payload".to_string()
};
let backtrace = std::backtrace::Backtrace::capture();
let backtrace = if backtrace.status() == std::backtrace::BacktraceStatus::Captured {
Some(backtrace)
} else {
None
};
Err(FixerError::Panic { message, backtrace })
}
}
}
pub fn detect_and_plan(
detector: &dyn Detector,
workspace: &FsWorkspace,
preferences: &crate::FixerPreferences,
) -> Result<crate::builtin_fixers::DiagnosticPlan, FixerError> {
catch_panic(|| {
let diagnostics = detector.detect(workspace, preferences)?;
crate::builtin_fixers::plan_diagnostics(workspace.base_path(), &diagnostics, preferences)
})
}
pub fn detect_and_fix(
detector: &dyn Detector,
workspace: &FsWorkspace,
preferences: &crate::FixerPreferences,
) -> Result<crate::FixerResult, FixerError> {
catch_panic(|| detector.apply(workspace, preferences))
}
pub struct DetectorRegistration {
pub name: &'static str,
pub lintian_tags: &'static [&'static str],
pub create: fn() -> Box<dyn Detector>,
pub after: &'static [&'static str],
pub before: &'static [&'static str],
pub triggers: &'static [Trigger],
pub cost: DetectorCost,
}
inventory::collect!(DetectorRegistration);
pub fn iter_detectors() -> impl Iterator<Item = Box<dyn Detector>> {
inventory::iter::<DetectorRegistration>
.into_iter()
.map(|reg| (reg.create)())
}
pub fn iter_detector_registrations() -> impl Iterator<Item = &'static DetectorRegistration> {
inventory::iter::<DetectorRegistration>.into_iter()
}
#[derive(Debug, PartialEq, Eq)]
pub struct UnknownDetector(pub String);
impl std::fmt::Display for UnknownDetector {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Unknown detector: {}", self.0)
}
}
impl std::error::Error for UnknownDetector {}
pub fn select_detectors(
detectors: Vec<Box<dyn Detector>>,
names: Option<&[&str]>,
exclude: Option<&[&str]>,
) -> Result<Vec<Box<dyn Detector>>, UnknownDetector> {
use std::collections::HashSet;
let mut select_set = names.map(|names| names.iter().cloned().collect::<HashSet<_>>());
let mut exclude_set = exclude.map(|exclude| exclude.iter().cloned().collect::<HashSet<_>>());
let mut ret = vec![];
for d in detectors.into_iter() {
if let Some(select_set) = select_set.as_mut() {
if !select_set.remove(d.name()) {
if let Some(exclude_set) = exclude_set.as_mut() {
exclude_set.remove(d.name());
}
continue;
}
}
if let Some(exclude_set) = exclude_set.as_mut() {
if exclude_set.remove(d.name()) {
continue;
}
}
ret.push(d);
}
if let Some(select_set) = select_set.filter(|x| !x.is_empty()) {
Err(UnknownDetector(
select_set.iter().next().unwrap().to_string(),
))
} else if let Some(exclude_set) = exclude_set.filter(|x| !x.is_empty()) {
Err(UnknownDetector(
exclude_set.iter().next().unwrap().to_string(),
))
} else {
Ok(ret)
}
}
#[macro_export]
macro_rules! declare_detector {
(
name: $name:expr,
tags: [$($tag:expr),* $(,)?],
$(after: [$($after:expr),* $(,)?],)?
$(before: [$($before:expr),* $(,)?],)?
$(triggers: [$($trigger:expr),* $(,)?],)?
$(cost: $cost:expr,)?
detect: $detect_fn:expr
$(, describe: $describe_fn:expr)?
$(,)?
) => {
struct DetectorImpl;
impl $crate::detector::Detector for DetectorImpl {
fn name(&self) -> &'static str { $name }
fn lintian_tags(&self) -> &'static [&'static str] { &[$($tag),*] }
fn triggers(&self) -> &'static [::debian_workspace::Trigger] {
&[$($($trigger),*)?]
}
$(
fn cost(&self) -> $crate::detector::DetectorCost {
$cost
}
)?
fn detect(
&self,
ws: &dyn ::debian_workspace::Workspace,
preferences: &$crate::FixerPreferences,
) -> Result<Vec<$crate::diagnostic::Diagnostic>, $crate::FixerError> {
let detect_fn: fn(
&dyn ::debian_workspace::Workspace,
&$crate::FixerPreferences,
) -> Result<Vec<$crate::diagnostic::Diagnostic>, $crate::FixerError> = $detect_fn;
detect_fn(ws, preferences)
}
$(
fn describe(
&self,
fixed: &[(
$crate::diagnostic::Diagnostic,
$crate::diagnostic::ActionPlan,
)],
actions: &[$crate::diagnostic::Action],
) -> String {
let describe_fn: fn(
&[(
$crate::diagnostic::Diagnostic,
$crate::diagnostic::ActionPlan,
)],
&[$crate::diagnostic::Action],
) -> String = $describe_fn;
describe_fn(fixed, actions)
}
)?
}
const __COST: $crate::detector::DetectorCost = {
#[allow(unused_mut, unused_assignments)]
let mut c = $crate::detector::DetectorCost::Cheap;
$(c = $cost;)?
c
};
$crate::inventory::submit! {
$crate::detector::DetectorRegistration {
name: $name,
lintian_tags: &[$($tag),*],
create: || Box::new(DetectorImpl),
after: &[$($($after),*)?],
before: &[$($($before),*)?],
triggers: &[$($($trigger),*)?],
cost: __COST,
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyDetector {
name: &'static str,
tags: &'static [&'static str],
}
impl Detector for DummyDetector {
fn name(&self) -> &'static str {
self.name
}
fn lintian_tags(&self) -> &'static [&'static str] {
self.tags
}
fn detect(
&self,
_ws: &dyn Workspace,
_preferences: &crate::FixerPreferences,
) -> Result<Vec<crate::diagnostic::Diagnostic>, FixerError> {
unimplemented!()
}
}
fn dummies() -> Vec<Box<dyn Detector>> {
vec![
Box::new(DummyDetector {
name: "dummy1",
tags: &["some-tag"],
}),
Box::new(DummyDetector {
name: "dummy2",
tags: &["other-tag"],
}),
]
}
#[test]
fn select_detectors_includes() {
let result = select_detectors(dummies(), Some(["dummy1"].as_slice()), None).map(|m| {
m.into_iter()
.map(|d| d.name().to_string())
.collect::<Vec<_>>()
});
assert_eq!(result, Ok(vec!["dummy1".to_string()]));
}
#[test]
fn select_detectors_unknown_include() {
assert!(select_detectors(dummies(), Some(["other"].as_slice()), None).is_err());
}
#[test]
fn select_detectors_unknown_exclude() {
assert!(select_detectors(
dummies(),
Some(["dummy"].as_slice()),
Some(["some-other"].as_slice())
)
.is_err());
}
#[test]
fn select_detectors_excludes() {
let result = select_detectors(
dummies(),
Some(["dummy1"].as_slice()),
Some(["dummy2"].as_slice()),
)
.map(|m| {
m.into_iter()
.map(|d| d.name().to_string())
.collect::<Vec<_>>()
});
assert_eq!(result, Ok(vec!["dummy1".to_string()]));
}
#[test]
fn triggers_reach_registered_detector() {
let det = inventory::iter::<DetectorRegistration>
.into_iter()
.find(|reg| reg.name == "empty-debian-patches-series")
.expect("empty-debian-patches-series registered");
let triggers = det.triggers;
assert_eq!(triggers.len(), 1);
assert!(matches!(
triggers[0],
Trigger::File("debian/patches/series")
));
let untriggered = DummyDetector {
name: "untriggered",
tags: &[],
};
assert!(untriggered.triggers().is_empty());
}
#[test]
fn cost_reaches_registered_detector() {
let net = inventory::iter::<DetectorRegistration>
.into_iter()
.find(|reg| reg.name == "debian-watch-file-is-missing")
.expect("debian-watch-file-is-missing registered");
assert_eq!(net.cost, DetectorCost::Network);
assert_eq!((net.create)().cost(), DetectorCost::Network);
let cheap = inventory::iter::<DetectorRegistration>
.into_iter()
.find(|reg| reg.name == "empty-debian-patches-series")
.expect("empty-debian-patches-series registered");
assert_eq!(cheap.cost, DetectorCost::Cheap);
assert_eq!((cheap.create)().cost(), DetectorCost::Cheap);
}
#[test]
fn detector_cost_ordering_is_cheap_to_expensive() {
assert!(DetectorCost::Cheap < DetectorCost::Filesystem);
assert!(DetectorCost::Filesystem < DetectorCost::Subprocess);
assert!(DetectorCost::Subprocess < DetectorCost::Network);
}
}