╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
656 } 657 658 test "user and repo" { 659 const io = std.testing.io; 660 const allocator = std.testing.allocator; 661 const temp_dir_name = "temp-event-user-and-repo"; 662 663 // create the temp dir 664 const cwd = std.Io.Dir.cwd(); 665 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 666 if (temp_dir_or_err) |*temp_dir| { 667 temp_dir.close(io); 668 try cwd.deleteTree(io, temp_dir_name); 669 } else |_| {} 670 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 671 defer cwd.deleteTree(io, temp_dir_name) catch {}; 672 defer temp_dir.close(io); 673 674 const cwd_path = try std.process.currentPathAlloc(io, allocator); 675 defer allocator.free(cwd_path); 676 677 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 678 defer allocator.free(work_path); 679 680 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 681 const Repo = rp.Repo(.xit, repo_opts); 682 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 683 defer repo.deinit(io, allocator); 684 685 var arena = std.heap.ArenaAllocator.init(allocator); 686 defer arena.deinit(); 687 688 // 689 // define test events 690 // 691 692 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 693 const user_event_id = evt.EventWithId.randomId(prng.random()); 694 const repo_event_id = evt.EventWithId.randomId(prng.random()); 695 696 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 697 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 698 699 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 700 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 701 702 const events_to_consume = [_]evt.EventWithId{ 703 .{ 704 .id = std.fmt.bytesToHex(user_event_id, .lower), 705 .event = .{ 706 .user = .{ 707 .name = "alice", 708 .display_name = "Alice Example", 709 .email = "alice@example.test", 710 .password_hash = first_password_hash, 711 }, 712 }, 713 }, 714 // this event edits the previous one because it has the same id 715 .{ 716 .id = std.fmt.bytesToHex(user_event_id, .lower), 717 .event = .{ 718 .user = .{ 719 .name = "alice", 720 .display_name = "Alice Example", 721 .email = "alice@example.test", 722 .password_hash = second_password_hash, 723 }, 724 }, 725 }, 726 .{ 727 .id = std.fmt.bytesToHex(repo_event_id, .lower), 728 .event = .{ 729 .repo = .{ 730 .user_id = &user_event_id, 731 .name = "ziglings", 732 .description = "Learn the Zig programming language by fixing tiny broken programs", 733 }, 734 }, 735 }, 736 }; 737 738 // commit and consume the seed events 739 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 740 741 { 742 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 743 744 // get the map of users 745 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 746 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 747 748 // get the user out of the map that was edited 749 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 750 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 751 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 752 753 // the password was correctly edited 754 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 755 756 // get the map of repos 757 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 758 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 759 760 // get the repo out of the map 761 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 762 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 763 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 764 765 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 766 try std.testing.expectEqualStrings("ziglings", repo_event.name); 767 768 // get the repos created by the user 769 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 770 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 771 772 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 773 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 774 775 try std.testing.expectEqual(1, try user_repos.count()); 776 777 // the set is keyed by orderKey([created-ts][event-id]) 778 const order_key = evt.orderKey(repo_event.created_ts, &repo_event_id); 779 try std.testing.expect(try user_repos.contains(&order_key)); 780 } 781 782 // 783 // remove the repo 784 // 785 786 const events_to_consume2 = [_]evt.EventWithId{ 787 .{ 788 .id = std.fmt.bytesToHex(repo_event_id, .lower), 789 .event = .{ .repo = null }, 790 }, 791 }; 792 793 // commit and consume the removal 794 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 795 796 { 797 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 798 799 // get the map of repos 800 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 801 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 802 803 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 804 805 // get the repos created by the user 806 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 807 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 808 809 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 810 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 811 812 // removing the repo emptied the user's set 813 try std.testing.expectEqual(0, try user_repos.count()); 814 } 815 816 // 817 // remove the user 818 // 819 820 const events_to_consume3 = [_]evt.EventWithId{ 821 .{ 822 .id = std.fmt.bytesToHex(user_event_id, .lower), 823 .event = .{ .user = null }, 824 }, 825 }; 826 827 // commit and consume the removal 828 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 829 830 { 831 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 832 833 // get the map of users 834 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 835 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 836 837 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 838 } 839 } 840 841 test "repos and users paginate in creation order" { 842 const io = std.testing.io; 843 const allocator = std.testing.allocator; 844 const temp_dir_name = "temp-event-order"; 845 846 const cwd = std.Io.Dir.cwd(); 847 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 848 if (temp_dir_or_err) |*temp_dir| { 849 temp_dir.close(io); 850 try cwd.deleteTree(io, temp_dir_name); 851 } else |_| {} 852 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 853 defer cwd.deleteTree(io, temp_dir_name) catch {}; 854 defer temp_dir.close(io); 855 856 const cwd_path = try std.process.currentPathAlloc(io, allocator); 857 defer allocator.free(cwd_path); 858 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 859 defer allocator.free(work_path); 860 861 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 862 const Repo = rp.Repo(.xit, repo_opts); 863 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 864 defer repo.deinit(io, allocator); 865 866 var arena = std.heap.ArenaAllocator.init(allocator); 867 defer arena.deinit(); 868 869 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 870 const user_id = evt.EventWithId.randomId(prng.random()); 871 var repo_ids: [4][evt.event_id_size]u8 = undefined; 872 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 873 874 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 875 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 876 877 // asserts the repo-id-set holds exactly `expected` ids in that order 878 const Check = struct { 879 fn order( 880 comptime DB: type, 881 comptime hash_kind: hash.HashKind, 882 moment: DB.HashMap(.read_only), 883 expected: []const *const [evt.event_id_size]u8, 884 ) !void { 885 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 886 const set = try DB.SortedSet(.read_only).init(cursor); 887 try std.testing.expectEqual(expected.len, try set.count()); 888 for (expected, 0..) |id, i| { 889 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 890 // the key is orderKey ([timestamp][event-id]); its trailing bytes are the id 891 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 892 _ = try kv.key_cursor.readBytes(&order_key); 893 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 894 } 895 } 896 }; 897 898 // one user, then four repos, each with its own creation timestamp (user@100, 899 // repos@101..104) 900 const events = [_]evt.EventWithId{ 901 .{ .id = std.fmt.bytesToHex(user_id, .lower), .timestamp = 100, .event = .{ .user = .{ .name = "alice", .display_name = "Alice", .email = "alice@example.test", .password_hash = pw } } }, 902 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 903 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 904 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 905 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 906 }; 907 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 908 909 { 910 const moment = try evt.currentMoment(repo_opts, &repo); 911 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[1], &repo_ids[2], &repo_ids[3] }); 912 913 // the single user shows up in its own ordered set 914 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 915 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 916 try std.testing.expectEqual(1, try uset.count()); 917 } 918 919 // delete repo1 -> dense order (no tombstone) 920 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 921 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 200, .event = .{ .repo = null } }, 922 }); 923 { 924 const moment = try evt.currentMoment(repo_opts, &repo); 925 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 926 } 927 928 // update repo0 at a later timestamp -> keeps its original slot 929 try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 930 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 931 }); 932 { 933 const moment = try evt.currentMoment(repo_opts, &repo); 934 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 935 936 // the value really was updated 937 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 938 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 939 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 940 const rm = try Repo.DB.HashMap(.read_only).init(rc); 941 const re = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, rm); 942 try std.testing.expectEqualStrings("updated", re.description); 943 } 944 } 945