qbt-clean 0.122.0

Automated rules-based cleaning of qBittorrent torrents.
fn now() -> std::time::SystemTime {
	// A stable non-epoch time so duration subtraction works in both directions.
	std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_700_000_000)
}

#[test]
fn none_slow() {
	let m = crate::Match {
		none: vec![
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^foo$"]).unwrap()),
				..Default::default()
			},
			crate::Match {
				tracker_url: Some(regex::RegexSet::new([r"^http://bar/.*$"]).unwrap()),
				..Default::default()
			},
		],
		..Default::default()
	};

	let t = crate::Torrent::zero();
	assert_eq!(m.match_fast(now(), &t), None);

	// One
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "foo".into(),
			url: "".into(),
		},
	]));
	// The other
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "".into(),
			url: "http://bar/x".into(),
		},
	]));
	// Both
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "foo".into(),
			url: "http://bar/x".into(),
		},
	]));
	// Neither
	assert!(m.match_slow(&t, &[
		crate::Tracker {
			msg: "x".into(),
			url: "http://y/".into(),
		},
	]));
}

#[test]
fn any_slow() {
	let m = crate::Match {
		any: Some(vec![
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^alpha$"]).unwrap()),
				..Default::default()
			},
			crate::Match {
				tracker_url: Some(regex::RegexSet::new([r"^http://beta/.*$"]).unwrap()),
				..Default::default()
			},
		]),
		..Default::default()
	};

	let t = crate::Torrent::zero();
	assert_eq!(m.match_fast(now(), &t), None);

	// One matches
	assert!(m.match_slow(&t, &[
		crate::Tracker {
			msg: "alpha".into(),
			url: "".into(),
		},
	]));
	// Other matches
	assert!(m.match_slow(&t, &[
		crate::Tracker {
			msg: "".into(),
			url: "http://beta/x".into(),
		},
	]));
	// Both match
	assert!(m.match_slow(&t, &[
		crate::Tracker {
			msg: "alpha".into(),
			url: "http://beta/x".into(),
		},
	]));
	// Neither
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "x".into(),
			url: "http://y/".into(),
		},
	]));
}

#[test]
fn any_empty() {
	let m = crate::Match {
		any: Some(vec![]),
		..Default::default()
	};

	let t = crate::Torrent::zero();
	assert_eq!(m.match_fast(now(), &t), Some(false));
	assert!(!m.match_slow(&t, &[]));
}

#[test]
fn any_fast() {
	// A definite-true `any` child short-circuits, overriding a slow sibling.
	let m = crate::Match {
		any: Some(vec![
			crate::Match {
				is_private: Some(true),
				..Default::default()
			},
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^does-not-matter$"]).unwrap()),
				..Default::default()
			},
		]),
		..Default::default()
	};

	let t = crate::Torrent {
		private: Some(true),
		..crate::Torrent::zero()
	};
	assert_eq!(m.match_fast(now(), &t), Some(true));
	// Slow path agrees: fast_yes child trivially matches for slow too.
	assert!(m.match_slow(&t, &[]));
}

#[test]
fn none_fast() {
	// A definite-true child of `none` forces the outer to false even when
	// another child is slow-only.
	let m = crate::Match {
		none: vec![
			crate::Match {
				is_private: Some(true),
				..Default::default()
			},
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^whatever$"]).unwrap()),
				..Default::default()
			},
		],
		..Default::default()
	};

	let t = crate::Torrent {
		private: Some(true),
		..crate::Torrent::zero()
	};
	assert_eq!(m.match_fast(now(), &t), Some(false));
	// Slow path agrees: fast_yes child trivially matches for slow too.
	assert!(!m.match_slow(&t, &[]));
}

#[test]
fn any_none_slow() {
	let m = crate::Match {
		any: Some(vec![
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^linux$"]).unwrap()),
				..Default::default()
			},
			crate::Match {
				tracker_msg: Some(regex::RegexSet::new([r"^llm$"]).unwrap()),
				..Default::default()
			},
		]),
		none: vec![crate::Match {
			tracker_msg: Some(regex::RegexSet::new([r"^.*preview.*$"]).unwrap()),
			..Default::default()
		}],
		..Default::default()
	};

	let t = crate::Torrent::zero();
	assert_eq!(m.match_fast(now(), &t), None);

	// `llm` satisfies any, and doesn't match preview -> true.
	assert!(m.match_slow(&t, &[
		crate::Tracker {
			msg: "llm".into(),
			url: "".into(),
		},
	]));
	// A second "preview" tracker activates the none -> false.
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "llm".into(),
			url: "".into(),
		},
		crate::Tracker {
			msg: "preview-build".into(),
			url: "".into(),
		},
	]));
	// Nothing in any -> false.
	assert!(!m.match_slow(&t, &[
		crate::Tracker {
			msg: "datasets".into(),
			url: "".into(),
		},
	]));
}