sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/resultstore/files_test.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package resultstore 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "slices" 24 "strings" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 29 "google.golang.org/protobuf/testing/protocmp" 30 "google.golang.org/protobuf/types/known/wrapperspb" 31 "k8s.io/apimachinery/pkg/util/sets" 32 pio "sigs.k8s.io/prow/pkg/io" 33 ) 34 35 // fakeFileFinder is a testing fake for a subset of pio.Opener. 36 type fakeFileFinder struct { 37 files map[string]pio.Attributes 38 } 39 40 // Assert this matches our subset of pio.Opener. 41 var _ fileFinder = &fakeFileFinder{} 42 43 var ( 44 // A file with this value causes Iterator() to return error. 45 wantIterErr = pio.Attributes{ContentEncoding: "__wantIterErr__"} 46 // A file with this value causes Iterator.Next() to return error. 47 wantNextErr = pio.Attributes{ContentEncoding: "__wantNextErr__"} 48 ) 49 50 // Iterator iterates at a prefix, which includes the provider and 51 // bucket; the output names are relative to the bucket. 52 func (f *fakeFileFinder) Iterator(_ context.Context, prefix, delimiter string) (pio.ObjectIterator, error) { 53 var fs []string 54 seenDirs := sets.Set[string]{} 55 b, err := bucket(prefix) 56 if err != nil { 57 return nil, err 58 } 59 for n, a := range f.files { 60 if !strings.HasPrefix(n, prefix) { 61 continue 62 } 63 if delimiter == "" || !strings.Contains(n[len(prefix):], delimiter) { 64 if a.ContentEncoding == wantIterErr.ContentEncoding { 65 return nil, fmt.Errorf("iterator error at %q", n) 66 } 67 fs = append(fs, n) 68 continue 69 } 70 ps := strings.SplitN(n[len(prefix):], delimiter, 2) 71 if seenDirs.Has(ps[0]) { 72 continue 73 } 74 fs = append(fs, fmt.Sprintf("%s%s/", prefix, ps[0])) 75 seenDirs.Insert(ps[0]) 76 } 77 slices.Sort(fs) 78 return &fakeIterator{finder: f, files: fs, bucket: b}, nil 79 } 80 81 type fakeIterator struct { 82 finder *fakeFileFinder 83 files []string 84 bucket string 85 pos int 86 } 87 88 // Next only populates ObjectAttributes fields used by this package: 89 // Name and IsDir. 90 func (i *fakeIterator) Next(_ context.Context) (pio.ObjectAttributes, error) { 91 oa := pio.ObjectAttributes{} 92 if i.pos >= len(i.files) { 93 return oa, io.EOF 94 } 95 n := i.files[i.pos] 96 i.pos++ 97 if i.finder.files[n].ContentEncoding == wantNextErr.ContentEncoding { 98 return oa, fmt.Errorf("next error at %q", n) 99 } 100 oa.Name = strings.TrimPrefix(n, i.bucket) 101 oa.IsDir = strings.HasSuffix(n, "/") 102 oa.Size = i.finder.files[n].Size 103 return oa, nil 104 } 105 106 func TestArtifactFiles(t *testing.T) { 107 ctx := context.Background() 108 base := "gs://bucket/pr-logs/1234" 109 for _, tc := range []struct { 110 desc string 111 ff *fakeFileFinder 112 opts ArtifactOpts 113 want []*resultstore.File 114 wantErr bool 115 }{ 116 { 117 desc: "success", 118 ff: &fakeFileFinder{ 119 files: map[string]pio.Attributes{ 120 base + "/build-log.txt": { 121 Size: 9000, 122 }, 123 base + "/started.json": { 124 Size: 350, 125 }, 126 base + "/artifacts/artifact.txt": { 127 Size: 10000, 128 }, 129 }, 130 }, 131 opts: ArtifactOpts{ 132 Dir: base, 133 DefaultFiles: []DefaultFile{ 134 { 135 Name: "prowjob.json", 136 Size: 1984, 137 }, 138 { 139 Name: "started.json", 140 Size: 3500, 141 }, 142 }, 143 }, 144 want: []*resultstore.File{ 145 { 146 Uid: "build.log", 147 Uri: "gs://bucket/pr-logs/1234/build-log.txt", 148 Length: &wrapperspb.Int64Value{Value: 9000}, 149 ContentType: "text/plain", 150 }, 151 { 152 Uid: "started.json", 153 Uri: "gs://bucket/pr-logs/1234/started.json", 154 Length: &wrapperspb.Int64Value{Value: 350}, 155 ContentType: "application/json", 156 }, 157 { 158 Uid: "prowjob.json", 159 Uri: "gs://bucket/pr-logs/1234/prowjob.json", 160 Length: &wrapperspb.Int64Value{Value: 1984}, 161 ContentType: "application/json", 162 }, 163 { 164 Uid: "artifacts/artifact.txt", 165 Uri: "gs://bucket/pr-logs/1234/artifacts/artifact.txt", 166 Length: &wrapperspb.Int64Value{Value: 10000}, 167 ContentType: "text/plain", 168 }, 169 }, 170 }, 171 { 172 desc: "artifacts dir", 173 ff: &fakeFileFinder{ 174 files: map[string]pio.Attributes{ 175 base + "/build-log.txt": { 176 Size: 9000, 177 }, 178 base + "/started.json": { 179 Size: 350, 180 }, 181 base + "/artifacts/artifact.txt": { 182 Size: 10000, 183 }, 184 }, 185 }, 186 opts: ArtifactOpts{ 187 Dir: base, 188 ArtifactsDirOnly: true, 189 }, 190 want: []*resultstore.File{ 191 { 192 Uid: "build.log", 193 Uri: "gs://bucket/pr-logs/1234/build-log.txt", 194 Length: &wrapperspb.Int64Value{Value: 9000}, 195 ContentType: "text/plain", 196 }, 197 { 198 Uid: "started.json", 199 Uri: "gs://bucket/pr-logs/1234/started.json", 200 Length: &wrapperspb.Int64Value{Value: 350}, 201 ContentType: "application/json", 202 }, 203 { 204 Uid: "artifacts/", 205 Uri: "gs://bucket/pr-logs/1234/artifacts/", 206 }, 207 }, 208 }, 209 { 210 desc: "exclude unwanted subdirs", 211 ff: &fakeFileFinder{ 212 files: map[string]pio.Attributes{ 213 base + "/not-artifacts-subdir/unwanted": {}, 214 }, 215 }, 216 opts: ArtifactOpts{ 217 Dir: base, 218 }, 219 want: nil, 220 }, 221 { 222 desc: "exclude build.log", 223 ff: &fakeFileFinder{ 224 files: map[string]pio.Attributes{ 225 base + "/build.log": {}, 226 }, 227 }, 228 opts: ArtifactOpts{ 229 Dir: base, 230 }, 231 want: nil, 232 }, 233 { 234 desc: "empty", 235 ff: &fakeFileFinder{}, 236 opts: ArtifactOpts{ 237 Dir: base, 238 }, 239 want: nil, 240 }, 241 { 242 desc: "iterator error", 243 ff: &fakeFileFinder{ 244 files: map[string]pio.Attributes{ 245 base + "/iterator-error.txt": wantIterErr, 246 }, 247 }, 248 opts: ArtifactOpts{ 249 Dir: base, 250 }, 251 wantErr: true, 252 }, 253 { 254 desc: "iteration error", 255 ff: &fakeFileFinder{ 256 files: map[string]pio.Attributes{ 257 base + "/expected.txt": { 258 Size: 100, 259 }, 260 base + "/next-error.txt": wantNextErr, 261 base + "/artifacts/unexpected.txt": { 262 Size: 1000, 263 }, 264 }, 265 }, 266 opts: ArtifactOpts{ 267 Dir: base, 268 }, 269 want: []*resultstore.File{ 270 { 271 Uid: "expected.txt", 272 Uri: "gs://bucket/pr-logs/1234/expected.txt", 273 Length: &wrapperspb.Int64Value{Value: 100}, 274 ContentType: "text/plain", 275 }, 276 }, 277 wantErr: true, 278 }, 279 { 280 desc: "artifacts iteration error", 281 ff: &fakeFileFinder{ 282 files: map[string]pio.Attributes{ 283 base + "/ok.txt": { 284 Size: 100, 285 }, 286 base + "/artifacts/a.txt": { 287 Size: 1000, 288 }, 289 base + "/artifacts/b.txt": wantNextErr, 290 base + "/artifacts/c.txt": { 291 Size: 10000, 292 }, 293 }, 294 }, 295 opts: ArtifactOpts{ 296 Dir: base, 297 }, 298 want: []*resultstore.File{ 299 { 300 Uid: "ok.txt", 301 Uri: "gs://bucket/pr-logs/1234/ok.txt", 302 Length: &wrapperspb.Int64Value{Value: 100}, 303 ContentType: "text/plain", 304 }, 305 { 306 Uid: "artifacts/a.txt", 307 Uri: "gs://bucket/pr-logs/1234/artifacts/a.txt", 308 Length: &wrapperspb.Int64Value{Value: 1000}, 309 ContentType: "text/plain", 310 }, 311 }, 312 wantErr: true, 313 }, 314 } { 315 t.Run(tc.desc, func(t *testing.T) { 316 got, err := ArtifactFiles(ctx, tc.ff, tc.opts) 317 if err != nil { 318 if tc.wantErr { 319 t.Logf("got expected error: %v", err) 320 } else { 321 t.Fatalf("got unwanted error: %v", err) 322 } 323 } else if tc.wantErr { 324 t.Fatal("wanted error, got nil") 325 } 326 if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" { 327 t.Errorf("diff (-want +got):\n%s", diff) 328 } 329 }) 330 } 331 } 332 333 func TestEnsureTrailingSlash(t *testing.T) { 334 for _, tc := range []struct { 335 in string 336 want string 337 }{ 338 {"some/path", "some/path/"}, 339 {"some/path/", "some/path/"}, 340 } { 341 if got := ensureTrailingSlash(tc.in); got != tc.want { 342 t.Errorf("ensureTrailingSlash(%q) got %s, want %s", tc.in, got, tc.want) 343 } 344 } 345 }