Chromium Code Reviews| 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 |
| 133 type uploaderUploadFileArgs struct { | |
| 134 item *Item | |
| 135 ps *isolatedclient.PushState | |
| 136 } | |
| 137 | |
| 119 func (uploader *fakeUploader) UploadBytes(relPath string, isolJSON []byte, ps *i solatedclient.PushState, f func()) { | 138 func (uploader *fakeUploader) UploadBytes(relPath string, isolJSON []byte, ps *i solatedclient.PushState, f func()) { |
| 120 uploader.uploadBytesCalls = append(uploader.uploadBytesCalls, uploaderUp loadBytesArgs{relPath, isolJSON, ps}) | 139 uploader.uploadBytesCalls = append(uploader.uploadBytesCalls, uploaderUp loadBytesArgs{relPath, isolJSON, ps}) |
| 121 } | 140 } |
| 122 | 141 |
| 142 func (uploader *fakeUploader) UploadFile(item *Item, ps *isolatedclient.PushStat e, done func()) { | |
| 143 uploader.uploadFileCalls = append(uploader.uploadFileCalls, uploaderUplo adFileArgs{item, ps}) | |
|
mithro
2017/08/01 04:45:42
Do you ever call the done function? Don't you need
mcgreevy
2017/08/02 03:59:30
I didn't bother because the done function is only
| |
| 144 } | |
| 123 func (uploader *fakeUploader) Close() error { return nil } | 145 func (uploader *fakeUploader) Close() error { return nil } |
| 124 | 146 |
| 125 func TestSkipsUpload(t *testing.T) { | 147 func TestSkipsUpload(t *testing.T) { |
| 126 t.Parallel() | 148 t.Parallel() |
| 127 Convey(`nil push state signals that upload should be skipped`, t, func() { | 149 Convey(`nil push state signals that upload should be skipped`, t, func() { |
| 128 isol := &isolated.Isolated{} | 150 isol := &isolated.Isolated{} |
| 129 | 151 |
| 130 // nil PushState means skip the upload. | 152 // nil PushState means skip the upload. |
| 131 checker := &fakeChecker{ps: nil} | 153 checker := &fakeChecker{ps: nil} |
| 132 | 154 |
| 133 uploader := &fakeUploader{} | 155 uploader := &fakeUploader{} |
| 134 | 156 |
| 135 ut := NewUploadTracker(checker, uploader, isol) | 157 ut := NewUploadTracker(checker, uploader, isol) |
| 136 fos := &fakeOS{} | 158 fos := &fakeOS{} |
| 137 ut.lOS = fos // Override filesystem calls with fake. | 159 ut.lOS = fos // Override filesystem calls with fake. |
| 138 | 160 |
| 139 parts := partitionedDeps{} // no actual deps. | 161 parts := partitionedDeps{} // no actual deps. |
| 140 err := ut.UploadDeps(parts) | 162 err := ut.UploadDeps(parts) |
| 141 So(err, ShouldBeNil) | 163 So(err, ShouldBeNil) |
| 142 | 164 |
| 143 // No deps, so Files should be empty. | 165 // No deps, so Files should be empty. |
| 144 wantFiles := map[string]isolated.File{} | 166 wantFiles := map[string]isolated.File{} |
| 145 So(ut.isol.Files, ShouldResemble, wantFiles) | 167 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 146 | 168 |
| 147 isolSummary, err := ut.Finalize("/a/isolatedPath") | 169 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 148 So(err, ShouldBeNil) | 170 So(err, ShouldBeNil) |
| 149 | 171 |
| 150 // In this test, the only item that is checked and uploaded is t he generated isolated file. | 172 // In this test, the only item that is checked and uploaded is t he generated isolated file. |
| 151 wantIsolJSON := []byte(`{"algo":"","version":""}`) | 173 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 152 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola tedPath": wantIsolJSON}) | 174 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/ isolatedPath": wantIsolJSON}) |
| 153 | 175 |
| 154 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) | 176 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) |
| 155 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 177 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 156 | 178 |
| 157 So(uploader.uploadBytesCalls, ShouldEqual, nil) | 179 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 158 }) | 180 }) |
| 159 } | 181 } |
| 160 | 182 |
| 161 func TestDontSkipUpload(t *testing.T) { | 183 func TestDontSkipUpload(t *testing.T) { |
| 162 t.Parallel() | 184 t.Parallel() |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 178 | 200 |
| 179 // No deps, so Files should be empty. | 201 // No deps, so Files should be empty. |
| 180 wantFiles := map[string]isolated.File{} | 202 wantFiles := map[string]isolated.File{} |
| 181 So(ut.isol.Files, ShouldResemble, wantFiles) | 203 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 182 | 204 |
| 183 isolSummary, err := ut.Finalize("/a/isolatedPath") | 205 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 184 So(err, ShouldBeNil) | 206 So(err, ShouldBeNil) |
| 185 | 207 |
| 186 // In this test, the only item that is checked and uploaded is t he generated isolated file. | 208 // In this test, the only item that is checked and uploaded is t he generated isolated file. |
| 187 wantIsolJSON := []byte(`{"algo":"","version":""}`) | 209 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 188 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola tedPath": wantIsolJSON}) | 210 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/ isolatedPath": wantIsolJSON}) |
|
mithro
2017/08/01 04:45:42
Thought to consider (no action needed in this CL..
mcgreevy
2017/08/02 03:59:30
I notice that after I uploaded this CL. But then
| |
| 189 | 211 |
| 190 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) | 212 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) |
| 191 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 213 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 192 | 214 |
| 193 // Upload was not skipped. | 215 // Upload was not skipped. |
| 194 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy tesArgs{ | 216 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy tesArgs{ |
| 195 {"isolatedPath", wantIsolJSON, checker.ps}, | 217 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 196 }) | 218 }) |
| 197 }) | 219 }) |
| 198 } | 220 } |
| (...skipping 30 matching lines...) Expand all Loading... | |
| 229 So(ut.isol.Files, ShouldResemble, wantFiles) | 251 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 230 | 252 |
| 231 // Symlinks are not uploaded. | 253 // Symlinks are not uploaded. |
| 232 So(uploader.uploadBytesCalls, ShouldEqual, nil) | 254 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 233 | 255 |
| 234 isolSummary, err := ut.Finalize("/a/isolatedPath") | 256 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 235 So(err, ShouldBeNil) | 257 So(err, ShouldBeNil) |
| 236 | 258 |
| 237 // In this test, the only item that is checked and uploaded is t he generated isolated file. | 259 // 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":""}`) | 260 wantIsolJSON := []byte(`{"algo":"","files":{"c":{"l":"link:/a/b/ c"}},"version":""}`) |
| 239 » » So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola tedPath": wantIsolJSON}) | 261 » » So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/ isolatedPath": wantIsolJSON}) |
| 240 | 262 |
| 241 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) | 263 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) |
| 242 So(isolSummary.Name, ShouldEqual, "isolatedPath") | 264 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 243 | 265 |
| 244 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy tesArgs{ | 266 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy tesArgs{ |
| 245 {"isolatedPath", wantIsolJSON, checker.ps}, | 267 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 246 }) | 268 }) |
| 247 }) | 269 }) |
| 248 } | 270 } |
| 271 | |
| 272 func TestHandlesIndividualFiles(t *testing.T) { | |
| 273 t.Parallel() | |
| 274 Convey(`Individual files should be stored in the isolated json and uploa ded`, t, func() { | |
| 275 isol := &isolated.Isolated{} | |
| 276 | |
| 277 // non-nil PushState means don't skip the upload. | |
|
mithro
2017/08/01 04:45:42
I'm a bit confused at how the push state is being
mcgreevy
2017/08/02 03:59:30
The PushState is not really meant to be inspected
| |
| 278 pushState := &isolatedclient.PushState{} | |
| 279 checker := &fakeChecker{ps: pushState} | |
| 280 uploader := &fakeUploader{} | |
| 281 | |
| 282 ut := NewUploadTracker(checker, uploader, isol) | |
| 283 fos := &fakeOS{ | |
| 284 readFiles: map[string]io.Reader{ | |
| 285 "/a/b/foo": strings.NewReader("foo contents"), | |
| 286 "/a/b/bar": strings.NewReader("bar contents"), | |
| 287 }, | |
| 288 } | |
| 289 ut.lOS = fos // Override filesystem calls with fake. | |
| 290 | |
| 291 parts := partitionedDeps{ | |
| 292 indivFiles: itemGroup{ | |
| 293 items: []*Item{ | |
| 294 {Path: "/a/b/foo", RelPath: "foo", Size: 1, Mode: 006}, | |
| 295 {Path: "/a/b/bar", RelPath: "bar", Size: 2, Mode: 006}, | |
| 296 }, | |
| 297 totalSize: 3, | |
| 298 }, | |
| 299 } | |
| 300 err := ut.UploadDeps(parts) | |
| 301 So(err, ShouldBeNil) | |
| 302 | |
| 303 fooHash := isolated.HashBytes([]byte("foo contents")) | |
| 304 barHash := isolated.HashBytes([]byte("bar contents")) | |
| 305 wantFiles := map[string]isolated.File{ | |
| 306 "foo": { | |
| 307 Digest: fooHash, | |
| 308 Mode: Int(6), | |
| 309 Size: Int64(1)}, | |
| 310 "bar": { | |
| 311 Digest: barHash, | |
| 312 Mode: Int(6), | |
| 313 Size: Int64(2)}, | |
| 314 } | |
| 315 | |
| 316 So(ut.isol.Files, ShouldResemble, wantFiles) | |
| 317 | |
| 318 So(uploader.uploadBytesCalls, ShouldEqual, nil) | |
| 319 | |
| 320 isolSummary, err := ut.Finalize("/a/isolatedPath") | |
| 321 So(err, ShouldBeNil) | |
| 322 | |
| 323 wantIsolJSONTmpl := `{"algo":"","files":{"bar":{"h":"%s","m":6," s":2},"foo":{"h":"%s","m":6,"s":1}},"version":""}` | |
| 324 wantIsolJSON := []byte(fmt.Sprintf(wantIsolJSONTmpl, barHash, fo oHash)) | |
|
mithro
2017/08/01 04:45:42
Is there any reason particular reason to define wa
mcgreevy
2017/08/02 03:59:29
Line length.
It's not Go style to split function
| |
| 325 So(fos.writeFiles, shouldResembleByteMap, map[string][]byte{"/a/ isolatedPath": wantIsolJSON}) | |
| 326 | |
| 327 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs olJSON)) | |
| 328 So(isolSummary.Name, ShouldEqual, "isolatedPath") | |
| 329 | |
| 330 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy tesArgs{ | |
| 331 {"isolatedPath", wantIsolJSON, checker.ps}, | |
| 332 }) | |
| 333 So(uploader.uploadFileCalls, ShouldResemble, []uploaderUploadFil eArgs{ | |
| 334 { | |
| 335 item: &Item{ | |
| 336 Path: "/a/b/foo", | |
| 337 RelPath: "foo", | |
| 338 Size: 1, | |
| 339 Mode: os.FileMode(6), | |
| 340 Digest: fooHash, | |
| 341 }, | |
| 342 ps: pushState, | |
| 343 }, | |
| 344 { | |
| 345 item: &Item{ | |
| 346 Path: "/a/b/bar", | |
| 347 RelPath: "bar", | |
| 348 Size: 2, | |
| 349 Mode: os.FileMode(6), | |
|
mithro
2017/08/01 04:45:42
You should be trying with different File modes may
mcgreevy
2017/08/02 03:59:30
Done.
| |
| 350 Digest: barHash, | |
| 351 }, | |
| 352 ps: pushState, | |
| 353 }, | |
| 354 }) | |
| 355 }) | |
| 356 } | |
| 357 | |
| 358 func Int(i int) *int { return &i } | |
| 359 func Int64(i int64) *int64 { return &i } | |
|
mithro
2017/08/01 04:45:43
Could you add a comment about why these casts are
mcgreevy
2017/08/02 03:59:30
Done.
FYI they're not casts, they're just helpers
| |
| OLD | NEW |