gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/filesystem/siafile/persist_test.go (about) 1 package siafile 2 3 import ( 4 "bytes" 5 "encoding/hex" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "gitlab.com/NebulousLabs/errors" 16 "gitlab.com/NebulousLabs/fastrand" 17 "gitlab.com/NebulousLabs/writeaheadlog" 18 19 "gitlab.com/SkynetLabs/skyd/skymodules" 20 "go.sia.tech/siad/crypto" 21 "go.sia.tech/siad/modules" 22 "go.sia.tech/siad/types" 23 ) 24 25 // closeFileInTest is a small helper for calling close on a file in a test 26 func closeFileInTest(t *testing.T, f *os.File) { 27 err := f.Close() 28 if err != nil { 29 t.Fatal(err) 30 } 31 } 32 33 // equalFiles is a helper that compares two SiaFiles for equality. 34 func equalFiles(sf, sf2 *SiaFile) error { 35 // Backup the metadata structs for both files. 36 md := sf.staticMetadata 37 md2 := sf2.staticMetadata 38 // Compare the timestamps first since they can't be compared with 39 // DeepEqual. 40 if sf.staticMetadata.AccessTime.Unix() != sf2.staticMetadata.AccessTime.Unix() { 41 return errors.New("AccessTime's don't match") 42 } 43 if sf.staticMetadata.ChangeTime.Unix() != sf2.staticMetadata.ChangeTime.Unix() { 44 return errors.New("ChangeTime's don't match") 45 } 46 if sf.staticMetadata.CreateTime.Unix() != sf2.staticMetadata.CreateTime.Unix() { 47 return errors.New("CreateTime's don't match") 48 } 49 if sf.staticMetadata.ModTime.Unix() != sf2.staticMetadata.ModTime.Unix() { 50 return errors.New("ModTime's don't match") 51 } 52 if sf.staticMetadata.LastHealthCheckTime.Unix() != sf2.staticMetadata.LastHealthCheckTime.Unix() { 53 return errors.New("LastHealthCheckTime's don't match") 54 } 55 ec := sf.ErasureCode() 56 ec2 := sf2.ErasureCode() 57 if ec.Identifier() != ec2.Identifier() { 58 return errors.New("erasure coder doesn't match") 59 } 60 // Set the timestamps to zero for DeepEqual. 61 sf.staticMetadata.AccessTime = time.Time{} 62 sf.staticMetadata.ChangeTime = time.Time{} 63 sf.staticMetadata.CreateTime = time.Time{} 64 sf.staticMetadata.ModTime = time.Time{} 65 sf.staticMetadata.LastHealthCheckTime = time.Time{} 66 sf.staticMetadata.staticErasureCode = nil 67 sf2.staticMetadata.AccessTime = time.Time{} 68 sf2.staticMetadata.ChangeTime = time.Time{} 69 sf2.staticMetadata.CreateTime = time.Time{} 70 sf2.staticMetadata.ModTime = time.Time{} 71 sf2.staticMetadata.LastHealthCheckTime = time.Time{} 72 sf2.staticMetadata.staticErasureCode = nil 73 // Compare the rest of sf and sf2. 74 if !reflect.DeepEqual(sf.staticMetadata, sf2.staticMetadata) { 75 fmt.Println(sf.staticMetadata) 76 fmt.Println(sf2.staticMetadata) 77 return errors.New("sf metadata doesn't equal sf2 metadata") 78 } 79 if !reflect.DeepEqual(sf.pubKeyTable, sf2.pubKeyTable) { 80 fmt.Println(sf.pubKeyTable) 81 fmt.Println(sf2.pubKeyTable) 82 return errors.New("sf pubKeyTable doesn't equal sf2 pubKeyTable") 83 } 84 if sf.numChunks != sf2.numChunks { 85 return errors.New(fmt.Sprint("sf numChunks doesn't equal sf2 numChunks", sf.numChunks, sf2.numChunks)) 86 } 87 if sf.siaFilePath != sf2.siaFilePath { 88 return fmt.Errorf("sf2 filepath was %v but should be %v", 89 sf2.siaFilePath, sf.siaFilePath) 90 } 91 // Restore the original metadata. 92 sf.staticMetadata = md 93 sf2.staticMetadata = md2 94 return nil 95 } 96 97 // addRandomHostKeys adds n random host keys to the SiaFile's pubKeyTable. It 98 // doesn't write them to disk. 99 func (sf *SiaFile) addRandomHostKeys(n int) { 100 for i := 0; i < n; i++ { 101 // Create random specifier and key. 102 algorithm := types.Specifier{} 103 fastrand.Read(algorithm[:]) 104 105 // Create random key. 106 key := fastrand.Bytes(32) 107 108 // Append new key to slice. 109 sf.pubKeyTable = append(sf.pubKeyTable, HostPublicKey{ 110 PublicKey: types.SiaPublicKey{ 111 Algorithm: algorithm, 112 Key: key, 113 }, 114 Used: true, 115 }) 116 } 117 } 118 119 // customTestFileAndWAL creates an empty SiaFile for testing and also returns 120 // the WAL used in the creation and the path of the WAL. 121 func customTestFileAndWAL(siaFilePath, source string, rc skymodules.ErasureCoder, sk crypto.CipherKey, fileSize uint64, numChunks int, fileMode os.FileMode) (*SiaFile, *writeaheadlog.WAL, string) { 122 // Create the path to the file. 123 dir, _ := filepath.Split(siaFilePath) 124 err := os.MkdirAll(dir, 0700) 125 if err != nil { 126 panic(err) 127 } 128 // Create a test wal 129 wal, walPath := newTestWAL() 130 // Create the file. 131 sf, err := New(siaFilePath, source, wal, rc, sk, fileSize, fileMode) 132 if err != nil { 133 panic(err) 134 } 135 if sf.staticMetadata.StaticVersion != metadataVersion { 136 panic("incorrect metadata version for new file") 137 } 138 // Check that the number of chunks in the file is correct. 139 if numChunks >= 0 && sf.numChunks != numChunks { 140 panic(fmt.Sprintf("newTestFile didn't create the expected number of chunks: %v %v %v", sf.numChunks, numChunks, fileSize)) 141 } 142 return sf, wal, walPath 143 } 144 145 // newBlankTestFileAndWAL is like customTestFileAndWAL but uses random params 146 // and allows the caller to specify how many chunks the file should at least 147 // contain. 148 func newBlankTestFileAndWAL(minChunks int) (*SiaFile, *writeaheadlog.WAL, string) { 149 siaFilePath, _, source, rc, sk, fileSize, numChunks, fileMode := newTestFileParams(minChunks, true) 150 return customTestFileAndWAL(siaFilePath, source, rc, sk, fileSize, numChunks, fileMode) 151 } 152 153 // newBlankTestFile is a helper method to create a SiaFile for testing without 154 // any hosts or uploaded pieces. 155 func newBlankTestFile() *SiaFile { 156 sf, _, _ := newBlankTestFileAndWAL(1) 157 return sf 158 } 159 160 // newTestFile creates a SiaFile for testing where each chunk has a random 161 // number of pieces. 162 func newTestFile() *SiaFile { 163 sf := newBlankTestFile() 164 // Add pieces to each chunk. 165 for chunkIndex := 0; chunkIndex < sf.numChunks; chunkIndex++ { 166 for pieceIndex := 0; pieceIndex < sf.ErasureCode().NumPieces(); pieceIndex++ { 167 numPieces := fastrand.Intn(3) // up to 2 hosts for each piece 168 for i := 0; i < numPieces; i++ { 169 pk := types.SiaPublicKey{Key: fastrand.Bytes(crypto.EntropySize)} 170 mr := crypto.Hash{} 171 fastrand.Read(mr[:]) 172 if err := sf.AddPiece(pk, uint64(chunkIndex), uint64(pieceIndex), mr); err != nil { 173 panic(err) 174 } 175 } 176 } 177 } 178 return sf 179 } 180 181 // newTestFileParams creates the required parameters for creating a siafile and 182 // creates a directory for the file 183 func newTestFileParams(minChunks int, partialChunk bool) (string, skymodules.SiaPath, string, skymodules.ErasureCoder, crypto.CipherKey, uint64, int, os.FileMode) { 184 rc, err := skymodules.NewRSCode(10, 20) 185 if err != nil { 186 panic(err) 187 } 188 return newTestFileParamsWithRC(minChunks, partialChunk, rc) 189 } 190 191 // newTestFileParamsWithRC creates the required parameters for creating a siafile and 192 // creates a directory for the file. 193 func newTestFileParamsWithRC(minChunks int, partialChunk bool, rc skymodules.ErasureCoder) (string, skymodules.SiaPath, string, skymodules.ErasureCoder, crypto.CipherKey, uint64, int, os.FileMode) { 194 // Create arguments for new file. 195 sk := crypto.GenerateSiaKey(crypto.TypeDefaultRenter) 196 pieceSize := modules.SectorSize - sk.Type().Overhead() 197 siaPath := skymodules.RandomSiaPath() 198 numChunks := fastrand.Intn(10) + minChunks 199 chunkSize := pieceSize * uint64(rc.MinPieces()) 200 fileSize := chunkSize * uint64(numChunks) 201 if partialChunk { 202 fileSize-- // force partial chunk 203 } 204 fileMode := os.FileMode(777) 205 source := string(hex.EncodeToString(fastrand.Bytes(8))) 206 207 // Create the path to the file. 208 siaFilePath := siaPath.SiaFileSysPath(filepath.Join(os.TempDir(), "siafiles", hex.EncodeToString(fastrand.Bytes(16)))) 209 dir, _ := filepath.Split(siaFilePath) 210 if err := os.MkdirAll(dir, 0700); err != nil { 211 panic(err) 212 } 213 return siaFilePath, siaPath, source, rc, sk, fileSize, numChunks, fileMode 214 } 215 216 // newTestWal is a helper method to create a WAL for testing. 217 func newTestWAL() (*writeaheadlog.WAL, string) { 218 // Create the wal. 219 walsDir := filepath.Join(os.TempDir(), "wals") 220 if err := os.MkdirAll(walsDir, 0700); err != nil { 221 panic(err) 222 } 223 walFilePath := filepath.Join(walsDir, hex.EncodeToString(fastrand.Bytes(8))) 224 _, wal, err := writeaheadlog.New(walFilePath) 225 if err != nil { 226 panic(err) 227 } 228 return wal, walFilePath 229 } 230 231 // TestNewFile tests that a new file has the correct contents and size and that 232 // loading it from disk also works. 233 func TestNewFile(t *testing.T) { 234 if testing.Short() { 235 t.SkipNow() 236 } 237 t.Parallel() 238 239 // Create a siafile without a partial chunk. 240 siaFilePath, _, source, rc, sk, fileSize, numChunks, fileMode := newTestFileParams(1, false) 241 sf, _, _ := customTestFileAndWAL(siaFilePath, source, rc, sk, fileSize, numChunks, fileMode) 242 243 // Add pieces to each chunk. 244 for chunkIndex := 0; chunkIndex < sf.numChunks; chunkIndex++ { 245 for pieceIndex := 0; pieceIndex < sf.ErasureCode().NumPieces(); pieceIndex++ { 246 numPieces := fastrand.Intn(3) // up to 2 hosts for each piece 247 for i := 0; i < numPieces; i++ { 248 pk := types.SiaPublicKey{Key: fastrand.Bytes(crypto.EntropySize)} 249 mr := crypto.Hash{} 250 fastrand.Read(mr[:]) 251 if err := sf.AddPiece(pk, uint64(chunkIndex), uint64(pieceIndex), mr); err != nil { 252 panic(err) 253 } 254 } 255 } 256 } 257 258 // Check that StaticPagesPerChunk was set correctly. 259 if sf.staticMetadata.StaticPagesPerChunk != numChunkPagesRequired(sf.staticMetadata.staticErasureCode.NumPieces()) { 260 t.Fatal("StaticPagesPerChunk wasn't set correctly") 261 } 262 263 // Marshal the metadata. 264 md, err := marshalMetadata(sf.staticMetadata) 265 if err != nil { 266 t.Fatal(err) 267 } 268 // Marshal the pubKeyTable. 269 pkt, err := marshalPubKeyTable(sf.pubKeyTable) 270 if err != nil { 271 t.Fatal(err) 272 } 273 // Marshal the chunks. 274 var chunks [][]byte 275 var chunksMarshaled []chunk 276 err = sf.iterateChunksReadonly(func(chunk chunk) error { 277 c := marshalChunk(chunk) 278 chunks = append(chunks, c) 279 return nil 280 }) 281 if err != nil { 282 t.Fatal(err) 283 } 284 285 // Save the SiaFile to make sure cached fields are persisted too. 286 if err := sf.saveFile(chunksMarshaled); err != nil { 287 t.Fatal(err) 288 } 289 290 // Open the file. 291 f, err := os.OpenFile(sf.siaFilePath, os.O_RDWR, 777) 292 if err != nil { 293 t.Fatal("Failed to open file", err) 294 } 295 defer closeFileInTest(t, f) 296 // Check the filesize. It should be equal to the offset of the last chunk 297 // on disk + its marshaled length. 298 fi, err := f.Stat() 299 if err != nil { 300 t.Fatal(err) 301 } 302 // If the file only has 1 partial chunk and no full chunk don't do this check. 303 if fi.Size() != sf.chunkOffset(sf.numChunks-1)+int64(len(chunks[len(chunks)-1])) { 304 t.Fatal("file doesn't have right size") 305 } 306 // Compare the metadata to the on-disk metadata. 307 readMD := make([]byte, len(md)) 308 _, err = f.ReadAt(readMD, 0) 309 if err != nil { 310 t.Fatal(err) 311 } 312 if !bytes.Equal(readMD, md) { 313 t.Log(string(readMD)) 314 t.Log(string(md)) 315 t.Fatal("metadata doesn't equal on-disk metadata") 316 } 317 // Compare the pubKeyTable to the on-disk pubKeyTable. 318 readPKT := make([]byte, len(pkt)) 319 _, err = f.ReadAt(readPKT, sf.staticMetadata.PubKeyTableOffset) 320 if err != nil { 321 t.Fatal(err) 322 } 323 if !bytes.Equal(readPKT, pkt) { 324 t.Fatal("pubKeyTable doesn't equal on-disk pubKeyTable") 325 } 326 // Compare the chunks to the on-disk chunks one-by-one. 327 readChunk := make([]byte, int(sf.staticMetadata.StaticPagesPerChunk)*pageSize) 328 err = sf.iterateChunksReadonly(func(chunk chunk) error { 329 _, err := f.ReadAt(readChunk, sf.chunkOffset(chunk.Index)) 330 if err != nil && !errors.Contains(err, io.EOF) { 331 t.Fatal(err) 332 } 333 if !bytes.Equal(readChunk[:len(chunks[chunk.Index])], chunks[chunk.Index]) { 334 t.Fatal("readChunks don't equal on-disk readChunks") 335 } 336 return nil 337 }) 338 if err != nil { 339 t.Fatal(err) 340 } 341 // Load the file from disk and check that they are the same. 342 sf2, err := LoadSiaFile(sf.siaFilePath, sf.wal) 343 if err != nil { 344 t.Fatal("failed to load SiaFile from disk", err) 345 } 346 // Compare the files. 347 if err := equalFiles(sf, sf2); err != nil { 348 t.Fatal(err) 349 } 350 } 351 352 // TestCreateReadInsertUpdate tests if an update can be created using createInsertUpdate 353 // and if the created update can be read using readInsertUpdate. 354 func TestCreateReadInsertUpdate(t *testing.T) { 355 if testing.Short() { 356 t.SkipNow() 357 } 358 t.Parallel() 359 360 sf := newTestFile() 361 // Create update randomly 362 index := int64(fastrand.Intn(100)) 363 data := fastrand.Bytes(10) 364 update := sf.createInsertUpdate(index, data) 365 // Read update 366 readPath, readIndex, readData, err := readInsertUpdate(update) 367 if err != nil { 368 t.Fatal("Failed to read update", err) 369 } 370 // Compare values 371 if readPath != sf.siaFilePath { 372 t.Error("paths doesn't match") 373 } 374 if readIndex != index { 375 t.Error("index doesn't match") 376 } 377 if !bytes.Equal(readData, data) { 378 t.Error("data doesn't match") 379 } 380 } 381 382 // TestCreateReadDeleteUpdate tests if an update can be created using 383 // createDeleteUpdate and if the created update can be read using 384 // readDeleteUpdate. 385 func TestCreateReadDeleteUpdate(t *testing.T) { 386 if testing.Short() { 387 t.SkipNow() 388 } 389 t.Parallel() 390 391 sf := newTestFile() 392 update := sf.createDeleteUpdate() 393 // Read update 394 path := readDeleteUpdate(update) 395 // Compare values 396 if path != sf.siaFilePath { 397 t.Error("paths doesn't match") 398 } 399 } 400 401 // TestDelete tests if deleting a siafile removes the file from disk and sets 402 // the deleted flag correctly. 403 func TestDelete(t *testing.T) { 404 if testing.Short() { 405 t.SkipNow() 406 } 407 t.Parallel() 408 409 // Create SiaFileSet with SiaFile 410 entry := newTestFile() 411 // Delete file. 412 if err := entry.Delete(); err != nil { 413 t.Fatal("Failed to delete file", err) 414 } 415 // Check if file was deleted and if deleted flag was set. 416 if !entry.Deleted() { 417 t.Fatal("Deleted flag was not set correctly") 418 } 419 if _, err := os.Open(entry.siaFilePath); !os.IsNotExist(err) { 420 t.Fatal("Expected a file doesn't exist error but got", err) 421 } 422 } 423 424 // TestRename tests if renaming a siafile moves the file correctly and also 425 // updates the metadata. 426 func TestRename(t *testing.T) { 427 if testing.Short() { 428 t.SkipNow() 429 } 430 t.Parallel() 431 432 // Create SiaFileSet with SiaFile 433 entry := newTestFile() 434 435 // Create new paths for the file. 436 oldSiaFilePath := entry.SiaFilePath() 437 newSiaFilePath := strings.TrimSuffix(entry.SiaFilePath(), skymodules.SiaFileExtension) + "_renamed" + skymodules.SiaFileExtension 438 439 // Rename file 440 if err := entry.Rename(newSiaFilePath); err != nil { 441 t.Fatal("Failed to rename file", err) 442 } 443 444 // Check if the file was moved. 445 if _, err := os.Open(oldSiaFilePath); !os.IsNotExist(err) { 446 t.Fatal("Expected a file doesn't exist error but got", err) 447 } 448 f, err := os.Open(newSiaFilePath) 449 if err != nil { 450 t.Fatal("Failed to open file at new location", err) 451 } 452 if err := f.Close(); err != nil { 453 t.Fatal(err) 454 } 455 // Check the metadata. 456 if entry.siaFilePath != newSiaFilePath { 457 t.Fatal("SiaFilePath wasn't updated correctly") 458 } 459 if entry.SiaFilePath() != newSiaFilePath { 460 t.Fatal("SiaPath wasn't updated correctly", entry.SiaFilePath(), newSiaFilePath) 461 } 462 } 463 464 // TestApplyUpdates tests a variety of functions that are used to apply 465 // updates. 466 func TestApplyUpdates(t *testing.T) { 467 if testing.Short() { 468 t.SkipNow() 469 } 470 t.Parallel() 471 472 t.Run("TestApplyUpdates", func(t *testing.T) { 473 siaFile := newTestFile() 474 testApply(t, siaFile, ApplyUpdates) 475 }) 476 t.Run("TestSiaFileApplyUpdates", func(t *testing.T) { 477 siaFile := newTestFile() 478 testApply(t, siaFile, siaFile.applyUpdates) 479 }) 480 t.Run("TestCreateAndApplyTransaction", func(t *testing.T) { 481 siaFile := newTestFile() 482 testApply(t, siaFile, siaFile.createAndApplyTransaction) 483 }) 484 } 485 486 // TestSaveSmallHeader tests the saveHeader method for a header that is not big 487 // enough to need more than a single page on disk. 488 func TestSaveSmallHeader(t *testing.T) { 489 if testing.Short() { 490 t.SkipNow() 491 } 492 t.Parallel() 493 494 sf := newBlankTestFile() 495 496 // Add some host keys. 497 sf.addRandomHostKeys(10) 498 499 // Save the header. 500 updates, err := sf.saveHeaderUpdates() 501 if err != nil { 502 t.Fatal("Failed to create updates to save header", err) 503 } 504 if err := sf.createAndApplyTransaction(updates...); err != nil { 505 t.Fatal("Failed to save header", err) 506 } 507 508 // Manually open the file to check its contents. 509 f, err := os.Open(sf.siaFilePath) 510 if err != nil { 511 t.Fatal("Failed to open file", err) 512 } 513 defer closeFileInTest(t, f) 514 515 // Make sure the metadata was written to disk correctly. 516 rawMetadata, err := marshalMetadata(sf.staticMetadata) 517 if err != nil { 518 t.Fatal("Failed to marshal metadata", err) 519 } 520 readMetadata := make([]byte, len(rawMetadata)) 521 if _, err := f.ReadAt(readMetadata, 0); err != nil { 522 t.Fatal("Failed to read metadata", err) 523 } 524 if !bytes.Equal(rawMetadata, readMetadata) { 525 fmt.Println(string(rawMetadata)) 526 fmt.Println(string(readMetadata)) 527 t.Fatal("Metadata on disk doesn't match marshaled metadata") 528 } 529 530 // Make sure that the pubKeyTable was written to disk correctly. 531 rawPubKeyTAble, err := marshalPubKeyTable(sf.pubKeyTable) 532 if err != nil { 533 t.Fatal("Failed to marshal pubKeyTable", err) 534 } 535 readPubKeyTable := make([]byte, len(rawPubKeyTAble)) 536 if _, err := f.ReadAt(readPubKeyTable, sf.staticMetadata.PubKeyTableOffset); err != nil { 537 t.Fatal("Failed to read pubKeyTable", err) 538 } 539 if !bytes.Equal(rawPubKeyTAble, readPubKeyTable) { 540 t.Fatal("pubKeyTable on disk doesn't match marshaled pubKeyTable") 541 } 542 } 543 544 // TestSaveLargeHeader tests the saveHeader method for a header that uses more than a single page on disk and forces a call to allocateHeaderPage 545 func TestSaveLargeHeader(t *testing.T) { 546 if testing.Short() { 547 t.SkipNow() 548 } 549 t.Parallel() 550 551 sf := newBlankTestFile() 552 553 // Add some host keys. This should force the SiaFile to allocate a new page 554 // for the pubKeyTable. 555 sf.addRandomHostKeys(100) 556 557 // Open the file. 558 f, err := os.OpenFile(sf.siaFilePath, os.O_RDWR, 777) 559 if err != nil { 560 t.Fatal("Failed to open file", err) 561 } 562 defer closeFileInTest(t, f) 563 564 // Write some data right after the ChunkOffset as a checksum. 565 chunkData := fastrand.Bytes(100) 566 _, err = f.WriteAt(chunkData, sf.staticMetadata.ChunkOffset) 567 if err != nil { 568 t.Fatal("Failed to write random chunk data", err) 569 } 570 571 // Save the header. 572 updates, err := sf.saveHeaderUpdates() 573 if err != nil { 574 t.Fatal("Failed to create updates to save header", err) 575 } 576 if err := sf.createAndApplyTransaction(updates...); err != nil { 577 t.Fatal("Failed to save header", err) 578 } 579 580 // Make sure the chunkOffset was updated correctly. 581 if sf.staticMetadata.ChunkOffset != 2*pageSize { 582 t.Fatal("ChunkOffset wasn't updated correctly", sf.staticMetadata.ChunkOffset, 2*pageSize) 583 } 584 585 // Make sure that the checksum was moved correctly. 586 readChunkData := make([]byte, len(chunkData)) 587 if _, err := f.ReadAt(readChunkData, sf.staticMetadata.ChunkOffset); err != nil { 588 t.Fatal("Checksum wasn't moved correctly") 589 } 590 591 // Make sure the metadata was written to disk correctly. 592 rawMetadata, err := marshalMetadata(sf.staticMetadata) 593 if err != nil { 594 t.Fatal("Failed to marshal metadata", err) 595 } 596 readMetadata := make([]byte, len(rawMetadata)) 597 if _, err := f.ReadAt(readMetadata, 0); err != nil { 598 t.Fatal("Failed to read metadata", err) 599 } 600 if !bytes.Equal(rawMetadata, readMetadata) { 601 fmt.Println(string(rawMetadata)) 602 fmt.Println(string(readMetadata)) 603 t.Fatal("Metadata on disk doesn't match marshaled metadata") 604 } 605 606 // Make sure that the pubKeyTable was written to disk correctly. 607 rawPubKeyTAble, err := marshalPubKeyTable(sf.pubKeyTable) 608 if err != nil { 609 t.Fatal("Failed to marshal pubKeyTable", err) 610 } 611 readPubKeyTable := make([]byte, len(rawPubKeyTAble)) 612 if _, err := f.ReadAt(readPubKeyTable, sf.staticMetadata.PubKeyTableOffset); err != nil { 613 t.Fatal("Failed to read pubKeyTable", err) 614 } 615 if !bytes.Equal(rawPubKeyTAble, readPubKeyTable) { 616 t.Fatal("pubKeyTable on disk doesn't match marshaled pubKeyTable") 617 } 618 } 619 620 // testApply tests if a given method applies a set of updates correctly. 621 func testApply(t *testing.T, siaFile *SiaFile, apply func(...writeaheadlog.Update) error) { 622 // Create an update that writes random data to a random index i. 623 index := fastrand.Intn(100) + 1 624 data := fastrand.Bytes(100) 625 update := siaFile.createInsertUpdate(int64(index), data) 626 627 // Apply update. 628 if err := apply(update); err != nil { 629 t.Fatal("Failed to apply update", err) 630 } 631 // Open file. 632 file, err := os.Open(siaFile.siaFilePath) 633 if err != nil { 634 t.Fatal("Failed to open file", err) 635 } 636 // Check if correct data was written. 637 readData := make([]byte, len(data)) 638 if _, err := file.ReadAt(readData, int64(index)); err != nil { 639 t.Fatal("Failed to read written data back from disk", err) 640 } 641 if !bytes.Equal(data, readData) { 642 t.Fatal("Read data doesn't equal written data") 643 } 644 } 645 646 // TestUpdateUsedHosts tests the updateUsedHosts method. 647 func TestUpdateUsedHosts(t *testing.T) { 648 if testing.Short() { 649 t.SkipNow() 650 } 651 t.Parallel() 652 653 sf := newBlankTestFile() 654 sf.addRandomHostKeys(10) 655 656 // All the host keys should be used. 657 for _, entry := range sf.pubKeyTable { 658 if !entry.Used { 659 t.Fatal("all hosts are expected to be used at the beginning of the test") 660 } 661 } 662 663 // Report only half the hosts as still being used. 664 var used []types.SiaPublicKey 665 for i, entry := range sf.pubKeyTable { 666 if i%2 == 0 { 667 used = append(used, entry.PublicKey) 668 } 669 } 670 updates, err := sf.updateUsedHosts(used) 671 if err != nil { 672 t.Fatal("failed to update hosts", err) 673 } 674 err = sf.createAndApplyTransaction(updates...) 675 if err != nil { 676 t.Fatal(err) 677 } 678 679 // Create a map of the used keys for faster lookups. 680 usedMap := make(map[string]struct{}) 681 for _, key := range used { 682 usedMap[key.String()] = struct{}{} 683 } 684 685 // Check that the flag was set correctly. 686 for _, entry := range sf.pubKeyTable { 687 _, exists := usedMap[entry.PublicKey.String()] 688 if entry.Used != exists { 689 t.Errorf("expected flag to be %v but was %v", exists, entry.Used) 690 } 691 } 692 693 // Reload the siafile to see if the flags were also persisted. 694 sf, err = LoadSiaFile(sf.siaFilePath, sf.wal) 695 if err != nil { 696 t.Fatal(err) 697 } 698 699 // Check that the flags are still set correctly. 700 for _, entry := range sf.pubKeyTable { 701 _, exists := usedMap[entry.PublicKey.String()] 702 if entry.Used != exists { 703 t.Errorf("expected flag to be %v but was %v", exists, entry.Used) 704 } 705 } 706 707 // Also check the flags in order. Making sure that persisting them didn't 708 // change the order. 709 for i, entry := range sf.pubKeyTable { 710 expectedUsed := i%2 == 0 711 if entry.Used != expectedUsed { 712 t.Errorf("expected flag to be %v but was %v", expectedUsed, entry.Used) 713 } 714 } 715 } 716 717 // TestChunkOffset tests the chunkOffset method. 718 func TestChunkOffset(t *testing.T) { 719 if testing.Short() { 720 t.SkipNow() 721 } 722 t.Parallel() 723 724 sf := newTestFile() 725 726 // Set the static pages per chunk to a random value. 727 sf.staticMetadata.StaticPagesPerChunk = uint8(fastrand.Intn(5)) + 1 728 729 // Calculate the offset of the first chunk. 730 offset1 := sf.chunkOffset(0) 731 if expectedOffset := sf.staticMetadata.ChunkOffset; expectedOffset != offset1 { 732 t.Fatalf("expected offset %v but got %v", sf.staticMetadata.ChunkOffset, offset1) 733 } 734 735 // Calculate the offset of the second chunk. 736 offset2 := sf.chunkOffset(1) 737 if expectedOffset := offset1 + int64(sf.staticMetadata.StaticPagesPerChunk)*pageSize; expectedOffset != offset2 { 738 t.Fatalf("expected offset %v but got %v", expectedOffset, offset2) 739 } 740 741 // Make sure that the offsets we calculated are not the same due to not 742 // initializing the file correctly. 743 if offset2 == offset1 { 744 t.Fatal("the calculated offsets are the same") 745 } 746 } 747 748 // TestSaveChunk checks that saveChunk creates an updated which if applied 749 // writes the correct data to disk. 750 func TestSaveChunk(t *testing.T) { 751 if testing.Short() { 752 t.SkipNow() 753 } 754 t.Parallel() 755 756 sf := newTestFile() 757 758 // Choose a random chunk from the file and replace it. 759 chunkIndex := fastrand.Intn(sf.numChunks) 760 chunk := randomChunk() 761 chunk.Index = chunkIndex 762 763 // Write the chunk to disk using saveChunk. 764 update := sf.saveChunkUpdate(chunk) 765 if err := sf.createAndApplyTransaction(update); err != nil { 766 t.Fatal(err) 767 } 768 769 // Marshal the chunk. 770 marshaledChunk := marshalChunk(chunk) 771 772 // Read the chunk from disk. 773 f, err := os.Open(sf.siaFilePath) 774 if err != nil { 775 t.Fatal(err) 776 } 777 defer closeFileInTest(t, f) 778 779 readChunk := make([]byte, len(marshaledChunk)) 780 if n, err := f.ReadAt(readChunk, sf.chunkOffset(chunkIndex)); err != nil { 781 t.Fatal(err, n, len(marshaledChunk)) 782 } 783 784 // The marshaled chunk should equal the chunk we read from disk. 785 if !bytes.Equal(readChunk, marshaledChunk) { 786 t.Fatal("marshaled chunk doesn't equal chunk on disk", len(readChunk), len(marshaledChunk)) 787 } 788 } 789 790 // TestCreateAndApplyTransactionPanic verifies that the 791 // createAndApplyTransaction helpers panic when the updates can't be applied. 792 func TestCreateAndApplyTransactionPanic(t *testing.T) { 793 if testing.Short() { 794 t.SkipNow() 795 } 796 t.Parallel() 797 798 // Create invalid update that triggers a panic. 799 update := writeaheadlog.Update{ 800 Name: "invalid name", 801 } 802 803 // Declare a helper to check for a panic. 804 assertRecover := func() { 805 if r := recover(); r == nil { 806 t.Fatalf("Expected a panic") 807 } 808 } 809 810 // Run the test for both the method and function 811 sf := newBlankTestFile() 812 func() { 813 defer assertRecover() 814 _ = sf.createAndApplyTransaction(update) 815 }() 816 func() { 817 defer assertRecover() 818 _ = createAndApplyTransaction(sf.wal, update) 819 }() 820 } 821 822 // TestDeleteUpdateRegression is a regression test that ensure apply updates 823 // won't panic when called with a set of updates with the last one being 824 // a delete update. 825 func TestDeleteUpdateRegression(t *testing.T) { 826 if testing.Short() { 827 t.SkipNow() 828 } 829 t.Parallel() 830 831 // Create siafile 832 sf := newBlankTestFile() 833 834 // Apply updates with the last update as a delete update. This use to trigger 835 // a panic. No need to check the return value as we are only concerned with the 836 // panic 837 update := sf.createDeleteUpdate() 838 sf.createAndApplyTransaction(update, update) 839 }