pruner 0.74.0

A command-line utility and library to prune backups on a schedule.
Documentation
struct TraceItem<T>(T);

impl<T: crate::Item + std::fmt::Debug> crate::Item for TraceItem<T>
where T::Period: std::fmt::Debug,
{
	type Period = T::Period;

	fn matches(&self, old: &Self, period: &T::Period) -> bool {
		let r = self.0.matches(&old.0, period);
		eprintln!("matches({:?} - {:?} >= {:?}) -> {}", self.0, old.0, period, r);
		r
	}
}

fn assert_keep<Item: crate::Item + std::cmp::Ord + std::fmt::Debug + Clone>(
	config: &crate::Config<Item::Period>,
	candidates: impl IntoIterator<Item=Item>,
	saved: impl IntoIterator<Item=Item>,
)
where Item::Period: std::fmt::Debug
{
	let actual = std::cell::RefCell::new(std::collections::BTreeSet::new());
	let expected = saved.into_iter().collect::<std::collections::BTreeSet<_>>();

	let traced_candidates = candidates.into_iter()
		.map(TraceItem)
		.inspect(|c| {
			let mut actual = actual.borrow_mut();
			eprintln!("Alive {:?}", actual);
			actual.insert(c.0.clone());
			eprintln!("Processing {:?}", c.0);
		});

	for evicted in crate::prune(config, traced_candidates) {
		eprintln!("Evict {:?}", evicted.0);
		let mut actual = actual.borrow_mut();
		assert!(actual.remove(&evicted.0), "{:?} was evicted twice.", evicted.0);
	}

	assert_eq!(*actual.borrow(), expected);
}

#[test]
fn daily() {
	let c = crate::Config {
		buckets: vec![crate::Bucket{period: 3, count: 3}],
	};

	// For cases like this we essentially pick an arbitrary reference point. It isn't elegant but it works.
	assert_keep(&c,
		1..=12,
		[4, 7, 10]);

	assert_keep(&c,
		2..=12,
		[5, 8, 11]);

	// However subsequent runs stay synchronized.
	assert_keep(&c,
		[4, 7, 10, 13],
		[7, 10, 13]);

	assert_keep(&c,
		[7, 10, 13, 14],
		[7, 10, 13]);

	assert_keep(&c,
		[4, 7, 10, 13, 14],
		[7, 10, 13]);

	assert_keep(&c,
		[4, 7, 10, 13, 14, 15, 16, 17],
		[10, 13, 16]);

	assert_keep(&c,
		[5, 8, 11, 13, 14],
		[8, 11, 14]);
}

#[test]
fn daily_weekly() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: 3, count: 3},
			crate::Bucket{period: 6, count: 4},
		],
	};

	assert_keep(&c,
		1..=10,
		[1, 4, 7, 10]);

	assert_keep(&c,
		1..=20,
		[1, 7, 13, 16, 19]);

	assert_keep(&c,
		1..=30,
		[7, 13, 19, 22, 25, 28]);

	assert_keep(&c,
		1..=50,
		[31, 37, 43, 46, 49]);
}

#[test]
fn daily_weekly_unaligned() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: 3, count: 2},
			crate::Bucket{period: 7, count: 4},
		],
	};

	assert_keep(&c,
		1..=10,
		[1, 7, 10]);

	assert_keep(&c,
		1..=20,
		[1, 10, 16, 19]);

	assert_keep(&c,
		1..=30,
		[1, 10, 19, 25, 28]);

	assert_keep(&c,
		1..=50,
		[19, 28, 37, 46, 49]);
}

#[test]
fn multi_aligned() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: 2, count: 4},
			crate::Bucket{period: 4, count: 3},
			crate::Bucket{period: 8, count: 2},
			crate::Bucket{period: 16, count: 2},
		],
	};

	assert_keep(&c,
		1..=20,
		[1, 9, 13, 15, 17, 19]);

	assert_keep(&c,
		1..=30,
		[1, 17, 21, 23, 25, 27, 29]);

	assert_keep(&c,
		1..=50,
		[33, 41, 43, 45, 47, 49]);
}

#[test]
fn multi_unaligned() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: 4, count: 4},
			crate::Bucket{period: 5, count: 3},
			crate::Bucket{period: 10, count: 4},
			crate::Bucket{period: 12, count: 4},
		],
	};

	assert_keep(&c,
		1..=20,
		[1, 5, 9, 13, 17]);

	assert_keep(&c,
		1..=30,
		[1, 9, 17, 21, 25, 29]);

	assert_keep(&c,
		1..=50,
		[1, 17, 33, 37, 41, 45, 49]);
}

#[test]
fn smaller_bucket_aligned() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: 2, count: 6},
			crate::Bucket{period: 4, count: 2},
		],
	};

	// This is wrong. The 7 is kept because we assume that if the element is the last one and there is a bucket after us that it will take it. This does eventually clean itself up when the later bucket gets an item that it is interested in.
	assert_keep(&c,
		1..=20,
		[7, 9, 11, 13, 15, 17, 19]);
}

#[test]
fn dates() {
	let c = crate::Config {
		buckets: vec![
			crate::Bucket{period: chrono::Duration::days(2), count: 3},
			crate::Bucket{period: chrono::Duration::weeks(1), count: 4},
		],
	};

	assert_keep(&c,
		(1..=30).map(|d| chrono::NaiveDate::from_ymd(1, 1, d)),
		[
			chrono::NaiveDate::from_ymd(1, 1, 1),
			chrono::NaiveDate::from_ymd(1, 1, 9),
			chrono::NaiveDate::from_ymd(1, 1, 17),
			chrono::NaiveDate::from_ymd(1, 1, 25),
			chrono::NaiveDate::from_ymd(1, 1, 27),
			chrono::NaiveDate::from_ymd(1, 1, 29),
		]);
}