use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use dashmap::DashMap;
use indexmap::IndexSet;
use itertools::Itertools;
use pkgcraft::repo::EbuildRepo;
use pkgcraft::restrict::{Restrict, Scope, TryIntoRestrict};
use pkgcraft::utils::bounded_jobs;
use tracing::{info, warn};
use crate::check::{Check, CheckRunner};
use crate::error::Error;
use crate::ignore::Ignore;
use crate::iter::{ReportIter, ReportSender};
use crate::report::{Report, ReportKind, ReportSet, ReportTarget};
use crate::source::PkgFilter;
#[derive(Debug, Default, Clone)]
pub struct Scanner {
jobs: usize,
force: bool,
sort: bool,
reports: IndexSet<ReportTarget>,
exit: IndexSet<ReportSet>,
filters: IndexSet<PkgFilter>,
failed: Arc<AtomicBool>,
stats: Arc<DashMap<Check, Duration>>,
}
impl Scanner {
pub fn new() -> Self {
Self::default()
}
pub fn jobs(mut self, value: usize) -> Self {
self.jobs = value;
self
}
pub fn force(mut self, value: bool) -> Self {
self.force = value;
self
}
pub fn sort(mut self, value: bool) -> Self {
self.sort = value;
self
}
pub fn reports<I>(mut self, values: I) -> Self
where
I: IntoIterator,
I::Item: Into<ReportTarget>,
{
self.reports = values.into_iter().map(Into::into).collect();
self
}
pub fn exit<I>(mut self, values: I) -> Self
where
I: IntoIterator,
I::Item: Into<ReportSet>,
{
self.exit = values.into_iter().map(Into::into).collect();
self
}
pub fn filters<I>(mut self, values: I) -> Self
where
I: IntoIterator<Item = PkgFilter>,
{
self.filters = values.into_iter().collect();
self
}
pub fn failed(&self) -> bool {
self.failed.load(Ordering::Relaxed)
}
pub fn stats(&self) -> &Arc<DashMap<Check, Duration>> {
&self.stats
}
pub fn run<T>(&self, repo: &EbuildRepo, value: T) -> crate::Result<ReportIter>
where
T: TryIntoRestrict<EbuildRepo>,
{
let mut run = ScannerRun::new(self, repo, value)?;
let defaults = ReportKind::defaults(repo);
let supported = ReportKind::supported(repo, run.scope);
let (enabled, selected) = if self.reports.is_empty() {
(defaults.clone(), Default::default())
} else {
ReportTarget::collapse(&self.reports, &defaults, &supported)?
};
run.exit = self
.exit
.iter()
.flat_map(|x| x.expand(&defaults, &supported))
.collect();
let pkg_filtering = !self.filters.is_empty();
run.enabled = enabled
.iter()
.copied()
.map(|report| {
if let Some(scope) = report.scoped(run.scope) {
Err(Error::ReportInit(report, format!("requires {scope} scope")))
} else if pkg_filtering && report.finish_check(run.scope) {
Err(Error::ReportInit(report, "requires no package filtering".to_string()))
} else {
Ok(report)
}
})
.filter(|result| {
if let Err(Error::ReportInit(report, msg)) = &result
&& !selected.contains(report)
{
warn!("skipping {report} report: {msg}");
return false;
}
true
})
.try_collect()?;
let selected = Check::iter_report(&selected).collect();
run.runners = Check::iter_report(&enabled)
.unique()
.sorted()
.map(|check| {
if pkg_filtering && check.filtered() {
Err(Error::CheckInit(check, "requires no package filtering".to_string()))
} else if let Some(context) = check.skipped(repo, &selected) {
Err(Error::CheckInit(check, format!("requires {context} context")))
} else if let Some(scope) = check.scoped(run.scope) {
Err(Error::CheckInit(check, format!("requires {scope} scope")))
} else {
Ok(check.to_runner(&run))
}
})
.filter(|result| {
if let Err(Error::CheckInit(check, msg)) = &result
&& !selected.contains(check)
{
warn!("skipping {check} check: {msg}");
return false;
}
true
})
.try_collect()?;
Ok(ReportIter::new(run))
}
}
pub(crate) struct ScannerRun {
pub(crate) repo: EbuildRepo,
pub(crate) restrict: Restrict,
pub(crate) scope: Scope,
force: bool,
pub(crate) jobs: usize,
pub(crate) filters: IndexSet<PkgFilter>,
pub(crate) runners: IndexSet<CheckRunner>,
pub(crate) ignore: Ignore,
enabled: IndexSet<ReportKind>,
exit: IndexSet<ReportKind>,
failed: Arc<AtomicBool>,
pub(crate) sort: bool,
pub(crate) sender: OnceLock<ReportSender>,
pub(crate) stats: Arc<DashMap<Check, Duration>>,
}
impl ScannerRun {
fn new<T>(scanner: &Scanner, repo: &EbuildRepo, value: T) -> crate::Result<Self>
where
T: TryIntoRestrict<EbuildRepo>,
{
let restrict = value.try_into_restrict(repo)?;
let scope = Scope::from(&restrict);
info!("repo: {repo}");
info!("scope: {scope}");
info!("target: {restrict:?}");
Ok(Self {
repo: repo.clone(),
restrict,
scope,
force: scanner.force,
jobs: bounded_jobs(scanner.jobs),
filters: scanner.filters.clone(),
runners: Default::default(),
ignore: Ignore::new(repo),
enabled: Default::default(),
exit: Default::default(),
failed: scanner.failed.clone(),
sort: scanner.sort,
sender: Default::default(),
stats: scanner.stats.clone(),
})
}
pub(crate) fn sender(&self) -> &ReportSender {
self.sender.get().expect("failed getting sender")
}
pub(crate) fn report(&self, report: Report) {
let kind = report.kind;
if self.enabled(kind)
&& (self.force
|| kind == ReportKind::IgnoreInvalid
|| !self.ignore.ignored(&report, self))
{
if report.scope() <= &self.scope {
if self.exit.contains(&kind) {
self.failed.store(true, Ordering::Relaxed);
}
self.sender().report(report);
}
}
}
pub(crate) fn enabled(&self, kind: ReportKind) -> bool {
self.enabled.contains(&kind)
}
}
#[cfg(test)]
mod tests {
use camino::Utf8Path;
use pkgcraft::test::*;
use tracing_test::traced_test;
use crate::check::{CheckKind, Context};
use crate::report::ReportLevel;
use crate::test::*;
use super::*;
#[test]
fn targets() {
let data = test_data();
let repo = data.ebuild_repo("qa-primary").unwrap();
let path = repo.path();
let scanner = Scanner::new();
let expected = glob_reports!("{path}/**/reports.json");
let reports = scanner.run(repo, repo).unwrap();
assert_unordered_reports!(reports, expected);
let expected = glob_reports!("{path}/Keywords/*/reports.json");
let reports = scanner.run(repo, Utf8Path::new("Keywords")).unwrap();
assert_unordered_reports!(reports, expected);
let expected = glob_reports!("{path}/Dependency/DependencyInvalid/reports.json");
let reports = scanner
.run(repo, Utf8Path::new("Dependency/DependencyInvalid"))
.unwrap();
assert_ordered_reports!(reports, expected);
let expected = glob_reports!("{path}/Whitespace/WhitespaceInvalid/reports.json");
let reports = scanner.run(repo, "Whitespace/WhitespaceInvalid-0").unwrap();
assert_ordered_reports!(reports, expected);
let reports = scanner.run(repo, "nonexistent/pkg").unwrap();
assert_unordered_reports!(reports, []);
}
#[test]
fn reports() {
let data = test_data();
let repo = data.ebuild_repo("qa-primary").unwrap();
let path = repo.path();
let scanner = Scanner::new();
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([ReportSet::All]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([ReportSet::Finalize]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([CheckKind::Dependency]);
let expected = glob_reports!("{path}/Dependency/**/reports.json");
let reports = scanner.run(repo, repo).unwrap();
assert_unordered_reports!(reports, expected);
let latest = "latest".parse().unwrap();
let scanner = Scanner::new()
.reports([CheckKind::Filesdir])
.filters([latest]);
let result = scanner.run(repo, repo);
assert_err_re!(result, "Filesdir: check requires no package filtering");
let scanner = Scanner::new().reports([CheckKind::PythonUpdate]);
let result = scanner.run(repo, repo);
assert_err_re!(result, "PythonUpdate: check requires gentoo-inherited context");
let scanner = Scanner::new().reports([CheckKind::Filesdir]);
let result = scanner.run(repo, "Filesdir/FilesUnused-0");
assert_err_re!(result, "FilesUnused: report requires package scope");
let scanner = Scanner::new().reports([Context::Optional]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([ReportLevel::Warning]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([ReportKind::DependencyDeprecated]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
let scanner = Scanner::new().reports([Scope::Version]);
let reports = scanner.run(repo, repo).unwrap().count();
assert!(reports > 0);
}
#[test]
fn repos() {
let data = test_data();
let scanner = Scanner::new();
let repo = data.ebuild_repo("bad").unwrap();
let path = repo.path();
let expected = glob_reports!("{path}/**/reports.json");
let reports = scanner.run(repo, repo).unwrap();
assert_unordered_reports!(reports, expected);
let repo = data.ebuild_repo("empty").unwrap();
let reports = scanner.run(repo, repo).unwrap();
assert_unordered_reports!(reports, []);
let reports = scanner.run(repo, "nonexistent/pkg").unwrap();
assert_unordered_reports!(reports, []);
let repo = data.ebuild_repo("qa-secondary").unwrap();
let reports = scanner.run(repo, repo).unwrap();
assert_unordered_reports!(reports, []);
}
#[traced_test]
#[test]
fn skip_check() {
let data = test_data();
let repo = data.ebuild_repo("bad").unwrap();
let path = repo.path();
let scanner = Scanner::new();
let reports = scanner.run(repo, "eapi/invalid-9999").unwrap();
let expected = glob_reports!("{path}/eapi/invalid/reports.json");
assert_unordered_reports!(reports, expected);
assert_logs_re!(format!(".+: skipping due to invalid pkg: eapi/invalid-9999"));
}
#[test]
fn filters() {
let data = test_data();
let repo = data.ebuild_repo("qa-primary").unwrap();
let reports: Vec<_> = Scanner::new()
.filters(["live", "!live"].iter().map(|x| x.parse().unwrap()))
.run(repo, repo)
.unwrap()
.collect();
assert_unordered_reports!(&reports, &[]);
let repo = data.ebuild_repo("gentoo").unwrap();
let pkgdir = repo.path().join("Header/HeaderInvalid");
let expected = glob_reports!("{pkgdir}/reports.json");
let mut scanner = Scanner::new().reports([ReportKind::HeaderInvalid]);
let reports: Vec<_> = scanner.run(repo, repo).unwrap().collect();
assert_unordered_reports!(&reports, &expected);
for (filters, expected) in [
(vec!["latest"], &expected[5..]),
(vec!["!latest"], &expected[..5]),
(vec!["latest", "!latest"], &[]),
(vec!["latest-slots"], &[&expected[1..=1], &expected[5..]].concat()),
(vec!["!latest-slots"], &[&expected[..1], &expected[2..5]].concat()),
(vec!["live"], &expected[5..]),
(vec!["!live"], &expected[..5]),
(vec!["stable"], &expected[..3]),
(vec!["!stable"], &expected[3..5]),
(vec!["stable", "latest"], &expected[2..=2]),
(vec!["masked"], &expected[..1]),
(vec!["!masked"], &expected[1..]),
(vec!["slot == '1'"], &expected[2..]),
(vec!["!slot == '1'"], &expected[..2]),
] {
scanner = scanner.filters(filters.iter().map(|x| x.parse().unwrap()));
let reports: Vec<_> = scanner.run(repo, repo).unwrap().collect();
let failed = filters.iter().join(", ");
assert_unordered_reports!(
&reports,
expected,
format!("repo scope: failed filters: {failed}")
);
let reports: Vec<_> = scanner.run(repo, pkgdir.as_path()).unwrap().collect();
assert_unordered_reports!(
&reports,
expected,
format!("pkg scope: failed filters: {failed}")
);
}
}
#[test]
fn failed() {
let data = test_data();
let repo = data.ebuild_repo("qa-primary").unwrap();
let scanner = Scanner::new();
scanner.run(repo, repo).unwrap().count();
assert!(!scanner.failed());
let scanner = scanner.exit([ReportKind::HeaderInvalid]);
scanner.run(repo, repo).unwrap().count();
assert!(!scanner.failed());
let scanner = scanner.exit([ReportKind::DependencyDeprecated]);
scanner.run(repo, repo).unwrap().count();
assert!(scanner.failed());
let scanner = scanner.exit([CheckKind::Dependency]);
scanner.run(repo, repo).unwrap().count();
assert!(scanner.failed());
let scanner = scanner.exit([ReportLevel::Warning]);
scanner.run(repo, repo).unwrap().count();
assert!(scanner.failed());
}
}