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