use derive_setters::Setters;
use jiff::{Span, Zoned};
use serde_derive::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as, skip_serializing_none};
use crate::{
error::{ErrorKind, RusticError, RusticResult},
repofile::{
SnapshotFile, StringList,
snapshotfile::{
SnapshotId,
grouping::{Group, Grouped, SnapshotGroup},
},
},
};
type CheckFunction = fn(&SnapshotFile, &SnapshotFile) -> bool;
#[derive(Debug, Serialize)]
pub struct ForgetGroup {
pub group: SnapshotGroup,
pub snapshots: Vec<ForgetSnapshot>,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct ForgetSnapshot {
pub snapshot: SnapshotFile,
pub keep: bool,
pub reasons: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ForgetGroups(pub Vec<Group<ForgetSnapshot>>);
impl ForgetGroups {
pub fn from_grouped_snapshots_with_retention(
g: Grouped<SnapshotFile>,
keep: &KeepOptions,
now: &Zoned,
) -> RusticResult<Self> {
let groups = g
.groups
.into_iter()
.map(|group| -> RusticResult<_> {
Ok(Group {
group_key: group.group_key,
items: keep.apply(group.items, now)?,
})
})
.collect::<RusticResult<_>>()?;
Ok(Self(groups))
}
#[must_use]
pub fn from_snapshots(snapshots: Vec<SnapshotFile>, now: &Zoned) -> Self {
let snapshots = snapshots
.into_iter()
.map(|sn| {
let keep = sn.must_keep(now);
ForgetSnapshot {
snapshot: sn,
keep,
reasons: vec![if keep { "snapshot" } else { "if argument" }.to_string()],
}
})
.collect();
let group = Group::default_group(snapshots);
Self(vec![group])
}
#[must_use]
pub fn into_forget_ids(self) -> Vec<SnapshotId> {
self.0
.into_iter()
.flat_map(|fg| {
fg.items
.into_iter()
.filter_map(|fsn| (!fsn.keep).then_some(fsn.snapshot.id))
})
.collect()
}
}
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[skip_serializing_none]
#[serde_as]
#[derive(Clone, Debug, Default, Serialize, Deserialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[non_exhaustive]
pub struct KeepOptions {
#[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
#[cfg_attr(feature = "merge", merge(strategy=conflate::vec::overwrite_empty))]
#[serde_as(as = "Vec<DisplayFromStr>")]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keep_tags: Vec<StringList>,
#[cfg_attr(feature = "clap", clap(long = "keep-id", value_name = "ID"))]
#[cfg_attr(feature = "merge", merge(strategy=conflate::vec::overwrite_empty))]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub keep_ids: Vec<String>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'l', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_last: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'M', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_minutely: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'H', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_hourly: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'd', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_daily: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'w', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_weekly: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'm', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_monthly: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_quarter_yearly: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_half_yearly: Option<i32>,
#[cfg_attr(
feature = "clap",
clap(long, short = 'y', value_name = "N", allow_hyphen_values = true, value_parser = clap::value_parser!(i32).range(-1..))
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_yearly: Option<i32>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_minutely: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_hourly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_daily: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_weekly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_monthly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_quarter_yearly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_half_yearly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub keep_within_yearly: Option<Span>,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy=conflate::bool::overwrite_false))]
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub keep_none: bool,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy=conflate::bool::overwrite_false))]
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub delete_unchanged: bool,
}
const fn always_false(_sn1: &SnapshotFile, _sn2: &SnapshotFile) -> bool {
false
}
fn equal_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
sn1.time.year() == sn2.time.year()
}
fn equal_half_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_year(sn1, sn2) && (sn1.time.month() - 1) / 6 == (sn2.time.month() - 1) / 6
}
fn equal_quarter_year(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_year(sn1, sn2) && (sn1.time.month() - 1) / 3 == (sn2.time.month() - 1) / 3
}
fn equal_month(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_year(sn1, sn2) && sn1.time.month() == sn2.time.month()
}
fn equal_week(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_year(sn1, sn2)
&& sn1.time.clone().iso_week_date().week() == sn2.time.clone().iso_week_date().week()
}
fn equal_day(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_year(sn1, sn2) && sn1.time.day_of_year() == sn2.time.day_of_year()
}
fn equal_hour(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_day(sn1, sn2) && sn1.time.hour() == sn2.time.hour()
}
fn equal_minute(sn1: &SnapshotFile, sn2: &SnapshotFile) -> bool {
equal_half_year(sn1, sn2) && sn1.time.minute() == sn2.time.minute()
}
impl KeepOptions {
fn is_valid(&self) -> bool {
!self.keep_tags.is_empty()
|| !self.keep_ids.is_empty()
|| self.keep_last.is_some()
|| self.keep_minutely.is_some()
|| self.keep_hourly.is_some()
|| self.keep_daily.is_some()
|| self.keep_weekly.is_some()
|| self.keep_monthly.is_some()
|| self.keep_quarter_yearly.is_some()
|| self.keep_half_yearly.is_some()
|| self.keep_within.is_some()
|| self.keep_yearly.is_some()
|| self.keep_within_minutely.is_some()
|| self.keep_within_hourly.is_some()
|| self.keep_within_daily.is_some()
|| self.keep_within_weekly.is_some()
|| self.keep_within_monthly.is_some()
|| self.keep_within_quarter_yearly.is_some()
|| self.keep_within_half_yearly.is_some()
|| self.keep_within_yearly.is_some()
|| self.keep_none
}
#[allow(clippy::too_many_lines)]
fn matches(
&mut self,
sn: &SnapshotFile,
last: Option<&SnapshotFile>,
has_next: bool,
latest_time: &Zoned,
) -> Vec<&str> {
type MatchParameters<'a> = (
CheckFunction,
&'a mut Option<i32>,
&'a str,
Option<Span>,
&'a str,
);
let mut reason = Vec::new();
let snapshot_id_hex = sn.id.to_hex();
if self
.keep_ids
.iter()
.any(|id| snapshot_id_hex.starts_with(id))
{
reason.push("id");
}
if !self.keep_tags.is_empty() && sn.tags.matches(&self.keep_tags) {
reason.push("tags");
}
let keep_checks: [MatchParameters<'_>; 9] = [
(
always_false,
&mut self.keep_last,
"last",
self.keep_within,
"within",
),
(
equal_minute,
&mut self.keep_minutely,
"minutely",
self.keep_within_minutely,
"within minutely",
),
(
equal_hour,
&mut self.keep_hourly,
"hourly",
self.keep_within_hourly,
"within hourly",
),
(
equal_day,
&mut self.keep_daily,
"daily",
self.keep_within_daily,
"within daily",
),
(
equal_week,
&mut self.keep_weekly,
"weekly",
self.keep_within_weekly,
"within weekly",
),
(
equal_month,
&mut self.keep_monthly,
"monthly",
self.keep_within_monthly,
"within monthly",
),
(
equal_quarter_year,
&mut self.keep_quarter_yearly,
"quarter-yearly",
self.keep_within_quarter_yearly,
"within quarter-yearly",
),
(
equal_half_year,
&mut self.keep_half_yearly,
"half-yearly",
self.keep_within_half_yearly,
"within half-yearly",
),
(
equal_year,
&mut self.keep_yearly,
"yearly",
self.keep_within_yearly,
"within yearly",
),
];
for (check_fun, counter, reason1, within, reason2) in keep_checks {
if !has_next || last.is_none() || !check_fun(sn, last.unwrap()) {
if let Some(counter) = counter
&& *counter != 0
{
reason.push(reason1);
if *counter > 0 {
*counter -= 1;
}
}
if let Some(within) = within
&& sn.time.saturating_add(within) > *latest_time
{
reason.push(reason2);
}
}
}
reason
}
pub fn apply(
&self,
mut snapshots: Vec<SnapshotFile>,
now: &Zoned,
) -> RusticResult<Vec<ForgetSnapshot>> {
if !self.is_valid() {
return Err(RusticError::new(
ErrorKind::InvalidInput,
"Invalid keep options specified, please make sure to specify at least one keep-* option.",
));
}
let mut group_keep = self.clone();
let mut snaps = Vec::new();
if snapshots.is_empty() {
return Ok(snaps);
}
snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse());
let latest_time = snapshots[0].time.clone();
let mut last = None;
let mut iter = snapshots.into_iter().peekable();
while let Some(sn) = iter.next() {
let (keep, reasons) = {
if sn.must_keep(now) {
(true, vec!["snapshot"])
} else if sn.must_delete(now) {
(false, vec!["snapshot"])
} else if self.delete_unchanged
&& iter.peek().is_some_and(|sn_next| sn_next.tree == sn.tree)
{
(false, vec!["unchanged"])
} else {
let reasons =
group_keep.matches(&sn, last.as_ref(), iter.peek().is_some(), &latest_time);
let keep = !reasons.is_empty();
(keep, reasons)
}
};
last = Some(sn.clone());
snaps.push(ForgetSnapshot {
snapshot: sn,
keep,
reasons: reasons.iter().map(ToString::to_string).collect(),
});
}
Ok(snaps)
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::repofile::DeleteOption;
use super::*;
use anyhow::Result;
use insta::{Settings, assert_ron_snapshot};
use jiff::{Timestamp, civil::DateTime, tz::TimeZone};
use rstest::{fixture, rstest};
use serde_json;
#[derive(Serialize)]
struct ForgetResult(Vec<(Timestamp, bool, Vec<String>)>);
fn parse_time(time: &str) -> Result<Zoned> {
Ok(DateTime::from_str(time)?.to_zoned(TimeZone::UTC)?)
}
#[fixture]
fn test_snapshots() -> Vec<SnapshotFile> {
let by_date = [
"2014-09-01 10:20:30",
"2014-09-02 10:20:30",
"2014-09-05 10:20:30",
"2014-09-06 10:20:30",
"2014-09-08 10:20:30",
"2014-09-09 10:20:30",
"2014-09-10 10:20:30",
"2014-09-11 10:20:30",
"2014-09-20 10:20:30",
"2014-09-22 10:20:30",
"2014-08-08 10:20:30",
"2014-08-10 10:20:30",
"2014-08-12 10:20:30",
"2014-08-13 10:20:30",
"2014-08-15 10:20:30",
"2014-08-18 10:20:30",
"2014-08-20 10:20:30",
"2014-08-21 10:20:30",
"2014-08-22 10:20:30",
"2014-11-18 10:20:30",
"2014-11-20 10:20:30",
"2014-11-21 10:20:30",
"2014-11-22 10:20:30",
"2015-09-01 10:20:30",
"2015-09-02 10:20:30",
"2015-09-05 10:20:30",
"2015-09-06 10:20:30",
"2015-09-08 10:20:30",
"2015-09-09 10:20:30",
"2015-09-10 10:20:30",
"2015-09-11 10:20:30",
"2015-09-20 10:20:30",
"2015-09-22 10:20:30",
"2015-08-08 10:20:30",
"2015-08-10 10:20:30",
"2015-08-12 10:20:30",
"2015-08-13 10:20:30",
"2015-08-15 10:20:30",
"2015-08-18 10:20:30",
"2015-08-20 10:20:30",
"2015-08-21 10:20:30",
"2015-08-22 10:20:30",
"2015-10-01 10:20:30",
"2015-10-02 10:20:30",
"2015-10-05 10:20:30",
"2015-10-06 10:20:30",
"2015-10-08 10:20:30",
"2015-10-09 10:20:30",
"2015-10-10 10:20:30",
"2015-10-11 10:20:30",
"2015-10-20 10:20:30",
"2015-10-22 10:20:30",
"2015-10-22 10:20:30",
"2015-11-08 10:20:30",
"2015-11-10 10:20:30",
"2015-11-12 10:20:30",
"2015-11-13 10:20:30",
"2015-11-15 10:20:30",
"2015-11-18 10:20:30",
"2015-11-20 10:20:30",
"2015-11-21 10:20:30",
"2015-11-22 10:20:30",
"2016-01-01 01:02:03",
"2016-01-01 01:03:03",
"2016-01-01 07:08:03",
"2016-01-03 07:02:03",
"2016-01-04 10:23:03",
"2016-01-04 11:23:03",
"2016-01-04 12:24:03",
"2016-01-04 12:28:03",
"2016-01-04 12:30:03",
"2016-01-04 16:23:03",
"2016-01-07 10:02:03",
"2016-01-08 20:02:03",
"2016-01-09 21:02:03",
"2016-01-12 21:02:03",
"2016-01-12 21:08:03",
"2016-01-18 12:02:03",
];
let by_date_and_id = [
(
"2016-01-05 09:02:03",
"23ef833f60639018019262ac63be5b87601ab58d23880bf6a474adea83dbbf8b",
),
(
"2016-01-06 08:02:03",
"aca6165188e4ee770bb5c7a959a7c6612121960360a2f898203dc67dd75be8da",
),
(
"2016-01-04 12:23:03",
"23ef833d367ddd53bb95cdad23207a1323b770494eae746496094f1db2416c5c",
),
];
let by_date_and_tag = [
("2014-10-01 10:20:31", "foo"),
("2014-10-02 10:20:31", "foo"),
("2014-10-05 10:20:31", "foo"),
("2014-10-06 10:20:31", "foo"),
("2014-10-08 10:20:31", "foo"),
("2014-10-09 10:20:31", "foo"),
("2014-10-10 10:20:31", "foo"),
("2014-10-11 10:20:31", "foo"),
("2014-10-20 10:20:31", "foo"),
("2014-10-22 10:20:31", "foo"),
("2014-11-08 10:20:31", "foo"),
("2014-11-10 10:20:31", "foo"),
("2014-11-12 10:20:31", "foo"),
("2014-11-13 10:20:31", "foo"),
("2014-11-15 10:20:31", "foo,bar"),
("2015-10-22 10:20:31", "foo,bar"),
("2015-10-22 10:20:31", "foo,bar"),
];
let delete_never = ["2014-09-01 10:25:37"];
let delete_at = [
("2014-09-01 10:28:37", "2014-09-01 10:28:37"),
("2014-09-01 10:29:37", "2025-09-01 10:29:37"),
];
let snaps: Vec<_> = by_date
.into_iter()
.map(|time| -> Result<_> {
let opts = &crate::SnapshotOptions::default().time(parse_time(time)?);
Ok(SnapshotFile::from_options(opts)?)
})
.chain(by_date_and_id.into_iter().map(|(time, id)| -> Result<_> {
let opts = &crate::SnapshotOptions::default().time(parse_time(time)?);
let mut snap = SnapshotFile::from_options(opts)?;
snap.id = id.parse()?;
Ok(snap)
}))
.chain(
by_date_and_tag
.into_iter()
.map(|(time, tags)| -> Result<_> {
let opts = &crate::SnapshotOptions::default()
.time(parse_time(time)?)
.tags(vec![StringList::from_str(tags)?]);
Ok(SnapshotFile::from_options(opts)?)
}),
)
.chain(delete_never.into_iter().map(|time| -> Result<_> {
let opts = &crate::SnapshotOptions::default().time(parse_time(time)?);
let mut snap = SnapshotFile::from_options(opts)?;
snap.delete = DeleteOption::Never;
Ok(snap)
}))
.chain(delete_at.into_iter().map(|(time, delete)| -> Result<_> {
let opts = &crate::SnapshotOptions::default().time(parse_time(time)?);
let mut snap = SnapshotFile::from_options(opts)?;
let delete = parse_time(delete)?;
snap.delete = DeleteOption::After(delete);
Ok(snap)
}))
.collect::<Result<_>>()
.unwrap();
snaps
}
#[fixture]
fn insta_forget_snapshots_redaction() -> Settings {
let mut settings = Settings::clone_current();
settings.add_redaction(".**.snapshot", "[snapshot]");
settings
}
#[test]
fn apply_empty_snapshots() -> Result<()> {
let now = Zoned::now();
let options = KeepOptions::default().keep_last(10);
let result = options.apply(vec![], &now)?;
assert!(result.is_empty());
Ok(())
}
#[rstest]
#[case(KeepOptions::default())]
fn test_apply_fails(#[case] options: KeepOptions, test_snapshots: Vec<SnapshotFile>) {
let now = Zoned::now();
let result = options.apply(test_snapshots, &now);
assert!(result.is_err());
}
#[rstest]
#[case(KeepOptions::default().keep_last(10))]
#[case(KeepOptions::default().keep_last(15))]
#[case(KeepOptions::default().keep_last(99))]
#[case(KeepOptions::default().keep_last(200))]
#[case(KeepOptions::default().keep_hourly(20))]
#[case(KeepOptions::default().keep_daily(3))]
#[case(KeepOptions::default().keep_daily(10))]
#[case(KeepOptions::default().keep_daily(30))]
#[case(KeepOptions::default().keep_last(5).keep_daily(5))]
#[case(KeepOptions::default().keep_last(2).keep_daily(10))]
#[case(KeepOptions::default().keep_weekly(2))]
#[case(KeepOptions::default().keep_weekly(4))]
#[case(KeepOptions::default().keep_daily(3).keep_weekly(4))]
#[case(KeepOptions::default().keep_monthly(6))]
#[case(KeepOptions::default().keep_daily(2).keep_weekly(2).keep_monthly(6))]
#[case(KeepOptions::default().keep_yearly(10))]
#[case(KeepOptions::default().keep_quarter_yearly(10))]
#[case(KeepOptions::default().keep_half_yearly(10))]
#[case(KeepOptions::default().keep_daily(7).keep_weekly(2).keep_monthly(3).keep_yearly(10))]
#[case(KeepOptions::default().keep_tags(vec![StringList::from_str("foo")?]))]
#[case(KeepOptions::default().keep_tags(vec![StringList::from_str("foo,bar")?]))]
#[case(KeepOptions::default().keep_within(Span::from_str("1d").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("2d").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("7d").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("1m").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("1mo14d").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("1y1mo1d").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("13d23h").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("2mo2h").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_hourly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_daily(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_weekly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_monthly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_quarter_yearly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_half_yearly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within_yearly(Span::from_str("1y2mo3d3h").unwrap()))]
#[case(KeepOptions::default().keep_within(Span::from_str("1h").unwrap()).keep_within_hourly(Span::from_str("1d").unwrap()).keep_within_daily(Span::from_str("1w").unwrap()).keep_within_weekly(Span::from_str("1mo").unwrap()).keep_within_monthly(Span::from_str("1y").unwrap()).keep_within_yearly(Span::from_str("9999y").unwrap()))]
#[case(KeepOptions::default().keep_last(-1))]
#[case(KeepOptions::default().keep_last(-1).keep_hourly(-1))]
#[case(KeepOptions::default().keep_hourly(-1))]
#[case(KeepOptions::default().keep_daily(3).keep_weekly(2).keep_monthly(-1).keep_yearly(-1))]
#[case(KeepOptions::default().keep_ids(vec!["23ef".to_string()]))]
#[case(KeepOptions::default().keep_none(true))]
fn test_apply(
#[case] options: KeepOptions,
test_snapshots: Vec<SnapshotFile>,
insta_forget_snapshots_redaction: Settings,
) -> Result<()> {
let now = parse_time("2016-01-18 12:02:03")?;
let result = options.apply(test_snapshots.clone(), &now)?;
let now = parse_time("2020-01-18 12:02:03")?;
let result2 = options.apply(test_snapshots, &now)?;
assert_eq!(result, result2);
let result = ForgetResult(
result
.into_iter()
.map(|s| (s.snapshot.time.timestamp(), s.keep, s.reasons))
.collect(),
);
let mut options = serde_json::to_string(&options)?;
options.retain(|c| !"{}\":".contains(c));
if options.len() > 40 {
options = options[..35].to_string();
options.push_str("[cut]");
}
insta_forget_snapshots_redaction.bind(|| {
assert_ron_snapshot!(options, result);
});
Ok(())
}
}