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