14819: Merge branch 'master' into 14819-arvados-jobs-on-stretch
[arvados.git] / services / keepstore / volume_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bytes"
9         "context"
10         "crypto/md5"
11         "errors"
12         "fmt"
13         "io"
14         "os"
15         "strings"
16         "sync"
17         "time"
18 )
19
20 // A TestableVolume allows test suites to manipulate the state of an
21 // underlying Volume, in order to test behavior in cases that are
22 // impractical to achieve with a sequence of normal Volume operations.
23 type TestableVolume interface {
24         Volume
25         // [Over]write content for a locator with the given data,
26         // bypassing all constraints like readonly and serialize.
27         PutRaw(locator string, data []byte)
28
29         // Specify the value Mtime() should return, until the next
30         // call to Touch, TouchWithDate, or Put.
31         TouchWithDate(locator string, lastPut time.Time)
32
33         // Clean up, delete temporary files.
34         Teardown()
35 }
36
37 // MockVolumes are test doubles for Volumes, used to test handlers.
38 type MockVolume struct {
39         Store      map[string][]byte
40         Timestamps map[string]time.Time
41
42         // Bad volumes return an error for every operation.
43         Bad            bool
44         BadVolumeError error
45
46         // Touchable volumes' Touch() method succeeds for a locator
47         // that has been Put().
48         Touchable bool
49
50         // Readonly volumes return an error for Put, Delete, and
51         // Touch.
52         Readonly bool
53
54         // Gate is a "starting gate", allowing test cases to pause
55         // volume operations long enough to inspect state. Every
56         // operation (except Status) starts by receiving from
57         // Gate. Sending one value unblocks one operation; closing the
58         // channel unblocks all operations. By default, Gate is a
59         // closed channel, so all operations proceed without
60         // blocking. See trash_worker_test.go for an example.
61         Gate chan struct{}
62
63         called map[string]int
64         mutex  sync.Mutex
65 }
66
67 // CreateMockVolume returns a non-Bad, non-Readonly, Touchable mock
68 // volume.
69 func CreateMockVolume() *MockVolume {
70         gate := make(chan struct{})
71         close(gate)
72         return &MockVolume{
73                 Store:      make(map[string][]byte),
74                 Timestamps: make(map[string]time.Time),
75                 Bad:        false,
76                 Touchable:  true,
77                 Readonly:   false,
78                 called:     map[string]int{},
79                 Gate:       gate,
80         }
81 }
82
83 // CallCount returns how many times the named method has been called.
84 func (v *MockVolume) CallCount(method string) int {
85         v.mutex.Lock()
86         defer v.mutex.Unlock()
87         c, ok := v.called[method]
88         if !ok {
89                 return 0
90         }
91         return c
92 }
93
94 func (v *MockVolume) gotCall(method string) {
95         v.mutex.Lock()
96         defer v.mutex.Unlock()
97         if _, ok := v.called[method]; !ok {
98                 v.called[method] = 1
99         } else {
100                 v.called[method]++
101         }
102 }
103
104 func (v *MockVolume) Compare(ctx context.Context, loc string, buf []byte) error {
105         v.gotCall("Compare")
106         <-v.Gate
107         if v.Bad {
108                 return v.BadVolumeError
109         } else if block, ok := v.Store[loc]; ok {
110                 if fmt.Sprintf("%x", md5.Sum(block)) != loc {
111                         return DiskHashError
112                 }
113                 if bytes.Compare(buf, block) != 0 {
114                         return CollisionError
115                 }
116                 return nil
117         } else {
118                 return NotFoundError
119         }
120 }
121
122 func (v *MockVolume) Get(ctx context.Context, loc string, buf []byte) (int, error) {
123         v.gotCall("Get")
124         <-v.Gate
125         if v.Bad {
126                 return 0, v.BadVolumeError
127         } else if block, ok := v.Store[loc]; ok {
128                 copy(buf[:len(block)], block)
129                 return len(block), nil
130         }
131         return 0, os.ErrNotExist
132 }
133
134 func (v *MockVolume) Put(ctx context.Context, loc string, block []byte) error {
135         v.gotCall("Put")
136         <-v.Gate
137         if v.Bad {
138                 return v.BadVolumeError
139         }
140         if v.Readonly {
141                 return MethodDisabledError
142         }
143         v.Store[loc] = block
144         return v.Touch(loc)
145 }
146
147 func (v *MockVolume) Touch(loc string) error {
148         v.gotCall("Touch")
149         <-v.Gate
150         if v.Readonly {
151                 return MethodDisabledError
152         }
153         if v.Touchable {
154                 v.Timestamps[loc] = time.Now()
155                 return nil
156         }
157         return errors.New("Touch failed")
158 }
159
160 func (v *MockVolume) Mtime(loc string) (time.Time, error) {
161         v.gotCall("Mtime")
162         <-v.Gate
163         var mtime time.Time
164         var err error
165         if v.Bad {
166                 err = v.BadVolumeError
167         } else if t, ok := v.Timestamps[loc]; ok {
168                 mtime = t
169         } else {
170                 err = os.ErrNotExist
171         }
172         return mtime, err
173 }
174
175 func (v *MockVolume) IndexTo(prefix string, w io.Writer) error {
176         v.gotCall("IndexTo")
177         <-v.Gate
178         for loc, block := range v.Store {
179                 if !IsValidLocator(loc) || !strings.HasPrefix(loc, prefix) {
180                         continue
181                 }
182                 _, err := fmt.Fprintf(w, "%s+%d %d\n",
183                         loc, len(block), 123456789)
184                 if err != nil {
185                         return err
186                 }
187         }
188         return nil
189 }
190
191 func (v *MockVolume) Trash(loc string) error {
192         v.gotCall("Delete")
193         <-v.Gate
194         if v.Readonly {
195                 return MethodDisabledError
196         }
197         if _, ok := v.Store[loc]; ok {
198                 if time.Since(v.Timestamps[loc]) < time.Duration(theConfig.BlobSignatureTTL) {
199                         return nil
200                 }
201                 delete(v.Store, loc)
202                 return nil
203         }
204         return os.ErrNotExist
205 }
206
207 func (v *MockVolume) DeviceID() string {
208         return "mock-device-id"
209 }
210
211 func (v *MockVolume) Type() string {
212         return "Mock"
213 }
214
215 func (v *MockVolume) Start() error {
216         return nil
217 }
218
219 func (v *MockVolume) Untrash(loc string) error {
220         return nil
221 }
222
223 func (v *MockVolume) Status() *VolumeStatus {
224         var used uint64
225         for _, block := range v.Store {
226                 used = used + uint64(len(block))
227         }
228         return &VolumeStatus{"/bogo", 123, 1000000 - used, used}
229 }
230
231 func (v *MockVolume) String() string {
232         return "[MockVolume]"
233 }
234
235 func (v *MockVolume) Writable() bool {
236         return !v.Readonly
237 }
238
239 func (v *MockVolume) Replication() int {
240         return 1
241 }
242
243 func (v *MockVolume) EmptyTrash() {
244 }
245
246 func (v *MockVolume) GetStorageClasses() []string {
247         return nil
248 }