Policy filter and verdict pipeline (#54)
This commit was merged in pull request #54.
This commit is contained in:
Generated
+3
@@ -48,6 +48,9 @@ version = "0.1.0"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "arr-core"
|
name = "arr-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"arr-parse",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arr-daemon"
|
name = "arr-daemon"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ repository.workspace = true
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arr-parse.workspace = true
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
+35
-16
@@ -4,6 +4,10 @@
|
|||||||
|
|
||||||
use std::{collections::BTreeMap, path::PathBuf, time::SystemTime};
|
use std::{collections::BTreeMap, path::PathBuf, time::SystemTime};
|
||||||
|
|
||||||
|
pub mod policy;
|
||||||
|
|
||||||
|
pub use arr_parse::NameClaims as ParsedRelease;
|
||||||
|
|
||||||
macro_rules! id_type {
|
macro_rules! id_type {
|
||||||
($name:ident) => {
|
($name:ident) => {
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
@@ -56,12 +60,43 @@ pub enum Source {
|
|||||||
WebDl,
|
WebDl,
|
||||||
WebRip,
|
WebRip,
|
||||||
Hdtv,
|
Hdtv,
|
||||||
|
Dvd,
|
||||||
|
Telecine,
|
||||||
Telesync,
|
Telesync,
|
||||||
Cam,
|
Cam,
|
||||||
Screener,
|
Screener,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<arr_parse::Resolution> for Resolution {
|
||||||
|
fn from(value: arr_parse::Resolution) -> Self {
|
||||||
|
match value {
|
||||||
|
arr_parse::Resolution::P480 => Self::Other(480),
|
||||||
|
arr_parse::Resolution::P576 => Self::Other(576),
|
||||||
|
arr_parse::Resolution::P720 => Self::R720p,
|
||||||
|
arr_parse::Resolution::P1080 => Self::R1080p,
|
||||||
|
arr_parse::Resolution::P2160 => Self::R2160p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<arr_parse::Source> for Source {
|
||||||
|
fn from(value: arr_parse::Source) -> Self {
|
||||||
|
match value {
|
||||||
|
arr_parse::Source::Cam => Self::Cam,
|
||||||
|
arr_parse::Source::Telesync => Self::Telesync,
|
||||||
|
arr_parse::Source::Telecine => Self::Telecine,
|
||||||
|
arr_parse::Source::Screener => Self::Screener,
|
||||||
|
arr_parse::Source::Dvd => Self::Dvd,
|
||||||
|
arr_parse::Source::Hdtv => Self::Hdtv,
|
||||||
|
arr_parse::Source::WebRip => Self::WebRip,
|
||||||
|
arr_parse::Source::WebDl => Self::WebDl,
|
||||||
|
arr_parse::Source::BluRay => Self::BluRay,
|
||||||
|
arr_parse::Source::Remux => Self::Remux,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct DolbyVisionProfile {
|
pub struct DolbyVisionProfile {
|
||||||
pub profile: u8,
|
pub profile: u8,
|
||||||
@@ -77,14 +112,6 @@ pub enum HdrFormat {
|
|||||||
DolbyVision(DolbyVisionProfile),
|
DolbyVision(DolbyVisionProfile),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum HdrClaim {
|
|
||||||
Hdr10,
|
|
||||||
Hdr10Plus,
|
|
||||||
Hlg,
|
|
||||||
DolbyVision,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum RequiredAudio {
|
pub enum RequiredAudio {
|
||||||
OriginalLanguage,
|
OriginalLanguage,
|
||||||
@@ -192,14 +219,6 @@ pub struct MediaFile {
|
|||||||
pub waiver: Option<Rule>,
|
pub waiver: Option<Rule>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ParsedRelease {
|
|
||||||
pub resolution: Option<Resolution>,
|
|
||||||
pub source: Option<Source>,
|
|
||||||
pub hdr: Vec<HdrClaim>,
|
|
||||||
pub audio_languages: Vec<Language>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Release {
|
pub struct Release {
|
||||||
pub id: ReleaseId,
|
pub id: ReleaseId,
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
use arr_parse::NameClaims;
|
||||||
|
|
||||||
|
use crate::{MovieOverrides, Policy, ProbedMedia, Resolution, Rule, Source, Verdict};
|
||||||
|
|
||||||
|
/// The evidence available while evaluating a candidate.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Candidate<'a> {
|
||||||
|
/// Claims parsed from a release name before a grab.
|
||||||
|
PreGrab(&'a NameClaims),
|
||||||
|
/// Facts reported by `ffprobe` after a download.
|
||||||
|
PostDownload(&'a ProbedMedia),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Candidate<'_> {
|
||||||
|
/// Which side of the grab boundary supplied the evidence.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn phase(&self) -> EvaluationPhase {
|
||||||
|
match self {
|
||||||
|
Self::PreGrab(_) => EvaluationPhase::PreGrab,
|
||||||
|
Self::PostDownload(_) => EvaluationPhase::PostDownload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolution(&self) -> Option<Resolution> {
|
||||||
|
match self {
|
||||||
|
Self::PreGrab(claims) => claims.resolution.map(Resolution::from),
|
||||||
|
Self::PostDownload(media) => Some(media.resolution),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source(&self) -> Option<Source> {
|
||||||
|
match self {
|
||||||
|
Self::PreGrab(claims) => claims.source.map(Source::from),
|
||||||
|
Self::PostDownload(media) => media.source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two phases of policy evaluation.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum EvaluationPhase {
|
||||||
|
PreGrab,
|
||||||
|
PostDownload,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Everything a policy rule may inspect.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct EvaluationContext<'a> {
|
||||||
|
pub policy: &'a Policy,
|
||||||
|
pub overrides: &'a MovieOverrides,
|
||||||
|
pub candidate: Candidate<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rule's identity when no violation exists to carry concrete evidence.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RuleKind {
|
||||||
|
RequiredAudio,
|
||||||
|
DubBlacklist,
|
||||||
|
DolbyVisionProfile,
|
||||||
|
Resolution,
|
||||||
|
Source,
|
||||||
|
Size,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The result of evaluating one rule.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RuleEvaluation {
|
||||||
|
Pass(RuleKind),
|
||||||
|
Unknown(RuleKind),
|
||||||
|
SoftFail(Rule),
|
||||||
|
HardFail(Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A policy rule that can participate in the verdict pipeline.
|
||||||
|
pub trait PolicyRule: std::fmt::Debug {
|
||||||
|
fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full evaluation details, including rules whose evidence was unknown.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct Evaluation {
|
||||||
|
pub verdict: Verdict,
|
||||||
|
pub rules: Vec<RuleEvaluation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply the built-in filter rules and retain the verdict and every rule result.
|
||||||
|
#[must_use]
|
||||||
|
pub fn evaluate(
|
||||||
|
policy: &Policy,
|
||||||
|
overrides: &MovieOverrides,
|
||||||
|
candidate: Candidate<'_>,
|
||||||
|
) -> Evaluation {
|
||||||
|
let context = EvaluationContext {
|
||||||
|
policy,
|
||||||
|
overrides,
|
||||||
|
candidate,
|
||||||
|
};
|
||||||
|
let rules: [&dyn PolicyRule; 2] = [&ResolutionRule, &SourceRule];
|
||||||
|
evaluate_rules(&context, &rules)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduce an ordered set of policy rules to a verdict.
|
||||||
|
///
|
||||||
|
/// The first hard failure decides rejection. If none exists, the first soft
|
||||||
|
/// failure decides the waiver. Unknown evaluations abstain from reduction and
|
||||||
|
/// remain visible in the returned report.
|
||||||
|
#[must_use]
|
||||||
|
pub fn evaluate_rules(context: &EvaluationContext<'_>, rules: &[&dyn PolicyRule]) -> Evaluation {
|
||||||
|
let evaluations: Vec<RuleEvaluation> =
|
||||||
|
rules.iter().map(|rule| rule.evaluate(context)).collect();
|
||||||
|
|
||||||
|
let hard_failure = evaluations.iter().find_map(|evaluation| {
|
||||||
|
if let RuleEvaluation::HardFail(rule) = evaluation {
|
||||||
|
Some(rule.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let soft_failure = || {
|
||||||
|
evaluations.iter().find_map(|evaluation| {
|
||||||
|
if let RuleEvaluation::SoftFail(rule) = evaluation {
|
||||||
|
Some(rule.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let verdict = hard_failure.map_or_else(
|
||||||
|
|| soft_failure().map_or(Verdict::Eligible, Verdict::Waived),
|
||||||
|
Verdict::Rejected,
|
||||||
|
);
|
||||||
|
|
||||||
|
Evaluation {
|
||||||
|
verdict,
|
||||||
|
rules: evaluations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether the candidate's resolution is allowed by the effective policy.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct ResolutionRule;
|
||||||
|
|
||||||
|
impl PolicyRule for ResolutionRule {
|
||||||
|
fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||||
|
let Some(resolution) = context.candidate.resolution() else {
|
||||||
|
return RuleEvaluation::Unknown(RuleKind::Resolution);
|
||||||
|
};
|
||||||
|
|
||||||
|
let allowed_by_root = context.policy.resolution_preference.contains(&resolution);
|
||||||
|
if allowed_by_root && (!context.overrides.only_4k || resolution == Resolution::R2160p) {
|
||||||
|
RuleEvaluation::Pass(RuleKind::Resolution)
|
||||||
|
} else if allowed_by_root
|
||||||
|
&& context.overrides.only_4k
|
||||||
|
&& context.candidate.phase() == EvaluationPhase::PostDownload
|
||||||
|
{
|
||||||
|
RuleEvaluation::SoftFail(Rule::Resolution(resolution))
|
||||||
|
} else {
|
||||||
|
RuleEvaluation::HardFail(Rule::Resolution(resolution))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rejects sources that are never watchable release candidates.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct SourceRule;
|
||||||
|
|
||||||
|
impl PolicyRule for SourceRule {
|
||||||
|
fn evaluate(&self, context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||||
|
let Some(source) = context.candidate.source() else {
|
||||||
|
return RuleEvaluation::Unknown(RuleKind::Source);
|
||||||
|
};
|
||||||
|
match source {
|
||||||
|
Source::Cam | Source::Telesync | Source::Screener => {
|
||||||
|
RuleEvaluation::HardFail(Rule::Source(source))
|
||||||
|
}
|
||||||
|
Source::Other => RuleEvaluation::Unknown(RuleKind::Source),
|
||||||
|
Source::Dvd
|
||||||
|
| Source::Telecine
|
||||||
|
| Source::Hdtv
|
||||||
|
| Source::WebRip
|
||||||
|
| Source::WebDl
|
||||||
|
| Source::BluRay
|
||||||
|
| Source::Remux => RuleEvaluation::Pass(RuleKind::Source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use arr_parse::{NameClaims, Resolution as ClaimedResolution, Source as ClaimedSource};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{HdrFormat, HdrRules, PolicyId, RequiredAudio};
|
||||||
|
|
||||||
|
fn policy() -> Policy {
|
||||||
|
Policy {
|
||||||
|
id: PolicyId(1),
|
||||||
|
name: "test".to_owned(),
|
||||||
|
required_audio: RequiredAudio::OriginalLanguage,
|
||||||
|
dub_blacklist: Vec::new(),
|
||||||
|
hdr_rules: HdrRules {
|
||||||
|
rejected_dolby_vision_profiles: Vec::new(),
|
||||||
|
},
|
||||||
|
size_bands: BTreeMap::new(),
|
||||||
|
resolution_preference: vec![Resolution::R2160p, Resolution::R1080p],
|
||||||
|
source_weights: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn claims(resolution: Option<ClaimedResolution>, source: Option<ClaimedSource>) -> NameClaims {
|
||||||
|
NameClaims {
|
||||||
|
resolution,
|
||||||
|
source,
|
||||||
|
..NameClaims::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn probed(resolution: Resolution, source: Option<Source>) -> ProbedMedia {
|
||||||
|
ProbedMedia {
|
||||||
|
resolution,
|
||||||
|
source,
|
||||||
|
hdr: HdrFormat::Sdr,
|
||||||
|
audio_tracks: Vec::new(),
|
||||||
|
subtitle_tracks: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verdict(policy: &Policy, overrides: &MovieOverrides, candidate: Candidate<'_>) -> Verdict {
|
||||||
|
evaluate(policy, overrides, candidate).verdict
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_pre_grab_evidence_stays_unknown() {
|
||||||
|
let policy = policy();
|
||||||
|
let claims = claims(None, None);
|
||||||
|
let report = evaluate(
|
||||||
|
&policy,
|
||||||
|
&MovieOverrides::default(),
|
||||||
|
Candidate::PreGrab(&claims),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(report.verdict, Verdict::Eligible);
|
||||||
|
assert_eq!(
|
||||||
|
report.rules,
|
||||||
|
vec![
|
||||||
|
RuleEvaluation::Unknown(RuleKind::Resolution),
|
||||||
|
RuleEvaluation::Unknown(RuleKind::Source),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preferred_resolutions_pass_in_both_phases() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
|
||||||
|
for claimed in [ClaimedResolution::P2160, ClaimedResolution::P1080] {
|
||||||
|
let claims = claims(Some(claimed), Some(ClaimedSource::WebDl));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
Verdict::Eligible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for resolution in [Resolution::R2160p, Resolution::R1080p] {
|
||||||
|
let media = probed(resolution, Some(Source::WebDl));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||||
|
Verdict::Eligible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolution_outside_root_policy_hard_fails_in_both_phases() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
let cases = [
|
||||||
|
(ClaimedResolution::P480, Resolution::Other(480)),
|
||||||
|
(ClaimedResolution::P576, Resolution::Other(576)),
|
||||||
|
(ClaimedResolution::P720, Resolution::R720p),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (claimed_resolution, resolution) in cases {
|
||||||
|
let claims = claims(Some(claimed_resolution), Some(ClaimedSource::WebDl));
|
||||||
|
let media = probed(resolution, Some(Source::WebDl));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
Verdict::Rejected(Rule::Resolution(resolution))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||||
|
Verdict::Rejected(Rule::Resolution(resolution))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_4k_tightens_the_root_policy_in_both_phases() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides {
|
||||||
|
only_4k: true,
|
||||||
|
..MovieOverrides::default()
|
||||||
|
};
|
||||||
|
let claims_1080 = claims(Some(ClaimedResolution::P1080), Some(ClaimedSource::WebDl));
|
||||||
|
let media = probed(Resolution::R1080p, Some(Source::WebDl));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims_1080)),
|
||||||
|
Verdict::Rejected(Rule::Resolution(Resolution::R1080p))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||||
|
Verdict::Waived(Rule::Resolution(Resolution::R1080p))
|
||||||
|
);
|
||||||
|
|
||||||
|
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
Verdict::Eligible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_4k_never_expands_the_root_policy() {
|
||||||
|
let mut policy = policy();
|
||||||
|
policy.resolution_preference = vec![Resolution::R1080p];
|
||||||
|
let overrides = MovieOverrides {
|
||||||
|
only_4k: true,
|
||||||
|
..MovieOverrides::default()
|
||||||
|
};
|
||||||
|
let claims = claims(Some(ClaimedResolution::P2160), Some(ClaimedSource::WebDl));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
Verdict::Rejected(Rule::Resolution(Resolution::R2160p))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_unsafe_source_hard_fails_in_both_phases() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
let cases = [
|
||||||
|
(ClaimedSource::Cam, Source::Cam),
|
||||||
|
(ClaimedSource::Telesync, Source::Telesync),
|
||||||
|
(ClaimedSource::Screener, Source::Screener),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (claimed_source, source) in cases {
|
||||||
|
let claims = claims(Some(ClaimedResolution::P2160), Some(claimed_source));
|
||||||
|
let media = probed(Resolution::R2160p, Some(source));
|
||||||
|
let expected = Verdict::Rejected(Rule::Source(source));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsed_telesync_name_is_rejected() {
|
||||||
|
let policy = policy();
|
||||||
|
let claims = arr_parse::parse("Movie.2024.1080p.TS-GROUP");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
verdict(
|
||||||
|
&policy,
|
||||||
|
&MovieOverrides::default(),
|
||||||
|
Candidate::PreGrab(&claims),
|
||||||
|
),
|
||||||
|
Verdict::Rejected(Rule::Source(Source::Telesync))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_safe_source_passes_in_both_phases() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
let cases = [
|
||||||
|
(ClaimedSource::Dvd, Source::Dvd),
|
||||||
|
(ClaimedSource::Telecine, Source::Telecine),
|
||||||
|
(ClaimedSource::Hdtv, Source::Hdtv),
|
||||||
|
(ClaimedSource::WebRip, Source::WebRip),
|
||||||
|
(ClaimedSource::WebDl, Source::WebDl),
|
||||||
|
(ClaimedSource::BluRay, Source::BluRay),
|
||||||
|
(ClaimedSource::Remux, Source::Remux),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (claimed_source, source) in cases {
|
||||||
|
let claims = claims(Some(ClaimedResolution::P2160), Some(claimed_source));
|
||||||
|
let media = probed(Resolution::R2160p, Some(source));
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PreGrab(&claims)),
|
||||||
|
Verdict::Eligible
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
verdict(&policy, &overrides, Candidate::PostDownload(&media)),
|
||||||
|
Verdict::Eligible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_post_download_source_is_preserved_in_report() {
|
||||||
|
let policy = policy();
|
||||||
|
for source in [None, Some(Source::Other)] {
|
||||||
|
let media = probed(Resolution::R2160p, source);
|
||||||
|
let report = evaluate(
|
||||||
|
&policy,
|
||||||
|
&MovieOverrides::default(),
|
||||||
|
Candidate::PostDownload(&media),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(report.verdict, Verdict::Eligible);
|
||||||
|
assert_eq!(report.rules[1], RuleEvaluation::Unknown(RuleKind::Source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FixedRule {
|
||||||
|
evaluation: RuleEvaluation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PolicyRule for FixedRule {
|
||||||
|
fn evaluate(&self, _context: &EvaluationContext<'_>) -> RuleEvaluation {
|
||||||
|
self.evaluation.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hard_failure_wins_over_an_earlier_soft_failure() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
let claims = claims(None, None);
|
||||||
|
let context = EvaluationContext {
|
||||||
|
policy: &policy,
|
||||||
|
overrides: &overrides,
|
||||||
|
candidate: Candidate::PreGrab(&claims),
|
||||||
|
};
|
||||||
|
let soft = FixedRule {
|
||||||
|
evaluation: RuleEvaluation::SoftFail(Rule::Other("soft".to_owned())),
|
||||||
|
};
|
||||||
|
let hard = FixedRule {
|
||||||
|
evaluation: RuleEvaluation::HardFail(Rule::Other("hard".to_owned())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let report = evaluate_rules(&context, &[&soft, &hard]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report.verdict,
|
||||||
|
Verdict::Rejected(Rule::Other("hard".to_owned()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_failure_of_the_deciding_kind_is_stable() {
|
||||||
|
let policy = policy();
|
||||||
|
let overrides = MovieOverrides::default();
|
||||||
|
let claims = claims(None, None);
|
||||||
|
let context = EvaluationContext {
|
||||||
|
policy: &policy,
|
||||||
|
overrides: &overrides,
|
||||||
|
candidate: Candidate::PreGrab(&claims),
|
||||||
|
};
|
||||||
|
let first = FixedRule {
|
||||||
|
evaluation: RuleEvaluation::HardFail(Rule::Other("first".to_owned())),
|
||||||
|
};
|
||||||
|
let second = FixedRule {
|
||||||
|
evaluation: RuleEvaluation::HardFail(Rule::Other("second".to_owned())),
|
||||||
|
};
|
||||||
|
|
||||||
|
let report = evaluate_rules(&context, &[&first, &second]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
report.verdict,
|
||||||
|
Verdict::Rejected(Rule::Other("first".to_owned()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user