7179: Add TestableVolume interface, make UnixVolume tests a bit more reusable.
[arvados.git] / services / keepstore / volume_test.go
1 package main
2
3 import (
4         "errors"
5         "fmt"
6         "io"
7         "os"
8         "strings"
9         "sync"
10         "time"
11 )
12
13 // A TestableVolume allows test suites to manipulate the state of an
14 // underlying Volume, in order to test behavior in cases that are
15 // impractical to achieve with a sequence of normal Volume operations.
16 type TestableVolume interface {
17         Volume
18         // [Over]write content for a locator with the given data,
19         // bypassing all constraints like readonly and serialize.
20         PutRaw(locator string, data []byte)
21         // Specify the value Mtime() should return, until the next
22         // call to Touch, TouchWithDate, or Put.
23         TouchWithDate(locator string, lastPut time.Time)
24         // Clean up, delete temporary files.
25         Teardown()
26 }
27
28 // MockVolumes are test doubles for Volumes, used to test handlers.
29 type MockVolume struct {
30         Store      map[string][]byte
31         Timestamps map[string]time.Time
32         // Bad volumes return an error for every operation.
33         Bad bool
34         // Touchable volumes' Touch() method succeeds for a locator
35         // that has been Put().
36         Touchable bool
37         // Readonly volumes return an error for Put, Delete, and
38         // Touch.
39         Readonly bool
40         // Gate is a "starting gate", allowing test cases to pause
41         // volume operations long enough to inspect state. Every
42         // operation (except Status) starts by receiving from
43         // Gate. Sending one value unblocks one operation; closing the
44         // channel unblocks all operations. By default, Gate is a
45         // closed channel, so all operations proceed without
46         // blocking. See trash_worker_test.go for an example.
47         Gate   chan struct{}
48         called map[string]int
49         mutex  sync.Mutex
50 }
51
52 // CreateMockVolume returns a non-Bad, non-Readonly, Touchable mock
53 // volume.
54 func CreateMockVolume() *MockVolume {
55         gate := make(chan struct{})
56         close(gate)
57         return &MockVolume{
58                 Store:      make(map[string][]byte),
59                 Timestamps: make(map[string]time.Time),
60                 Bad:        false,
61                 Touchable:  true,
62                 Readonly:   false,
63                 called:     map[string]int{},
64                 Gate:       gate,
65         }
66 }
67
68 // CallCount returns how many times the named method has been called.
69 func (v *MockVolume) CallCount(method string) int {
70         v.mutex.Lock()
71         defer v.mutex.Unlock()
72         if c, ok := v.called[method]; !ok {
73                 return 0
74         } else {
75                 return c
76         }
77 }
78
79 func (v *MockVolume) gotCall(method string) {
80         v.mutex.Lock()
81         defer v.mutex.Unlock()
82         if _, ok := v.called[method]; !ok {
83                 v.called[method] = 1
84         } else {
85                 v.called[method]++
86         }
87 }
88
89 func (v *MockVolume) Get(loc string) ([]byte, error) {
90         v.gotCall("Get")
91         <-v.Gate
92         if v.Bad {
93                 return nil, errors.New("Bad volume")
94         } else if block, ok := v.Store[loc]; ok {
95                 buf := bufs.Get(len(block))
96                 copy(buf, block)
97                 return buf, nil
98         }
99         return nil, os.ErrNotExist
100 }
101
102 func (v *MockVolume) Put(loc string, block []byte) error {
103         v.gotCall("Put")
104         <-v.Gate
105         if v.Bad {
106                 return errors.New("Bad volume")
107         }
108         if v.Readonly {
109                 return MethodDisabledError
110         }
111         v.Store[loc] = block
112         return v.Touch(loc)
113 }
114
115 func (v *MockVolume) Touch(loc string) error {
116         v.gotCall("Touch")
117         <-v.Gate
118         if v.Readonly {
119                 return MethodDisabledError
120         }
121         if v.Touchable {
122                 v.Timestamps[loc] = time.Now()
123                 return nil
124         }
125         return errors.New("Touch failed")
126 }
127
128 func (v *MockVolume) Mtime(loc string) (time.Time, error) {
129         v.gotCall("Mtime")
130         <-v.Gate
131         var mtime time.Time
132         var err error
133         if v.Bad {
134                 err = errors.New("Bad volume")
135         } else if t, ok := v.Timestamps[loc]; ok {
136                 mtime = t
137         } else {
138                 err = os.ErrNotExist
139         }
140         return mtime, err
141 }
142
143 func (v *MockVolume) IndexTo(prefix string, w io.Writer) error {
144         v.gotCall("IndexTo")
145         <-v.Gate
146         for loc, block := range v.Store {
147                 if !IsValidLocator(loc) || !strings.HasPrefix(loc, prefix) {
148                         continue
149                 }
150                 _, err := fmt.Fprintf(w, "%s+%d %d\n",
151                         loc, len(block), 123456789)
152                 if err != nil {
153                         return err
154                 }
155         }
156         return nil
157 }
158
159 func (v *MockVolume) Delete(loc string) error {
160         v.gotCall("Delete")
161         <-v.Gate
162         if v.Readonly {
163                 return MethodDisabledError
164         }
165         if _, ok := v.Store[loc]; ok {
166                 if time.Since(v.Timestamps[loc]) < blob_signature_ttl {
167                         return nil
168                 }
169                 delete(v.Store, loc)
170                 return nil
171         }
172         return os.ErrNotExist
173 }
174
175 func (v *MockVolume) Status() *VolumeStatus {
176         var used uint64
177         for _, block := range v.Store {
178                 used = used + uint64(len(block))
179         }
180         return &VolumeStatus{"/bogo", 123, 1000000 - used, used}
181 }
182
183 func (v *MockVolume) String() string {
184         return "[MockVolume]"
185 }
186
187 func (v *MockVolume) Writable() bool {
188         return !v.Readonly
189 }