1package restic_test
2
3import (
4	"encoding/json"
5	"fmt"
6	"io/ioutil"
7	"path/filepath"
8	"testing"
9	"time"
10
11	"github.com/google/go-cmp/cmp"
12	"github.com/google/go-cmp/cmp/cmpopts"
13	"github.com/restic/restic/internal/restic"
14)
15
16func parseTimeUTC(s string) time.Time {
17	t, err := time.Parse("2006-01-02 15:04:05", s)
18	if err != nil {
19		panic(err)
20	}
21
22	return t.UTC()
23}
24
25func parseDuration(s string) restic.Duration {
26	d, err := restic.ParseDuration(s)
27	if err != nil {
28		panic(err)
29	}
30
31	return d
32}
33
34func TestExpireSnapshotOps(t *testing.T) {
35	data := []struct {
36		expectEmpty bool
37		expectSum   int
38		p           *restic.ExpirePolicy
39	}{
40		{true, 0, &restic.ExpirePolicy{}},
41		{true, 0, &restic.ExpirePolicy{Tags: []restic.TagList{}}},
42		{false, 22, &restic.ExpirePolicy{Daily: 7, Weekly: 2, Monthly: 3, Yearly: 10}},
43	}
44	for i, d := range data {
45		isEmpty := d.p.Empty()
46		if isEmpty != d.expectEmpty {
47			t.Errorf("empty test %v: wrong result, want:\n  %#v\ngot:\n  %#v", i, d.expectEmpty, isEmpty)
48		}
49		hasSum := d.p.Sum()
50		if hasSum != d.expectSum {
51			t.Errorf("sum test %v: wrong result, want:\n  %#v\ngot:\n  %#v", i, d.expectSum, hasSum)
52		}
53	}
54}
55
56// ApplyPolicyResult is used to marshal/unmarshal the golden files for
57// TestApplyPolicy.
58type ApplyPolicyResult struct {
59	Keep    restic.Snapshots    `json:"keep"`
60	Reasons []restic.KeepReason `json:"reasons,omitempty"`
61}
62
63func loadGoldenFile(t testing.TB, filename string) (res ApplyPolicyResult) {
64	buf, err := ioutil.ReadFile(filename)
65	if err != nil {
66		t.Fatalf("error loading golden file %v: %v", filename, err)
67	}
68
69	err = json.Unmarshal(buf, &res)
70	if err != nil {
71		t.Fatalf("error unmarshalling golden file %v: %v", filename, err)
72	}
73
74	return res
75}
76
77func saveGoldenFile(t testing.TB, filename string, keep restic.Snapshots, reasons []restic.KeepReason) {
78	res := ApplyPolicyResult{
79		Keep:    keep,
80		Reasons: reasons,
81	}
82
83	buf, err := json.MarshalIndent(res, "", "  ")
84	if err != nil {
85		t.Fatalf("error marshaling result: %v", err)
86	}
87
88	if err = ioutil.WriteFile(filename, buf, 0644); err != nil {
89		t.Fatalf("unable to update golden file: %v", err)
90	}
91}
92
93func TestApplyPolicy(t *testing.T) {
94	var testExpireSnapshots = restic.Snapshots{
95		{Time: parseTimeUTC("2014-09-01 10:20:30")},
96		{Time: parseTimeUTC("2014-09-02 10:20:30")},
97		{Time: parseTimeUTC("2014-09-05 10:20:30")},
98		{Time: parseTimeUTC("2014-09-06 10:20:30")},
99		{Time: parseTimeUTC("2014-09-08 10:20:30")},
100		{Time: parseTimeUTC("2014-09-09 10:20:30")},
101		{Time: parseTimeUTC("2014-09-10 10:20:30")},
102		{Time: parseTimeUTC("2014-09-11 10:20:30")},
103		{Time: parseTimeUTC("2014-09-20 10:20:30")},
104		{Time: parseTimeUTC("2014-09-22 10:20:30")},
105		{Time: parseTimeUTC("2014-08-08 10:20:30")},
106		{Time: parseTimeUTC("2014-08-10 10:20:30")},
107		{Time: parseTimeUTC("2014-08-12 10:20:30")},
108		{Time: parseTimeUTC("2014-08-13 10:20:30")},
109		{Time: parseTimeUTC("2014-08-13 10:20:30.1")},
110		{Time: parseTimeUTC("2014-08-15 10:20:30")},
111		{Time: parseTimeUTC("2014-08-18 10:20:30")},
112		{Time: parseTimeUTC("2014-08-20 10:20:30")},
113		{Time: parseTimeUTC("2014-08-21 10:20:30")},
114		{Time: parseTimeUTC("2014-08-22 10:20:30")},
115		{Time: parseTimeUTC("2014-10-01 10:20:30"), Tags: []string{"foo"}},
116		{Time: parseTimeUTC("2014-10-02 10:20:30"), Tags: []string{"foo"}},
117		{Time: parseTimeUTC("2014-10-05 10:20:30"), Tags: []string{"foo"}},
118		{Time: parseTimeUTC("2014-10-06 10:20:30"), Tags: []string{"foo"}},
119		{Time: parseTimeUTC("2014-10-08 10:20:30"), Tags: []string{"foo"}},
120		{Time: parseTimeUTC("2014-10-09 10:20:30"), Tags: []string{"foo"}},
121		{Time: parseTimeUTC("2014-10-10 10:20:30"), Tags: []string{"foo"}},
122		{Time: parseTimeUTC("2014-10-11 10:20:30"), Tags: []string{"foo"}},
123		{Time: parseTimeUTC("2014-10-20 10:20:30"), Tags: []string{"foo"}},
124		{Time: parseTimeUTC("2014-10-22 10:20:30"), Tags: []string{"foo"}},
125		{Time: parseTimeUTC("2014-11-08 10:20:30"), Tags: []string{"foo"}},
126		{Time: parseTimeUTC("2014-11-10 10:20:30"), Tags: []string{"foo"}},
127		{Time: parseTimeUTC("2014-11-12 10:20:30"), Tags: []string{"foo"}},
128		{Time: parseTimeUTC("2014-11-13 10:20:30"), Tags: []string{"foo"}},
129		{Time: parseTimeUTC("2014-11-13 10:20:30.1"), Tags: []string{"bar"}},
130		{Time: parseTimeUTC("2014-11-15 10:20:30"), Tags: []string{"foo", "bar"}},
131		{Time: parseTimeUTC("2014-11-18 10:20:30")},
132		{Time: parseTimeUTC("2014-11-20 10:20:30")},
133		{Time: parseTimeUTC("2014-11-21 10:20:30")},
134		{Time: parseTimeUTC("2014-11-22 10:20:30")},
135		{Time: parseTimeUTC("2015-09-01 10:20:30")},
136		{Time: parseTimeUTC("2015-09-02 10:20:30")},
137		{Time: parseTimeUTC("2015-09-05 10:20:30")},
138		{Time: parseTimeUTC("2015-09-06 10:20:30")},
139		{Time: parseTimeUTC("2015-09-08 10:20:30")},
140		{Time: parseTimeUTC("2015-09-09 10:20:30")},
141		{Time: parseTimeUTC("2015-09-10 10:20:30")},
142		{Time: parseTimeUTC("2015-09-11 10:20:30")},
143		{Time: parseTimeUTC("2015-09-20 10:20:30")},
144		{Time: parseTimeUTC("2015-09-22 10:20:30")},
145		{Time: parseTimeUTC("2015-08-08 10:20:30")},
146		{Time: parseTimeUTC("2015-08-10 10:20:30")},
147		{Time: parseTimeUTC("2015-08-12 10:20:30")},
148		{Time: parseTimeUTC("2015-08-13 10:20:30")},
149		{Time: parseTimeUTC("2015-08-13 10:20:30.1")},
150		{Time: parseTimeUTC("2015-08-15 10:20:30")},
151		{Time: parseTimeUTC("2015-08-18 10:20:30")},
152		{Time: parseTimeUTC("2015-08-20 10:20:30")},
153		{Time: parseTimeUTC("2015-08-21 10:20:30")},
154		{Time: parseTimeUTC("2015-08-22 10:20:30")},
155		{Time: parseTimeUTC("2015-10-01 10:20:30")},
156		{Time: parseTimeUTC("2015-10-02 10:20:30")},
157		{Time: parseTimeUTC("2015-10-05 10:20:30")},
158		{Time: parseTimeUTC("2015-10-06 10:20:30")},
159		{Time: parseTimeUTC("2015-10-08 10:20:30")},
160		{Time: parseTimeUTC("2015-10-09 10:20:30")},
161		{Time: parseTimeUTC("2015-10-10 10:20:30")},
162		{Time: parseTimeUTC("2015-10-11 10:20:30")},
163		{Time: parseTimeUTC("2015-10-20 10:20:30")},
164		{Time: parseTimeUTC("2015-10-22 10:20:30")},
165		{Time: parseTimeUTC("2015-10-22 10:20:30")},
166		{Time: parseTimeUTC("2015-10-22 10:20:30"), Tags: []string{"foo", "bar"}},
167		{Time: parseTimeUTC("2015-10-22 10:20:30"), Tags: []string{"foo", "bar"}},
168		{Time: parseTimeUTC("2015-10-22 10:20:30"), Tags: []string{"foo", "bar"}, Paths: []string{"path1", "path2"}},
169		{Time: parseTimeUTC("2015-11-08 10:20:30")},
170		{Time: parseTimeUTC("2015-11-10 10:20:30")},
171		{Time: parseTimeUTC("2015-11-12 10:20:30")},
172		{Time: parseTimeUTC("2015-11-13 10:20:30")},
173		{Time: parseTimeUTC("2015-11-13 10:20:30.1")},
174		{Time: parseTimeUTC("2015-11-15 10:20:30")},
175		{Time: parseTimeUTC("2015-11-18 10:20:30")},
176		{Time: parseTimeUTC("2015-11-20 10:20:30")},
177		{Time: parseTimeUTC("2015-11-21 10:20:30")},
178		{Time: parseTimeUTC("2015-11-22 10:20:30")},
179		{Time: parseTimeUTC("2016-01-01 01:02:03")},
180		{Time: parseTimeUTC("2016-01-01 01:03:03")},
181		{Time: parseTimeUTC("2016-01-01 07:08:03")},
182		{Time: parseTimeUTC("2016-01-03 07:02:03")},
183		{Time: parseTimeUTC("2016-01-04 10:23:03")},
184		{Time: parseTimeUTC("2016-01-04 11:23:03")},
185		{Time: parseTimeUTC("2016-01-04 12:23:03")},
186		{Time: parseTimeUTC("2016-01-04 12:24:03")},
187		{Time: parseTimeUTC("2016-01-04 12:28:03")},
188		{Time: parseTimeUTC("2016-01-04 12:30:03")},
189		{Time: parseTimeUTC("2016-01-04 16:23:03")},
190		{Time: parseTimeUTC("2016-01-05 09:02:03")},
191		{Time: parseTimeUTC("2016-01-06 08:02:03")},
192		{Time: parseTimeUTC("2016-01-07 10:02:03")},
193		{Time: parseTimeUTC("2016-01-08 20:02:03")},
194		{Time: parseTimeUTC("2016-01-09 21:02:03")},
195		{Time: parseTimeUTC("2016-01-12 21:02:03")},
196		{Time: parseTimeUTC("2016-01-12 21:08:03")},
197		{Time: parseTimeUTC("2016-01-18 12:02:03")},
198	}
199
200	var tests = []restic.ExpirePolicy{
201		{},
202		{Last: 10},
203		{Last: 15},
204		{Last: 99},
205		{Last: 200},
206		{Hourly: 20},
207		{Daily: 3},
208		{Daily: 10},
209		{Daily: 30},
210		{Last: 5, Daily: 5},
211		{Last: 2, Daily: 10},
212		{Weekly: 2},
213		{Weekly: 4},
214		{Daily: 3, Weekly: 4},
215		{Monthly: 6},
216		{Daily: 2, Weekly: 2, Monthly: 6},
217		{Yearly: 10},
218		{Daily: 7, Weekly: 2, Monthly: 3, Yearly: 10},
219		{Tags: []restic.TagList{{"foo"}}},
220		{Tags: []restic.TagList{{"foo", "bar"}}},
221		{Tags: []restic.TagList{{"foo"}, {"bar"}}},
222		{Within: parseDuration("1d")},
223		{Within: parseDuration("2d")},
224		{Within: parseDuration("7d")},
225		{Within: parseDuration("1m")},
226		{Within: parseDuration("1m14d")},
227		{Within: parseDuration("1y1d1m")},
228		{Within: parseDuration("13d23h")},
229		{Within: parseDuration("2m2h")},
230		{Within: parseDuration("1y2m3d3h")},
231		{WithinHourly: parseDuration("1y2m3d3h")},
232		{WithinDaily: parseDuration("1y2m3d3h")},
233		{WithinWeekly: parseDuration("1y2m3d3h")},
234		{WithinMonthly: parseDuration("1y2m3d3h")},
235		{WithinYearly: parseDuration("1y2m3d3h")},
236		{Within: parseDuration("1h"),
237			WithinHourly:  parseDuration("1d"),
238			WithinDaily:   parseDuration("7d"),
239			WithinWeekly:  parseDuration("1m"),
240			WithinMonthly: parseDuration("1y"),
241			WithinYearly:  parseDuration("9999y")},
242	}
243
244	for i, p := range tests {
245		t.Run("", func(t *testing.T) {
246
247			keep, remove, reasons := restic.ApplyPolicy(testExpireSnapshots, p)
248
249			if len(keep)+len(remove) != len(testExpireSnapshots) {
250				t.Errorf("len(keep)+len(remove) = %d != len(testExpireSnapshots) = %d",
251					len(keep)+len(remove), len(testExpireSnapshots))
252			}
253
254			if p.Sum() > 0 && len(keep) > p.Sum() {
255				t.Errorf("not enough snapshots removed: policy allows %v snapshots to remain, but ended up with %v",
256					p.Sum(), len(keep))
257			}
258
259			if len(keep) != len(reasons) {
260				t.Errorf("got %d keep reasons for %d snapshots to keep, these must be equal", len(reasons), len(keep))
261			}
262
263			goldenFilename := filepath.Join("testdata", fmt.Sprintf("policy_keep_snapshots_%d", i))
264
265			if *updateGoldenFiles {
266				saveGoldenFile(t, goldenFilename, keep, reasons)
267			}
268
269			want := loadGoldenFile(t, goldenFilename)
270
271			cmpOpts := cmpopts.IgnoreUnexported(restic.Snapshot{})
272
273			if !cmp.Equal(want.Keep, keep, cmpOpts) {
274				t.Error(cmp.Diff(want.Keep, keep, cmpOpts))
275			}
276
277			if !cmp.Equal(want.Reasons, reasons, cmpOpts) {
278				t.Error(cmp.Diff(want.Reasons, reasons, cmpOpts))
279			}
280		})
281	}
282}
283