[DEPRECATED] Go implementation of plcbundle
at main 844 lines 21 kB view raw
1package bundleindex_test 2 3import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "sync" 8 "testing" 9 "time" 10 11 "tangled.org/atscan.net/plcbundle-go/internal/bundleindex" 12 "tangled.org/atscan.net/plcbundle-go/internal/types" 13) 14 15type testLogger struct { 16 t *testing.T 17} 18 19func (l *testLogger) Printf(format string, v ...interface{}) { 20 l.t.Logf(format, v...) 21} 22 23func (l *testLogger) Println(v ...interface{}) { 24 l.t.Log(v...) 25} 26 27// ==================================================================================== 28// INDEX CREATION & BASIC OPERATIONS 29// ==================================================================================== 30 31func TestIndexCreation(t *testing.T) { 32 t.Run("NewIndex", func(t *testing.T) { 33 idx := bundleindex.NewIndex("https://plc.directory") 34 35 if idx == nil { 36 t.Fatal("NewIndex returned nil") 37 } 38 39 if idx.Version != types.INDEX_VERSION { 40 t.Errorf("version mismatch: got %s, want %s", idx.Version, types.INDEX_VERSION) 41 } 42 43 if idx.Origin != "https://plc.directory" { 44 t.Errorf("origin mismatch: got %s", idx.Origin) 45 } 46 47 if idx.Count() != 0 { 48 t.Error("new index should be empty") 49 } 50 }) 51 52 t.Run("NewIndex_EmptyOrigin", func(t *testing.T) { 53 idx := bundleindex.NewIndex("") 54 55 if idx.Origin != "" { 56 t.Error("should allow empty origin") 57 } 58 }) 59} 60 61func TestIndexAddBundle(t *testing.T) { 62 t.Run("AddSingleBundle", func(t *testing.T) { 63 idx := bundleindex.NewIndex("test-origin") 64 65 meta := &bundleindex.BundleMetadata{ 66 BundleNumber: 1, 67 StartTime: time.Now(), 68 EndTime: time.Now().Add(time.Hour), 69 OperationCount: types.BUNDLE_SIZE, 70 DIDCount: 1000, 71 Hash: "hash123", 72 ContentHash: "content123", 73 CompressedHash: "compressed123", 74 CompressedSize: 1024, 75 UncompressedSize: 5120, 76 } 77 78 idx.AddBundle(meta) 79 80 if idx.Count() != 1 { 81 t.Errorf("count should be 1, got %d", idx.Count()) 82 } 83 84 retrieved, err := idx.GetBundle(1) 85 if err != nil { 86 t.Fatalf("GetBundle failed: %v", err) 87 } 88 89 if retrieved.Hash != "hash123" { 90 t.Error("hash mismatch after retrieval") 91 } 92 }) 93 94 t.Run("AddMultipleBundles_AutoSort", func(t *testing.T) { 95 idx := bundleindex.NewIndex("test-origin") 96 97 // Add bundles out of order: 3, 1, 2 98 for _, num := range []int{3, 1, 2} { 99 meta := &bundleindex.BundleMetadata{ 100 BundleNumber: num, 101 StartTime: time.Now(), 102 EndTime: time.Now().Add(time.Hour), 103 OperationCount: types.BUNDLE_SIZE, 104 } 105 idx.AddBundle(meta) 106 } 107 108 bundles := idx.GetBundles() 109 110 // Should be sorted: 1, 2, 3 111 if bundles[0].BundleNumber != 1 { 112 t.Error("bundles not sorted") 113 } 114 if bundles[1].BundleNumber != 2 { 115 t.Error("bundles not sorted") 116 } 117 if bundles[2].BundleNumber != 3 { 118 t.Error("bundles not sorted") 119 } 120 }) 121 122 t.Run("UpdateExistingBundle", func(t *testing.T) { 123 idx := bundleindex.NewIndex("test-origin") 124 125 original := &bundleindex.BundleMetadata{ 126 BundleNumber: 1, 127 Hash: "original_hash", 128 StartTime: time.Now(), 129 EndTime: time.Now().Add(time.Hour), 130 OperationCount: types.BUNDLE_SIZE, 131 } 132 133 idx.AddBundle(original) 134 135 // Add again with different hash (update) 136 updated := &bundleindex.BundleMetadata{ 137 BundleNumber: 1, 138 Hash: "updated_hash", 139 StartTime: time.Now(), 140 EndTime: time.Now().Add(time.Hour), 141 OperationCount: types.BUNDLE_SIZE, 142 } 143 144 idx.AddBundle(updated) 145 146 // Should have only 1 bundle (updated, not duplicated) 147 if idx.Count() != 1 { 148 t.Errorf("should have 1 bundle after update, got %d", idx.Count()) 149 } 150 151 retrieved, _ := idx.GetBundle(1) 152 if retrieved.Hash != "updated_hash" { 153 t.Error("bundle was not updated") 154 } 155 }) 156} 157 158// ==================================================================================== 159// SAVE & LOAD TESTS 160// ==================================================================================== 161 162func TestIndexPersistence(t *testing.T) { 163 tmpDir := t.TempDir() 164 165 t.Run("SaveAndLoad", func(t *testing.T) { 166 indexPath := filepath.Join(tmpDir, "test_index.json") 167 168 // Create and populate index 169 idx := bundleindex.NewIndex("https://plc.directory") 170 171 for i := 1; i <= 5; i++ { 172 meta := &bundleindex.BundleMetadata{ 173 BundleNumber: i, 174 StartTime: time.Now().Add(time.Duration(i-1) * time.Hour), 175 EndTime: time.Now().Add(time.Duration(i) * time.Hour), 176 OperationCount: types.BUNDLE_SIZE, 177 DIDCount: 1000 * i, 178 Hash: fmt.Sprintf("hash%d", i), 179 ContentHash: fmt.Sprintf("content%d", i), 180 CompressedHash: fmt.Sprintf("compressed%d", i), 181 CompressedSize: int64(1024 * i), 182 UncompressedSize: int64(5120 * i), 183 } 184 idx.AddBundle(meta) 185 } 186 187 // Save 188 if err := idx.Save(indexPath); err != nil { 189 t.Fatalf("Save failed: %v", err) 190 } 191 192 // Verify file exists 193 if _, err := os.Stat(indexPath); os.IsNotExist(err) { 194 t.Fatal("index file not created") 195 } 196 197 // Load 198 loaded, err := bundleindex.LoadIndex(indexPath) 199 if err != nil { 200 t.Fatalf("LoadIndex failed: %v", err) 201 } 202 203 // Verify data integrity 204 if loaded.Count() != 5 { 205 t.Errorf("loaded count mismatch: got %d, want 5", loaded.Count()) 206 } 207 208 if loaded.Origin != "https://plc.directory" { 209 t.Error("origin not preserved") 210 } 211 212 if loaded.LastBundle != 5 { 213 t.Error("LastBundle not calculated correctly") 214 } 215 216 // Verify specific bundle 217 bundle3, err := loaded.GetBundle(3) 218 if err != nil { 219 t.Fatalf("GetBundle(3) failed: %v", err) 220 } 221 222 if bundle3.Hash != "hash3" { 223 t.Error("bundle data not preserved") 224 } 225 }) 226 227 t.Run("AtomicSave", func(t *testing.T) { 228 indexPath := filepath.Join(tmpDir, "atomic_test.json") 229 230 idx := bundleindex.NewIndex("test") 231 idx.AddBundle(&bundleindex.BundleMetadata{ 232 BundleNumber: 1, 233 StartTime: time.Now(), 234 EndTime: time.Now(), 235 OperationCount: types.BUNDLE_SIZE, 236 }) 237 238 idx.Save(indexPath) 239 240 // Verify no .tmp file left behind 241 tmpPath := indexPath + ".tmp" 242 if _, err := os.Stat(tmpPath); !os.IsNotExist(err) { 243 t.Error("temporary file should not exist after successful save") 244 } 245 }) 246 247 t.Run("LoadInvalidVersion", func(t *testing.T) { 248 indexPath := filepath.Join(tmpDir, "invalid_version.json") 249 250 // Write index with wrong version 251 invalidData := `{"version":"99.99","origin":"test","bundles":[]}` 252 os.WriteFile(indexPath, []byte(invalidData), 0644) 253 254 _, err := bundleindex.LoadIndex(indexPath) 255 if err == nil { 256 t.Error("should reject index with invalid version") 257 } 258 }) 259 260 t.Run("LoadCorruptedJSON", func(t *testing.T) { 261 indexPath := filepath.Join(tmpDir, "corrupted.json") 262 263 os.WriteFile(indexPath, []byte("{invalid json"), 0644) 264 265 _, err := bundleindex.LoadIndex(indexPath) 266 if err == nil { 267 t.Error("should reject corrupted JSON") 268 } 269 }) 270} 271 272// ==================================================================================== 273// QUERY OPERATIONS 274// ==================================================================================== 275 276func TestIndexQueries(t *testing.T) { 277 idx := bundleindex.NewIndex("test") 278 279 // Populate with bundles 280 for i := 1; i <= 10; i++ { 281 meta := &bundleindex.BundleMetadata{ 282 BundleNumber: i, 283 StartTime: time.Now().Add(time.Duration(i-1) * time.Hour), 284 EndTime: time.Now().Add(time.Duration(i) * time.Hour), 285 OperationCount: types.BUNDLE_SIZE, 286 CompressedSize: int64(i * 1000), 287 } 288 idx.AddBundle(meta) 289 } 290 291 t.Run("GetBundle", func(t *testing.T) { 292 meta, err := idx.GetBundle(5) 293 if err != nil { 294 t.Fatalf("GetBundle failed: %v", err) 295 } 296 297 if meta.BundleNumber != 5 { 298 t.Error("wrong bundle returned") 299 } 300 }) 301 302 t.Run("GetBundle_NotFound", func(t *testing.T) { 303 _, err := idx.GetBundle(999) 304 if err == nil { 305 t.Error("should return error for nonexistent bundle") 306 } 307 }) 308 309 t.Run("GetLastBundle", func(t *testing.T) { 310 last := idx.GetLastBundle() 311 312 if last == nil { 313 t.Fatal("GetLastBundle returned nil") 314 } 315 316 if last.BundleNumber != 10 { 317 t.Errorf("last bundle should be 10, got %d", last.BundleNumber) 318 } 319 }) 320 321 t.Run("GetLastBundle_Empty", func(t *testing.T) { 322 emptyIdx := bundleindex.NewIndex("test") 323 324 last := emptyIdx.GetLastBundle() 325 326 if last != nil { 327 t.Error("empty index should return nil for GetLastBundle") 328 } 329 }) 330 331 t.Run("GetBundleRange", func(t *testing.T) { 332 bundles := idx.GetBundleRange(3, 7) 333 334 if len(bundles) != 5 { 335 t.Errorf("expected 5 bundles, got %d", len(bundles)) 336 } 337 338 if bundles[0].BundleNumber != 3 || bundles[4].BundleNumber != 7 { 339 t.Error("range boundaries incorrect") 340 } 341 }) 342 343 t.Run("GetBundleRange_OutOfBounds", func(t *testing.T) { 344 bundles := idx.GetBundleRange(100, 200) 345 346 if len(bundles) != 0 { 347 t.Errorf("expected 0 bundles for out-of-range query, got %d", len(bundles)) 348 } 349 }) 350 351 t.Run("GetBundles_ReturnsShallowCopy", func(t *testing.T) { 352 bundles1 := idx.GetBundles() 353 bundles2 := idx.GetBundles() 354 355 // Should be different slices 356 if &bundles1[0] == &bundles2[0] { 357 t.Error("GetBundles should return copy, not same slice") 358 } 359 360 // But same data 361 if bundles1[0].BundleNumber != bundles2[0].BundleNumber { 362 t.Error("bundle data should be same") 363 } 364 }) 365} 366 367// ==================================================================================== 368// GAP DETECTION - CRITICAL FOR INTEGRITY 369// ==================================================================================== 370 371func TestIndexFindGaps(t *testing.T) { 372 t.Run("NoGaps", func(t *testing.T) { 373 idx := bundleindex.NewIndex("test") 374 375 for i := 1; i <= 10; i++ { 376 idx.AddBundle(createTestMetadata(i)) 377 } 378 379 gaps := idx.FindGaps() 380 381 if len(gaps) != 0 { 382 t.Errorf("expected no gaps, found %d: %v", len(gaps), gaps) 383 } 384 }) 385 386 t.Run("SingleGap", func(t *testing.T) { 387 idx := bundleindex.NewIndex("test") 388 389 // Add bundles 1, 2, 4, 5 (missing 3) 390 for _, num := range []int{1, 2, 4, 5} { 391 idx.AddBundle(createTestMetadata(num)) 392 } 393 394 gaps := idx.FindGaps() 395 396 if len(gaps) != 1 { 397 t.Errorf("expected 1 gap, got %d", len(gaps)) 398 } 399 400 if len(gaps) > 0 && gaps[0] != 3 { 401 t.Errorf("expected gap at 3, got %d", gaps[0]) 402 } 403 }) 404 405 t.Run("MultipleGaps", func(t *testing.T) { 406 idx := bundleindex.NewIndex("test") 407 408 // Add bundles 1, 2, 5, 6, 9, 10 (missing 3, 4, 7, 8) 409 for _, num := range []int{1, 2, 5, 6, 9, 10} { 410 idx.AddBundle(createTestMetadata(num)) 411 } 412 413 gaps := idx.FindGaps() 414 415 expectedGaps := []int{3, 4, 7, 8} 416 if len(gaps) != len(expectedGaps) { 417 t.Errorf("expected %d gaps, got %d", len(expectedGaps), len(gaps)) 418 } 419 420 for i, expected := range expectedGaps { 421 if gaps[i] != expected { 422 t.Errorf("gap %d: got %d, want %d", i, gaps[i], expected) 423 } 424 } 425 }) 426 427 t.Run("FindGaps_EmptyIndex", func(t *testing.T) { 428 idx := bundleindex.NewIndex("test") 429 430 gaps := idx.FindGaps() 431 432 if len(gaps) > 0 { 433 t.Error("empty index should have no gaps") 434 } 435 }) 436 437 t.Run("FindGaps_NonSequentialStart", func(t *testing.T) { 438 idx := bundleindex.NewIndex("test") 439 440 // Start at bundle 100 441 for i := 100; i <= 105; i++ { 442 idx.AddBundle(createTestMetadata(i)) 443 } 444 445 gaps := idx.FindGaps() 446 447 // No gaps between 100-105 448 if len(gaps) != 0 { 449 t.Errorf("expected no gaps, got %d", len(gaps)) 450 } 451 }) 452} 453 454// ==================================================================================== 455// STATISTICS & DERIVED FIELDS 456// ==================================================================================== 457 458func TestIndexStatistics(t *testing.T) { 459 idx := bundleindex.NewIndex("test") 460 461 t.Run("StatsEmpty", func(t *testing.T) { 462 stats := idx.GetStats() 463 464 if stats["bundle_count"].(int) != 0 { 465 t.Error("empty index should have count 0") 466 } 467 }) 468 469 t.Run("StatsPopulated", func(t *testing.T) { 470 totalSize := int64(0) 471 totalUncompressed := int64(0) 472 473 for i := 1; i <= 5; i++ { 474 meta := &bundleindex.BundleMetadata{ 475 BundleNumber: i, 476 StartTime: time.Now().Add(time.Duration(i-1) * time.Hour), 477 EndTime: time.Now().Add(time.Duration(i) * time.Hour), 478 OperationCount: types.BUNDLE_SIZE, 479 CompressedSize: int64(1000 * i), 480 UncompressedSize: int64(5000 * i), 481 } 482 idx.AddBundle(meta) 483 totalSize += meta.CompressedSize 484 totalUncompressed += meta.UncompressedSize 485 } 486 487 stats := idx.GetStats() 488 489 if stats["bundle_count"].(int) != 5 { 490 t.Error("bundle count mismatch") 491 } 492 493 if stats["first_bundle"].(int) != 1 { 494 t.Error("first_bundle mismatch") 495 } 496 497 if stats["last_bundle"].(int) != 5 { 498 t.Error("last_bundle mismatch") 499 } 500 501 if stats["total_size"].(int64) != totalSize { 502 t.Errorf("total_size mismatch: got %d, want %d", stats["total_size"].(int64), totalSize) 503 } 504 505 if stats["total_uncompressed_size"].(int64) != totalUncompressed { 506 t.Error("total_uncompressed_size mismatch") 507 } 508 509 if _, ok := stats["start_time"]; !ok { 510 t.Error("stats missing start_time") 511 } 512 513 if _, ok := stats["end_time"]; !ok { 514 t.Error("stats missing end_time") 515 } 516 517 if stats["gaps"].(int) != 0 { 518 t.Error("should have no gaps") 519 } 520 }) 521 522 t.Run("StatsRecalculateAfterAdd", func(t *testing.T) { 523 idx := bundleindex.NewIndex("test") 524 525 idx.AddBundle(&bundleindex.BundleMetadata{ 526 BundleNumber: 1, 527 StartTime: time.Now(), 528 EndTime: time.Now(), 529 OperationCount: types.BUNDLE_SIZE, 530 CompressedSize: 1000, 531 }) 532 533 stats1 := idx.GetStats() 534 size1 := stats1["total_size"].(int64) 535 536 // Add another bundle 537 idx.AddBundle(&bundleindex.BundleMetadata{ 538 BundleNumber: 2, 539 StartTime: time.Now(), 540 EndTime: time.Now(), 541 OperationCount: types.BUNDLE_SIZE, 542 CompressedSize: 2000, 543 }) 544 545 stats2 := idx.GetStats() 546 size2 := stats2["total_size"].(int64) 547 548 if size2 != size1+2000 { 549 t.Errorf("total_size not recalculated: got %d, want %d", size2, size1+2000) 550 } 551 552 if stats2["last_bundle"].(int) != 2 { 553 t.Error("last_bundle not recalculated") 554 } 555 }) 556} 557 558// ==================================================================================== 559// REBUILD OPERATION 560// ==================================================================================== 561 562func TestIndexRebuild(t *testing.T) { 563 t.Run("RebuildFromMetadata", func(t *testing.T) { 564 idx := bundleindex.NewIndex("original") 565 566 // Add some bundles 567 for i := 1; i <= 3; i++ { 568 idx.AddBundle(createTestMetadata(i)) 569 } 570 571 if idx.Count() != 3 { 572 t.Fatal("setup failed") 573 } 574 575 // Create new metadata for rebuild 576 newMetadata := []*bundleindex.BundleMetadata{ 577 createTestMetadata(1), 578 createTestMetadata(2), 579 createTestMetadata(5), 580 createTestMetadata(6), 581 } 582 583 // Rebuild 584 idx.Rebuild(newMetadata) 585 586 // Should now have 4 bundles 587 if idx.Count() != 4 { 588 t.Errorf("after rebuild, expected 4 bundles, got %d", idx.Count()) 589 } 590 591 // Should have new bundles 5, 6 592 if _, err := idx.GetBundle(5); err != nil { 593 t.Error("should have bundle 5 after rebuild") 594 } 595 596 // Should not have bundle 3 597 if _, err := idx.GetBundle(3); err == nil { 598 t.Error("should not have bundle 3 after rebuild") 599 } 600 601 // Origin should be preserved 602 if idx.Origin != "original" { 603 t.Error("origin should be preserved during rebuild") 604 } 605 }) 606 607 t.Run("RebuildAutoSorts", func(t *testing.T) { 608 idx := bundleindex.NewIndex("test") 609 610 // Rebuild with unsorted data 611 unsorted := []*bundleindex.BundleMetadata{ 612 createTestMetadata(5), 613 createTestMetadata(2), 614 createTestMetadata(8), 615 createTestMetadata(1), 616 } 617 618 idx.Rebuild(unsorted) 619 620 bundles := idx.GetBundles() 621 622 // Should be sorted 623 for i := 0; i < len(bundles)-1; i++ { 624 if bundles[i].BundleNumber >= bundles[i+1].BundleNumber { 625 t.Error("bundles not sorted after rebuild") 626 } 627 } 628 }) 629} 630 631// ==================================================================================== 632// CLEAR OPERATION 633// ==================================================================================== 634 635func TestIndexClear(t *testing.T) { 636 idx := bundleindex.NewIndex("test") 637 638 // Populate 639 for i := 1; i <= 10; i++ { 640 idx.AddBundle(createTestMetadata(i)) 641 } 642 643 if idx.Count() != 10 { 644 t.Fatal("setup failed") 645 } 646 647 // Clear 648 idx.Clear() 649 650 if idx.Count() != 0 { 651 t.Error("count should be 0 after clear") 652 } 653 654 if idx.LastBundle != 0 { 655 t.Error("LastBundle should be 0 after clear") 656 } 657 658 if idx.TotalSize != 0 { 659 t.Error("TotalSize should be 0 after clear") 660 } 661 662 // Should be able to add after clear 663 idx.AddBundle(createTestMetadata(1)) 664 665 if idx.Count() != 1 { 666 t.Error("should be able to add after clear") 667 } 668} 669 670// ==================================================================================== 671// CONCURRENCY TESTS 672// ==================================================================================== 673 674func TestIndexConcurrency(t *testing.T) { 675 t.Run("ConcurrentReads", func(t *testing.T) { 676 idx := bundleindex.NewIndex("test") 677 678 // Populate 679 for i := 1; i <= 100; i++ { 680 idx.AddBundle(createTestMetadata(i)) 681 } 682 683 // 100 concurrent readers 684 var wg sync.WaitGroup 685 errors := make(chan error, 100) 686 687 for i := 0; i < 100; i++ { 688 wg.Add(1) 689 go func(id int) { 690 defer wg.Done() 691 692 // Various read operations 693 idx.Count() 694 idx.GetLastBundle() 695 idx.GetBundles() 696 idx.FindGaps() 697 idx.GetStats() 698 699 if _, err := idx.GetBundle(id%100 + 1); err != nil { 700 errors <- err 701 } 702 }(i) 703 } 704 705 wg.Wait() 706 close(errors) 707 708 for err := range errors { 709 t.Errorf("concurrent read error: %v", err) 710 } 711 }) 712 713 t.Run("ConcurrentReadsDuringSave", func(t *testing.T) { 714 tmpDir := t.TempDir() 715 indexPath := filepath.Join(tmpDir, "concurrent.json") 716 717 idx := bundleindex.NewIndex("test") 718 719 for i := 1; i <= 50; i++ { 720 idx.AddBundle(createTestMetadata(i)) 721 } 722 723 var wg sync.WaitGroup 724 725 // Saver goroutine 726 wg.Add(1) 727 go func() { 728 defer wg.Done() 729 for i := 0; i < 10; i++ { 730 idx.Save(indexPath) 731 time.Sleep(10 * time.Millisecond) 732 } 733 }() 734 735 // Reader goroutines 736 for i := 0; i < 10; i++ { 737 wg.Add(1) 738 go func() { 739 defer wg.Done() 740 for j := 0; j < 50; j++ { 741 idx.Count() 742 idx.GetBundles() 743 time.Sleep(5 * time.Millisecond) 744 } 745 }() 746 } 747 748 wg.Wait() 749 }) 750} 751 752// ==================================================================================== 753// REMOTE UPDATE TESTS (FOR CLONING) 754// ==================================================================================== 755 756func TestIndexUpdateFromRemote(t *testing.T) { 757 758 t.Run("UpdateFromRemote_Basic", func(t *testing.T) { 759 idx := bundleindex.NewIndex("test") 760 761 // Local has bundles 1-3 762 for i := 1; i <= 3; i++ { 763 idx.AddBundle(createTestMetadata(i)) 764 } 765 766 // Remote has bundles 1-5 767 remoteMeta := make(map[int]*bundleindex.BundleMetadata) 768 for i := 1; i <= 5; i++ { 769 remoteMeta[i] = createTestMetadata(i) 770 } 771 772 bundlesToUpdate := []int{4, 5} 773 774 // Mock file existence (4 and 5 exist) 775 fileExists := func(bundleNum int) bool { 776 return bundleNum == 4 || bundleNum == 5 777 } 778 779 logger := &testLogger{t: &testing.T{}} 780 781 err := idx.UpdateFromRemote(bundlesToUpdate, remoteMeta, fileExists, false, logger) 782 if err != nil { 783 t.Fatalf("UpdateFromRemote failed: %v", err) 784 } 785 786 // Should now have 5 bundles 787 if idx.Count() != 5 { 788 t.Errorf("expected 5 bundles after update, got %d", idx.Count()) 789 } 790 }) 791 792 t.Run("UpdateFromRemote_SkipsMissingFiles", func(t *testing.T) { 793 idx := bundleindex.NewIndex("test") 794 795 remoteMeta := map[int]*bundleindex.BundleMetadata{ 796 1: createTestMetadata(1), 797 2: createTestMetadata(2), 798 } 799 800 bundlesToUpdate := []int{1, 2} 801 802 // Only bundle 1 exists locally 803 fileExists := func(bundleNum int) bool { 804 return bundleNum == 1 805 } 806 807 logger := &testLogger{t: &testing.T{}} 808 809 err := idx.UpdateFromRemote(bundlesToUpdate, remoteMeta, fileExists, false, logger) 810 if err != nil { 811 t.Fatalf("UpdateFromRemote failed: %v", err) 812 } 813 814 // Should only have bundle 1 815 if idx.Count() != 1 { 816 t.Errorf("expected 1 bundle, got %d", idx.Count()) 817 } 818 819 if _, err := idx.GetBundle(2); err == nil { 820 t.Error("should not have bundle 2 (file missing)") 821 } 822 }) 823} 824 825// ==================================================================================== 826// HELPER FUNCTIONS 827// ==================================================================================== 828 829func createTestMetadata(bundleNum int) *bundleindex.BundleMetadata { 830 return &bundleindex.BundleMetadata{ 831 BundleNumber: bundleNum, 832 StartTime: time.Now().Add(time.Duration(bundleNum-1) * time.Hour), 833 EndTime: time.Now().Add(time.Duration(bundleNum) * time.Hour), 834 OperationCount: types.BUNDLE_SIZE, 835 DIDCount: 1000, 836 Hash: fmt.Sprintf("hash%d", bundleNum), 837 ContentHash: fmt.Sprintf("content%d", bundleNum), 838 Parent: fmt.Sprintf("parent%d", bundleNum-1), 839 CompressedHash: fmt.Sprintf("compressed%d", bundleNum), 840 CompressedSize: int64(1000 * bundleNum), 841 UncompressedSize: int64(5000 * bundleNum), 842 CreatedAt: time.Now(), 843 } 844}