╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
371 // create the temp dir 372 const cwd = std.Io.Dir.cwd(); 373 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 374 if (temp_dir_or_err) |*temp_dir| { 375 temp_dir.close(io); 376 try cwd.deleteTree(io, temp_dir_name); 377 } else |_| {} 378 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 379 defer cwd.deleteTree(io, temp_dir_name) catch {}; 380 defer temp_dir.close(io); 381 382 const cwd_path = try std.process.currentPathAlloc(io, allocator); 383 defer allocator.free(cwd_path); 384 385 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 386 defer allocator.free(work_path); 387 388 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 389 const Repo = rp.Repo(.xit, repo_opts); 390 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 391 defer repo.deinit(io, allocator); 392 393 var arena = std.heap.ArenaAllocator.init(allocator); 394 defer arena.deinit(); 395 396 // 397 // define test events 398 // 399 400 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 401 402 const events_to_consume = [_]evt.EventWithId{ 403 .{ 404 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 405 .event = .{ 406 .issue = .{ 407 .title = "Login form clears password on validation error", 408 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 409 .tags = "bug\x00priority-high\x00ui", 410 }, 411 }, 412 }, 413 .{ 414 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 415 .event = .{ 416 .issue = .{ 417 .title = "Search results ignore archived project filter", 418 .description = "Filtering search results to active projects still returns issues from archived projects. Apply the archived flag before ranking results.", 419 .tags = "bug\x00search\x00backend", 420 }, 421 }, 422 }, 423 .{ 424 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 425 .event = .{ 426 .issue = .{ 427 .title = "Issue list does not persist selected sort order", 428 .description = "Changing the issue list sort order is lost after refresh. Store the selected sort field and direction with the user's view preferences.", 429 .tags = "enhancement\x00frontend\x00preferences", 430 }, 431 }, 432 }, 433 }; 434 435 // 436 // insert issues as commits in the repo 437 // 438 439 var first_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 440 441 { 442 var json: std.Io.Writer.Allocating = .init(allocator); 443 defer json.deinit(); 444 445 for (events_to_consume, 0..) |event, i| { 446 json.clearRetainingCapacity(); 447 448 try std.json.Stringify.value(event, .{}, &json.writer); 449 450 // commit the event into a special branch 451 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 452 if (i == 0) { 453 _ = try std.fmt.hexToBytes(&first_oid, &oid); 454 } 455 } 456 } 457 458 // 459 // consume events into the database 460 // 461 462 { 463 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 464 465 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 466 467 // read the moment we just created 468 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 469 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 470 471 // get the last object id 472 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 473 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 474 _ = try last_object_id_cursor.readBytes(&last_object_id); 475 476 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 477 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 478 479 try std.testing.expectEqual(1, try haxy.count()); 480 481 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 482 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 483 484 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 485 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 486 487 // get the map of issues 488 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 489 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 490 491 // get the issue out of the map 492 var first_issue_id: [evt.event_id_size]u8 = undefined; 493 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 494 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 495 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 496 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 497 498 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.description, first_issue.description); 499 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.tags, first_issue.tags); 500 } 501 502 // 503 // define more test events 504 // 505 506 const events_to_consume2 = [_]evt.EventWithId{ 507 .{ 508 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 509 .event = .{ 510 .issue = .{ 511 .title = "Kanban card status badge falls behind after drag", 512 .description = "Moving an issue between columns updates the card position immediately, but the status badge keeps showing the previous state until refresh.", 513 .tags = "bug\x00kanban\x00frontend", 514 }, 515 }, 516 }, 517 .{ 518 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 519 .event = .{ 520 .issue = .{ 521 .title = "Assignee autocomplete omits recently invited users", 522 .description = "Users invited during the current session do not appear in the assignee picker until the project page is reloaded.", 523 .tags = "bug\x00assignees\x00api", 524 }, 525 }, 526 }, 527 .{ 528 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 529 .event = .{ 530 .issue = .{ 531 .title = "Add due date warning for issues blocked by dependencies", 532 .description = "Show a warning when an issue's due date is earlier than an unresolved blocking issue so planners can adjust the schedule.", 533 .tags = "enhancement\x00planning\x00dependencies", 534 }, 535 }, 536 }, 537 }; 538 539 // 540 // insert issues as commits in the repo 541 // 542 543 { 544 var json: std.Io.Writer.Allocating = .init(allocator); 545 defer json.deinit(); 546 547 for (events_to_consume2, 0..) |event, i| { 548 json.clearRetainingCapacity(); 549 550 try std.json.Stringify.value(event, .{}, &json.writer); 551 552 // commit the event into a special branch 553 _ = try repo.commitAtRef( 554 io, 555 allocator, 556 // make the first commit a child of the first commit on the haxy/meta branch 557 .{ .parent_oids = if (i == 0) &.{std.fmt.bytesToHex(first_oid, .lower)} else null, .message = json.written() }, 558 null, 559 .{ .kind = .head, .name = "haxy/other" }, 560 ); 561 } 562 } 563 564 // 565 // check out the haxy/meta branch and merge the haxy/other branch into it 566 // 567 568 const merge_oid = blk: { 569 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 570 defer result.deinit(); 571 572 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 573 defer merge.deinit(); 574 575 try std.testing.expect(.success == merge.result); 576 577 break :blk merge.result.success.oid; 578 }; 579 580 // 581 // consume events into the database 582 // 583 584 { 585 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 586 587 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 588 589 // read the moment we just created 590 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 591 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 592 593 // get the last object id 594 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 595 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 596 _ = try last_object_id_cursor.readBytes(&last_object_id); 597 598 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 599 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 600 601 try std.testing.expectEqual(2, try haxy.count()); 602 603 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 604 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 605 606 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 607 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 608 609 // get the map of issues 610 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 611 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 612 613 // make sure one of the original issues is still there 614 { 615 // get the issue out of the map 616 var first_issue_id: [evt.event_id_size]u8 = undefined; 617 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume[0].id); 618 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 619 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 620 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 621 622 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.description, first_issue.description); 623 try std.testing.expectEqualStrings(events_to_consume[0].event.issue.?.tags, first_issue.tags); 624 } 625 626 // make sure one of the new issues is there 627 { 628 // get the issue out of the map 629 var first_issue_id: [evt.event_id_size]u8 = undefined; 630 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 631 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)) orelse return error.NotFound; 632 const first_issue_map = try Repo.DB.HashMap(.read_only).init(first_issue_cursor); 633 const first_issue = try evt.read(evt.Issue, Repo.DB, repo_opts.hash, &arena, first_issue_map); 634 635 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.description, first_issue.description); 636 try std.testing.expectEqualStrings(events_to_consume2[0].event.issue.?.tags, first_issue.tags); 637 } 638 } 639 640 // 641 // insert issues as commits in the repo 642 // 643 644 { 645 var json: std.Io.Writer.Allocating = .init(allocator); 646 defer json.deinit(); 647 648 for (events_to_consume, 0..) |event, i| { 649 json.clearRetainingCapacity(); 650 651 try std.json.Stringify.value(event, .{}, &json.writer); 652 653 // commit the event into a special branch 654 _ = try repo.commitAtRef( 655 io, 656 allocator, 657 // make the first event have no parents so we completely rebase the branch, 658 // undoing the merge that we did above 659 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 660 null, 661 .{ .kind = .head, .name = "haxy/meta" }, 662 ); 663 } 664 } 665 666 // 667 // consume events into the database 668 // 669 670 { 671 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 672 673 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 674 675 // read the moment we just created 676 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 677 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 678 679 // get the last object id 680 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 681 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 682 _ = try last_object_id_cursor.readBytes(&last_object_id); 683 684 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 685 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 686 687 try std.testing.expectEqual(1, try haxy.count()); 688 689 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 690 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 691 692 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 693 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 694 695 // get the map of issues 696 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 697 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 698 699 // make sure the new issue is not there 700 { 701 // get the issue out of the map 702 var first_issue_id: [evt.event_id_size]u8 = undefined; 703 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 704 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 705 try std.testing.expect(null == first_issue_cursor); 706 } 707 } 708 709 // 710 // define test events that modify the same issue in conflicting ways 711 // 712 713 const events_to_consume3 = [_]evt.EventWithId{ 714 .{ 715 .id = events_to_consume[0].id, 716 .event = .{ 717 .issue = .{ 718 .title = "Login form clears password on validation error", 719 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 720 .tags = "bug\x00priority-low\x00ui", 721 }, 722 }, 723 }, 724 .{ 725 .id = events_to_consume[0].id, 726 .event = .{ 727 .issue = .{ 728 .title = "Login form clears password on validation error", 729 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 730 .tags = "bug\x00priority-medium\x00ui", 731 }, 732 }, 733 }, 734 }; 735 736 // 737 // insert issues as commits in the repo on different branches 738 // 739 740 { 741 var json: std.Io.Writer.Allocating = .init(allocator); 742 defer json.deinit(); 743 744 for (events_to_consume3, 0..) |event, i| { 745 json.clearRetainingCapacity(); 746 747 try std.json.Stringify.value(event, .{}, &json.writer); 748 749 // commit the event into a special branch 750 _ = try repo.commitAtRef( 751 io, 752 allocator, 753 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 754 null, 755 .{ .kind = .head, .name = switch (i) { 756 0 => "haxy/meta", 757 1 => "haxy/other", 758 else => unreachable, 759 } }, 760 ); 761 } 762 } 763 764 // 765 // check out the haxy/meta branch and merge the haxy/other branch into it 766 // 767 768 { 769 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 770 defer result.deinit(); 771 772 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 773 defer merge.deinit(); 774 775 try std.testing.expect(.success == merge.result); 776 } 777 778 // 779 // consume events into the database 780 // 781 782 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); 783 } 784 785 test "user and repo" { 786 const io = std.testing.io; 787 const allocator = std.testing.allocator; 788 const temp_dir_name = "temp-event-user-and-repo"; 789 790 // create the temp dir 791 const cwd = std.Io.Dir.cwd(); 792 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 793 if (temp_dir_or_err) |*temp_dir| { 794 temp_dir.close(io); 795 try cwd.deleteTree(io, temp_dir_name); 796 } else |_| {} 797 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 798 defer cwd.deleteTree(io, temp_dir_name) catch {}; 799 defer temp_dir.close(io); 800 801 const cwd_path = try std.process.currentPathAlloc(io, allocator); 802 defer allocator.free(cwd_path); 803 804 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 805 defer allocator.free(work_path); 806 807 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 808 const Repo = rp.Repo(.xit, repo_opts); 809 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 810 defer repo.deinit(io, allocator); 811 812 var arena = std.heap.ArenaAllocator.init(allocator); 813 defer arena.deinit(); 814 815 // 816 // define test events 817 // 818 819 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 820 const user_event_id = evt.EventWithId.randomId(prng.random()); 821 const repo_event_id = evt.EventWithId.randomId(prng.random()); 822 823 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 824 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 825 826 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 827 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 828 829 const events_to_consume = [_]evt.EventWithId{ 830 .{ 831 .id = std.fmt.bytesToHex(user_event_id, .lower), 832 .event = .{ 833 .user = .{ 834 .name = "alice", 835 .display_name = "Alice Example", 836 .email = "alice@example.test", 837 .password_hash = first_password_hash, 838 }, 839 }, 840 }, 841 // this event edits the previous one because it has the same id 842 .{ 843 .id = std.fmt.bytesToHex(user_event_id, .lower), 844 .event = .{ 845 .user = .{ 846 .name = "alice", 847 .display_name = "Alice Example", 848 .email = "alice@example.test", 849 .password_hash = second_password_hash, 850 }, 851 }, 852 }, 853 .{ 854 .id = std.fmt.bytesToHex(repo_event_id, .lower), 855 .event = .{ 856 .repo = .{ 857 .user_id = &user_event_id, 858 .name = "ziglings", 859 .description = "Learn the Zig programming language by fixing tiny broken programs", 860 .enable_issue = true, 861 }, 862 }, 863 }, 864 }; 865 866 // 867 // insert users and repos as commits in the repo 868 // 869 870 { 871 var json: std.Io.Writer.Allocating = .init(allocator); 872 defer json.deinit(); 873 874 for (events_to_consume) |event| { 875 json.clearRetainingCapacity(); 876 877 try std.json.Stringify.value(event, .{}, &json.writer); 878 879 // commit the event into a special branch 880 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 881 } 882 } 883 884 // 885 // consume events into the database 886 // 887 888 { 889 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 890 891 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 892 893 // read the moment we just created 894 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 895 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 896 897 // get the last object id 898 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 899 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 900 _ = try last_object_id_cursor.readBytes(&last_object_id); 901 902 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 903 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 904 905 try std.testing.expectEqual(1, try haxy.count()); 906 907 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 908 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 909 910 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 911 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 912 913 // get the map of users 914 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 915 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 916 917 // get the user out of the map that was edited 918 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 919 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 920 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 921 922 // the password was correctly edited 923 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 924 925 // get the map of repos 926 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 927 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 928 929 // get the repo out of the map 930 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 931 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 932 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 933 934 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 935 try std.testing.expectEqualStrings("ziglings", repo_event.name); 936 937 // get the repos created by the user 938 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 939 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 940 941 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 942 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 943 944 try std.testing.expectEqual(1, try user_repos.count()); 945 946 try std.testing.expect(null != try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 947 } 948 949 // 950 // remove the repo 951 // 952 953 const events_to_consume2 = [_]evt.EventWithId{ 954 .{ 955 .id = std.fmt.bytesToHex(repo_event_id, .lower), 956 .event = .{ .repo = null }, 957 }, 958 }; 959 960 { 961 var json: std.Io.Writer.Allocating = .init(allocator); 962 defer json.deinit(); 963 964 for (events_to_consume2) |event| { 965 json.clearRetainingCapacity(); 966 967 try std.json.Stringify.value(event, .{}, &json.writer); 968 969 // commit the event into a special branch 970 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 971 } 972 } 973 974 // 975 // consume the removal into the database 976 // 977 978 { 979 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 980 981 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 982 983 // read the moment we just created 984 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 985 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 986 987 // get the last object id 988 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 989 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 990 _ = try last_object_id_cursor.readBytes(&last_object_id); 991 992 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 993 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 994 995 try std.testing.expectEqual(2, try haxy.count()); 996 997 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 998 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 999 1000 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 1001 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 1002 1003 // get the map of repos 1004 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1005 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1006 1007 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1008 1009 // get the repos created by the user 1010 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 1011 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 1012 1013 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1014 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 1015 1016 try std.testing.expectEqual(0, try user_repos.count()); 1017 try std.testing.expect(null == try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 1018 } 1019 1020 // 1021 // remove the user 1022 // 1023 1024 const events_to_consume3 = [_]evt.EventWithId{ 1025 .{ 1026 .id = std.fmt.bytesToHex(user_event_id, .lower), 1027 .event = .{ .user = null }, 1028 }, 1029 }; 1030 1031 { 1032 var json: std.Io.Writer.Allocating = .init(allocator); 1033 defer json.deinit(); 1034 1035 for (events_to_consume3) |event| { 1036 json.clearRetainingCapacity(); 1037 1038 try std.json.Stringify.value(event, .{}, &json.writer); 1039 1040 // commit the event into a special branch 1041 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, .{ .kind = .head, .name = "haxy/meta" }); 1042 } 1043 } 1044 1045 // 1046 // consume the user removal into the database 1047 // 1048 1049 { 1050 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 1051 1052 const history = try Repo.DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 1053 1054 // read the moment we just created 1055 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 1056 const moment = try Repo.DB.HashMap(.read_only).init(moment_cursor); 1057 1058 // get the last object id 1059 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 1060 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1061 _ = try last_object_id_cursor.readBytes(&last_object_id); 1062 1063 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 1064 const haxy = try Repo.DB.ArrayList(.read_only).init(haxy_cursor); 1065 1066 try std.testing.expectEqual(3, try haxy.count()); 1067 1068 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 1069 const haxy_moments = try Repo.DB.HashMap(.read_only).init(haxy_moments_cursor); 1070 1071 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 1072 const haxy_moment = try Repo.DB.HashMap(.read_only).init(haxy_moment_cursor); 1073 1074 // get the map of users 1075 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1076 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1077 1078 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 1079 } 1080 } 1081