| OLD | NEW |
| 1 // Copyright 2017 The LUCI Authors. | 1 // Copyright 2017 The LUCI Authors. |
| 2 // | 2 // |
| 3 // Licensed under the Apache License, Version 2.0 (the "License"); | 3 // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 // you may not use this file except in compliance with the License. | 4 // you may not use this file except in compliance with the License. |
| 5 // You may obtain a copy of the License at | 5 // You may obtain a copy of the License at |
| 6 // | 6 // |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 | 7 // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 // | 8 // |
| 9 // Unless required by applicable law or agreed to in writing, software | 9 // Unless required by applicable law or agreed to in writing, software |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, | 10 // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 // See the License for the specific language governing permissions and | 12 // See the License for the specific language governing permissions and |
| 13 // limitations under the License. | 13 // limitations under the License. |
| 14 | 14 |
| 15 package main | 15 package main |
| 16 | 16 |
| 17 import ( | 17 import ( |
| 18 "bytes" | 18 "bytes" |
| 19 "fmt" | 19 "fmt" |
| 20 "io" | 20 "io" |
| 21 "io/ioutil" |
| 21 "os" | 22 "os" |
| 22 "reflect" | 23 "reflect" |
| 24 "strings" |
| 23 "testing" | 25 "testing" |
| 24 | 26 |
| 25 "github.com/luci/luci-go/common/isolated" | 27 "github.com/luci/luci-go/common/isolated" |
| 26 "github.com/luci/luci-go/common/isolatedclient" | 28 "github.com/luci/luci-go/common/isolatedclient" |
| 27 . "github.com/smartystreets/goconvey/convey" | 29 . "github.com/smartystreets/goconvey/convey" |
| 28 ) | 30 ) |
| 29 | 31 |
| 30 // Fake OS imitiates a filesystem by storing file contents in a map. | 32 // Fake OS imitiates a filesystem by storing file contents in a map. |
| 31 // It also provides a dummy Readlink implementation. | 33 // It also provides a dummy Readlink implementation. |
| 32 type fakeOS struct { | 34 type fakeOS struct { |
| 33 » files map[string]*bytes.Buffer | 35 » writeFiles map[string]*bytes.Buffer |
| 36 » readFiles map[string]io.Reader |
| 34 } | 37 } |
| 35 | 38 |
| 36 // shouldResembleByteMap asserts that actual (a map[string]*bytes.Buffer) contai
ns | 39 // shouldResembleByteMap asserts that actual (a map[string]*bytes.Buffer) contai
ns |
| 37 // the same data as expected (a map[string][]byte). | 40 // the same data as expected (a map[string][]byte). |
| 38 // The types of actual and expected differ to make writing tests with fakeOS mor
e convenient. | 41 // The types of actual and expected differ to make writing tests with fakeOS mor
e convenient. |
| 39 func shouldResembleByteMap(actual interface{}, expected ...interface{}) string { | 42 func shouldResembleByteMap(actual interface{}, expected ...interface{}) string { |
| 40 act, ok := actual.(map[string]*bytes.Buffer) | 43 act, ok := actual.(map[string]*bytes.Buffer) |
| 41 if !ok { | 44 if !ok { |
| 42 return "actual is not a map[string]*bytes.Buffer" | 45 return "actual is not a map[string]*bytes.Buffer" |
| 43 } | 46 } |
| (...skipping 22 matching lines...) Expand all Loading... |
| 66 } | 69 } |
| 67 | 70 |
| 68 type nopWriteCloser struct { | 71 type nopWriteCloser struct { |
| 69 io.Writer | 72 io.Writer |
| 70 } | 73 } |
| 71 | 74 |
| 72 func (nopWriteCloser) Close() error { return nil } | 75 func (nopWriteCloser) Close() error { return nil } |
| 73 | 76 |
| 74 // implements OpenFile by returning a writer that writes to a bytes.Buffer. | 77 // implements OpenFile by returning a writer that writes to a bytes.Buffer. |
| 75 func (fos *fakeOS) OpenFile(name string, flag int, perm os.FileMode) (io.WriteCl
oser, error) { | 78 func (fos *fakeOS) OpenFile(name string, flag int, perm os.FileMode) (io.WriteCl
oser, error) { |
| 76 » if fos.files == nil { | 79 » if fos.writeFiles == nil { |
| 77 » » fos.files = make(map[string]*bytes.Buffer) | 80 » » fos.writeFiles = make(map[string]*bytes.Buffer) |
| 78 } | 81 } |
| 79 » fos.files[name] = &bytes.Buffer{} | 82 » fos.writeFiles[name] = &bytes.Buffer{} |
| 80 » return nopWriteCloser{fos.files[name]}, nil | 83 » return nopWriteCloser{fos.writeFiles[name]}, nil |
| 84 } |
| 85 |
| 86 // implements Open by returning a pre-configured Reader. |
| 87 func (fos *fakeOS) Open(name string) (io.ReadCloser, error) { |
| 88 » r, ok := fos.readFiles[name] |
| 89 » if !ok { |
| 90 » » panic(fmt.Sprintf("fakeOS: file not found (%s); not implemented.
", name)) |
| 91 » } |
| 92 » return ioutil.NopCloser(r), nil |
| 81 } | 93 } |
| 82 | 94 |
| 83 // fakeChecker implements Checker by responding to method invocations by | 95 // fakeChecker implements Checker by responding to method invocations by |
| 84 // invoking the supplied callback with the supplied item, and a hard-coded *Push
State. | 96 // invoking the supplied callback with the supplied item, and a hard-coded *Push
State. |
| 85 type fakeChecker struct { | 97 type fakeChecker struct { |
| 86 ps *isolatedclient.PushState | 98 ps *isolatedclient.PushState |
| 87 } | 99 } |
| 88 | 100 |
| 89 type checkerAddItemArgs struct { | 101 type checkerAddItemArgs struct { |
| 90 item *Item | 102 item *Item |
| 91 isolated bool | 103 isolated bool |
| 92 } | 104 } |
| 93 | 105 |
| 94 type checkerAddItemResponse struct { | 106 type checkerAddItemResponse struct { |
| 95 item *Item | 107 item *Item |
| 96 ps *isolatedclient.PushState | 108 ps *isolatedclient.PushState |
| 97 } | 109 } |
| 98 | 110 |
| 99 func (checker *fakeChecker) AddItem(item *Item, isolated bool, callback CheckerC
allback) { | 111 func (checker *fakeChecker) AddItem(item *Item, isolated bool, callback CheckerC
allback) { |
| 100 callback(item, checker.ps) | 112 callback(item, checker.ps) |
| 101 } | 113 } |
| 102 | 114 |
| 103 func (checker *fakeChecker) Close() error { return nil } | 115 func (checker *fakeChecker) Close() error { return nil } |
| 104 | 116 |
| 105 // fakeChecker implements Uploader while recording method arguments. | 117 // fakeChecker implements Uploader while recording method arguments. |
| 106 type fakeUploader struct { | 118 type fakeUploader struct { |
| 107 // uploadBytesCalls is a record of the arguments to each call of UploadB
ytes. | 119 // uploadBytesCalls is a record of the arguments to each call of UploadB
ytes. |
| 108 uploadBytesCalls []uploaderUploadBytesArgs | 120 uploadBytesCalls []uploaderUploadBytesArgs |
| 121 // uploadFileCalls is a record of the arguments to each call of UploadFi
le. |
| 122 uploadFileCalls []uploaderUploadFileArgs |
| 109 | 123 |
| 110 Uploader // TODO(mcgreevy): implement other methods. | 124 Uploader // TODO(mcgreevy): implement other methods. |
| 111 } | 125 } |
| 112 | 126 |
| 113 type uploaderUploadBytesArgs struct { | 127 type uploaderUploadBytesArgs struct { |
| 114 relPath string | 128 relPath string |
| 115 isolJSON []byte | 129 isolJSON []byte |
| 116 ps *isolatedclient.PushState | 130 ps *isolatedclient.PushState |
| 117 } | 131 } |
| 118 | 132 |
| 119 func (uploader *fakeUploader) UploadBytes(relPath string, isolJSON []byte, ps *i
solatedclient.PushState, f func()) { | 133 type uploaderUploadFileArgs struct { |
| 120 » uploader.uploadBytesCalls = append(uploader.uploadBytesCalls, uploaderUp
loadBytesArgs{relPath, isolJSON, ps}) | 134 » item *Item |
| 135 » ps *isolatedclient.PushState |
| 121 } | 136 } |
| 122 | 137 |
| 138 func (uploader *fakeUploader) UploadBytes(relPath string, isolJSON []byte, ps *i
solatedclient.PushState, done func()) { |
| 139 uploader.uploadBytesCalls = append(uploader.uploadBytesCalls, uploaderUp
loadBytesArgs{relPath, isolJSON, ps}) |
| 140 done() |
| 141 } |
| 142 |
| 143 func (uploader *fakeUploader) UploadFile(item *Item, ps *isolatedclient.PushStat
e, done func()) { |
| 144 uploader.uploadFileCalls = append(uploader.uploadFileCalls, uploaderUplo
adFileArgs{item, ps}) |
| 145 done() |
| 146 } |
| 123 func (uploader *fakeUploader) Close() error { return nil } | 147 func (uploader *fakeUploader) Close() error { return nil } |
| 124 | 148 |
| 125 func TestSkipsUpload(t *testing.T) { | 149 func TestSkipsUpload(t *testing.T) { |
| 126 t.Parallel() | 150 t.Parallel() |
| 127 Convey(`nil push state signals that upload should be skipped`, t, func()
{ | 151 Convey(`nil push state signals that upload should be skipped`, t, func()
{ |
| 128 isol := &isolated.Isolated{} | 152 isol := &isolated.Isolated{} |
| 129 | 153 |
| 130 // nil PushState means skip the upload. | 154 // nil PushState means skip the upload. |
| 131 checker := &fakeChecker{ps: nil} | 155 checker := &fakeChecker{ps: nil} |
| 132 | 156 |
| 133 uploader := &fakeUploader{} | 157 uploader := &fakeUploader{} |
| 134 | 158 |
| 135 ut := NewUploadTracker(checker, uploader, isol) | 159 ut := NewUploadTracker(checker, uploader, isol) |
| 136 fos := &fakeOS{} | 160 fos := &fakeOS{} |
| 137 ut.lOS = fos // Override filesystem calls with fake. | 161 ut.lOS = fos // Override filesystem calls with fake. |
| 138 | 162 |
| 139 parts := partitionedDeps{} // no actual deps. | 163 parts := partitionedDeps{} // no actual deps. |
| 140 err := ut.UploadDeps(parts) | 164 err := ut.UploadDeps(parts) |
| 141 So(err, ShouldBeNil) | 165 So(err, ShouldBeNil) |
| 142 | 166 |
| 143 // No deps, so Files should be empty. | 167 // No deps, so Files should be empty. |
| 144 wantFiles := map[string]isolated.File{} | 168 wantFiles := map[string]isolated.File{} |
| 145 So(ut.isol.Files, ShouldResemble, wantFiles) | 169 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 146 | 170 |
| 147 isolSummary, err := ut.Finalize("/a/isolatedPath") | 171 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 148 So(err, ShouldBeNil) | 172 So(err, ShouldBeNil) |
| 149 | 173 |
| 150 // In this test, the only item that is checked and uploaded is t
he generated isolated file. | 174 // In this test, the only item that is checked and uploaded is t
he generated isolated file. |
| 151 wantIsolJSON := []byte(`{"algo":"","version":""}`) | 175 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 152 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) | 176 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/
isolatedPath": wantIsolJSON}) |
| 153 | 177 |
| 154 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) | 178 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 155 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 179 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 156 | 180 |
| 157 So(uploader.uploadBytesCalls, ShouldEqual, nil) | 181 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 158 }) | 182 }) |
| 159 } | 183 } |
| 160 | 184 |
| 161 func TestDontSkipUpload(t *testing.T) { | 185 func TestDontSkipUpload(t *testing.T) { |
| 162 t.Parallel() | 186 t.Parallel() |
| (...skipping 15 matching lines...) Expand all Loading... |
| 178 | 202 |
| 179 // No deps, so Files should be empty. | 203 // No deps, so Files should be empty. |
| 180 wantFiles := map[string]isolated.File{} | 204 wantFiles := map[string]isolated.File{} |
| 181 So(ut.isol.Files, ShouldResemble, wantFiles) | 205 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 182 | 206 |
| 183 isolSummary, err := ut.Finalize("/a/isolatedPath") | 207 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 184 So(err, ShouldBeNil) | 208 So(err, ShouldBeNil) |
| 185 | 209 |
| 186 // In this test, the only item that is checked and uploaded is t
he generated isolated file. | 210 // In this test, the only item that is checked and uploaded is t
he generated isolated file. |
| 187 wantIsolJSON := []byte(`{"algo":"","version":""}`) | 211 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 188 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) | 212 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/
isolatedPath": wantIsolJSON}) |
| 189 | 213 |
| 190 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) | 214 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 191 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 215 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 192 | 216 |
| 193 // Upload was not skipped. | 217 // Upload was not skipped. |
| 194 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ | 218 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ |
| 195 {"isolatedPath", wantIsolJSON, checker.ps}, | 219 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 196 }) | 220 }) |
| 197 }) | 221 }) |
| 198 } | 222 } |
| (...skipping 30 matching lines...) Expand all Loading... |
| 229 So(ut.isol.Files, ShouldResemble, wantFiles) | 253 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 230 | 254 |
| 231 // Symlinks are not uploaded. | 255 // Symlinks are not uploaded. |
| 232 So(uploader.uploadBytesCalls, ShouldEqual, nil) | 256 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 233 | 257 |
| 234 isolSummary, err := ut.Finalize("/a/isolatedPath") | 258 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 235 So(err, ShouldBeNil) | 259 So(err, ShouldBeNil) |
| 236 | 260 |
| 237 // In this test, the only item that is checked and uploaded is t
he generated isolated file. | 261 // In this test, the only item that is checked and uploaded is t
he generated isolated file. |
| 238 wantIsolJSON := []byte(`{"algo":"","files":{"c":{"l":"link:/a/b/
c"}},"version":""}`) | 262 wantIsolJSON := []byte(`{"algo":"","files":{"c":{"l":"link:/a/b/
c"}},"version":""}`) |
| 239 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) | 263 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/
isolatedPath": wantIsolJSON}) |
| 240 | 264 |
| 241 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) | 265 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 242 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 266 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 267 |
| 268 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ |
| 269 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 270 }) |
| 271 }) |
| 272 } |
| 273 |
| 274 func TestHandlesIndividualFiles(t *testing.T) { |
| 275 t.Parallel() |
| 276 Convey(`Individual files should be stored in the isolated json and uploa
ded`, t, func() { |
| 277 isol := &isolated.Isolated{} |
| 278 |
| 279 // non-nil PushState means don't skip the upload. |
| 280 pushState := &isolatedclient.PushState{} |
| 281 checker := &fakeChecker{ps: pushState} |
| 282 uploader := &fakeUploader{} |
| 283 |
| 284 ut := NewUploadTracker(checker, uploader, isol) |
| 285 fos := &fakeOS{ |
| 286 readFiles: map[string]io.Reader{ |
| 287 "/a/b/foo": strings.NewReader("foo contents"), |
| 288 "/a/b/bar": strings.NewReader("bar contents"), |
| 289 }, |
| 290 } |
| 291 ut.lOS = fos // Override filesystem calls with fake. |
| 292 |
| 293 parts := partitionedDeps{ |
| 294 indivFiles: itemGroup{ |
| 295 items: []*Item{ |
| 296 {Path: "/a/b/foo", RelPath: "foo", Size:
1, Mode: 004}, |
| 297 {Path: "/a/b/bar", RelPath: "bar", Size:
2, Mode: 006}, |
| 298 }, |
| 299 totalSize: 3, |
| 300 }, |
| 301 } |
| 302 err := ut.UploadDeps(parts) |
| 303 So(err, ShouldBeNil) |
| 304 |
| 305 fooHash := isolated.HashBytes([]byte("foo contents")) |
| 306 barHash := isolated.HashBytes([]byte("bar contents")) |
| 307 wantFiles := map[string]isolated.File{ |
| 308 "foo": { |
| 309 Digest: fooHash, |
| 310 Mode: Int(4), |
| 311 Size: Int64(1)}, |
| 312 "bar": { |
| 313 Digest: barHash, |
| 314 Mode: Int(6), |
| 315 Size: Int64(2)}, |
| 316 } |
| 317 |
| 318 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 319 |
| 320 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 321 |
| 322 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 323 So(err, ShouldBeNil) |
| 324 |
| 325 wantIsolJSONTmpl := `{"algo":"","files":{"bar":{"h":"%s","m":6,"
s":2},"foo":{"h":"%s","m":4,"s":1}},"version":""}` |
| 326 wantIsolJSON := []byte(fmt.Sprintf(wantIsolJSONTmpl, barHash, fo
oHash)) |
| 327 So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/
isolatedPath": wantIsolJSON}) |
| 328 |
| 329 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 330 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 243 | 331 |
| 244 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ | 332 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ |
| 245 {"isolatedPath", wantIsolJSON, checker.ps}, | 333 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 246 }) | 334 }) |
| 335 So(uploader.uploadFileCalls, ShouldResemble, []uploaderUploadFil
eArgs{ |
| 336 { |
| 337 item: &Item{ |
| 338 Path: "/a/b/foo", |
| 339 RelPath: "foo", |
| 340 Size: 1, |
| 341 Mode: os.FileMode(4), |
| 342 Digest: fooHash, |
| 343 }, |
| 344 ps: pushState, |
| 345 }, |
| 346 { |
| 347 item: &Item{ |
| 348 Path: "/a/b/bar", |
| 349 RelPath: "bar", |
| 350 Size: 2, |
| 351 Mode: os.FileMode(6), |
| 352 Digest: barHash, |
| 353 }, |
| 354 ps: pushState, |
| 355 }, |
| 356 }) |
| 247 }) | 357 }) |
| 248 } | 358 } |
| 359 |
| 360 // Int is a helper routine that allocates a new int value to store v and returns
a pointer to it. |
| 361 func Int(i int) *int { return &i } |
| 362 |
| 363 // Int64 is a helper routine that allocates a new int64 value to store v and ret
urns a pointer to it. |
| 364 func Int64(i int64) *int64 { return &i } |
| OLD | NEW |