github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/file/internal/testutil/testutil.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package testutil 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "math/rand" 13 "runtime" 14 "sort" 15 "testing" 16 "time" 17 18 "github.com/Schaudge/grailbase/errors" 19 "github.com/Schaudge/grailbase/file" 20 "github.com/Schaudge/grailbase/ioctx" 21 "github.com/Schaudge/grailbase/traverse" 22 "github.com/grailbio/testutil/assert" 23 ) 24 25 func doRead(t *testing.T, r io.Reader, len int) string { 26 data := make([]byte, len) 27 n, err := io.ReadFull(r, data) 28 assert.EQ(t, len, n) 29 if err == io.EOF { 30 assert.EQ(t, 0, n) 31 } else { 32 assert.NoError(t, err) 33 } 34 return string(data) 35 } 36 37 func doReadAll(t *testing.T, r io.Reader) string { 38 data, err := ioutil.ReadAll(r) 39 assert.NoError(t, err) 40 return string(data) 41 } 42 43 func doSeek(t *testing.T, r io.Seeker, off int64, whence int) { 44 n, err := r.Seek(off, whence) 45 assert.NoError(t, err) 46 if whence == io.SeekStart { 47 assert.EQ(t, n, off) 48 } 49 } 50 51 func doReadFile(ctx context.Context, t *testing.T, impl file.Implementation, path string) string { 52 f, err := impl.Open(ctx, path) 53 assert.NoError(t, err, "open: %v", path) 54 data := doReadAll(t, f.Reader(ctx)) 55 assert.NoError(t, f.Close(ctx)) 56 return data 57 } 58 59 func doWriteFile(ctx context.Context, t *testing.T, impl file.Implementation, path string, data string) { 60 f, err := impl.Create(ctx, path) 61 assert.NoError(t, err, "create: %v", path) 62 w := f.Writer(ctx) 63 _, err = w.Write([]byte(data)) 64 assert.NoError(t, err) 65 assert.NoError(t, f.Close(ctx)) 66 } 67 68 func fileExists(ctx context.Context, impl file.Implementation, path string) bool { 69 _, err := impl.Stat(ctx, path) 70 if err != nil && !errors.Is(errors.NotExist, err) { 71 panic(err) 72 } 73 return err == nil 74 } 75 76 // TestEmpty creates an empty file and tests its operations. 77 func TestEmpty( 78 ctx context.Context, 79 t *testing.T, 80 impl file.Implementation, 81 path string) { 82 f, err := impl.Create(ctx, path) 83 assert.NoError(t, err) 84 assert.NoError(t, err) 85 assert.NoError(t, f.Close(ctx)) 86 87 f, err = impl.Open(ctx, path) 88 assert.NoError(t, err) 89 assert.EQ(t, "", doReadAll(t, f.Reader(ctx))) 90 assert.NoError(t, f.Close(ctx)) 91 92 // Seek past the end of the file. 93 f, err = impl.Open(ctx, path) 94 assert.NoError(t, err) 95 r := f.Reader(ctx) 96 off, err := r.Seek(10, io.SeekStart) 97 assert.NoError(t, err) 98 assert.EQ(t, int64(10), off) 99 assert.EQ(t, "", doReadAll(t, f.Reader(ctx))) 100 assert.NoError(t, f.Close(ctx)) 101 } 102 103 // TestNotExist tests that the implementation behaves correctly 104 // for paths that do not exist. 105 func TestNotExist( 106 ctx context.Context, 107 t *testing.T, 108 impl file.Implementation, 109 path string) { 110 _, err := impl.Open(ctx, path) 111 assert.True(t, errors.Is(errors.NotExist, err)) 112 _, err = impl.Stat(ctx, path) 113 assert.True(t, errors.Is(errors.NotExist, err)) 114 } 115 116 // TestErrors tests handling of errors. "path" shouldn't exist. 117 func TestErrors( 118 ctx context.Context, 119 t *testing.T, 120 impl file.Implementation, 121 path string) { 122 _, err := impl.Stat(ctx, path) 123 assert.NotNil(t, err) 124 f, err := impl.Open(ctx, path) 125 if err == nil { 126 // S3 allows opening an non-existent file. But Stat or any other operation 127 // for such a file fails. 128 _, err := f.Stat(ctx) 129 t.Logf("errortest %s: stat error %v", path, err) 130 assert.NotNil(t, err) 131 assert.NoError(t, f.Close(ctx)) 132 } 133 } 134 135 // TestReads tests various combination of reads and seeks. 136 func TestReads( 137 ctx context.Context, 138 t *testing.T, 139 impl file.Implementation, 140 path string) { 141 expected := "A purple fox jumped over a blue cat" 142 doWriteFile(ctx, t, impl, path, expected) 143 144 // Read everything. 145 f, err := impl.Open(ctx, path) 146 assert.NoError(t, err) 147 assert.EQ(t, expected, doReadAll(t, f.Reader(ctx))) 148 149 // Read in two chunks. 150 r := f.Reader(ctx) 151 doSeek(t, r, 0, io.SeekStart) 152 assert.EQ(t, expected[:3], doRead(t, r, 3)) 153 assert.EQ(t, expected[3:], doReadAll(t, r)) 154 155 // Stat 156 stat, err := f.Stat(ctx) 157 assert.NoError(t, err) 158 assert.EQ(t, int64(len(expected)), stat.Size()) 159 160 // Reading again should provide no data, since the seek pointer is at the end. 161 r = f.Reader(ctx) 162 assert.EQ(t, "", doReadAll(t, r)) 163 doSeek(t, r, 3, io.SeekStart) 164 assert.EQ(t, expected[3:], doReadAll(t, r)) 165 166 // Read bytes 4-7. 167 doSeek(t, r, 4, io.SeekStart) 168 assert.EQ(t, expected[4:7], doRead(t, r, 3)) 169 170 // Seek beyond the end of the file. 171 doSeek(t, r, int64(len(expected)+1), io.SeekStart) 172 assert.EQ(t, "", doReadAll(t, r)) 173 174 // Seek to the beginning. 175 doSeek(t, r, 0, io.SeekStart) 176 assert.EQ(t, expected, doReadAll(t, r)) 177 178 // Seek twice to the same offset 179 doSeek(t, r, 1, io.SeekStart) 180 doSeek(t, r, 1, io.SeekStart) 181 assert.EQ(t, expected[1:], doReadAll(t, r)) 182 183 doSeek(t, r, 8, io.SeekStart) 184 doSeek(t, r, -6, io.SeekCurrent) 185 assert.EQ(t, "purple", doRead(t, r, 6)) 186 187 doSeek(t, r, -3, io.SeekEnd) 188 assert.EQ(t, "cat", doReadAll(t, r)) 189 } 190 191 // TestWrites tests file Write functions. 192 func TestWrites(ctx context.Context, t *testing.T, impl file.Implementation, dir string) { 193 path := dir + "/tmp.txt" 194 _ = impl.Remove(ctx, path) 195 196 f, err := impl.Create(ctx, path) 197 assert.NoError(t, err) 198 assert.EQ(t, f.Name(), path) 199 w := f.Writer(ctx) 200 n, err := w.Write([]byte("writetest")) 201 assert.NoError(t, err) 202 assert.EQ(t, n, 9) 203 204 // The file shouldn't exist before we call Close. 205 assert.False(t, fileExists(ctx, impl, path), "write %v", path) 206 // After close, the file becomes visible. 207 assert.NoError(t, f.Close(ctx)) 208 assert.True(t, fileExists(ctx, impl, path), "write %v", path) 209 210 // Read the file back. 211 assert.EQ(t, doReadFile(ctx, t, impl, path), "writetest") 212 213 // Overwrite the file 214 f, err = impl.Create(ctx, path) 215 assert.NoError(t, err) 216 w = f.Writer(ctx) 217 n, err = w.Write([]byte("anotherwrite")) 218 assert.NoError(t, err) 219 assert.EQ(t, n, 12) 220 221 // Before closing, the file should store old contents 222 assert.EQ(t, doReadFile(ctx, t, impl, path), "writetest") 223 224 // On close, the file is updated to the new contents. 225 assert.NoError(t, f.Close(ctx)) 226 assert.EQ(t, doReadFile(ctx, t, impl, path), "anotherwrite") 227 } 228 229 func TestDiscard(ctx context.Context, t *testing.T, impl file.Implementation, dir string) { 230 path := dir + "/tmp.txt" 231 _ = impl.Remove(ctx, path) 232 233 f, err := impl.Create(ctx, path) 234 assert.NoError(t, err) 235 w := f.Writer(ctx) 236 _, err = w.Write([]byte("writetest")) 237 assert.NoError(t, err) 238 239 // Discard, and then make sure it doesn't exist. 240 f.Discard(ctx) 241 if fileExists(ctx, impl, path) { 242 t.Errorf("path %s exists after call to discard", path) 243 } 244 } 245 246 // TestRemove tests file Remove() function. 247 func TestRemove(ctx context.Context, t *testing.T, impl file.Implementation, path string) { 248 doWriteFile(ctx, t, impl, path, "removetest") 249 assert.True(t, fileExists(ctx, impl, path)) 250 assert.NoError(t, impl.Remove(ctx, path)) 251 assert.False(t, fileExists(ctx, impl, path)) 252 } 253 254 // TestStat tests Stat method implementations. 255 func TestStat(ctx context.Context, t *testing.T, impl file.Implementation, path string) { 256 // {min,max}ModTime define the range of reasonable modtime for the test file. 257 // We allow for 1 minute slack to account for clock skew on the file server. 258 minModTime := time.Now().Add(-60 * time.Second) 259 doWriteFile(ctx, t, impl, path, "stattest0") 260 261 dir := path + "dir" 262 doWriteFile(ctx, t, impl, dir+"/file", "stattest1") 263 maxModTime := time.Now().Add(60 * time.Second) 264 265 f, err := impl.Open(ctx, path) 266 assert.NoError(t, err) 267 info, err := f.Stat(ctx) 268 assert.NoError(t, f.Close(ctx)) 269 270 assert.NoError(t, err) 271 assert.EQ(t, int64(9), info.Size()) 272 assert.True(t, info.ModTime().After(minModTime) && info.ModTime().Before(maxModTime), 273 "Info: %+v, min %+v, max %+v", info.ModTime(), minModTime, maxModTime) 274 275 info2, err := impl.Stat(ctx, path) 276 assert.NoError(t, err) 277 assert.EQ(t, info, info2) 278 279 // Stat on directory is not supported. 280 _, err = impl.Stat(ctx, dir) 281 assert.NotNil(t, err) 282 } 283 284 type dirEntry struct { 285 path string 286 size int64 287 } 288 289 // TestList tests List implementations. 290 func TestList(ctx context.Context, t *testing.T, impl file.Implementation, dir string) { 291 doList := func(prefix string) (ents []dirEntry) { 292 lister := impl.List(ctx, prefix, true) 293 for lister.Scan() { 294 ents = append(ents, dirEntry{lister.Path(), lister.Info().Size()}) 295 } 296 sort.Slice(ents, func(i, j int) bool { return ents[i].path < ents[j].path }) 297 return 298 } 299 doWriteFile(ctx, t, impl, dir+"/f0.txt", "f0") 300 doWriteFile(ctx, t, impl, dir+"/g0.txt", "g12") 301 doWriteFile(ctx, t, impl, dir+"/d0.txt", "d0e1") 302 doWriteFile(ctx, t, impl, dir+"/d0/f2.txt", "d0/f23") 303 doWriteFile(ctx, t, impl, dir+"/d0/d1/f3.txt", "d0/f345") 304 305 assert.EQ(t, []dirEntry{ 306 dirEntry{dir + "/f0.txt", 2}, 307 }, doList(dir+"/f0.txt")) 308 309 assert.EQ(t, []dirEntry{ 310 dirEntry{dir + "/d0.txt", 4}, 311 dirEntry{dir + "/d0/d1/f3.txt", 7}, 312 dirEntry{dir + "/d0/f2.txt", 6}, 313 dirEntry{dir + "/f0.txt", 2}, 314 dirEntry{dir + "/g0.txt", 3}, 315 }, doList(dir)) 316 317 // List only lists files under the given directory. 318 // So listing "d0" should exclude d0.txt. 319 assert.EQ(t, []dirEntry{ 320 dirEntry{dir + "/d0/d1/f3.txt", 7}, 321 dirEntry{dir + "/d0/f2.txt", 6}, 322 }, doList(dir+"/d0")) 323 assert.EQ(t, []dirEntry{ 324 dirEntry{dir + "/d0/d1/f3.txt", 7}, 325 dirEntry{dir + "/d0/f2.txt", 6}, 326 }, doList(dir+"/d0/")) 327 } 328 329 // TestListDir tests ListDir implementations. 330 func TestListDir(ctx context.Context, t *testing.T, impl file.Implementation, dir string) { 331 doList := func(prefix string) (ents []dirEntry) { 332 lister := impl.List(ctx, prefix, false) 333 for lister.Scan() { 334 de := dirEntry{lister.Path(), 0} 335 if !lister.IsDir() { 336 de.size = lister.Info().Size() 337 } 338 ents = append(ents, de) 339 } 340 sort.Slice(ents, func(i, j int) bool { return ents[i].path < ents[j].path }) 341 return 342 } 343 doWriteFile(ctx, t, impl, dir+"/f0.txt", "f0") 344 doWriteFile(ctx, t, impl, dir+"/g0.txt", "g12") 345 doWriteFile(ctx, t, impl, dir+"/d0.txt", "d0e1") 346 doWriteFile(ctx, t, impl, dir+"/d0/f2.txt", "d0/f23") 347 doWriteFile(ctx, t, impl, dir+"/d0/d1/f3.txt", "d0/f345") 348 349 assert.EQ(t, []dirEntry{ 350 dirEntry{dir + "/d0", 0}, 351 dirEntry{dir + "/d0.txt", 4}, 352 dirEntry{dir + "/f0.txt", 2}, 353 dirEntry{dir + "/g0.txt", 3}, 354 }, doList(dir)) 355 356 // List only lists files under the given directory. 357 // So listing "d0" should exclude d0.txt. 358 assert.EQ(t, []dirEntry{ 359 dirEntry{dir + "/d0/d1", 0}, 360 dirEntry{dir + "/d0/f2.txt", 6}, 361 }, doList(dir+"/d0")) 362 assert.EQ(t, []dirEntry{ 363 dirEntry{dir + "/d0/d1", 0}, 364 dirEntry{dir + "/d0/f2.txt", 6}, 365 }, doList(dir+"/d0/")) 366 } 367 368 // TestStandard runs tests for all of the standard file API functionality. 369 func TestStandard(ctx context.Context, t *testing.T, impl file.Implementation, dir string) { 370 iName := impl.String() 371 372 t.Run(iName+"_Empty", func(t *testing.T) { TestEmpty(ctx, t, impl, dir+"/empty.txt") }) 373 t.Run(iName+"_NotExist", func(t *testing.T) { TestNotExist(ctx, t, impl, dir+"/notexist.txt") }) 374 t.Run(iName+"_Errors", func(t *testing.T) { TestErrors(ctx, t, impl, dir+"/errors.txt") }) 375 t.Run(iName+"_Reads", func(t *testing.T) { TestReads(ctx, t, impl, dir+"/reads.txt") }) 376 t.Run(iName+"_Writes", func(t *testing.T) { TestWrites(ctx, t, impl, dir+"/writes") }) 377 t.Run(iName+"_Discard", func(t *testing.T) { TestDiscard(ctx, t, impl, dir+"/discard") }) 378 t.Run(iName+"_Remove", func(t *testing.T) { TestRemove(ctx, t, impl, dir+"/remove.txt") }) 379 t.Run(iName+"_Stat", func(t *testing.T) { TestStat(ctx, t, impl, dir+"/stat.txt") }) 380 t.Run(iName+"_List", func(t *testing.T) { TestList(ctx, t, impl, dir+"/match") }) 381 t.Run(iName+"_ListDir", func(t *testing.T) { TestListDir(ctx, t, impl, dir+"/dirmatch") }) 382 } 383 384 // TestConcurrentOffsetReads tests arbitrarily-ordered, concurrent reads. 385 func TestConcurrentOffsetReads( 386 ctx context.Context, 387 t *testing.T, 388 impl file.Implementation, 389 path string, 390 ) { 391 expected := "A purple fox jumped over a blue cat" 392 doWriteFile(ctx, t, impl, path, expected) 393 394 parallelism := runtime.NumCPU() 395 const readsPerShard = 1024 396 397 f, err := impl.Open(ctx, path) 398 assert.NoError(t, err) 399 400 rnds := make([]*rand.Rand, parallelism) 401 rnds[0] = rand.New(rand.NewSource(1)) 402 for i := 1; i < len(rnds); i++ { 403 rnds[i] = rand.New(rand.NewSource(rnds[0].Int63())) 404 } 405 406 assert.NoError(t, traverse.Limit(parallelism).Each(parallelism, func(shard int) (err error) { 407 rnd := rnds[shard] 408 for i := 0; i < readsPerShard; i++ { 409 start := rnd.Intn(len(expected)) 410 limit := start + rnd.Intn(len(expected)+1-start) 411 got := make([]byte, limit-start) 412 rc := f.OffsetReader(int64(start)) 413 defer errors.CleanUpCtx(ctx, rc.Close, &err) 414 _, err = io.ReadFull(ioctx.ToStdReader(ctx, rc), got) 415 if err != nil { 416 return err 417 } 418 if got, want := string(got), expected[start:limit]; got != want { 419 return fmt.Errorf("got: %s, want: %s", got, want) 420 } 421 } 422 return nil 423 })) 424 425 assert.NoError(t, f.Close(ctx)) 426 }