1// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package cache
6
7import (
8	"bytes"
9	"encoding/binary"
10	"fmt"
11	"io/ioutil"
12	"os"
13	"path/filepath"
14	"testing"
15	"time"
16)
17
18func init() {
19	verify = false // even if GODEBUG is set
20}
21
22func TestBasic(t *testing.T) {
23	dir, err := ioutil.TempDir("", "cachetest-")
24	if err != nil {
25		t.Fatal(err)
26	}
27	defer os.RemoveAll(dir)
28	_, err = Open(filepath.Join(dir, "notexist"))
29	if err == nil {
30		t.Fatal(`Open("tmp/notexist") succeeded, want failure`)
31	}
32
33	cdir := filepath.Join(dir, "c1")
34	if err := os.Mkdir(cdir, 0777); err != nil {
35		t.Fatal(err)
36	}
37
38	c1, err := Open(cdir)
39	if err != nil {
40		t.Fatalf("Open(c1) (create): %v", err)
41	}
42	if err := c1.putIndexEntry(dummyID(1), dummyID(12), 13, true); err != nil {
43		t.Fatalf("addIndexEntry: %v", err)
44	}
45	if err := c1.putIndexEntry(dummyID(1), dummyID(2), 3, true); err != nil { // overwrite entry
46		t.Fatalf("addIndexEntry: %v", err)
47	}
48	if entry, err := c1.Get(dummyID(1)); err != nil || entry.OutputID != dummyID(2) || entry.Size != 3 {
49		t.Fatalf("c1.Get(1) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(2), 3)
50	}
51
52	c2, err := Open(cdir)
53	if err != nil {
54		t.Fatalf("Open(c2) (reuse): %v", err)
55	}
56	if entry, err := c2.Get(dummyID(1)); err != nil || entry.OutputID != dummyID(2) || entry.Size != 3 {
57		t.Fatalf("c2.Get(1) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(2), 3)
58	}
59	if err := c2.putIndexEntry(dummyID(2), dummyID(3), 4, true); err != nil {
60		t.Fatalf("addIndexEntry: %v", err)
61	}
62	if entry, err := c1.Get(dummyID(2)); err != nil || entry.OutputID != dummyID(3) || entry.Size != 4 {
63		t.Fatalf("c1.Get(2) = %x, %v, %v, want %x, %v, nil", entry.OutputID, entry.Size, err, dummyID(3), 4)
64	}
65}
66
67func TestGrowth(t *testing.T) {
68	dir, err := ioutil.TempDir("", "cachetest-")
69	if err != nil {
70		t.Fatal(err)
71	}
72	defer os.RemoveAll(dir)
73
74	c, err := Open(dir)
75	if err != nil {
76		t.Fatalf("Open: %v", err)
77	}
78
79	n := 10000
80	if testing.Short() {
81		n = 10
82	}
83
84	for i := 0; i < n; i++ {
85		if err := c.putIndexEntry(dummyID(i), dummyID(i*99), int64(i)*101, true); err != nil {
86			t.Fatalf("addIndexEntry: %v", err)
87		}
88		id := ActionID(dummyID(i))
89		entry, err := c.Get(id)
90		if err != nil {
91			t.Fatalf("Get(%x): %v", id, err)
92		}
93		if entry.OutputID != dummyID(i*99) || entry.Size != int64(i)*101 {
94			t.Errorf("Get(%x) = %x, %d, want %x, %d", id, entry.OutputID, entry.Size, dummyID(i*99), int64(i)*101)
95		}
96	}
97	for i := 0; i < n; i++ {
98		id := ActionID(dummyID(i))
99		entry, err := c.Get(id)
100		if err != nil {
101			t.Fatalf("Get2(%x): %v", id, err)
102		}
103		if entry.OutputID != dummyID(i*99) || entry.Size != int64(i)*101 {
104			t.Errorf("Get2(%x) = %x, %d, want %x, %d", id, entry.OutputID, entry.Size, dummyID(i*99), int64(i)*101)
105		}
106	}
107}
108
109func TestVerifyPanic(t *testing.T) {
110	os.Setenv("GODEBUG", "gocacheverify=1")
111	initEnv()
112	defer func() {
113		os.Unsetenv("GODEBUG")
114		verify = false
115	}()
116
117	if !verify {
118		t.Fatal("initEnv did not set verify")
119	}
120
121	dir, err := ioutil.TempDir("", "cachetest-")
122	if err != nil {
123		t.Fatal(err)
124	}
125	defer os.RemoveAll(dir)
126
127	c, err := Open(dir)
128	if err != nil {
129		t.Fatalf("Open: %v", err)
130	}
131
132	id := ActionID(dummyID(1))
133	if err := c.PutBytes(id, []byte("abc")); err != nil {
134		t.Fatal(err)
135	}
136
137	defer func() {
138		if err := recover(); err != nil {
139			t.Log(err)
140			return
141		}
142	}()
143	c.PutBytes(id, []byte("def"))
144	t.Fatal("mismatched Put did not panic in verify mode")
145}
146
147func dummyID(x int) [HashSize]byte {
148	var out [HashSize]byte
149	binary.LittleEndian.PutUint64(out[:], uint64(x))
150	return out
151}
152
153func TestCacheTrim(t *testing.T) {
154	dir, err := ioutil.TempDir("", "cachetest-")
155	if err != nil {
156		t.Fatal(err)
157	}
158	defer os.RemoveAll(dir)
159
160	c, err := Open(dir)
161	if err != nil {
162		t.Fatalf("Open: %v", err)
163	}
164	const start = 1000000000
165	now := int64(start)
166	c.now = func() time.Time { return time.Unix(now, 0) }
167
168	checkTime := func(name string, mtime int64) {
169		t.Helper()
170		file := filepath.Join(c.dir, name[:2], name)
171		info, err := os.Stat(file)
172		if err != nil {
173			t.Fatal(err)
174		}
175		if info.ModTime().Unix() != mtime {
176			t.Fatalf("%s mtime = %d, want %d", name, info.ModTime().Unix(), mtime)
177		}
178	}
179
180	id := ActionID(dummyID(1))
181	c.PutBytes(id, []byte("abc"))
182	entry, _ := c.Get(id)
183	c.PutBytes(ActionID(dummyID(2)), []byte("def"))
184	mtime := now
185	checkTime(fmt.Sprintf("%x-a", id), mtime)
186	checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime)
187
188	// Get should not change recent mtimes.
189	now = start + 10
190	c.Get(id)
191	checkTime(fmt.Sprintf("%x-a", id), mtime)
192	checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime)
193
194	// Get should change distant mtimes.
195	now = start + 5000
196	mtime2 := now
197	if _, err := c.Get(id); err != nil {
198		t.Fatal(err)
199	}
200	c.OutputFile(entry.OutputID)
201	checkTime(fmt.Sprintf("%x-a", id), mtime2)
202	checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime2)
203
204	// Trim should leave everything alone: it's all too new.
205	c.Trim()
206	if _, err := c.Get(id); err != nil {
207		t.Fatal(err)
208	}
209	c.OutputFile(entry.OutputID)
210	data, err := ioutil.ReadFile(filepath.Join(dir, "trim.txt"))
211	if err != nil {
212		t.Fatal(err)
213	}
214	checkTime(fmt.Sprintf("%x-a", dummyID(2)), start)
215
216	// Trim less than a day later should not do any work at all.
217	now = start + 80000
218	c.Trim()
219	if _, err := c.Get(id); err != nil {
220		t.Fatal(err)
221	}
222	c.OutputFile(entry.OutputID)
223	data2, err := ioutil.ReadFile(filepath.Join(dir, "trim.txt"))
224	if err != nil {
225		t.Fatal(err)
226	}
227	if !bytes.Equal(data, data2) {
228		t.Fatalf("second trim did work: %q -> %q", data, data2)
229	}
230
231	// Fast forward and do another trim just before the 5 day cutoff.
232	// Note that because of usedQuantum the cutoff is actually 5 days + 1 hour.
233	// We used c.Get(id) just now, so 5 days later it should still be kept.
234	// On the other hand almost a full day has gone by since we wrote dummyID(2)
235	// and we haven't looked at it since, so 5 days later it should be gone.
236	now += 5 * 86400
237	checkTime(fmt.Sprintf("%x-a", dummyID(2)), start)
238	c.Trim()
239	if _, err := c.Get(id); err != nil {
240		t.Fatal(err)
241	}
242	c.OutputFile(entry.OutputID)
243	mtime3 := now
244	if _, err := c.Get(dummyID(2)); err == nil { // haven't done a Get for this since original write above
245		t.Fatalf("Trim did not remove dummyID(2)")
246	}
247
248	// The c.Get(id) refreshed id's mtime again.
249	// Check that another 5 days later it is still not gone,
250	// but check by using checkTime, which doesn't bring mtime forward.
251	now += 5 * 86400
252	c.Trim()
253	checkTime(fmt.Sprintf("%x-a", id), mtime3)
254	checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime3)
255
256	// Half a day later Trim should still be a no-op, because there was a Trim recently.
257	// Even though the entry for id is now old enough to be trimmed,
258	// it gets a reprieve until the time comes for a new Trim scan.
259	now += 86400 / 2
260	c.Trim()
261	checkTime(fmt.Sprintf("%x-a", id), mtime3)
262	checkTime(fmt.Sprintf("%x-d", entry.OutputID), mtime3)
263
264	// Another half a day later, Trim should actually run, and it should remove id.
265	now += 86400/2 + 1
266	c.Trim()
267	if _, err := c.Get(dummyID(1)); err == nil {
268		t.Fatal("Trim did not remove dummyID(1)")
269	}
270}
271