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