github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/chunker/chunker_test.go (about) 1 package chunker 2 3 import ( 4 "bytes" 5 "os" 6 "path/filepath" 7 "testing" 8 9 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 10 "github.com/bazelbuild/remote-apis-sdks/go/pkg/uploadinfo" 11 "github.com/google/go-cmp/cmp" 12 "github.com/google/go-cmp/cmp/cmpopts" 13 ) 14 15 var tests = []struct { 16 name string 17 blob []byte 18 wantChunks []*Chunk 19 chunkSize int 20 }{ 21 { 22 name: "empty", 23 wantChunks: []*Chunk{&Chunk{}}, 24 chunkSize: 3, 25 }, 26 { 27 name: "one", 28 blob: []byte("12"), 29 wantChunks: []*Chunk{&Chunk{Data: []byte("12")}}, 30 chunkSize: 3, 31 }, 32 { 33 name: "one-even", 34 blob: []byte("123"), 35 wantChunks: []*Chunk{&Chunk{Data: []byte("123")}}, 36 chunkSize: 3, 37 }, 38 { 39 name: "two", 40 blob: []byte("12345"), 41 wantChunks: []*Chunk{ 42 &Chunk{Data: []byte("123")}, 43 &Chunk{Data: []byte("45"), Offset: 3}, 44 }, 45 chunkSize: 3, 46 }, 47 { 48 name: "three-even", 49 blob: []byte("123456789"), 50 wantChunks: []*Chunk{ 51 &Chunk{Data: []byte("123")}, 52 &Chunk{Data: []byte("456"), Offset: 3}, 53 &Chunk{Data: []byte("789"), Offset: 6}, 54 }, 55 chunkSize: 3, 56 }, 57 { 58 name: "three", 59 blob: []byte("123456789"), 60 wantChunks: []*Chunk{ 61 &Chunk{Data: []byte("1234")}, 62 &Chunk{Data: []byte("5678"), Offset: 4}, 63 &Chunk{Data: []byte("9"), Offset: 8}, 64 }, 65 chunkSize: 4, 66 }, 67 { 68 name: "many", 69 blob: []byte("1234567890abcdefghijklmnopqrstuvwxyz!"), 70 wantChunks: []*Chunk{ 71 &Chunk{Data: []byte("1234")}, 72 &Chunk{Data: []byte("5678"), Offset: 4}, 73 &Chunk{Data: []byte("90ab"), Offset: 8}, 74 &Chunk{Data: []byte("cdef"), Offset: 12}, 75 &Chunk{Data: []byte("ghij"), Offset: 16}, 76 &Chunk{Data: []byte("klmn"), Offset: 20}, 77 &Chunk{Data: []byte("opqr"), Offset: 24}, 78 &Chunk{Data: []byte("stuv"), Offset: 28}, 79 &Chunk{Data: []byte("wxyz"), Offset: 32}, 80 &Chunk{Data: []byte("!"), Offset: 36}, 81 }, 82 chunkSize: 4, 83 }, 84 } 85 86 var bufferSizes = []int{3, 4, 8, 100} 87 88 func TestChunkerFromBlob(t *testing.T) { 89 t.Parallel() 90 for _, tc := range tests { 91 t.Run(tc.name, func(t *testing.T) { 92 ue := uploadinfo.EntryFromBlob(tc.blob) 93 c, err := New(ue, false, tc.chunkSize) 94 if err != nil { 95 t.Fatalf("Could not make chunker from UEntry: %v", err) 96 } 97 var gotChunks []*Chunk 98 for _, wantChunk := range tc.wantChunks { 99 if !c.HasNext() { 100 t.Errorf("%s: c.HasNext() was false on blob %q , expecting next chunk %q", tc.name, tc.blob, string(wantChunk.Data)) 101 } 102 got, err := c.Next() 103 if err != nil { 104 t.Errorf("%s: c.Next() gave error %v on blob %q , expecting next chunk %q", tc.name, err, tc.blob, string(wantChunk.Data)) 105 } 106 gotChunks = append(gotChunks, got) 107 } 108 if diff := cmp.Diff(tc.wantChunks, gotChunks); diff != "" { 109 t.Errorf("%s: Chunker gave result diff (-want +got):\n%s", tc.name, diff) 110 } 111 }) 112 } 113 } 114 115 func TestChunkerFromFile(t *testing.T) { 116 execRoot := t.TempDir() 117 for _, tc := range tests { 118 t.Run(tc.name, func(t *testing.T) { 119 path := filepath.Join(execRoot, tc.name) 120 if err := os.WriteFile(path, tc.blob, 0777); err != nil { 121 t.Fatalf("failed to write temp file: %v", err) 122 } 123 for _, bufSize := range bufferSizes { 124 if bufSize < tc.chunkSize { 125 continue 126 } 127 dg := digest.NewFromBlob(tc.blob) 128 IOBufferSize = bufSize 129 ue := uploadinfo.EntryFromFile(dg, path) 130 c, err := New(ue, false, tc.chunkSize) 131 if err != nil { 132 t.Fatalf("Could not make chunker from UEntry: %v", err) 133 } 134 var gotChunks []*Chunk 135 for _, wantChunk := range tc.wantChunks { 136 if !c.HasNext() { 137 t.Errorf("%s: c.HasNext() was false on blob %q buffer size %d, expecting next chunk %q", tc.name, tc.blob, bufSize, string(wantChunk.Data)) 138 } 139 got, err := c.Next() 140 if err != nil { 141 t.Errorf("%s: c.Next() gave error %v on blob %q buffer size %d, expecting next chunk %q", tc.name, err, tc.blob, bufSize, string(wantChunk.Data)) 142 } 143 gotChunks = append(gotChunks, got) 144 } 145 if diff := cmp.Diff(tc.wantChunks, gotChunks); diff != "" { 146 t.Errorf("%s: Chunker buffer size %d gave result diff (-want +got):\n%s", tc.name, bufSize, diff) 147 } 148 } 149 }) 150 } 151 } 152 153 func TestChunkerFullData(t *testing.T) { 154 t.Parallel() 155 for _, tc := range tests { 156 t.Run(tc.name, func(t *testing.T) { 157 ue := uploadinfo.EntryFromBlob(tc.blob) 158 c, err := New(ue, false, tc.chunkSize) 159 if err != nil { 160 t.Fatalf("Could not make chunker from UEntry: %v", err) 161 } 162 gotBlob, err := c.FullData() 163 if err != nil { 164 t.Errorf("c.FullData() gave error %v on blob %q", err, tc.blob) 165 } 166 if diff := cmp.Diff(tc.blob, gotBlob, cmpopts.EquateEmpty()); diff != "" { 167 t.Errorf("FullData gave result diff (-want +got):\n%s", diff) 168 } 169 }) 170 } 171 } 172 173 func TestChunkerFromBlob_Reset(t *testing.T) { 174 t.Parallel() 175 for _, tc := range tests { 176 t.Run(tc.name, func(t *testing.T) { 177 for reset := 1; reset < len(tc.wantChunks); reset++ { 178 ue := uploadinfo.EntryFromBlob(tc.blob) 179 c, err := New(ue, false, tc.chunkSize) 180 if err != nil { 181 t.Fatalf("Could not make chunker from UEntry: %v", err) 182 } 183 var gotChunks []*Chunk 184 for i, wantChunk := range tc.wantChunks { 185 if !c.HasNext() { 186 t.Errorf("%s: c.HasNext() was false on blob %q , expecting next chunk %q", tc.name, tc.blob, string(wantChunk.Data)) 187 } 188 got, err := c.Next() 189 if err != nil { 190 t.Errorf("%s: c.Next() gave error %v on blob %q , expecting next chunk %q", tc.name, err, tc.blob, string(wantChunk.Data)) 191 } 192 gotChunks = append(gotChunks, got) 193 if i == reset { 194 if err := c.Reset(); err != nil { 195 t.Errorf("failed to reset: %v", err) 196 } 197 break 198 } 199 } 200 if diff := cmp.Diff(tc.wantChunks[:len(gotChunks)], gotChunks); diff != "" { 201 t.Errorf("%s: Chunker gave result diff (-want +got):\n%s", tc.name, diff) 202 } 203 gotChunks = nil 204 if reset >= len(tc.wantChunks) { 205 continue 206 } 207 for _, wantChunk := range tc.wantChunks { 208 if !c.HasNext() { 209 t.Errorf("%s: c.HasNext() was false on blob %q , expecting next chunk %q", tc.name, tc.blob, string(wantChunk.Data)) 210 } 211 got, err := c.Next() 212 if err != nil { 213 t.Errorf("%s: c.Next() gave error %v on blob %q , expecting next chunk %q", tc.name, err, tc.blob, string(wantChunk.Data)) 214 } 215 gotChunks = append(gotChunks, got) 216 } 217 if diff := cmp.Diff(tc.wantChunks, gotChunks); diff != "" { 218 t.Errorf("%s: Chunker gave result diff (-want +got):\n%s", tc.name, diff) 219 } 220 } 221 }) 222 } 223 } 224 225 func TestChunkerFromFile_Reset(t *testing.T) { 226 execRoot := t.TempDir() 227 for _, tc := range tests { 228 t.Run(tc.name, func(t *testing.T) { 229 path := filepath.Join(execRoot, tc.name) 230 if err := os.WriteFile(path, tc.blob, 0777); err != nil { 231 t.Fatalf("failed to write temp file: %v", err) 232 } 233 for _, bufSize := range bufferSizes { 234 if bufSize < tc.chunkSize { 235 continue 236 } 237 dg := digest.NewFromBlob(tc.blob) 238 IOBufferSize = bufSize 239 for reset := 1; reset < len(tc.wantChunks); reset++ { 240 ue := uploadinfo.EntryFromFile(dg, path) 241 c, err := New(ue, false, tc.chunkSize) 242 if err != nil { 243 t.Fatalf("Could not make chunker from UEntry: %v", err) 244 } 245 var gotChunks []*Chunk 246 for i, wantChunk := range tc.wantChunks { 247 if !c.HasNext() { 248 t.Errorf("%s: c.HasNext() was false on blob %q buffer size %d, expecting next chunk %q", tc.name, tc.blob, bufSize, string(wantChunk.Data)) 249 } 250 got, err := c.Next() 251 if err != nil { 252 t.Errorf("%s: c.Next() gave error %v on blob %q buffer size %d, expecting next chunk %q", tc.name, err, tc.blob, bufSize, string(wantChunk.Data)) 253 } 254 gotChunks = append(gotChunks, got) 255 if i == reset { 256 if err := c.Reset(); err != nil { 257 t.Errorf("failed to reset: %v", err) 258 } 259 break 260 } 261 } 262 if diff := cmp.Diff(tc.wantChunks[:len(gotChunks)], gotChunks); diff != "" { 263 t.Errorf("%s: Chunker buffer size %d gave result diff (-want +got):\n%s", tc.name, bufSize, diff) 264 } 265 gotChunks = nil 266 if reset >= len(tc.wantChunks) { 267 continue 268 } 269 for _, wantChunk := range tc.wantChunks { 270 if !c.HasNext() { 271 t.Errorf("%s: c.HasNext() was false on blob %q buffer size %d, expecting next chunk %q", tc.name, tc.blob, bufSize, string(wantChunk.Data)) 272 } 273 got, err := c.Next() 274 if err != nil { 275 t.Errorf("%s: c.Next() gave error %v on blob %q buffer size %d, expecting next chunk %q", tc.name, err, tc.blob, bufSize, string(wantChunk.Data)) 276 } 277 gotChunks = append(gotChunks, got) 278 } 279 if diff := cmp.Diff(tc.wantChunks, gotChunks); diff != "" { 280 t.Errorf("%s: Chunker buffer size %d gave result diff (-want +got):\n%s", tc.name, bufSize, diff) 281 } 282 } 283 } 284 }) 285 } 286 } 287 288 func TestChunkerErrors_ErrEOF(t *testing.T) { 289 ue := uploadinfo.EntryFromBlob([]byte("12")) 290 c, err := New(ue, false, 2) 291 if err != nil { 292 t.Fatalf("Could not make chunker from UEntry: %v", err) 293 } 294 _, err = c.Next() 295 if err != nil { 296 t.Errorf("c.Next() gave error %v, expecting next chunk \"12\"", err) 297 } 298 got, err := c.Next() 299 if err == nil { 300 t.Errorf("c.Next() gave %v, %v, expecting _, error", got, err) 301 } 302 } 303 304 func TestChunkerResetOptimization_SmallFile(t *testing.T) { 305 // Files smaller than IOBufferSize are loaded into memory once and not re-read on Reset. 306 execRoot := t.TempDir() 307 308 blob := []byte("123") 309 path := filepath.Join(execRoot, "file") 310 if err := os.WriteFile(path, blob, 0777); err != nil { 311 t.Fatalf("failed to write temp file: %v", err) 312 } 313 dg := digest.NewFromBlob(blob) 314 IOBufferSize = 10 315 ue := uploadinfo.EntryFromFile(dg, path) 316 c, err := New(ue, false, 4) 317 if err != nil { 318 t.Fatalf("Could not make chunker from UEntry: %v", err) 319 } 320 got, err := c.Next() 321 if err != nil { 322 t.Errorf("c.Next() gave error %v", err) 323 } 324 wantChunk := &Chunk{Data: blob} 325 if diff := cmp.Diff(wantChunk, got); diff != "" { 326 t.Errorf("c.Next() gave result diff (-want +got):\n%s", diff) 327 } 328 if err := c.Reset(); err != nil { 329 t.Errorf("failed to reset: %v", err) 330 } 331 // Change the file contents. 332 if err := os.WriteFile(path, []byte("321"), 0777); err != nil { 333 t.Fatalf("failed to write temp file: %v", err) 334 } 335 got, err = c.Next() 336 if err != nil { 337 t.Errorf("c.Next() gave error %v", err) 338 } 339 if diff := cmp.Diff(wantChunk, got); diff != "" { 340 t.Errorf("c.Next() gave result diff (-want +got):\n%s", diff) 341 } 342 } 343 344 func TestChunkerResetOptimization_FullData(t *testing.T) { 345 // After FullData is called once, the file contents will remain loaded into memory and not 346 // re-read on Reset, even if the file is larger than IOBufferSize. 347 execRoot := t.TempDir() 348 349 blob := []byte("12345678") 350 path := filepath.Join(execRoot, "file") 351 if err := os.WriteFile(path, blob, 0777); err != nil { 352 t.Fatalf("failed to write temp file: %v", err) 353 } 354 dg := digest.NewFromBlob(blob) 355 IOBufferSize = 5 356 ue := uploadinfo.EntryFromFile(dg, path) 357 c, err := New(ue, false, 3) 358 if err != nil { 359 t.Fatalf("Could not make chunker from UEntry: %v", err) 360 } 361 got, err := c.FullData() 362 if err != nil { 363 t.Errorf("c.FullData() gave error %v", err) 364 } 365 if !bytes.Equal(got, blob) { 366 t.Errorf("c.FullData() gave result diff, want %q, got %q", string(blob), string(got)) 367 } 368 if err := c.Reset(); err != nil { 369 t.Errorf("failed to reset: %v", err) 370 } 371 // Change the file contents. 372 if err := os.WriteFile(path, []byte("987654321"), 0777); err != nil { 373 t.Fatalf("failed to write temp file: %v", err) 374 } 375 got, err = c.FullData() 376 if err != nil { 377 t.Errorf("c.FullData() gave error %v", err) 378 } 379 if !bytes.Equal(got, blob) { 380 t.Errorf("c.FullData() gave result diff, want %q, got %q", string(blob), string(got)) 381 } 382 }