| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The LUCI Authors. |
| 2 // |
| 3 // Licensed under the Apache License, Version 2.0 (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 |
| 6 // |
| 7 // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 // |
| 9 // Unless required by applicable law or agreed to in writing, software |
| 10 // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 // See the License for the specific language governing permissions and |
| 13 // limitations under the License. |
| 14 |
| 15 package main |
| 16 |
| 17 import ( |
| 18 "bytes" |
| 19 "fmt" |
| 20 "io" |
| 21 "os" |
| 22 "reflect" |
| 23 "testing" |
| 24 |
| 25 "github.com/luci/luci-go/common/isolated" |
| 26 "github.com/luci/luci-go/common/isolatedclient" |
| 27 . "github.com/smartystreets/goconvey/convey" |
| 28 ) |
| 29 |
| 30 // Fake OS imitiates a filesystem by storing file contents in a map. |
| 31 // It also provides a dummy Readlink implementation. |
| 32 type fakeOS struct { |
| 33 files map[string]*bytes.Buffer |
| 34 } |
| 35 |
| 36 // shouldResembleByteMap asserts that actual (a map[string]*bytes.Buffer) contai
ns |
| 37 // 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. |
| 39 func shouldResembleByteMap(actual interface{}, expected ...interface{}) string { |
| 40 act, ok := actual.(map[string]*bytes.Buffer) |
| 41 if !ok { |
| 42 return "actual is not a map[string]*bytes.Buffer" |
| 43 } |
| 44 if len(expected) != 1 { |
| 45 return "expected is not a map[string][]byte" |
| 46 } |
| 47 exp, ok := expected[0].(map[string][]byte) |
| 48 if !ok { |
| 49 return "expected is not a map[string][]byte" |
| 50 } |
| 51 |
| 52 if len(act) != len(exp) { |
| 53 return fmt.Sprintf("len(actual) != len(expected): %v != %v", len
(act), len(exp)) |
| 54 } |
| 55 |
| 56 for k, v := range act { |
| 57 if got, want := v.Bytes(), exp[k]; !reflect.DeepEqual(got, want)
{ |
| 58 return fmt.Sprintf("actual[%q] != expected[%q]: %q != %q
", k, k, got, want) |
| 59 } |
| 60 } |
| 61 return "" |
| 62 } |
| 63 |
| 64 func (fos *fakeOS) Readlink(name string) (string, error) { |
| 65 return "link:" + name, nil |
| 66 } |
| 67 |
| 68 type nopWriteCloser struct { |
| 69 io.Writer |
| 70 } |
| 71 |
| 72 func (nopWriteCloser) Close() error { return nil } |
| 73 |
| 74 // 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) { |
| 76 if fos.files == nil { |
| 77 fos.files = make(map[string]*bytes.Buffer) |
| 78 } |
| 79 fos.files[name] = &bytes.Buffer{} |
| 80 return nopWriteCloser{fos.files[name]}, nil |
| 81 } |
| 82 |
| 83 // fakeChecker implements Checker by responding to method invocations by |
| 84 // invoking the supplied callback with the supplied item, and a hard-coded *Push
State. |
| 85 type fakeChecker struct { |
| 86 ps *isolatedclient.PushState |
| 87 } |
| 88 |
| 89 type checkerAddItemArgs struct { |
| 90 item *Item |
| 91 isolated bool |
| 92 } |
| 93 |
| 94 type checkerAddItemResponse struct { |
| 95 item *Item |
| 96 ps *isolatedclient.PushState |
| 97 } |
| 98 |
| 99 func (checker *fakeChecker) AddItem(item *Item, isolated bool, callback CheckerC
allback) { |
| 100 callback(item, checker.ps) |
| 101 } |
| 102 |
| 103 func (checker *fakeChecker) Close() error { return nil } |
| 104 |
| 105 // fakeChecker implements Uploader while recording method arguments. |
| 106 type fakeUploader struct { |
| 107 // uploadBytesCalls is a record of the arguments to each call of UploadB
ytes. |
| 108 uploadBytesCalls []uploaderUploadBytesArgs |
| 109 |
| 110 Uploader // TODO(mcgreevy): implement other methods. |
| 111 } |
| 112 |
| 113 type uploaderUploadBytesArgs struct { |
| 114 relPath string |
| 115 isolJSON []byte |
| 116 ps *isolatedclient.PushState |
| 117 } |
| 118 |
| 119 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}) |
| 121 } |
| 122 |
| 123 func (uploader *fakeUploader) Close() error { return nil } |
| 124 |
| 125 func TestSkipsUpload(t *testing.T) { |
| 126 t.Parallel() |
| 127 Convey(`nil push state signals that upload should be skipped`, t, func()
{ |
| 128 isol := &isolated.Isolated{} |
| 129 |
| 130 // nil PushState means skip the upload. |
| 131 checker := &fakeChecker{ps: nil} |
| 132 |
| 133 uploader := &fakeUploader{} |
| 134 |
| 135 ut := NewUploadTracker(checker, uploader, isol) |
| 136 fos := &fakeOS{} |
| 137 ut.lOS = fos // Override filesystem calls with fake. |
| 138 |
| 139 parts := partitionedDeps{} // no actual deps. |
| 140 err := ut.UploadDeps(parts) |
| 141 So(err, ShouldBeNil) |
| 142 |
| 143 // No deps, so Files should be empty. |
| 144 wantFiles := map[string]isolated.File{} |
| 145 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 146 |
| 147 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 148 So(err, ShouldBeNil) |
| 149 |
| 150 // In this test, the only item that is checked and uploaded is t
he generated isolated file. |
| 151 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 152 So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) |
| 153 |
| 154 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 155 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 156 |
| 157 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 158 }) |
| 159 } |
| 160 |
| 161 func TestDontSkipUpload(t *testing.T) { |
| 162 t.Parallel() |
| 163 Convey(`passing non-nil push state signals that upload should be perform
ed`, t, func() { |
| 164 isol := &isolated.Isolated{} |
| 165 |
| 166 // non-nil PushState means don't skip the upload. |
| 167 checker := &fakeChecker{ps: &isolatedclient.PushState{}} |
| 168 |
| 169 uploader := &fakeUploader{} |
| 170 |
| 171 ut := NewUploadTracker(checker, uploader, isol) |
| 172 fos := &fakeOS{} |
| 173 ut.lOS = fos // Override filesystem calls with fake. |
| 174 |
| 175 parts := partitionedDeps{} // no actual deps. |
| 176 err := ut.UploadDeps(parts) |
| 177 So(err, ShouldBeNil) |
| 178 |
| 179 // No deps, so Files should be empty. |
| 180 wantFiles := map[string]isolated.File{} |
| 181 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 182 |
| 183 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 184 So(err, ShouldBeNil) |
| 185 |
| 186 // In this test, the only item that is checked and uploaded is t
he generated isolated file. |
| 187 wantIsolJSON := []byte(`{"algo":"","version":""}`) |
| 188 So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) |
| 189 |
| 190 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 191 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 192 |
| 193 // Upload was not skipped. |
| 194 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ |
| 195 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 196 }) |
| 197 }) |
| 198 } |
| 199 |
| 200 func TestHandlesSymlinks(t *testing.T) { |
| 201 t.Parallel() |
| 202 Convey(`Symlinks should be stored in the isolated json`, t, func() { |
| 203 isol := &isolated.Isolated{} |
| 204 |
| 205 // The checker and uploader will only be used for uploading the
isolated JSON. |
| 206 // The symlinks themselves do not need to be checked or uploaded
. |
| 207 // non-nil PushState means don't skip the upload. |
| 208 checker := &fakeChecker{ps: &isolatedclient.PushState{}} |
| 209 uploader := &fakeUploader{} |
| 210 |
| 211 ut := NewUploadTracker(checker, uploader, isol) |
| 212 fos := &fakeOS{} |
| 213 ut.lOS = fos // Override filesystem calls with fake. |
| 214 |
| 215 parts := partitionedDeps{ |
| 216 links: itemGroup{ |
| 217 items: []*Item{ |
| 218 {Path: "/a/b/c", RelPath: "c"}, |
| 219 }, |
| 220 totalSize: 9, |
| 221 }, |
| 222 } |
| 223 err := ut.UploadDeps(parts) |
| 224 So(err, ShouldBeNil) |
| 225 |
| 226 path := "link:/a/b/c" |
| 227 wantFiles := map[string]isolated.File{"c": {Link: &path}} |
| 228 |
| 229 So(ut.isol.Files, ShouldResemble, wantFiles) |
| 230 |
| 231 // Symlinks are not uploaded. |
| 232 So(uploader.uploadBytesCalls, ShouldEqual, nil) |
| 233 |
| 234 isolSummary, err := ut.Finalize("/a/isolatedPath") |
| 235 So(err, ShouldBeNil) |
| 236 |
| 237 // 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":""}`) |
| 239 So(fos.files, shouldResembleByteMap, map[string][]byte{"/a/isola
tedPath": wantIsolJSON}) |
| 240 |
| 241 So(isolSummary.Digest, ShouldResemble, isolated.HashBytes(wantIs
olJSON)) |
| 242 So(isolSummary.Name, ShouldEqual, "isolatedPath") |
| 243 |
| 244 So(uploader.uploadBytesCalls, ShouldResemble, []uploaderUploadBy
tesArgs{ |
| 245 {"isolatedPath", wantIsolJSON, checker.ps}, |
| 246 }) |
| 247 }) |
| 248 } |
| OLD | NEW |