╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
531 // consume events into the database 532 // 533 534 { 535 try evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }); 536 537 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 538 539 // get the map of issues 540 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 541 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 542 543 // make sure the new issue is not there 544 { 545 // get the issue out of the map 546 var first_issue_id: [evt.event_id_size]u8 = undefined; 547 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 548 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 549 try std.testing.expect(null == first_issue_cursor); 550 } 551 } 552 553 // 554 // define test events that modify the same issue in conflicting ways 555 // 556 557 const events_to_consume3 = [_]evt.EventWithId{ 558 .{ 559 .id = events_to_consume[0].id, 560 .event = .{ 561 .issue = .{ 562 .title = "Login form clears password on validation error", 563 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 564 .tags = "bug\x00priority-low\x00ui", 565 }, 566 }, 567 }, 568 .{ 569 .id = events_to_consume[0].id, 570 .event = .{ 571 .issue = .{ 572 .title = "Login form clears password on validation error", 573 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 574 .tags = "bug\x00priority-medium\x00ui", 575 }, 576 }, 577 }, 578 }; 579 580 // 581 // insert issues as commits in the repo on different branches 582 // 583 584 { 585 var json: std.Io.Writer.Allocating = .init(allocator); 586 defer json.deinit(); 587 588 for (events_to_consume3, 0..) |event, i| { 589 json.clearRetainingCapacity(); 590 591 try std.json.Stringify.value(event, .{}, &json.writer); 592 593 // commit the event into a special branch 594 _ = try repo.commitAtRef( 595 io, 596 allocator, 597 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 598 null, 599 .{ .kind = .head, .name = switch (i) { 600 0 => "haxy/meta", 601 1 => "haxy/other", 602 else => unreachable, 603 } }, 604 ); 605 } 606 } 607 608 // 609 // check out the haxy/meta branch and merge the haxy/other branch into it 610 // 611 612 { 613 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); 614 defer result.deinit(); 615 616 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null); 617 defer merge.deinit(); 618 619 try std.testing.expect(.success == merge.result); 620 } 621 622 // 623 // consume events into the database 624 // 625 626 try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); 627 } 628 629 test "user and repo" { 630 const io = std.testing.io; 631 const allocator = std.testing.allocator; 632 const temp_dir_name = "temp-event-user-and-repo"; 633 634 // create the temp dir 635 const cwd = std.Io.Dir.cwd(); 636 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 637 if (temp_dir_or_err) |*temp_dir| { 638 temp_dir.close(io); 639 try cwd.deleteTree(io, temp_dir_name); 640 } else |_| {} 641 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 642 defer cwd.deleteTree(io, temp_dir_name) catch {}; 643 defer temp_dir.close(io); 644 645 const cwd_path = try std.process.currentPathAlloc(io, allocator); 646 defer allocator.free(cwd_path); 647 648 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 649 defer allocator.free(work_path); 650 651 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 652 const Repo = rp.Repo(.xit, repo_opts); 653 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 654 defer repo.deinit(io, allocator); 655 656 var arena = std.heap.ArenaAllocator.init(allocator); 657 defer arena.deinit(); 658 659 // 660 // define test events 661 // 662 663 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 664 const user_event_id = evt.EventWithId.randomId(prng.random()); 665 const repo_event_id = evt.EventWithId.randomId(prng.random()); 666 667 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 668 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 669 670 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 671 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 672 673 const events_to_consume = [_]evt.EventWithId{ 674 .{ 675 .id = std.fmt.bytesToHex(user_event_id, .lower), 676 .event = .{ 677 .user = .{ 678 .name = "alice", 679 .display_name = "Alice Example", 680 .email = "alice@example.test", 681 .password_hash = first_password_hash, 682 }, 683 }, 684 }, 685 // this event edits the previous one because it has the same id 686 .{ 687 .id = std.fmt.bytesToHex(user_event_id, .lower), 688 .event = .{ 689 .user = .{ 690 .name = "alice", 691 .display_name = "Alice Example", 692 .email = "alice@example.test", 693 .password_hash = second_password_hash, 694 }, 695 }, 696 }, 697 .{ 698 .id = std.fmt.bytesToHex(repo_event_id, .lower), 699 .event = .{ 700 .repo = .{ 701 .user_id = &user_event_id, 702 .name = "ziglings", 703 .description = "Learn the Zig programming language by fixing tiny broken programs", 704 .enable_issue = true, 705 }, 706 }, 707 }, 708 }; 709 710 // commit and consume the seed events 711 try evt.commitAndConsume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }, &events_to_consume); 712 713 { 714 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 715 716 // get the map of users 717 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 718 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 719 720 // get the user out of the map that was edited 721 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 722 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 723 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 724 725 // the password was correctly edited 726 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 727 728 // get the map of repos 729 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 730 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 731 732 // get the repo out of the map 733 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 734 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 735 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 736 737 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 738 try std.testing.expectEqualStrings("ziglings", repo_event.name); 739 740 // get the repos created by the user 741 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 742 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 743 744 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 745 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 746 747 try std.testing.expectEqual(1, try user_repos.count()); 748 749 try std.testing.expect(null != try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 750 } 751 752 // 753 // remove the repo 754 // 755 756 const events_to_consume2 = [_]evt.EventWithId{ 757 .{ 758 .id = std.fmt.bytesToHex(repo_event_id, .lower), 759 .event = .{ .repo = null }, 760 }, 761 }; 762 763 // commit and consume the removal 764 try evt.commitAndConsume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }, &events_to_consume2); 765 766 { 767 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 768 769 // get the map of repos 770 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 771 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 772 773 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 774 775 // get the repos created by the user 776 const user_id_to_repos_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repos")) orelse return error.NotFound; 777 const user_id_to_repos = try Repo.DB.HashMap(.read_only).init(user_id_to_repos_cursor); 778 779 const user_repos_cursor = try user_id_to_repos.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 780 const user_repos = try Repo.DB.CountedHashSet(.read_only).init(user_repos_cursor); 781 782 try std.testing.expectEqual(0, try user_repos.count()); 783 try std.testing.expect(null == try user_repos.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 784 } 785 786 // 787 // remove the user 788 // 789 790 const events_to_consume3 = [_]evt.EventWithId{ 791 .{ 792 .id = std.fmt.bytesToHex(user_event_id, .lower), 793 .event = .{ .user = null }, 794 }, 795 }; 796 797 // commit and consume the removal 798 try evt.commitAndConsume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" }, &events_to_consume3); 799 800 { 801 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 802 803 // get the map of users 804 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 805 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 806 807 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 808 } 809 } 810