| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 package bigtable | 5 package bigtable |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "bytes" | 8 "bytes" |
| 9 » "errors" | 9 » "fmt" |
| 10 "testing" | 10 "testing" |
| 11 "time" | 11 "time" |
| 12 | 12 |
| 13 "github.com/luci/gkvlite" | 13 "github.com/luci/gkvlite" |
| 14 "github.com/luci/luci-go/common/logdog/types" | 14 "github.com/luci/luci-go/common/logdog/types" |
| 15 "github.com/luci/luci-go/common/recordio" | 15 "github.com/luci/luci-go/common/recordio" |
| 16 "github.com/luci/luci-go/server/logdog/storage" | 16 "github.com/luci/luci-go/server/logdog/storage" |
| 17 "golang.org/x/net/context" | 17 "golang.org/x/net/context" |
| 18 "google.golang.org/cloud/bigtable" | |
| 19 | 18 |
| 20 . "github.com/luci/luci-go/common/testing/assertions" | 19 . "github.com/luci/luci-go/common/testing/assertions" |
| 21 . "github.com/smartystreets/goconvey/convey" | 20 . "github.com/smartystreets/goconvey/convey" |
| 22 ) | 21 ) |
| 23 | 22 |
| 24 // btTableTest is an in-memory implementation of btTable interface for testing. | 23 // btTableTest is an in-memory implementation of btTable interface for testing. |
| 25 // | 24 // |
| 26 // This is a simple implementation; not an efficient one. | 25 // This is a simple implementation; not an efficient one. |
| 27 type btTableTest struct { | 26 type btTableTest struct { |
| 28 s *gkvlite.Store | 27 s *gkvlite.Store |
| 29 c *gkvlite.Collection | 28 c *gkvlite.Collection |
| 30 | 29 |
| 31 // err, if true, is the error immediately returned by functions. | 30 // err, if true, is the error immediately returned by functions. |
| 32 err error | 31 err error |
| 33 | 32 |
| 34 // maxLogAge is the currently-configured maximum log age. | 33 // maxLogAge is the currently-configured maximum log age. |
| 35 maxLogAge time.Duration | 34 maxLogAge time.Duration |
| 35 |
| 36 // countLegacy, if true, simulates legacy BigTable rows by not writing o
r |
| 37 // reading the "count" field. |
| 38 countLegacy bool |
| 36 } | 39 } |
| 37 | 40 |
| 38 func (t *btTableTest) close() { | 41 func (t *btTableTest) close() { |
| 39 if t.s != nil { | 42 if t.s != nil { |
| 40 t.s.Close() | 43 t.s.Close() |
| 41 t.s = nil | 44 t.s = nil |
| 42 } | 45 } |
| 43 } | 46 } |
| 44 | 47 |
| 45 func (t *btTableTest) collection() *gkvlite.Collection { | 48 func (t *btTableTest) collection() *gkvlite.Collection { |
| 46 if t.s == nil { | 49 if t.s == nil { |
| 47 var err error | 50 var err error |
| 48 t.s, err = gkvlite.NewStore(nil) | 51 t.s, err = gkvlite.NewStore(nil) |
| 49 if err != nil { | 52 if err != nil { |
| 50 panic(err) | 53 panic(err) |
| 51 } | 54 } |
| 52 t.c = t.s.MakePrivateCollection(bytes.Compare) | 55 t.c = t.s.MakePrivateCollection(bytes.Compare) |
| 53 } | 56 } |
| 54 return t.c | 57 return t.c |
| 55 } | 58 } |
| 56 | 59 |
| 57 func (t *btTableTest) putLogData(c context.Context, rk *rowKey, d []byte) error
{ | 60 func (t *btTableTest) putLogData(c context.Context, rk *rowKey, d []byte) error
{ |
| 58 if t.err != nil { | 61 if t.err != nil { |
| 59 return t.err | 62 return t.err |
| 60 } | 63 } |
| 61 | 64 |
| 65 // Record/count sanity check. |
| 66 records, err := recordio.Split(d) |
| 67 if err != nil { |
| 68 return err |
| 69 } |
| 70 if int64(len(records)) != rk.count { |
| 71 return fmt.Errorf("count mismatch (%d != %d)", len(records), rk.
count) |
| 72 } |
| 73 |
| 62 enc := []byte(rk.encode()) | 74 enc := []byte(rk.encode()) |
| 63 coll := t.collection() | 75 coll := t.collection() |
| 64 if item, _ := coll.Get(enc); item != nil { | 76 if item, _ := coll.Get(enc); item != nil { |
| 65 return storage.ErrExists | 77 return storage.ErrExists |
| 66 } | 78 } |
| 67 | 79 |
| 68 clone := make([]byte, len(d)) | 80 clone := make([]byte, len(d)) |
| 69 copy(clone, d) | 81 copy(clone, d) |
| 70 if err := coll.Set(enc, clone); err != nil { | 82 if err := coll.Set(enc, clone); err != nil { |
| 71 panic(err) | 83 panic(err) |
| (...skipping 12 matching lines...) Expand all Loading... |
| 84 err := t.collection().VisitItemsAscend(enc, !keysOnly, func(i *gkvlite.I
tem) bool { | 96 err := t.collection().VisitItemsAscend(enc, !keysOnly, func(i *gkvlite.I
tem) bool { |
| 85 var drk *rowKey | 97 var drk *rowKey |
| 86 drk, ierr = decodeRowKey(string(i.Key)) | 98 drk, ierr = decodeRowKey(string(i.Key)) |
| 87 if ierr != nil { | 99 if ierr != nil { |
| 88 return false | 100 return false |
| 89 } | 101 } |
| 90 if drk.pathPrefix() != prefix { | 102 if drk.pathPrefix() != prefix { |
| 91 return false | 103 return false |
| 92 } | 104 } |
| 93 | 105 |
| 94 » » if ierr = cb(drk, i.Val); ierr != nil { | 106 » » // If we're simulating legacy (no count), clear our row key's co
unt. |
| 107 » » if t.countLegacy { |
| 108 » » » drk.count = 0 |
| 109 » » } |
| 110 |
| 111 » » rowData := i.Val |
| 112 » » if keysOnly { |
| 113 » » » rowData = nil |
| 114 » » } |
| 115 |
| 116 » » if ierr = cb(drk, rowData); ierr != nil { |
| 95 if ierr == errStop { | 117 if ierr == errStop { |
| 96 ierr = nil | 118 ierr = nil |
| 97 } | 119 } |
| 98 return false | 120 return false |
| 99 } | 121 } |
| 100 | 122 |
| 101 if limit > 0 { | 123 if limit > 0 { |
| 102 limit-- | 124 limit-- |
| 103 if limit == 0 { | 125 if limit == 0 { |
| 104 return false | 126 return false |
| (...skipping 22 matching lines...) Expand all Loading... |
| 127 err := t.collection().VisitItemsAscend([]byte(nil), true, func(i *gkvlit
e.Item) bool { | 149 err := t.collection().VisitItemsAscend([]byte(nil), true, func(i *gkvlit
e.Item) bool { |
| 128 result[string(i.Key)] = i.Val | 150 result[string(i.Key)] = i.Val |
| 129 return true | 151 return true |
| 130 }) | 152 }) |
| 131 if err != nil { | 153 if err != nil { |
| 132 panic(err) | 154 panic(err) |
| 133 } | 155 } |
| 134 return result | 156 return result |
| 135 } | 157 } |
| 136 | 158 |
| 137 func TestStorage(t *testing.T) { | 159 func testStorageImpl(t *testing.T, legacy bool) { |
| 138 t.Parallel() | 160 t.Parallel() |
| 139 | 161 |
| 140 Convey(`A BigTable storage instance bound to a testing BigTable instance
`, t, func() { | 162 Convey(`A BigTable storage instance bound to a testing BigTable instance
`, t, func() { |
| 141 » » bt := btTableTest{} | 163 » » bt := btTableTest{ |
| 164 » » » countLegacy: legacy, |
| 165 » » } |
| 142 defer bt.close() | 166 defer bt.close() |
| 143 | 167 |
| 144 s := newBTStorage(context.Background(), Options{ | 168 s := newBTStorage(context.Background(), Options{ |
| 145 Project: "test-project", | 169 Project: "test-project", |
| 146 Zone: "test-zone", | 170 Zone: "test-zone", |
| 147 Cluster: "test-cluster", | 171 Cluster: "test-cluster", |
| 148 LogTable: "test-log-table", | 172 LogTable: "test-log-table", |
| 149 }, nil, nil) | 173 }, nil, nil) |
| 150 | 174 |
| 151 s.raw = &bt | 175 s.raw = &bt |
| (...skipping 19 matching lines...) Expand all Loading... |
| 171 data[i] = []byte(v) | 195 data[i] = []byte(v) |
| 172 } | 196 } |
| 173 | 197 |
| 174 return s.Put(storage.PutRequest{ | 198 return s.Put(storage.PutRequest{ |
| 175 Path: types.StreamPath(path), | 199 Path: types.StreamPath(path), |
| 176 Index: types.MessageIndex(index), | 200 Index: types.MessageIndex(index), |
| 177 Values: data, | 201 Values: data, |
| 178 }) | 202 }) |
| 179 } | 203 } |
| 180 | 204 |
| 181 » » ekey := func(p string, v int64) string { | 205 » » ekey := func(p string, v, c int64) string { |
| 182 » » » return newRowKey(p, v).encode() | 206 » » » return newRowKey(p, v, c).encode() |
| 183 } | 207 } |
| 184 records := func(s ...string) []byte { | 208 records := func(s ...string) []byte { |
| 185 buf := bytes.Buffer{} | 209 buf := bytes.Buffer{} |
| 186 w := recordio.NewWriter(&buf) | 210 w := recordio.NewWriter(&buf) |
| 187 | 211 |
| 188 for _, v := range s { | 212 for _, v := range s { |
| 189 if _, err := w.Write([]byte(v)); err != nil { | 213 if _, err := w.Write([]byte(v)); err != nil { |
| 190 panic(err) | 214 panic(err) |
| 191 } | 215 } |
| 192 if err := w.Flush(); err != nil { | 216 if err := w.Flush(); err != nil { |
| 193 panic(err) | 217 panic(err) |
| 194 } | 218 } |
| 195 } | 219 } |
| 196 | 220 |
| 197 return buf.Bytes() | 221 return buf.Bytes() |
| 198 } | 222 } |
| 199 | 223 |
| 200 Convey(`With an artificial maximum BigTable row size of two reco
rds`, func() { | 224 Convey(`With an artificial maximum BigTable row size of two reco
rds`, func() { |
| 201 // Artificially constrain row size. 4 = 2*{size/1, data/
1} RecordIO | 225 // Artificially constrain row size. 4 = 2*{size/1, data/
1} RecordIO |
| 202 // entries. | 226 // entries. |
| 203 s.maxRowSize = 4 | 227 s.maxRowSize = 4 |
| 204 | 228 |
| 205 Convey(`Will split row data that overflows the table int
o multiple rows.`, func() { | 229 Convey(`Will split row data that overflows the table int
o multiple rows.`, func() { |
| 206 So(put("A", 0, "0", "1", "2", "3"), ShouldBeNil) | 230 So(put("A", 0, "0", "1", "2", "3"), ShouldBeNil) |
| 207 | 231 |
| 208 So(bt.dataMap(), ShouldResemble, map[string][]by
te{ | 232 So(bt.dataMap(), ShouldResemble, map[string][]by
te{ |
| 209 » » » » » ekey("A", 1): records("0", "1"), | 233 » » » » » ekey("A", 1, 2): records("0", "1"), |
| 210 » » » » » ekey("A", 3): records("2", "3"), | 234 » » » » » ekey("A", 3, 2): records("2", "3"), |
| 211 }) | 235 }) |
| 212 }) | 236 }) |
| 213 | 237 |
| 214 Convey(`Loading a single row data beyond the maximum row
size will fail.`, func() { | 238 Convey(`Loading a single row data beyond the maximum row
size will fail.`, func() { |
| 215 So(put("A", 0, "0123"), ShouldErrLike, "single r
ow entry exceeds maximum size") | 239 So(put("A", 0, "0123"), ShouldErrLike, "single r
ow entry exceeds maximum size") |
| 216 }) | 240 }) |
| 217 }) | 241 }) |
| 218 | 242 |
| 219 Convey(`With row data: A{0, 1, 2, 3, 4}, B{10, 12, 13}`, func()
{ | 243 Convey(`With row data: A{0, 1, 2, 3, 4}, B{10, 12, 13}`, func()
{ |
| 220 So(put("A", 0, "0", "1", "2"), ShouldBeNil) | 244 So(put("A", 0, "0", "1", "2"), ShouldBeNil) |
| 221 So(put("A", 3, "3", "4"), ShouldBeNil) | 245 So(put("A", 3, "3", "4"), ShouldBeNil) |
| 222 So(put("B", 10, "10"), ShouldBeNil) | 246 So(put("B", 10, "10"), ShouldBeNil) |
| 223 So(put("B", 12, "12", "13"), ShouldBeNil) | 247 So(put("B", 12, "12", "13"), ShouldBeNil) |
| 224 | 248 |
| 225 Convey(`Testing "Put"...`, func() { | 249 Convey(`Testing "Put"...`, func() { |
| 226 Convey(`Loads the row data.`, func() { | 250 Convey(`Loads the row data.`, func() { |
| 227 So(bt.dataMap(), ShouldResemble, map[str
ing][]byte{ | 251 So(bt.dataMap(), ShouldResemble, map[str
ing][]byte{ |
| 228 » » » » » » ekey("A", 2): records("0", "1",
"2"), | 252 » » » » » » ekey("A", 2, 3): records("0", "
1", "2"), |
| 229 » » » » » » ekey("A", 4): records("3", "4")
, | 253 » » » » » » ekey("A", 4, 2): records("3", "
4"), |
| 230 » » » » » » ekey("B", 10): records("10"), | 254 » » » » » » ekey("B", 10, 1): records("10"), |
| 231 » » » » » » ekey("B", 13): records("12", "13
"), | 255 » » » » » » ekey("B", 13, 2): records("12",
"13"), |
| 232 }) | 256 }) |
| 233 }) | 257 }) |
| 234 }) | 258 }) |
| 235 | 259 |
| 236 Convey(`Testing "Get"...`, func() { | 260 Convey(`Testing "Get"...`, func() { |
| 237 Convey(`Can fetch the full row, "A".`, func() { | 261 Convey(`Can fetch the full row, "A".`, func() { |
| 238 got, err := get("A", 0, 0) | 262 got, err := get("A", 0, 0) |
| 239 So(err, ShouldBeNil) | 263 So(err, ShouldBeNil) |
| 240 So(got, ShouldResemble, []string{"0", "1
", "2", "3", "4"}) | 264 So(got, ShouldResemble, []string{"0", "1
", "2", "3", "4"}) |
| 241 }) | 265 }) |
| 242 | 266 |
| 243 » » » » Convey(`Will fetch A{1, 2, 3, 4} with when index
=1.`, func() { | 267 » » » » Convey(`Will fetch A{1, 2, 3, 4} with index=1.`,
func() { |
| 244 got, err := get("A", 1, 0) | 268 got, err := get("A", 1, 0) |
| 245 So(err, ShouldBeNil) | 269 So(err, ShouldBeNil) |
| 246 So(got, ShouldResemble, []string{"1", "2
", "3", "4"}) | 270 So(got, ShouldResemble, []string{"1", "2
", "3", "4"}) |
| 247 }) | 271 }) |
| 248 | 272 |
| 249 » » » » Convey(`Will fetch A{1, 2} with when index=1 and
limit=2.`, func() { | 273 » » » » Convey(`Will fetch A{1, 2} with index=1 and limi
t=2.`, func() { |
| 250 got, err := get("A", 1, 2) | 274 got, err := get("A", 1, 2) |
| 251 So(err, ShouldBeNil) | 275 So(err, ShouldBeNil) |
| 252 So(got, ShouldResemble, []string{"1", "2
"}) | 276 So(got, ShouldResemble, []string{"1", "2
"}) |
| 253 }) | 277 }) |
| 254 | 278 |
| 255 Convey(`Will fetch B{10, 12, 13} for B.`, func()
{ | 279 Convey(`Will fetch B{10, 12, 13} for B.`, func()
{ |
| 256 got, err := get("B", 0, 0) | 280 got, err := get("B", 0, 0) |
| 257 So(err, ShouldBeNil) | 281 So(err, ShouldBeNil) |
| 258 So(got, ShouldResemble, []string{"10", "
12", "13"}) | 282 So(got, ShouldResemble, []string{"10", "
12", "13"}) |
| 259 }) | 283 }) |
| (...skipping 28 matching lines...) Expand all Loading... |
| 288 So(err, ShouldBeNil) | 312 So(err, ShouldBeNil) |
| 289 So(got, ShouldEqual, "13") | 313 So(got, ShouldEqual, "13") |
| 290 }) | 314 }) |
| 291 | 315 |
| 292 Convey(`A tail request for "INVALID" errors NOT
FOUND.`, func() { | 316 Convey(`A tail request for "INVALID" errors NOT
FOUND.`, func() { |
| 293 _, err := tail("INVALID") | 317 _, err := tail("INVALID") |
| 294 So(err, ShouldEqual, storage.ErrDoesNotE
xist) | 318 So(err, ShouldEqual, storage.ErrDoesNotE
xist) |
| 295 }) | 319 }) |
| 296 }) | 320 }) |
| 297 }) | 321 }) |
| 298 | |
| 299 Convey(`Given a fake BigTable row`, func() { | |
| 300 fakeRow := bigtable.Row{ | |
| 301 "log": []bigtable.ReadItem{ | |
| 302 { | |
| 303 Row: "testrow", | |
| 304 Column: "log:data", | |
| 305 Value: []byte("here is my data"
), | |
| 306 }, | |
| 307 }, | |
| 308 } | |
| 309 | |
| 310 Convey(`Can extract log data.`, func() { | |
| 311 d, err := getLogData(fakeRow) | |
| 312 So(err, ShouldBeNil) | |
| 313 So(d, ShouldResemble, []byte("here is my data")) | |
| 314 }) | |
| 315 | |
| 316 Convey(`Will fail to extract if the column is missing.`,
func() { | |
| 317 fakeRow["log"][0].Column = "not-data" | |
| 318 _, err := getLogData(fakeRow) | |
| 319 So(err, ShouldEqual, storage.ErrDoesNotExist) | |
| 320 }) | |
| 321 | |
| 322 Convey(`Will fail to extract if the family does not exis
t.`, func() { | |
| 323 So(getReadItem(fakeRow, "invalid", "invalid"), S
houldBeNil) | |
| 324 }) | |
| 325 | |
| 326 Convey(`Will fail to extract if the column does not exis
t.`, func() { | |
| 327 So(getReadItem(fakeRow, "log", "invalid"), Shoul
dBeNil) | |
| 328 }) | |
| 329 }) | |
| 330 | |
| 331 Convey(`When pushing a configuration`, func() { | |
| 332 cfg := storage.Config{ | |
| 333 MaxLogAge: 1 * time.Hour, | |
| 334 } | |
| 335 | |
| 336 Convey(`Can successfully apply configuration.`, func() { | |
| 337 So(s.Config(cfg), ShouldBeNil) | |
| 338 So(bt.maxLogAge, ShouldEqual, cfg.MaxLogAge) | |
| 339 }) | |
| 340 | |
| 341 Convey(`With return an error if the configuration fails
to apply.`, func() { | |
| 342 bt.err = errors.New("test error") | |
| 343 | |
| 344 So(s.Config(cfg), ShouldEqual, bt.err) | |
| 345 }) | |
| 346 }) | |
| 347 }) | 322 }) |
| 348 } | 323 } |
| 324 |
| 325 func TestStorage(t *testing.T) { |
| 326 testStorageImpl(t, false) |
| 327 } |
| 328 |
| 329 func TestStorageLegacy(t *testing.T) { |
| 330 testStorageImpl(t, true) |
| 331 } |
| OLD | NEW |