From 14cea4ff19147455db70e69ff306dcb5b2c6ba9f Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 10 Jun 2024 16:01:17 +0200 Subject: [PATCH 01/45] wip: issue 2283 txtar --- .../cmd/gnoland/testdata/issue_2283.txtar | 111 ++++++++++++++++++ .../testdata/issue_2283_cacheTypes.txtar | 103 ++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 gno.land/cmd/gnoland/testdata/issue_2283.txtar create mode 100644 gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar diff --git a/gno.land/cmd/gnoland/testdata/issue_2283.txtar b/gno.land/cmd/gnoland/testdata/issue_2283.txtar new file mode 100644 index 00000000000..653a4dd79b0 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/issue_2283.txtar @@ -0,0 +1,111 @@ +# Test for https://github.com/gnolang/gno/issues/2283 + +# These are not necessary, but they "alleviate" add_feeds.tx from the +# responsibility of loading standard libraries, thus not making it exceed +# the --gas-wanted. +loadpkg gno.land/r/demo/users +loadpkg gno.land/r/demo/boards +loadpkg gno.land/r/demo/imports $WORK/imports + +gnoland start + +! gnokey broadcast $WORK/add_feeds.tx + +gnokey maketx addpkg -pkgdir $WORK/bye -pkgpath gno.land/r/demo/bye -gas-fee 1000000ugnot -gas-wanted 10_000_000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +-- imports/imports.gno -- +// Handles "implicit" imports before running the main failing transaction sequence. +package imports + +import ( + _ "encoding/binary" +) + +-- add_feeds.tx -- +{ + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "Name": "social_feeds", + "Path": "gno.land/r/demo/teritori/social_feeds", + "Files": [ + { + "Name": "binutils_extra.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/binary\"\n)\n\nfunc EncodeLengthPrefixedStringUint32BE(s string) []byte {\n\tb := make([]byte, 4+len(s))\n\tbinary.BigEndian.PutUint32(b, uint32(len(s)))\n\tcopy(b[4:], s)\n\treturn b\n}\n" + }, + { + "Name": "feed.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/teritori/flags_index\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype FeedID uint64\n\nfunc (fid FeedID) String() string {\n\treturn strconv.Itoa(int(fid))\n}\n\nfunc (fid *FeedID) FromJSON(ast *ujson.JSONASTNode) {\n\tval, err := strconv.Atoi(ast.Value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t*fid = FeedID(val)\n}\n\nfunc (fid FeedID) ToJSON() string {\n\treturn strconv.Itoa(int(fid))\n}\n\ntype Feed struct {\n\tid FeedID\n\turl string\n\tname string\n\tcreator std.Address\n\towner std.Address\n\tposts avl.Tree // pidkey -> *Post\n\tcreatedAt int64\n\n\tflags *flags_index.FlagsIndex\n\thiddenPostsByUser avl.Tree // std.Address => *avl.Tree (postID => bool)\n\n\tpostsCtr uint64\n}\n\nfunc newFeed(fid FeedID, url string, name string, creator std.Address) *Feed {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid feed name: \" + name)\n\t}\n\n\tif gFeedsByName.Has(name) {\n\t\tpanic(\"feed already exists: \" + name)\n\t}\n\n\treturn &Feed{\n\t\tid: fid,\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\towner: creator,\n\t\tposts: avl.Tree{},\n\t\tcreatedAt: time.Now().Unix(),\n\t\tflags: flags_index.NewFlagsIndex(),\n\t\tpostsCtr: 0,\n\t}\n}\n\nfunc (feed *Feed) incGetPostID() PostID {\n\tfeed.postsCtr++\n\treturn PostID(feed.postsCtr)\n}\n\nfunc (feed *Feed) GetPost(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpost_, exists := feed.posts.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn post_.(*Post)\n}\n\nfunc (feed *Feed) MustGetPost(pid PostID) *Post {\n\tpost := feed.GetPost(pid)\n\tif post == nil {\n\t\tpanic(\"post does not exist\")\n\t}\n\treturn post\n}\n\nfunc (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, metadata string) *Post {\n\tpid := feed.incGetPostID()\n\tpidkey := postIDKey(pid)\n\n\tpost := newPost(feed, pid, creator, parentID, category, metadata)\n\tfeed.posts.Set(pidkey, post)\n\n\t// If post is a comment then increase the comment count on parent\n\tif uint64(parentID) != 0 {\n\t\tparent := feed.MustGetPost(parentID)\n\t\tparent.commentsCount += 1\n\t}\n\n\treturn post\n}\n\nfunc (feed *Feed) FlagPost(flagBy std.Address, pid PostID) {\n\tflagID := getFlagID(feed.id, pid)\n\n\tif feed.flags.HasFlagged(flagID, flagBy.String()) {\n\t\tpanic(\"already flagged\")\n\t}\n\n\tfeed.flags.Flag(flagID, flagBy.String())\n}\n\nfunc (feed *Feed) BanPost(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := feed.posts.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (feed *Feed) HidePostForUser(caller std.Address, pid PostID) {\n\tuserAddr := caller.String()\n\n\tvalue, exists := feed.hiddenPostsByUser.Get(userAddr)\n\tvar hiddenPosts *avl.Tree\n\tif exists {\n\t\thiddenPosts = value.(*avl.Tree)\n\t} else {\n\t\thiddenPosts = avl.NewTree()\n\t\tfeed.hiddenPostsByUser.Set(userAddr, hiddenPosts)\n\t}\n\n\tif hiddenPosts.Has(pid.String()) {\n\t\tpanic(\"PostID is already hidden: \" + pid.String())\n\t}\n\n\thiddenPosts.Set(pid.String(), true)\n}\n\nfunc (feed *Feed) UnHidePostForUser(userAddress std.Address, pid PostID) {\n\tvalue, exists := feed.hiddenPostsByUser.Get(userAddress.String())\n\tvar hiddenPosts *avl.Tree\n\tif exists {\n\t\thiddenPosts = value.(*avl.Tree)\n\t\t_, removed := hiddenPosts.Remove(pid.String())\n\t\tif !removed {\n\t\t\tpanic(\"Post is not hidden: \" + pid.String())\n\t\t}\n\t} else {\n\t\tpanic(\"User has not hidden post: \" + pid.String())\n\t}\n}\n\nfunc (feed *Feed) Render() string {\n\tpkgpath := std.CurrentRealmPath()\n\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"Feed: %s (ID: %s) - Owner: %s\", feed.name, feed.id, feed.owner)\n\tstr += \"\\n\\n There are \" + intToString(feed.posts.Size()) + \" post(s) \\n\\n\"\n\n\tif feed.posts.Size() > 0 {\n\t\tfeed.posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"\\n\"\n\t\t\t}\n\n\t\t\tpost := value.(*Post)\n\t\t\tpostUrl := strings.Replace(pkgpath, \"gno.land\", \"\", -1) + \":\" + feed.name + \"/\" + post.id.String()\n\n\t\t\tstr += \" * [\" +\n\t\t\t\t\"PostID: \" + post.id.String() +\n\t\t\t\t\" - \" + intToString(post.reactions.Size()) + \" reactions \" +\n\t\t\t\t\" - \" + ufmt.Sprintf(\"%d\", post.tipAmount) + \" tip amount\" +\n\t\t\t\t\"]\" +\n\t\t\t\t\"(\" + postUrl + \")\" +\n\t\t\t\t\"\\n\"\n\t\t\treturn false\n\t\t})\n\n\t\tstr += \"-------------------------\\n\"\n\t\tstr += feed.flags.Dump()\n\t}\n\n\tstr += \"---------------------------------------\\n\"\n\tif feed.hiddenPostsByUser.Size() > 0 {\n\t\tstr += \"Hidden posts by users:\\n\\n\"\n\n\t\tfeed.hiddenPostsByUser.Iterate(\"\", \"\", func(userAddr string, value interface{}) bool {\n\t\t\thiddenPosts := value.(*avl.Tree)\n\t\t\tstr += \"\\nUser address: \" + userAddr + \"\\n\"\n\n\t\t\thiddenPosts.Iterate(\"\", \"\", func(pid string, value interface{}) bool {\n\t\t\t\tstr += \"- PostID: \" + pid + \"\\n\"\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\treturn false\n\t\t})\n\t}\n\n\treturn str\n}\n\nfunc (feed *Feed) ToJSON() string {\n\tposts := []ujson.FormatKV{}\n\tfeed.posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tposts = append(posts, ujson.FormatKV{\n\t\t\tKey: key,\n\t\t\tValue: value.(*Post),\n\t\t})\n\t\treturn false\n\t})\n\tfeedJSON := ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"id\", Value: uint64(feed.id)},\n\t\t{Key: \"url\", Value: feed.url},\n\t\t{Key: \"name\", Value: feed.name},\n\t\t{Key: \"creator\", Value: feed.creator},\n\t\t{Key: \"owner\", Value: feed.owner},\n\t\t{Key: \"posts\", Value: ujson.FormatObject(posts), Raw: true},\n\t\t{Key: \"createdAt\", Value: feed.createdAt},\n\t\t{Key: \"postsCtr\", Value: feed.postsCtr},\n\t\t// TODO: convert flags, hiddenPostsByUser\n\t\t// {Key: \"flags\", Value: feed.flags},\n\t\t// {Key: \"hiddenPostsByUser\", Value: feed.hiddenPostsByUser},\n\t})\n\treturn feedJSON\n}\n\nfunc (feed *Feed) FromJSON(jsonData string) {\n\tast := ujson.TokenizeAndParse(jsonData)\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"id\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tfid, _ := strconv.Atoi(node.Value)\n\t\t\tfeed.id = FeedID(fid)\n\t\t}},\n\t\t{Key: \"url\", Value: &feed.url},\n\t\t{Key: \"name\", Value: &feed.name},\n\t\t{Key: \"creator\", Value: &feed.creator},\n\t\t{Key: \"owner\", Value: &feed.owner},\n\t\t{Key: \"posts\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tposts := avl.NewTree()\n\t\t\tfor _, child := range node.ObjectChildren {\n\t\t\t\tpostNode := child.Value\n\n\t\t\t\tpost := Post{}\n\t\t\t\tpost.FromJSON(postNode.String())\n\t\t\t\tposts.Set(child.Key, &post)\n\t\t\t}\n\t\t\tfeed.posts = *posts\n\t\t}},\n\t\t{Key: \"createdAt\", Value: &feed.createdAt},\n\t\t{Key: \"postsCtr\", Value: &feed.postsCtr},\n\t})\n}\n" + }, + { + "Name": "feeds.gno", + "Body": "package social_feeds\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgFeeds avl.Tree // id -> *Feed\n\tgFeedsCtr int // increments Feed.id\n\tgFeedsByName avl.Tree // name -> *Feed\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "Name": "feeds_test.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t// Fake previous version for testing\n\tfeedsV7 \"gno.land/r/demo/teritori/social_feeds\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\trootPostID = PostID(0)\n\tpostID1 = PostID(1)\n\tfeedID1 = FeedID(1)\n\tcat1 = uint64(1)\n\tcat2 = uint64(2)\n\tuser = testutils.TestAddress(\"user\")\n\tfilter_all = []uint64{}\n)\n\nfunc getFeed1() *Feed {\n\treturn mustGetFeed(feedID1)\n}\n\nfunc getPost1() *Post {\n\tfeed1 := getFeed1()\n\tpost1 := feed1.MustGetPost(postID1)\n\treturn post1\n}\n\nfunc testCreateFeed(t *testing.T) {\n\tfeedID := CreateFeed(\"teritori1\")\n\tfeed := mustGetFeed(feedID)\n\n\tif feedID != 1 {\n\t\tt.Fatalf(\"expected feedID: 1, got %q.\", feedID)\n\t}\n\n\tif feed.name != \"teritori1\" {\n\t\tt.Fatalf(\"expected feedName: teritori1, got %q.\", feed.name)\n\t}\n}\n\nfunc testCreatePost(t *testing.T) {\n\tmetadata := `{\"gifs\": [], \"files\": [], \"title\": \"\", \"message\": \"testouille\", \"hashtags\": [], \"mentions\": [], \"createdAt\": \"2023-03-29T12:19:04.858Z\", \"updatedAt\": \"2023-03-29T12:19:04.858Z\"}`\n\tpostID := CreatePost(feedID1, rootPostID, cat1, metadata)\n\tfeed := mustGetFeed(feedID1)\n\tpost := feed.MustGetPost(postID)\n\n\tif postID != 1 {\n\t\tt.Fatalf(\"expected postID: 1, got %q.\", postID)\n\t}\n\n\tif post.category != cat1 {\n\t\tt.Fatalf(\"expected categoryID: %q, got %q.\", cat1, post.category)\n\t}\n}\n\nfunc toPostIDsStr(posts []*Post) string {\n\tvar postIDs []string\n\tfor _, post := range posts {\n\t\tpostIDs = append(postIDs, post.id.String())\n\t}\n\n\tpostIDsStr := strings.Join(postIDs, \",\")\n\treturn postIDsStr\n}\n\nfunc testGetPosts(t *testing.T) {\n\tuser := std.Address(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tfeedID := CreateFeed(\"teritori10\")\n\tfeed := mustGetFeed(feedID)\n\n\tCreatePost(feedID, rootPostID, cat1, \"post1\")\n\tCreatePost(feedID, rootPostID, cat1, \"post2\")\n\tCreatePost(feedID, rootPostID, cat1, \"post3\")\n\tCreatePost(feedID, rootPostID, cat1, \"post4\")\n\tCreatePost(feedID, rootPostID, cat1, \"post5\")\n\tpostIDToFlagged := CreatePost(feedID, rootPostID, cat1, \"post6\")\n\tpostIDToHide := CreatePost(feedID, rootPostID, cat1, \"post7\")\n\tCreatePost(feedID, rootPostID, cat1, \"post8\")\n\n\tvar posts []*Post\n\tvar postIDsStr string\n\n\t// Query last 3 posts\n\tposts = getPosts(feed, 0, \"\", \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,7,6\" {\n\t\tt.Fatalf(\"expected posts order: 8,7,6. Got: %s\", postIDsStr)\n\t}\n\n\t// Query page 2\n\tposts = getPosts(feed, 0, \"\", \"\", []uint64{}, 3, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\tif postIDsStr != \"5,4,3\" {\n\t\tt.Fatalf(\"expected posts order: 5,4,3. Got: %s\", postIDsStr)\n\t}\n\n\t// Exclude hidden post\n\tHidePostForMe(feed.id, postIDToHide)\n\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,6,5\" {\n\t\tt.Fatalf(\"expected posts order: 8,6,5. Got: %s\", postIDsStr)\n\t}\n\n\t// Exclude flagged post\n\tFlagPost(feed.id, postIDToFlagged)\n\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,5,4\" {\n\t\tt.Fatalf(\"expected posts order: 8,5,4. Got: %s\", postIDsStr)\n\t}\n\n\t// Pagination with hidden/flagged posts\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 3, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"3,2,1\" {\n\t\tt.Fatalf(\"expected posts order: 3,2,1. Got: %s\", postIDsStr)\n\t}\n\n\t// Query out of range\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 6, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"\" {\n\t\tt.Fatalf(\"expected posts order: ''. Got: %s\", postIDsStr)\n\t}\n}\n\nfunc testReactPost(t *testing.T) {\n\tfeed := getFeed1()\n\tpost := getPost1()\n\n\ticon := \"🥰\"\n\tReactPost(feed.id, post.id, icon, true)\n\n\t// Set reaction\n\treactionCount_, ok := post.reactions.Get(\"🥰\")\n\tif !ok {\n\t\tt.Fatalf(\"expected 🥰 exists\")\n\t}\n\n\treactionCount := reactionCount_.(int)\n\tif reactionCount != 1 {\n\t\tt.Fatalf(\"expected reactionCount: 1, got %q.\", reactionCount)\n\t}\n\n\t// Unset reaction\n\tReactPost(feed.id, post.id, icon, false)\n\t_, exist := post.reactions.Get(\"🥰\")\n\tif exist {\n\t\tt.Fatalf(\"expected 🥰 not exist\")\n\t}\n}\n\nfunc testCreateAndDeleteComment(t *testing.T) {\n\tfeed1 := getFeed1()\n\tpost1 := getPost1()\n\n\tmetadata := `empty_meta_data`\n\n\tcommentID1 := CreatePost(feed1.id, post1.id, cat1, metadata)\n\tcommentID2 := CreatePost(feed1.id, post1.id, cat1, metadata)\n\tcomment2 := feed1.MustGetPost(commentID2)\n\n\tif comment2.id != 3 { // 1 post + 2 comments = 3\n\t\tt.Fatalf(\"expected comment postID: 3, got %q.\", comment2.id)\n\t}\n\n\tif comment2.parentID != post1.id {\n\t\tt.Fatalf(\"expected comment parentID: %q, got %q.\", post1.id, comment2.parentID)\n\t}\n\n\t// Check comment count on parent\n\tif post1.commentsCount != 2 {\n\t\tt.Fatalf(\"expected comments count: 2, got %d.\", post1.commentsCount)\n\t}\n\n\t// Get comments\n\tcomments := GetComments(feed1.id, post1.id, 0, 10)\n\tcommentsParsed := ujson.ParseSlice(comments)\n\n\tif len(commentsParsed) != 2 {\n\t\tt.Fatalf(\"expected encoded comments: 2, got %q.\", commentsParsed)\n\t}\n\n\t// Delete 1 comment\n\tDeletePost(feed1.id, comment2.id)\n\tcomments = GetComments(feed1.id, post1.id, 0, 10)\n\tcommentsParsed = ujson.ParseSlice(comments)\n\n\tif len(commentsParsed) != 1 {\n\t\tt.Fatalf(\"expected encoded comments: 1, got %q.\", commentsParsed)\n\t}\n\n\t// Check comment count on parent\n\tif post1.commentsCount != 1 {\n\t\tt.Fatalf(\"expected comments count: 1, got %d.\", post1.commentsCount)\n\t}\n}\n\nfunc countPosts(feedID FeedID, categories []uint64, limit uint8) int {\n\toffset := uint64(0)\n\n\tpostsStr := GetPosts(feedID, 0, \"\", categories, offset, limit)\n\tif postsStr == \"[]\" {\n\t\treturn 0\n\t}\n\n\tparsedPosts := ujson.ParseSlice(postsStr)\n\tpostsCount := len(parsedPosts)\n\treturn postsCount\n}\n\nfunc countPostsByUser(feedID FeedID, user string) int {\n\toffset := uint64(0)\n\tlimit := uint8(10)\n\n\tpostsStr := GetPosts(feedID, 0, user, []uint64{}, offset, limit)\n\tif postsStr == \"[]\" {\n\t\treturn 0\n\t}\n\n\tparsedPosts := ujson.ParseSlice(postsStr)\n\tpostsCount := len(parsedPosts)\n\treturn postsCount\n}\n\nfunc testFilterByCategories(t *testing.T) {\n\t// // Re-add reaction to test post list\n\t// ReactPost(1, postID, \"🥰\", true)\n\t// ReactPost(1, postID, \"😇\", true)\n\n\tfilter_cat1 := []uint64{1}\n\tfilter_cat1_2 := []uint64{1, 2}\n\tfilter_cat9 := []uint64{9}\n\tfilter_cat1_2_9 := []uint64{1, 2, 9}\n\n\tfeedID2 := CreateFeed(\"teritori2\")\n\tfeed2 := mustGetFeed(feedID2)\n\n\t// Create 2 posts on root with cat1\n\tpostID1 := CreatePost(feed2.id, rootPostID, cat1, \"metadata\")\n\tpostID2 := CreatePost(feed2.id, rootPostID, cat1, \"metadata\")\n\n\t// Create 1 posts on root with cat2\n\tpostID3 := CreatePost(feed2.id, rootPostID, cat2, \"metadata\")\n\n\t// Create comments on post 1\n\tcommentPostID1 := CreatePost(feed2.id, postID1, cat1, \"metadata\")\n\n\t// cat1: Should return max = limit\n\tif count := countPosts(feed2.id, filter_cat1, 1); count != 1 {\n\t\tt.Fatalf(\"expected posts count: 1, got %q.\", count)\n\t}\n\n\t// cat1: Should return max = total\n\tif count := countPosts(feed2.id, filter_cat1, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// cat 1 + 2: Should return max = limit\n\tif count := countPosts(feed2.id, filter_cat1_2, 2); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// cat 1 + 2: Should return max = total on both\n\tif count := countPosts(feed2.id, filter_cat1_2, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// cat 1, 2, 9: Should return total of 1, 2\n\tif count := countPosts(feed2.id, filter_cat1_2_9, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// cat 9: Should return 0\n\tif count := countPosts(feed2.id, filter_cat9, 10); count != 0 {\n\t\tt.Fatalf(\"expected posts count: 0, got %q.\", count)\n\t}\n\n\t// cat all: should return all\n\tif count := countPosts(feed2.id, filter_all, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// add comments should not impact the results\n\tCreatePost(feed2.id, postID1, cat1, \"metadata\")\n\tCreatePost(feed2.id, postID2, cat1, \"metadata\")\n\n\tif count := countPosts(feed2.id, filter_all, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// delete a post should affect the result\n\tDeletePost(feed2.id, postID1)\n\n\tif count := countPosts(feed2.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n}\n\nfunc testTipPost(t *testing.T) {\n\tcreator := testutils.TestAddress(\"creator\")\n\tstd.TestIssueCoins(creator, std.Coins{{\"ugnot\", 100_000_000}})\n\n\t// NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1))\n\ttipper := testutils.TestAddress(\"tipper\")\n\tstd.TestIssueCoins(tipper, std.Coins{{\"ugnot\", 50_000_000}})\n\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\n\t// Check Original coins of creator/tipper\n\tif coins := banker.GetCoins(creator); coins[0].Amount != 100_000_000 {\n\t\tt.Fatalf(\"expected creator coin count: 100_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\tif coins := banker.GetCoins(tipper); coins[0].Amount != 50_000_000 {\n\t\tt.Fatalf(\"expected tipper coin count: 50_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\t// Creator creates feed, post\n\tstd.TestSetOrigCaller(creator)\n\n\tfeedID3 := CreateFeed(\"teritori3\")\n\tfeed3 := mustGetFeed(feedID3)\n\n\tpostID1 := CreatePost(feed3.id, rootPostID, cat1, \"metadata\")\n\tpost1 := feed3.MustGetPost(postID1)\n\n\t// Tiper tips the ppst\n\tstd.TestSetOrigCaller(tipper)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 1_000_000}}, nil)\n\tTipPost(feed3.id, post1.id)\n\n\t// Coin must be increased for creator\n\tif coins := banker.GetCoins(creator); coins[0].Amount != 101_000_000 {\n\t\tt.Fatalf(\"expected creator coin after beging tipped: 101_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\t// Total tip amount should increased\n\tif post1.tipAmount != 1_000_000 {\n\t\tt.Fatalf(\"expected total tipAmount: 1_000_000, got %d.\", post1.tipAmount)\n\t}\n\n\t// Add more tip should update this total\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 2_000_000}}, nil)\n\tTipPost(feed3.id, post1.id)\n\n\tif post1.tipAmount != 3_000_000 {\n\t\tt.Fatalf(\"expected total tipAmount: 3_000_000, got %d.\", post1.tipAmount)\n\t}\n}\n\nfunc testFlagPost(t *testing.T) {\n\tflagger := testutils.TestAddress(\"flagger\")\n\n\tfeedID9 := CreateFeed(\"teritori9\")\n\tfeed9 := mustGetFeed(feedID9)\n\n\tCreatePost(feed9.id, rootPostID, cat1, \"metadata1\")\n\tpid := CreatePost(feed9.id, rootPostID, cat1, \"metadata1\")\n\n\t// Flag post\n\tstd.TestSetOrigCaller(flagger)\n\tFlagPost(feed9.id, pid)\n\n\t// Another user flags\n\tanother := testutils.TestAddress(\"another\")\n\tstd.TestSetOrigCaller(another)\n\tFlagPost(feed9.id, pid)\n\n\tflaggedPostsStr := GetFlaggedPosts(feed9.id, 0, 10)\n\tparsed := ujson.ParseSlice(flaggedPostsStr)\n\tif flaggedPostsCount := len(parsed); flaggedPostsCount != 1 {\n\t\tt.Fatalf(\"expected flagged posts: 1, got %d.\", flaggedPostsCount)\n\t}\n}\n\nfunc testFilterUser(t *testing.T) {\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\n\t// User1 create 2 posts\n\tstd.TestSetOrigCaller(user1)\n\n\tfeedID4 := CreateFeed(\"teritori4\")\n\tfeed4 := mustGetFeed(feedID4)\n\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata2\": \"value\"}`)\n\n\t// User2 create 1 post\n\tstd.TestSetOrigCaller(user2)\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\n\tif count := countPostsByUser(feed4.id, user1.String()); count != 2 {\n\t\tt.Fatalf(\"expected total posts by user1: 2, got %d.\", count)\n\t}\n\n\tif count := countPostsByUser(feed4.id, user2.String()); count != 1 {\n\t\tt.Fatalf(\"expected total posts by user2: 1, got %d.\", count)\n\t}\n\n\tif count := countPostsByUser(feed4.id, \"\"); count != 3 {\n\t\tt.Fatalf(\"expected total posts: 3, got %d.\", count)\n\t}\n}\n\nfunc testHidePostForMe(t *testing.T) {\n\tuser := std.Address(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tfeedID8 := CreateFeed(\"teritor8\")\n\tfeed8 := mustGetFeed(feedID8)\n\n\tpostIDToHide := CreatePost(feed8.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\tpostID := CreatePost(feed8.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// Hide a post for me\n\tHidePostForMe(feed8.id, postIDToHide)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 1 {\n\t\tt.Fatalf(\"expected posts count after hidding: 1, got %q.\", count)\n\t}\n\n\t// Query from another user should return full list\n\tanother := std.Address(\"another\")\n\tstd.TestSetOrigCaller(another)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count from another: 2, got %q.\", count)\n\t}\n\n\t// UnHide a post for me\n\tstd.TestSetOrigCaller(user)\n\tUnHidePostForMe(feed8.id, postIDToHide)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count after unhidding: 2, got %q.\", count)\n\t}\n}\n\nfunc testMigrateFeedData(t *testing.T) string {\n\tfeedID := feedsV7.CreateFeed(\"teritor11\")\n\n\t// Post to test\n\tpostID := feedsV7.CreatePost(feedID, feedsV7.PostID(0), 2, `{\"metadata\": \"value\"}`)\n\tfeedsV7.ReactPost(feedID, postID, \"🇬🇸\", true)\n\n\t// Add comment to post\n\tcommentID := feedsV7.CreatePost(feedID, postID, 2, `{\"comment1\": \"value\"}`)\n\tfeedsV7.ReactPost(feedID, commentID, \"🇬🇸\", true)\n\n\t// // Post with json metadata\n\tfeedsV7.CreatePost(feedID, feedsV7.PostID(0), 2, `{'a':1}`)\n\n\t// Expect: should convert feed data to JSON successfully without error\n\tdataJSON := feedsV7.ExportFeedData(feedID)\n\tif dataJSON == \"\" {\n\t\tt.Fatalf(\"expected feed data exported successfully\")\n\t}\n\n\t// Import data =====================================\n\tImportFeedData(FeedID(uint64(feedID)), dataJSON)\n\n\t// Test public func\n\t// MigrateFromPreviousFeed(feedID)\n}\n\nfunc Test(t *testing.T) {\n\ttestCreateFeed(t)\n\n\ttestCreatePost(t)\n\n\ttestGetPosts(t)\n\n\ttestReactPost(t)\n\n\ttestCreateAndDeleteComment(t)\n\n\ttestFilterByCategories(t)\n\n\ttestTipPost(t)\n\n\ttestFilterUser(t)\n\n\ttestFlagPost(t)\n\n\ttestHidePostForMe(t)\n\n\ttestMigrateFeedData(t)\n}\n" + }, + { + "Name": "flags.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/teritori/flags_index\"\n)\n\nvar SEPARATOR = \"/\"\n\nfunc getFlagID(fid FeedID, pid PostID) flags_index.FlagID {\n\treturn flags_index.FlagID(fid.String() + SEPARATOR + pid.String())\n}\n\nfunc parseFlagID(flagID flags_index.FlagID) (FeedID, PostID) {\n\tparts := strings.Split(string(flagID), SEPARATOR)\n\tif len(parts) != 2 {\n\t\tpanic(\"invalid flag ID '\" + string(flagID) + \"'\")\n\t}\n\tfid, err := strconv.Atoi(parts[0])\n\tif err != nil || fid == 0 {\n\t\tpanic(\"invalid feed ID in flag ID '\" + parts[0] + \"'\")\n\t}\n\tpid, err := strconv.Atoi(parts[1])\n\tif err != nil || pid == 0 {\n\t\tpanic(\"invalid post ID in flag ID '\" + parts[1] + \"'\")\n\t}\n\treturn FeedID(fid), PostID(pid)\n}\n" + }, + { + "Name": "gno.mod", + "Body": "module gno.land/r/demo/teritori/social_feeds\n\nrequire (\n\tgno.land/p/demo/avl v0.0.0-latest\n\tgno.land/p/demo/teritori/dao_interfaces v0.0.0-latest\n\tgno.land/p/demo/teritori/flags_index v0.0.0-latest\n\tgno.land/p/demo/teritori/ujson v0.0.0-latest\n\tgno.land/p/demo/testutils v0.0.0-latest\n\tgno.land/p/demo/ufmt v0.0.0-latest\n\tgno.land/r/demo/boards v0.0.0-latest\n\tgno.land/r/demo/users v0.0.0-latest\n)\n" + }, + { + "Name": "messages.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/teritori/dao_interfaces\"\n\t\"gno.land/p/demo/teritori/ujson\"\n)\n\nvar PKG_PATH = \"gno.land/r/demo/teritori/social_feeds\"\n\n// Ban a post\ntype ExecutableMessageBanPost struct {\n\tdao_interfaces.ExecutableMessage\n\n\tFeedID FeedID\n\tPostID PostID\n\tReason string\n}\n\nfunc (msg ExecutableMessageBanPost) Type() string {\n\treturn \"gno.land/r/demo/teritori/social_feeds.BanPost\"\n}\n\nfunc (msg *ExecutableMessageBanPost) ToJSON() string {\n\treturn ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"feedId\", Value: msg.FeedID},\n\t\t{Key: \"postId\", Value: msg.PostID},\n\t\t{Key: \"reason\", Value: msg.Reason},\n\t})\n}\n\nfunc (msg *ExecutableMessageBanPost) String() string {\n\tvar ss []string\n\tss = append(ss, msg.Type())\n\n\tfeed := getFeed(msg.FeedID)\n\ts := \"\"\n\n\tif feed != nil {\n\t\ts += \"Feed: \" + feed.name + \" (\" + feed.id.String() + \")\"\n\n\t\tpost := feed.GetPost(msg.PostID)\n\t\tif post != nil {\n\t\t\ts += \"\\n Post: \" + post.id.String()\n\t\t} else {\n\t\t\ts += \"\\n Post: \" + msg.PostID.String() + \" (not found)\"\n\t\t}\n\t} else {\n\t\ts += \"Feed: \" + msg.FeedID.String() + \" (not found)\"\n\t}\n\n\ts += \"\\nReason: \" + msg.Reason\n\n\tss = append(ss, s)\n\n\treturn strings.Join(ss, \"\\n---\\n\")\n}\n\ntype BanPostHandler struct {\n\tdao_interfaces.MessageHandler\n}\n\nfunc NewBanPostHandler() *BanPostHandler {\n\treturn &BanPostHandler{}\n}\n\nfunc (h *BanPostHandler) Execute(iMsg dao_interfaces.ExecutableMessage) {\n\tmsg := iMsg.(*ExecutableMessageBanPost)\n\tBanPost(msg.FeedID, msg.PostID, msg.Reason)\n}\n\nfunc (h BanPostHandler) Type() string {\n\treturn ExecutableMessageBanPost{}.Type()\n}\n\nfunc (h *BanPostHandler) MessageFromJSON(ast *ujson.JSONASTNode) dao_interfaces.ExecutableMessage {\n\tmsg := &ExecutableMessageBanPost{}\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"feedId\", Value: &msg.FeedID},\n\t\t{Key: \"postId\", Value: &msg.PostID},\n\t\t{Key: \"reason\", Value: &msg.Reason},\n\t})\n\treturn msg\n}\n" + }, + { + "Name": "misc.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/base64\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc getFeed(fid FeedID) *Feed {\n\tfidkey := feedIDKey(fid)\n\tfeed_, exists := gFeeds.Get(fidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\tfeed := feed_.(*Feed)\n\treturn feed\n}\n\nfunc mustGetFeed(fid FeedID) *Feed {\n\tfeed := getFeed(fid)\n\tif feed == nil {\n\t\tpanic(\"Feed does not exist\")\n\t}\n\treturn feed\n}\n\nfunc incGetFeedID() FeedID {\n\tgFeedsCtr++\n\treturn FeedID(gFeedsCtr)\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t} else {\n\t\treturn user.Name\n\t}\n}\n\nfunc feedIDKey(fid FeedID) string {\n\treturn padZero(uint64(fid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) >= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) >= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc bytesToString(b []byte) string {\n\treturn base64.RawURLEncoding.EncodeToString(b)\n}\n\nfunc intToString(val int) string {\n\treturn strconv.Itoa(val)\n}\n" + }, + { + "Name": "post.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n)\n\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\nfunc (pid *PostID) FromJSON(ast *ujson.JSONASTNode) {\n\tval, err := strconv.Atoi(ast.Value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t*pid = PostID(val)\n}\n\nfunc (pid PostID) ToJSON() string {\n\treturn strconv.Itoa(int(pid))\n}\n\ntype Reaction struct {\n\ticon string\n\tcount uint64\n}\n\nvar Categories []string = []string{\n\t\"Reaction\",\n\t\"Comment\",\n\t\"Normal\",\n\t\"Article\",\n\t\"Picture\",\n\t\"Audio\",\n\t\"Video\",\n}\n\ntype Post struct {\n\tid PostID\n\tparentID PostID\n\tfeedID FeedID\n\tcategory uint64\n\tmetadata string\n\treactions avl.Tree // icon -> count\n\tcomments avl.Tree // Post.id -> *Post\n\tcreator std.Address\n\ttipAmount uint64\n\tdeleted bool\n\tcommentsCount uint64\n\n\tcreatedAt int64\n\tupdatedAt int64\n\tdeletedAt int64\n}\n\nfunc newPost(feed *Feed, id PostID, creator std.Address, parentID PostID, category uint64, metadata string) *Post {\n\treturn &Post{\n\t\tid: id,\n\t\tparentID: parentID,\n\t\tfeedID: feed.id,\n\t\tcategory: category,\n\t\tmetadata: metadata,\n\t\treactions: avl.Tree{},\n\t\tcreator: creator,\n\t\tcreatedAt: time.Now().Unix(),\n\t}\n}\n\nfunc (post *Post) String() string {\n\treturn post.ToJSON()\n}\n\nfunc (post *Post) Update(category uint64, metadata string) {\n\tpost.category = category\n\tpost.metadata = metadata\n\tpost.updatedAt = time.Now().Unix()\n}\n\nfunc (post *Post) Delete() {\n\tpost.deleted = true\n\tpost.deletedAt = time.Now().Unix()\n}\n\nfunc (post *Post) Tip(from std.Address, to std.Address) {\n\treceivedCoins := std.GetOrigSend()\n\tamount := receivedCoins[0].Amount\n\n\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\t// banker := std.GetBanker(std.BankerTypeRealmSend)\n\tcoinsToSend := std.Coins{std.Coin{Denom: \"ugnot\", Amount: amount}}\n\tpkgaddr := std.GetOrigPkgAddr()\n\n\tbanker.SendCoins(pkgaddr, to, coinsToSend)\n\n\t// Update tip amount\n\tpost.tipAmount += uint64(amount)\n}\n\n// Always remove reaction if count = 0\nfunc (post *Post) React(icon string, up bool) {\n\tcount_, ok := post.reactions.Get(icon)\n\tcount := 0\n\n\tif ok {\n\t\tcount = count_.(int)\n\t}\n\n\tif up {\n\t\tcount++\n\t} else {\n\t\tcount--\n\t}\n\n\tif count <= 0 {\n\t\tpost.reactions.Remove(icon)\n\t} else {\n\t\tpost.reactions.Set(icon, count)\n\t}\n}\n\nfunc (post *Post) Render() string {\n\treturn post.metadata\n}\n\nfunc (post *Post) FromJSON(jsonData string) {\n\tast := ujson.TokenizeAndParse(jsonData)\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"id\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tpid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.id = PostID(pid)\n\t\t}},\n\t\t{Key: \"parentID\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tpid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.parentID = PostID(pid)\n\t\t}},\n\t\t{Key: \"feedID\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tfid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.feedID = FeedID(fid)\n\t\t}},\n\t\t{Key: \"category\", Value: &post.category},\n\t\t{Key: \"metadata\", Value: &post.metadata},\n\t\t{Key: \"reactions\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\treactions := avl.NewTree()\n\t\t\tfor _, child := range node.ObjectChildren {\n\t\t\t\treactionCount := child.Value\n\t\t\t\treactions.Set(child.Key, reactionCount)\n\t\t\t}\n\t\t\tpost.reactions = *reactions\n\t\t}},\n\t\t{Key: \"commentsCount\", Value: &post.commentsCount},\n\t\t{Key: \"creator\", Value: &post.creator},\n\t\t{Key: \"tipAmount\", Value: &post.tipAmount},\n\t\t{Key: \"deleted\", Value: &post.deleted},\n\t\t{Key: \"createdAt\", Value: &post.createdAt},\n\t\t{Key: \"updatedAt\", Value: &post.updatedAt},\n\t\t{Key: \"deletedAt\", Value: &post.deletedAt},\n\t})\n}\n\nfunc (post *Post) ToJSON() string {\n\treactionsKV := []ujson.FormatKV{}\n\tpost.reactions.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tcount := value.(int)\n\t\tdata := ujson.FormatKV{Key: key, Value: count}\n\t\treactionsKV = append(reactionsKV, data)\n\t\treturn false\n\t})\n\treactions := ujson.FormatObject(reactionsKV)\n\n\tpostJSON := ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"id\", Value: uint64(post.id)},\n\t\t{Key: \"parentID\", Value: uint64(post.parentID)},\n\t\t{Key: \"feedID\", Value: uint64(post.feedID)},\n\t\t{Key: \"category\", Value: post.category},\n\t\t{Key: \"metadata\", Value: post.metadata},\n\t\t{Key: \"reactions\", Value: reactions, Raw: true},\n\t\t{Key: \"creator\", Value: post.creator},\n\t\t{Key: \"tipAmount\", Value: post.tipAmount},\n\t\t{Key: \"deleted\", Value: post.deleted},\n\t\t{Key: \"commentsCount\", Value: post.commentsCount},\n\t\t{Key: \"createdAt\", Value: post.createdAt},\n\t\t{Key: \"updatedAt\", Value: post.updatedAt},\n\t\t{Key: \"deletedAt\", Value: post.deletedAt},\n\t})\n\treturn postJSON\n}\n" + }, + { + "Name": "public.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/teritori/flags_index\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Only registered user can create a new feed\n// For the flexibility when testing, allow all user to create feed\nfunc CreateFeed(name string) FeedID {\n\tpkgpath := std.CurrentRealmPath()\n\n\tfid := incGetFeedID()\n\tcaller := std.PrevRealm().Addr()\n\turl := strings.Replace(pkgpath, \"gno.land\", \"\", -1) + \":\" + name\n\tfeed := newFeed(fid, url, name, caller)\n\tfidkey := feedIDKey(fid)\n\tgFeeds.Set(fidkey, feed)\n\tgFeedsByName.Set(name, feed)\n\treturn feed.id\n}\n\n// Anyone can create a post in a existing feed, allow un-registered users also\nfunc CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) PostID {\n\tcaller := std.PrevRealm().Addr()\n\n\tfeed := mustGetFeed(fid)\n\tpost := feed.AddPost(caller, parentID, catetory, metadata)\n\treturn post.id\n}\n\n// Only post's owner can edit post\nfunc EditPost(fid FeedID, pid PostID, category uint64, metadata string) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tif caller != post.creator {\n\t\tpanic(\"you are not creator of this post\")\n\t}\n\n\tpost.Update(category, metadata)\n}\n\n// Only feed creator/owner can call this\nfunc SetOwner(fid FeedID, newOwner std.Address) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tif caller != feed.creator && caller != feed.owner {\n\t\tpanic(\"you are not creator/owner of this feed\")\n\t}\n\n\tfeed.owner = newOwner\n}\n\n// Only feed creator/owner or post creator can delete the post\nfunc DeletePost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tif caller != post.creator && caller != feed.creator && caller != feed.owner {\n\t\tpanic(\"you are nor creator of this post neither creator/owner of the feed\")\n\t}\n\n\tpost.Delete()\n\n\t// If post is comment then decrease comments count on parent\n\tif uint64(post.parentID) != 0 {\n\t\tparent := feed.MustGetPost(post.parentID)\n\t\tparent.commentsCount -= 1\n\t}\n}\n\n// Only feed owner can ban the post\nfunc BanPost(fid FeedID, pid PostID, reason string) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\t_ = feed.MustGetPost(pid)\n\n\t// For experimenting, we ban only the post for now\n\t// TODO: recursive delete/ban comments\n\tif caller != feed.owner {\n\t\tpanic(\"you are owner of the feed\")\n\t}\n\n\tfeed.BanPost(pid)\n\n\tfeed.flags.ClearFlagCount(getFlagID(fid, pid))\n}\n\n// Any one can react post\nfunc ReactPost(fid FeedID, pid PostID, icon string, up bool) {\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tpost.React(icon, up)\n}\n\nfunc TipPost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tpost.Tip(caller, post.creator)\n}\n\n// Get a list of flagged posts\n// NOTE: We can support multi feeds in the future but for now we will have only 1 feed\n// Return stringified list in format: postStr-count,postStr-count\nfunc GetFlaggedPosts(fid FeedID, offset uint64, limit uint8) string {\n\tfeed := mustGetFeed(fid)\n\n\t// Already sorted by count descending\n\tflags := feed.flags.GetFlags(uint64(limit), offset)\n\n\tvar postList []string\n\tfor _, flagCount := range flags {\n\t\tflagID := flagCount.FlagID\n\n\t\tfeedID, postID := parseFlagID(flagID)\n\t\tif feedID != feed.id {\n\t\t\tcontinue\n\t\t}\n\n\t\tpost := feed.GetPost(postID)\n\t\tpostList = append(postList, ufmt.Sprintf(\"%s\", post))\n\t}\n\n\tSEPARATOR := \",\"\n\tres := strings.Join(postList, SEPARATOR)\n\treturn ufmt.Sprintf(\"[%s]\", res)\n}\n\n// NOTE: due to bug of std.PrevRealm().Addr() return \"\" when query so we user this proxy function temporary\n// in waiting of correct behaviour of std.PrevRealm().Addr()\nfunc GetPosts(fid FeedID, parentID PostID, user string, categories []uint64, offset uint64, limit uint8) string {\n\tcaller := std.PrevRealm().Addr()\n\tdata := GetPostsWithCaller(fid, parentID, caller.String(), user, categories, offset, limit)\n\treturn data\n}\n\nfunc GetPostsWithCaller(fid FeedID, parentID PostID, callerAddrStr string, user string, categories []uint64, offset uint64, limit uint8) string {\n\t// Return flagged posts, we process flagged posts differently using FlagIndex\n\tif len(categories) == 1 && categories[0] == uint64(9) {\n\t\treturn GetFlaggedPosts(fid, offset, limit)\n\t}\n\n\t// BUG: normally std.PrevRealm().Addr() should return a value instead of empty\n\t// Fix is in progress on Gno side\n\tfeed := mustGetFeed(fid)\n\tposts := getPosts(feed, parentID, callerAddrStr, user, categories, offset, limit)\n\n\tSEPARATOR := \",\"\n\tvar postListStr []string\n\n\tfor _, post := range posts {\n\t\tpostListStr = append(postListStr, post.String())\n\t}\n\n\tres := strings.Join(postListStr, SEPARATOR)\n\treturn ufmt.Sprintf(\"[%s]\", res)\n}\n\n// user here is: filter by user\nfunc getPosts(feed *Feed, parentID PostID, callerAddrStr string, user string, categories []uint64, offset uint64, limit uint8) []*Post {\n\tcaller := std.Address(callerAddrStr)\n\n\tvar posts []*Post\n\tvar skipped uint64\n\n\t// Create an avlTree for optimizing the check\n\trequestedCategories := avl.NewTree()\n\tfor _, category := range categories {\n\t\tcatStr := strconv.FormatUint(category, 10)\n\t\trequestedCategories.Set(catStr, true)\n\t}\n\n\tfeed.posts.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\n\t\tpostCatStr := strconv.FormatUint(post.category, 10)\n\n\t\t// NOTE: this search mechanism is not efficient, only for demo purpose\n\t\tif post.parentID == parentID && post.deleted == false {\n\t\t\tif requestedCategories.Size() > 0 && !requestedCategories.Has(postCatStr) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif user != \"\" && std.Address(user) != post.creator {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Filter hidden post\n\t\t\tflagID := getFlagID(feed.id, post.id)\n\t\t\tif feed.flags.HasFlagged(flagID, callerAddrStr) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check if post is in hidden list\n\t\t\tvalue, exists := feed.hiddenPostsByUser.Get(caller.String())\n\t\t\tif exists {\n\t\t\t\thiddenPosts := value.(*avl.Tree)\n\t\t\t\t// If post.id exists in hiddenPosts tree => that post is hidden\n\t\t\t\tif hiddenPosts.Has(post.id.String()) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif skipped < offset {\n\t\t\t\tskipped++\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tposts = append(posts, post)\n\t\t}\n\n\t\tif len(posts) == int(limit) {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn posts\n}\n\n// Get comments list\nfunc GetComments(fid FeedID, parentID PostID, offset uint64, limit uint8) string {\n\treturn GetPosts(fid, parentID, \"\", []uint64{}, offset, limit)\n}\n\n// Get Post\nfunc GetPost(fid FeedID, pid PostID) string {\n\tfeed := mustGetFeed(fid)\n\n\tdata, ok := feed.posts.Get(postIDKey(pid))\n\tif !ok {\n\t\tpanic(\"Unable to get post\")\n\t}\n\n\tpost := data.(*Post)\n\treturn post.String()\n}\n\nfunc FlagPost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.FlagPost(caller, pid)\n}\n\nfunc HidePostForMe(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.HidePostForUser(caller, pid)\n}\n\nfunc UnHidePostForMe(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.UnHidePostForUser(caller, pid)\n}\n\nfunc GetFlags(fid FeedID, limit uint64, offset uint64) string {\n\tfeed := mustGetFeed(fid)\n\n\ttype FlagCount struct {\n\t\tFlagID flags_index.FlagID\n\t\tCount uint64\n\t}\n\n\tflags := feed.flags.GetFlags(limit, offset)\n\n\tvar res []string\n\tfor _, flag := range flags {\n\t\tres = append(res, ufmt.Sprintf(\"%s:%d\", flag.FlagID, flag.Count))\n\t}\n\n\treturn strings.Join(res, \"|\")\n}\n\n// TODO: allow only creator to call\nfunc GetFeedByID(fid FeedID) *Feed {\n\treturn mustGetFeed(fid)\n}\n\n// TODO: allow only admin to call\nfunc ExportFeedData(fid FeedID) string {\n\tfeed := mustGetFeed(fid)\n\tfeedJSON := feed.ToJSON()\n\treturn feedJSON\n}\n\n// TODO: allow only admin to call\nfunc ImportFeedData(fid FeedID, jsonData string) {\n\tfeed := mustGetFeed(fid)\n\tfeed.FromJSON(jsonData)\n}\n\n// func MigrateFromPreviousFeed(fid feedsV7.FeedID) {\n// \t// Get exported data from previous feeds\n// \tjsonData := feedsV7.ExportFeedData(fid)\n// \tImportFeedData(FeedID(uint64(fid)), jsonData)\n// }\n" + }, + { + "Name": "render.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc renderFeed(parts []string) string {\n\t// /r/demo/social_feeds_v4:FEED_NAME\n\tname := parts[0]\n\tfeedI, exists := gFeedsByName.Get(name)\n\tif !exists {\n\t\treturn \"feed does not exist: \" + name\n\t}\n\treturn feedI.(*Feed).Render()\n}\n\nfunc renderPost(parts []string) string {\n\t// /r/demo/boards:FEED_NAME/POST_ID\n\tname := parts[0]\n\tfeedI, exists := gFeedsByName.Get(name)\n\tif !exists {\n\t\treturn \"feed does not exist: \" + name\n\t}\n\tpid, err := strconv.Atoi(parts[1])\n\tif err != nil {\n\t\treturn \"invalid thread id: \" + parts[1]\n\t}\n\tfeed := feedI.(*Feed)\n\tpost := feed.MustGetPost(PostID(pid))\n\treturn post.Render()\n}\n\nfunc renderFeedsList() string {\n\tstr := \"There are \" + intToString(gFeeds.Size()) + \" available feeds:\\n\\n\"\n\tgFeeds.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tfeed := value.(*Feed)\n\t\tstr += \" * [\" + feed.url + \" (FeedID: \" + feed.id.String() + \")](\" + feed.url + \")\\n\"\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderFeedsList()\n\t}\n\n\tparts := strings.Split(path, \"/\")\n\n\tif len(parts) == 1 {\n\t\t// /r/demo/social_feeds_v4:FEED_NAME\n\t\treturn renderFeed(parts)\n\t} else if len(parts) == 2 {\n\t\t// /r/demo/social_feeds_v4:FEED_NAME/POST_ID\n\t\treturn renderPost(parts)\n\t}\n\n\treturn \"Not found\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "100000000", + "gas_fee": "10000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeySecp256k1", + "value": "A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y" + }, + "signature": "fg01rLWLymXHVn9fE9vNyo4i2idOAEJn6KsPnlMT5JdrWqjzLScI65JVpJJErQUQMdpx/LvBPNVG3Atv/VGekg==" + } + ], + "memo": "" +} + +-- bye/bye.gno -- +package bye + +import ( + "encoding/base64" +) + +func init() { + val, _ := base64.StdEncoding.DecodeString("heyhey") + println(val) + base64.StdEncoding.EncodeToString([]byte(val)) +} diff --git a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar new file mode 100644 index 00000000000..235d16902dc --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar @@ -0,0 +1,103 @@ +# Test for https://github.com/gnolang/gno/issues/2283 +# This is an earlier variant of the txtar which likely shares the same cause; +# but instead of causing an index out of range, it causes a "should not happen" +# panic from the store. + +# These are not necessary, but they "alleviate" add_feeds.tx from the +# responsibility of loading standard libraries, thus not making it exceed +# the --gas-wanted. +loadpkg gno.land/r/demo/users +loadpkg gno.land/r/demo/boards + +gnoland start + +! gnokey broadcast $WORK/add_feeds.tx + +gnokey maketx addpkg -pkgdir $WORK/bye -pkgpath gno.land/r/demo/bye -gas-fee 1000000ugnot -gas-wanted 10_000_000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +-- add_feeds.tx -- +{ + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "Name": "social_feeds", + "Path": "gno.land/r/demo/teritori/social_feeds", + "Files": [ + { + "Name": "binutils_extra.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/binary\"\n)\n\nfunc EncodeLengthPrefixedStringUint32BE(s string) []byte {\n\tb := make([]byte, 4+len(s))\n\tbinary.BigEndian.PutUint32(b, uint32(len(s)))\n\tcopy(b[4:], s)\n\treturn b\n}\n" + }, + { + "Name": "feed.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/teritori/flags_index\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\ntype FeedID uint64\n\nfunc (fid FeedID) String() string {\n\treturn strconv.Itoa(int(fid))\n}\n\nfunc (fid *FeedID) FromJSON(ast *ujson.JSONASTNode) {\n\tval, err := strconv.Atoi(ast.Value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t*fid = FeedID(val)\n}\n\nfunc (fid FeedID) ToJSON() string {\n\treturn strconv.Itoa(int(fid))\n}\n\ntype Feed struct {\n\tid FeedID\n\turl string\n\tname string\n\tcreator std.Address\n\towner std.Address\n\tposts avl.Tree // pidkey -> *Post\n\tcreatedAt int64\n\n\tflags *flags_index.FlagsIndex\n\thiddenPostsByUser avl.Tree // std.Address => *avl.Tree (postID => bool)\n\n\tpostsCtr uint64\n}\n\nfunc newFeed(fid FeedID, url string, name string, creator std.Address) *Feed {\n\tif !reName.MatchString(name) {\n\t\tpanic(\"invalid feed name: \" + name)\n\t}\n\n\tif gFeedsByName.Has(name) {\n\t\tpanic(\"feed already exists: \" + name)\n\t}\n\n\treturn &Feed{\n\t\tid: fid,\n\t\turl: url,\n\t\tname: name,\n\t\tcreator: creator,\n\t\towner: creator,\n\t\tposts: avl.Tree{},\n\t\tcreatedAt: time.Now().Unix(),\n\t\tflags: flags_index.NewFlagsIndex(),\n\t\tpostsCtr: 0,\n\t}\n}\n\nfunc (feed *Feed) incGetPostID() PostID {\n\tfeed.postsCtr++\n\treturn PostID(feed.postsCtr)\n}\n\nfunc (feed *Feed) GetPost(pid PostID) *Post {\n\tpidkey := postIDKey(pid)\n\tpost_, exists := feed.posts.Get(pidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\treturn post_.(*Post)\n}\n\nfunc (feed *Feed) MustGetPost(pid PostID) *Post {\n\tpost := feed.GetPost(pid)\n\tif post == nil {\n\t\tpanic(\"post does not exist\")\n\t}\n\treturn post\n}\n\nfunc (feed *Feed) AddPost(creator std.Address, parentID PostID, category uint64, metadata string) *Post {\n\tpid := feed.incGetPostID()\n\tpidkey := postIDKey(pid)\n\n\tpost := newPost(feed, pid, creator, parentID, category, metadata)\n\tfeed.posts.Set(pidkey, post)\n\n\t// If post is a comment then increase the comment count on parent\n\tif uint64(parentID) != 0 {\n\t\tparent := feed.MustGetPost(parentID)\n\t\tparent.commentsCount += 1\n\t}\n\n\treturn post\n}\n\nfunc (feed *Feed) FlagPost(flagBy std.Address, pid PostID) {\n\tflagID := getFlagID(feed.id, pid)\n\n\tif feed.flags.HasFlagged(flagID, flagBy.String()) {\n\t\tpanic(\"already flagged\")\n\t}\n\n\tfeed.flags.Flag(flagID, flagBy.String())\n}\n\nfunc (feed *Feed) BanPost(pid PostID) {\n\tpidkey := postIDKey(pid)\n\t_, removed := feed.posts.Remove(pidkey)\n\tif !removed {\n\t\tpanic(\"post does not exist with id \" + pid.String())\n\t}\n}\n\nfunc (feed *Feed) HidePostForUser(caller std.Address, pid PostID) {\n\tuserAddr := caller.String()\n\n\tvalue, exists := feed.hiddenPostsByUser.Get(userAddr)\n\tvar hiddenPosts *avl.Tree\n\tif exists {\n\t\thiddenPosts = value.(*avl.Tree)\n\t} else {\n\t\thiddenPosts = avl.NewTree()\n\t\tfeed.hiddenPostsByUser.Set(userAddr, hiddenPosts)\n\t}\n\n\tif hiddenPosts.Has(pid.String()) {\n\t\tpanic(\"PostID is already hidden: \" + pid.String())\n\t}\n\n\thiddenPosts.Set(pid.String(), true)\n}\n\nfunc (feed *Feed) UnHidePostForUser(userAddress std.Address, pid PostID) {\n\tvalue, exists := feed.hiddenPostsByUser.Get(userAddress.String())\n\tvar hiddenPosts *avl.Tree\n\tif exists {\n\t\thiddenPosts = value.(*avl.Tree)\n\t\t_, removed := hiddenPosts.Remove(pid.String())\n\t\tif !removed {\n\t\t\tpanic(\"Post is not hidden: \" + pid.String())\n\t\t}\n\t} else {\n\t\tpanic(\"User has not hidden post: \" + pid.String())\n\t}\n}\n\nfunc (feed *Feed) Render() string {\n\tpkgpath := std.CurrentRealmPath()\n\n\tstr := \"\"\n\tstr += ufmt.Sprintf(\"Feed: %s (ID: %s) - Owner: %s\", feed.name, feed.id, feed.owner)\n\tstr += \"\\n\\n There are \" + intToString(feed.posts.Size()) + \" post(s) \\n\\n\"\n\n\tif feed.posts.Size() > 0 {\n\t\tfeed.posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\t\tif str != \"\" {\n\t\t\t\tstr += \"\\n\"\n\t\t\t}\n\n\t\t\tpost := value.(*Post)\n\t\t\tpostUrl := strings.Replace(pkgpath, \"gno.land\", \"\", -1) + \":\" + feed.name + \"/\" + post.id.String()\n\n\t\t\tstr += \" * [\" +\n\t\t\t\t\"PostID: \" + post.id.String() +\n\t\t\t\t\" - \" + intToString(post.reactions.Size()) + \" reactions \" +\n\t\t\t\t\" - \" + ufmt.Sprintf(\"%d\", post.tipAmount) + \" tip amount\" +\n\t\t\t\t\"]\" +\n\t\t\t\t\"(\" + postUrl + \")\" +\n\t\t\t\t\"\\n\"\n\t\t\treturn false\n\t\t})\n\n\t\tstr += \"-------------------------\\n\"\n\t\tstr += feed.flags.Dump()\n\t}\n\n\tstr += \"---------------------------------------\\n\"\n\tif feed.hiddenPostsByUser.Size() > 0 {\n\t\tstr += \"Hidden posts by users:\\n\\n\"\n\n\t\tfeed.hiddenPostsByUser.Iterate(\"\", \"\", func(userAddr string, value interface{}) bool {\n\t\t\thiddenPosts := value.(*avl.Tree)\n\t\t\tstr += \"\\nUser address: \" + userAddr + \"\\n\"\n\n\t\t\thiddenPosts.Iterate(\"\", \"\", func(pid string, value interface{}) bool {\n\t\t\t\tstr += \"- PostID: \" + pid + \"\\n\"\n\t\t\t\treturn false\n\t\t\t})\n\n\t\t\treturn false\n\t\t})\n\t}\n\n\treturn str\n}\n\nfunc (feed *Feed) ToJSON() string {\n\tposts := []ujson.FormatKV{}\n\tfeed.posts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tposts = append(posts, ujson.FormatKV{\n\t\t\tKey: key,\n\t\t\tValue: value.(*Post),\n\t\t})\n\t\treturn false\n\t})\n\tfeedJSON := ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"id\", Value: uint64(feed.id)},\n\t\t{Key: \"url\", Value: feed.url},\n\t\t{Key: \"name\", Value: feed.name},\n\t\t{Key: \"creator\", Value: feed.creator},\n\t\t{Key: \"owner\", Value: feed.owner},\n\t\t{Key: \"posts\", Value: ujson.FormatObject(posts), Raw: true},\n\t\t{Key: \"createdAt\", Value: feed.createdAt},\n\t\t{Key: \"postsCtr\", Value: feed.postsCtr},\n\t\t// TODO: convert flags, hiddenPostsByUser\n\t\t// {Key: \"flags\", Value: feed.flags},\n\t\t// {Key: \"hiddenPostsByUser\", Value: feed.hiddenPostsByUser},\n\t})\n\treturn feedJSON\n}\n\nfunc (feed *Feed) FromJSON(jsonData string) {\n\tast := ujson.TokenizeAndParse(jsonData)\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"id\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tfid, _ := strconv.Atoi(node.Value)\n\t\t\tfeed.id = FeedID(fid)\n\t\t}},\n\t\t{Key: \"url\", Value: &feed.url},\n\t\t{Key: \"name\", Value: &feed.name},\n\t\t{Key: \"creator\", Value: &feed.creator},\n\t\t{Key: \"owner\", Value: &feed.owner},\n\t\t{Key: \"posts\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tposts := avl.NewTree()\n\t\t\tfor _, child := range node.ObjectChildren {\n\t\t\t\tpostNode := child.Value\n\n\t\t\t\tpost := Post{}\n\t\t\t\tpost.FromJSON(postNode.String())\n\t\t\t\tposts.Set(child.Key, &post)\n\t\t\t}\n\t\t\tfeed.posts = *posts\n\t\t}},\n\t\t{Key: \"createdAt\", Value: &feed.createdAt},\n\t\t{Key: \"postsCtr\", Value: &feed.postsCtr},\n\t})\n}\n" + }, + { + "Name": "feeds.gno", + "Body": "package social_feeds\n\nimport (\n\t\"regexp\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n//----------------------------------------\n// Realm (package) state\n\nvar (\n\tgFeeds avl.Tree // id -> *Feed\n\tgFeedsCtr int // increments Feed.id\n\tgFeedsByName avl.Tree // name -> *Feed\n\tgDefaultAnonFee = 100000000 // minimum fee required if anonymous\n)\n\n//----------------------------------------\n// Constants\n\nvar reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)\n" + }, + { + "Name": "feeds_test.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"gno.land/p/demo/avl\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n\t\"gno.land/p/demo/testutils\"\n\t\"gno.land/r/demo/boards\"\n\t// Fake previous version for testing\n\tfeedsV7 \"gno.land/r/demo/teritori/social_feeds\"\n\t\"gno.land/r/demo/users\"\n)\n\nvar (\n\trootPostID = PostID(0)\n\tpostID1 = PostID(1)\n\tfeedID1 = FeedID(1)\n\tcat1 = uint64(1)\n\tcat2 = uint64(2)\n\tuser = testutils.TestAddress(\"user\")\n\tfilter_all = []uint64{}\n)\n\nfunc getFeed1() *Feed {\n\treturn mustGetFeed(feedID1)\n}\n\nfunc getPost1() *Post {\n\tfeed1 := getFeed1()\n\tpost1 := feed1.MustGetPost(postID1)\n\treturn post1\n}\n\nfunc testCreateFeed(t *testing.T) {\n\tfeedID := CreateFeed(\"teritori1\")\n\tfeed := mustGetFeed(feedID)\n\n\tif feedID != 1 {\n\t\tt.Fatalf(\"expected feedID: 1, got %q.\", feedID)\n\t}\n\n\tif feed.name != \"teritori1\" {\n\t\tt.Fatalf(\"expected feedName: teritori1, got %q.\", feed.name)\n\t}\n}\n\nfunc testCreatePost(t *testing.T) {\n\tmetadata := `{\"gifs\": [], \"files\": [], \"title\": \"\", \"message\": \"testouille\", \"hashtags\": [], \"mentions\": [], \"createdAt\": \"2023-03-29T12:19:04.858Z\", \"updatedAt\": \"2023-03-29T12:19:04.858Z\"}`\n\tpostID := CreatePost(feedID1, rootPostID, cat1, metadata)\n\tfeed := mustGetFeed(feedID1)\n\tpost := feed.MustGetPost(postID)\n\n\tif postID != 1 {\n\t\tt.Fatalf(\"expected postID: 1, got %q.\", postID)\n\t}\n\n\tif post.category != cat1 {\n\t\tt.Fatalf(\"expected categoryID: %q, got %q.\", cat1, post.category)\n\t}\n}\n\nfunc toPostIDsStr(posts []*Post) string {\n\tvar postIDs []string\n\tfor _, post := range posts {\n\t\tpostIDs = append(postIDs, post.id.String())\n\t}\n\n\tpostIDsStr := strings.Join(postIDs, \",\")\n\treturn postIDsStr\n}\n\nfunc testGetPosts(t *testing.T) {\n\tuser := std.Address(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tfeedID := CreateFeed(\"teritori10\")\n\tfeed := mustGetFeed(feedID)\n\n\tCreatePost(feedID, rootPostID, cat1, \"post1\")\n\tCreatePost(feedID, rootPostID, cat1, \"post2\")\n\tCreatePost(feedID, rootPostID, cat1, \"post3\")\n\tCreatePost(feedID, rootPostID, cat1, \"post4\")\n\tCreatePost(feedID, rootPostID, cat1, \"post5\")\n\tpostIDToFlagged := CreatePost(feedID, rootPostID, cat1, \"post6\")\n\tpostIDToHide := CreatePost(feedID, rootPostID, cat1, \"post7\")\n\tCreatePost(feedID, rootPostID, cat1, \"post8\")\n\n\tvar posts []*Post\n\tvar postIDsStr string\n\n\t// Query last 3 posts\n\tposts = getPosts(feed, 0, \"\", \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,7,6\" {\n\t\tt.Fatalf(\"expected posts order: 8,7,6. Got: %s\", postIDsStr)\n\t}\n\n\t// Query page 2\n\tposts = getPosts(feed, 0, \"\", \"\", []uint64{}, 3, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\tif postIDsStr != \"5,4,3\" {\n\t\tt.Fatalf(\"expected posts order: 5,4,3. Got: %s\", postIDsStr)\n\t}\n\n\t// Exclude hidden post\n\tHidePostForMe(feed.id, postIDToHide)\n\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,6,5\" {\n\t\tt.Fatalf(\"expected posts order: 8,6,5. Got: %s\", postIDsStr)\n\t}\n\n\t// Exclude flagged post\n\tFlagPost(feed.id, postIDToFlagged)\n\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 0, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"8,5,4\" {\n\t\tt.Fatalf(\"expected posts order: 8,5,4. Got: %s\", postIDsStr)\n\t}\n\n\t// Pagination with hidden/flagged posts\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 3, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"3,2,1\" {\n\t\tt.Fatalf(\"expected posts order: 3,2,1. Got: %s\", postIDsStr)\n\t}\n\n\t// Query out of range\n\tposts = getPosts(feed, 0, user.String(), \"\", []uint64{}, 6, 3)\n\tpostIDsStr = toPostIDsStr(posts)\n\n\tif postIDsStr != \"\" {\n\t\tt.Fatalf(\"expected posts order: ''. Got: %s\", postIDsStr)\n\t}\n}\n\nfunc testReactPost(t *testing.T) {\n\tfeed := getFeed1()\n\tpost := getPost1()\n\n\ticon := \"🥰\"\n\tReactPost(feed.id, post.id, icon, true)\n\n\t// Set reaction\n\treactionCount_, ok := post.reactions.Get(\"🥰\")\n\tif !ok {\n\t\tt.Fatalf(\"expected 🥰 exists\")\n\t}\n\n\treactionCount := reactionCount_.(int)\n\tif reactionCount != 1 {\n\t\tt.Fatalf(\"expected reactionCount: 1, got %q.\", reactionCount)\n\t}\n\n\t// Unset reaction\n\tReactPost(feed.id, post.id, icon, false)\n\t_, exist := post.reactions.Get(\"🥰\")\n\tif exist {\n\t\tt.Fatalf(\"expected 🥰 not exist\")\n\t}\n}\n\nfunc testCreateAndDeleteComment(t *testing.T) {\n\tfeed1 := getFeed1()\n\tpost1 := getPost1()\n\n\tmetadata := `empty_meta_data`\n\n\tcommentID1 := CreatePost(feed1.id, post1.id, cat1, metadata)\n\tcommentID2 := CreatePost(feed1.id, post1.id, cat1, metadata)\n\tcomment2 := feed1.MustGetPost(commentID2)\n\n\tif comment2.id != 3 { // 1 post + 2 comments = 3\n\t\tt.Fatalf(\"expected comment postID: 3, got %q.\", comment2.id)\n\t}\n\n\tif comment2.parentID != post1.id {\n\t\tt.Fatalf(\"expected comment parentID: %q, got %q.\", post1.id, comment2.parentID)\n\t}\n\n\t// Check comment count on parent\n\tif post1.commentsCount != 2 {\n\t\tt.Fatalf(\"expected comments count: 2, got %d.\", post1.commentsCount)\n\t}\n\n\t// Get comments\n\tcomments := GetComments(feed1.id, post1.id, 0, 10)\n\tcommentsParsed := ujson.ParseSlice(comments)\n\n\tif len(commentsParsed) != 2 {\n\t\tt.Fatalf(\"expected encoded comments: 2, got %q.\", commentsParsed)\n\t}\n\n\t// Delete 1 comment\n\tDeletePost(feed1.id, comment2.id)\n\tcomments = GetComments(feed1.id, post1.id, 0, 10)\n\tcommentsParsed = ujson.ParseSlice(comments)\n\n\tif len(commentsParsed) != 1 {\n\t\tt.Fatalf(\"expected encoded comments: 1, got %q.\", commentsParsed)\n\t}\n\n\t// Check comment count on parent\n\tif post1.commentsCount != 1 {\n\t\tt.Fatalf(\"expected comments count: 1, got %d.\", post1.commentsCount)\n\t}\n}\n\nfunc countPosts(feedID FeedID, categories []uint64, limit uint8) int {\n\toffset := uint64(0)\n\n\tpostsStr := GetPosts(feedID, 0, \"\", categories, offset, limit)\n\tif postsStr == \"[]\" {\n\t\treturn 0\n\t}\n\n\tparsedPosts := ujson.ParseSlice(postsStr)\n\tpostsCount := len(parsedPosts)\n\treturn postsCount\n}\n\nfunc countPostsByUser(feedID FeedID, user string) int {\n\toffset := uint64(0)\n\tlimit := uint8(10)\n\n\tpostsStr := GetPosts(feedID, 0, user, []uint64{}, offset, limit)\n\tif postsStr == \"[]\" {\n\t\treturn 0\n\t}\n\n\tparsedPosts := ujson.ParseSlice(postsStr)\n\tpostsCount := len(parsedPosts)\n\treturn postsCount\n}\n\nfunc testFilterByCategories(t *testing.T) {\n\t// // Re-add reaction to test post list\n\t// ReactPost(1, postID, \"🥰\", true)\n\t// ReactPost(1, postID, \"😇\", true)\n\n\tfilter_cat1 := []uint64{1}\n\tfilter_cat1_2 := []uint64{1, 2}\n\tfilter_cat9 := []uint64{9}\n\tfilter_cat1_2_9 := []uint64{1, 2, 9}\n\n\tfeedID2 := CreateFeed(\"teritori2\")\n\tfeed2 := mustGetFeed(feedID2)\n\n\t// Create 2 posts on root with cat1\n\tpostID1 := CreatePost(feed2.id, rootPostID, cat1, \"metadata\")\n\tpostID2 := CreatePost(feed2.id, rootPostID, cat1, \"metadata\")\n\n\t// Create 1 posts on root with cat2\n\tpostID3 := CreatePost(feed2.id, rootPostID, cat2, \"metadata\")\n\n\t// Create comments on post 1\n\tcommentPostID1 := CreatePost(feed2.id, postID1, cat1, \"metadata\")\n\n\t// cat1: Should return max = limit\n\tif count := countPosts(feed2.id, filter_cat1, 1); count != 1 {\n\t\tt.Fatalf(\"expected posts count: 1, got %q.\", count)\n\t}\n\n\t// cat1: Should return max = total\n\tif count := countPosts(feed2.id, filter_cat1, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// cat 1 + 2: Should return max = limit\n\tif count := countPosts(feed2.id, filter_cat1_2, 2); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// cat 1 + 2: Should return max = total on both\n\tif count := countPosts(feed2.id, filter_cat1_2, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// cat 1, 2, 9: Should return total of 1, 2\n\tif count := countPosts(feed2.id, filter_cat1_2_9, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// cat 9: Should return 0\n\tif count := countPosts(feed2.id, filter_cat9, 10); count != 0 {\n\t\tt.Fatalf(\"expected posts count: 0, got %q.\", count)\n\t}\n\n\t// cat all: should return all\n\tif count := countPosts(feed2.id, filter_all, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// add comments should not impact the results\n\tCreatePost(feed2.id, postID1, cat1, \"metadata\")\n\tCreatePost(feed2.id, postID2, cat1, \"metadata\")\n\n\tif count := countPosts(feed2.id, filter_all, 10); count != 3 {\n\t\tt.Fatalf(\"expected posts count: 3, got %q.\", count)\n\t}\n\n\t// delete a post should affect the result\n\tDeletePost(feed2.id, postID1)\n\n\tif count := countPosts(feed2.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n}\n\nfunc testTipPost(t *testing.T) {\n\tcreator := testutils.TestAddress(\"creator\")\n\tstd.TestIssueCoins(creator, std.Coins{{\"ugnot\", 100_000_000}})\n\n\t// NOTE: Dont know why the address should be this to be able to call banker (= std.GetCallerAt(1))\n\ttipper := testutils.TestAddress(\"tipper\")\n\tstd.TestIssueCoins(tipper, std.Coins{{\"ugnot\", 50_000_000}})\n\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\n\t// Check Original coins of creator/tipper\n\tif coins := banker.GetCoins(creator); coins[0].Amount != 100_000_000 {\n\t\tt.Fatalf(\"expected creator coin count: 100_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\tif coins := banker.GetCoins(tipper); coins[0].Amount != 50_000_000 {\n\t\tt.Fatalf(\"expected tipper coin count: 50_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\t// Creator creates feed, post\n\tstd.TestSetOrigCaller(creator)\n\n\tfeedID3 := CreateFeed(\"teritori3\")\n\tfeed3 := mustGetFeed(feedID3)\n\n\tpostID1 := CreatePost(feed3.id, rootPostID, cat1, \"metadata\")\n\tpost1 := feed3.MustGetPost(postID1)\n\n\t// Tiper tips the ppst\n\tstd.TestSetOrigCaller(tipper)\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 1_000_000}}, nil)\n\tTipPost(feed3.id, post1.id)\n\n\t// Coin must be increased for creator\n\tif coins := banker.GetCoins(creator); coins[0].Amount != 101_000_000 {\n\t\tt.Fatalf(\"expected creator coin after beging tipped: 101_000_000, got %d.\", coins[0].Amount)\n\t}\n\n\t// Total tip amount should increased\n\tif post1.tipAmount != 1_000_000 {\n\t\tt.Fatalf(\"expected total tipAmount: 1_000_000, got %d.\", post1.tipAmount)\n\t}\n\n\t// Add more tip should update this total\n\tstd.TestSetOrigSend(std.Coins{{\"ugnot\", 2_000_000}}, nil)\n\tTipPost(feed3.id, post1.id)\n\n\tif post1.tipAmount != 3_000_000 {\n\t\tt.Fatalf(\"expected total tipAmount: 3_000_000, got %d.\", post1.tipAmount)\n\t}\n}\n\nfunc testFlagPost(t *testing.T) {\n\tflagger := testutils.TestAddress(\"flagger\")\n\n\tfeedID9 := CreateFeed(\"teritori9\")\n\tfeed9 := mustGetFeed(feedID9)\n\n\tCreatePost(feed9.id, rootPostID, cat1, \"metadata1\")\n\tpid := CreatePost(feed9.id, rootPostID, cat1, \"metadata1\")\n\n\t// Flag post\n\tstd.TestSetOrigCaller(flagger)\n\tFlagPost(feed9.id, pid)\n\n\t// Another user flags\n\tanother := testutils.TestAddress(\"another\")\n\tstd.TestSetOrigCaller(another)\n\tFlagPost(feed9.id, pid)\n\n\tflaggedPostsStr := GetFlaggedPosts(feed9.id, 0, 10)\n\tparsed := ujson.ParseSlice(flaggedPostsStr)\n\tif flaggedPostsCount := len(parsed); flaggedPostsCount != 1 {\n\t\tt.Fatalf(\"expected flagged posts: 1, got %d.\", flaggedPostsCount)\n\t}\n}\n\nfunc testFilterUser(t *testing.T) {\n\tuser1 := testutils.TestAddress(\"user1\")\n\tuser2 := testutils.TestAddress(\"user2\")\n\n\t// User1 create 2 posts\n\tstd.TestSetOrigCaller(user1)\n\n\tfeedID4 := CreateFeed(\"teritori4\")\n\tfeed4 := mustGetFeed(feedID4)\n\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata2\": \"value\"}`)\n\n\t// User2 create 1 post\n\tstd.TestSetOrigCaller(user2)\n\tCreatePost(feed4.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\n\tif count := countPostsByUser(feed4.id, user1.String()); count != 2 {\n\t\tt.Fatalf(\"expected total posts by user1: 2, got %d.\", count)\n\t}\n\n\tif count := countPostsByUser(feed4.id, user2.String()); count != 1 {\n\t\tt.Fatalf(\"expected total posts by user2: 1, got %d.\", count)\n\t}\n\n\tif count := countPostsByUser(feed4.id, \"\"); count != 3 {\n\t\tt.Fatalf(\"expected total posts: 3, got %d.\", count)\n\t}\n}\n\nfunc testHidePostForMe(t *testing.T) {\n\tuser := std.Address(\"user\")\n\tstd.TestSetOrigCaller(user)\n\n\tfeedID8 := CreateFeed(\"teritor8\")\n\tfeed8 := mustGetFeed(feedID8)\n\n\tpostIDToHide := CreatePost(feed8.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\tpostID := CreatePost(feed8.id, rootPostID, cat1, `{\"metadata\": \"value\"}`)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count: 2, got %q.\", count)\n\t}\n\n\t// Hide a post for me\n\tHidePostForMe(feed8.id, postIDToHide)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 1 {\n\t\tt.Fatalf(\"expected posts count after hidding: 1, got %q.\", count)\n\t}\n\n\t// Query from another user should return full list\n\tanother := std.Address(\"another\")\n\tstd.TestSetOrigCaller(another)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count from another: 2, got %q.\", count)\n\t}\n\n\t// UnHide a post for me\n\tstd.TestSetOrigCaller(user)\n\tUnHidePostForMe(feed8.id, postIDToHide)\n\n\tif count := countPosts(feed8.id, filter_all, 10); count != 2 {\n\t\tt.Fatalf(\"expected posts count after unhidding: 2, got %q.\", count)\n\t}\n}\n\nfunc testMigrateFeedData(t *testing.T) string {\n\tfeedID := feedsV7.CreateFeed(\"teritor11\")\n\n\t// Post to test\n\tpostID := feedsV7.CreatePost(feedID, feedsV7.PostID(0), 2, `{\"metadata\": \"value\"}`)\n\tfeedsV7.ReactPost(feedID, postID, \"🇬🇸\", true)\n\n\t// Add comment to post\n\tcommentID := feedsV7.CreatePost(feedID, postID, 2, `{\"comment1\": \"value\"}`)\n\tfeedsV7.ReactPost(feedID, commentID, \"🇬🇸\", true)\n\n\t// // Post with json metadata\n\tfeedsV7.CreatePost(feedID, feedsV7.PostID(0), 2, `{'a':1}`)\n\n\t// Expect: should convert feed data to JSON successfully without error\n\tdataJSON := feedsV7.ExportFeedData(feedID)\n\tif dataJSON == \"\" {\n\t\tt.Fatalf(\"expected feed data exported successfully\")\n\t}\n\n\t// Import data =====================================\n\tImportFeedData(FeedID(uint64(feedID)), dataJSON)\n\n\t// Test public func\n\t// MigrateFromPreviousFeed(feedID)\n}\n\nfunc Test(t *testing.T) {\n\ttestCreateFeed(t)\n\n\ttestCreatePost(t)\n\n\ttestGetPosts(t)\n\n\ttestReactPost(t)\n\n\ttestCreateAndDeleteComment(t)\n\n\ttestFilterByCategories(t)\n\n\ttestTipPost(t)\n\n\ttestFilterUser(t)\n\n\ttestFlagPost(t)\n\n\ttestHidePostForMe(t)\n\n\ttestMigrateFeedData(t)\n}\n" + }, + { + "Name": "flags.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/teritori/flags_index\"\n)\n\nvar SEPARATOR = \"/\"\n\nfunc getFlagID(fid FeedID, pid PostID) flags_index.FlagID {\n\treturn flags_index.FlagID(fid.String() + SEPARATOR + pid.String())\n}\n\nfunc parseFlagID(flagID flags_index.FlagID) (FeedID, PostID) {\n\tparts := strings.Split(string(flagID), SEPARATOR)\n\tif len(parts) != 2 {\n\t\tpanic(\"invalid flag ID '\" + string(flagID) + \"'\")\n\t}\n\tfid, err := strconv.Atoi(parts[0])\n\tif err != nil || fid == 0 {\n\t\tpanic(\"invalid feed ID in flag ID '\" + parts[0] + \"'\")\n\t}\n\tpid, err := strconv.Atoi(parts[1])\n\tif err != nil || pid == 0 {\n\t\tpanic(\"invalid post ID in flag ID '\" + parts[1] + \"'\")\n\t}\n\treturn FeedID(fid), PostID(pid)\n}\n" + }, + { + "Name": "gno.mod", + "Body": "module gno.land/r/demo/teritori/social_feeds\n\nrequire (\n\tgno.land/p/demo/avl v0.0.0-latest\n\tgno.land/p/demo/teritori/dao_interfaces v0.0.0-latest\n\tgno.land/p/demo/teritori/flags_index v0.0.0-latest\n\tgno.land/p/demo/teritori/ujson v0.0.0-latest\n\tgno.land/p/demo/testutils v0.0.0-latest\n\tgno.land/p/demo/ufmt v0.0.0-latest\n\tgno.land/r/demo/boards v0.0.0-latest\n\tgno.land/r/demo/users v0.0.0-latest\n)\n" + }, + { + "Name": "messages.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strings\"\n\n\t\"gno.land/p/demo/teritori/dao_interfaces\"\n\t\"gno.land/p/demo/teritori/ujson\"\n)\n\nvar PKG_PATH = \"gno.land/r/demo/teritori/social_feeds\"\n\n// Ban a post\ntype ExecutableMessageBanPost struct {\n\tdao_interfaces.ExecutableMessage\n\n\tFeedID FeedID\n\tPostID PostID\n\tReason string\n}\n\nfunc (msg ExecutableMessageBanPost) Type() string {\n\treturn \"gno.land/r/demo/teritori/social_feeds.BanPost\"\n}\n\nfunc (msg *ExecutableMessageBanPost) ToJSON() string {\n\treturn ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"feedId\", Value: msg.FeedID},\n\t\t{Key: \"postId\", Value: msg.PostID},\n\t\t{Key: \"reason\", Value: msg.Reason},\n\t})\n}\n\nfunc (msg *ExecutableMessageBanPost) String() string {\n\tvar ss []string\n\tss = append(ss, msg.Type())\n\n\tfeed := getFeed(msg.FeedID)\n\ts := \"\"\n\n\tif feed != nil {\n\t\ts += \"Feed: \" + feed.name + \" (\" + feed.id.String() + \")\"\n\n\t\tpost := feed.GetPost(msg.PostID)\n\t\tif post != nil {\n\t\t\ts += \"\\n Post: \" + post.id.String()\n\t\t} else {\n\t\t\ts += \"\\n Post: \" + msg.PostID.String() + \" (not found)\"\n\t\t}\n\t} else {\n\t\ts += \"Feed: \" + msg.FeedID.String() + \" (not found)\"\n\t}\n\n\ts += \"\\nReason: \" + msg.Reason\n\n\tss = append(ss, s)\n\n\treturn strings.Join(ss, \"\\n---\\n\")\n}\n\ntype BanPostHandler struct {\n\tdao_interfaces.MessageHandler\n}\n\nfunc NewBanPostHandler() *BanPostHandler {\n\treturn &BanPostHandler{}\n}\n\nfunc (h *BanPostHandler) Execute(iMsg dao_interfaces.ExecutableMessage) {\n\tmsg := iMsg.(*ExecutableMessageBanPost)\n\tBanPost(msg.FeedID, msg.PostID, msg.Reason)\n}\n\nfunc (h BanPostHandler) Type() string {\n\treturn ExecutableMessageBanPost{}.Type()\n}\n\nfunc (h *BanPostHandler) MessageFromJSON(ast *ujson.JSONASTNode) dao_interfaces.ExecutableMessage {\n\tmsg := &ExecutableMessageBanPost{}\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"feedId\", Value: &msg.FeedID},\n\t\t{Key: \"postId\", Value: &msg.PostID},\n\t\t{Key: \"reason\", Value: &msg.Reason},\n\t})\n\treturn msg\n}\n" + }, + { + "Name": "misc.gno", + "Body": "package social_feeds\n\nimport (\n\t\"encoding/base64\"\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/r/demo/users\"\n)\n\nfunc getFeed(fid FeedID) *Feed {\n\tfidkey := feedIDKey(fid)\n\tfeed_, exists := gFeeds.Get(fidkey)\n\tif !exists {\n\t\treturn nil\n\t}\n\tfeed := feed_.(*Feed)\n\treturn feed\n}\n\nfunc mustGetFeed(fid FeedID) *Feed {\n\tfeed := getFeed(fid)\n\tif feed == nil {\n\t\tpanic(\"Feed does not exist\")\n\t}\n\treturn feed\n}\n\nfunc incGetFeedID() FeedID {\n\tgFeedsCtr++\n\treturn FeedID(gFeedsCtr)\n}\n\nfunc usernameOf(addr std.Address) string {\n\tuser := users.GetUserByAddress(addr)\n\tif user == nil {\n\t\treturn \"\"\n\t} else {\n\t\treturn user.Name\n\t}\n}\n\nfunc feedIDKey(fid FeedID) string {\n\treturn padZero(uint64(fid), 10)\n}\n\nfunc postIDKey(pid PostID) string {\n\treturn padZero(uint64(pid), 10)\n}\n\nfunc padLeft(str string, length int) string {\n\tif len(str) >= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\" \", length-len(str)) + str\n\t}\n}\n\nfunc padZero(u64 uint64, length int) string {\n\tstr := strconv.Itoa(int(u64))\n\tif len(str) >= length {\n\t\treturn str\n\t} else {\n\t\treturn strings.Repeat(\"0\", length-len(str)) + str\n\t}\n}\n\nfunc bytesToString(b []byte) string {\n\treturn base64.RawURLEncoding.EncodeToString(b)\n}\n\nfunc intToString(val int) string {\n\treturn strconv.Itoa(val)\n}\n" + }, + { + "Name": "post.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n\tujson \"gno.land/p/demo/teritori/ujson\"\n)\n\ntype PostID uint64\n\nfunc (pid PostID) String() string {\n\treturn strconv.Itoa(int(pid))\n}\n\nfunc (pid *PostID) FromJSON(ast *ujson.JSONASTNode) {\n\tval, err := strconv.Atoi(ast.Value)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t*pid = PostID(val)\n}\n\nfunc (pid PostID) ToJSON() string {\n\treturn strconv.Itoa(int(pid))\n}\n\ntype Reaction struct {\n\ticon string\n\tcount uint64\n}\n\nvar Categories []string = []string{\n\t\"Reaction\",\n\t\"Comment\",\n\t\"Normal\",\n\t\"Article\",\n\t\"Picture\",\n\t\"Audio\",\n\t\"Video\",\n}\n\ntype Post struct {\n\tid PostID\n\tparentID PostID\n\tfeedID FeedID\n\tcategory uint64\n\tmetadata string\n\treactions avl.Tree // icon -> count\n\tcomments avl.Tree // Post.id -> *Post\n\tcreator std.Address\n\ttipAmount uint64\n\tdeleted bool\n\tcommentsCount uint64\n\n\tcreatedAt int64\n\tupdatedAt int64\n\tdeletedAt int64\n}\n\nfunc newPost(feed *Feed, id PostID, creator std.Address, parentID PostID, category uint64, metadata string) *Post {\n\treturn &Post{\n\t\tid: id,\n\t\tparentID: parentID,\n\t\tfeedID: feed.id,\n\t\tcategory: category,\n\t\tmetadata: metadata,\n\t\treactions: avl.Tree{},\n\t\tcreator: creator,\n\t\tcreatedAt: time.Now().Unix(),\n\t}\n}\n\nfunc (post *Post) String() string {\n\treturn post.ToJSON()\n}\n\nfunc (post *Post) Update(category uint64, metadata string) {\n\tpost.category = category\n\tpost.metadata = metadata\n\tpost.updatedAt = time.Now().Unix()\n}\n\nfunc (post *Post) Delete() {\n\tpost.deleted = true\n\tpost.deletedAt = time.Now().Unix()\n}\n\nfunc (post *Post) Tip(from std.Address, to std.Address) {\n\treceivedCoins := std.GetOrigSend()\n\tamount := receivedCoins[0].Amount\n\n\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n\t// banker := std.GetBanker(std.BankerTypeRealmSend)\n\tcoinsToSend := std.Coins{std.Coin{Denom: \"ugnot\", Amount: amount}}\n\tpkgaddr := std.GetOrigPkgAddr()\n\n\tbanker.SendCoins(pkgaddr, to, coinsToSend)\n\n\t// Update tip amount\n\tpost.tipAmount += uint64(amount)\n}\n\n// Always remove reaction if count = 0\nfunc (post *Post) React(icon string, up bool) {\n\tcount_, ok := post.reactions.Get(icon)\n\tcount := 0\n\n\tif ok {\n\t\tcount = count_.(int)\n\t}\n\n\tif up {\n\t\tcount++\n\t} else {\n\t\tcount--\n\t}\n\n\tif count <= 0 {\n\t\tpost.reactions.Remove(icon)\n\t} else {\n\t\tpost.reactions.Set(icon, count)\n\t}\n}\n\nfunc (post *Post) Render() string {\n\treturn post.metadata\n}\n\nfunc (post *Post) FromJSON(jsonData string) {\n\tast := ujson.TokenizeAndParse(jsonData)\n\tast.ParseObject([]*ujson.ParseKV{\n\t\t{Key: \"id\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tpid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.id = PostID(pid)\n\t\t}},\n\t\t{Key: \"parentID\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tpid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.parentID = PostID(pid)\n\t\t}},\n\t\t{Key: \"feedID\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\tfid, _ := strconv.Atoi(node.Value)\n\t\t\tpost.feedID = FeedID(fid)\n\t\t}},\n\t\t{Key: \"category\", Value: &post.category},\n\t\t{Key: \"metadata\", Value: &post.metadata},\n\t\t{Key: \"reactions\", CustomParser: func(node *ujson.JSONASTNode) {\n\t\t\treactions := avl.NewTree()\n\t\t\tfor _, child := range node.ObjectChildren {\n\t\t\t\treactionCount := child.Value\n\t\t\t\treactions.Set(child.Key, reactionCount)\n\t\t\t}\n\t\t\tpost.reactions = *reactions\n\t\t}},\n\t\t{Key: \"commentsCount\", Value: &post.commentsCount},\n\t\t{Key: \"creator\", Value: &post.creator},\n\t\t{Key: \"tipAmount\", Value: &post.tipAmount},\n\t\t{Key: \"deleted\", Value: &post.deleted},\n\t\t{Key: \"createdAt\", Value: &post.createdAt},\n\t\t{Key: \"updatedAt\", Value: &post.updatedAt},\n\t\t{Key: \"deletedAt\", Value: &post.deletedAt},\n\t})\n}\n\nfunc (post *Post) ToJSON() string {\n\treactionsKV := []ujson.FormatKV{}\n\tpost.reactions.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tcount := value.(int)\n\t\tdata := ujson.FormatKV{Key: key, Value: count}\n\t\treactionsKV = append(reactionsKV, data)\n\t\treturn false\n\t})\n\treactions := ujson.FormatObject(reactionsKV)\n\n\tpostJSON := ujson.FormatObject([]ujson.FormatKV{\n\t\t{Key: \"id\", Value: uint64(post.id)},\n\t\t{Key: \"parentID\", Value: uint64(post.parentID)},\n\t\t{Key: \"feedID\", Value: uint64(post.feedID)},\n\t\t{Key: \"category\", Value: post.category},\n\t\t{Key: \"metadata\", Value: post.metadata},\n\t\t{Key: \"reactions\", Value: reactions, Raw: true},\n\t\t{Key: \"creator\", Value: post.creator},\n\t\t{Key: \"tipAmount\", Value: post.tipAmount},\n\t\t{Key: \"deleted\", Value: post.deleted},\n\t\t{Key: \"commentsCount\", Value: post.commentsCount},\n\t\t{Key: \"createdAt\", Value: post.createdAt},\n\t\t{Key: \"updatedAt\", Value: post.updatedAt},\n\t\t{Key: \"deletedAt\", Value: post.deletedAt},\n\t})\n\treturn postJSON\n}\n" + }, + { + "Name": "public.gno", + "Body": "package social_feeds\n\nimport (\n\t\"std\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"gno.land/p/demo/avl\"\n\t\"gno.land/p/demo/teritori/flags_index\"\n\t\"gno.land/p/demo/ufmt\"\n)\n\n// Only registered user can create a new feed\n// For the flexibility when testing, allow all user to create feed\nfunc CreateFeed(name string) FeedID {\n\tpkgpath := std.CurrentRealmPath()\n\n\tfid := incGetFeedID()\n\tcaller := std.PrevRealm().Addr()\n\turl := strings.Replace(pkgpath, \"gno.land\", \"\", -1) + \":\" + name\n\tfeed := newFeed(fid, url, name, caller)\n\tfidkey := feedIDKey(fid)\n\tgFeeds.Set(fidkey, feed)\n\tgFeedsByName.Set(name, feed)\n\treturn feed.id\n}\n\n// Anyone can create a post in a existing feed, allow un-registered users also\nfunc CreatePost(fid FeedID, parentID PostID, catetory uint64, metadata string) PostID {\n\tcaller := std.PrevRealm().Addr()\n\n\tfeed := mustGetFeed(fid)\n\tpost := feed.AddPost(caller, parentID, catetory, metadata)\n\treturn post.id\n}\n\n// Only post's owner can edit post\nfunc EditPost(fid FeedID, pid PostID, category uint64, metadata string) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tif caller != post.creator {\n\t\tpanic(\"you are not creator of this post\")\n\t}\n\n\tpost.Update(category, metadata)\n}\n\n// Only feed creator/owner can call this\nfunc SetOwner(fid FeedID, newOwner std.Address) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tif caller != feed.creator && caller != feed.owner {\n\t\tpanic(\"you are not creator/owner of this feed\")\n\t}\n\n\tfeed.owner = newOwner\n}\n\n// Only feed creator/owner or post creator can delete the post\nfunc DeletePost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tif caller != post.creator && caller != feed.creator && caller != feed.owner {\n\t\tpanic(\"you are nor creator of this post neither creator/owner of the feed\")\n\t}\n\n\tpost.Delete()\n\n\t// If post is comment then decrease comments count on parent\n\tif uint64(post.parentID) != 0 {\n\t\tparent := feed.MustGetPost(post.parentID)\n\t\tparent.commentsCount -= 1\n\t}\n}\n\n// Only feed owner can ban the post\nfunc BanPost(fid FeedID, pid PostID, reason string) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\t_ = feed.MustGetPost(pid)\n\n\t// For experimenting, we ban only the post for now\n\t// TODO: recursive delete/ban comments\n\tif caller != feed.owner {\n\t\tpanic(\"you are owner of the feed\")\n\t}\n\n\tfeed.BanPost(pid)\n\n\tfeed.flags.ClearFlagCount(getFlagID(fid, pid))\n}\n\n// Any one can react post\nfunc ReactPost(fid FeedID, pid PostID, icon string, up bool) {\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tpost.React(icon, up)\n}\n\nfunc TipPost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\tpost := feed.MustGetPost(pid)\n\n\tpost.Tip(caller, post.creator)\n}\n\n// Get a list of flagged posts\n// NOTE: We can support multi feeds in the future but for now we will have only 1 feed\n// Return stringified list in format: postStr-count,postStr-count\nfunc GetFlaggedPosts(fid FeedID, offset uint64, limit uint8) string {\n\tfeed := mustGetFeed(fid)\n\n\t// Already sorted by count descending\n\tflags := feed.flags.GetFlags(uint64(limit), offset)\n\n\tvar postList []string\n\tfor _, flagCount := range flags {\n\t\tflagID := flagCount.FlagID\n\n\t\tfeedID, postID := parseFlagID(flagID)\n\t\tif feedID != feed.id {\n\t\t\tcontinue\n\t\t}\n\n\t\tpost := feed.GetPost(postID)\n\t\tpostList = append(postList, ufmt.Sprintf(\"%s\", post))\n\t}\n\n\tSEPARATOR := \",\"\n\tres := strings.Join(postList, SEPARATOR)\n\treturn ufmt.Sprintf(\"[%s]\", res)\n}\n\n// NOTE: due to bug of std.PrevRealm().Addr() return \"\" when query so we user this proxy function temporary\n// in waiting of correct behaviour of std.PrevRealm().Addr()\nfunc GetPosts(fid FeedID, parentID PostID, user string, categories []uint64, offset uint64, limit uint8) string {\n\tcaller := std.PrevRealm().Addr()\n\tdata := GetPostsWithCaller(fid, parentID, caller.String(), user, categories, offset, limit)\n\treturn data\n}\n\nfunc GetPostsWithCaller(fid FeedID, parentID PostID, callerAddrStr string, user string, categories []uint64, offset uint64, limit uint8) string {\n\t// Return flagged posts, we process flagged posts differently using FlagIndex\n\tif len(categories) == 1 && categories[0] == uint64(9) {\n\t\treturn GetFlaggedPosts(fid, offset, limit)\n\t}\n\n\t// BUG: normally std.PrevRealm().Addr() should return a value instead of empty\n\t// Fix is in progress on Gno side\n\tfeed := mustGetFeed(fid)\n\tposts := getPosts(feed, parentID, callerAddrStr, user, categories, offset, limit)\n\n\tSEPARATOR := \",\"\n\tvar postListStr []string\n\n\tfor _, post := range posts {\n\t\tpostListStr = append(postListStr, post.String())\n\t}\n\n\tres := strings.Join(postListStr, SEPARATOR)\n\treturn ufmt.Sprintf(\"[%s]\", res)\n}\n\n// user here is: filter by user\nfunc getPosts(feed *Feed, parentID PostID, callerAddrStr string, user string, categories []uint64, offset uint64, limit uint8) []*Post {\n\tcaller := std.Address(callerAddrStr)\n\n\tvar posts []*Post\n\tvar skipped uint64\n\n\t// Create an avlTree for optimizing the check\n\trequestedCategories := avl.NewTree()\n\tfor _, category := range categories {\n\t\tcatStr := strconv.FormatUint(category, 10)\n\t\trequestedCategories.Set(catStr, true)\n\t}\n\n\tfeed.posts.ReverseIterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tpost := value.(*Post)\n\n\t\tpostCatStr := strconv.FormatUint(post.category, 10)\n\n\t\t// NOTE: this search mechanism is not efficient, only for demo purpose\n\t\tif post.parentID == parentID && post.deleted == false {\n\t\t\tif requestedCategories.Size() > 0 && !requestedCategories.Has(postCatStr) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tif user != \"\" && std.Address(user) != post.creator {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Filter hidden post\n\t\t\tflagID := getFlagID(feed.id, post.id)\n\t\t\tif feed.flags.HasFlagged(flagID, callerAddrStr) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\t// Check if post is in hidden list\n\t\t\tvalue, exists := feed.hiddenPostsByUser.Get(caller.String())\n\t\t\tif exists {\n\t\t\t\thiddenPosts := value.(*avl.Tree)\n\t\t\t\t// If post.id exists in hiddenPosts tree => that post is hidden\n\t\t\t\tif hiddenPosts.Has(post.id.String()) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif skipped < offset {\n\t\t\t\tskipped++\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tposts = append(posts, post)\n\t\t}\n\n\t\tif len(posts) == int(limit) {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn posts\n}\n\n// Get comments list\nfunc GetComments(fid FeedID, parentID PostID, offset uint64, limit uint8) string {\n\treturn GetPosts(fid, parentID, \"\", []uint64{}, offset, limit)\n}\n\n// Get Post\nfunc GetPost(fid FeedID, pid PostID) string {\n\tfeed := mustGetFeed(fid)\n\n\tdata, ok := feed.posts.Get(postIDKey(pid))\n\tif !ok {\n\t\tpanic(\"Unable to get post\")\n\t}\n\n\tpost := data.(*Post)\n\treturn post.String()\n}\n\nfunc FlagPost(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.FlagPost(caller, pid)\n}\n\nfunc HidePostForMe(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.HidePostForUser(caller, pid)\n}\n\nfunc UnHidePostForMe(fid FeedID, pid PostID) {\n\tcaller := std.PrevRealm().Addr()\n\tfeed := mustGetFeed(fid)\n\n\tfeed.UnHidePostForUser(caller, pid)\n}\n\nfunc GetFlags(fid FeedID, limit uint64, offset uint64) string {\n\tfeed := mustGetFeed(fid)\n\n\ttype FlagCount struct {\n\t\tFlagID flags_index.FlagID\n\t\tCount uint64\n\t}\n\n\tflags := feed.flags.GetFlags(limit, offset)\n\n\tvar res []string\n\tfor _, flag := range flags {\n\t\tres = append(res, ufmt.Sprintf(\"%s:%d\", flag.FlagID, flag.Count))\n\t}\n\n\treturn strings.Join(res, \"|\")\n}\n\n// TODO: allow only creator to call\nfunc GetFeedByID(fid FeedID) *Feed {\n\treturn mustGetFeed(fid)\n}\n\n// TODO: allow only admin to call\nfunc ExportFeedData(fid FeedID) string {\n\tfeed := mustGetFeed(fid)\n\tfeedJSON := feed.ToJSON()\n\treturn feedJSON\n}\n\n// TODO: allow only admin to call\nfunc ImportFeedData(fid FeedID, jsonData string) {\n\tfeed := mustGetFeed(fid)\n\tfeed.FromJSON(jsonData)\n}\n\n// func MigrateFromPreviousFeed(fid feedsV7.FeedID) {\n// \t// Get exported data from previous feeds\n// \tjsonData := feedsV7.ExportFeedData(fid)\n// \tImportFeedData(FeedID(uint64(fid)), jsonData)\n// }\n" + }, + { + "Name": "render.gno", + "Body": "package social_feeds\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc renderFeed(parts []string) string {\n\t// /r/demo/social_feeds_v4:FEED_NAME\n\tname := parts[0]\n\tfeedI, exists := gFeedsByName.Get(name)\n\tif !exists {\n\t\treturn \"feed does not exist: \" + name\n\t}\n\treturn feedI.(*Feed).Render()\n}\n\nfunc renderPost(parts []string) string {\n\t// /r/demo/boards:FEED_NAME/POST_ID\n\tname := parts[0]\n\tfeedI, exists := gFeedsByName.Get(name)\n\tif !exists {\n\t\treturn \"feed does not exist: \" + name\n\t}\n\tpid, err := strconv.Atoi(parts[1])\n\tif err != nil {\n\t\treturn \"invalid thread id: \" + parts[1]\n\t}\n\tfeed := feedI.(*Feed)\n\tpost := feed.MustGetPost(PostID(pid))\n\treturn post.Render()\n}\n\nfunc renderFeedsList() string {\n\tstr := \"There are \" + intToString(gFeeds.Size()) + \" available feeds:\\n\\n\"\n\tgFeeds.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tfeed := value.(*Feed)\n\t\tstr += \" * [\" + feed.url + \" (FeedID: \" + feed.id.String() + \")](\" + feed.url + \")\\n\"\n\t\treturn false\n\t})\n\treturn str\n}\n\nfunc Render(path string) string {\n\tif path == \"\" {\n\t\treturn renderFeedsList()\n\t}\n\n\tparts := strings.Split(path, \"/\")\n\n\tif len(parts) == 1 {\n\t\t// /r/demo/social_feeds_v4:FEED_NAME\n\t\treturn renderFeed(parts)\n\t} else if len(parts) == 2 {\n\t\t// /r/demo/social_feeds_v4:FEED_NAME/POST_ID\n\t\treturn renderPost(parts)\n\t}\n\n\treturn \"Not found\"\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "100000000", + "gas_fee": "10000000ugnot" + }, + "signatures": [ + { + "pub_key": { + "@type": "/tm.PubKeySecp256k1", + "value": "A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y" + }, + "signature": "fg01rLWLymXHVn9fE9vNyo4i2idOAEJn6KsPnlMT5JdrWqjzLScI65JVpJJErQUQMdpx/LvBPNVG3Atv/VGekg==" + } + ], + "memo": "" +} + +-- bye/bye.gno -- +package bye + +import ( + "encoding/base64" +) + +func Call(s string) { + base64.StdEncoding.DecodeString(v) +} From a132452f1ae2f88bcf201cdbbf5f7e2839c930bb Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 3 Jul 2024 23:53:17 +0200 Subject: [PATCH 02/45] current status dump --- gno.land/pkg/gnoland/app.go | 15 +- gno.land/pkg/sdk/gnostore/store.go | 101 +++++++++ gno.land/pkg/sdk/vm/builtins.go | 46 ++-- gno.land/pkg/sdk/vm/keeper.go | 79 ++----- gnovm/pkg/gnolang/preprocess.go | 4 +- gnovm/pkg/gnolang/store.go | 329 ++++++++++++++++++++--------- 6 files changed, 390 insertions(+), 184 deletions(-) create mode 100644 gno.land/pkg/sdk/gnostore/store.go diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 8f5f33d0d9a..a57680d28fc 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -5,8 +5,11 @@ import ( "log/slog" "path/filepath" + "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -65,6 +68,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Capabilities keys. mainKey := store.NewStoreKey("main") baseKey := store.NewStoreKey("base") + gnoKey := store.NewStoreKey("gno") // Create BaseApp. // TODO: Add a consensus based min gas prices for the node, by default it does not check @@ -74,14 +78,17 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Set mounts for BaseApp's MultiStore. baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) + // XXX: Embed this ? + stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") + baseApp.MountStoreWithDB(gnoKey, gnostore.StoreConstructor(gnolang.NewAllocator(500*1000*1000), func(gs gnolang.Store) { + gs.SetPackageGetter(vm.PackageGetter(stdlibsDir)) + gs.SetNativeStore(stdlibs.NativeStore) + }), cfg.DB) // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - - // XXX: Embed this ? - stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") - vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) + vmKpr := vm.NewVMKeeper(gnoKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler)) diff --git a/gno.land/pkg/sdk/gnostore/store.go b/gno.land/pkg/sdk/gnostore/store.go new file mode 100644 index 00000000000..1cec5088149 --- /dev/null +++ b/gno.land/pkg/sdk/gnostore/store.go @@ -0,0 +1,101 @@ +// Package gnostore implements a tm2 store which can interoperate with the GnoVM's +// own store. +package gnostore + +import ( + "github.com/gnolang/gno/gnovm/pkg/gnolang" + dbm "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/cache" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" + "github.com/gnolang/gno/tm2/pkg/store/types" +) + +// StoreConstructor implements store.CommitStoreConstructor. +// It can be used in conjunction with CommitMultiStore.MountStoreWithDB. +// initialize should only contain basic setter for the immutable config +// (like SetNativeStore); it should not initialize packages. +func StoreConstructor(alloc *gnolang.Allocator, initialize func(gs gnolang.Store)) store.CommitStoreConstructor { + return func(db dbm.DB, opts types.StoreOptions) types.CommitStore { + iavlStore := iavl.StoreConstructor(db, opts) + base := dbadapter.StoreConstructor(db, opts) + gno := gnolang.NewStore(alloc, base, iavlStore) + initialize(gno) + return &Store{ + Store: iavlStore.(*iavl.Store), + opts: opts, + base: base.(dbadapter.Store), + gno: gno, + } + } +} + +func GetGnoStore(s types.Store) gnolang.Store { + gs, ok := s.(interface { + GnoStore() gnolang.Store + }) + if ok { + return gs.GnoStore() + } + return nil +} + +type Store struct { + *iavl.Store // iavl + + opts types.StoreOptions + base dbadapter.Store + gno gnolang.Store +} + +func (s *Store) GetStoreOptions() types.StoreOptions { return s.opts } + +func (s *Store) SetStoreOptions(opts2 types.StoreOptions) { + s.opts = opts2 + s.Store.SetStoreOptions(opts2) +} + +func (s *Store) GnoStore() gnolang.Store { return s.gno } + +type cacheStore struct { + types.Store + + base types.Store + gno gnolang.TransactionStore + rootGno gnolang.Store +} + +func (s *Store) CacheWrap() types.Store { + s2 := &cacheStore{ + Store: cache.New(s.Store), + base: cache.New(s.base), + rootGno: s.gno, + } + s2.gno = s.gno.BeginTransaction(s2.base, s2.Store) + return s2 +} + +func (store *cacheStore) Write() { + store.Store.Write() + store.base.Write() + store.gno.Write() +} + +func (store *cacheStore) Flush() { + store.Store.(types.Flusher).Flush() + store.base.(types.Flusher).Flush() + store.gno.Write() +} + +func (s *cacheStore) CacheWrap() types.Store { + s2 := &cacheStore{ + Store: cache.New(s.Store), + base: cache.New(s.base), + rootGno: s.rootGno, + } + s2.gno = s.rootGno.BeginTransaction(s2.base, s2.Store) + return s2 +} + +func (s *cacheStore) GnoStore() gnolang.Store { return s.gno } diff --git a/gno.land/pkg/sdk/vm/builtins.go b/gno.land/pkg/sdk/vm/builtins.go index cbf6df02e93..6b88b083d25 100644 --- a/gno.land/pkg/sdk/vm/builtins.go +++ b/gno.land/pkg/sdk/vm/builtins.go @@ -17,29 +17,31 @@ import ( // NOTE: native functions/methods added here must be quick operations, or // account for gas before operation. // TODO: define criteria for inclusion, and solve gas calculations(???). -func (vm *VMKeeper) getPackage(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { - // otherwise, built-in package value. - // first, load from filepath. - stdlibPath := filepath.Join(vm.stdlibsDir, pkgPath) - if !osm.DirExists(stdlibPath) { - // does not exist. - return nil, nil - } - memPkg := gno.ReadMemPackage(stdlibPath, pkgPath) - if memPkg.IsEmpty() { - // no gno files are present, skip this package - return nil, nil - } +func PackageGetter(stdlibsDir string) func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { + return func(pkgPath string, store gno.Store) (pn *gno.PackageNode, pv *gno.PackageValue) { + // otherwise, built-in package value. + // first, load from filepath. + stdlibPath := filepath.Join(stdlibsDir, pkgPath) + if !osm.DirExists(stdlibPath) { + // does not exist. + return nil, nil + } + memPkg := gno.ReadMemPackage(stdlibPath, pkgPath) + if memPkg.IsEmpty() { + // no gno files are present, skip this package + return nil, nil + } - m2 := gno.NewMachineWithOptions(gno.MachineOptions{ - PkgPath: "gno.land/r/stdlibs/" + pkgPath, - // PkgPath: pkgPath, - Output: os.Stdout, - Store: store, - }) - defer m2.Release() - pn, pv = m2.RunMemPackage(memPkg, true) - return + m2 := gno.NewMachineWithOptions(gno.MachineOptions{ + PkgPath: "gno.land/r/stdlibs/" + pkgPath, + // PkgPath: pkgPath, + Output: os.Stdout, + Store: store, + }) + defer m2.Release() + pn, pv = m2.RunMemPackage(memPkg, true) + return + } } // ---------------------------------------- diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 95f17ce09f0..db59f653479 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/errors" @@ -40,58 +41,43 @@ var _ VMKeeperI = &VMKeeper{} // VMKeeper holds all package code and store state. type VMKeeper struct { - baseKey store.StoreKey - iavlKey store.StoreKey - acck auth.AccountKeeper - bank bank.BankKeeper - stdlibsDir string - - // cached, the DeliverTx persistent state. - gnoStore gno.Store + gnoStoreKey store.StoreKey + acck auth.AccountKeeper + bank bank.BankKeeper maxCycles int64 // max allowed cylces on VM executions } // NewVMKeeper returns a new VMKeeper. func NewVMKeeper( - baseKey store.StoreKey, - iavlKey store.StoreKey, + gnoStoreKey store.StoreKey, acck auth.AccountKeeper, bank bank.BankKeeper, - stdlibsDir string, maxCycles int64, ) *VMKeeper { // TODO: create an Options struct to avoid too many constructor parameters vmk := &VMKeeper{ - baseKey: baseKey, - iavlKey: iavlKey, - acck: acck, - bank: bank, - stdlibsDir: stdlibsDir, - maxCycles: maxCycles, + gnoStoreKey: gnoStoreKey, + acck: acck, + bank: bank, + maxCycles: maxCycles, } return vmk } func (vm *VMKeeper) Initialize(ms store.MultiStore) { - if vm.gnoStore != nil { - panic("should not happen") - } - alloc := gno.NewAllocator(maxAllocTx) - baseSDKStore := ms.GetStore(vm.baseKey) - iavlSDKStore := ms.GetStore(vm.iavlKey) - vm.gnoStore = gno.NewStore(alloc, baseSDKStore, iavlSDKStore) - vm.gnoStore.SetPackageGetter(vm.getPackage) - vm.gnoStore.SetNativeStore(stdlibs.NativeStore) - if vm.gnoStore.NumMemPackages() > 0 { + gnoStore := gnostore.GetGnoStore(ms.GetStore(vm.gnoStoreKey)) + + if gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. // TODO remove this, and generally solve for in-mem garbage collection // and memory management across many objects/types/nodes/packages. + gnoStoreTx := gnoStore.BeginTransaction(nil, nil) m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", Output: os.Stdout, // XXX - Store: vm.gnoStore, + Store: gnoStoreTx, }) defer m2.Release() gno.DisableDebug() @@ -101,39 +87,12 @@ func (vm *VMKeeper) Initialize(ms store.MultiStore) { } func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { - // construct main store if nil. - if vm.gnoStore == nil { - panic("VMKeeper must first be initialized") - } - switch ctx.Mode() { - case sdk.RunTxModeDeliver: - // swap sdk store of existing store. - // this is needed due to e.g. gas wrappers. - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - vm.gnoStore.SwapStores(baseSDKStore, iavlSDKStore) - // clear object cache for every transaction. - // NOTE: this is inefficient, but simple. - // in the future, replace with more advanced caching strategy. - vm.gnoStore.ClearObjectCache() - return vm.gnoStore - case sdk.RunTxModeCheck: - // For query??? XXX Why not RunTxModeQuery? - simStore := vm.gnoStore.Fork() - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - simStore.SwapStores(baseSDKStore, iavlSDKStore) - return simStore - case sdk.RunTxModeSimulate: - // always make a new store for simulate for isolation. - simStore := vm.gnoStore.Fork() - baseSDKStore := ctx.Store(vm.baseKey) - iavlSDKStore := ctx.Store(vm.iavlKey) - simStore.SwapStores(baseSDKStore, iavlSDKStore) - return simStore - default: - panic("should not happen") + sto := ctx.MultiStore().GetStore(vm.gnoStoreKey) + gs := gnostore.GetGnoStore(sto) + if gs == nil { + panic("could not get gno store") } + return gs } // AddPackage adds a package with given fileset. diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index c830d1a18f0..11258b259a0 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2084,7 +2084,7 @@ func evalStaticType(store Store, last BlockNode, x Expr) Type { // See comment in evalStaticTypeOfRaw. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.Fork() + store = store.preprocessFork() store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) @@ -2158,7 +2158,7 @@ func evalStaticTypeOfRaw(store Store, last BlockNode, x Expr) (t Type) { // yet predefined this time around. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.Fork() + store = store.preprocessFork() store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 12666c3d7ad..96160ddc8b4 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -12,7 +12,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/colors" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" - "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/gnolang/gno/tm2/pkg/store/utils" stringz "github.com/gnolang/gno/tm2/pkg/strings" ) @@ -32,8 +31,12 @@ type PackageInjector func(store Store, pn *PackageNode) // NativeStore is a function which can retrieve native bodies of native functions. type NativeStore func(pkgName string, name Name) func(m *Machine) +// Store is the central interface that specifies the communications between the +// GnoVM and the underlying data store; currently, generally the Gno.land +// blockchain, or the file system. type Store interface { // STABLE + BeginTransaction(baseStore, iavlStore store.Store) TransactionStore SetPackageGetter(PackageGetter) GetPackage(pkgPath string, isImport bool) *PackageValue SetCachePackage(*PackageValue) @@ -62,9 +65,6 @@ type Store interface { GetMemPackage(path string) *std.MemPackage GetMemFile(path string, name string) *std.MemFile IterMemPackage() <-chan *std.MemPackage - ClearObjectCache() // for each delivertx. - Fork() Store // for checktx, simulate, and queries. - SwapStores(baseStore, iavlStore store.Store) // for gas wrappers. SetPackageInjector(PackageInjector) // for natives SetNativeStore(NativeStore) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX @@ -73,50 +73,218 @@ type Store interface { LogSwitchRealm(rlmpath string) // to mark change of realm boundaries ClearCache() Print() + + preprocessFork() Store +} + +// TransactionStore is a store where the operations modifying the underlying store's +// caches are temporarily held in a buffer, and then executed together after +// executing Write. +type TransactionStore interface { + Store + + // Write commits the current buffered transaction data to the underlying store. + // It also clears the current buffer of the transaction. Write() - Flush() } -// Used to keep track of in-mem objects during tx. type defaultStore struct { - alloc *Allocator // for accounting for cached items - pkgGetter PackageGetter // non-realm packages - cacheObjects map[ObjectID]Object - cacheTypes map[TypeID]Type - cacheNodes map[Location]BlockNode - cacheNativeTypes map[reflect.Type]Type // go spec: reflect.Type are comparable - baseStore store.Store // for objects, types, nodes - iavlStore store.Store // for escaped object hashes + // underlying stores used to keep data + baseStore store.Store // for objects, types, nodes + iavlStore store.Store // for escaped object hashes + + // transaction-scoped + parentStore *defaultStore // set only during transactions. + cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. + cacheTypes bufferedTxMap[TypeID, Type] // this re-uses the parent store's. + cacheNodes bufferedTxMap[Location, BlockNode] // until BlockNode persistance is implemented, this is an actual store. + alloc *Allocator // for accounting for cached items + + // store configuration; cannot be modified in a transaction + pkgGetter PackageGetter // non-realm packages + cacheNativeTypes map[reflect.Type]Type // reflect doc: reflect.Type are comparable pkgInjector PackageInjector // for injecting natives nativeStore NativeStore // for injecting natives go2gnoStrict bool // if true, native->gno type conversion must be registered. + // XXX panic when changing these and parentStore != nil // transient - opslog []StoreOp // for debugging and testing. current []string // for detecting import cycles. + opslog []StoreOp // for debugging and testing. +} + +type bufferedTxMap[K comparable, V any] struct { + source map[K]V + dirty map[K]deletable[V] +} + +// init should be called when creating the bufferedTxMap, in a non-buffer +// context. +func (b *bufferedTxMap[K, V]) init() { + b.source = make(map[K]V) +} + +// buffered creates a copy of b, which has a usable dirty map. +func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { + if b.dirty != nil { + panic("cannot stack buffered tx maps") + } + return bufferedTxMap[K, V]{ + source: b.source, + dirty: make(map[K]deletable[V]), + } +} + +// write commits the data in dirty to the map in source. +func (b *bufferedTxMap[K, V]) write() { + for k, v := range b.dirty { + if v.deleted { + delete(b.source, k) + } else { + b.source[k] = v.v + } + } + b.dirty = make(map[K]deletable[V]) +} + +func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { + if b.dirty != nil { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + } + v, ok := b.source[k] + return v, ok +} + +func (b bufferedTxMap[K, V]) Set(k K, v V) { + if b.dirty == nil { + b.source[k] = v + return + } + b.dirty[k] = deletable[V]{v: v} +} + +func (b bufferedTxMap[K, V]) Delete(k K) { + if b.dirty == nil { + delete(b.source, k) + return + } + b.dirty[k] = deletable[V]{deleted: true} +} + +type deletable[V any] struct { + v V + deleted bool } func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore { ds := &defaultStore{ - alloc: alloc, + baseStore: baseStore, + iavlStore: iavlStore, + alloc: alloc, + + // cacheObjects is set; objects in the store will be copied over for any transaction. + cacheObjects: make(map[ObjectID]Object), + + // store configuration pkgGetter: nil, - cacheObjects: make(map[ObjectID]Object), - cacheTypes: make(map[TypeID]Type), - cacheNodes: make(map[Location]BlockNode), cacheNativeTypes: make(map[reflect.Type]Type), - baseStore: baseStore, - iavlStore: iavlStore, + pkgInjector: nil, + nativeStore: nil, go2gnoStrict: true, } + ds.cacheTypes.init() + ds.cacheNodes.init() InitStoreCaches(ds) return ds } +// If nil baseStore and iavlStore, the baseStores are re-used. +func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) TransactionStore { + if baseStore == nil { + baseStore = ds.baseStore + } + if iavlStore == nil { + iavlStore = ds.iavlStore + } + ds2 := &defaultStore{ + // underlying stores + baseStore: baseStore, + iavlStore: iavlStore, + + // transaction-scoped + parentStore: ds, + cacheObjects: maps.Clone(ds.cacheObjects), + cacheTypes: ds.cacheTypes.buffered(), + cacheNodes: ds.cacheNodes.buffered(), + alloc: ds.alloc.Fork().Reset(), + + // store configuration + pkgGetter: ds.pkgGetter, + cacheNativeTypes: ds.cacheNativeTypes, + pkgInjector: ds.pkgInjector, + nativeStore: ds.nativeStore, + go2gnoStrict: ds.go2gnoStrict, + + // transient + current: nil, + opslog: nil, + } + return transactionStore{ds2} +} + +func (ds *defaultStore) preprocessFork() Store { + // XXX IMPROVE + ds2 := &defaultStore{ + // underlying stores + baseStore: ds.baseStore, + iavlStore: ds.iavlStore, + + // transaction-scoped + parentStore: ds, + cacheObjects: make(map[ObjectID]Object), + cacheTypes: ds.cacheTypes, + cacheNodes: ds.cacheNodes, + alloc: ds.alloc.Fork().Reset(), + + // store configuration + pkgGetter: ds.pkgGetter, + cacheNativeTypes: ds.cacheNativeTypes, + pkgInjector: ds.pkgInjector, + nativeStore: ds.nativeStore, + go2gnoStrict: ds.go2gnoStrict, + + // transient + current: nil, + opslog: nil, + } + ds2.SetCachePackage(Uverse()) + return ds2 +} + +type transactionStore struct{ *defaultStore } + +func (t transactionStore) Write() { t.write() } + +// writes to parentStore. +func (ds *defaultStore) write() { + ds.cacheTypes.write() + ds.cacheNodes.write() +} + func (ds *defaultStore) GetAllocator() *Allocator { return ds.alloc } func (ds *defaultStore) SetPackageGetter(pg PackageGetter) { + if ds.parentStore != nil { + panic("package getter cannot be modified in a transaction") + } ds.pkgGetter = pg } @@ -394,7 +562,7 @@ func (ds *defaultStore) GetType(tid TypeID) Type { func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { // check cache. - if tt, exists := ds.cacheTypes[tid]; exists { + if tt, exists := ds.cacheTypes.Get(tid); exists { return tt } // check backend. @@ -411,7 +579,7 @@ func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { } } // set in cache. - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) // after setting in cache, fill tt. fillType(ds, tt) return tt @@ -422,7 +590,7 @@ func (ds *defaultStore) GetTypeSafe(tid TypeID) Type { func (ds *defaultStore) SetCacheType(tt Type) { tid := tt.TypeID() - if tt2, exists := ds.cacheTypes[tid]; exists { + if tt2, exists := ds.cacheTypes.Get(tid); exists { if tt != tt2 { // NOTE: not sure why this would happen. panic("should not happen") @@ -430,14 +598,14 @@ func (ds *defaultStore) SetCacheType(tt Type) { // already set. } } else { - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) } } func (ds *defaultStore) SetType(tt Type) { tid := tt.TypeID() // return if tid already known. - if tt2, exists := ds.cacheTypes[tid]; exists { + if tt2, exists := ds.cacheTypes.Get(tid); exists { if tt != tt2 { // this can happen for a variety of reasons. // TODO classify them and optimize. @@ -452,7 +620,7 @@ func (ds *defaultStore) SetType(tt Type) { ds.baseStore.Set([]byte(key), bz) } // save type to cache. - ds.cacheTypes[tid] = tt + ds.cacheTypes.Set(tid, tt) } func (ds *defaultStore) GetBlockNode(loc Location) BlockNode { @@ -465,7 +633,7 @@ func (ds *defaultStore) GetBlockNode(loc Location) BlockNode { func (ds *defaultStore) GetBlockNodeSafe(loc Location) BlockNode { // check cache. - if bn, exists := ds.cacheNodes[loc]; exists { + if bn, exists := ds.cacheNodes.Get(loc); exists { return bn } // check backend. @@ -481,7 +649,7 @@ func (ds *defaultStore) GetBlockNodeSafe(loc Location) BlockNode { loc, bn.GetLocation())) } } - ds.cacheNodes[loc] = bn + ds.cacheNodes.Set(loc, bn) return bn } } @@ -500,7 +668,7 @@ func (ds *defaultStore) SetBlockNode(bn BlockNode) { // ds.backend.Set([]byte(key), bz) } // save node to cache. - ds.cacheNodes[loc] = bn + ds.cacheNodes.Set(loc, bn) // XXX duplicate? // XXX } @@ -550,12 +718,12 @@ func (ds *defaultStore) AddMemPackage(memPkg *std.MemPackage) { // GetMemPackage retrieves the MemPackage at the given path. // It returns nil if the package could not be found. func (ds *defaultStore) GetMemPackage(path string) *std.MemPackage { - return ds.getMemPackage(path, false) + return getMemPackage(ds, ds.iavlStore, path, false) } -func (ds *defaultStore) getMemPackage(path string, isRetry bool) *std.MemPackage { +func getMemPackage(store Store, iavlStore store.Store, path string, isRetry bool) *std.MemPackage { pathkey := []byte(backendPackagePathKey(path)) - bz := ds.iavlStore.Get(pathkey) + bz := iavlStore.Get(pathkey) if bz == nil { // If this is the first try, attempt using GetPackage to retrieve the // package, first. GetPackage can leverage pkgGetter, which in most @@ -563,12 +731,13 @@ func (ds *defaultStore) getMemPackage(path string, isRetry bool) *std.MemPackage // which would add the package to the store after running. // Some packages may never be persisted, thus why we only attempt this twice. if !isRetry { - if pv := ds.GetPackage(path, false); pv != nil { - return ds.getMemPackage(path, true) + if pv := store.GetPackage(path, false); pv != nil { + return getMemPackage(store, iavlStore, path, true) } } return nil } + var memPkg *std.MemPackage amino.MustUnmarshal(bz, &memPkg) return memPkg @@ -614,51 +783,6 @@ func (ds *defaultStore) IterMemPackage() <-chan *std.MemPackage { } } -// Unstable. -// This function is used to clear the object cache every transaction. -// It also sets a new allocator. -func (ds *defaultStore) ClearObjectCache() { - ds.alloc.Reset() - ds.cacheObjects = make(map[ObjectID]Object) // new cache. - ds.opslog = nil // new ops log. - ds.SetCachePackage(Uverse()) -} - -// Unstable. -// This function is used to handle queries and checktx transactions. -func (ds *defaultStore) Fork() Store { - ds2 := &defaultStore{ - alloc: ds.alloc.Fork().Reset(), - - // Re-initialize caches. Some are cloned for speed. - cacheObjects: make(map[ObjectID]Object), - cacheTypes: maps.Clone(ds.cacheTypes), - // XXX: This is bad to say the least (ds.cacheNodes is shared with a - // child Store); however, cacheNodes is _not_ a cache, but a proper - // data store instead. SetBlockNode does not write anything to - // the underlying baseStore, and cloning this map makes everything run - // 4x slower, so here we are, copying the reference. - cacheNodes: ds.cacheNodes, - cacheNativeTypes: maps.Clone(ds.cacheNativeTypes), - - // baseStore and iavlStore should generally be changed using SwapStores. - baseStore: ds.baseStore, - iavlStore: ds.iavlStore, - - // native injections / store "config" - pkgGetter: ds.pkgGetter, - pkgInjector: ds.pkgInjector, - nativeStore: ds.nativeStore, - go2gnoStrict: ds.go2gnoStrict, - - // reset opslog and current. - opslog: nil, - current: nil, - } - ds2.SetCachePackage(Uverse()) - return ds2 -} - // TODO: consider a better/faster/simpler way of achieving the overall same goal? func (ds *defaultStore) SwapStores(baseStore, iavlStore store.Store) { ds.baseStore = baseStore @@ -680,18 +804,6 @@ func (ds *defaultStore) GetNative(pkgPath string, name Name) func(m *Machine) { return nil } -// Writes one level of cache to store. -func (ds *defaultStore) Write() { - ds.baseStore.(types.Writer).Write() - ds.iavlStore.(types.Writer).Write() -} - -// Flush cached writes to disk. -func (ds *defaultStore) Flush() { - ds.baseStore.(types.Flusher).Flush() - ds.iavlStore.(types.Flusher).Flush() -} - // ---------------------------------------- // StoreOp @@ -761,9 +873,12 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { } func (ds *defaultStore) ClearCache() { + if ds.parentStore != nil { + panic("ClearCache can only be called on non-transactional stores") + } ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes = make(map[TypeID]Type) - ds.cacheNodes = make(map[Location]BlockNode) + ds.cacheTypes.init() + ds.cacheNodes.init() ds.cacheNativeTypes = make(map[reflect.Type]Type) // restore builtin types to cache. InitStoreCaches(ds) @@ -779,16 +894,38 @@ func (ds *defaultStore) Print() { utils.Print(ds.iavlStore) fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheTypes...")) - for tid, typ := range ds.cacheTypes { + for tid, typ := range ds.cacheTypes.source { fmt.Printf("- %v: %v\n", tid, stringz.TrimN(fmt.Sprintf("%v", typ), 50)) } + if len(ds.cacheTypes.dirty) > 0 { + fmt.Println(colors.Green("defaultStore:cacheTypes (pending)...")) + for tid, typ := range ds.cacheTypes.dirty { + if typ.deleted { + fmt.Printf("- %v (deleted)\n", tid) + } else { + fmt.Printf("- %v: %v\n", tid, + stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) + } + } + } fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheNodes...")) - for loc, bn := range ds.cacheNodes { + for loc, bn := range ds.cacheNodes.source { fmt.Printf("- %v: %v\n", loc, stringz.TrimN(fmt.Sprintf("%v", bn), 50)) } + if len(ds.cacheNodes.dirty) > 0 { + fmt.Println(colors.Green("defaultStore:cacheNodes (pending)...")) + for tid, typ := range ds.cacheNodes.dirty { + if typ.deleted { + fmt.Printf("- %v (deleted)\n", tid) + } else { + fmt.Printf("- %v: %v\n", tid, + stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) + } + } + } fmt.Println(colors.Red("//----------------------------------------")) } From bf9a6924b7455528833a9e3f73488fa41d2e2feb Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Thu, 18 Jul 2024 14:28:35 -0700 Subject: [PATCH 03/45] txtar for test4 issue --- .../testdata/restart_missing_type.txtar | 198 ++++++++++++++++++ gno.land/pkg/gnoland/node_inmemory.go | 8 +- .../pkg/integration/testing_integration.go | 26 ++- gno.land/pkg/sdk/vm/keeper.go | 41 ++-- 4 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/restart_missing_type.txtar diff --git a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar new file mode 100644 index 00000000000..f0c551e62a3 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar @@ -0,0 +1,198 @@ +loadpkg gno.land/p/demo/avl +gnoland start + +gnokey sign -tx-path $WORK/tx1.tx -chainid tendermint_test -account-sequence 0 test1 +! gnokey broadcast $WORK/tx1.tx +stderr 'out of gas' + +gnokey sign -tx-path $WORK/tx2.tx -chainid tendermint_test -account-sequence 1 test1 +gnokey broadcast $WORK/tx2.tx +stdout 'OK!' + +gnokey sign -tx-path $WORK/tx3.tx -chainid tendermint_test -account-sequence 2 test1 +gnokey broadcast $WORK/tx3.tx +stdout 'OK!' + +gnoland restart + +-- tx1.tx -- +{ + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "zentasktic", + "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic", + "files": [ + { + "name": "README.md", + "body": "# ZenTasktic Core\n\nA basic, minimalisitc Asess-Decide-Do implementations as `p/zentasktic`. The diagram below shows a simplified ADD workflow.\n\n![ZenTasktic](ZenTasktic-framework.png)\n\nThis implementation will expose all the basic features of the framework: tasks & projects with complete workflows. Ideally, this should offer all the necessary building blocks for any other custom implementation.\n\n## Object Definitions and Default Values\n\nAs an unopinionated ADD workflow, `zentastic_core` defines the following objects:\n\n- Realm\n\nRealms act like containers for tasks & projects during their journey from Assess to Do, via Decide. Each realm has a certain restrictions, e.g. a task's Body can only be edited in Assess, a Context, Due date and Alert can only be added in Decide, etc.\n\nIf someone observes different realms, there is support for adding and removing arbitrary Realms.\n\n_note: the Ids between 1 and 4 are reserved for: 1-Assess, 2-Decide, 3-Do, 4-Collection. Trying to add or remove such a Realm will raise an error._\n\n\nRealm data definition:\n\n```\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n```\n\n- Task\n\nA task is the minimal data structure in ZenTasktic, with the following definition:\n\n```\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n```\n\n- Project\n\nProjects are unopinionated collections of Tasks. A Task in a Project can be in any Realm, but the restrictions are propagated upwards to the Project: e.g. if a Task is marked as 'done' in the Do realm (namely changing its RealmId property to \"1\", Assess, or \"4\" Collection), and the rest of the tasks are not, the Project cannot be moved back to Decide or Asses, all Tasks must have consisted RealmId properties.\n\nA Task can be arbitrarily added to, removed from and moved to another Project.\n\nProject data definition:\n\n\n```\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"ProjectDue\"`\n}\n```\n\n\n- Context\n\nContexts act as tags, grouping together Tasks and Project, e.g. \"Backend\", \"Frontend\", \"Marketing\". Contexts have no defaults and can be added or removed arbitrarily.\n\nContext data definition:\n\n```\ntype Context struct {\n\tId \t\t\tstring `json:\"contextId\"`\n\tName \t\tstring `json:\"contextName\"`\n}\n```\n\n- Collection\n\nCollections are intended as an agnostic storage for Tasks & Projects which are either not ready to be Assessed, or they have been already marked as done, and, for whatever reason, they need to be kept in the system. There is a special Realm Id for Collections, \"4\", although technically they are not part of the Assess-Decide-Do workflow.\n\nCollection data definition:\n\n```\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n```\n\n- ObjectPath\n\nObjectPaths are minimalistic representations of the journey taken by a Task or a Project in the Assess-Decide-Do workflow. By recording their movement between various Realms, one can extract their `ZenStatus`, e.g., if a Task has been moved many times between Assess and Decide, never making it to Do, we can infer the following:\n-- either the Assess part was incomplete\n-- the resources needed for that Task are not yet ready\n\nObjectPath data definition:\n\n```\ntype ObjectPath struct {\n\tObjectType\tstring `json:\"objectType\"` // Task, Project\n\tId \t\t\tstring `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId \tstring `json:\"realmId\"`\n}\n```\n\n_note: the core implementation offers the basic adding and retrieving functionality, but it's up to the client realm using the `zentasktic` package to call them when an object is moved from one Realm to another._\n\n## Example Workflow\n\n```\npackage example_zentasktic\n\nimport \"gno.land/p/demo/zentasktic\"\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n}\n\n// initializing a task, assuming we get the value POSTed by some call to the current realm\n\nnewTask := zentasktic.Task{Id: \"20\", Body: \"Buy milk\"}\nztm.AddTask(newTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n...\n\neditedTask := zentasktic.Task{Id: \"20\", Body: \"Buy fresh milk\"}\nztm.EditTask(editedTask)\n\n...\n\n// moving it to Decide\n\nztm.MoveTaskToRealm(\"20\", \"2\")\n\n// adding context, due date and alert, assuming they're received from other calls\n\nshoppingContext := zcm.GetContextById(\"2\")\n\ncerr := zcm.AddContextToTask(ztm, shoppingContext, editedTask)\n\nderr := ztm.SetTaskDueDate(editedTask.Id, \"2024-04-10\")\nnow := time.Now() // replace with the actual time of the alert\nalertTime := now.Format(\"2006-01-02 15:04:05\")\naerr := ztm.SetTaskAlert(editedTask.Id, alertTime)\n\n...\n\n// move the Task to Do\n\nztm.MoveTaskToRealm(editedTask.Id, \"2\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"2\"}\nzom.AddPath(taskPath)\n\n// after the task is done, we sent it back to Assess\n\nztm.MoveTaskToRealm(editedTask.Id,\"1\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n\n// from here, we can add it to a collection\n\nmyCollection := zcm.GetCollectionById(\"1\")\n\nzcm.AddTaskToCollection(ztm, myCollection, editedTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"4\"}\nzom.AddPath(taskPath)\n\n```\n\nAll tests are in the `*_test.gno` files, e.g. `tasks_test.gno`, `projects_test.gno`, etc." + }, + { + "name": "collections.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n\ntype ZCollectionManager struct {\n\tCollections *avl.Tree \n\tCollectionTasks *avl.Tree\n\tCollectionProjects *avl.Tree \n}\n\nfunc NewZCollectionManager() *ZCollectionManager {\n return &ZCollectionManager{\n Collections: avl.NewTree(),\n CollectionTasks: avl.NewTree(),\n CollectionProjects: avl.NewTree(),\n }\n}\n\n\n// actions\n\nfunc (zcolm *ZCollectionManager) AddCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrCollectionIdAlreadyExists\n\t\t}\n\t}\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) EditCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\t\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveCollection(c Collection) (err error) {\n // implementation\n if zcolm.Collections.Size() != 0 {\n collectionInterface, exist := zcolm.Collections.Get(c.Id)\n if !exist {\n return ErrCollectionIdNotFound\n }\n collection := collectionInterface.(Collection)\n\n _, removed := zcolm.Collections.Remove(collection.Id)\n if !removed {\n return ErrCollectionNotRemoved\n }\n\n if zcolm.CollectionTasks.Size() != 0 {\n _, removedTasks := zcolm.CollectionTasks.Remove(collection.Id)\n if !removedTasks {\n return ErrCollectionNotRemoved\n }\t\n }\n\n if zcolm.CollectionProjects.Size() != 0 {\n _, removedProjects := zcolm.CollectionProjects.Remove(collection.Id)\n if !removedProjects {\n return ErrCollectionNotRemoved\n }\t\n }\n }\n return nil\n}\n\n\nfunc (zcolm *ZCollectionManager) AddProjectToCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no projects yet, initialize the slice.\n\t\texistingCollectionProjects = []Project{}\n\t} else {\n\t\tprojects, ok := existingCollectionProjects.([]Project)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsProjectsNotFound\n\t\t}\n\t\texistingCollectionProjects = projects\n\t}\n\tp.RealmId = \"4\"\n\tif err := zpm.EditProject(p); err != nil {\n\t\treturn err\n\t}\n\tupdatedProjects := append(existingCollectionProjects.([]Project), p)\n\tzcolm.CollectionProjects.Set(c.Id, updatedProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) AddTaskToCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no tasks yet, initialize the slice.\n\t\texistingCollectionTasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingCollectionTasks.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsTasksNotFound\n\t\t}\n\t\texistingCollectionTasks = tasks\n\t}\n\tt.RealmId = \"4\"\n\tif err := ztm.EditTask(t); err != nil {\n\t\treturn err\n\t}\n\tupdatedTasks := append(existingCollectionTasks.([]Task), t)\n\tzcolm.CollectionTasks.Set(c.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveProjectFromCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no projects yet, return appropriate error\n\t\treturn ErrCollectionsProjectsNotFound\n\t}\n\n\t// Find the index of the project to be removed.\n\tvar index int = -1\n\tfor i, project := range existingCollectionProjects.([]Project) {\n\t\tif project.Id == p.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the project was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default we send it back to Assess\n\t\tp.RealmId = \"1\"\n\t\tzpm.EditProject(p)\n\t\texistingCollectionProjects = append(existingCollectionProjects.([]Project)[:index], existingCollectionProjects.([]Project)[index+1:]...)\n\t} else {\n\t\t// Project not found in the collection\n\t\treturn ErrProjectByIdNotFound \n\t}\n\tzcolm.CollectionProjects.Set(c.Id, existingCollectionProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveTaskFromCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no tasks yet, return appropriate error\n\t\treturn ErrCollectionsTasksNotFound\n\t}\n\n\t// Find the index of the task to be removed.\n\tvar index int = -1\n\tfor i, task := range existingCollectionTasks.([]Task) {\n\t\tif task.Id == t.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the task was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default, we send the task to Assess\n\t\tt.RealmId = \"1\"\n\t\tztm.EditTask(t)\n\t\texistingCollectionTasks = append(existingCollectionTasks.([]Task)[:index], existingCollectionTasks.([]Task)[index+1:]...)\n\t} else {\n\t\t// Task not found in the collection\n\t\treturn ErrTaskByIdNotFound \n\t}\n\tzcolm.CollectionTasks.Set(c.Id, existingCollectionTasks)\n\n\treturn nil\n}\n\n// getters\n\nfunc (zcolm *ZCollectionManager) GetCollectionById(collectionId string) (Collection, error) {\n if zcolm.Collections.Size() != 0 {\n cInterface, exist := zcolm.Collections.Get(collectionId)\n if exist {\n collection := cInterface.(Collection)\n // look for collection Tasks, Projects\n existingCollectionTasks, texist := zcolm.CollectionTasks.Get(collectionId)\n if texist {\n collection.Tasks = existingCollectionTasks.([]Task)\n }\n existingCollectionProjects, pexist := zcolm.CollectionProjects.Get(collectionId)\n if pexist {\n collection.Projects = existingCollectionProjects.([]Project)\n }\n return collection, nil\n }\n return Collection{}, ErrCollectionByIdNotFound\n }\n return Collection{}, ErrCollectionByIdNotFound\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionTasks(c Collection) (tasks []Task, err error) {\n\t\n\tif zcolm.CollectionTasks.Size() != 0 {\n\t\ttask, exist := zcolm.CollectionTasks.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionTasks, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsTasksNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Task\n\t\t\texistingCollectionTasks, ok := task.([]Task)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrTaskFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionTasks, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionProjects(c Collection) (projects []Project, err error) {\n\t\n\tif zcolm.CollectionProjects.Size() != 0 {\n\t\tproject, exist := zcolm.CollectionProjects.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionProjets, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsProjectsNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Projet\n\t\t\texistingCollectionProjects, ok := project.([]Project)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrProjectFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionProjects, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetAllCollections() (collections string, err error) {\n\t// implementation\n\tvar allCollections []Collection\n\t\n\t// Iterate over the Collections AVL tree to collect all Project objects.\n\t\n\tzcolm.Collections.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif collection, ok := value.(Collection); ok {\n\t\t\t// get collection tasks, if any\n\t\t\tcollectionTasks, _ := zcolm.GetCollectionTasks(collection)\n\t\t\tif collectionTasks != nil {\n\t\t\t\tcollection.Tasks = collectionTasks\n\t\t\t}\n\t\t\t// get collection prokects, if any\n\t\t\tcollectionProjects, _ := zcolm.GetCollectionProjects(collection)\n\t\t\tif collectionProjects != nil {\n\t\t\t\tcollection.Projects = collectionProjects\n\t\t\t}\n\t\t\tallCollections = append(allCollections, collection)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a CollectionsObject with all collected tasks.\n\tcollectionsObject := CollectionsObject{\n\t\tCollections: allCollections,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the collections into JSON.\n\tmarshalledCollections, merr := collectionsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t} \n\treturn string(marshalledCollections), nil\n} " + }, + { + "name": "collections_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\n\nfunc Test_AddCollection(t *testing.T) {\n \n collection := Collection{Id: \"1\", RealmId: \"4\", Name: \"First collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := collection.AddCollection()\n if cerr != ErrCollectionIdAlreadyExists {\n t.Errorf(\"Expected ErrCollectionIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveCollection(t *testing.T) {\n \n collection := Collection{Id: \"20\", RealmId: \"4\", Name: \"Removable collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n retrievedCollection, rerr := GetCollectionById(collection.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added collection\")\n }\n\n // Test removing a collection\n terr := retrievedCollection.RemoveCollection()\n if terr != ErrCollectionNotRemoved {\n t.Errorf(\"Expected ErrCollectionNotRemoved, got %v\", terr)\n }\n}\n\nfunc Test_EditCollection(t *testing.T) {\n \n collection := Collection{Id: \"2\", RealmId: \"4\", Name: \"Second collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test editing the collection\n editedCollection := Collection{Id: collection.Id, RealmId: collection.RealmId, Name: \"Edited collection\",}\n cerr := editedCollection.EditCollection()\n if cerr != nil {\n t.Errorf(\"Failed to edit the collection\")\n }\n\n retrievedCollection, _ := GetCollectionById(editedCollection.Id)\n if retrievedCollection.Name != \"Edited collection\" {\n t.Errorf(\"Collection was not edited\")\n }\n}\n\nfunc Test_AddProjectToCollection(t *testing.T){\n // Example Collection and Projects\n col := Collection{Id: \"1\", Name: \"First collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n project Project\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n project: prj,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"200\", Name: \"Collection 200\", RealmId: \"4\",},\n project: prj,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddProjectToCollection(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddProjectToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddProjectToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the project is added to the collection's tasks.\n if !tt.wantErr {\n projects, exist := CollectionProjects.Get(tt.collection.Id)\n if !exist || len(projects.([]Project)) == 0 {\n t.Errorf(\"Project was not added to the collection\")\n } else {\n found := false\n for _, project := range projects.([]Project) {\n if project.Id == tt.project.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Project was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_AddTaskToCollection(t *testing.T){\n // Example Collection and Tasks\n col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"210\", Name: \"Collection 210\", RealmId: \"4\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddTaskToCollection(tt.task)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddTaskToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddTaskToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the collection's tasks.\n if !tt.wantErr {\n tasks, exist := CollectionTasks.Get(tt.collection.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not added to the collection\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_RemoveProjectFromCollection(t *testing.T){\n // Setup:\n\tcollection := Collection{Id: \"300\", Name: \"Collection 300\",}\n\tproject1 := Project{Id: \"21\", Body: \"Project 21\", RealmId: \"1\",}\n\tproject2 := Project{Id: \"22\", Body: \"Project 22\", RealmId: \"1\",}\n\n collection.AddCollection()\n project1.AddProject()\n project2.AddProject()\n collection.AddProjectToCollection(project1)\n collection.AddProjectToCollection(project2)\n\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing project from collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove project from non-existing collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing project from collection\",\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveProjectFromCollection(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the project is no longer part of the collection's projects\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tprojects, _ := CollectionProjects.Get(tt.collection.Id)\n\t\t\t\t\tfor _, project := range projects.([]Project) {\n\t\t\t\t\t\tif project.Id == tt.project.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: project was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RemoveTaskFromCollection(t *testing.T){\n // setup, re-using parts from Test_AddTaskToCollection\n\tcollection := Collection{Id: \"40\", Name: \"Collection 40\",}\n task1 := Task{Id: \"40\", Body: \"Task 40\", RealmId: \"1\",}\n\n collection.AddCollection()\n task1.AddTask()\n collection.AddTaskToCollection(task1)\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing task from collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove task from non-existing collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing task from collection\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveTaskFromCollection(tt.task)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the task is no longer part of the collection's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := CollectionTasks.Get(tt.collection.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetCollectionById(t *testing.T){\n // test getting a non-existing collection\n nonCollection, err := GetCollectionById(\"0\")\n if err != ErrCollectionByIdNotFound {\n t.Fatalf(\"Expected ErrCollectionByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct collection by id\n correctCollection, err := GetCollectionById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get collection by id, error: %v\", err)\n }\n\n if correctCollection.Name != \"First collection\" {\n t.Fatalf(\"Got the wrong collection, with name: %v\", correctCollection.Name)\n }\n}\n\nfunc Test_GetCollectionTasks(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n collection, cerr := GetCollectionById(\"2\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionTasks, pterr := collection.GetCollectionTasks()\n if len(collectionTasks) == 0 {\n t.Errorf(\"GetCollectionTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveTaskFromCollection(tsk)\n if dtterr != nil {\n t.Errorf(\"RemoveTaskFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoTasks, pterr := collection.GetCollectionTasks()\n if len(collectionWithNoTasks) != 0 {\n t.Errorf(\"GetCollectionTasks() after detach failed, %v\", pterr)\n }\n\n // add task back to collection, for tests mockup integrity\n collection.AddTaskToCollection(tsk)\n}\n\nfunc Test_GetCollectionProjects(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"1\", Name: \"First Collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"}\n\n collection, cerr := GetCollectionById(\"1\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionProjects, pterr := collection.GetCollectionProjects()\n if len(collectionProjects) == 0 {\n t.Errorf(\"GetCollectionProjects() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveProjectFromCollection(prj)\n if dtterr != nil {\n t.Errorf(\"RemoveProjectFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoProjects, pterr := collection.GetCollectionProjects()\n if len(collectionWithNoProjects) != 0 {\n t.Errorf(\"GetCollectionProjects() after detach failed, %v\", pterr)\n }\n\n // add project back to collection, for tests mockup integrity\n collection.AddProjectToCollection(prj)\n}\n\nfunc Test_GetAllCollections(t *testing.T){\n // mocking the collections based on previous tests\n // TODO: add isolation?\n knownCollections := []Collection{\n {\n Id: \"1\",\n RealmId: \"4\",\n Name: \"First collection\",\n Tasks: nil, \n Projects: []Project{\n {\n Id: \"10\",\n ContextId: \"2\",\n RealmId: \"4\",\n Tasks: nil, \n Body: \"Project 10\",\n Due: \"2024-01-01\",\n },\n },\n },\n {\n Id: \"2\",\n RealmId: \"4\",\n Name: \"Second Collection\",\n Tasks: []Task{\n {\n Id:\"30\",\n ProjectId:\"\",\n ContextId:\"\",\n RealmId:\"4\",\n Body:\"Task 30\",\n Due:\"\",\n Alert:\"\",\n },\n },\n Projects: nil, \n },\n {\n Id:\"20\",\n RealmId:\"4\",\n Name:\"Removable collection\",\n Tasks: nil,\n Projects: nil,\n },\n {\n Id: \"300\",\n Name: \"Collection 300\",\n Tasks: nil, \n Projects: []Project {\n {\n Id:\"22\",\n ContextId:\"\",\n RealmId:\"4\",\n Tasks: nil,\n Body:\"Project 22\",\n Due:\"\",\n },\n }, \n },\n {\n Id: \"40\",\n Name: \"Collection 40\",\n Tasks: nil, \n Projects: nil, \n },\n }\n \n\n // Manually marshal the known collections to create the expected outcome.\n collectionsObject := CollectionsObject{Collections: knownCollections}\n expected, err := collectionsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known collections: %v\", err)\n }\n\n // Execute GetAllCollections() to get the actual outcome.\n actual, err := GetAllCollections()\n if err != nil {\n t.Fatalf(\"GetAllCollections() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual collections JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n\n\n\n" + }, + { + "name": "contexts.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Context struct {\n\tId string `json:\"contextId\"`\n\tName string `json:\"contextName\"`\n}\n\ntype ZContextManager struct {\n\tContexts *avl.Tree\n}\n\nfunc NewZContextManager() *ZContextManager {\n\treturn &ZContextManager{\n\t\tContexts: avl.NewTree(),\n\t}\n}\n\n// Actions\n\nfunc (zcm *ZContextManager) AddContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrContextIdAlreadyExists\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) EditContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) RemoveContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcontext, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t\t_, removed := zcm.Contexts.Remove(context.(Context).Id)\n\t\tif !removed {\n\t\t\treturn ErrContextNotRemoved\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToTask(ztm *ZTaskManager, c Context, t Task) error {\n\ttaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif t.RealmId == \"2\" {\n\t\ttask := taskInterface.(Task)\n\t\ttask.ContextId = c.Id\n\t\tztm.Tasks.Set(t.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProject(zpm *ZProjectManager, c Context, p Project) error {\n\tprojectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif p.RealmId == \"2\" {\n\t\tproject := projectInterface.(Project)\n\t\tproject.ContextId = c.Id\n\t\tzpm.Projects.Set(p.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProjectTask(zpm *ZProjectManager, c Context, p Project, projectTaskId string) error {\n\t\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].ContextId = c.Id\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zcm *ZContextManager) GetContextById(contextId string) (Context, error) {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcInterface, exist := zcm.Contexts.Get(contextId)\n\t\tif exist {\n\t\t\treturn cInterface.(Context), nil\n\t\t}\n\t\treturn Context{}, ErrContextIdNotFound\n\t}\n\treturn Context{}, ErrContextIdNotFound\n}\n\nfunc (zcm *ZContextManager) GetAllContexts() (string) {\n\tvar allContexts []Context\n\n\t// Iterate over the Contexts AVL tree to collect all Context objects.\n\tzcm.Contexts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif context, ok := value.(Context); ok {\n\t\t\tallContexts = append(allContexts, context)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ContextsObject with all collected contexts.\n\tcontextsObject := &ContextsObject{\n\t\tContexts: allContexts,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the contexts into JSON.\n\tmarshalledContexts, merr := contextsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t}\n\treturn string(marshalledContexts)\n}\n\n" + }, + { + "name": "contexts_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddContext(t *testing.T) {\n \n context := Context{Id: \"1\", Name: \"Work\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := context.AddContext()\n if cerr != ErrContextIdAlreadyExists {\n t.Errorf(\"Expected ErrContextIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_EditContext(t *testing.T) {\n \n context := Context{Id: \"2\", Name: \"Home\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test editing the context\n editedContext := Context{Id: \"2\", Name: \"Shopping\"}\n cerr := editedContext.EditContext()\n if cerr != nil {\n t.Errorf(\"Failed to edit the context\")\n }\n\n retrievedContext, _ := GetContextById(editedContext.Id)\n if retrievedContext.Name != \"Shopping\" {\n t.Errorf(\"Context was not edited\")\n }\n}\n\nfunc Test_RemoveContext(t *testing.T) {\n \n context := Context{Id: \"4\", Name: \"Gym\",}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n retrievedContext, rerr := GetContextById(context.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added context\")\n }\n // Test removing a context\n cerr := retrievedContext.RemoveContext()\n if cerr != ErrContextNotRemoved {\n t.Errorf(\"Expected ErrContextNotRemoved, got %v\", cerr)\n }\n}\n\nfunc Test_AddContextToTask(t *testing.T) {\n\n task := Task{Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n taskInDecide, exist := Tasks.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Task with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToTask(taskInDecide.(Task))\n if derr != nil {\n t.Errorf(\"Could not add context to a task in Decide, err %v\", derr)\n }\n}\n\nfunc Test_AddContextToProject(t *testing.T) {\n\n project := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n projectInDecide, exist := Projects.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Project with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToProject(projectInDecide.(Project))\n if derr != nil {\n t.Errorf(\"Could not add context to a project in Decide, err %v\", derr)\n }\n}\n\nfunc Test_GetAllContexts(t *testing.T) {\n \n // mocking the contexts based on previous tests\n // TODO: add isolation?\n knownContexts := []Context{\n {Id: \"1\", Name: \"Work\",},\n {Id: \"2\", Name: \"Shopping\",},\n {Id: \"4\", Name: \"Gym\",},\n }\n\n // Manually marshal the known contexts to create the expected outcome.\n contextsObject := ContextsObject{Contexts: knownContexts}\n expected, err := contextsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known contexts: %v\", err)\n }\n\n // Execute GetAllContexts() to get the actual outcome.\n actual, err := GetAllContexts()\n if err != nil {\n t.Fatalf(\"GetAllContexts() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual contexts JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n" + }, + { + "name": "core.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// holding the path of an object since creation\n// each time we move an object from one realm to another, we add to its path\ntype ObjectPath struct {\n\tObjectType string `json:\"objectType\"` // Task, Project\n\tId string `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId string `json:\"realmId\"`\n}\n\ntype ZObjectPathManager struct {\n\tPaths avl.Tree\n\tPathId int\n}\n\nfunc NewZObjectPathManager() *ZObjectPathManager {\n\treturn &ZObjectPathManager{\n\t\tPaths: *avl.NewTree(),\n\t\tPathId: 1,\n\t}\n}\n\nfunc (zopm *ZObjectPathManager) AddPath(o ObjectPath) error {\n\tzopm.PathId++\n\tupdated := zopm.Paths.Set(strconv.Itoa(zopm.PathId), o)\n\tif !updated {\n\t\treturn ErrObjectPathNotUpdated\n\t}\n\treturn nil\n}\n\nfunc (zopm *ZObjectPathManager) GetObjectJourney(objectType string, objectId string) (string, error) {\n\tvar objectPaths []ObjectPath\n\n\t// Iterate over the Paths AVL tree to collect all ObjectPath objects.\n\tzopm.Paths.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif objectPath, ok := value.(ObjectPath); ok {\n\t\t\tif objectPath.ObjectType == objectType && objectPath.Id == objectId {\n\t\t\t\tobjectPaths = append(objectPaths, objectPath)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create an ObjectJourney with all collected paths.\n\tobjectJourney := &ObjectJourney{\n\t\tObjectPaths: objectPaths,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the journey into JSON.\n\tmarshalledJourney, merr := objectJourney.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t}\n\treturn string(marshalledJourney), nil\n}\n\n\n// GetZenStatus\n/* todo: leave it to the client\nfunc () GetZenStatus() (zenStatus string, err error) {\n\t// implementation\n}\n*/\n" + }, + { + "name": "errors.gno", + "body": "package zentasktic\n\nimport \"errors\"\n\nvar (\n\tErrTaskNotEditable \t= errors.New(\"Task is not editable\")\n\tErrProjectNotEditable = errors.New(\"Project is not editable\")\n\tErrProjectIdNotFound\t\t\t= errors.New(\"Project id not found\")\n\tErrTaskIdNotFound\t\t\t\t= errors.New(\"Task id not found\")\n\tErrTaskFailedToAssert\t\t\t= errors.New(\"Failed to assert Task type\")\n\tErrProjectFailedToAssert\t\t= errors.New(\"Failed to assert Project type\")\n\tErrProjectTasksNotFound\t\t\t= errors.New(\"Could not get tasks for project\")\n\tErrCollectionsProjectsNotFound\t= errors.New(\"Could not get projects for this collection\")\n\tErrCollectionsTasksNotFound\t\t= errors.New(\"Could not get tasks for this collection\")\n\tErrTaskIdAlreadyExists\t\t\t= errors.New(\"A task with the provided id already exists\")\n\tErrCollectionIdAlreadyExists\t= errors.New(\"A collection with the provided id already exists\")\n\tErrProjectIdAlreadyExists\t\t= errors.New(\"A project with the provided id already exists\")\n\tErrTaskByIdNotFound\t\t\t\t= errors.New(\"Can't get task by id\")\n\tErrProjectByIdNotFound\t\t\t= errors.New(\"Can't get project by id\")\n\tErrCollectionByIdNotFound\t\t= errors.New(\"Can't get collection by id\")\n\tErrTaskNotRemovable\t\t\t\t= errors.New(\"Cannot remove a task directly from this realm\")\n\tErrProjectNotRemovable\t\t\t= errors.New(\"Cannot remove a project directly from this realm\")\n\tErrProjectTasksNotRemoved\t\t= errors.New(\"Project tasks were not removed\")\n\tErrTaskNotRemoved\t\t\t\t= errors.New(\"Task was not removed\")\n\tErrTaskNotInAssessRealm\t\t\t= errors.New(\"Task is not in Assess, cannot edit Body\")\n\tErrProjectNotInAssessRealm\t\t= errors.New(\"Project is not in Assess, cannot edit Body\")\n\tErrContextIdAlreadyExists\t\t= errors.New(\"A context with the provided id already exists\")\n\tErrContextIdNotFound\t\t\t= errors.New(\"Context id not found\")\n\tErrCollectionIdNotFound\t\t\t= errors.New(\"Collection id not found\")\n\tErrContextNotRemoved\t\t\t= errors.New(\"Context was not removed\")\n\tErrProjectNotRemoved\t\t\t= errors.New(\"Project was not removed\")\n\tErrCollectionNotRemoved\t\t\t= errors.New(\"Collection was not removed\")\n\tErrObjectPathNotUpdated\t\t\t= errors.New(\"Object path wasn't updated\")\n\tErrInvalidateDateFormat\t\t\t= errors.New(\"Invalida date format\")\n\tErrInvalidDateFilterType\t\t= errors.New(\"Invalid date filter type\")\n\tErrRealmIdAlreadyExists\t\t\t= errors.New(\"A realm with the same id already exists\")\n\tErrRealmIdNotAllowed\t\t\t= errors.New(\"This is a reserved realm id\")\n\tErrRealmIdNotFound\t\t\t\t= errors.New(\"Realm id not found\")\n\tErrRealmNotRemoved\t\t\t\t= errors.New(\"Realm was not removed\")\n)" + }, + { + "name": "marshals.gno", + "body": "package zentasktic\n\nimport (\n\t\"bytes\"\n)\n\n\ntype ContextsObject struct {\n\tContexts\t[]Context\n}\n\ntype TasksObject struct {\n\tTasks\t[]Task\n}\n\ntype ProjectsObject struct {\n\tProjects\t[]Project\n}\n\ntype CollectionsObject struct {\n\tCollections\t[]Collection\n}\n\ntype RealmsObject struct {\n\tRealms\t[]Realm\n}\n\ntype ObjectJourney struct {\n\tObjectPaths []ObjectPath\n}\n\nfunc (c Context) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"contextId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"contextName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (cs ContextsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"contexts\":[`)\n\t\n\tfor i, context := range cs.Contexts {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcontextJSON, cerr := context.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(contextJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (t Task) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"taskId\":\"`)\n\tb.WriteString(t.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskProjectId\":\"`)\n\tb.WriteString(t.ProjectId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskContextId\":\"`)\n\tb.WriteString(t.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskRealmId\":\"`)\n\tb.WriteString(t.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskBody\":\"`)\n\tb.WriteString(t.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskDue\":\"`)\n\tb.WriteString(t.Due)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskAlert\":\"`)\n\tb.WriteString(t.Alert)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ts TasksObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"tasks\":[`)\n\tfor i, task := range ts.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\ttaskJSON, cerr := task.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(taskJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (p Project) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"projectId\":\"`)\n\tb.WriteString(p.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectContextId\":\"`)\n\tb.WriteString(p.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectRealmId\":\"`)\n\tb.WriteString(p.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectTasks\":[`)\n\n\tif len(p.Tasks) != 0 {\n\t\tfor i, projectTask := range p.Tasks {\n\t\t\tif i > 0 {\n\t\t\t\tb.WriteString(`,`)\n\t\t\t}\n\t\t\tprojectTaskJSON, perr := projectTask.MarshalJSON()\n\t\t\tif perr == nil {\n\t\t\t\tb.WriteString(string(projectTaskJSON))\n\t\t\t}\n\t\t}\n\t}\n\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"projectBody\":\"`)\n\tb.WriteString(p.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectDue\":\"`)\n\tb.WriteString(p.Due)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ps ProjectsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"projects\":[`)\n\tfor i, project := range ps.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tprojectJSON, perr := project.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(projectJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (c Collection) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"collectionId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionRealmId\":\"`)\n\tb.WriteString(c.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionTasks\":[`)\n\tfor i, collectionTask := range c.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionTaskJSON, perr := collectionTask.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionTaskJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"collectionProjects\":[`)\n\tfor i, collectionProject := range c.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionProjectJSON, perr := collectionProject.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionProjectJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (co CollectionsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"collections\":[`)\n\tfor i, collection := range co.Collections {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionJSON, perr := collection.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (r Realm) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(r.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmName\":\"`)\n\tb.WriteString(r.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (rs RealmsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"realms\":[`)\n\t\n\tfor i, realm := range rs.Realms {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\trealmJSON, rerr := realm.MarshalJSON()\n\t\tif rerr == nil {\n\t\t\tb.WriteString(string(realmJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (op ObjectPath) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"objectType\":\"`)\n\tb.WriteString(op.ObjectType)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"id\":\"`)\n\tb.WriteString(op.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(op.RealmId)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (oj ObjectJourney) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"objectJourney\":[`)\n\t\n\tfor i, objectPath := range oj.ObjectPaths {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tobjectPathJSON, oerr := objectPath.MarshalJSON()\n\t\tif oerr == nil {\n\t\t\tb.WriteString(string(objectPathJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n" + }, + { + "name": "projects.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"projectDue\"`\n}\n\ntype ZProjectManager struct {\n\tProjects *avl.Tree // projectId -> Project\n\tProjectTasks *avl.Tree // projectId -> []Task\n}\n\n\nfunc NewZProjectManager() *ZProjectManager {\n\treturn &ZProjectManager{\n\t\tProjects: avl.NewTree(),\n\t\tProjectTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (zpm *ZProjectManager) AddProject(p Project) (err error) {\n\t// implementation\n\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif exist {\n\t\t\treturn ErrProjectIdAlreadyExists\n\t\t}\n\t}\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) RemoveProject(p Project) (err error) {\n\t// implementation, remove from ProjectTasks too\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t // project is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingProject.RealmId != \"1\" && existingProject.RealmId != \"4\" {\n\t\treturn ErrProjectNotRemovable\n\t}\n\n\t_, removed := zpm.Projects.Remove(existingProject.Id)\n\tif !removed {\n\t\treturn ErrProjectNotRemoved\n\t}\n\n\t// manage project tasks, if any\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\t_, exist := zpm.ProjectTasks.Get(existingProject.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in ProjectTasks, we don't have to remove anything\n\t\t\treturn nil\n\t\t} else {\n\t\t\t_, removed := zpm.ProjectTasks.Remove(existingProject.Id)\n\t\t\tif !removed {\n\t\t\t\treturn ErrProjectTasksNotRemoved\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProject(p Project) (err error) {\n\t// implementation, get project by Id and replace the object\n\t// this is for the project body and realm, project tasks are managed in the Tasks object\n\texistingProject := Project{}\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn ErrProjectIdNotFound\n\t\t}\n\t}\n\t\n\t// project Body is editable only when project is in Assess, RealmId = \"1\"\n\tif p.RealmId != \"1\" {\n\t\tif p.Body != existingProject.Body {\n\t\t\treturn ErrProjectNotInAssessRealm\n\t\t}\n\t}\n\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\n// helper function, we can achieve the same with EditProject() above\n/*func (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) (err error) {\n\t// implementation\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\texistingProject.RealmId = realmId\n\tzpm.Projects.Set(projectId, existingProject)\n\treturn nil\n}*/\n\nfunc (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) error {\n\t// Get the existing project from the Projects map\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t// Set the project's RealmId to the new RealmId\n\texistingProject.RealmId = realmId\n\n\t// Get the existing project tasks from the ProjectTasks map\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\n\t// Iterate through the project's tasks and set their RealmId to the new RealmId\n\tfor i := range tasks {\n\t\ttasks[i].RealmId = realmId\n\t}\n\n\t// Set the updated tasks back into the ProjectTasks map\n\tzpm.ProjectTasks.Set(projectId, tasks)\n\n\t// Set the updated project back into the Projects map\n\tzpm.Projects.Set(projectId, existingProject)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n // Get the existing project from the Projects map\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n // Get the existing project tasks from the ProjectTasks map\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n\n // Iterate through the project's tasks to find the task to be updated\n var taskFound bool\n for i, task := range tasks {\n if task.Id == projectTaskId {\n tasks[i].RealmId = \"4\" // Change the RealmId to \"4\"\n taskFound = true\n break\n }\n }\n\n if !taskFound {\n return ErrTaskByIdNotFound\n }\n\n // Set the updated tasks back into the ProjectTasks map\n zpm.ProjectTasks.Set(existingProject.Id, tasks)\n\n return nil\n}\n\n\nfunc (zpm *ZProjectManager) GetProjectTasks(p Project) (tasks []Task, err error) {\n\t// implementation, query ProjectTasks and return the []Tasks object\n\tvar existingProjectTasks []Task\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\tprojectTasksInterface, exist := zpm.ProjectTasks.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn nil, ErrProjectTasksNotFound\n\t\t}\n\t\texistingProjectTasks = projectTasksInterface.([]Task)\n\t\treturn existingProjectTasks, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectDueDate(projectId string, dueDate string) (err error) {\n\tprojectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\tproject := projectInterface.(Project)\n\n\t// check to see if project is in RealmId = 2 (Decide)\n\tif project.RealmId == \"2\" {\n\t\tproject.Due = dueDate\n\t\tzpm.Projects.Set(project.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) (err error){\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Due = dueDate\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zpm *ZProjectManager) GetProjectById(projectId string) (Project, error) {\n\tif zpm.Projects.Size() != 0 {\n\t\tpInterface, exist := zpm.Projects.Get(projectId)\n\t\tif exist {\n\t\t\treturn pInterface.(Project), nil\n\t\t}\n\t}\n\treturn Project{}, ErrProjectIdNotFound\n}\n\nfunc (zpm *ZProjectManager) GetAllProjects() (projects string) {\n\t// implementation\n\tvar allProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\t// get project tasks, if any\n\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\tif projectTasks != nil {\n\t\t\t\tproject.Tasks = projectTasks\n\t\t\t}\n\t\t\tallProjects = append(allProjects, project)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: allProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByRealm(realmId string) (projects string) {\n\t// implementation\n\tvar realmProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\trealmProjects = append(realmProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: realmProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByContextAndRealm(contextId string, realmId string) (projects string) {\n\t// implementation\n\tvar contextProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.ContextId == contextId && project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tcontextProjects = append(contextProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: contextProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByDate(projectDate string, filterType string) (projects string) {\n\t// implementation\n\tparsedDate, err:= time.Parse(\"2006-01-02\", projectDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredProjects []Project\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproject, ok := value.(Project)\n\t\tif !ok {\n\t\t\treturn false // Skip this iteration and continue.\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", project.Due)\n\t\tif serr != nil {\n\t\t\t// Skip projects with invalid dates.\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\t}\n\n\t\treturn false // Continue iteration.\n\t})\n\n\tif len(filteredProjects) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: filteredProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n\n}\n" + }, + { + "name": "projects_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddProject(t *testing.T) {\n \n project := Project{Id: \"1\", RealmId: \"1\", Body: \"First project\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test adding a duplicate project.\n cerr := project.AddProject()\n if cerr != ErrProjectIdAlreadyExists {\n t.Errorf(\"Expected ErrProjectIdAlreadyExists, got %v\", cerr)\n }\n}\n\n\nfunc Test_RemoveProject(t *testing.T) {\n \n project := Project{Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n retrievedProject, rerr := GetProjectById(project.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added project\")\n }\n\n // Test removing a project\n terr := retrievedProject.RemoveProject()\n if terr != ErrProjectNotRemoved {\n t.Errorf(\"Expected ErrProjectNotRemoved, got %v\", terr)\n }\n}\n\n\nfunc Test_EditProject(t *testing.T) {\n \n project := Project{Id: \"2\", Body: \"Second project content\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test editing the project\n editedProject := Project{Id: project.Id, Body: \"Edited project content\", RealmId: project.RealmId, ContextId: \"2\",}\n cerr := editedProject.EditProject()\n if cerr != nil {\n t.Errorf(\"Failed to edit the project\")\n }\n\n retrievedProject, _ := GetProjectById(editedProject.Id)\n if retrievedProject.Body != \"Edited project content\" {\n t.Errorf(\"Project was not edited\")\n }\n}\n\n\nfunc Test_MoveProjectToRealm(t *testing.T) {\n \n project := Project{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"1\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test moving the project to another realm\n \n cerr := project.MoveProjectToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move project to another realm\")\n }\n\n retrievedProject, _ := GetProjectById(project.Id)\n if retrievedProject.RealmId != \"2\" {\n t.Errorf(\"Project was moved to the wrong realm\")\n }\n}\n\nfunc Test_SetProjectDueDate(t *testing.T) {\n\tprojectRealmIdOne, _ := GetProjectById(\"1\")\n projectRealmIdTwo, _ := GetProjectById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Project does not exist\",\n\t\t\tproject: Project{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Project not editable due to wrong realm\",\n\t\t\tproject: projectRealmIdOne,\n\t\t\twantErr: ErrProjectNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\tproject: projectRealmIdTwo,\n\t\t\tdueDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.project.SetProjectDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedProject, exist := Projects.Get(tc.project.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Project %v was not found after setting the due date\", tc.project.Id)\n\t\t\t\t}\n\t\t\t\tif updatedProject.(Project).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedProject.(Project).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllProjects(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownProjects := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n {Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObject := ProjectsObject{Projects: knownProjects}\n expected, err := projectsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known projects: %v\", err)\n }\n\n // Execute GetAllProjects() to get the actual outcome.\n actual, err := GetAllProjects()\n if err != nil {\n t.Fatalf(\"GetAllProjects() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\tprojectDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2024-01-01\", \"specific\", `{\"projects\":[{\"projectId\":\"10\",\"projectContextId\":\"2\",\"projectRealmId\":\"2\",\"projectTasks\":[],\"projectBody\":\"Project 10\",\"projectDue\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2025-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetProjectsByDate(tt.projectDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetProjectTasks(t *testing.T){\n \n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n project, perr := GetProjectById(\"1\")\n if perr != nil {\n t.Errorf(\"GetProjectById() failed, %v\", perr)\n }\n\n // test attaching to an existing project\n atterr := task.AttachTaskToProject(project)\n if atterr != nil {\n t.Errorf(\"AttachTaskToProject() failed, %v\", atterr)\n }\n\n projectTasks, pterr := project.GetProjectTasks()\n if len(projectTasks) == 0 {\n t.Errorf(\"GetProjectTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing project\n dtterr := task.DetachTaskFromProject(project)\n if dtterr != nil {\n t.Errorf(\"DetachTaskFromProject() failed, %v\", dtterr)\n }\n\n projectWithNoTasks, pterr := project.GetProjectTasks()\n if len(projectWithNoTasks) != 0 {\n t.Errorf(\"GetProjectTasks() after detach failed, %v\", pterr)\n }\n}\n\nfunc Test_GetProjectById(t *testing.T){\n // test getting a non-existing project\n nonProject, err := GetProjectById(\"0\")\n if err != ErrProjectByIdNotFound {\n t.Fatalf(\"Expected ErrProjectByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctProject, err := GetProjectById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get project by id, error: %v\", err)\n }\n\n if correctProject.Body != \"First project\" {\n t.Fatalf(\"Got the wrong project, with body: %v\", correctProject.Body)\n }\n}\n\nfunc Test_GetProjectsByRealm(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInAssessRealm := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObjectAssess := ProjectsObject{Projects: projectsInAssessRealm}\n expected, err := projectsObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects in Assess: %v\", err)\n }\n\n actual, err := GetProjectsByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual projects JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByContext(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInContextOne := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n projectsObjectForContexts := ProjectsObject{Projects: projectsInContextOne}\n expected, err := projectsObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects for ContextId 1: %v\", err)\n }\n\n actual, err := GetProjectsByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectsByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n" + }, + { + "name": "realms.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// structs\n\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n\ntype ZRealmManager struct {\n\tRealms *avl.Tree\n}\n\nfunc NewZRealmManager() *ZRealmManager {\n\tzrm := &ZRealmManager{\n\t\tRealms: avl.NewTree(),\n\t}\n\tzrm.initializeHardcodedRealms()\n\treturn zrm\n}\n\n\nfunc (zrm *ZRealmManager) initializeHardcodedRealms() {\n\thardcodedRealms := []Realm{\n\t\t{Id: \"1\", Name: \"Assess\"},\n\t\t{Id: \"2\", Name: \"Decide\"},\n\t\t{Id: \"3\", Name: \"Do\"},\n\t\t{Id: \"4\", Name: \"Collections\"},\n\t}\n\n\tfor _, realm := range hardcodedRealms {\n\t\tzrm.Realms.Set(realm.Id, realm)\n\t}\n}\n\n\nfunc (zrm *ZRealmManager) AddRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif exist {\n\t\t\treturn ErrRealmIdAlreadyExists\n\t\t}\n\t}\n\t// check for hardcoded values\n\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\treturn ErrRealmIdNotAllowed\n\t}\n\tzrm.Realms.Set(r.Id, r)\n\treturn nil\n\t\n}\n\nfunc (zrm *ZRealmManager) RemoveRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif !exist {\n\t\t\treturn ErrRealmIdNotFound\n\t\t} else {\n\t\t\t// check for hardcoded values, not removable\n\t\t\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\t\t\treturn ErrRealmIdNotAllowed\n\t\t\t}\n\t\t}\n\t}\n\t\n\t_, removed := zrm.Realms.Remove(r.Id)\n\tif !removed {\n\t\treturn ErrRealmNotRemoved\n\t}\n\treturn nil\n\t\n}\n\n// getters\nfunc (zrm *ZRealmManager) GetRealmById(realmId string) (r Realm, err error) {\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\trInterface, exist := zrm.Realms.Get(realmId)\n\t\tif exist {\n\t\t\treturn rInterface.(Realm), nil\n\t\t} else {\n\t\t\treturn Realm{}, ErrRealmIdNotFound\n\t\t}\n\t}\n\treturn Realm{}, ErrRealmIdNotFound\n}\n\nfunc (zrm *ZRealmManager) GetRealms() (realms string, err error) {\n\t// implementation\n\tvar allRealms []Realm\n\n\t// Iterate over the Realms AVL tree to collect all Context objects.\n\tzrm.Realms.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif realm, ok := value.(Realm); ok {\n\t\t\tallRealms = append(allRealms, realm)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\n\t// Create a RealmsObject with all collected contexts.\n\trealmsObject := &RealmsObject{\n\t\tRealms: allRealms,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the realms into JSON.\n\tmarshalledRealms, rerr := realmsObject.MarshalJSON()\n\tif rerr != nil {\n\t\treturn \"\", rerr\n\t} \n\treturn string(marshalledRealms), nil\n}\n" + }, + { + "name": "tasks.gno", + "body": "package zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n\ntype ZTaskManager struct {\n\tTasks *avl.Tree\n}\n\nfunc NewZTaskManager() *ZTaskManager {\n\treturn &ZTaskManager{\n\t\tTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (ztm *ZTaskManager) AddTask(t Task) error {\n\tif ztm.Tasks.Size() != 0 {\n\t\t_, exist := ztm.Tasks.Get(t.Id)\n\t\tif exist {\n\t\t\treturn ErrTaskIdAlreadyExists\n\t\t}\n\t}\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) RemoveTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t // task is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingTask.RealmId != \"1\" && existingTask.RealmId != \"4\" {\n\t\treturn ErrTaskNotRemovable\n\t}\n\n\t_, removed := ztm.Tasks.Remove(existingTask.Id)\n\tif !removed {\n\t\treturn ErrTaskNotRemoved\n\t}\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) EditTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t// task Body is editable only when task is in Assess, RealmId = \"1\"\n\tif t.RealmId != \"1\" {\n\t\tif t.Body != existingTask.Body {\n\t\t\treturn ErrTaskNotInAssessRealm\n\t\t}\n\t}\n\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\n// Helper function to move a task to a different realm\nfunc (ztm *ZTaskManager) MoveTaskToRealm(taskId, realmId string) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\texistingTask.RealmId = realmId\n\tztm.Tasks.Set(taskId, existingTask)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskDueDate(taskId, dueDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Due = dueDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskAlert(taskId, alertDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Alert = alertDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\n// tasks & projects association\n\nfunc (zpm *ZProjectManager) AttachTaskToProject(ztm *ZTaskManager, t Task, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\texistingProject.Tasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingProjectTasksInterface.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrProjectTasksNotFound\n\t\t}\n\t\texistingProject.Tasks = tasks\n\t}\n\n\tt.ProjectId = p.Id\n\t// @todo we need to remove it from Tasks if it was previously added there, then detached\n\texistingTask, err := ztm.GetTaskById(t.Id)\n\tif err == nil {\n\t\tztm.RemoveTask(existingTask)\n\t}\n\tupdatedTasks := append(existingProject.Tasks, t)\n\tzpm.ProjectTasks.Set(p.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"1\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Body = projectTaskBody\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\nfunc (zpm *ZProjectManager) DetachTaskFromProject(ztm *ZTaskManager, projectTaskId string, detachedTaskId string, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar foundTask Task\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tfoundTask = task\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tfoundTask.ProjectId = \"\"\n\tfoundTask.Id = detachedTaskId\n\t// Tasks and ProjectTasks have different storage, if a task is detached from a Project\n\t// we add it to the Tasks storage\n\tif err := ztm.AddTask(foundTask); err != nil {\n\t\treturn err\n\t}\n\n\tzpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n\treturn nil\n}\n\n\nfunc (zpm *ZProjectManager) RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tzpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n\treturn nil\n}\n\n// getters\n\nfunc (ztm *ZTaskManager) GetTaskById(taskId string) (Task, error) {\n\tif ztm.Tasks.Size() != 0 {\n\t\ttInterface, exist := ztm.Tasks.Get(taskId)\n\t\tif exist {\n\t\t\treturn tInterface.(Task), nil\n\t\t}\n\t}\n\treturn Task{}, ErrTaskIdNotFound\n}\n\nfunc (ztm *ZTaskManager) GetAllTasks() (task string) {\n\tvar allTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tallTasks = append(allTasks, task)\n\t\t}\n\t\treturn false\n\t})\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: allTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\t\n}\n\nfunc (ztm *ZTaskManager) GetTasksByRealm(realmId string) (tasks string) {\n\tvar realmTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.RealmId == realmId {\n\t\t\t\trealmTasks = append(realmTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: realmTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByContextAndRealm(contextId string, realmId string) (tasks string) {\n\tvar contextTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.ContextId == contextId && task.ContextId == realmId {\n\t\t\t\tcontextTasks = append(contextTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: contextTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByDate(taskDate string, filterType string) (tasks string) {\n\tparsedDate, err := time.Parse(\"2006-01-02\", taskDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttask, ok := value.(Task)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", task.Due)\n\t\tif serr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: filteredTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n" + }, + { + "name": "tasks_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n\n// Shared instance of ZTaskManager\nvar ztm *ZTaskManager\n\nfunc init() {\n ztm = NewZTaskManager()\n}\n\nfunc Test_AddTask(t *testing.T) {\n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := ztm.AddTask(task)\n if cerr != ErrTaskIdAlreadyExists {\n t.Errorf(\"Expected ErrTaskIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveTask(t *testing.T) {\n \n task := Task{Id: \"20\", Body: \"Removable task\", RealmId: \"1\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n retrievedTask, rerr := ztm.GetTaskById(task.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added task\")\n }\n\n // Test removing a task\n terr := ztm.RemoveTask(retrievedTask)\n if terr != nil {\n t.Errorf(\"Expected nil, got %v\", terr)\n }\n}\n\nfunc Test_EditTask(t *testing.T) {\n \n task := Task{Id: \"2\", Body: \"First content\", RealmId: \"1\", ContextId: \"2\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test editing the task\n editedTask := Task{Id: task.Id, Body: \"Edited content\", RealmId: task.RealmId, ContextId: \"2\"}\n cerr := ztm.EditTask(editedTask)\n if cerr != nil {\n t.Errorf(\"Failed to edit the task\")\n }\n\n retrievedTask, _ := ztm.GetTaskById(editedTask.Id)\n if retrievedTask.Body != \"Edited content\" {\n t.Errorf(\"Task was not edited\")\n }\n}\n/*\nfunc Test_MoveTaskToRealm(t *testing.T) {\n \n task := Task{Id: \"3\", Body: \"First content\", RealmId: \"1\", ContextId: \"1\"}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test moving the task to another realm\n \n cerr := task.MoveTaskToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move task to another realm\")\n }\n\n retrievedTask, _ := GetTaskById(task.Id)\n if retrievedTask.RealmId != \"2\" {\n t.Errorf(\"Task was moved to the wrong realm\")\n }\n}\n\nfunc Test_AttachTaskToProject(t *testing.T) {\n \n // Example Projects and Tasks\n prj := Project{Id: \"1\", Body: \"Project 1\", RealmId: \"1\",}\n tsk := Task{Id: \"4\", Body: \"Task 4\", RealmId: \"1\",}\n\n Projects.Set(prj.Id, prj) // Mock existing project\n\n tests := []struct {\n name string\n project Project\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing project\",\n project: prj,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing project\",\n project: Project{Id: \"200\", Body: \"Project 200\", RealmId: \"1\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrProjectIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.task.AttachTaskToProject(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AttachTaskToProject() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AttachTaskToProject() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the project's tasks.\n if !tt.wantErr {\n tasks, exist := ProjectTasks.Get(tt.project.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not attached to the project\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the project\")\n }\n }\n }\n })\n }\n}\n\nfunc TestDetachTaskFromProject(t *testing.T) {\n\t\n\t// Setup:\n\tproject := Project{Id: \"p1\", Body: \"Test Project\"}\n\ttask1 := Task{Id: \"5\", Body: \"Task One\"}\n\ttask2 := Task{Id: \"6\", Body: \"Task Two\"}\n\n\tProjects.Set(project.Id, project)\n\tProjectTasks.Set(project.Id, []Task{task1, task2})\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tproject Project\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Detach existing task from project\",\n\t\t\ttask: task1,\n\t\t\tproject: project,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach task from non-existing project\",\n\t\t\ttask: task1,\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach non-existing task from project\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tproject: project,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.task.DetachTaskFromProject(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful detachment, verify the task is no longer part of the project's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := ProjectTasks.Get(tt.project.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the project\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskDueDate(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set due date\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\tdueDate: \"2023-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskAlert(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\talertDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\talertDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskAlert(tc.alertDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Alert != tc.alertDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.alertDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllTasks(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownTasks := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"2\", Due: \"2023-01-01\", Alert: \"2024-01-01\"},\n {Id: \"2\", Body: \"Edited content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"3\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObject := TasksObject{Tasks: knownTasks}\n expected, err := tasksObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known tasks: %v\", err)\n }\n\n // Execute GetAllTasks() to get the actual outcome.\n actual, err := GetAllTasks()\n if err != nil {\n t.Fatalf(\"GetAllTasks() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\ttaskDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2023-01-01\", \"specific\", `{\"tasks\":[{\"taskId\":\"10\",\"taskProjectId\":\"\",\"taskContextId\":\"2\",\"taskRealmId\":\"2\",\"taskBody\":\"First content\",\"taskDue\":\"2023-01-01\",\"taskAlert\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2023-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTasksByDate(tt.taskDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetTasksByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetTasksByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetTaskById(t *testing.T){\n // test getting a non-existing task\n nonTask, err := GetTaskById(\"0\")\n if err != ErrTaskByIdNotFound {\n t.Fatalf(\"Expected ErrTaskByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctTask, err := GetTaskById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get task by id, error: %v\", err)\n }\n\n if correctTask.Body != \"First task\" {\n t.Fatalf(\"Got the wrong task, with body: %v\", correctTask.Body)\n }\n}\n\nfunc Test_GetTasksByRealm(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInAssessRealm := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"2\", RealmId: \"1\", Body: \"Edited content\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectAssess := TasksObject{Tasks: tasksInAssessRealm}\n expected, err := tasksObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks in Assess: %v\", err)\n }\n\n actual, err := GetTasksByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByContext(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInContextOne := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"3\", RealmId: \"2\", Body: \"First content\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectForContexts := TasksObject{Tasks: tasksInContextOne}\n expected, err := tasksObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks for ContextId 1: %v\", err)\n }\n\n actual, err := GetTasksByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "10000000", + "gas_fee": "1000000ugnot" + }, + "signatures": [], + "memo": "" +} + +-- tx2.tx -- +{ + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "zentasktic", + "path": "gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic", + "files": [ + { + "name": "README.md", + "body": "# ZenTasktic Core\n\nA basic, minimalisitc Asess-Decide-Do implementations as `p/zentasktic`. The diagram below shows a simplified ADD workflow.\n\n![ZenTasktic](ZenTasktic-framework.png)\n\nThis implementation will expose all the basic features of the framework: tasks & projects with complete workflows. Ideally, this should offer all the necessary building blocks for any other custom implementation.\n\n## Object Definitions and Default Values\n\nAs an unopinionated ADD workflow, `zentastic_core` defines the following objects:\n\n- Realm\n\nRealms act like containers for tasks & projects during their journey from Assess to Do, via Decide. Each realm has a certain restrictions, e.g. a task's Body can only be edited in Assess, a Context, Due date and Alert can only be added in Decide, etc.\n\nIf someone observes different realms, there is support for adding and removing arbitrary Realms.\n\n_note: the Ids between 1 and 4 are reserved for: 1-Assess, 2-Decide, 3-Do, 4-Collection. Trying to add or remove such a Realm will raise an error._\n\n\nRealm data definition:\n\n```\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n```\n\n- Task\n\nA task is the minimal data structure in ZenTasktic, with the following definition:\n\n```\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n```\n\n- Project\n\nProjects are unopinionated collections of Tasks. A Task in a Project can be in any Realm, but the restrictions are propagated upwards to the Project: e.g. if a Task is marked as 'done' in the Do realm (namely changing its RealmId property to \"1\", Assess, or \"4\" Collection), and the rest of the tasks are not, the Project cannot be moved back to Decide or Asses, all Tasks must have consisted RealmId properties.\n\nA Task can be arbitrarily added to, removed from and moved to another Project.\n\nProject data definition:\n\n\n```\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"ProjectDue\"`\n}\n```\n\n\n- Context\n\nContexts act as tags, grouping together Tasks and Project, e.g. \"Backend\", \"Frontend\", \"Marketing\". Contexts have no defaults and can be added or removed arbitrarily.\n\nContext data definition:\n\n```\ntype Context struct {\n\tId \t\t\tstring `json:\"contextId\"`\n\tName \t\tstring `json:\"contextName\"`\n}\n```\n\n- Collection\n\nCollections are intended as an agnostic storage for Tasks & Projects which are either not ready to be Assessed, or they have been already marked as done, and, for whatever reason, they need to be kept in the system. There is a special Realm Id for Collections, \"4\", although technically they are not part of the Assess-Decide-Do workflow.\n\nCollection data definition:\n\n```\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n```\n\n- ObjectPath\n\nObjectPaths are minimalistic representations of the journey taken by a Task or a Project in the Assess-Decide-Do workflow. By recording their movement between various Realms, one can extract their `ZenStatus`, e.g., if a Task has been moved many times between Assess and Decide, never making it to Do, we can infer the following:\n-- either the Assess part was incomplete\n-- the resources needed for that Task are not yet ready\n\nObjectPath data definition:\n\n```\ntype ObjectPath struct {\n\tObjectType\tstring `json:\"objectType\"` // Task, Project\n\tId \t\t\tstring `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId \tstring `json:\"realmId\"`\n}\n```\n\n_note: the core implementation offers the basic adding and retrieving functionality, but it's up to the client realm using the `zentasktic` package to call them when an object is moved from one Realm to another._\n\n## Example Workflow\n\n```\npackage example_zentasktic\n\nimport \"gno.land/p/demo/zentasktic\"\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n}\n\n// initializing a task, assuming we get the value POSTed by some call to the current realm\n\nnewTask := zentasktic.Task{Id: \"20\", Body: \"Buy milk\"}\nztm.AddTask(newTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n...\n\neditedTask := zentasktic.Task{Id: \"20\", Body: \"Buy fresh milk\"}\nztm.EditTask(editedTask)\n\n...\n\n// moving it to Decide\n\nztm.MoveTaskToRealm(\"20\", \"2\")\n\n// adding context, due date and alert, assuming they're received from other calls\n\nshoppingContext := zcm.GetContextById(\"2\")\n\ncerr := zcm.AddContextToTask(ztm, shoppingContext, editedTask)\n\nderr := ztm.SetTaskDueDate(editedTask.Id, \"2024-04-10\")\nnow := time.Now() // replace with the actual time of the alert\nalertTime := now.Format(\"2006-01-02 15:04:05\")\naerr := ztm.SetTaskAlert(editedTask.Id, alertTime)\n\n...\n\n// move the Task to Do\n\nztm.MoveTaskToRealm(editedTask.Id, \"2\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"2\"}\nzom.AddPath(taskPath)\n\n// after the task is done, we sent it back to Assess\n\nztm.MoveTaskToRealm(editedTask.Id,\"1\")\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"1\"}\nzom.AddPath(taskPath)\n\n// from here, we can add it to a collection\n\nmyCollection := zcm.GetCollectionById(\"1\")\n\nzcm.AddTaskToCollection(ztm, myCollection, editedTask)\n\n// if we want to keep track of the object zen status, we update the object path\ntaskPath := zentasktic.ObjectPath{ObjectType: \"task\", Id: \"20\", RealmId: \"4\"}\nzom.AddPath(taskPath)\n\n```\n\nAll tests are in the `*_test.gno` files, e.g. `tasks_test.gno`, `projects_test.gno`, etc." + }, + { + "name": "collections.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Collection struct {\n\tId \t\t\tstring `json:\"collectionId\"`\n\tRealmId \tstring `json:\"collectionRealmId\"`\n\tName \t\tstring `json:\"collectionName\"`\n\tTasks\t\t[]Task `json:\"collectionTasks\"`\n\tProjects\t[]Project `json:\"collectionProjects\"`\n}\n\ntype ZCollectionManager struct {\n\tCollections *avl.Tree \n\tCollectionTasks *avl.Tree\n\tCollectionProjects *avl.Tree \n}\n\nfunc NewZCollectionManager() *ZCollectionManager {\n return &ZCollectionManager{\n Collections: avl.NewTree(),\n CollectionTasks: avl.NewTree(),\n CollectionProjects: avl.NewTree(),\n }\n}\n\n\n// actions\n\nfunc (zcolm *ZCollectionManager) AddCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrCollectionIdAlreadyExists\n\t\t}\n\t}\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) EditCollection(c Collection) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\t\n\tzcolm.Collections.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveCollection(c Collection) (err error) {\n // implementation\n if zcolm.Collections.Size() != 0 {\n collectionInterface, exist := zcolm.Collections.Get(c.Id)\n if !exist {\n return ErrCollectionIdNotFound\n }\n collection := collectionInterface.(Collection)\n\n _, removed := zcolm.Collections.Remove(collection.Id)\n if !removed {\n return ErrCollectionNotRemoved\n }\n\n if zcolm.CollectionTasks.Size() != 0 {\n _, removedTasks := zcolm.CollectionTasks.Remove(collection.Id)\n if !removedTasks {\n return ErrCollectionNotRemoved\n }\t\n }\n\n if zcolm.CollectionProjects.Size() != 0 {\n _, removedProjects := zcolm.CollectionProjects.Remove(collection.Id)\n if !removedProjects {\n return ErrCollectionNotRemoved\n }\t\n }\n }\n return nil\n}\n\n\nfunc (zcolm *ZCollectionManager) AddProjectToCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no projects yet, initialize the slice.\n\t\texistingCollectionProjects = []Project{}\n\t} else {\n\t\tprojects, ok := existingCollectionProjects.([]Project)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsProjectsNotFound\n\t\t}\n\t\texistingCollectionProjects = projects\n\t}\n\tp.RealmId = \"4\"\n\tif err := zpm.EditProject(p); err != nil {\n\t\treturn err\n\t}\n\tupdatedProjects := append(existingCollectionProjects.([]Project), p)\n\tzcolm.CollectionProjects.Set(c.Id, updatedProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) AddTaskToCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collections has no tasks yet, initialize the slice.\n\t\texistingCollectionTasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingCollectionTasks.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrCollectionsTasksNotFound\n\t\t}\n\t\texistingCollectionTasks = tasks\n\t}\n\tt.RealmId = \"4\"\n\tif err := ztm.EditTask(t); err != nil {\n\t\treturn err\n\t}\n\tupdatedTasks := append(existingCollectionTasks.([]Task), t)\n\tzcolm.CollectionTasks.Set(c.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveProjectFromCollection(zpm *ZProjectManager, c Collection, p Project) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionProjects, texist := zcolm.CollectionProjects.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no projects yet, return appropriate error\n\t\treturn ErrCollectionsProjectsNotFound\n\t}\n\n\t// Find the index of the project to be removed.\n\tvar index int = -1\n\tfor i, project := range existingCollectionProjects.([]Project) {\n\t\tif project.Id == p.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the project was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default we send it back to Assess\n\t\tp.RealmId = \"1\"\n\t\tzpm.EditProject(p)\n\t\texistingCollectionProjects = append(existingCollectionProjects.([]Project)[:index], existingCollectionProjects.([]Project)[index+1:]...)\n\t} else {\n\t\t// Project not found in the collection\n\t\treturn ErrProjectByIdNotFound \n\t}\n\tzcolm.CollectionProjects.Set(c.Id, existingCollectionProjects)\n\n\treturn nil\n}\n\nfunc (zcolm *ZCollectionManager) RemoveTaskFromCollection(ztm *ZTaskManager, c Collection, t Task) (err error) {\n\t// implementation\n\tif zcolm.Collections.Size() != 0 {\n\t\t_, exist := zcolm.Collections.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrCollectionIdNotFound\n\t\t}\n\t}\n\n\texistingCollectionTasks, texist := zcolm.CollectionTasks.Get(c.Id)\n\tif !texist {\n\t\t// If the collection has no tasks yet, return appropriate error\n\t\treturn ErrCollectionsTasksNotFound\n\t}\n\n\t// Find the index of the task to be removed.\n\tvar index int = -1\n\tfor i, task := range existingCollectionTasks.([]Task) {\n\t\tif task.Id == t.Id {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If the task was found, we remove it from the slice.\n\tif index != -1 {\n\t\t// by default, we send the task to Assess\n\t\tt.RealmId = \"1\"\n\t\tztm.EditTask(t)\n\t\texistingCollectionTasks = append(existingCollectionTasks.([]Task)[:index], existingCollectionTasks.([]Task)[index+1:]...)\n\t} else {\n\t\t// Task not found in the collection\n\t\treturn ErrTaskByIdNotFound \n\t}\n\tzcolm.CollectionTasks.Set(c.Id, existingCollectionTasks)\n\n\treturn nil\n}\n\n// getters\n\nfunc (zcolm *ZCollectionManager) GetCollectionById(collectionId string) (Collection, error) {\n if zcolm.Collections.Size() != 0 {\n cInterface, exist := zcolm.Collections.Get(collectionId)\n if exist {\n collection := cInterface.(Collection)\n // look for collection Tasks, Projects\n existingCollectionTasks, texist := zcolm.CollectionTasks.Get(collectionId)\n if texist {\n collection.Tasks = existingCollectionTasks.([]Task)\n }\n existingCollectionProjects, pexist := zcolm.CollectionProjects.Get(collectionId)\n if pexist {\n collection.Projects = existingCollectionProjects.([]Project)\n }\n return collection, nil\n }\n return Collection{}, ErrCollectionByIdNotFound\n }\n return Collection{}, ErrCollectionByIdNotFound\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionTasks(c Collection) (tasks []Task, err error) {\n\t\n\tif zcolm.CollectionTasks.Size() != 0 {\n\t\ttask, exist := zcolm.CollectionTasks.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionTasks, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsTasksNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Task\n\t\t\texistingCollectionTasks, ok := task.([]Task)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrTaskFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionTasks, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetCollectionProjects(c Collection) (projects []Project, err error) {\n\t\n\tif zcolm.CollectionProjects.Size() != 0 {\n\t\tproject, exist := zcolm.CollectionProjects.Get(c.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in CollectionProjets, we don't have to return anything\n\t\t\treturn nil, ErrCollectionsProjectsNotFound\n\t\t} else {\n\t\t\t// type assertion to convert interface{} to []Projet\n\t\t\texistingCollectionProjects, ok := project.([]Project)\n\t\t\tif !ok {\n\t\t\t\treturn nil, ErrProjectFailedToAssert\n\t\t\t}\n\t\t\treturn existingCollectionProjects, nil\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (zcolm *ZCollectionManager) GetAllCollections() (collections string, err error) {\n\t// implementation\n\tvar allCollections []Collection\n\t\n\t// Iterate over the Collections AVL tree to collect all Project objects.\n\t\n\tzcolm.Collections.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif collection, ok := value.(Collection); ok {\n\t\t\t// get collection tasks, if any\n\t\t\tcollectionTasks, _ := zcolm.GetCollectionTasks(collection)\n\t\t\tif collectionTasks != nil {\n\t\t\t\tcollection.Tasks = collectionTasks\n\t\t\t}\n\t\t\t// get collection prokects, if any\n\t\t\tcollectionProjects, _ := zcolm.GetCollectionProjects(collection)\n\t\t\tif collectionProjects != nil {\n\t\t\t\tcollection.Projects = collectionProjects\n\t\t\t}\n\t\t\tallCollections = append(allCollections, collection)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a CollectionsObject with all collected tasks.\n\tcollectionsObject := CollectionsObject{\n\t\tCollections: allCollections,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the collections into JSON.\n\tmarshalledCollections, merr := collectionsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t} \n\treturn string(marshalledCollections), nil\n} " + }, + { + "name": "collections_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\n\nfunc Test_AddCollection(t *testing.T) {\n \n collection := Collection{Id: \"1\", RealmId: \"4\", Name: \"First collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := collection.AddCollection()\n if cerr != ErrCollectionIdAlreadyExists {\n t.Errorf(\"Expected ErrCollectionIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveCollection(t *testing.T) {\n \n collection := Collection{Id: \"20\", RealmId: \"4\", Name: \"Removable collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n retrievedCollection, rerr := GetCollectionById(collection.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added collection\")\n }\n\n // Test removing a collection\n terr := retrievedCollection.RemoveCollection()\n if terr != ErrCollectionNotRemoved {\n t.Errorf(\"Expected ErrCollectionNotRemoved, got %v\", terr)\n }\n}\n\nfunc Test_EditCollection(t *testing.T) {\n \n collection := Collection{Id: \"2\", RealmId: \"4\", Name: \"Second collection\",}\n\n // Test adding a collection successfully.\n err := collection.AddCollection()\n if err != nil {\n t.Errorf(\"Failed to add collection: %v\", err)\n }\n\n // Test editing the collection\n editedCollection := Collection{Id: collection.Id, RealmId: collection.RealmId, Name: \"Edited collection\",}\n cerr := editedCollection.EditCollection()\n if cerr != nil {\n t.Errorf(\"Failed to edit the collection\")\n }\n\n retrievedCollection, _ := GetCollectionById(editedCollection.Id)\n if retrievedCollection.Name != \"Edited collection\" {\n t.Errorf(\"Collection was not edited\")\n }\n}\n\nfunc Test_AddProjectToCollection(t *testing.T){\n // Example Collection and Projects\n col := Collection{Id: \"1\", Name: \"First collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n project Project\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n project: prj,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"200\", Name: \"Collection 200\", RealmId: \"4\",},\n project: prj,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddProjectToCollection(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddProjectToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddProjectToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the project is added to the collection's tasks.\n if !tt.wantErr {\n projects, exist := CollectionProjects.Get(tt.collection.Id)\n if !exist || len(projects.([]Project)) == 0 {\n t.Errorf(\"Project was not added to the collection\")\n } else {\n found := false\n for _, project := range projects.([]Project) {\n if project.Id == tt.project.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Project was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_AddTaskToCollection(t *testing.T){\n // Example Collection and Tasks\n col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n Collections.Set(col.Id, col) // Mock existing collections\n\n tests := []struct {\n name string\n collection Collection\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing collection\",\n collection: col,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing collection\",\n collection: Collection{Id: \"210\", Name: \"Collection 210\", RealmId: \"4\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrCollectionIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.collection.AddTaskToCollection(tt.task)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AddTaskToCollection() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AddTaskToCollection() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the collection's tasks.\n if !tt.wantErr {\n tasks, exist := CollectionTasks.Get(tt.collection.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not added to the collection\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the collection\")\n }\n }\n }\n })\n }\n}\n\nfunc Test_RemoveProjectFromCollection(t *testing.T){\n // Setup:\n\tcollection := Collection{Id: \"300\", Name: \"Collection 300\",}\n\tproject1 := Project{Id: \"21\", Body: \"Project 21\", RealmId: \"1\",}\n\tproject2 := Project{Id: \"22\", Body: \"Project 22\", RealmId: \"1\",}\n\n collection.AddCollection()\n project1.AddProject()\n project2.AddProject()\n collection.AddProjectToCollection(project1)\n collection.AddProjectToCollection(project2)\n\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing project from collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove project from non-existing collection\",\n\t\t\tproject: project1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing project from collection\",\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveProjectFromCollection(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the project is no longer part of the collection's projects\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tprojects, _ := CollectionProjects.Get(tt.collection.Id)\n\t\t\t\t\tfor _, project := range projects.([]Project) {\n\t\t\t\t\t\tif project.Id == tt.project.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: project was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_RemoveTaskFromCollection(t *testing.T){\n // setup, re-using parts from Test_AddTaskToCollection\n\tcollection := Collection{Id: \"40\", Name: \"Collection 40\",}\n task1 := Task{Id: \"40\", Body: \"Task 40\", RealmId: \"1\",}\n\n collection.AddCollection()\n task1.AddTask()\n collection.AddTaskToCollection(task1)\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tcollection Collection\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Remove existing task from collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: collection,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove task from non-existing collection\",\n\t\t\ttask: task1,\n\t\t\tcollection: Collection{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrCollectionIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to remove non-existing task from collection\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tcollection: collection,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.collection.RemoveTaskFromCollection(tt.task)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful removal, verify the task is no longer part of the collection's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := CollectionTasks.Get(tt.collection.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the collection\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetCollectionById(t *testing.T){\n // test getting a non-existing collection\n nonCollection, err := GetCollectionById(\"0\")\n if err != ErrCollectionByIdNotFound {\n t.Fatalf(\"Expected ErrCollectionByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct collection by id\n correctCollection, err := GetCollectionById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get collection by id, error: %v\", err)\n }\n\n if correctCollection.Name != \"First collection\" {\n t.Fatalf(\"Got the wrong collection, with name: %v\", correctCollection.Name)\n }\n}\n\nfunc Test_GetCollectionTasks(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"2\", Name: \"Second Collection\", RealmId: \"4\",}\n tsk := Task{Id: \"30\", Body: \"Task 30\", RealmId: \"1\",}\n\n collection, cerr := GetCollectionById(\"2\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionTasks, pterr := collection.GetCollectionTasks()\n if len(collectionTasks) == 0 {\n t.Errorf(\"GetCollectionTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveTaskFromCollection(tsk)\n if dtterr != nil {\n t.Errorf(\"RemoveTaskFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoTasks, pterr := collection.GetCollectionTasks()\n if len(collectionWithNoTasks) != 0 {\n t.Errorf(\"GetCollectionTasks() after detach failed, %v\", pterr)\n }\n\n // add task back to collection, for tests mockup integrity\n collection.AddTaskToCollection(tsk)\n}\n\nfunc Test_GetCollectionProjects(t *testing.T) {\n // retrieving objects based on these mocks\n //col := Collection{Id: \"1\", Name: \"First Collection\", RealmId: \"4\",}\n prj := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"}\n\n collection, cerr := GetCollectionById(\"1\")\n if cerr != nil {\n t.Errorf(\"GetCollectionById() failed, %v\", cerr)\n }\n\n collectionProjects, pterr := collection.GetCollectionProjects()\n if len(collectionProjects) == 0 {\n t.Errorf(\"GetCollectionProjects() failed, %v\", pterr)\n }\n\n // test detaching from an existing collection\n dtterr := collection.RemoveProjectFromCollection(prj)\n if dtterr != nil {\n t.Errorf(\"RemoveProjectFromCollection() failed, %v\", dtterr)\n }\n\n collectionWithNoProjects, pterr := collection.GetCollectionProjects()\n if len(collectionWithNoProjects) != 0 {\n t.Errorf(\"GetCollectionProjects() after detach failed, %v\", pterr)\n }\n\n // add project back to collection, for tests mockup integrity\n collection.AddProjectToCollection(prj)\n}\n\nfunc Test_GetAllCollections(t *testing.T){\n // mocking the collections based on previous tests\n // TODO: add isolation?\n knownCollections := []Collection{\n {\n Id: \"1\",\n RealmId: \"4\",\n Name: \"First collection\",\n Tasks: nil, \n Projects: []Project{\n {\n Id: \"10\",\n ContextId: \"2\",\n RealmId: \"4\",\n Tasks: nil, \n Body: \"Project 10\",\n Due: \"2024-01-01\",\n },\n },\n },\n {\n Id: \"2\",\n RealmId: \"4\",\n Name: \"Second Collection\",\n Tasks: []Task{\n {\n Id:\"30\",\n ProjectId:\"\",\n ContextId:\"\",\n RealmId:\"4\",\n Body:\"Task 30\",\n Due:\"\",\n Alert:\"\",\n },\n },\n Projects: nil, \n },\n {\n Id:\"20\",\n RealmId:\"4\",\n Name:\"Removable collection\",\n Tasks: nil,\n Projects: nil,\n },\n {\n Id: \"300\",\n Name: \"Collection 300\",\n Tasks: nil, \n Projects: []Project {\n {\n Id:\"22\",\n ContextId:\"\",\n RealmId:\"4\",\n Tasks: nil,\n Body:\"Project 22\",\n Due:\"\",\n },\n }, \n },\n {\n Id: \"40\",\n Name: \"Collection 40\",\n Tasks: nil, \n Projects: nil, \n },\n }\n \n\n // Manually marshal the known collections to create the expected outcome.\n collectionsObject := CollectionsObject{Collections: knownCollections}\n expected, err := collectionsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known collections: %v\", err)\n }\n\n // Execute GetAllCollections() to get the actual outcome.\n actual, err := GetAllCollections()\n if err != nil {\n t.Fatalf(\"GetAllCollections() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual collections JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n\n\n\n" + }, + { + "name": "contexts.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Context struct {\n\tId string `json:\"contextId\"`\n\tName string `json:\"contextName\"`\n}\n\ntype ZContextManager struct {\n\tContexts *avl.Tree\n}\n\nfunc NewZContextManager() *ZContextManager {\n\treturn &ZContextManager{\n\t\tContexts: avl.NewTree(),\n\t}\n}\n\n// Actions\n\nfunc (zcm *ZContextManager) AddContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif exist {\n\t\t\treturn ErrContextIdAlreadyExists\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) EditContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\t_, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t}\n\tzcm.Contexts.Set(c.Id, c)\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) RemoveContext(c Context) error {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcontext, exist := zcm.Contexts.Get(c.Id)\n\t\tif !exist {\n\t\t\treturn ErrContextIdNotFound\n\t\t}\n\t\t_, removed := zcm.Contexts.Remove(context.(Context).Id)\n\t\tif !removed {\n\t\t\treturn ErrContextNotRemoved\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToTask(ztm *ZTaskManager, c Context, t Task) error {\n\ttaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif t.RealmId == \"2\" {\n\t\ttask := taskInterface.(Task)\n\t\ttask.ContextId = c.Id\n\t\tztm.Tasks.Set(t.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProject(zpm *ZProjectManager, c Context, p Project) error {\n\tprojectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\tif p.RealmId == \"2\" {\n\t\tproject := projectInterface.(Project)\n\t\tproject.ContextId = c.Id\n\t\tzpm.Projects.Set(p.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zcm *ZContextManager) AddContextToProjectTask(zpm *ZProjectManager, c Context, p Project, projectTaskId string) error {\n\t\n\t_, cexist := zcm.Contexts.Get(c.Id)\n\tif !cexist {\n\t\treturn ErrContextIdNotFound\n\t}\n\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].ContextId = c.Id\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zcm *ZContextManager) GetContextById(contextId string) (Context, error) {\n\tif zcm.Contexts.Size() != 0 {\n\t\tcInterface, exist := zcm.Contexts.Get(contextId)\n\t\tif exist {\n\t\t\treturn cInterface.(Context), nil\n\t\t}\n\t\treturn Context{}, ErrContextIdNotFound\n\t}\n\treturn Context{}, ErrContextIdNotFound\n}\n\nfunc (zcm *ZContextManager) GetAllContexts() (string) {\n\tvar allContexts []Context\n\n\t// Iterate over the Contexts AVL tree to collect all Context objects.\n\tzcm.Contexts.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif context, ok := value.(Context); ok {\n\t\t\tallContexts = append(allContexts, context)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ContextsObject with all collected contexts.\n\tcontextsObject := &ContextsObject{\n\t\tContexts: allContexts,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the contexts into JSON.\n\tmarshalledContexts, merr := contextsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t}\n\treturn string(marshalledContexts)\n}\n\n" + }, + { + "name": "contexts_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddContext(t *testing.T) {\n \n context := Context{Id: \"1\", Name: \"Work\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := context.AddContext()\n if cerr != ErrContextIdAlreadyExists {\n t.Errorf(\"Expected ErrContextIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_EditContext(t *testing.T) {\n \n context := Context{Id: \"2\", Name: \"Home\"}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n // Test editing the context\n editedContext := Context{Id: \"2\", Name: \"Shopping\"}\n cerr := editedContext.EditContext()\n if cerr != nil {\n t.Errorf(\"Failed to edit the context\")\n }\n\n retrievedContext, _ := GetContextById(editedContext.Id)\n if retrievedContext.Name != \"Shopping\" {\n t.Errorf(\"Context was not edited\")\n }\n}\n\nfunc Test_RemoveContext(t *testing.T) {\n \n context := Context{Id: \"4\", Name: \"Gym\",}\n\n // Test adding a context successfully.\n err := context.AddContext()\n if err != nil {\n t.Errorf(\"Failed to add context: %v\", err)\n }\n\n retrievedContext, rerr := GetContextById(context.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added context\")\n }\n // Test removing a context\n cerr := retrievedContext.RemoveContext()\n if cerr != ErrContextNotRemoved {\n t.Errorf(\"Expected ErrContextNotRemoved, got %v\", cerr)\n }\n}\n\nfunc Test_AddContextToTask(t *testing.T) {\n\n task := Task{Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n taskInDecide, exist := Tasks.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Task with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToTask(taskInDecide.(Task))\n if derr != nil {\n t.Errorf(\"Could not add context to a task in Decide, err %v\", derr)\n }\n}\n\nfunc Test_AddContextToProject(t *testing.T) {\n\n project := Project{Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n projectInDecide, exist := Projects.Get(\"10\")\n\tif !exist {\n\t\tt.Errorf(\"Project with id 10 not found\")\n\t}\n\t// check if context exists\n\tcontextToAdd, cexist := Contexts.Get(\"2\")\n\tif !cexist {\n\t\tt.Errorf(\"Context with id 2 not found\")\n\t}\n\n derr := contextToAdd.(Context).AddContextToProject(projectInDecide.(Project))\n if derr != nil {\n t.Errorf(\"Could not add context to a project in Decide, err %v\", derr)\n }\n}\n\nfunc Test_GetAllContexts(t *testing.T) {\n \n // mocking the contexts based on previous tests\n // TODO: add isolation?\n knownContexts := []Context{\n {Id: \"1\", Name: \"Work\",},\n {Id: \"2\", Name: \"Shopping\",},\n {Id: \"4\", Name: \"Gym\",},\n }\n\n // Manually marshal the known contexts to create the expected outcome.\n contextsObject := ContextsObject{Contexts: knownContexts}\n expected, err := contextsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known contexts: %v\", err)\n }\n\n // Execute GetAllContexts() to get the actual outcome.\n actual, err := GetAllContexts()\n if err != nil {\n t.Fatalf(\"GetAllContexts() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual contexts JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n" + }, + { + "name": "core.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n// holding the path of an object since creation\n// each time we move an object from one realm to another, we add to its path\ntype ObjectPath struct {\n\tObjectType string `json:\"objectType\"` // Task, Project\n\tId string `json:\"id\"` // this is the Id of the object moved, Task, Project\n\tRealmId string `json:\"realmId\"`\n}\n\ntype ZObjectPathManager struct {\n\tPaths avl.Tree\n\tPathId int\n}\n\nfunc NewZObjectPathManager() *ZObjectPathManager {\n\treturn &ZObjectPathManager{\n\t\tPaths: *avl.NewTree(),\n\t\tPathId: 1,\n\t}\n}\n\nfunc (zopm *ZObjectPathManager) AddPath(o ObjectPath) error {\n\tzopm.PathId++\n\tupdated := zopm.Paths.Set(strconv.Itoa(zopm.PathId), o)\n\tif !updated {\n\t\treturn ErrObjectPathNotUpdated\n\t}\n\treturn nil\n}\n\nfunc (zopm *ZObjectPathManager) GetObjectJourney(objectType string, objectId string) (string, error) {\n\tvar objectPaths []ObjectPath\n\n\t// Iterate over the Paths AVL tree to collect all ObjectPath objects.\n\tzopm.Paths.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif objectPath, ok := value.(ObjectPath); ok {\n\t\t\tif objectPath.ObjectType == objectType && objectPath.Id == objectId {\n\t\t\t\tobjectPaths = append(objectPaths, objectPath)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create an ObjectJourney with all collected paths.\n\tobjectJourney := &ObjectJourney{\n\t\tObjectPaths: objectPaths,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the journey into JSON.\n\tmarshalledJourney, merr := objectJourney.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\", merr\n\t}\n\treturn string(marshalledJourney), nil\n}\n\n\n// GetZenStatus\n/* todo: leave it to the client\nfunc () GetZenStatus() (zenStatus string, err error) {\n\t// implementation\n}\n*/\n" + }, + { + "name": "errors.gno", + "body": "package zentasktic\n\nimport \"errors\"\n\nvar (\n\tErrTaskNotEditable \t= errors.New(\"Task is not editable\")\n\tErrProjectNotEditable = errors.New(\"Project is not editable\")\n\tErrProjectIdNotFound\t\t\t= errors.New(\"Project id not found\")\n\tErrTaskIdNotFound\t\t\t\t= errors.New(\"Task id not found\")\n\tErrTaskFailedToAssert\t\t\t= errors.New(\"Failed to assert Task type\")\n\tErrProjectFailedToAssert\t\t= errors.New(\"Failed to assert Project type\")\n\tErrProjectTasksNotFound\t\t\t= errors.New(\"Could not get tasks for project\")\n\tErrCollectionsProjectsNotFound\t= errors.New(\"Could not get projects for this collection\")\n\tErrCollectionsTasksNotFound\t\t= errors.New(\"Could not get tasks for this collection\")\n\tErrTaskIdAlreadyExists\t\t\t= errors.New(\"A task with the provided id already exists\")\n\tErrCollectionIdAlreadyExists\t= errors.New(\"A collection with the provided id already exists\")\n\tErrProjectIdAlreadyExists\t\t= errors.New(\"A project with the provided id already exists\")\n\tErrTaskByIdNotFound\t\t\t\t= errors.New(\"Can't get task by id\")\n\tErrProjectByIdNotFound\t\t\t= errors.New(\"Can't get project by id\")\n\tErrCollectionByIdNotFound\t\t= errors.New(\"Can't get collection by id\")\n\tErrTaskNotRemovable\t\t\t\t= errors.New(\"Cannot remove a task directly from this realm\")\n\tErrProjectNotRemovable\t\t\t= errors.New(\"Cannot remove a project directly from this realm\")\n\tErrProjectTasksNotRemoved\t\t= errors.New(\"Project tasks were not removed\")\n\tErrTaskNotRemoved\t\t\t\t= errors.New(\"Task was not removed\")\n\tErrTaskNotInAssessRealm\t\t\t= errors.New(\"Task is not in Assess, cannot edit Body\")\n\tErrProjectNotInAssessRealm\t\t= errors.New(\"Project is not in Assess, cannot edit Body\")\n\tErrContextIdAlreadyExists\t\t= errors.New(\"A context with the provided id already exists\")\n\tErrContextIdNotFound\t\t\t= errors.New(\"Context id not found\")\n\tErrCollectionIdNotFound\t\t\t= errors.New(\"Collection id not found\")\n\tErrContextNotRemoved\t\t\t= errors.New(\"Context was not removed\")\n\tErrProjectNotRemoved\t\t\t= errors.New(\"Project was not removed\")\n\tErrCollectionNotRemoved\t\t\t= errors.New(\"Collection was not removed\")\n\tErrObjectPathNotUpdated\t\t\t= errors.New(\"Object path wasn't updated\")\n\tErrInvalidateDateFormat\t\t\t= errors.New(\"Invalida date format\")\n\tErrInvalidDateFilterType\t\t= errors.New(\"Invalid date filter type\")\n\tErrRealmIdAlreadyExists\t\t\t= errors.New(\"A realm with the same id already exists\")\n\tErrRealmIdNotAllowed\t\t\t= errors.New(\"This is a reserved realm id\")\n\tErrRealmIdNotFound\t\t\t\t= errors.New(\"Realm id not found\")\n\tErrRealmNotRemoved\t\t\t\t= errors.New(\"Realm was not removed\")\n)" + }, + { + "name": "marshals.gno", + "body": "package zentasktic\n\nimport (\n\t\"bytes\"\n)\n\n\ntype ContextsObject struct {\n\tContexts\t[]Context\n}\n\ntype TasksObject struct {\n\tTasks\t[]Task\n}\n\ntype ProjectsObject struct {\n\tProjects\t[]Project\n}\n\ntype CollectionsObject struct {\n\tCollections\t[]Collection\n}\n\ntype RealmsObject struct {\n\tRealms\t[]Realm\n}\n\ntype ObjectJourney struct {\n\tObjectPaths []ObjectPath\n}\n\nfunc (c Context) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"contextId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"contextName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (cs ContextsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"contexts\":[`)\n\t\n\tfor i, context := range cs.Contexts {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcontextJSON, cerr := context.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(contextJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (t Task) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"taskId\":\"`)\n\tb.WriteString(t.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskProjectId\":\"`)\n\tb.WriteString(t.ProjectId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskContextId\":\"`)\n\tb.WriteString(t.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskRealmId\":\"`)\n\tb.WriteString(t.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskBody\":\"`)\n\tb.WriteString(t.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskDue\":\"`)\n\tb.WriteString(t.Due)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"taskAlert\":\"`)\n\tb.WriteString(t.Alert)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ts TasksObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"tasks\":[`)\n\tfor i, task := range ts.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\ttaskJSON, cerr := task.MarshalJSON()\n\t\tif cerr == nil {\n\t\t\tb.WriteString(string(taskJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (p Project) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"projectId\":\"`)\n\tb.WriteString(p.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectContextId\":\"`)\n\tb.WriteString(p.ContextId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectRealmId\":\"`)\n\tb.WriteString(p.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectTasks\":[`)\n\n\tif len(p.Tasks) != 0 {\n\t\tfor i, projectTask := range p.Tasks {\n\t\t\tif i > 0 {\n\t\t\t\tb.WriteString(`,`)\n\t\t\t}\n\t\t\tprojectTaskJSON, perr := projectTask.MarshalJSON()\n\t\t\tif perr == nil {\n\t\t\t\tb.WriteString(string(projectTaskJSON))\n\t\t\t}\n\t\t}\n\t}\n\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"projectBody\":\"`)\n\tb.WriteString(p.Body)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"projectDue\":\"`)\n\tb.WriteString(p.Due)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (ps ProjectsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"projects\":[`)\n\tfor i, project := range ps.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tprojectJSON, perr := project.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(projectJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (c Collection) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"collectionId\":\"`)\n\tb.WriteString(c.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionRealmId\":\"`)\n\tb.WriteString(c.RealmId)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionName\":\"`)\n\tb.WriteString(c.Name)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"collectionTasks\":[`)\n\tfor i, collectionTask := range c.Tasks {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionTaskJSON, perr := collectionTask.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionTaskJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteString(`\"collectionProjects\":[`)\n\tfor i, collectionProject := range c.Projects {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionProjectJSON, perr := collectionProject.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionProjectJSON))\n\t\t}\n\t}\n\tb.WriteString(`],`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (co CollectionsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"collections\":[`)\n\tfor i, collection := range co.Collections {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tcollectionJSON, perr := collection.MarshalJSON()\n\t\tif perr == nil {\n\t\t\tb.WriteString(string(collectionJSON))\n\t\t}\n\t}\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (r Realm) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(r.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmName\":\"`)\n\tb.WriteString(r.Name)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (rs RealmsObject) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"realms\":[`)\n\t\n\tfor i, realm := range rs.Realms {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\trealmJSON, rerr := realm.MarshalJSON()\n\t\tif rerr == nil {\n\t\t\tb.WriteString(string(realmJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n\nfunc (op ObjectPath) MarshalJSON() ([]byte, error) {\n\n\tvar b bytes.Buffer\n\t\n\tb.WriteByte('{')\n\n\tb.WriteString(`\"objectType\":\"`)\n\tb.WriteString(op.ObjectType)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"id\":\"`)\n\tb.WriteString(op.Id)\n\tb.WriteString(`\",`)\n\n\tb.WriteString(`\"realmId\":\"`)\n\tb.WriteString(op.RealmId)\n\tb.WriteString(`\"`)\n\n\tb.WriteByte('}')\n\n\treturn b.Bytes(), nil\n}\n\nfunc (oj ObjectJourney) MarshalJSON() ([]byte, error) {\n\tvar b bytes.Buffer\n\n\tb.WriteString(`{\"objectJourney\":[`)\n\t\n\tfor i, objectPath := range oj.ObjectPaths {\n\t\tif i > 0 {\n\t\t\tb.WriteString(`,`)\n\t\t}\n\t\tobjectPathJSON, oerr := objectPath.MarshalJSON()\n\t\tif oerr == nil {\n\t\t\tb.WriteString(string(objectPathJSON))\n\t\t}\n\t}\n\n\tb.WriteString(`]}`)\n\n\treturn b.Bytes(), nil\n}\n" + }, + { + "name": "projects.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\n\ntype Project struct {\n\tId \t\t\tstring `json:\"projectId\"`\n\tContextId\tstring `json:\"projectContextId\"`\n\tRealmId \tstring `json:\"projectRealmId\"`\n\tTasks\t\t[]Task `json:\"projectTasks\"`\n\tBody \t\tstring `json:\"projectBody\"`\n\tDue\t\t\tstring `json:\"projectDue\"`\n}\n\ntype ZProjectManager struct {\n\tProjects *avl.Tree // projectId -> Project\n\tProjectTasks *avl.Tree // projectId -> []Task\n}\n\n\nfunc NewZProjectManager() *ZProjectManager {\n\treturn &ZProjectManager{\n\t\tProjects: avl.NewTree(),\n\t\tProjectTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (zpm *ZProjectManager) AddProject(p Project) (err error) {\n\t// implementation\n\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif exist {\n\t\t\treturn ErrProjectIdAlreadyExists\n\t\t}\n\t}\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) RemoveProject(p Project) (err error) {\n\t// implementation, remove from ProjectTasks too\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t // project is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingProject.RealmId != \"1\" && existingProject.RealmId != \"4\" {\n\t\treturn ErrProjectNotRemovable\n\t}\n\n\t_, removed := zpm.Projects.Remove(existingProject.Id)\n\tif !removed {\n\t\treturn ErrProjectNotRemoved\n\t}\n\n\t// manage project tasks, if any\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\t_, exist := zpm.ProjectTasks.Get(existingProject.Id)\n\t\tif !exist {\n\t\t\t// if there's no record in ProjectTasks, we don't have to remove anything\n\t\t\treturn nil\n\t\t} else {\n\t\t\t_, removed := zpm.ProjectTasks.Remove(existingProject.Id)\n\t\t\tif !removed {\n\t\t\t\treturn ErrProjectTasksNotRemoved\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProject(p Project) (err error) {\n\t// implementation, get project by Id and replace the object\n\t// this is for the project body and realm, project tasks are managed in the Tasks object\n\texistingProject := Project{}\n\tif zpm.Projects.Size() != 0 {\n\t\t_, exist := zpm.Projects.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn ErrProjectIdNotFound\n\t\t}\n\t}\n\t\n\t// project Body is editable only when project is in Assess, RealmId = \"1\"\n\tif p.RealmId != \"1\" {\n\t\tif p.Body != existingProject.Body {\n\t\t\treturn ErrProjectNotInAssessRealm\n\t\t}\n\t}\n\n\tzpm.Projects.Set(p.Id, p)\n\treturn nil\n}\n\n// helper function, we can achieve the same with EditProject() above\n/*func (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) (err error) {\n\t// implementation\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\texistingProject.RealmId = realmId\n\tzpm.Projects.Set(projectId, existingProject)\n\treturn nil\n}*/\n\nfunc (zpm *ZProjectManager) MoveProjectToRealm(projectId string, realmId string) error {\n\t// Get the existing project from the Projects map\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\t// Set the project's RealmId to the new RealmId\n\texistingProject.RealmId = realmId\n\n\t// Get the existing project tasks from the ProjectTasks map\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\n\t// Iterate through the project's tasks and set their RealmId to the new RealmId\n\tfor i := range tasks {\n\t\ttasks[i].RealmId = realmId\n\t}\n\n\t// Set the updated tasks back into the ProjectTasks map\n\tzpm.ProjectTasks.Set(projectId, tasks)\n\n\t// Set the updated project back into the Projects map\n\tzpm.Projects.Set(projectId, existingProject)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n // Get the existing project from the Projects map\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n // Get the existing project tasks from the ProjectTasks map\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n\n // Iterate through the project's tasks to find the task to be updated\n var taskFound bool\n for i, task := range tasks {\n if task.Id == projectTaskId {\n tasks[i].RealmId = \"4\" // Change the RealmId to \"4\"\n taskFound = true\n break\n }\n }\n\n if !taskFound {\n return ErrTaskByIdNotFound\n }\n\n // Set the updated tasks back into the ProjectTasks map\n zpm.ProjectTasks.Set(existingProject.Id, tasks)\n\n return nil\n}\n\n\nfunc (zpm *ZProjectManager) GetProjectTasks(p Project) (tasks []Task, err error) {\n\t// implementation, query ProjectTasks and return the []Tasks object\n\tvar existingProjectTasks []Task\n\n\tif zpm.ProjectTasks.Size() != 0 {\n\t\tprojectTasksInterface, exist := zpm.ProjectTasks.Get(p.Id)\n\t\tif !exist {\n\t\t\treturn nil, ErrProjectTasksNotFound\n\t\t}\n\t\texistingProjectTasks = projectTasksInterface.([]Task)\n\t\treturn existingProjectTasks, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectDueDate(projectId string, dueDate string) (err error) {\n\tprojectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\tproject := projectInterface.(Project)\n\n\t// check to see if project is in RealmId = 2 (Decide)\n\tif project.RealmId == \"2\" {\n\t\tproject.Due = dueDate\n\t\tzpm.Projects.Set(project.Id, project)\n\t} else {\n\t\treturn ErrProjectNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) (err error){\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"2\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Due = dueDate\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\n// getters\n\nfunc (zpm *ZProjectManager) GetProjectById(projectId string) (Project, error) {\n\tif zpm.Projects.Size() != 0 {\n\t\tpInterface, exist := zpm.Projects.Get(projectId)\n\t\tif exist {\n\t\t\treturn pInterface.(Project), nil\n\t\t}\n\t}\n\treturn Project{}, ErrProjectIdNotFound\n}\n\nfunc (zpm *ZProjectManager) GetAllProjects() (projects string) {\n\t// implementation\n\tvar allProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\t// get project tasks, if any\n\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\tif projectTasks != nil {\n\t\t\t\tproject.Tasks = projectTasks\n\t\t\t}\n\t\t\tallProjects = append(allProjects, project)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: allProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByRealm(realmId string) (projects string) {\n\t// implementation\n\tvar realmProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\trealmProjects = append(realmProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: realmProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByContextAndRealm(contextId string, realmId string) (projects string) {\n\t// implementation\n\tvar contextProjects []Project\n\t\n\t// Iterate over the Projects AVL tree to collect all Project objects.\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif project, ok := value.(Project); ok {\n\t\t\tif project.ContextId == contextId && project.RealmId == realmId {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tcontextProjects = append(contextProjects, project)\n\t\t\t}\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: contextProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n}\n\nfunc (zpm *ZProjectManager) GetProjectsByDate(projectDate string, filterType string) (projects string) {\n\t// implementation\n\tparsedDate, err:= time.Parse(\"2006-01-02\", projectDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredProjects []Project\n\t\n\tzpm.Projects.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tproject, ok := value.(Project)\n\t\tif !ok {\n\t\t\treturn false // Skip this iteration and continue.\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", project.Due)\n\t\tif serr != nil {\n\t\t\t// Skip projects with invalid dates.\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\t// get project tasks, if any\n\t\t\t\tprojectTasks, _ := zpm.GetProjectTasks(project)\n\t\t\t\tif projectTasks != nil {\n\t\t\t\t\tproject.Tasks = projectTasks\n\t\t\t\t}\n\t\t\t\tfilteredProjects = append(filteredProjects, project)\n\t\t\t}\n\t\t}\n\n\t\treturn false // Continue iteration.\n\t})\n\n\tif len(filteredProjects) == 0 {\n\t\treturn \"\"\n\t}\n\n\t// Create a ProjectsObject with all collected tasks.\n\tprojectsObject := ProjectsObject{\n\t\tProjects: filteredProjects,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledProjects, merr := projectsObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledProjects)\n\n}\n" + }, + { + "name": "projects_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n/*\nfunc Test_AddProject(t *testing.T) {\n \n project := Project{Id: \"1\", RealmId: \"1\", Body: \"First project\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test adding a duplicate project.\n cerr := project.AddProject()\n if cerr != ErrProjectIdAlreadyExists {\n t.Errorf(\"Expected ErrProjectIdAlreadyExists, got %v\", cerr)\n }\n}\n\n\nfunc Test_RemoveProject(t *testing.T) {\n \n project := Project{Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n retrievedProject, rerr := GetProjectById(project.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added project\")\n }\n\n // Test removing a project\n terr := retrievedProject.RemoveProject()\n if terr != ErrProjectNotRemoved {\n t.Errorf(\"Expected ErrProjectNotRemoved, got %v\", terr)\n }\n}\n\n\nfunc Test_EditProject(t *testing.T) {\n \n project := Project{Id: \"2\", Body: \"Second project content\", RealmId: \"1\", ContextId: \"2\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test editing the project\n editedProject := Project{Id: project.Id, Body: \"Edited project content\", RealmId: project.RealmId, ContextId: \"2\",}\n cerr := editedProject.EditProject()\n if cerr != nil {\n t.Errorf(\"Failed to edit the project\")\n }\n\n retrievedProject, _ := GetProjectById(editedProject.Id)\n if retrievedProject.Body != \"Edited project content\" {\n t.Errorf(\"Project was not edited\")\n }\n}\n\n\nfunc Test_MoveProjectToRealm(t *testing.T) {\n \n project := Project{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"1\", ContextId: \"1\",}\n\n // Test adding a project successfully.\n err := project.AddProject()\n if err != nil {\n t.Errorf(\"Failed to add project: %v\", err)\n }\n\n // Test moving the project to another realm\n \n cerr := project.MoveProjectToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move project to another realm\")\n }\n\n retrievedProject, _ := GetProjectById(project.Id)\n if retrievedProject.RealmId != \"2\" {\n t.Errorf(\"Project was moved to the wrong realm\")\n }\n}\n\nfunc Test_SetProjectDueDate(t *testing.T) {\n\tprojectRealmIdOne, _ := GetProjectById(\"1\")\n projectRealmIdTwo, _ := GetProjectById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\tproject Project\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Project does not exist\",\n\t\t\tproject: Project{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Project not editable due to wrong realm\",\n\t\t\tproject: projectRealmIdOne,\n\t\t\twantErr: ErrProjectNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\tproject: projectRealmIdTwo,\n\t\t\tdueDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.project.SetProjectDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedProject, exist := Projects.Get(tc.project.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Project %v was not found after setting the due date\", tc.project.Id)\n\t\t\t\t}\n\t\t\t\tif updatedProject.(Project).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedProject.(Project).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllProjects(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownProjects := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n {Id: \"10\", Body: \"Project 10\", RealmId: \"2\", ContextId: \"2\", Due: \"2024-01-01\"},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObject := ProjectsObject{Projects: knownProjects}\n expected, err := projectsObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known projects: %v\", err)\n }\n\n // Execute GetAllProjects() to get the actual outcome.\n actual, err := GetAllProjects()\n if err != nil {\n t.Fatalf(\"GetAllProjects() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\tprojectDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2024-01-01\", \"specific\", `{\"projects\":[{\"projectId\":\"10\",\"projectContextId\":\"2\",\"projectRealmId\":\"2\",\"projectTasks\":[],\"projectBody\":\"Project 10\",\"projectDue\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2025-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetProjectsByDate(tt.projectDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetProjectsByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetProjectTasks(t *testing.T){\n \n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n project, perr := GetProjectById(\"1\")\n if perr != nil {\n t.Errorf(\"GetProjectById() failed, %v\", perr)\n }\n\n // test attaching to an existing project\n atterr := task.AttachTaskToProject(project)\n if atterr != nil {\n t.Errorf(\"AttachTaskToProject() failed, %v\", atterr)\n }\n\n projectTasks, pterr := project.GetProjectTasks()\n if len(projectTasks) == 0 {\n t.Errorf(\"GetProjectTasks() failed, %v\", pterr)\n }\n\n // test detaching from an existing project\n dtterr := task.DetachTaskFromProject(project)\n if dtterr != nil {\n t.Errorf(\"DetachTaskFromProject() failed, %v\", dtterr)\n }\n\n projectWithNoTasks, pterr := project.GetProjectTasks()\n if len(projectWithNoTasks) != 0 {\n t.Errorf(\"GetProjectTasks() after detach failed, %v\", pterr)\n }\n}\n\nfunc Test_GetProjectById(t *testing.T){\n // test getting a non-existing project\n nonProject, err := GetProjectById(\"0\")\n if err != ErrProjectByIdNotFound {\n t.Fatalf(\"Expected ErrProjectByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctProject, err := GetProjectById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get project by id, error: %v\", err)\n }\n\n if correctProject.Body != \"First project\" {\n t.Fatalf(\"Got the wrong project, with body: %v\", correctProject.Body)\n }\n}\n\nfunc Test_GetProjectsByRealm(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInAssessRealm := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"2\", Body: \"Edited project content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable project\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"21\", Body: \"Project 21\", RealmId: \"1\",},\n {Id: \"22\", Body: \"Project 22\", RealmId: \"1\",},\n }\n\n // Manually marshal the known projects to create the expected outcome.\n projectsObjectAssess := ProjectsObject{Projects: projectsInAssessRealm}\n expected, err := projectsObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects in Assess: %v\", err)\n }\n\n actual, err := GetProjectsByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual projects JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetProjectsByContext(t *testing.T) {\n \n // mocking the projects based on previous tests\n // TODO: add isolation?\n projectsInContextOne := []Project{\n {Id: \"1\", Body: \"First project\", RealmId: \"1\", ContextId: \"1\",},\n\t\t{Id: \"3\", Body: \"Project id 3 content\", RealmId: \"2\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n projectsObjectForContexts := ProjectsObject{Projects: projectsInContextOne}\n expected, err := projectsObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal projects for ContextId 1: %v\", err)\n }\n\n actual, err := GetProjectsByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetProjectsByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual project JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n\n" + }, + { + "name": "realms.gno", + "body": "// base implementation\npackage zentasktic\n\nimport (\n\t\"gno.land/p/demo/avl\"\n)\n\n// structs\n\ntype Realm struct {\n\tId \t\t\tstring `json:\"realmId\"`\n\tName \t\tstring `json:\"realmName\"`\n}\n\ntype ZRealmManager struct {\n\tRealms *avl.Tree\n}\n\nfunc NewZRealmManager() *ZRealmManager {\n\tzrm := &ZRealmManager{\n\t\tRealms: avl.NewTree(),\n\t}\n\tzrm.initializeHardcodedRealms()\n\treturn zrm\n}\n\n\nfunc (zrm *ZRealmManager) initializeHardcodedRealms() {\n\thardcodedRealms := []Realm{\n\t\t{Id: \"1\", Name: \"Assess\"},\n\t\t{Id: \"2\", Name: \"Decide\"},\n\t\t{Id: \"3\", Name: \"Do\"},\n\t\t{Id: \"4\", Name: \"Collections\"},\n\t}\n\n\tfor _, realm := range hardcodedRealms {\n\t\tzrm.Realms.Set(realm.Id, realm)\n\t}\n}\n\n\nfunc (zrm *ZRealmManager) AddRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif exist {\n\t\t\treturn ErrRealmIdAlreadyExists\n\t\t}\n\t}\n\t// check for hardcoded values\n\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\treturn ErrRealmIdNotAllowed\n\t}\n\tzrm.Realms.Set(r.Id, r)\n\treturn nil\n\t\n}\n\nfunc (zrm *ZRealmManager) RemoveRealm(r Realm) (err error){\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\t_, exist := zrm.Realms.Get(r.Id)\n\t\tif !exist {\n\t\t\treturn ErrRealmIdNotFound\n\t\t} else {\n\t\t\t// check for hardcoded values, not removable\n\t\t\tif r.Id == \"1\" || r.Id == \"2\" || r.Id == \"3\" || r.Id == \"4\" {\n\t\t\t\treturn ErrRealmIdNotAllowed\n\t\t\t}\n\t\t}\n\t}\n\t\n\t_, removed := zrm.Realms.Remove(r.Id)\n\tif !removed {\n\t\treturn ErrRealmNotRemoved\n\t}\n\treturn nil\n\t\n}\n\n// getters\nfunc (zrm *ZRealmManager) GetRealmById(realmId string) (r Realm, err error) {\n\t// implementation\n\tif zrm.Realms.Size() != 0 {\n\t\trInterface, exist := zrm.Realms.Get(realmId)\n\t\tif exist {\n\t\t\treturn rInterface.(Realm), nil\n\t\t} else {\n\t\t\treturn Realm{}, ErrRealmIdNotFound\n\t\t}\n\t}\n\treturn Realm{}, ErrRealmIdNotFound\n}\n\nfunc (zrm *ZRealmManager) GetRealms() (realms string, err error) {\n\t// implementation\n\tvar allRealms []Realm\n\n\t// Iterate over the Realms AVL tree to collect all Context objects.\n\tzrm.Realms.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif realm, ok := value.(Realm); ok {\n\t\t\tallRealms = append(allRealms, realm)\n\t\t}\n\t\treturn false // Continue iteration until all nodes have been visited.\n\t})\n\n\n\t// Create a RealmsObject with all collected contexts.\n\trealmsObject := &RealmsObject{\n\t\tRealms: allRealms,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the realms into JSON.\n\tmarshalledRealms, rerr := realmsObject.MarshalJSON()\n\tif rerr != nil {\n\t\treturn \"\", rerr\n\t} \n\treturn string(marshalledRealms), nil\n}\n" + }, + { + "name": "tasks.gno", + "body": "package zentasktic\n\nimport (\n\t\"time\"\n\n\t\"gno.land/p/demo/avl\"\n)\n\ntype Task struct {\n\tId \t\t\tstring `json:\"taskId\"`\n\tProjectId \tstring `json:\"taskProjectId\"`\n\tContextId\tstring `json:\"taskContextId\"`\n\tRealmId \tstring `json:\"taskRealmId\"`\n\tBody \t\tstring `json:\"taskBody\"`\n\tDue\t\t\tstring `json:\"taskDue\"`\n\tAlert\t\tstring `json:\"taskAlert\"`\n}\n\ntype ZTaskManager struct {\n\tTasks *avl.Tree\n}\n\nfunc NewZTaskManager() *ZTaskManager {\n\treturn &ZTaskManager{\n\t\tTasks: avl.NewTree(),\n\t}\n}\n\n// actions\n\nfunc (ztm *ZTaskManager) AddTask(t Task) error {\n\tif ztm.Tasks.Size() != 0 {\n\t\t_, exist := ztm.Tasks.Get(t.Id)\n\t\tif exist {\n\t\t\treturn ErrTaskIdAlreadyExists\n\t\t}\n\t}\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) RemoveTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t // task is removable only in Asses (RealmId 1) or via a Collection (RealmId 4)\n\tif existingTask.RealmId != \"1\" && existingTask.RealmId != \"4\" {\n\t\treturn ErrTaskNotRemovable\n\t}\n\n\t_, removed := ztm.Tasks.Remove(existingTask.Id)\n\tif !removed {\n\t\treturn ErrTaskNotRemoved\n\t}\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) EditTask(t Task) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(t.Id)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\n\t// task Body is editable only when task is in Assess, RealmId = \"1\"\n\tif t.RealmId != \"1\" {\n\t\tif t.Body != existingTask.Body {\n\t\t\treturn ErrTaskNotInAssessRealm\n\t\t}\n\t}\n\n\tztm.Tasks.Set(t.Id, t)\n\treturn nil\n}\n\n// Helper function to move a task to a different realm\nfunc (ztm *ZTaskManager) MoveTaskToRealm(taskId, realmId string) error {\n\texistingTaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\texistingTask := existingTaskInterface.(Task)\n\texistingTask.RealmId = realmId\n\tztm.Tasks.Set(taskId, existingTask)\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskDueDate(taskId, dueDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Due = dueDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\nfunc (ztm *ZTaskManager) SetTaskAlert(taskId, alertDate string) error {\n\ttaskInterface, exist := ztm.Tasks.Get(taskId)\n\tif !exist {\n\t\treturn ErrTaskIdNotFound\n\t}\n\ttask := taskInterface.(Task)\n\n\tif task.RealmId == \"2\" {\n\t\ttask.Alert = alertDate\n\t\tztm.Tasks.Set(task.Id, task)\n\t} else {\n\t\treturn ErrTaskNotEditable\n\t}\n\n\treturn nil\n}\n\n// tasks & projects association\n\nfunc (zpm *ZProjectManager) AttachTaskToProject(ztm *ZTaskManager, t Task, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\texistingProject.Tasks = []Task{}\n\t} else {\n\t\ttasks, ok := existingProjectTasksInterface.([]Task)\n\t\tif !ok {\n\t\t\treturn ErrProjectTasksNotFound\n\t\t}\n\t\texistingProject.Tasks = tasks\n\t}\n\n\tt.ProjectId = p.Id\n\t// @todo we need to remove it from Tasks if it was previously added there, then detached\n\texistingTask, err := ztm.GetTaskById(t.Id)\n\tif err == nil {\n\t\tztm.RemoveTask(existingTask)\n\t}\n\tupdatedTasks := append(existingProject.Tasks, t)\n\tzpm.ProjectTasks.Set(p.Id, updatedTasks)\n\n\treturn nil\n}\n\nfunc (zpm *ZProjectManager) EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n existingProjectInterface, exist := zpm.Projects.Get(projectId)\n if !exist {\n return ErrProjectIdNotFound\n }\n existingProject := existingProjectInterface.(Project)\n\n\tif existingProject.RealmId != \"1\" {\n\t\treturn ErrProjectNotEditable\n\t}\n\n existingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n if !texist {\n return ErrProjectTasksNotFound\n }\n tasks, ok := existingProjectTasksInterface.([]Task)\n if !ok {\n return ErrProjectTasksNotFound\n }\n existingProject.Tasks = tasks\n\n var index int = -1\n for i, task := range existingProject.Tasks {\n if task.Id == projectTaskId {\n index = i\n break\n }\n }\n\n if index != -1 {\n existingProject.Tasks[index].Body = projectTaskBody\n } else {\n return ErrTaskByIdNotFound\n }\n\n zpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n return nil\n}\n\nfunc (zpm *ZProjectManager) DetachTaskFromProject(ztm *ZTaskManager, projectTaskId string, detachedTaskId string, p Project) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(p.Id)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(p.Id)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar foundTask Task\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tfoundTask = task\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tfoundTask.ProjectId = \"\"\n\tfoundTask.Id = detachedTaskId\n\t// Tasks and ProjectTasks have different storage, if a task is detached from a Project\n\t// we add it to the Tasks storage\n\tif err := ztm.AddTask(foundTask); err != nil {\n\t\treturn err\n\t}\n\n\tzpm.ProjectTasks.Set(p.Id, existingProject.Tasks)\n\treturn nil\n}\n\n\nfunc (zpm *ZProjectManager) RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\texistingProjectInterface, exist := zpm.Projects.Get(projectId)\n\tif !exist {\n\t\treturn ErrProjectIdNotFound\n\t}\n\texistingProject := existingProjectInterface.(Project)\n\n\texistingProjectTasksInterface, texist := zpm.ProjectTasks.Get(projectId)\n\tif !texist {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\ttasks, ok := existingProjectTasksInterface.([]Task)\n\tif !ok {\n\t\treturn ErrProjectTasksNotFound\n\t}\n\texistingProject.Tasks = tasks\n\n\tvar index int = -1\n\tfor i, task := range existingProject.Tasks {\n\t\tif task.Id == projectTaskId {\n\t\t\tindex = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif index != -1 {\n\t\texistingProject.Tasks = append(existingProject.Tasks[:index], existingProject.Tasks[index+1:]...)\n\t} else {\n\t\treturn ErrTaskByIdNotFound\n\t}\n\n\tzpm.ProjectTasks.Set(projectId, existingProject.Tasks)\n\treturn nil\n}\n\n// getters\n\nfunc (ztm *ZTaskManager) GetTaskById(taskId string) (Task, error) {\n\tif ztm.Tasks.Size() != 0 {\n\t\ttInterface, exist := ztm.Tasks.Get(taskId)\n\t\tif exist {\n\t\t\treturn tInterface.(Task), nil\n\t\t}\n\t}\n\treturn Task{}, ErrTaskIdNotFound\n}\n\nfunc (ztm *ZTaskManager) GetAllTasks() (task string) {\n\tvar allTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tallTasks = append(allTasks, task)\n\t\t}\n\t\treturn false\n\t})\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: allTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\t\n}\n\nfunc (ztm *ZTaskManager) GetTasksByRealm(realmId string) (tasks string) {\n\tvar realmTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.RealmId == realmId {\n\t\t\t\trealmTasks = append(realmTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: realmTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByContextAndRealm(contextId string, realmId string) (tasks string) {\n\tvar contextTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\tif task, ok := value.(Task); ok {\n\t\t\tif task.ContextId == contextId && task.ContextId == realmId {\n\t\t\t\tcontextTasks = append(contextTasks, task)\n\t\t\t}\n\t\t}\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: contextTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n\nfunc (ztm *ZTaskManager) GetTasksByDate(taskDate string, filterType string) (tasks string) {\n\tparsedDate, err := time.Parse(\"2006-01-02\", taskDate)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tvar filteredTasks []Task\n\n\tztm.Tasks.Iterate(\"\", \"\", func(key string, value interface{}) bool {\n\t\ttask, ok := value.(Task)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tstoredDate, serr := time.Parse(\"2006-01-02\", task.Due)\n\t\tif serr != nil {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch filterType {\n\t\tcase \"specific\":\n\t\t\tif storedDate.Format(\"2006-01-02\") == parsedDate.Format(\"2006-01-02\") {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"before\":\n\t\t\tif storedDate.Before(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\tcase \"after\":\n\t\t\tif storedDate.After(parsedDate) {\n\t\t\t\tfilteredTasks = append(filteredTasks, task)\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t})\n\n\t// Create a TasksObject with all collected tasks.\n\ttasksObject := &TasksObject{\n\t\tTasks: filteredTasks,\n\t}\n\n\t// Use the custom MarshalJSON method to marshal the tasks into JSON.\n\tmarshalledTasks, merr := tasksObject.MarshalJSON()\n\tif merr != nil {\n\t\treturn \"\"\n\t} \n\treturn string(marshalledTasks)\n}\n" + }, + { + "name": "tasks_test.gno", + "body": "package zentasktic\n\nimport (\n\t\"testing\"\n\n \"gno.land/p/demo/avl\"\n)\n\n// Shared instance of ZTaskManager\nvar ztm *ZTaskManager\n\nfunc init() {\n ztm = NewZTaskManager()\n}\n\nfunc Test_AddTask(t *testing.T) {\n task := Task{Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test adding a duplicate task.\n cerr := ztm.AddTask(task)\n if cerr != ErrTaskIdAlreadyExists {\n t.Errorf(\"Expected ErrTaskIdAlreadyExists, got %v\", cerr)\n }\n}\n\nfunc Test_RemoveTask(t *testing.T) {\n \n task := Task{Id: \"20\", Body: \"Removable task\", RealmId: \"1\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n retrievedTask, rerr := ztm.GetTaskById(task.Id)\n if rerr != nil {\n t.Errorf(\"Could not retrieve the added task\")\n }\n\n // Test removing a task\n terr := ztm.RemoveTask(retrievedTask)\n if terr != nil {\n t.Errorf(\"Expected nil, got %v\", terr)\n }\n}\n\nfunc Test_EditTask(t *testing.T) {\n \n task := Task{Id: \"2\", Body: \"First content\", RealmId: \"1\", ContextId: \"2\"}\n\n // Test adding a task successfully.\n err := ztm.AddTask(task)\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test editing the task\n editedTask := Task{Id: task.Id, Body: \"Edited content\", RealmId: task.RealmId, ContextId: \"2\"}\n cerr := ztm.EditTask(editedTask)\n if cerr != nil {\n t.Errorf(\"Failed to edit the task\")\n }\n\n retrievedTask, _ := ztm.GetTaskById(editedTask.Id)\n if retrievedTask.Body != \"Edited content\" {\n t.Errorf(\"Task was not edited\")\n }\n}\n/*\nfunc Test_MoveTaskToRealm(t *testing.T) {\n \n task := Task{Id: \"3\", Body: \"First content\", RealmId: \"1\", ContextId: \"1\"}\n\n // Test adding a task successfully.\n err := task.AddTask()\n if err != nil {\n t.Errorf(\"Failed to add task: %v\", err)\n }\n\n // Test moving the task to another realm\n \n cerr := task.MoveTaskToRealm(\"2\")\n if cerr != nil {\n t.Errorf(\"Failed to move task to another realm\")\n }\n\n retrievedTask, _ := GetTaskById(task.Id)\n if retrievedTask.RealmId != \"2\" {\n t.Errorf(\"Task was moved to the wrong realm\")\n }\n}\n\nfunc Test_AttachTaskToProject(t *testing.T) {\n \n // Example Projects and Tasks\n prj := Project{Id: \"1\", Body: \"Project 1\", RealmId: \"1\",}\n tsk := Task{Id: \"4\", Body: \"Task 4\", RealmId: \"1\",}\n\n Projects.Set(prj.Id, prj) // Mock existing project\n\n tests := []struct {\n name string\n project Project\n task Task\n wantErr bool\n errMsg error\n }{\n {\n name: \"Attach to existing project\",\n project: prj,\n task: tsk,\n wantErr: false,\n },\n {\n name: \"Attach to non-existing project\",\n project: Project{Id: \"200\", Body: \"Project 200\", RealmId: \"1\",},\n task: tsk,\n wantErr: true,\n errMsg: ErrProjectIdNotFound,\n },\n }\n\n for _, tt := range tests {\n t.Run(tt.name, func(t *testing.T) {\n err := tt.task.AttachTaskToProject(tt.project)\n if (err != nil) != tt.wantErr {\n t.Errorf(\"AttachTaskToProject() error = %v, wantErr %v\", err, tt.wantErr)\n }\n if tt.wantErr && err != tt.errMsg {\n t.Errorf(\"AttachTaskToProject() error = %v, expected %v\", err, tt.errMsg)\n }\n\n // For successful attach, verify the task is added to the project's tasks.\n if !tt.wantErr {\n tasks, exist := ProjectTasks.Get(tt.project.Id)\n if !exist || len(tasks.([]Task)) == 0 {\n t.Errorf(\"Task was not attached to the project\")\n } else {\n found := false\n for _, task := range tasks.([]Task) {\n if task.Id == tt.task.Id {\n found = true\n break\n }\n }\n if !found {\n t.Errorf(\"Task was not attached to the project\")\n }\n }\n }\n })\n }\n}\n\nfunc TestDetachTaskFromProject(t *testing.T) {\n\t\n\t// Setup:\n\tproject := Project{Id: \"p1\", Body: \"Test Project\"}\n\ttask1 := Task{Id: \"5\", Body: \"Task One\"}\n\ttask2 := Task{Id: \"6\", Body: \"Task Two\"}\n\n\tProjects.Set(project.Id, project)\n\tProjectTasks.Set(project.Id, []Task{task1, task2})\n\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tproject Project\n\t\twantErr bool\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname: \"Detach existing task from project\",\n\t\t\ttask: task1,\n\t\t\tproject: project,\n\t\t\twantErr: false,\n\t\t\texpectedErr: nil,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach task from non-existing project\",\n\t\t\ttask: task1,\n\t\t\tproject: Project{Id: \"nonexistent\"},\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrProjectIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Try to detach non-existing task from project\",\n\t\t\ttask: Task{Id: \"nonexistent\"},\n\t\t\tproject: project,\n\t\t\twantErr: true,\n\t\t\texpectedErr: ErrTaskByIdNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\terr := tt.task.DetachTaskFromProject(tt.project)\n\n\t\t\tif tt.wantErr {\n\t\t\t\tif err == nil || err != tt.expectedErr {\n\t\t\t\t\tt.Errorf(\"%s: expected error %v, got %v\", tt.name, tt.expectedErr, err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"%s: unexpected error: %v\", tt.name, err)\n\t\t\t\t}\n\n\t\t\t\t// For successful detachment, verify the task is no longer part of the project's tasks\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\ttasks, _ := ProjectTasks.Get(tt.project.Id)\n\t\t\t\t\tfor _, task := range tasks.([]Task) {\n\t\t\t\t\t\tif task.Id == tt.task.Id {\n\t\t\t\t\t\t\tt.Errorf(\"%s: task was not detached from the project\", tt.name)\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskDueDate(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\tdueDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set due date\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\tdueDate: \"2023-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskDueDate(tc.dueDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Due != tc.dueDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.dueDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_SetTaskAlert(t *testing.T) {\n\ttaskRealmIdOne, _ := GetTaskById(\"1\")\n taskRealmIdTwo, _ := GetTaskById(\"10\") \n\t// Define test cases\n\ttests := []struct {\n\t\tname string\n\t\ttask Task\n\t\talertDate string\n\t\twantErr error\n\t}{\n\t\t{\n\t\t\tname: \"Task does not exist\",\n\t\t\ttask: Task{Id: \"nonexistent\", RealmId: \"2\"},\n\t\t\twantErr: ErrTaskIdNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Task not editable due to wrong realm\",\n\t\t\ttask: taskRealmIdOne,\n\t\t\twantErr: ErrTaskNotEditable,\n\t\t},\n\t\t{\n\t\t\tname: \"Successfully set alert\",\n\t\t\ttask: taskRealmIdTwo,\n\t\t\talertDate: \"2024-01-01\",\n\t\t\twantErr: nil,\n\t\t},\n\t}\n\n\t// Execute test cases\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\terr := tc.task.SetTaskAlert(tc.alertDate)\n\n\t\t\t// Validate\n\t\t\tif err != tc.wantErr {\n\t\t\t\tt.Errorf(\"Expected error %v, got %v\", tc.wantErr, err)\n\t\t\t}\n\n\t\t\t// Additional check for the success case to ensure the due date was actually set\n\t\t\tif err == nil {\n\t\t\t\t// Fetch the task again to check if the due date was set correctly\n\t\t\t\tupdatedTask, exist := Tasks.Get(tc.task.Id)\n\t\t\t\tif !exist {\n\t\t\t\t\tt.Fatalf(\"Task %v was not found after setting the due date\", tc.task.Id)\n\t\t\t\t}\n\t\t\t\tif updatedTask.(Task).Alert != tc.alertDate {\n\t\t\t\t\tt.Errorf(\"Expected due date to be %v, got %v\", tc.alertDate, updatedTask.(Task).Due)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// getters\n\nfunc Test_GetAllTasks(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n knownTasks := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"10\", Body: \"First content\", RealmId: \"2\", ContextId: \"2\", Due: \"2023-01-01\", Alert: \"2024-01-01\"},\n {Id: \"2\", Body: \"Edited content\", RealmId: \"1\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"3\", Body: \"First content\", RealmId: \"2\", ContextId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObject := TasksObject{Tasks: knownTasks}\n expected, err := tasksObject.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal known tasks: %v\", err)\n }\n\n // Execute GetAllTasks() to get the actual outcome.\n actual, err := GetAllTasks()\n if err != nil {\n t.Fatalf(\"GetAllTasks() failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByDate(t *testing.T) {\n\t\n\ttests := []struct {\n\t\tname string\n\t\ttaskDate string\n\t\tfilterType string\n\t\twant string\n\t\twantErr bool\n\t}{\n\t\t{\"SpecificDate\", \"2023-01-01\", \"specific\", `{\"tasks\":[{\"taskId\":\"10\",\"taskProjectId\":\"\",\"taskContextId\":\"2\",\"taskRealmId\":\"2\",\"taskBody\":\"First content\",\"taskDue\":\"2023-01-01\",\"taskAlert\":\"2024-01-01\"}]}`, false},\n\t\t{\"BeforeDate\", \"2022-04-05\", \"before\", \"\", false},\n\t\t{\"AfterDate\", \"2023-04-05\", \"after\", \"\", false},\n\t\t{\"NoMatch\", \"2002-04-07\", \"specific\", \"\", false},\n\t\t{\"InvalidDateFormat\", \"April 5, 2023\", \"specific\", \"\", true},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := GetTasksByDate(tt.taskDate, tt.filterType)\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"GetTasksByDate() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif err == nil && got != tt.want {\n\t\t\t\tt.Errorf(\"GetTasksByDate() got = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_GetTaskById(t *testing.T){\n // test getting a non-existing task\n nonTask, err := GetTaskById(\"0\")\n if err != ErrTaskByIdNotFound {\n t.Fatalf(\"Expected ErrTaskByIdNotFound, got: %v\", err)\n }\n\n // test getting the correct task by id\n correctTask, err := GetTaskById(\"1\")\n if err != nil {\n t.Fatalf(\"Failed to get task by id, error: %v\", err)\n }\n\n if correctTask.Body != \"First task\" {\n t.Fatalf(\"Got the wrong task, with body: %v\", correctTask.Body)\n }\n}\n\nfunc Test_GetTasksByRealm(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInAssessRealm := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"2\", RealmId: \"1\", Body: \"Edited content\", ContextId: \"2\",},\n {Id: \"20\", Body: \"Removable task\", RealmId: \"1\",},\n {Id: \"40\", Body: \"Task 40\", RealmId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectAssess := TasksObject{Tasks: tasksInAssessRealm}\n expected, err := tasksObjectAssess.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks in Assess: %v\", err)\n }\n\n actual, err := GetTasksByRealm(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByRealm('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n\nfunc Test_GetTasksByContext(t *testing.T) {\n \n // mocking the tasks based on previous tests\n // TODO: add isolation?\n tasksInContextOne := []Task{\n {Id: \"1\", RealmId: \"1\", Body: \"First task\", ContextId: \"1\",},\n {Id: \"3\", RealmId: \"2\", Body: \"First content\", ContextId: \"1\",},\n }\n\n // Manually marshal the known tasks to create the expected outcome.\n tasksObjectForContexts := TasksObject{Tasks: tasksInContextOne}\n expected, err := tasksObjectForContexts.MarshalJSON()\n if err != nil {\n t.Fatalf(\"Failed to manually marshal tasks for ContextId 1: %v\", err)\n }\n\n actual, err := GetTasksByContext(\"1\")\n if err != nil {\n t.Fatalf(\"GetTasksByContext('1') failed with error: %v\", err)\n }\n\n // Compare the expected and actual outcomes.\n if string(expected) != actual {\n t.Errorf(\"Expected and actual task JSON strings do not match.\\nExpected: %s\\nActual: %s\", string(expected), actual)\n }\n}\n*/\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "15000000", + "gas_fee": "1000000ugnot" + }, + "signatures": [], + "memo": "" +} + +-- tx3.tx -- +{ + "msg": [ + { + "@type": "/vm.m_addpkg", + "creator": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "package": { + "name": "zentasktic_core", + "path": "gno.land/r/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic_core", + "files": [ + { + "name": "workable.gno", + "body": "package zentasktic_core\n\ntype Workable interface {\n\t// restrict implementation of Workable to this realm\n\tassertWorkable()\n}\n\ntype isWorkable struct {}\n\nfunc (wt *WorkableTask) assertWorkable() {}\n\nfunc (wp *WorkableProject) assertWorkable() {}\n\nvar _ Workable = &WorkableTask{}\nvar _ Workable = &WorkableProject{}\n" + }, + { + "name": "wrapper.gno", + "body": "package zentasktic_core\n\n\nimport (\n\t\"strconv\"\n\n\t\"gno.land/p/g17ernafy6ctpcz6uepfsq2js8x2vz0wladh5yc3/zentasktic\"\n)\n\n// this is a convenience wrapper on top of the functions declared in the zentasktic package\n// to maintain consistency\n\n// wrapping zentasktic types\n\ntype WorkableTask struct {\n zentasktic.Task\n}\ntype WorkableProject struct {\n zentasktic.Project\n}\n\ntype WorkableRealm struct {\n\tId string\n\tName string\n}\n\ntype WorkableContext struct {\n\tzentasktic.Context\n}\n\ntype WorkableCollection struct {\n\tzentasktic.Collection\n}\n\ntype WorkableObjectPath struct {\n\tzentasktic.ObjectPath\n}\n\n// zentasktic managers\n\nvar ztm *zentasktic.ZTaskManager\nvar zpm *zentasktic.ZProjectManager\nvar zrm *zentasktic.ZRealmManager\nvar zcm *zentasktic.ZContextManager\nvar zcl *zentasktic.ZCollectionManager\nvar zom *zentasktic.ZObjectPathManager\nvar currentTaskID int\nvar currentProjectTaskID int\nvar currentProjectID int\nvar currentContextID int\nvar currentCollectionID int\nvar currentPathID int\n\nfunc init() {\n ztm = zentasktic.NewZTaskManager()\n zpm = zentasktic.NewZProjectManager()\n\tzrm = zentasktic.NewZRealmManager()\n\tzcm = zentasktic.NewZContextManager()\n\tzcl = zentasktic.NewZCollectionManager()\n\tzom = zentasktic.NewZObjectPathManager()\n\tcurrentTaskID = 0\n\tcurrentProjectTaskID = 0\n\tcurrentProjectID = 0\n\tcurrentContextID = 0\n\tcurrentCollectionID = 0\n\tcurrentPathID = 0\n}\n\n// tasks\n\nfunc AddTask(taskBody string) error {\n\ttaskID := incrementTaskID()\n\twt := &WorkableTask{\n\t\tTask: zentasktic.Task{\n\t\t\tId: strconv.Itoa(taskID),\n\t\t\tBody: taskBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\treturn ztm.AddTask(wt.Task)\n}\n\n\nfunc EditTask(taskId string, taskBody string) error {\n\ttaskToEdit, err := GetTaskById(taskId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\ttaskToEdit.Body = taskBody;\n\treturn ztm.EditTask(taskToEdit.Task)\n}\n\nfunc RemoveTask(taskId string) error {\n\ttaskToRemove, err := GetTaskById(taskId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn ztm.RemoveTask(taskToRemove.Task)\n}\n\nfunc MoveTaskToRealm(taskId string, realmId string) error {\n\treturn ztm.MoveTaskToRealm(taskId, realmId)\n}\n\nfunc SetTaskDueDate(taskId string, dueDate string) error {\n\treturn ztm.SetTaskDueDate(taskId, dueDate)\n}\n\nfunc SetTaskAlert(taskId string, alert string) error {\n\treturn ztm.SetTaskAlert(taskId, alert)\n}\n\nfunc AttachTaskToProject(taskBody string, projectId string) error {\n\tprojectTaskID := incrementProjectTaskID()\n\twt := &WorkableTask{\n\t\tTask: zentasktic.Task{\n\t\t\tId: strconv.Itoa(projectTaskID),\n\t\t\tBody: taskBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\t//ztm.AddTask(wt.Task)\n\tprojectToAdd, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\treturn zpm.AttachTaskToProject(ztm, wt.Task, projectToAdd.Project)\n}\n\nfunc EditProjectTask(projectTaskId string, projectTaskBody string, projectId string) error {\n\treturn zpm.EditProjectTask(projectTaskId, projectTaskBody, projectId)\n}\n\nfunc DetachTaskFromProject(projectTaskId string, projectId string) error {\n\tprojectToDetachFrom, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tdetachedTaskId := strconv.Itoa(incrementTaskID())\n\treturn zpm.DetachTaskFromProject(ztm, projectTaskId, detachedTaskId, projectToDetachFrom.Project)\n}\n\nfunc RemoveTaskFromProject(projectTaskId string, projectId string) error {\n\treturn zpm.RemoveTaskFromProject(projectTaskId, projectId)\n}\n\nfunc GetTaskById(taskId string) (WorkableTask, error) {\n\ttask, err := ztm.GetTaskById(taskId)\n\tif err != nil {\n\t\treturn WorkableTask{}, err\n\t}\n\treturn WorkableTask{Task: task}, nil\n}\n\nfunc GetAllTasks() (string){\n\treturn ztm.GetAllTasks()\n}\n\nfunc GetTasksByRealm(realmId string) (string){\n\treturn ztm.GetTasksByRealm(realmId)\n}\n\nfunc GetTasksByContextAndRealm(contextId string, realmId string) (string){\n\treturn ztm.GetTasksByContextAndRealm(contextId, realmId)\n}\n\nfunc GetTasksByDate(dueDate string, filterType string) (string){\n\treturn ztm.GetTasksByDate(dueDate, filterType)\n}\n\nfunc incrementTaskID() int {\n\tcurrentTaskID++\n\treturn currentTaskID\n}\n\nfunc incrementProjectTaskID() int {\n\tcurrentProjectTaskID++\n\treturn currentProjectTaskID\n}\n\n// projects\n\nfunc AddProject(projectBody string) error {\n\tprojectID := incrementProjectID()\n\twp := &WorkableProject{\n\t\tProject: zentasktic.Project{\n\t\t\tId: strconv.Itoa(projectID),\n\t\t\tBody: projectBody,\n\t\t\tRealmId: \t \"1\",\n\t\t},\n\t}\n\treturn zpm.AddProject(wp.Project)\n}\n\nfunc EditProject(projectId string, projectBody string) error {\n\tprojectToEdit, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tprojectToEdit.Body = projectBody;\n\treturn zpm.EditProject(projectToEdit.Project)\n}\n\nfunc RemoveProject(projectId string) error {\n\tprojectToRemove, err := GetProjectById(projectId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn zpm.RemoveProject(projectToRemove.Project)\n}\n\nfunc MoveProjectToRealm(projectId string, realmId string) error {\n\treturn zpm.MoveProjectToRealm(projectId, realmId)\n}\n\nfunc MarkProjectTaskAsDone(projectId string, projectTaskId string) error {\n\treturn zpm.MarkProjectTaskAsDone(projectId, projectTaskId)\n}\n\nfunc GetProjectTasks(wp WorkableProject) ([]WorkableTask, error){\n\ttasks, err := zpm.GetProjectTasks(wp.Project)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Convert []zentasktic.Task to []WorkableTask\n\tvar workableTasks []WorkableTask\n\tfor _, task := range tasks {\n\t\tworkableTasks = append(workableTasks, WorkableTask{Task: task})\n\t}\n\n\treturn workableTasks, nil\n}\n\nfunc SetProjectDueDate(projectId string, dueDate string) error {\n\treturn zpm.SetProjectDueDate(projectId, dueDate)\n}\n\nfunc GetProjectById(projectId string) (WorkableProject, error) {\n\tproject, err := zpm.GetProjectById(projectId)\n\tif err != nil {\n\t\treturn WorkableProject{}, err\n\t}\n\treturn WorkableProject{Project: project}, nil\n}\n\nfunc SetProjectTaskDueDate(projectId string, projectTaskId string, dueDate string) error {\n\treturn zpm.SetProjectTaskDueDate(projectId, projectTaskId, dueDate)\n}\n\nfunc GetAllProjects() (string){\n\treturn zpm.GetAllProjects()\n}\n\nfunc GetProjectsByRealm(realmId string) (string){\n\treturn zpm.GetProjectsByRealm(realmId)\n}\n\nfunc GetProjectsByContextAndRealm(contextId string, realmId string) (string){\n\treturn zpm.GetProjectsByContextAndRealm(contextId, realmId)\n}\n\nfunc GetProjectsByDate(dueDate string, filterType string) (string){\n\treturn zpm.GetProjectsByDate(dueDate, filterType)\n}\n\nfunc incrementProjectID() int {\n\tcurrentProjectID++\n\treturn currentProjectID\n}\n\n// realms\n\nfunc AddRealm(wr WorkableRealm) error {\n\tr := zentasktic.Realm{\n\t\tId: wr.Id,\n\t\tName: wr.Name,\n\t}\n\treturn zrm.AddRealm(r)\n}\n\nfunc RemoveRealm(wr WorkableRealm) error {\n\tr := zentasktic.Realm{\n\t\tId: wr.Id,\n\t\tName: wr.Name,\n\t}\n\treturn zrm.RemoveRealm(r)\n}\n\nfunc GetRealmById(realmId string) (WorkableRealm, error) {\n\tr, err := zrm.GetRealmById(realmId)\n\tif err != nil {\n\t\treturn WorkableRealm{}, err\n\t}\n\treturn WorkableRealm{\n\t\tId: r.Id,\n\t\tName: r.Name,\n\t}, nil\n}\n\nfunc GetAllRealms() (string, error) {\n\treturn zrm.GetRealms()\n}\n\n// contexts\n\nfunc AddContext(contextName string) error {\n\tcontextID := incrementContextID()\n\twc := &WorkableContext{\n\t\tContext: zentasktic.Context{\n\t\t\tId: strconv.Itoa(contextID),\n\t\t\tName: contextName,\n\t\t},\n\t}\n\treturn zcm.AddContext(wc.Context)\n}\n\nfunc EditContext(contextId string, newContext string) error {\n\tcontextToEdit, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\t\n\t}\n\tcontextToEdit.Name = newContext;\n\treturn zcm.EditContext(contextToEdit.Context)\n}\n\nfunc RemoveContext(contextId string) error {\n\tcontextToRemove, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn zcm.RemoveContext(contextToRemove.Context)\n}\n\nfunc AddContextToTask(contextId string, taskId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttaskToAddContextTo, merr := GetTaskById(taskId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToTask(ztm, contextToAdd.Context, taskToAddContextTo.Task)\n}\n\nfunc AddContextToProject(contextId string, projectId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprojectToAddContextTo, merr := GetProjectById(projectId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToProject(zpm, contextToAdd.Context, projectToAddContextTo.Project)\n}\n\nfunc AddContextToProjectTask(contextId string, projectId string, projectTaskId string) error {\n\tcontextToAdd, err := GetContextById(contextId)\n\tif err != nil {\n\t\treturn err\n\t}\n\tprojectToAddContextTo, merr := GetProjectById(projectId)\n\tif merr != nil {\n\t\treturn merr\n\t}\n\treturn zcm.AddContextToProjectTask(zpm, contextToAdd.Context, projectToAddContextTo.Project, projectTaskId)\n}\n\nfunc GetContextById(contextId string) (WorkableContext, error) {\n\tcontext, err := zcm.GetContextById(contextId)\n\tif err != nil {\n\t\treturn WorkableContext{}, err\n\t}\n\treturn WorkableContext{Context: context}, nil\n}\n\nfunc GetAllContexts() (string) {\n\treturn zcm.GetAllContexts()\n}\n\nfunc incrementContextID() int {\n\tcurrentContextID++\n\treturn currentContextID\n}\n\n// collections\n/*\nfunc AddCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.AddCollection(c)\n}\n\nfunc EditCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.EditCollection(c)\n}\n\nfunc RemoveCollection(wc WorkableCollection) error {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t\tRealmId: wc.RealmId,\n\t\tName: wc.Name,\n\t\tTasks: toZentaskticTasks(wc.Tasks),\n\t\tProjects: toZentaskticProjects(wc.Projects),\n\t}\n\treturn zcl.RemoveCollection(c)\n}\n\nfunc GetCollectionById(collectionId string) (WorkableCollection, error) {\n\tc, err := zcl.GetCollectionById(collectionId)\n\tif err != nil {\n\t\treturn WorkableCollection{}, err\n\t}\n\treturn WorkableCollection{\n\t\tId: c.Id,\n\t\tRealmId: c.RealmId,\n\t\tName: c.Name,\n\t\tTasks: toWorkableTasks(c.Tasks),\n\t\tProjects: toWorkableProjects(c.Projects),\n\t}, nil\n}\n\nfunc GetCollectionTasks(wc WorkableCollection) ([]WorkableTask, error) {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t}\n\ttasks, err := zcl.GetCollectionTasks(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn toWorkableTasks(tasks), nil\n}\n\nfunc GetCollectionProjects(wc WorkableCollection) ([]WorkableProject, error) {\n\tc := zentasktic.Collection{\n\t\tId: wc.Id,\n\t}\n\tprojects, err := zcl.GetCollectionProjects(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn toWorkableProjects(projects), nil\n}\n\nfunc GetAllCollections() (string, error) {\n\treturn zcl.GetAllCollections()\n}\n\n// Helper functions to convert between Workable and zentasktic types\nfunc toZentaskticTasks(tasks []WorkableTask) []zentasktic.Task {\n\tztasks := make([]zentasktic.Task, len(tasks))\n\tfor i, t := range tasks {\n\t\tztasks[i] = t.Task\n\t}\n\treturn ztasks\n}\n\nfunc toWorkableTasks(tasks []zentasktic.Task) []WorkableTask {\n\twtasks := make([]WorkableTask, len(tasks))\n\tfor i, t := range tasks {\n\t\twtasks[i] = WorkableTask{Task: t}\n\t}\n\treturn wtasks\n}\n\nfunc toZentaskticProjects(projects []WorkableProject) []zentasktic.Project {\n\tzprojects := make([]zentasktic.Project, len(projects))\n\tfor i, p := range projects {\n\t\tzprojects[i] = p.Project\n\t}\n\treturn zprojects\n}\n\nfunc toWorkableProjects(projects []zentasktic.Project) []WorkableProject {\n\twprojects := make([]WorkableProject, len(projects))\n\tfor i, p := range projects {\n\t\twprojects[i] = WorkableProject{Project: p}\n\t}\n\treturn wprojects\n}*/\n\n// object Paths\n\nfunc AddPath(wop WorkableObjectPath) error {\n\to := zentasktic.ObjectPath{\n\t\tObjectType: wop.ObjectType,\n\t\tId: wop.Id,\n\t\tRealmId: wop.RealmId,\n\t}\n\treturn zom.AddPath(o)\n}\n\n\nfunc GetObjectJourney(objectType string, objectId string) (string, error) {\n\treturn zom.GetObjectJourney(objectType, objectId)\n}\n" + } + ] + }, + "deposit": "" + } + ], + "fee": { + "gas_wanted": "15000000", + "gas_fee": "1000000ugnot" + }, + "signatures": [], + "memo": "" +} + diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 02691f89c3e..990fc82c1c7 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -24,6 +24,7 @@ type InMemoryNodeConfig struct { TMConfig *tmcfg.Config GenesisTxHandler GenesisTxHandler GenesisMaxVMCycles int64 + DB *memdb.MemDB // will be initialized if nil } // NewMockedPrivValidator generate a new key @@ -86,6 +87,11 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, } evsw := events.NewEventSwitch() + // initialize db if nil + mdb := cfg.DB + if mdb == nil { + mdb = memdb.NewMemDB() + } // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ @@ -93,7 +99,7 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, GnoRootDir: cfg.TMConfig.RootDir, GenesisTxHandler: cfg.GenesisTxHandler, MaxCycles: cfg.GenesisMaxVMCycles, - DB: memdb.NewMemDB(), + DB: mdb, EventSwitch: evsw, CacheStdlibLoad: true, }) diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 0462b0c7639..12d37728a04 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -25,6 +25,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/bip39" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/db/memdb" tm2Log "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/std" "github.com/rogpeppe/go-internal/testscript" @@ -71,6 +72,7 @@ func RunGnolandTestscripts(t *testing.T, txtarDir string) { type testNode struct { *node.Node + cfg *gnoland.InMemoryNodeConfig nGnoKeyExec uint // Counter for execution of gnokey. } @@ -188,16 +190,38 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // setup genesis state cfg.Genesis.AppState = *genesis + cfg.DB = memdb.NewMemDB() // so it can be reused when restarting. n, remoteAddr := TestingInMemoryNode(t, logger, cfg) // Register cleanup - nodes[sid] = &testNode{Node: n} + nodes[sid] = &testNode{Node: n, cfg: cfg} // Add default environments ts.Setenv("RPC_ADDR", remoteAddr) fmt.Fprintln(ts.Stdout(), "node started successfully") + case "restart": + // XXX: unstable, should try to use it in a working scenario + n, ok := nodes[sid] + if !ok { + err = fmt.Errorf("node must be started before being restarted") + break + } + + if stopErr := n.Stop(); stopErr != nil { + err = fmt.Errorf("error stopping node: %w", stopErr) + break + } + + // Create new node with same config. + newNode, newRemoteAddr := TestingInMemoryNode(t, logger, n.cfg) + + // Update testNode and environment variables. + n.Node = newNode + ts.Setenv("RPC_ADDR", newRemoteAddr) + + fmt.Fprintln(ts.Stdout(), "node restarted successfully") case "stop": n, ok := nodes[sid] if !ok { diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 3caad44892b..c4acef4ea77 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -92,31 +92,21 @@ func (vm *VMKeeper) Initialize( if vm.gnoStore != nil { panic("should not happen") } - baseSDKStore := ms.GetStore(vm.baseKey) - iavlSDKStore := ms.GetStore(vm.iavlKey) + baseStore := ms.GetStore(vm.baseKey) + iavlStore := ms.GetStore(vm.iavlKey) - if cacheStdlibLoad { - // Testing case (using the cache speeds up starting many nodes) - vm.gnoStore = cachedStdlibLoad(vm.stdlibsDir, baseSDKStore, iavlSDKStore) - } else { - // On-chain case - vm.gnoStore = uncachedPackageLoad(logger, vm.stdlibsDir, baseSDKStore, iavlSDKStore) - } -} - -func uncachedPackageLoad( - logger *slog.Logger, - stdlibsDir string, - baseStore, iavlStore store.Store, -) gno.Store { alloc := gno.NewAllocator(maxAllocTx) - gnoStore := gno.NewStore(alloc, baseStore, iavlStore) - gnoStore.SetNativeStore(stdlibs.NativeStore) - if gnoStore.NumMemPackages() == 0 { + vm.gnoStore = gno.NewStore(alloc, baseStore, iavlStore) + vm.gnoStore.SetNativeStore(stdlibs.NativeStore) + if vm.gnoStore.NumMemPackages() == 0 { // No packages in the store; set up the stdlibs. start := time.Now() - loadStdlib(stdlibsDir, gnoStore) + if cacheStdlibLoad { + cachedLoadStdlib(vm.stdlibsDir, vm.gnoStore, baseStore, iavlStore) + } else { + loadStdlib(vm.stdlibsDir, vm.gnoStore) + } // XXX Quick and dirty to make this function work on non-validator nodes iter := iavlStore.Iterator(nil, nil) @@ -149,7 +139,7 @@ func uncachedPackageLoad( gno.MachineOptions{ PkgPath: "", Output: os.Stdout, // XXX - Store: gnoStore, + Store: vm.gnoStore, }) defer m2.Release() gno.DisableDebug() @@ -159,7 +149,6 @@ func uncachedPackageLoad( logger.Debug("GnoVM packages preprocessed", "elapsed", time.Since(start)) } - return gnoStore } var iavlBackupPrefix = []byte("init_iavl_backup:") @@ -173,7 +162,7 @@ func isStoreEmpty(st store.Store) bool { return true } -func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.Store { +func cachedLoadStdlib(stdlibsDir string, gnoStore gno.Store, baseStore, iavlStore store.Store) { cachedStdlibOnce.Do(func() { cachedStdlibBase = memdb.NewMemDB() cachedStdlibIavl = memdb.NewMemDB() @@ -195,11 +184,7 @@ func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.S iavlStore.Set(itr.Key(), itr.Value()) } - alloc := gno.NewAllocator(maxAllocTx) - gs := gno.NewStore(alloc, baseStore, iavlStore) - gs.SetNativeStore(stdlibs.NativeStore) - gno.CopyCachesFromStore(gs, cachedGnoStore) - return gs + gno.CopyCachesFromStore(gnoStore, cachedGnoStore) } var ( From 57f6f47c0895061faa21ddbc6ef1c8009916a1b3 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 02:25:26 +0200 Subject: [PATCH 04/45] chain init and restart now works! --- gno.land/pkg/gnoland/app.go | 32 +++++++++++------ gno.land/pkg/sdk/vm/keeper.go | 66 ++++++++--------------------------- tm2/pkg/sdk/baseapp.go | 14 ++++++++ 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index bc1f6ee405a..e55a5980efd 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -5,6 +5,7 @@ import ( "log/slog" "path/filepath" "strconv" + "time" "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -23,7 +24,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/iavl" // Only goleveldb is supported for now. _ "github.com/gnolang/gno/tm2/pkg/db/_tags" @@ -76,32 +76,28 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { } // Capabilities keys. - mainKey := store.NewStoreKey("main") baseKey := store.NewStoreKey("base") gnoKey := store.NewStoreKey("gno") // Create BaseApp. // TODO: Add a consensus based min gas prices for the node, by default it does not check - baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, gnoKey) baseApp.SetAppVersion("dev") // Set mounts for BaseApp's MultiStore. - baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) - // XXX: Embed this ? - stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") baseApp.MountStoreWithDB(gnoKey, gnostore.StoreConstructor(gnolang.NewAllocator(500*1000*1000), func(gs gnolang.Store) { - gs.SetPackageGetter(vm.PackageGetter(stdlibsDir)) gs.SetNativeStore(stdlibs.NativeStore) }), cfg.DB) // Construct keepers. - acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) + acctKpr := auth.NewAccountKeeper(gnoKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) vmk := vm.NewVMKeeper(gnoKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler)) + stdlibDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") + baseApp.SetInitChainer(InitChainer(baseApp, vmk, acctKpr, bankKpr, cfg.GenesisTxHandler, stdlibDir)) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -150,7 +146,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Initialize the VMKeeper. ms := baseApp.GetCacheMultiStore() - vmk.Initialize(cfg.Logger, ms, cfg.CacheStdlibLoad) + vmk.Initialize(cfg.Logger, ms) ms.MultiWrite() // XXX why was't this needed? return baseApp, nil @@ -195,11 +191,24 @@ func PanicOnFailingTxHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { // InitChainer returns a function that can initialize the chain with genesis. func InitChainer( baseApp *sdk.BaseApp, + vmKpr vm.VMKeeperI, acctKpr auth.AccountKeeperI, bankKpr bank.BankKeeperI, resHandler GenesisTxHandler, + stdlibDir string, ) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + start := time.Now() + ctx.Logger().Debug("InitChainer: started") + + baseApp.RunAsDeliverTx(func(deliverCtx sdk.Context) bool { + vmKpr.LoadStdlib(deliverCtx, stdlibDir) + return true + }) + + ctx.Logger().Debug("InitChainer: standard libraries loaded", + "elapsed", time.Since(start)) + txResponses := []abci.ResponseDeliverTx{} if req.AppState != nil { @@ -238,6 +247,9 @@ func InitChainer( } } + ctx.Logger().Debug("InitChainer: genesis transactions loaded", + "elapsed", time.Since(start)) + // Done! return abci.ResponseInitChain{ Validators: req.Validators, diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 059668d2c04..d3f5a98ad36 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -46,6 +46,7 @@ type VMKeeperI interface { Call(ctx sdk.Context, msg MsgCall) (res string, err error) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) + LoadStdlib(ctx sdk.Context, stdlibDir string) } var _ VMKeeperI = &VMKeeper{} @@ -79,58 +80,15 @@ func NewVMKeeper( func (vm *VMKeeper) Initialize( logger *slog.Logger, ms store.MultiStore, - cacheStdlibLoad bool, ) { gnoStore := gnostore.GetGnoStore(ms.GetStore(vm.gnoStoreKey)) - if cacheStdlibLoad { - // Testing case (using the cache speeds up starting many nodes) - cachedStdlibLoad(gnoStore, vm.stdlibsDir) - } else { - // On-chain case - uncachedPackageLoad(gnoStore, logger, vm.stdlibsDir) - } -} - -func uncachedPackageLoad( - gnoStore gno.Store, - logger *slog.Logger, - stdlibsDir string, -) gno.Store { - alloc := gno.NewAllocator(maxAllocTx) - if gnoStore.NumMemPackages() == 0 { - // No packages in the store; set up the stdlibs. - start := time.Now() - - loadStdlib(stdlibsDir, gnoStore) - - // XXX Quick and dirty to make this function work on non-validator nodes - iter := iavlStore.Iterator(nil, nil) - for ; iter.Valid(); iter.Next() { - baseStore.Set(append(iavlBackupPrefix, iter.Key()...), iter.Value()) - } - iter.Close() - - logger.Debug("Standard libraries initialized", - "elapsed", time.Since(start)) - } else { + if gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. // TODO remove this, and generally solve for in-mem garbage collection // and memory management across many objects/types/nodes/packages. start := time.Now() - // XXX Quick and dirty to make this function work on non-validator nodes - if isStoreEmpty(iavlStore) { - iter := baseStore.Iterator(iavlBackupPrefix, nil) - for ; iter.Valid(); iter.Next() { - if !bytes.HasPrefix(iter.Key(), iavlBackupPrefix) { - break - } - iavlStore.Set(iter.Key()[len(iavlBackupPrefix):], iter.Value()) - } - iter.Close() - } - m2 := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: "", @@ -145,7 +103,7 @@ func uncachedPackageLoad( logger.Debug("GnoVM packages preprocessed", "elapsed", time.Since(start)) } - return gnoStore + return } var iavlBackupPrefix = []byte("init_iavl_backup:") @@ -159,7 +117,7 @@ func isStoreEmpty(st store.Store) bool { return true } -func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.Store { +func cachedStdlibLoad(stdlibDir string, baseStore, iavlStore store.Store) gno.Store { cachedStdlibOnce.Do(func() { cachedStdlibBase = memdb.NewMemDB() cachedStdlibIavl = memdb.NewMemDB() @@ -168,7 +126,7 @@ func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.S dbadapter.StoreConstructor(cachedStdlibBase, types.StoreOptions{}), dbadapter.StoreConstructor(cachedStdlibIavl, types.StoreOptions{})) cachedGnoStore.SetNativeStore(stdlibs.NativeStore) - loadStdlib(stdlibsDir, cachedGnoStore) + loadStdlib(cachedGnoStore, stdlibDir) }) itr := cachedStdlibBase.Iterator(nil, nil) @@ -195,7 +153,13 @@ var ( cachedGnoStore gno.Store ) -func loadStdlib(stdlibsDir string, store gno.Store) { +// LoadStdlib loads the Gno standard library into the given store. +func (vm *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { + gs := vm.getGnoStore(ctx) + loadStdlib(gs, stdlibDir) +} + +func loadStdlib(store gno.Store, stdlibDir string) { stdlibInitList := stdlibs.InitOrder() for _, lib := range stdlibInitList { if lib == "testing" { @@ -203,12 +167,12 @@ func loadStdlib(stdlibsDir string, store gno.Store) { // like fmt and encoding/json continue } - loadStdlibPackage(lib, stdlibsDir, store) + loadStdlibPackage(lib, stdlibDir, store) } } -func loadStdlibPackage(pkgPath, stdlibsDir string, store gno.Store) { - stdlibPath := filepath.Join(stdlibsDir, pkgPath) +func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { + stdlibPath := filepath.Join(stdlibDir, pkgPath) if !osm.DirExists(stdlibPath) { // does not exist. panic(fmt.Sprintf("failed loading stdlib %q: does not exist", pkgPath)) diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 0fa26b817e1..f5cf58b9150 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -836,6 +836,20 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) return result } +func (app *BaseApp) RunAsDeliverTx(callback func(ctx Context) (ok bool)) { + ctx := app.getContextForTx(RunTxModeDeliver, nil) + + // Create a new context based off of the existing context with a cache wrapped + // multi-store in case message processing fails. + runMsgCtx, msCache := app.cacheTxContext(ctx) + ok := callback(runMsgCtx) + + // only update state if all messages pass + if ok { + msCache.MultiWrite() + } +} + // EndBlock implements the ABCI interface. func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { if app.endBlocker != nil { From ed84bf7243c75d8ad9b1f9b9a9fe621429328530 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 14:49:57 +0200 Subject: [PATCH 05/45] testing fixes, etc. --- gno.land/pkg/gnoland/app.go | 194 ++++++++++++----------- gno.land/pkg/gnoland/mock_test.go | 1 + gno.land/pkg/gnoland/node_inmemory.go | 23 +-- gno.land/pkg/integration/testing_node.go | 12 +- gno.land/pkg/sdk/gnostore/store.go | 61 +++---- gno.land/pkg/sdk/vm/common_test.go | 16 +- gno.land/pkg/sdk/vm/keeper.go | 57 ++----- gno.land/pkg/sdk/vm/keeper_test.go | 4 +- gnovm/pkg/gnolang/store.go | 38 +++-- tm2/pkg/sdk/baseapp.go | 14 -- 10 files changed, 207 insertions(+), 213 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e55a5980efd..e68462c85c5 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -10,8 +10,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" - "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/stdlibs" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -32,28 +30,22 @@ import ( ) type AppOptions struct { - DB dbm.DB - // `gnoRootDir` should point to the local location of the gno repository. - // It serves as the gno equivalent of GOROOT. - GnoRootDir string - GenesisTxHandler GenesisTxHandler - Logger *slog.Logger - EventSwitch events.EventSwitch - MaxCycles int64 - // Whether to cache the result of loading the standard libraries. - // This is useful if you have to start many nodes, like in testing. - // This disables loading existing packages; so it should only be used - // on a fresh database. - CacheStdlibLoad bool + DB dbm.DB + Logger *slog.Logger + EventSwitch events.EventSwitch + MaxCycles int64 + InitChainerConfig } func NewAppOptions() *AppOptions { return &AppOptions{ - GenesisTxHandler: PanicOnFailingTxHandler, - Logger: log.NewNoopLogger(), - DB: memdb.NewMemDB(), - GnoRootDir: gnoenv.RootDir(), - EventSwitch: events.NilEventSwitch(), + Logger: log.NewNoopLogger(), + DB: memdb.NewMemDB(), + EventSwitch: events.NilEventSwitch(), + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + }, } } @@ -86,9 +78,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Set mounts for BaseApp's MultiStore. baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) - baseApp.MountStoreWithDB(gnoKey, gnostore.StoreConstructor(gnolang.NewAllocator(500*1000*1000), func(gs gnolang.Store) { - gs.SetNativeStore(stdlibs.NativeStore) - }), cfg.DB) + baseApp.MountStoreWithDB(gnoKey, gnostore.StoreConstructor, cfg.DB) // Construct keepers. acctKpr := auth.NewAccountKeeper(gnoKey, ProtoGnoAccount) @@ -96,8 +86,10 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { vmk := vm.NewVMKeeper(gnoKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer - stdlibDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") - baseApp.SetInitChainer(InitChainer(baseApp, vmk, acctKpr, bankKpr, cfg.GenesisTxHandler, stdlibDir)) + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr = acctKpr, bankKpr, vmk + baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -163,7 +155,7 @@ func NewApp( cfg := NewAppOptions() if skipFailingGenesisTxs { - cfg.GenesisTxHandler = NoopGenesisTxHandler + cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } // Get main DB. @@ -178,84 +170,106 @@ func NewApp( return NewAppWithOptions(cfg) } -type GenesisTxHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) +// GenesisTxResultHandler is called in the InitChainer after a genesis +// transaction is executed. +type GenesisTxResultHandler func(ctx sdk.Context, tx std.Tx, res sdk.Result) -func NoopGenesisTxHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} +// NoopGenesisTxResultHandler is a no-op GenesisTxResultHandler. +func NoopGenesisTxResultHandler(_ sdk.Context, _ std.Tx, _ sdk.Result) {} -func PanicOnFailingTxHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { +// PanicOnFailingTxResultHandler handles genesis transactions by panicking if +// res.IsErr() returns true. +func PanicOnFailingTxResultHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { if res.IsErr() { panic(res.Log) } } -// InitChainer returns a function that can initialize the chain with genesis. -func InitChainer( - baseApp *sdk.BaseApp, - vmKpr vm.VMKeeperI, - acctKpr auth.AccountKeeperI, - bankKpr bank.BankKeeperI, - resHandler GenesisTxHandler, - stdlibDir string, -) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { - return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - start := time.Now() - ctx.Logger().Debug("InitChainer: started") - - baseApp.RunAsDeliverTx(func(deliverCtx sdk.Context) bool { - vmKpr.LoadStdlib(deliverCtx, stdlibDir) - return true - }) - - ctx.Logger().Debug("InitChainer: standard libraries loaded", - "elapsed", time.Since(start)) - - txResponses := []abci.ResponseDeliverTx{} - - if req.AppState != nil { - // Get genesis state - genState := req.AppState.(GnoGenesisState) - - // Parse and set genesis state balances - for _, bal := range genState.Balances { - acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) - acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) - if err != nil { - panic(err) - } - } +// InitChainerConfig keeps the configuration for the InitChainer. +type InitChainerConfig struct { + // Handles the results of each genesis transaction. + GenesisTxResultHandler + + // Standard library directory. + StdlibDir string + // Whether to keep a record of the DB operations to load standard libraries, + // so they can be quickly replicated on additional genesis executions. + // This should be used for integration testing, where InitChainer will be + // called several times. + CacheStdlibLoad bool + + // These fields are passed directly by NewAppWithOptions, and should not be + // configurable by end-users. + baseApp *sdk.BaseApp + vmKpr vm.VMKeeperI + acctKpr auth.AccountKeeperI + bankKpr bank.BankKeeperI +} + +// InitChainer is the function that can be used as a [sdk.InitChainer]. +func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + start := time.Now() + ctx.Logger().Debug("InitChainer: started") + + // load standard libraries + // need to write to the MultiStore directly - so that the standard + // libraries are available when we process genesis txs + if cfg.CacheStdlibLoad { + cfg.vmKpr.LoadStdlibCached(ctx, cfg.StdlibDir) + } else { + cfg.vmKpr.LoadStdlib(ctx, cfg.StdlibDir) + } + ctx.MultiStore().MultiWrite() - // Run genesis txs - for _, tx := range genState.Txs { - res := baseApp.Deliver(tx) - if res.IsErr() { - ctx.Logger().Error( - "Unable to deliver genesis tx", - "log", res.Log, - "error", res.Error, - "gas-used", res.GasUsed, - ) - } - - txResponses = append(txResponses, abci.ResponseDeliverTx{ - ResponseBase: res.ResponseBase, - GasWanted: res.GasWanted, - GasUsed: res.GasUsed, - }) - - resHandler(ctx, tx, res) + ctx.Logger().Debug("InitChainer: standard libraries loaded", + "elapsed", time.Since(start)) + + txResponses := []abci.ResponseDeliverTx{} + + if req.AppState != nil { + // Get genesis state + genState := req.AppState.(GnoGenesisState) + + // Parse and set genesis state balances + for _, bal := range genState.Balances { + acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) + cfg.acctKpr.SetAccount(ctx, acc) + err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount) + if err != nil { + panic(err) } } - ctx.Logger().Debug("InitChainer: genesis transactions loaded", - "elapsed", time.Since(start)) + // Run genesis txs + for _, tx := range genState.Txs { + res := cfg.baseApp.Deliver(tx) + if res.IsErr() { + ctx.Logger().Error( + "Unable to deliver genesis tx", + "log", res.Log, + "error", res.Error, + "gas-used", res.GasUsed, + ) + } + + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: res.ResponseBase, + GasWanted: res.GasWanted, + GasUsed: res.GasUsed, + }) - // Done! - return abci.ResponseInitChain{ - Validators: req.Validators, - TxResponses: txResponses, + cfg.GenesisTxResultHandler(ctx, tx, res) } } + + ctx.Logger().Debug("InitChainer: genesis transactions loaded", + "elapsed", time.Since(start)) + + // Done! + return abci.ResponseInitChain{ + Validators: req.Validators, + TxResponses: txResponses, + } } // endBlockerApp is the app abstraction required by any EndBlocker diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 1ff9f168bd1..53aed44a21f 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -53,6 +53,7 @@ type ( ) type mockVMKeeper struct { + vm.VMKeeperI addPackageFn addPackageDelegate callFn callDelegate queryFn queryEvalDelegate diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 02691f89c3e..c4a35b2b1de 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -3,6 +3,7 @@ package gnoland import ( "fmt" "log/slog" + "path/filepath" "time" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -22,8 +23,10 @@ type InMemoryNodeConfig struct { PrivValidator bft.PrivValidator // identity of the validator Genesis *bft.GenesisDoc TMConfig *tmcfg.Config - GenesisTxHandler GenesisTxHandler GenesisMaxVMCycles int64 + + // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") + InitChainerConfig } // NewMockedPrivValidator generate a new key @@ -70,7 +73,7 @@ func (cfg *InMemoryNodeConfig) validate() error { return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory") } - if cfg.GenesisTxHandler == nil { + if cfg.GenesisTxResultHandler == nil { return fmt.Errorf("`GenesisTxHandler` is required but not provided") } @@ -87,15 +90,17 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, evsw := events.NewEventSwitch() + if cfg.StdlibDir == "" { + cfg.StdlibDir = filepath.Join(cfg.TMConfig.RootDir, "gnovm", "stdlibs") + } + // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ - Logger: logger, - GnoRootDir: cfg.TMConfig.RootDir, - GenesisTxHandler: cfg.GenesisTxHandler, - MaxCycles: cfg.GenesisMaxVMCycles, - DB: memdb.NewMemDB(), - EventSwitch: evsw, - CacheStdlibLoad: true, + Logger: logger, + MaxCycles: cfg.GenesisMaxVMCycles, + DB: memdb.NewMemDB(), + EventSwitch: evsw, + InitChainerConfig: cfg.InitChainerConfig, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 993386f6b04..4b90ddac7c4 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -71,10 +71,14 @@ func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNode genesis := DefaultTestingGenesisConfig(t, gnoroot, pv.GetPubKey(), tmconfig) return &gnoland.InMemoryNodeConfig{ - PrivValidator: pv, - Genesis: genesis, - TMConfig: tmconfig, - GenesisTxHandler: gnoland.PanicOnFailingTxHandler, + PrivValidator: pv, + Genesis: genesis, + TMConfig: tmconfig, + InitChainerConfig: gnoland.InitChainerConfig{ + GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler, + CacheStdlibLoad: true, + // StdlibDir automatically set + }, } } diff --git a/gno.land/pkg/sdk/gnostore/store.go b/gno.land/pkg/sdk/gnostore/store.go index 1cec5088149..709757f1559 100644 --- a/gno.land/pkg/sdk/gnostore/store.go +++ b/gno.land/pkg/sdk/gnostore/store.go @@ -4,30 +4,33 @@ package gnostore import ( "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" dbm "github.com/gnolang/gno/tm2/pkg/db" - "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/cache" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" "github.com/gnolang/gno/tm2/pkg/store/iavl" "github.com/gnolang/gno/tm2/pkg/store/types" ) +// Allocation limit for GnoVM. +const maxAllocTx = 500_000_000 + // StoreConstructor implements store.CommitStoreConstructor. // It can be used in conjunction with CommitMultiStore.MountStoreWithDB. // initialize should only contain basic setter for the immutable config // (like SetNativeStore); it should not initialize packages. -func StoreConstructor(alloc *gnolang.Allocator, initialize func(gs gnolang.Store)) store.CommitStoreConstructor { - return func(db dbm.DB, opts types.StoreOptions) types.CommitStore { - iavlStore := iavl.StoreConstructor(db, opts) - base := dbadapter.StoreConstructor(db, opts) - gno := gnolang.NewStore(alloc, base, iavlStore) - initialize(gno) - return &Store{ - Store: iavlStore.(*iavl.Store), - opts: opts, - base: base.(dbadapter.Store), - gno: gno, - } +func StoreConstructor(db dbm.DB, opts types.StoreOptions) types.CommitStore { + iavlStore := iavl.StoreConstructor(db, opts) + base := dbadapter.StoreConstructor(db, opts) + + alloc := gnolang.NewAllocator(maxAllocTx) + gno := gnolang.NewStore(alloc, base, iavlStore) + gno.SetNativeStore(stdlibs.NativeStore) + return &Store{ + Store: iavlStore.(*iavl.Store), + opts: opts, + base: base.(dbadapter.Store), + gno: gno, } } @@ -58,14 +61,6 @@ func (s *Store) SetStoreOptions(opts2 types.StoreOptions) { func (s *Store) GnoStore() gnolang.Store { return s.gno } -type cacheStore struct { - types.Store - - base types.Store - gno gnolang.TransactionStore - rootGno gnolang.Store -} - func (s *Store) CacheWrap() types.Store { s2 := &cacheStore{ Store: cache.New(s.Store), @@ -76,16 +71,24 @@ func (s *Store) CacheWrap() types.Store { return s2 } -func (store *cacheStore) Write() { - store.Store.Write() - store.base.Write() - store.gno.Write() +type cacheStore struct { + types.Store + + base types.Store + gno gnolang.TransactionStore + rootGno gnolang.Store +} + +func (s *cacheStore) Write() { + s.Store.Write() + s.base.Write() + s.gno.Write() } -func (store *cacheStore) Flush() { - store.Store.(types.Flusher).Flush() - store.base.(types.Flusher).Flush() - store.gno.Write() +func (s *cacheStore) Flush() { + s.Store.(types.Flusher).Flush() + s.base.(types.Flusher).Flush() + s.gno.Write() } func (s *cacheStore) CacheWrap() types.Store { diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 6dd8050d6b6..00b4d04e163 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -5,6 +5,7 @@ package vm import ( "path/filepath" + "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/log" @@ -14,7 +15,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/iavl" ) type testEnv struct { @@ -36,21 +36,23 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { db := memdb.NewMemDB() baseCapKey := store.NewStoreKey("baseCapKey") - iavlCapKey := store.NewStoreKey("iavlCapKey") + gnoCapKey := store.NewStoreKey("gnoCapKey") + // Mount db store and gnostore ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) - ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) + ms.MountStoreWithDB(gnoCapKey, gnostore.StoreConstructor, db) ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) - acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) + acck := authm.NewAccountKeeper(gnoCapKey, std.ProtoBaseAccount) bank := bankm.NewBankKeeper(acck) - stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") - vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, stdlibsDir, 100_000_000) + vmk := NewVMKeeper(gnoCapKey, acck, bank, 100_000_000) mcw := ms.MultiCacheWrap() - vmk.Initialize(log.NewNoopLogger(), mcw, cacheStdlibs) + vmk.Initialize(log.NewNoopLogger(), mcw) + stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") + vmk.LoadStdlibCached(ctx, stdlibsDir) mcw.MultiWrite() return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index d3f5a98ad36..7d169644c09 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -34,10 +34,7 @@ import ( "go.opentelemetry.io/otel/metric" ) -const ( - maxAllocTx = 500 * 1000 * 1000 - maxAllocQuery = 1500 * 1000 * 1000 // higher limit for queries -) +const maxAllocQuery = 1_500_000_000 // vm.VMKeeperI defines a module interface that supports Gno // smart contracts programming (scripting). @@ -47,6 +44,7 @@ type VMKeeperI interface { QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) Run(ctx sdk.Context, msg MsgRun) (res string, err error) LoadStdlib(ctx sdk.Context, stdlibDir string) + LoadStdlibCached(ctx sdk.Context, stdlibDir string) } var _ VMKeeperI = &VMKeeper{} @@ -106,53 +104,28 @@ func (vm *VMKeeper) Initialize( return } -var iavlBackupPrefix = []byte("init_iavl_backup:") - -func isStoreEmpty(st store.Store) bool { - iter := st.Iterator(nil, nil) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - return false - } - return true -} +var ( + cachedStdlibOnce sync.Once + cachedStdlibBase store.Store + cachedStdlibIavl store.Store + cachedGnoStore gno.Store +) -func cachedStdlibLoad(stdlibDir string, baseStore, iavlStore store.Store) gno.Store { +// LoadStdlib loads the Gno standard library into the given store. +func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { cachedStdlibOnce.Do(func() { - cachedStdlibBase = memdb.NewMemDB() - cachedStdlibIavl = memdb.NewMemDB() + cachedStdlibBase = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) + cachedStdlibIavl = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) - cachedGnoStore = gno.NewStore(nil, - dbadapter.StoreConstructor(cachedStdlibBase, types.StoreOptions{}), - dbadapter.StoreConstructor(cachedStdlibIavl, types.StoreOptions{})) + cachedGnoStore = gno.NewStore(nil, cachedStdlibBase, cachedStdlibIavl) cachedGnoStore.SetNativeStore(stdlibs.NativeStore) loadStdlib(cachedGnoStore, stdlibDir) }) - itr := cachedStdlibBase.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - baseStore.Set(itr.Key(), itr.Value()) - } - - itr = cachedStdlibIavl.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - iavlStore.Set(itr.Key(), itr.Value()) - } - - alloc := gno.NewAllocator(maxAllocTx) - gs := gno.NewStore(alloc, baseStore, iavlStore) - gs.SetNativeStore(stdlibs.NativeStore) - gno.CopyCachesFromStore(gs, cachedGnoStore) - return gs + gs := vm.getGnoStore(ctx) + gno.CopyFromCachedStore(gs, cachedGnoStore, cachedStdlibBase, cachedStdlibIavl) } -var ( - cachedStdlibOnce sync.Once - cachedStdlibBase *memdb.MemDB - cachedStdlibIavl *memdb.MemDB - cachedGnoStore gno.Store -) - // LoadStdlib loads the Gno standard library into the given store. func (vm *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { gs := vm.getGnoStore(ctx) diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index a86ca5e4a97..fda9b9c93f4 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -36,12 +36,12 @@ func Echo() string {return "hello world"}`, } pkgPath := "gno.land/r/test" msg1 := NewMsgAddPackage(addr, pkgPath, files) - assert.Nil(t, env.vmk.gnoStore.GetPackage(pkgPath, false)) + assert.Nil(t, env.vmk.getGnoStore(ctx).GetPackage(pkgPath, false)) err := env.vmk.AddPackage(ctx, msg1) assert.NoError(t, err) - assert.NotNil(t, env.vmk.gnoStore.GetPackage(pkgPath, false)) + assert.NotNil(t, env.vmk.getGnoStore(ctx).GetPackage(pkgPath, false)) err = env.vmk.AddPackage(ctx, msg1) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 6eedebeba9b..06def83c39c 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -127,17 +127,6 @@ func (b *bufferedTxMap[K, V]) init() { b.source = make(map[K]V) } -// clone allows to create a shallow clone of b, in a non-buffered context. -func (b bufferedTxMap[K, V]) clone() bufferedTxMap[K, V] { - if b.dirty != nil { - panic("cannot clone with a dirty buffer") - } - - return bufferedTxMap[K, V]{ - source: maps.Clone(b.source), - } -} - // buffered creates a copy of b, which has a usable dirty map. func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { if b.dirty != nil { @@ -294,11 +283,28 @@ func (ds *defaultStore) write() { // CopyCachesFromStore allows to copy a store's internal object, type and // BlockNode cache into the dst store. // This is mostly useful for testing, where many stores have to be initialized. -func CopyCachesFromStore(dst, src Store) { - ds, ss := dst.(*defaultStore), src.(*defaultStore) - ds.cacheObjects = maps.Clone(ss.cacheObjects) - ds.cacheTypes = ss.cacheTypes.clone() - ds.cacheNodes = ss.cacheNodes.clone() +func CopyFromCachedStore(destStore, cachedStore Store, cachedBase, cachedIavl store.Store) { + ds, ss := destStore.(transactionStore), cachedStore.(*defaultStore) + + iter := cachedBase.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + ds.baseStore.Set(iter.Key(), iter.Value()) + } + iter = cachedIavl.Iterator(nil, nil) + for ; iter.Valid(); iter.Next() { + ds.iavlStore.Set(iter.Key(), iter.Value()) + } + + if ss.cacheTypes.dirty != nil || + ss.cacheNodes.dirty != nil { + panic("cacheTypes and cacheNodes should be unbuffered") + } + for k, v := range ss.cacheTypes.source { + ds.cacheTypes.Set(k, v) + } + for k, v := range ss.cacheNodes.source { + ds.cacheNodes.Set(k, v) + } } func (ds *defaultStore) GetAllocator() *Allocator { diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index f5cf58b9150..0fa26b817e1 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -836,20 +836,6 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) return result } -func (app *BaseApp) RunAsDeliverTx(callback func(ctx Context) (ok bool)) { - ctx := app.getContextForTx(RunTxModeDeliver, nil) - - // Create a new context based off of the existing context with a cache wrapped - // multi-store in case message processing fails. - runMsgCtx, msCache := app.cacheTxContext(ctx) - ok := callback(runMsgCtx) - - // only update state if all messages pass - if ok { - msCache.MultiWrite() - } -} - // EndBlock implements the ABCI interface. func (app *BaseApp) EndBlock(req abci.RequestEndBlock) (res abci.ResponseEndBlock) { if app.endBlocker != nil { From 4b318a04e273dbccff75b81191b233e451a22322 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 18:28:22 +0200 Subject: [PATCH 06/45] fixup gnodev --- contribs/gnodev/pkg/dev/node.go | 4 ++-- gno.land/pkg/sdk/gnostore/store.go | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 7f0c266bf48..bf3f32fab3b 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -468,7 +468,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Setup node config nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) - nodeConfig.GenesisTxHandler = n.genesisTxHandler + nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock // recoverFromError handles panics and converts them to errors. @@ -511,7 +511,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) return nil } -func (n *Node) genesisTxHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { +func (n *Node) genesisTxResultHandler(ctx sdk.Context, tx std.Tx, res sdk.Result) { if !res.IsErr() { return } diff --git a/gno.land/pkg/sdk/gnostore/store.go b/gno.land/pkg/sdk/gnostore/store.go index 709757f1559..03268854654 100644 --- a/gno.land/pkg/sdk/gnostore/store.go +++ b/gno.land/pkg/sdk/gnostore/store.go @@ -3,6 +3,9 @@ package gnostore import ( + "fmt" + + "github.com/davecgh/go-spew/spew" "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -35,6 +38,12 @@ func StoreConstructor(db dbm.DB, opts types.StoreOptions) types.CommitStore { } func GetGnoStore(s types.Store) gnolang.Store { + fmt.Printf("XXXXXXXXX: %T\n", s) + if _, ok := s.(interface{ Print() }); ok { + (&spew.ConfigState{ + MaxDepth: 3, + }).Dump(s) + } gs, ok := s.(interface { GnoStore() gnolang.Store }) From 7be7e3766f5d976037354a727fa360896d6c59a3 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 22:12:07 +0200 Subject: [PATCH 07/45] testing --- gno.land/pkg/gnoland/app.go | 44 +++++++---- gno.land/pkg/sdk/gnostore/store.go | 113 ----------------------------- gno.land/pkg/sdk/vm/keeper.go | 90 +++++++++++++++-------- tm2/pkg/sdk/abci.go | 8 ++ tm2/pkg/sdk/baseapp.go | 7 ++ tm2/pkg/sdk/options.go | 14 ++++ 6 files changed, 117 insertions(+), 159 deletions(-) delete mode 100644 gno.land/pkg/sdk/gnostore/store.go diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e68462c85c5..4b3e0ad4cdd 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -7,7 +7,6 @@ import ( "strconv" "time" - "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -22,6 +21,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" // Only goleveldb is supported for now. _ "github.com/gnolang/gno/tm2/pkg/db/_tags" @@ -68,22 +68,22 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { } // Capabilities keys. + mainKey := store.NewStoreKey("iavl") baseKey := store.NewStoreKey("base") - gnoKey := store.NewStoreKey("gno") // Create BaseApp. // TODO: Add a consensus based min gas prices for the node, by default it does not check - baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, gnoKey) + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) baseApp.SetAppVersion("dev") // Set mounts for BaseApp's MultiStore. + baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) - baseApp.MountStoreWithDB(gnoKey, gnostore.StoreConstructor, cfg.DB) // Construct keepers. - acctKpr := auth.NewAccountKeeper(gnoKey, ProtoGnoAccount) + acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - vmk := vm.NewVMKeeper(gnoKey, acctKpr, bankKpr, cfg.MaxCycles) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer icc := cfg.InitChainerConfig @@ -102,14 +102,22 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { func(ctx sdk.Context, tx std.Tx, simulate bool) ( newCtx sdk.Context, res sdk.Result, abort bool, ) { + // Create Gno transaction store. + ctx = vmk.MakeGnoTransactionStore(ctx) + // Override auth params. - ctx = ctx.WithValue( - auth.AuthParamsContextKey{}, auth.DefaultParams()) + ctx = ctx. + WithValue(auth.AuthParamsContextKey{}, auth.DefaultParams()) // Continue on with default auth ante handler. newCtx, res, abort = authAnteHandler(ctx, tx, simulate) return }, ) + baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) { + if result.IsOK() { + vmk.CommitGnoTransactionStore(ctx) + } + }) // Set up the event collector c := newCollector[validatorUpdate]( @@ -211,15 +219,19 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh start := time.Now() ctx.Logger().Debug("InitChainer: started") - // load standard libraries - // need to write to the MultiStore directly - so that the standard - // libraries are available when we process genesis txs - if cfg.CacheStdlibLoad { - cfg.vmKpr.LoadStdlibCached(ctx, cfg.StdlibDir) - } else { - cfg.vmKpr.LoadStdlib(ctx, cfg.StdlibDir) + { + // load standard libraries + // need to write to the MultiStore directly - so that the standard + // libraries are available when we process genesis txs + stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) + if cfg.CacheStdlibLoad { + cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) + } else { + cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) + } + cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) + stdlibCtx.MultiStore().MultiWrite() } - ctx.MultiStore().MultiWrite() ctx.Logger().Debug("InitChainer: standard libraries loaded", "elapsed", time.Since(start)) diff --git a/gno.land/pkg/sdk/gnostore/store.go b/gno.land/pkg/sdk/gnostore/store.go deleted file mode 100644 index 03268854654..00000000000 --- a/gno.land/pkg/sdk/gnostore/store.go +++ /dev/null @@ -1,113 +0,0 @@ -// Package gnostore implements a tm2 store which can interoperate with the GnoVM's -// own store. -package gnostore - -import ( - "fmt" - - "github.com/davecgh/go-spew/spew" - "github.com/gnolang/gno/gnovm/pkg/gnolang" - "github.com/gnolang/gno/gnovm/stdlibs" - dbm "github.com/gnolang/gno/tm2/pkg/db" - "github.com/gnolang/gno/tm2/pkg/store/cache" - "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/iavl" - "github.com/gnolang/gno/tm2/pkg/store/types" -) - -// Allocation limit for GnoVM. -const maxAllocTx = 500_000_000 - -// StoreConstructor implements store.CommitStoreConstructor. -// It can be used in conjunction with CommitMultiStore.MountStoreWithDB. -// initialize should only contain basic setter for the immutable config -// (like SetNativeStore); it should not initialize packages. -func StoreConstructor(db dbm.DB, opts types.StoreOptions) types.CommitStore { - iavlStore := iavl.StoreConstructor(db, opts) - base := dbadapter.StoreConstructor(db, opts) - - alloc := gnolang.NewAllocator(maxAllocTx) - gno := gnolang.NewStore(alloc, base, iavlStore) - gno.SetNativeStore(stdlibs.NativeStore) - return &Store{ - Store: iavlStore.(*iavl.Store), - opts: opts, - base: base.(dbadapter.Store), - gno: gno, - } -} - -func GetGnoStore(s types.Store) gnolang.Store { - fmt.Printf("XXXXXXXXX: %T\n", s) - if _, ok := s.(interface{ Print() }); ok { - (&spew.ConfigState{ - MaxDepth: 3, - }).Dump(s) - } - gs, ok := s.(interface { - GnoStore() gnolang.Store - }) - if ok { - return gs.GnoStore() - } - return nil -} - -type Store struct { - *iavl.Store // iavl - - opts types.StoreOptions - base dbadapter.Store - gno gnolang.Store -} - -func (s *Store) GetStoreOptions() types.StoreOptions { return s.opts } - -func (s *Store) SetStoreOptions(opts2 types.StoreOptions) { - s.opts = opts2 - s.Store.SetStoreOptions(opts2) -} - -func (s *Store) GnoStore() gnolang.Store { return s.gno } - -func (s *Store) CacheWrap() types.Store { - s2 := &cacheStore{ - Store: cache.New(s.Store), - base: cache.New(s.base), - rootGno: s.gno, - } - s2.gno = s.gno.BeginTransaction(s2.base, s2.Store) - return s2 -} - -type cacheStore struct { - types.Store - - base types.Store - gno gnolang.TransactionStore - rootGno gnolang.Store -} - -func (s *cacheStore) Write() { - s.Store.Write() - s.base.Write() - s.gno.Write() -} - -func (s *cacheStore) Flush() { - s.Store.(types.Flusher).Flush() - s.base.(types.Flusher).Flush() - s.gno.Write() -} - -func (s *cacheStore) CacheWrap() types.Store { - s2 := &cacheStore{ - Store: cache.New(s.Store), - base: cache.New(s.base), - rootGno: s.rootGno, - } - s2.gno = s.rootGno.BeginTransaction(s2.base, s2.Store) - return s2 -} - -func (s *cacheStore) GnoStore() gnolang.Store { return s.gno } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 7d169644c09..987fe45100b 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" + "github.com/gnolang/gno/gnovm/pkg/gnolang" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -34,7 +34,10 @@ import ( "go.opentelemetry.io/otel/metric" ) -const maxAllocQuery = 1_500_000_000 +const ( + maxAllocTx = 500_000_000 + maxAllocQuery = 1_500_000_000 // higher limit for queries +) // vm.VMKeeperI defines a module interface that supports Gno // smart contracts programming (scripting). @@ -45,32 +48,40 @@ type VMKeeperI interface { Run(ctx sdk.Context, msg MsgRun) (res string, err error) LoadStdlib(ctx sdk.Context, stdlibDir string) LoadStdlibCached(ctx sdk.Context, stdlibDir string) + MakeGnoTransactionStore(ctx sdk.Context) sdk.Context + CommitGnoTransactionStore(ctx sdk.Context) } var _ VMKeeperI = &VMKeeper{} // VMKeeper holds all package code and store state. type VMKeeper struct { - gnoStoreKey store.StoreKey - acck auth.AccountKeeper - bank bank.BankKeeper + baseKey store.StoreKey + iavlKey store.StoreKey + acck auth.AccountKeeper + bank bank.BankKeeper + + // cached, the DeliverTx persistent state. + gnoStore gno.Store maxCycles int64 // max allowed cylces on VM executions } // NewVMKeeper returns a new VMKeeper. func NewVMKeeper( - gnoStoreKey store.StoreKey, + baseKey store.StoreKey, + iavlKey store.StoreKey, acck auth.AccountKeeper, bank bank.BankKeeper, maxCycles int64, ) *VMKeeper { // TODO: create an Options struct to avoid too many constructor parameters vmk := &VMKeeper{ - gnoStoreKey: gnoStoreKey, - acck: acck, - bank: bank, - maxCycles: maxCycles, + baseKey: baseKey, + iavlKey: iavlKey, + acck: acck, + bank: bank, + maxCycles: maxCycles, } return vmk } @@ -79,9 +90,17 @@ func (vm *VMKeeper) Initialize( logger *slog.Logger, ms store.MultiStore, ) { - gnoStore := gnostore.GetGnoStore(ms.GetStore(vm.gnoStoreKey)) + if vm.gnoStore != nil { + panic("should not happen") + } + baseSDKStore := ms.GetStore(vm.baseKey) + iavlSDKStore := ms.GetStore(vm.iavlKey) - if gnoStore.NumMemPackages() > 0 { + alloc := gnolang.NewAllocator(maxAllocTx) + vm.gnoStore = gnolang.NewStore(alloc, baseSDKStore, iavlSDKStore) + vm.gnoStore.SetNativeStore(stdlibs.NativeStore) + + if vm.gnoStore.NumMemPackages() > 0 { // for now, all mem packages must be re-run after reboot. // TODO remove this, and generally solve for in-mem garbage collection // and memory management across many objects/types/nodes/packages. @@ -91,7 +110,7 @@ func (vm *VMKeeper) Initialize( gno.MachineOptions{ PkgPath: "", Output: os.Stdout, // XXX - Store: gnoStore, + Store: vm.gnoStore, }) defer m2.Release() gno.DisableDebug() @@ -122,13 +141,13 @@ func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { loadStdlib(cachedGnoStore, stdlibDir) }) - gs := vm.getGnoStore(ctx) + gs := vm.gnoStore gno.CopyFromCachedStore(gs, cachedGnoStore, cachedStdlibBase, cachedStdlibIavl) } // LoadStdlib loads the Gno standard library into the given store. func (vm *VMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { - gs := vm.getGnoStore(ctx) + gs := vm.getGnoTransactionStore(ctx) loadStdlib(gs, stdlibDir) } @@ -166,13 +185,24 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { m.RunMemPackage(memPkg, true) } -func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { - sto := ctx.MultiStore().GetStore(vm.gnoStoreKey) - gs := gnostore.GetGnoStore(sto) - if gs == nil { - panic("could not get gno store") - } - return gs +type gnoStoreContextKeyType struct{} + +var gnoStoreContextKey gnoStoreContextKeyType + +func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + ms := ctx.MultiStore() + base := ms.GetStore(vm.baseKey) + iavl := ms.GetStore(vm.iavlKey) + st := vm.gnoStore.BeginTransaction(base, iavl) + return ctx.WithValue(gnoStoreContextKey, st) +} + +func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + vm.getGnoTransactionStore(ctx).Write() +} + +func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { + return ctx.Value(gnoStoreContextKey).(gno.TransactionStore) } // Namespace can be either a user or crypto address. @@ -182,7 +212,7 @@ var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { const sysUsersPkg = "gno.land/r/sys/users" - store := vm.getGnoStore(ctx) + store := vm.getGnoTransactionStore(ctx) match := reNamespace.FindStringSubmatch(pkgPath) switch len(match) { @@ -263,7 +293,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { pkgPath := msg.Package.Path memPkg := msg.Package deposit := msg.Deposit - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) // Validate arguments. if creator.IsZero() { @@ -360,7 +390,7 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { pkgPath := msg.PkgPath // to import fnc := msg.Func - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) // Get the package and function type. pv := gnostore.GetPackage(pkgPath, false) pl := gno.PackageNodeLocation(pkgPath) @@ -471,7 +501,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { caller := msg.Caller pkgAddr := caller - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) send := msg.Send memPkg := msg.Package @@ -585,7 +615,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // QueryFuncs returns public facing function signatures. func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) { - store := vm.getGnoStore(ctx) + store := vm.getGnoTransactionStore(ctx) // Ensure pkgPath is realm. if !gno.IsRealmPath(pkgPath) { err = ErrInvalidPkgPath(fmt.Sprintf( @@ -648,7 +678,7 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS // TODO: then, rename to "Eval". func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -715,7 +745,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // TODO: then, rename to "EvalString". func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoStore(ctx) + gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -776,7 +806,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string } func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) { - store := vm.getGnoStore(ctx) + store := vm.getGnoTransactionStore(ctx) dirpath, filename := std.SplitFilepath(filepath) if filename != "" { memFile := store.GetMemFile(dirpath, filename) diff --git a/tm2/pkg/sdk/abci.go b/tm2/pkg/sdk/abci.go index a9cd14e9ed3..4857541ea41 100644 --- a/tm2/pkg/sdk/abci.go +++ b/tm2/pkg/sdk/abci.go @@ -16,3 +16,11 @@ type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeg // Note: applications which set create_empty_blocks=false will not have regular block timing and should use // e.g. BFT timestamps rather than block height for any periodic EndBlock logic type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock + +// BeginTxHook is a BaseApp-specific hook, called to modify the context with any +// additional application-specific information. +type BeginTxHook func(ctx Context) Context + +// EndTxHook is a BaseApp-specific hook, called after all the messages in a +// transaction have terminated. +type EndTxHook func(ctx Context, result Result) diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 0fa26b817e1..36f3e5fd5c9 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -42,6 +42,9 @@ type BaseApp struct { beginBlocker BeginBlocker // logic to run before any txs endBlocker EndBlocker // logic to run after all txs, and to determine valset changes + beginTxHook BeginTxHook // BaseApp-specific hook run before running transaction messages. + endTxHook EndTxHook // BaseApp-specific hook run after a transaction + // -------------------- // Volatile state // checkState is set on initialization and reset on Commit. @@ -828,6 +831,10 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) return result } + if app.endTxHook != nil { + app.endTxHook(ctx, result) + } + // only update state if all messages pass if result.IsOK() { msCache.MultiWrite() diff --git a/tm2/pkg/sdk/options.go b/tm2/pkg/sdk/options.go index f174b5501a2..2bcaa4475d8 100644 --- a/tm2/pkg/sdk/options.go +++ b/tm2/pkg/sdk/options.go @@ -85,3 +85,17 @@ func (app *BaseApp) SetAnteHandler(ah AnteHandler) { } app.anteHandler = ah } + +func (app *BaseApp) SetBeginTxHook(endTx BeginTxHook) { + if app.sealed { + panic("SetBeginTxHook() on sealed BaseApp") + } + app.beginTxHook = endTx +} + +func (app *BaseApp) SetEndTxHook(endTx EndTxHook) { + if app.sealed { + panic("SetEndTxHook() on sealed BaseApp") + } + app.endTxHook = endTx +} From 48593e7a3ba44a539edb7663ea0ce81b566bd039 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 22:56:48 +0200 Subject: [PATCH 08/45] fixup --- gno.land/pkg/gnoland/app.go | 9 +++++---- gno.land/pkg/sdk/vm/common_test.go | 12 ++++++------ gno.land/pkg/sdk/vm/keeper.go | 11 +++++++---- gno.land/pkg/sdk/vm/keeper_test.go | 6 +++--- tm2/pkg/sdk/baseapp.go | 7 ++++++- tm2/pkg/sdk/options.go | 4 ++-- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 4b3e0ad4cdd..115b947de27 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -68,7 +68,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { } // Capabilities keys. - mainKey := store.NewStoreKey("iavl") + mainKey := store.NewStoreKey("main") baseKey := store.NewStoreKey("base") // Create BaseApp. @@ -102,9 +102,6 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { func(ctx sdk.Context, tx std.Tx, simulate bool) ( newCtx sdk.Context, res sdk.Result, abort bool, ) { - // Create Gno transaction store. - ctx = vmk.MakeGnoTransactionStore(ctx) - // Override auth params. ctx = ctx. WithValue(auth.AuthParamsContextKey{}, auth.DefaultParams()) @@ -113,6 +110,10 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { return }, ) + baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { + // Create Gno transaction store. + return vmk.MakeGnoTransactionStore(ctx) + }) baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) { if result.IsOK() { vmk.CommitGnoTransactionStore(ctx) diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 00b4d04e163..2af3fc1dac6 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -5,7 +5,6 @@ package vm import ( "path/filepath" - "github.com/gnolang/gno/gno.land/pkg/sdk/gnostore" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/log" @@ -15,6 +14,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" ) type testEnv struct { @@ -35,19 +35,19 @@ func setupTestEnvCold() testEnv { func _setupTestEnv(cacheStdlibs bool) testEnv { db := memdb.NewMemDB() + iavlCapKey := store.NewStoreKey("iavlCapKey") baseCapKey := store.NewStoreKey("baseCapKey") - gnoCapKey := store.NewStoreKey("gnoCapKey") - // Mount db store and gnostore + // Mount db store and iavlstore ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) - ms.MountStoreWithDB(gnoCapKey, gnostore.StoreConstructor, db) ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) - acck := authm.NewAccountKeeper(gnoCapKey, std.ProtoBaseAccount) + acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) bank := bankm.NewBankKeeper(acck) - vmk := NewVMKeeper(gnoCapKey, acck, bank, 100_000_000) + vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, 100_000_000) mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 987fe45100b..fa73e0c75fe 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -14,7 +14,6 @@ import ( "sync" "time" - "github.com/gnolang/gno/gnovm/pkg/gnolang" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -96,8 +95,8 @@ func (vm *VMKeeper) Initialize( baseSDKStore := ms.GetStore(vm.baseKey) iavlSDKStore := ms.GetStore(vm.iavlKey) - alloc := gnolang.NewAllocator(maxAllocTx) - vm.gnoStore = gnolang.NewStore(alloc, baseSDKStore, iavlSDKStore) + alloc := gno.NewAllocator(maxAllocTx) + vm.gnoStore = gno.NewStore(alloc, baseSDKStore, iavlSDKStore) vm.gnoStore.SetNativeStore(stdlibs.NativeStore) if vm.gnoStore.NumMemPackages() > 0 { @@ -141,7 +140,7 @@ func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { loadStdlib(cachedGnoStore, stdlibDir) }) - gs := vm.gnoStore + gs := vm.getGnoTransactionStore(ctx) gno.CopyFromCachedStore(gs, cachedGnoStore, cachedStdlibBase, cachedStdlibIavl) } @@ -615,6 +614,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // QueryFuncs returns public facing function signatures. func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) store := vm.getGnoTransactionStore(ctx) // Ensure pkgPath is realm. if !gno.IsRealmPath(pkgPath) { @@ -677,6 +677,7 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "Eval". func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) @@ -744,6 +745,7 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "EvalString". func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) gnostore := vm.getGnoTransactionStore(ctx) pkgAddr := gno.DerivePkgAddr(pkgPath) @@ -806,6 +808,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string } func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) { + ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) store := vm.getGnoTransactionStore(ctx) dirpath, filename := std.SplitFilepath(filepath) if filename != "" { diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index fda9b9c93f4..30e7a5018fe 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -36,12 +36,12 @@ func Echo() string {return "hello world"}`, } pkgPath := "gno.land/r/test" msg1 := NewMsgAddPackage(addr, pkgPath, files) - assert.Nil(t, env.vmk.getGnoStore(ctx).GetPackage(pkgPath, false)) + assert.Nil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) err := env.vmk.AddPackage(ctx, msg1) assert.NoError(t, err) - assert.NotNil(t, env.vmk.getGnoStore(ctx).GetPackage(pkgPath, false)) + assert.NotNil(t, env.vmk.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) err = env.vmk.AddPackage(ctx, msg1) @@ -49,7 +49,7 @@ func Echo() string {return "hello world"}`, assert.True(t, errors.Is(err, InvalidPkgPathError{})) // added package is formatted - store := vmk.getGnoStore(ctx) + store := vmk.getGnoTransactionStore(ctx) memFile := store.GetMemFile("gno.land/r/test", "test.gno") assert.NotNil(t, memFile) expected := `package test diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 36f3e5fd5c9..386a5fb206f 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -823,6 +823,11 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) // Create a new context based off of the existing context with a cache wrapped // multi-store in case message processing fails. runMsgCtx, msCache := app.cacheTxContext(ctx) + + if app.beginTxHook != nil { + runMsgCtx = app.beginTxHook(runMsgCtx) + } + result = app.runMsgs(runMsgCtx, msgs, mode) result.GasWanted = gasWanted @@ -832,7 +837,7 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) } if app.endTxHook != nil { - app.endTxHook(ctx, result) + app.endTxHook(runMsgCtx, result) } // only update state if all messages pass diff --git a/tm2/pkg/sdk/options.go b/tm2/pkg/sdk/options.go index 2bcaa4475d8..b9840a7510b 100644 --- a/tm2/pkg/sdk/options.go +++ b/tm2/pkg/sdk/options.go @@ -86,11 +86,11 @@ func (app *BaseApp) SetAnteHandler(ah AnteHandler) { app.anteHandler = ah } -func (app *BaseApp) SetBeginTxHook(endTx BeginTxHook) { +func (app *BaseApp) SetBeginTxHook(beginTx BeginTxHook) { if app.sealed { panic("SetBeginTxHook() on sealed BaseApp") } - app.beginTxHook = endTx + app.beginTxHook = beginTx } func (app *BaseApp) SetEndTxHook(endTx EndTxHook) { From 74df3ea2778e434ffc3383b0fce0def667787124 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 26 Jul 2024 23:05:47 +0200 Subject: [PATCH 09/45] fix gnodev tests --- contribs/gnodev/pkg/dev/node.go | 1 + 1 file changed, 1 insertion(+) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index bf3f32fab3b..35484caf0d4 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -469,6 +469,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Setup node config nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler + nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock // recoverFromError handles panics and converts them to errors. From dfab57a6a7d077201f5dc6bc6d721e211b6a7a69 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 2 Aug 2024 19:17:07 +0200 Subject: [PATCH 10/45] fix vm tests --- gno.land/pkg/sdk/vm/common_test.go | 7 ++++++- gno.land/pkg/sdk/vm/gas_test.go | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 2af3fc1dac6..26c1fcc5962 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -51,8 +51,13 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) + ctx = vmk.MakeGnoTransactionStore(ctx) stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") - vmk.LoadStdlibCached(ctx, stdlibsDir) + if cacheStdlibs { + vmk.LoadStdlibCached(ctx, stdlibsDir) + } else { + vmk.LoadStdlib(ctx, stdlibsDir) + } mcw.MultiWrite() return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck} diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 66655994bd4..7f5ace9c1fd 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -42,7 +42,7 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) { assert.True(t, abort) assert.False(t, res.IsOK()) gasCheck := gctx.GasMeter().GasConsumed() - assert.Equal(t, int64(3231), gasCheck) + assert.Equal(t, int64(3462), gasCheck) } else { t.Errorf("should panic") } @@ -69,7 +69,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(92825), gasDeliver) + assert.Equal(t, int64(16989), gasDeliver) } // Enough gas for a failed transaction. @@ -88,7 +88,7 @@ func TestAddPkgDeliverTxFailed(t *testing.T) { gasDeliver := gctx.GasMeter().GasConsumed() assert.False(t, res.IsOK()) - assert.Equal(t, int64(2231), gasDeliver) + assert.Equal(t, int64(1231), gasDeliver) } // Not enough gas for a failed transaction. @@ -100,7 +100,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { ctx = ctx.WithMode(sdk.RunTxModeDeliver) simulate = false - tx.Fee.GasWanted = 2230 + tx.Fee.GasWanted = 1230 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) var res sdk.Result @@ -118,7 +118,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { assert.True(t, abort) assert.False(t, res.IsOK()) gasCheck := gctx.GasMeter().GasConsumed() - assert.Equal(t, int64(2231), gasCheck) + assert.Equal(t, int64(1231), gasCheck) } else { t.Errorf("should panic") } From 4f919560ce8e3b860f29131c8868c78a327983b7 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 2 Aug 2024 21:52:36 +0200 Subject: [PATCH 11/45] add back clearobjectcache --- gno.land/pkg/sdk/vm/keeper.go | 4 +++- gnovm/pkg/gnolang/machine.go | 3 ++- gnovm/pkg/gnolang/store.go | 13 ++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 4a3d425f671..a2f1d3e15d2 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -201,7 +201,9 @@ func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { } func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { - return ctx.Value(gnoStoreContextKey).(gno.TransactionStore) + txStore := ctx.Value(gnoStoreContextKey).(gno.TransactionStore) + txStore.ClearObjectCache() + return txStore } // Namespace can be either a user or crypto address. diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 24f94abc10b..718ee803fe1 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -2214,7 +2214,8 @@ func (m *Machine) String() string { builder.WriteString(" Blocks:\n") - for b := m.LastBlock(); b != nil; { + for i := len(m.Blocks) - 1; i > 0; i-- { + b := m.Blocks[i] gen := builder.Len()/3 + 1 gens := "@" // strings.Repeat("@", gen) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 06def83c39c..3ae38e004d1 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -65,6 +65,7 @@ type Store interface { GetMemPackage(path string) *std.MemPackage GetMemFile(path string, name string) *std.MemFile IterMemPackage() <-chan *std.MemPackage + ClearObjectCache() SetPackageInjector(PackageInjector) // for natives SetNativeStore(NativeStore) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX @@ -207,6 +208,16 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore return ds } +// Unstable. +// This function is used to clear the object cache every transaction. +// It also sets a new allocator. +func (ds *defaultStore) ClearObjectCache() { + ds.alloc.Reset() + ds.cacheObjects = make(map[ObjectID]Object) // new cache. + ds.opslog = nil // new ops log. + ds.SetCachePackage(Uverse()) +} + // If nil baseStore and iavlStore, the baseStores are re-used. func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) TransactionStore { if baseStore == nil { @@ -274,7 +285,7 @@ type transactionStore struct{ *defaultStore } func (t transactionStore) Write() { t.write() } -// writes to parentStore. +// writes to parentStore, prepares the transactional ds for a new execution. func (ds *defaultStore) write() { ds.cacheTypes.write() ds.cacheNodes.write() From a45d9c5e13c32918ec9f76c78ca8da687bf7d5db Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 6 Aug 2024 01:25:48 +0200 Subject: [PATCH 12/45] fixup gnoland restart --- gno.land/cmd/gnoland/testdata/restart_missing_type.txtar | 2 +- gno.land/pkg/gnoland/node_inmemory.go | 9 ++++----- gno.land/pkg/integration/testing_node.go | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar index f0c551e62a3..3336a988b46 100644 --- a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar +++ b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar @@ -83,7 +83,7 @@ gnoland restart } ], "fee": { - "gas_wanted": "10000000", + "gas_wanted": "1000000", "gas_fee": "1000000ugnot" }, "signatures": [], diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index b09d4e3cbc2..edc9a24118a 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -95,16 +95,15 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, cfg.StdlibDir = filepath.Join(cfg.TMConfig.RootDir, "gnovm", "stdlibs") } // initialize db if nil - mdb := cfg.DB - if mdb == nil { - mdb = memdb.NewMemDB() + if cfg.DB == nil { + cfg.DB = memdb.NewMemDB() } // Initialize the application with the provided options gnoApp, err := NewAppWithOptions(&AppOptions{ Logger: logger, MaxCycles: cfg.GenesisMaxVMCycles, - DB: mdb, + DB: cfg.DB, EventSwitch: evsw, InitChainerConfig: cfg.InitChainerConfig, }) @@ -125,7 +124,7 @@ func NewInMemoryNode(logger *slog.Logger, cfg *InMemoryNodeConfig) (*node.Node, // Create genesis factory genProvider := func() (*bft.GenesisDoc, error) { return cfg.Genesis, nil } - dbProvider := func(*node.DBContext) (db.DB, error) { return memdb.NewMemDB(), nil } + dbProvider := func(*node.DBContext) (db.DB, error) { return cfg.DB, nil } // Generate p2p node identity nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 4b90ddac7c4..bf25201f665 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -11,6 +11,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/node" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/std" "github.com/stretchr/testify/require" ) @@ -74,10 +75,10 @@ func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNode PrivValidator: pv, Genesis: genesis, TMConfig: tmconfig, + DB: memdb.NewMemDB(), InitChainerConfig: gnoland.InitChainerConfig{ GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler, CacheStdlibLoad: true, - // StdlibDir automatically set }, } } From 818558f0c3161cfd00614e10a10f111d97e5fec0 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 11:23:23 +0200 Subject: [PATCH 13/45] some reorg, add comments --- contribs/gnodev/pkg/dev/node.go | 1 + gno.land/pkg/gnoland/app.go | 57 ++++++++++++++++-------------- gno.land/pkg/sdk/vm/common_test.go | 4 +-- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 35484caf0d4..024b6bffebe 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -469,6 +469,7 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Setup node config nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, genesis) nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler + // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 115b947de27..6f2593bb36d 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -1,3 +1,4 @@ +// Package gnoland contains the bootstrapping code to launch a gno.land node. package gnoland import ( @@ -26,42 +27,35 @@ import ( // Only goleveldb is supported for now. _ "github.com/gnolang/gno/tm2/pkg/db/_tags" _ "github.com/gnolang/gno/tm2/pkg/db/goleveldb" - "github.com/gnolang/gno/tm2/pkg/db/memdb" ) +// AppOptions contains the options to create the gno.land ABCI application. type AppOptions struct { - DB dbm.DB - Logger *slog.Logger - EventSwitch events.EventSwitch - MaxCycles int64 - InitChainerConfig + DB dbm.DB // required + Logger *slog.Logger // defaults to log.NewNoopLogger() + EventSwitch events.EventSwitch // defaults to events.NilEventSwitch() + MaxCycles int64 // defaults to 0 (unlimited) + InitChainerConfig // options related to InitChainer } -func NewAppOptions() *AppOptions { - return &AppOptions{ - Logger: log.NewNoopLogger(), - DB: memdb.NewMemDB(), - EventSwitch: events.NilEventSwitch(), - InitChainerConfig: InitChainerConfig{ - GenesisTxResultHandler: PanicOnFailingTxResultHandler, - StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), - }, +func (c *AppOptions) validate() error { + // Required fields + if c.DB == nil { + return fmt.Errorf("no db provided") } -} -func (c *AppOptions) validate() error { + // Set defaults if c.Logger == nil { - return fmt.Errorf("no logger provided") + c.Logger = log.NewNoopLogger() } - - if c.DB == nil { - return fmt.Errorf("no db provided") + if c.EventSwitch == nil { + c.EventSwitch = events.NilEventSwitch() } return nil } -// NewAppWithOptions creates the GnoLand application with specified options +// NewAppWithOptions creates the gno.land application with specified options. func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { if err := cfg.validate(); err != nil { return nil, err @@ -110,6 +104,11 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { return }, ) + + // Set begin and end transaction hooks. + // These are used to create gno transaction stores and commit them when finishing + // the tx - in other words, data from a failing transaction won't be persisted + // to the gno store caches. baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { // Create Gno transaction store. return vmk.MakeGnoTransactionStore(ctx) @@ -162,7 +161,14 @@ func NewApp( ) (abci.Application, error) { var err error - cfg := NewAppOptions() + cfg := &AppOptions{ + Logger: logger, + EventSwitch: evsw, + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + }, + } if skipFailingGenesisTxs { cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } @@ -173,9 +179,6 @@ func NewApp( return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, dataRootDir, err) } - cfg.Logger = logger - cfg.EventSwitch = evsw - return NewAppWithOptions(cfg) } @@ -195,6 +198,8 @@ func PanicOnFailingTxResultHandler(_ sdk.Context, _ std.Tx, res sdk.Result) { } // InitChainerConfig keeps the configuration for the InitChainer. +// [NewAppWithOptions] will set [InitChainerConfig.InitChainer] as its InitChainer +// function. type InitChainerConfig struct { // Handles the results of each genesis transaction. GenesisTxResultHandler diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 26c1fcc5962..cb184718c52 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -35,13 +35,13 @@ func setupTestEnvCold() testEnv { func _setupTestEnv(cacheStdlibs bool) testEnv { db := memdb.NewMemDB() - iavlCapKey := store.NewStoreKey("iavlCapKey") baseCapKey := store.NewStoreKey("baseCapKey") + iavlCapKey := store.NewStoreKey("iavlCapKey") // Mount db store and iavlstore ms := store.NewCommitMultiStore(db) - ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) + ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) ms.LoadLatestVersion() ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) From e360017de6ce5b2915cef46eecb6af97872fdc03 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 15:43:21 +0200 Subject: [PATCH 14/45] refactors; add tests for pkg/gnoland --- gno.land/pkg/gnoland/app.go | 6 +- gno.land/pkg/gnoland/app_test.go | 92 +++++++++++++++++++++++++++++-- gno.land/pkg/gnoland/mock_test.go | 61 ++++++++++++++++---- gno.land/pkg/sdk/vm/keeper.go | 33 ++++++++--- 4 files changed, 162 insertions(+), 30 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 6f2593bb36d..bedc5425060 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -33,7 +33,7 @@ import ( type AppOptions struct { DB dbm.DB // required Logger *slog.Logger // defaults to log.NewNoopLogger() - EventSwitch events.EventSwitch // defaults to events.NilEventSwitch() + EventSwitch events.EventSwitch // defaults to events.NewEventSwitch() MaxCycles int64 // defaults to 0 (unlimited) InitChainerConfig // options related to InitChainer } @@ -49,7 +49,7 @@ func (c *AppOptions) validate() error { c.Logger = log.NewNoopLogger() } if c.EventSwitch == nil { - c.EventSwitch = events.NilEventSwitch() + c.EventSwitch = events.NewEventSwitch() } return nil @@ -152,7 +152,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { return baseApp, nil } -// NewApp creates the GnoLand application. +// NewApp creates the gno.land application. func NewApp( dataRootDir string, skipFailingGenesisTxs bool, diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 852d090f3af..9d98e86631e 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -1,6 +1,7 @@ package gnoland import ( + "context" "errors" "fmt" "strings" @@ -8,13 +9,92 @@ import ( "github.com/gnolang/gno/gnovm/stdlibs/std" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/store" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/iavl" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// Tests that NewAppWithOptions works even when only providing a simple DB. +func TestNewAppWithOptions(t *testing.T) { + app, err := NewAppWithOptions(&AppOptions{DB: memdb.NewMemDB()}) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + assert.Equal(t, "dev", bapp.AppVersion()) + assert.Equal(t, "gnoland", bapp.Name()) +} + +func TestNewAppWithOptions_ErrNoDB(t *testing.T) { + _, err := NewAppWithOptions(&AppOptions{}) + assert.ErrorContains(t, err, "no db provided") +} + +// Test whether InitChainer calls to load the stdlibs correctly. +func TestInitChainer_LoadStdlib(t *testing.T) { + t.Run("cached", func(t *testing.T) { testInitChainerLoadStdlib(t, true) }) + t.Run("uncached", func(t *testing.T) { testInitChainerLoadStdlib(t, false) }) +} + +func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper + type gsContextType string + const ( + stdlibDir = "test-stdlib-dir" + gnoStoreKey gsContextType = "gno-store-key" + gnoStoreValue gsContextType = "gno-store-value" + ) + db := memdb.NewMemDB() + ms := store.NewCommitMultiStore(db) + baseCapKey := store.NewStoreKey("baseCapKey") + iavlCapKey := store.NewStoreKey("iavlCapKey") + + ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) + ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) + ms.LoadLatestVersion() + testCtx := sdk.NewContext(sdk.RunTxModeDeliver, ms.MultiCacheWrap(), &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + + containsGnoStore := func(ctx sdk.Context) bool { + return ctx.Context().Value(gnoStoreKey) == gnoStoreValue + } + loadStdlib := func(ctx sdk.Context, dir string) { + assert.Equal(t, stdlibDir, dir, "stdlibDir should match provided dir") + assert.True(t, containsGnoStore(ctx), "should contain gno store") + } + mock := &mockVMKeeper{ + makeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context { + assert.False(t, containsGnoStore(ctx), "should not already contain gno store") + return ctx.WithContext(context.WithValue(ctx.Context(), gnoStoreKey, gnoStoreValue)) + }, + commitGnoTransactionStoreFn: func(ctx sdk.Context) { + assert.True(t, containsGnoStore(ctx), "should contain gno store") + }, + loadStdlibFn: loadStdlib, + loadStdlibCachedFn: loadStdlib, + calls: make(map[string]int), + } + cfg := InitChainerConfig{ + StdlibDir: stdlibDir, + vmKpr: mock, + CacheStdlibLoad: cached, + } + cfg.InitChainer(testCtx, abci.RequestInitChain{}) + exp := map[string]int{ + "MakeGnoTransactionStore": 1, + "CommitGnoTransactionStore": 1, + } + if cached { + exp["LoadStdlibCached"] = 1 + } else { + exp["LoadStdlib"] = 1 + } + assert.Equal(t, mock.calls, exp) +} + // generateValidatorUpdates generates dummy validator updates func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate { t.Helper() @@ -81,7 +161,7 @@ func TestEndBlocker(t *testing.T) { t.Run("no collector events", func(t *testing.T) { t.Parallel() - noFilter := func(e events.Event) []validatorUpdate { + noFilter := func(_ events.Event) []validatorUpdate { return []validatorUpdate{} } @@ -102,7 +182,7 @@ func TestEndBlocker(t *testing.T) { t.Parallel() var ( - noFilter = func(e events.Event) []validatorUpdate { + noFilter = func(_ events.Event) []validatorUpdate { return make([]validatorUpdate, 1) // 1 update } @@ -145,7 +225,7 @@ func TestEndBlocker(t *testing.T) { t.Parallel() var ( - noFilter = func(e events.Event) []validatorUpdate { + noFilter = func(_ events.Event) []validatorUpdate { return make([]validatorUpdate, 1) // 1 update } @@ -227,8 +307,8 @@ func TestEndBlocker(t *testing.T) { } // Fire the tx result event - txEvent := types.EventTx{ - Result: types.TxResult{ + txEvent := bft.EventTx{ + Result: bft.TxResult{ Response: abci.ResponseDeliverTx{ ResponseBase: abci.ResponseBase{ Events: vmEvents, diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 53aed44a21f..f22c453f018 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -45,22 +45,27 @@ func (m *mockEventSwitch) RemoveListener(listenerID string) { } } -type ( - addPackageDelegate func(sdk.Context, vm.MsgAddPackage) error - callDelegate func(sdk.Context, vm.MsgCall) (string, error) - queryEvalDelegate func(sdk.Context, string, string) (string, error) - runDelegate func(sdk.Context, vm.MsgRun) (string, error) -) - type mockVMKeeper struct { - vm.VMKeeperI - addPackageFn addPackageDelegate - callFn callDelegate - queryFn queryEvalDelegate - runFn runDelegate + addPackageFn func(sdk.Context, vm.MsgAddPackage) error + callFn func(sdk.Context, vm.MsgCall) (string, error) + queryFn func(sdk.Context, string, string) (string, error) + runFn func(sdk.Context, vm.MsgRun) (string, error) + loadStdlibFn func(sdk.Context, string) + loadStdlibCachedFn func(sdk.Context, string) + makeGnoTransactionStoreFn func(ctx sdk.Context) sdk.Context + commitGnoTransactionStoreFn func(ctx sdk.Context) + + calls map[string]int +} + +func (m *mockVMKeeper) call(name string) { + if m.calls != nil { + m.calls[name]++ + } } func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { + m.call("AddPackage") if m.addPackageFn != nil { return m.addPackageFn(ctx, msg) } @@ -69,6 +74,7 @@ func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { } func (m *mockVMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err error) { + m.call("Call") if m.callFn != nil { return m.callFn(ctx, msg) } @@ -77,6 +83,7 @@ func (m *mockVMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err er } func (m *mockVMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res string, err error) { + m.call("QueryEval") if m.queryFn != nil { return m.queryFn(ctx, pkgPath, expr) } @@ -85,6 +92,7 @@ func (m *mockVMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res str } func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err error) { + m.call("Run") if m.runFn != nil { return m.runFn(ctx, msg) } @@ -92,6 +100,35 @@ func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err erro return "", nil } +func (m *mockVMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { + m.call("LoadStdlib") + if m.loadStdlibFn != nil { + m.loadStdlibFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { + m.call("LoadStdlibCached") + if m.loadStdlibCachedFn != nil { + m.loadStdlibCachedFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + m.call("MakeGnoTransactionStore") + if m.makeGnoTransactionStoreFn != nil { + return m.makeGnoTransactionStoreFn(ctx) + } + return ctx +} + +func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + m.call("CommitGnoTransactionStore") + if m.commitGnoTransactionStoreFn != nil { + m.commitGnoTransactionStoreFn(ctx) + } +} + type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 5d29bef459d..1d19c60c906 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -121,26 +121,41 @@ func (vm *VMKeeper) Initialize( } } +type stdlibCache struct { + dir string + base store.Store + iavl store.Store + gno gno.Store +} + var ( cachedStdlibOnce sync.Once - cachedStdlibBase store.Store - cachedStdlibIavl store.Store - cachedGnoStore gno.Store + cachedStdlib stdlibCache ) // LoadStdlib loads the Gno standard library into the given store. func (vm *VMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { cachedStdlibOnce.Do(func() { - cachedStdlibBase = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) - cachedStdlibIavl = dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}) + cachedStdlib = stdlibCache{ + dir: stdlibDir, + base: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), + iavl: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), + } - cachedGnoStore = gno.NewStore(nil, cachedStdlibBase, cachedStdlibIavl) - cachedGnoStore.SetNativeStore(stdlibs.NativeStore) - loadStdlib(cachedGnoStore, stdlibDir) + gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl) + gs.SetNativeStore(stdlibs.NativeStore) + loadStdlib(gs, stdlibDir) + cachedStdlib.gno = gs }) + if stdlibDir != cachedStdlib.dir { + panic(fmt.Sprintf( + "cannot load cached stdlib: cached stdlib is in dir %q; wanted to load stdlib in dir %q", + cachedStdlib.dir, stdlibDir)) + } + gs := vm.getGnoTransactionStore(ctx) - gno.CopyFromCachedStore(gs, cachedGnoStore, cachedStdlibBase, cachedStdlibIavl) + gno.CopyFromCachedStore(gs, cachedStdlib.gno, cachedStdlib.base, cachedStdlib.iavl) } // LoadStdlib loads the Gno standard library into the given store. From 63641a18143d178decd37c0d78e073806ddb574d Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 15:55:46 +0200 Subject: [PATCH 15/45] add test for `gnoland start`, and docs --- gno.land/cmd/gnoland/testdata/restart.txtar | 24 +++++++++++++++++++ gno.land/pkg/integration/doc.go | 4 +++- .../pkg/integration/testing_integration.go | 3 +-- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/restart.txtar diff --git a/gno.land/cmd/gnoland/testdata/restart.txtar b/gno.land/cmd/gnoland/testdata/restart.txtar new file mode 100644 index 00000000000..8d50dd15814 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/restart.txtar @@ -0,0 +1,24 @@ +# simple test for the `gnoland restart` command; +# should restart the gno.land node and recover state. + +loadpkg gno.land/r/demo/counter $WORK +gnoland start + +gnokey maketx call -pkgpath gno.land/r/demo/counter -func Incr -gas-fee 1000000ugnot -gas-wanted 100000 -broadcast -chainid tendermint_test test1 +stdout '\(1 int\)' + +gnoland restart + +gnokey maketx call -pkgpath gno.land/r/demo/counter -func Incr -gas-fee 1000000ugnot -gas-wanted 100000 -broadcast -chainid tendermint_test test1 +stdout '\(2 int\)' + +-- counter.gno -- +package counter + +var counter int + +func Incr() int { + counter++ + return counter +} + diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go index 2b6d24c23b8..9be4be0af07 100644 --- a/gno.land/pkg/integration/doc.go +++ b/gno.land/pkg/integration/doc.go @@ -8,9 +8,11 @@ // // Additional Command Overview: // -// 1. `gnoland [start|stop]`: +// 1. `gnoland [start|stop|restart]`: // - The gnoland node doesn't start automatically. This enables the user to do some // pre-configuration or pass custom arguments to the start command. +// - `gnoland restart` will simulate restarting a node, as in stopping and +// starting it again, recovering state from the persisted database data. // // 2. `gnokey`: // - Supports most of the common commands. diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 12d37728a04..7813f748b9f 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -153,7 +153,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ "gnoland": func(ts *testscript.TestScript, neg bool, args []string) { if len(args) == 0 { - tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop]")) + tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop|restart]")) return } @@ -202,7 +202,6 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { fmt.Fprintln(ts.Stdout(), "node started successfully") case "restart": - // XXX: unstable, should try to use it in a working scenario n, ok := nodes[sid] if !ok { err = fmt.Errorf("node must be started before being restarted") From 738ff0b1804a25f0600c7ba415e241a5cc6a3ea5 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 15:58:46 +0200 Subject: [PATCH 16/45] move restart.txtar to pkg/integration tests --- gno.land/{cmd/gnoland => pkg/integration}/testdata/restart.txtar | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gno.land/{cmd/gnoland => pkg/integration}/testdata/restart.txtar (100%) diff --git a/gno.land/cmd/gnoland/testdata/restart.txtar b/gno.land/pkg/integration/testdata/restart.txtar similarity index 100% rename from gno.land/cmd/gnoland/testdata/restart.txtar rename to gno.land/pkg/integration/testdata/restart.txtar From 7baf0ded2731868f162f93efe19924af76c170a7 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 17:10:38 +0200 Subject: [PATCH 17/45] unit tests for option setters --- tm2/pkg/sdk/baseapp_test.go | 48 +++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 1680b99a5c6..f54801b8277 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -19,9 +19,9 @@ import ( "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/sdk/testutils" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" "github.com/gnolang/gno/tm2/pkg/store/iavl" - store "github.com/gnolang/gno/tm2/pkg/store/types" ) var ( @@ -199,6 +199,45 @@ func TestLoadVersionInvalid(t *testing.T) { require.Error(t, err) } +func TestOptionSetters(t *testing.T) { + tt := []struct { + // Calling BaseApp.[method]([value]) should change BaseApp.[fieldName] to [value]. + method string + fieldName string + value any + }{ + {"SetName", "name", "hello"}, + {"SetAppVersion", "appVersion", "12345"}, + {"SetDB", "db", memdb.NewMemDB()}, + {"SetCMS", "cms", store.NewCommitMultiStore(memdb.NewMemDB())}, + {"SetInitChainer", "initChainer", func(Context, abci.RequestInitChain) abci.ResponseInitChain { panic("not implemented") }}, + {"SetBeginBlocker", "beginBlocker", func(Context, abci.RequestBeginBlock) abci.ResponseBeginBlock { panic("not implemented") }}, + {"SetEndBlocker", "endBlocker", func(Context, abci.RequestEndBlock) abci.ResponseEndBlock { panic("not implemented") }}, + {"SetAnteHandler", "anteHandler", func(Context, Tx, bool) (Context, Result, bool) { panic("not implemented") }}, + {"SetBeginTxHook", "beginTxHook", func(Context) Context { panic("not implemented") }}, + {"SetEndTxHook", "endTxHook", func(Context, Result) { panic("not implemented") }}, + } + + for _, tc := range tt { + t.Run(tc.method, func(t *testing.T) { + t.Helper() + + var ba BaseApp + rv := reflect.ValueOf(&ba) + + rv.MethodByName(tc.method).Call([]reflect.Value{reflect.ValueOf(tc.value)}) + changed := rv.Elem().FieldByName(tc.fieldName) + + if reflect.TypeOf(tc.value).Kind() == reflect.Func { + assert.Equal(t, reflect.ValueOf(tc.value).Pointer(), changed.Pointer(), "%s(%#v): function value should have changed", tc.method, tc.value) + } else { + assert.True(t, reflect.ValueOf(tc.value).Equal(changed), "%s(%#v): wanted %v got %v", tc.method, tc.value, tc.value, changed) + } + assert.False(t, changed.IsZero(), "%s(%#v): field's new value should not be zero value", tc.method, tc.value) + }) + } +} + func testLoadVersionHelper(t *testing.T, app *BaseApp, expectedHeight int64, expectedID store.CommitID) { t.Helper() @@ -272,6 +311,12 @@ func TestBaseAppOptionSeal(t *testing.T) { require.Panics(t, func() { app.SetAnteHandler(nil) }) + require.Panics(t, func() { + app.SetBeginTxHook(nil) + }) + require.Panics(t, func() { + app.SetEndTxHook(nil) + }) } func TestSetMinGasPrices(t *testing.T) { @@ -927,7 +972,6 @@ func TestMaxBlockGasLimits(t *testing.T) { } for i, tc := range testCases { - fmt.Printf("debug i: %v\n", i) tx := tc.tx // reset the block gas From a38ff80c2e4b608e0e9d05f3262bf4a1015bbd61 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 19:47:11 +0200 Subject: [PATCH 18/45] fix ci --- gno.land/pkg/integration/testdata/gnoland.txtar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/pkg/integration/testdata/gnoland.txtar b/gno.land/pkg/integration/testdata/gnoland.txtar index c675e7578b6..78bdc9cae4e 100644 --- a/gno.land/pkg/integration/testdata/gnoland.txtar +++ b/gno.land/pkg/integration/testdata/gnoland.txtar @@ -28,7 +28,7 @@ cmp stderr gnoland-already-stop.stderr.golden -- gnoland-no-arguments.stdout.golden -- -- gnoland-no-arguments.stderr.golden -- -"gnoland" error: syntax: gnoland [start|stop] +"gnoland" error: syntax: gnoland [start|stop|restart] -- gnoland-start.stdout.golden -- node started successfully -- gnoland-start.stderr.golden -- From 3e6bb61cce5d7ff9d42a7009e61b29903d513b81 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 20 Aug 2024 21:07:00 +0200 Subject: [PATCH 19/45] further gnoland app tests --- gno.land/pkg/gnoland/app_test.go | 85 +++++++++++++++++-- gno.land/pkg/sdk/vm/keeper_test.go | 62 ++++++++++++++ .../pkg/sdk/vm/testdata/emptystdlib/README | 1 + 3 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 gno.land/pkg/sdk/vm/testdata/emptystdlib/README diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 9d98e86631e..6dcdae08fb0 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -4,16 +4,23 @@ import ( "context" "errors" "fmt" + "path/filepath" "strings" "testing" + "time" - "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + gnostd "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" "github.com/gnolang/gno/tm2/pkg/store/iavl" @@ -23,11 +30,77 @@ import ( // Tests that NewAppWithOptions works even when only providing a simple DB. func TestNewAppWithOptions(t *testing.T) { - app, err := NewAppWithOptions(&AppOptions{DB: memdb.NewMemDB()}) + app, err := NewAppWithOptions(&AppOptions{ + DB: memdb.NewMemDB(), + InitChainerConfig: InitChainerConfig{ + CacheStdlibLoad: true, + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + }, + }) require.NoError(t, err) bapp := app.(*sdk.BaseApp) assert.Equal(t, "dev", bapp.AppVersion()) assert.Equal(t, "gnoland", bapp.Name()) + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(abci.RequestInitChain{ + Time: time.Now(), + ChainID: "dev", + ConsensusParams: &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 100_000_000, // 100M gas + TimeIotaMS: 100, // 100ms + }, + }, + Validators: []abci.ValidatorUpdate{}, + AppState: GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + }, + Txs: []std.Tx{ + { + Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.MemFile{ + { + Name: "demo.gno", + Body: "package demo; func Hello() string { return `hello`; }", + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + }, + }, + }) + + if !resp.IsOK() { + t.Fatal(resp) + } + + tx := amino.MustMarshal(std.Tx{ + Msgs: []std.Msg{vm.NewMsgCall(addr, nil, "gno.land/r/demo", "Hello", nil)}, + Fee: std.Fee{ + GasWanted: 100_000, + GasFee: std.Coin{ + Denom: "ugnot", + Amount: 1_000_000, + }, + }, + Signatures: []std.Signature{{}}, // one empty signature + Memo: "", + }) + dtxResp := bapp.DeliverTx(abci.RequestDeliverTx{ + RequestBase: abci.RequestBase{}, + Tx: tx, + }) + if !dtxResp.IsOK() { + t.Fatal(dtxResp) + } } func TestNewAppWithOptions_ErrNoDB(t *testing.T) { @@ -206,7 +279,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(std.GnoEvent{}) + mockEventSwitch.FireEvent(gnostd.GnoEvent{}) // Create the EndBlocker eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) @@ -249,7 +322,7 @@ func TestEndBlocker(t *testing.T) { c := newCollector[validatorUpdate](mockEventSwitch, noFilter) // Fire a GnoVM event - mockEventSwitch.FireEvent(std.GnoEvent{}) + mockEventSwitch.FireEvent(gnostd.GnoEvent{}) // Create the EndBlocker eb := EndBlocker(c, mockVMKeeper, &mockEndBlockerApp{}) @@ -288,7 +361,7 @@ func TestEndBlocker(t *testing.T) { // Construct the GnoVM events vmEvents := make([]abci.Event, 0, len(changes)) for index := range changes { - event := std.GnoEvent{ + event := gnostd.GnoEvent{ Type: validatorAddedEvent, PkgPath: valRealm, } @@ -297,7 +370,7 @@ func TestEndBlocker(t *testing.T) { if index%2 == 0 { changes[index].Power = 0 - event = std.GnoEvent{ + event = gnostd.GnoEvent{ Type: validatorRemovedEvent, PkgPath: valRealm, } diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index a0a653b26b8..afbc13fcf8b 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -9,10 +9,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/types" ) var coinsString = ugnot.ValueString(10000000) @@ -455,3 +461,59 @@ func Echo(msg string) string { }, ) } + +func TestVMKeeperReinitialize(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + env.bank.SetCoins(ctx, addr, std.MustParseCoins(coinsString)) + assert.True(t, env.bank.GetCoins(ctx, addr).IsEqual(std.MustParseCoins(coinsString))) + + // Create test package. + files := []*std.MemFile{ + {"init.gno", ` +package test + +func Echo(msg string) string { + return "echo:"+msg +}`}, + } + pkgPath := "gno.land/r/test" + msg1 := NewMsgAddPackage(addr, pkgPath, files) + err := env.vmk.AddPackage(ctx, msg1) + require.NoError(t, err) + + // Run Echo function. + msg2 := NewMsgCall(addr, nil, pkgPath, "Echo", []string{"hello world"}) + res, err := env.vmk.Call(ctx, msg2) + require.NoError(t, err) + assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) + + // Clear out gnovm and reinitialize. + env.vmk.gnoStore = nil + mcw := env.ctx.MultiStore().MultiCacheWrap() + env.vmk.Initialize(log.NewNoopLogger(), mcw) + mcw.MultiWrite() + + // Run echo again, and it should still work. + res, err = env.vmk.Call(ctx, msg2) + require.NoError(t, err) + assert.Equal(t, `("echo:hello world" string)`+"\n\n", res) +} + +func Test_loadStdlibPackage(t *testing.T) { + mdb := memdb.NewMemDB() + cs := dbadapter.StoreConstructor(mdb, types.StoreOptions{}) + + gs := gnolang.NewStore(nil, cs, cs) + assert.PanicsWithValue(t, `failed loading stdlib "notfound": does not exist`, func() { + loadStdlibPackage("notfound", "./testdata", gs) + }) + assert.PanicsWithValue(t, `failed loading stdlib "emptystdlib": not a valid MemPackage`, func() { + loadStdlibPackage("emptystdlib", "./testdata", gs) + }) +} diff --git a/gno.land/pkg/sdk/vm/testdata/emptystdlib/README b/gno.land/pkg/sdk/vm/testdata/emptystdlib/README new file mode 100644 index 00000000000..e4454ed67f8 --- /dev/null +++ b/gno.land/pkg/sdk/vm/testdata/emptystdlib/README @@ -0,0 +1 @@ +see keeper_test.go From 1347c5ff467e8a68c1cb8158d47538ff2677d8bf Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 21 Aug 2024 15:22:00 +0200 Subject: [PATCH 20/45] benchmark and test out "stackable" maps --- .../testdata/restart_missing_type.txtar | 4 + gnovm/pkg/gnolang/store.go | 18 +- gnovm/pkg/gnolang/store_test.go | 388 ++++++++++++++++++ 3 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 gnovm/pkg/gnolang/store_test.go diff --git a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar index 3336a988b46..7592693eeff 100644 --- a/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar +++ b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar @@ -1,3 +1,7 @@ +# This txtar is a regression test for a bug, whereby a type is committed to +# the defaultStore.cacheTypes map, but not to the underlying store (due to a +# failing transaction). +# For more information: https://github.com/gnolang/gno/pull/2605 loadpkg gno.land/p/demo/avl gnoland start diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 3ae38e004d1..0c2fba76071 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -2,7 +2,6 @@ package gnolang import ( "fmt" - "maps" "reflect" "slices" "strconv" @@ -114,6 +113,10 @@ type defaultStore struct { opslog []StoreOp // for debugging and testing. } +// bufferedTxMap is a wrapper around the map type, supporting regular Get, Set +// and Delete operations. Additionally, it can create a "buffered" version of +// itself, which will keep track of all write (set and delete) operations to the +// map; so that they can all be atomically committed when calling "write". type bufferedTxMap[K comparable, V any] struct { source map[K]V dirty map[K]deletable[V] @@ -131,7 +134,7 @@ func (b *bufferedTxMap[K, V]) init() { // buffered creates a copy of b, which has a usable dirty map. func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { if b.dirty != nil { - panic("cannot stack buffered tx maps") + panic("cannot stack multiple bufferedTxMap") } return bufferedTxMap[K, V]{ source: b.source, @@ -233,7 +236,7 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans // transaction-scoped parentStore: ds, - cacheObjects: maps.Clone(ds.cacheObjects), + cacheObjects: make(map[ObjectID]Object), cacheTypes: ds.cacheTypes.buffered(), cacheNodes: ds.cacheNodes.buffered(), alloc: ds.alloc.Fork().Reset(), @@ -249,11 +252,17 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans current: nil, opslog: nil, } + ds2.SetCachePackage(Uverse()) + return transactionStore{ds2} } func (ds *defaultStore) preprocessFork() Store { - // XXX IMPROVE + // XXX: + // This is only used internally, in Preprocess, when using evalStaticType + // and evalStaticTypeOfRaw. + // Could be joined with BeginTransaction if we allowed for stacking. + ds2 := &defaultStore{ // underlying stores baseStore: ds.baseStore, @@ -278,6 +287,7 @@ func (ds *defaultStore) preprocessFork() Store { opslog: nil, } ds2.SetCachePackage(Uverse()) + return ds2 } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go new file mode 100644 index 00000000000..f6db9ca118a --- /dev/null +++ b/gnovm/pkg/gnolang/store_test.go @@ -0,0 +1,388 @@ +package gnolang + +import ( + "maps" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_bufferedTxMap(t *testing.T) { + type Value struct{} + + // Full "integration test" of the bufferedTxMap. + var m bufferedTxMap[int, *Value] + m.init() + + vs := [...]*Value{ + {}, + {}, + {}, + {}, + } + m.Set(0, vs[0]) + m.Set(1, vs[1]) + m.Set(2, vs[2]) + + { + // Attempt getting, and deleting an item. + v, ok := m.Get(0) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + m.Delete(0) + v, ok = m.Get(0) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + } + + saved := maps.Clone(m.source) + bm := m.buffered() + + { + // Attempt getting, deleting an item on a buffered map; + // then creating a new one. + v, ok := bm.Get(1) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[1] == v, "pointer returned should be ==") + + bm.Delete(1) + v, ok = bm.Get(1) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + + bm.Set(3, vs[3]) + v, ok = bm.Get(3) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[3] == v, "pointer returned should be ==") + + // The original bufferedTxMap should still not know about the + // new value, and the internal "source" map should still be the + // same. + v, ok = m.Get(3) + assert.Nil(t, v) + assert.False(t, ok) + v, ok = m.Get(1) + assert.True(t, vs[1] == v) + assert.True(t, ok) + assert.Equal(t, saved, m.source) + assert.Equal(t, saved, bm.source) + } + + { + // Using write() should cause bm's internal buffer to be cleared; + // and for all changes to show up on the source map. + bm.write() + assert.Empty(t, bm.dirty) + assert.Equal(t, m.source, bm.source) + assert.NotEqual(t, saved, m.source) + + v, ok := m.Get(3) + assert.True(t, vs[3] == v) + assert.True(t, ok) + v, ok = m.Get(1) + assert.Nil(t, v) + assert.False(t, ok) + } +} + +func Test_bufferedTxMap_initErr(t *testing.T) { + var b bufferedTxMap[bool, bool] + b.init() + + assert.PanicsWithValue(t, "cannot init with a dirty buffer", func() { + buf := b.buffered() + buf.init() + }) +} + +func Test_bufferedTxMap_bufferedErr(t *testing.T) { + var b bufferedTxMap[bool, bool] + b.init() + buf := b.buffered() + + assert.PanicsWithValue(t, "cannot stack multiple bufferedTxMap", func() { + buf.buffered() + }) +} + +type hashMap[K comparable, V any] interface { + Get(K) (V, bool) + Set(K, V) + Delete(K) +} + +type stackableTxMap[K comparable, V any] struct { + source hashMap[K, V] + dirty map[K]deletable[V] +} + +func newStackable[K comparable, V any](source hashMap[K, V]) stackableTxMap[K, V] { + return stackableTxMap[K, V]{ + source: source, + dirty: make(map[K]deletable[V]), + } +} + +// write commits the data in dirty to the map in source. +func (b *stackableTxMap[K, V]) write() { + for k, v := range b.dirty { + if v.deleted { + b.source.Delete(k) + } else { + b.source.Set(k, v.v) + } + } + b.dirty = make(map[K]deletable[V]) +} + +func (b stackableTxMap[K, V]) Get(k K) (V, bool) { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + + return b.source.Get(k) +} + +func (b stackableTxMap[K, V]) Set(k K, v V) { + b.dirty[k] = deletable[V]{v: v} +} + +func (b stackableTxMap[K, V]) Delete(k K) { + b.dirty[k] = deletable[V]{deleted: true} +} + +type mapWrapper[K comparable, V any] map[K]V + +func (m mapWrapper[K, V]) Get(k K) (V, bool) { + v, ok := m[k] + return v, ok +} + +func (m mapWrapper[K, V]) Set(k K, v V) { + m[k] = v +} + +func (m mapWrapper[K, V]) Delete(k K) { + delete(m, k) +} + +func Benchmark_stackableTxMapRead(b *testing.B) { + const maxValues = (1 << 10) * 9 // must be multiple of 9 + + var ( + baseMap = make(map[int]int) // all values filled + wrapped = mapWrapper[int, int](baseMap) // wrapper around baseMap + stack1 = newStackable(wrapped) // n+1, n+4, n+7 values filled (n%9 == 0) + stack2 = newStackable(stack1) // n'th values filled (n%9 == 0) + ) + + for i := 0; i < maxValues; i++ { + baseMap[i] = i + switch i % 9 { + case 1, 4, 7: + stack1.Set(i, i+1_000_000) + case 0: + stack2.Set(i, i+10_000_000) + } + } + + var v int + var ok bool + _, _ = v, ok + + // through closure, so func calls have to go through "indirection". + runbench := func(b *testing.B, src hashMap[int, int]) { //nolint:thelper + for i := 0; i < b.N; i++ { + v, ok = src.Get(i % maxValues) + } + } + + b.Run("stack2", func(b *testing.B) { runbench(b, stack2) }) + b.Run("stack1", func(b *testing.B) { runbench(b, stack1) }) + b.Run("wrapped", func(b *testing.B) { runbench(b, wrapped) }) + b.Run("baseline", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = baseMap[i%maxValues] + } + }) +} + +func Benchmark_stackableTxMapWrite(b *testing.B) { + // after this amount of values, the maps are re-initialized. + // you can tweak this to see how the benchmarks behave on a variety of + // values. + // NOTE: setting this too high will skew the benchmark in favour those which + // have a smaller N, as those with a higher N have to allocate more in a + // single map. + const maxValues = 1 << 15 // 32768 + + var v int + var ok bool + _, _ = v, ok + + b.Run("stack1", func(b *testing.B) { + src := mapWrapper[int, int](make(map[int]int)) + st := newStackable(src) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + st.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = st.Get(k) + + if k == maxValues-1 { + st = newStackable(src) + } + } + }) + b.Run("wrapped", func(b *testing.B) { + src := mapWrapper[int, int](make(map[int]int)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + src.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = src.Get(k) + + if k == maxValues-1 { + src = mapWrapper[int, int](make(map[int]int)) + } + } + }) + b.Run("baseline", func(b *testing.B) { + // this serves to have a baseline value in the benchmark results + // for when we just use a map directly. + m := make(map[int]int) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + m[k] = i + v, ok = m[k] + + if k == maxValues-1 { + m = make(map[int]int) + } + } + }) +} + +func Benchmark_bufferedTxMapRead(b *testing.B) { + const maxValues = (1 << 10) * 9 // must be multiple of 9 + + var ( + baseMap = make(map[int]int) // all values filled + wrapped = bufferedTxMap[int, int]{source: baseMap} + stack1 = wrapped.buffered() // n, n+1, n+4, n+7 values filled (n%9 == 0) + // this test doesn't have stack2 as bufferedTxMap + // does not support stacking + ) + + for i := 0; i < maxValues; i++ { + baseMap[i] = i + switch i % 9 { + case 0, 1, 4, 7: + stack1.Set(i, i+1_000_000) + } + } + + var v int + var ok bool + _, _ = v, ok + + b.Run("stack1", func(b *testing.B) { + for i := 0; i < b.N; i++ { + // use assignment to avoid the compiler optimizing out the loops + v, ok = stack1.Get(i % maxValues) + } + }) + b.Run("wrapped", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = wrapped.Get(i % maxValues) + } + }) + b.Run("baseline", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = baseMap[i%maxValues] + } + }) +} + +func Benchmark_bufferedTxMapWrite(b *testing.B) { + // after this amount of values, the maps are re-initialized. + // you can tweak this to see how the benchmarks behave on a variety of + // values. + // NOTE: setting this too high will skew the benchmark in favour those which + // have a smaller N, as those with a higher N have to allocate more in a + // single map. + const maxValues = 1 << 15 // 32768 + + var v int + var ok bool + _, _ = v, ok + + b.Run("buffered", func(b *testing.B) { + var orig bufferedTxMap[int, int] + orig.init() + txm := orig.buffered() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + txm.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = txm.Get(k) + + if k == maxValues-1 { + txm = orig.buffered() + } + } + }) + b.Run("unbuffered", func(b *testing.B) { + var txm bufferedTxMap[int, int] + txm.init() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + txm.Set(k, i) + v, ok = txm.Get(k) + + if k == maxValues-1 { + txm.init() + } + } + }) + b.Run("baseline", func(b *testing.B) { + // this serves to have a baseline value in the benchmark results + // for when we just use a map directly. + m := make(map[int]int) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + m[k] = i + v, ok = m[k] + + if k == maxValues-1 { + m = make(map[int]int) + } + } + }) +} From 84ad015499e503df7ecd3e4e6508834e62e7663d Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 21 Aug 2024 15:54:39 +0200 Subject: [PATCH 21/45] add txLogMap as the default solution --- gnovm/pkg/gnolang/preprocess.go | 4 +- gnovm/pkg/gnolang/store.go | 232 ++++++++++++++------------------ gnovm/pkg/gnolang/store_test.go | 94 +++++++------ 3 files changed, 154 insertions(+), 176 deletions(-) diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ee05b2a9e2f..cb21160f85e 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -2454,7 +2454,7 @@ func evalStaticType(store Store, last BlockNode, x Expr) Type { // See comment in evalStaticTypeOfRaw. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.preprocessFork() + store = store.BeginTransaction(nil, nil) store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) @@ -2528,7 +2528,7 @@ func evalStaticTypeOfRaw(store Store, last BlockNode, x Expr) (t Type) { // yet predefined this time around. if store != nil && pn.PkgPath != uversePkgPath { pv := pn.NewPackage() // temporary - store = store.preprocessFork() + store = store.BeginTransaction(nil, nil) store.SetCachePackage(pv) } m := NewMachine(pn.PkgPath, store) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 0c2fba76071..6bee20cab82 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -73,8 +73,6 @@ type Store interface { LogSwitchRealm(rlmpath string) // to mark change of realm boundaries ClearCache() Print() - - preprocessFork() Store } // TransactionStore is a store where the operations modifying the underlying store's @@ -94,11 +92,11 @@ type defaultStore struct { iavlStore store.Store // for escaped object hashes // transaction-scoped - parentStore *defaultStore // set only during transactions. - cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. - cacheTypes bufferedTxMap[TypeID, Type] // this re-uses the parent store's. - cacheNodes bufferedTxMap[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. - alloc *Allocator // for accounting for cached items + parentStore *defaultStore // set only during transactions. + cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. + cacheTypes hashMap[TypeID, Type] // this re-uses the parent store's. + cacheNodes hashMap[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. + alloc *Allocator // for accounting for cached items // store configuration; cannot be modified in a transaction pkgGetter PackageGetter // non-realm packages @@ -113,75 +111,109 @@ type defaultStore struct { opslog []StoreOp // for debugging and testing. } -// bufferedTxMap is a wrapper around the map type, supporting regular Get, Set -// and Delete operations. Additionally, it can create a "buffered" version of -// itself, which will keep track of all write (set and delete) operations to the -// map; so that they can all be atomically committed when calling "write". -type bufferedTxMap[K comparable, V any] struct { - source map[K]V - dirty map[K]deletable[V] +type hashMap[K comparable, V any] interface { + Get(K) (V, bool) + Set(K, V) + Delete(K) + Iterate() func(yield func(K, V) bool) } -// init should be called when creating the bufferedTxMap, in a non-buffered -// context. -func (b *bufferedTxMap[K, V]) init() { - if b.dirty != nil { - panic("cannot init with a dirty buffer") - } - b.source = make(map[K]V) +type txLogMap[K comparable, V any] struct { + source hashMap[K, V] + dirty map[K]deletable[V] } -// buffered creates a copy of b, which has a usable dirty map. -func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { - if b.dirty != nil { - panic("cannot stack multiple bufferedTxMap") - } - return bufferedTxMap[K, V]{ - source: b.source, +func newTxLog[K comparable, V any](source hashMap[K, V]) *txLogMap[K, V] { + return &txLogMap[K, V]{ + source: source, dirty: make(map[K]deletable[V]), } } // write commits the data in dirty to the map in source. -func (b *bufferedTxMap[K, V]) write() { +func (b *txLogMap[K, V]) write() { for k, v := range b.dirty { if v.deleted { - delete(b.source, k) + b.source.Delete(k) } else { - b.source[k] = v.v + b.source.Set(k, v.v) } } b.dirty = make(map[K]deletable[V]) } -func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { - if b.dirty != nil { - if bufValue, ok := b.dirty[k]; ok { - if bufValue.deleted { - var zeroV V - return zeroV, false +func (b txLogMap[K, V]) Get(k K) (V, bool) { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + + return b.source.Get(k) +} + +func (b txLogMap[K, V]) Set(k K, v V) { + b.dirty[k] = deletable[V]{v: v} +} + +func (b txLogMap[K, V]) Delete(k K) { + b.dirty[k] = deletable[V]{deleted: true} +} + +func (b txLogMap[K, V]) Iterate() func(yield func(K, V) bool) { + return func(yield func(K, V) bool) { + b.source.Iterate()(func(k K, v V) bool { + if dirty, ok := b.dirty[k]; ok { + if dirty.deleted { + return true + } + return yield(k, dirty.v) + } + + // not in dirty + return yield(k, v) + }) + // yield for new values + for k, v := range b.dirty { + if v.deleted { + continue + } + _, ok := b.source.Get(k) + if ok { + continue + } + if !yield(k, v.v) { + break } - return bufValue.v, true } } - v, ok := b.source[k] +} + +type mapWrapper[K comparable, V any] map[K]V + +func (m mapWrapper[K, V]) Get(k K) (V, bool) { + v, ok := m[k] return v, ok } -func (b bufferedTxMap[K, V]) Set(k K, v V) { - if b.dirty == nil { - b.source[k] = v - return - } - b.dirty[k] = deletable[V]{v: v} +func (m mapWrapper[K, V]) Set(k K, v V) { + m[k] = v } -func (b bufferedTxMap[K, V]) Delete(k K) { - if b.dirty == nil { - delete(b.source, k) - return +func (m mapWrapper[K, V]) Delete(k K) { + delete(m, k) +} + +func (m mapWrapper[K, V]) Iterate() func(yield func(K, V) bool) { + return func(yield func(K, V) bool) { + for k, v := range m { + if !yield(k, v) { + return + } + } } - b.dirty[k] = deletable[V]{deleted: true} } type deletable[V any] struct { @@ -197,6 +229,8 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore // cacheObjects is set; objects in the store will be copied over for any transaction. cacheObjects: make(map[ObjectID]Object), + cacheTypes: mapWrapper[TypeID, Type](map[TypeID]Type{}), + cacheNodes: mapWrapper[Location, BlockNode](map[Location]BlockNode{}), // store configuration pkgGetter: nil, @@ -205,8 +239,6 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore nativeStore: nil, go2gnoStrict: true, } - ds.cacheTypes.init() - ds.cacheNodes.init() InitStoreCaches(ds) return ds } @@ -237,8 +269,8 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans // transaction-scoped parentStore: ds, cacheObjects: make(map[ObjectID]Object), - cacheTypes: ds.cacheTypes.buffered(), - cacheNodes: ds.cacheNodes.buffered(), + cacheTypes: newTxLog(ds.cacheTypes), + cacheNodes: newTxLog(ds.cacheNodes), alloc: ds.alloc.Fork().Reset(), // store configuration @@ -257,48 +289,10 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans return transactionStore{ds2} } -func (ds *defaultStore) preprocessFork() Store { - // XXX: - // This is only used internally, in Preprocess, when using evalStaticType - // and evalStaticTypeOfRaw. - // Could be joined with BeginTransaction if we allowed for stacking. - - ds2 := &defaultStore{ - // underlying stores - baseStore: ds.baseStore, - iavlStore: ds.iavlStore, - - // transaction-scoped - parentStore: ds, - cacheObjects: make(map[ObjectID]Object), - cacheTypes: ds.cacheTypes, - cacheNodes: ds.cacheNodes, - alloc: ds.alloc.Fork().Reset(), - - // store configuration - pkgGetter: ds.pkgGetter, - cacheNativeTypes: ds.cacheNativeTypes, - pkgInjector: ds.pkgInjector, - nativeStore: ds.nativeStore, - go2gnoStrict: ds.go2gnoStrict, - - // transient - current: nil, - opslog: nil, - } - ds2.SetCachePackage(Uverse()) - - return ds2 -} - type transactionStore struct{ *defaultStore } -func (t transactionStore) Write() { t.write() } - -// writes to parentStore, prepares the transactional ds for a new execution. -func (ds *defaultStore) write() { - ds.cacheTypes.write() - ds.cacheNodes.write() +func (t transactionStore) Write() { + t.cacheNodes.(*txLogMap[Location, BlockNode]).write() } // CopyCachesFromStore allows to copy a store's internal object, type and @@ -316,16 +310,14 @@ func CopyFromCachedStore(destStore, cachedStore Store, cachedBase, cachedIavl st ds.iavlStore.Set(iter.Key(), iter.Value()) } - if ss.cacheTypes.dirty != nil || - ss.cacheNodes.dirty != nil { - panic("cacheTypes and cacheNodes should be unbuffered") - } - for k, v := range ss.cacheTypes.source { + ss.cacheTypes.Iterate()(func(k TypeID, v Type) bool { ds.cacheTypes.Set(k, v) - } - for k, v := range ss.cacheNodes.source { + return true + }) + ss.cacheNodes.Iterate()(func(k Location, v BlockNode) bool { ds.cacheNodes.Set(k, v) - } + return true + }) } func (ds *defaultStore) GetAllocator() *Allocator { @@ -931,8 +923,8 @@ func (ds *defaultStore) ClearCache() { panic("ClearCache can only be called on non-transactional stores") } ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes.init() - ds.cacheNodes.init() + ds.cacheTypes = mapWrapper[TypeID, Type](map[TypeID]Type{}) + ds.cacheNodes = mapWrapper[Location, BlockNode](map[Location]BlockNode{}) ds.cacheNativeTypes = make(map[reflect.Type]Type) // restore builtin types to cache. InitStoreCaches(ds) @@ -948,38 +940,18 @@ func (ds *defaultStore) Print() { utils.Print(ds.iavlStore) fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheTypes...")) - for tid, typ := range ds.cacheTypes.source { + ds.cacheTypes.Iterate()(func(tid TypeID, typ Type) bool { fmt.Printf("- %v: %v\n", tid, stringz.TrimN(fmt.Sprintf("%v", typ), 50)) - } - if len(ds.cacheTypes.dirty) > 0 { - fmt.Println(colors.Green("defaultStore:cacheTypes (pending)...")) - for tid, typ := range ds.cacheTypes.dirty { - if typ.deleted { - fmt.Printf("- %v (deleted)\n", tid) - } else { - fmt.Printf("- %v: %v\n", tid, - stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) - } - } - } + return true + }) fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheNodes...")) - for loc, bn := range ds.cacheNodes.source { + ds.cacheNodes.Iterate()(func(loc Location, bn BlockNode) bool { fmt.Printf("- %v: %v\n", loc, stringz.TrimN(fmt.Sprintf("%v", bn), 50)) - } - if len(ds.cacheNodes.dirty) > 0 { - fmt.Println(colors.Green("defaultStore:cacheNodes (pending)...")) - for tid, typ := range ds.cacheNodes.dirty { - if typ.deleted { - fmt.Printf("- %v (deleted)\n", tid) - } else { - fmt.Printf("- %v: %v\n", tid, - stringz.TrimN(fmt.Sprintf("%v", typ.v), 50)) - } - } - } + return true + }) fmt.Println(colors.Red("//----------------------------------------")) } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index f6db9ca118a..b626a2e3854 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -106,79 +106,85 @@ func Test_bufferedTxMap_bufferedErr(t *testing.T) { }) } -type hashMap[K comparable, V any] interface { - Get(K) (V, bool) - Set(K, V) - Delete(K) +// bufferedTxMap is a wrapper around the map type, supporting regular Get, Set +// and Delete operations. Additionally, it can create a "buffered" version of +// itself, which will keep track of all write (set and delete) operations to the +// map; so that they can all be atomically committed when calling "write". +type bufferedTxMap[K comparable, V any] struct { + source map[K]V + dirty map[K]deletable[V] } -type stackableTxMap[K comparable, V any] struct { - source hashMap[K, V] - dirty map[K]deletable[V] +// init should be called when creating the bufferedTxMap, in a non-buffered +// context. +func (b *bufferedTxMap[K, V]) init() { + if b.dirty != nil { + panic("cannot init with a dirty buffer") + } + b.source = make(map[K]V) } -func newStackable[K comparable, V any](source hashMap[K, V]) stackableTxMap[K, V] { - return stackableTxMap[K, V]{ - source: source, +// buffered creates a copy of b, which has a usable dirty map. +func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { + if b.dirty != nil { + panic("cannot stack multiple bufferedTxMap") + } + return bufferedTxMap[K, V]{ + source: b.source, dirty: make(map[K]deletable[V]), } } // write commits the data in dirty to the map in source. -func (b *stackableTxMap[K, V]) write() { +func (b *bufferedTxMap[K, V]) write() { for k, v := range b.dirty { if v.deleted { - b.source.Delete(k) + delete(b.source, k) } else { - b.source.Set(k, v.v) + b.source[k] = v.v } } b.dirty = make(map[K]deletable[V]) } -func (b stackableTxMap[K, V]) Get(k K) (V, bool) { - if bufValue, ok := b.dirty[k]; ok { - if bufValue.deleted { - var zeroV V - return zeroV, false +func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { + if b.dirty != nil { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true } - return bufValue.v, true } - - return b.source.Get(k) + v, ok := b.source[k] + return v, ok } -func (b stackableTxMap[K, V]) Set(k K, v V) { +func (b bufferedTxMap[K, V]) Set(k K, v V) { + if b.dirty == nil { + b.source[k] = v + return + } b.dirty[k] = deletable[V]{v: v} } -func (b stackableTxMap[K, V]) Delete(k K) { +func (b bufferedTxMap[K, V]) Delete(k K) { + if b.dirty == nil { + delete(b.source, k) + return + } b.dirty[k] = deletable[V]{deleted: true} } -type mapWrapper[K comparable, V any] map[K]V - -func (m mapWrapper[K, V]) Get(k K) (V, bool) { - v, ok := m[k] - return v, ok -} - -func (m mapWrapper[K, V]) Set(k K, v V) { - m[k] = v -} - -func (m mapWrapper[K, V]) Delete(k K) { - delete(m, k) -} - -func Benchmark_stackableTxMapRead(b *testing.B) { +func Benchmark_txLogMapRead(b *testing.B) { const maxValues = (1 << 10) * 9 // must be multiple of 9 var ( baseMap = make(map[int]int) // all values filled wrapped = mapWrapper[int, int](baseMap) // wrapper around baseMap - stack1 = newStackable(wrapped) // n+1, n+4, n+7 values filled (n%9 == 0) - stack2 = newStackable(stack1) // n'th values filled (n%9 == 0) + stack1 = newTxLog(wrapped) // n+1, n+4, n+7 values filled (n%9 == 0) + stack2 = newTxLog(stack1) // n'th values filled (n%9 == 0) ) for i := 0; i < maxValues; i++ { @@ -212,7 +218,7 @@ func Benchmark_stackableTxMapRead(b *testing.B) { }) } -func Benchmark_stackableTxMapWrite(b *testing.B) { +func Benchmark_txLogMapWrite(b *testing.B) { // after this amount of values, the maps are re-initialized. // you can tweak this to see how the benchmarks behave on a variety of // values. @@ -227,7 +233,7 @@ func Benchmark_stackableTxMapWrite(b *testing.B) { b.Run("stack1", func(b *testing.B) { src := mapWrapper[int, int](make(map[int]int)) - st := newStackable(src) + st := newTxLog(src) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -239,7 +245,7 @@ func Benchmark_stackableTxMapWrite(b *testing.B) { v, ok = st.Get(k) if k == maxValues-1 { - st = newStackable(src) + st = newTxLog(src) } } }) From 2235cb6feba7f4c014dca07a6234222fee531ba9 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 21 Aug 2024 16:03:17 +0200 Subject: [PATCH 22/45] clone test for txLogMap --- gnovm/pkg/gnolang/store_test.go | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index b626a2e3854..3721b34492e 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -7,6 +7,84 @@ import ( "github.com/stretchr/testify/assert" ) +func Test_txLogMap(t *testing.T) { + type Value struct{} + + // Full "integration test" of the txLogMap + mapwrapper. + source := mapWrapper[int, *Value](map[int]*Value{}) + + vs := [...]*Value{ + {}, + {}, + {}, + {}, + } + source.Set(0, vs[0]) + source.Set(1, vs[1]) + source.Set(2, vs[2]) + + { + // Attempt getting, and deleting an item. + v, ok := source.Get(0) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + source.Delete(0) + v, ok = source.Get(0) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + } + + saved := mapWrapper[int, *Value](maps.Clone(source)) + txm := newTxLog(source) + + { + // Attempt getting, deleting an item on a buffered map; + // then creating a new one. + v, ok := txm.Get(1) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[1] == v, "pointer returned should be ==") + + txm.Delete(1) + v, ok = txm.Get(1) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + + txm.Set(3, vs[3]) + v, ok = txm.Get(3) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[3] == v, "pointer returned should be ==") + + // The original bufferedTxMap should still not know about the + // new value, and the internal "source" map should still be the + // same. + v, ok = source.Get(3) + assert.Nil(t, v) + assert.False(t, ok) + v, ok = source.Get(1) + assert.True(t, vs[1] == v) + assert.True(t, ok) + assert.Equal(t, saved, source) + assert.Equal(t, saved, txm.source) + } + + { + // Using write() should cause bm's internal buffer to be cleared; + // and for all changes to show up on the source map. + txm.write() + assert.Empty(t, txm.dirty) + assert.Equal(t, source, txm.source) + assert.NotEqual(t, saved, source) + + v, ok := source.Get(3) + assert.True(t, vs[3] == v) + assert.True(t, ok) + v, ok = source.Get(1) + assert.Nil(t, v) + assert.False(t, ok) + } +} + func Test_bufferedTxMap(t *testing.T) { type Value struct{} From 9a1ba12d49a0492999dda765e20b411183b072a1 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 21 Aug 2024 16:18:40 +0200 Subject: [PATCH 23/45] oops --- gnovm/pkg/gnolang/store.go | 1 + gnovm/pkg/gnolang/store_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 6bee20cab82..df63d39dee8 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -292,6 +292,7 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans type transactionStore struct{ *defaultStore } func (t transactionStore) Write() { + t.cacheTypes.(*txLogMap[TypeID, Type]).write() t.cacheNodes.(*txLogMap[Location, BlockNode]).write() } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 3721b34492e..9e664b55ae3 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -35,7 +35,7 @@ func Test_txLogMap(t *testing.T) { assert.Nil(t, v, "pointer returned should be nil") } - saved := mapWrapper[int, *Value](maps.Clone(source)) + saved := maps.Clone(source) txm := newTxLog(source) { From 40fae4827aef89e22d740f570a64054376f95cb5 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 21 Aug 2024 16:40:12 +0200 Subject: [PATCH 24/45] move panics into methods of transactionStore --- gnovm/pkg/gnolang/store.go | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index df63d39dee8..d50800665a3 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -92,7 +92,6 @@ type defaultStore struct { iavlStore store.Store // for escaped object hashes // transaction-scoped - parentStore *defaultStore // set only during transactions. cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. cacheTypes hashMap[TypeID, Type] // this re-uses the parent store's. cacheNodes hashMap[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. @@ -104,7 +103,6 @@ type defaultStore struct { pkgInjector PackageInjector // for injecting natives nativeStore NativeStore // for injecting natives go2gnoStrict bool // if true, native->gno type conversion must be registered. - // XXX panic when changing these and parentStore != nil // transient current []string // for detecting import cycles. @@ -267,7 +265,6 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans iavlStore: iavlStore, // transaction-scoped - parentStore: ds, cacheObjects: make(map[ObjectID]Object), cacheTypes: newTxLog(ds.cacheTypes), cacheNodes: newTxLog(ds.cacheNodes), @@ -296,6 +293,33 @@ func (t transactionStore) Write() { t.cacheNodes.(*txLogMap[Location, BlockNode]).write() } +func (transactionStore) SetPackageGetter(pg PackageGetter) { + panic("package getter may not be modified in a transaction store") +} + +func (transactionStore) ClearCache() { + panic("ClearCache may not be called in a transaction store") +} + +// XXX: we should block Go2GnoType, because it uses a global cache map; +// but it's called during preprocess and thus breaks some testing code. +// let's wait until we remove Go2Gno entirely. +// func (transactionStore) Go2GnoType(reflect.Type) Type { +// panic("Go2GnoType may not be called in a transaction store") +// } + +func (transactionStore) SetPackageInjector(inj PackageInjector) { + panic("SetPackageInjector may not be called in a transaction store") +} + +func (transactionStore) SetNativeStore(ns NativeStore) { + panic("SetNativeStore may not be called in a transaction store") +} + +func (transactionStore) SetStrictGo2GnoMapping(strict bool) { + panic("SetStrictGo2GnoMapping may not be called in a transaction store") +} + // CopyCachesFromStore allows to copy a store's internal object, type and // BlockNode cache into the dst store. // This is mostly useful for testing, where many stores have to be initialized. @@ -326,9 +350,6 @@ func (ds *defaultStore) GetAllocator() *Allocator { } func (ds *defaultStore) SetPackageGetter(pg PackageGetter) { - if ds.parentStore != nil { - panic("package getter cannot be modified in a transaction") - } ds.pkgGetter = pg } @@ -920,9 +941,6 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { } func (ds *defaultStore) ClearCache() { - if ds.parentStore != nil { - panic("ClearCache can only be called on non-transactional stores") - } ds.cacheObjects = make(map[ObjectID]Object) ds.cacheTypes = mapWrapper[TypeID, Type](map[TypeID]Type{}) ds.cacheNodes = mapWrapper[Location, BlockNode](map[Location]BlockNode{}) From bcf0e0ec9c58ebe5bfed1b55daba553e6a2cf006 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 17:28:32 +0200 Subject: [PATCH 25/45] codereview changes --- gno.land/pkg/gnoland/app.go | 6 ++++-- gno.land/pkg/gnoland/app_test.go | 17 ++++++++++------- gnovm/pkg/gnolang/store.go | 24 +++++++++--------------- gnovm/pkg/gnolang/store_test.go | 8 ++++++++ tm2/pkg/sdk/baseapp_test.go | 2 ++ 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index bedc5425060..84326f4d793 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -43,7 +43,10 @@ func (c *AppOptions) validate() error { if c.DB == nil { return fmt.Errorf("no db provided") } + return nil +} +func (c *AppOptions) setDefaults() { // Set defaults if c.Logger == nil { c.Logger = log.NewNoopLogger() @@ -51,8 +54,6 @@ func (c *AppOptions) validate() error { if c.EventSwitch == nil { c.EventSwitch = events.NewEventSwitch() } - - return nil } // NewAppWithOptions creates the gno.land application with specified options. @@ -60,6 +61,7 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { if err := cfg.validate(); err != nil { return nil, err } + cfg.setDefaults() // Capabilities keys. mainKey := store.NewStoreKey("main") diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 6dcdae08fb0..c34b467de26 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -30,6 +30,8 @@ import ( // Tests that NewAppWithOptions works even when only providing a simple DB. func TestNewAppWithOptions(t *testing.T) { + t.Parallel() + app, err := NewAppWithOptions(&AppOptions{ DB: memdb.NewMemDB(), InitChainerConfig: InitChainerConfig{ @@ -77,10 +79,7 @@ func TestNewAppWithOptions(t *testing.T) { }, }, }) - - if !resp.IsOK() { - t.Fatal(resp) - } + require.True(t, resp.IsOK(), "InitChain response: %v", resp) tx := amino.MustMarshal(std.Tx{ Msgs: []std.Msg{vm.NewMsgCall(addr, nil, "gno.land/r/demo", "Hello", nil)}, @@ -98,23 +97,27 @@ func TestNewAppWithOptions(t *testing.T) { RequestBase: abci.RequestBase{}, Tx: tx, }) - if !dtxResp.IsOK() { - t.Fatal(dtxResp) - } + require.True(t, dtxResp.IsOK(), "DeliverTx response: %v", dtxResp) } func TestNewAppWithOptions_ErrNoDB(t *testing.T) { + t.Parallel() + _, err := NewAppWithOptions(&AppOptions{}) assert.ErrorContains(t, err, "no db provided") } // Test whether InitChainer calls to load the stdlibs correctly. func TestInitChainer_LoadStdlib(t *testing.T) { + t.Parallel() + t.Run("cached", func(t *testing.T) { testInitChainerLoadStdlib(t, true) }) t.Run("uncached", func(t *testing.T) { testInitChainerLoadStdlib(t, false) }) } func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper + t.Parallel() + type gsContextType string const ( stdlibDir = "test-stdlib-dir" diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index d50800665a3..60268542e11 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -64,7 +64,7 @@ type Store interface { GetMemPackage(path string) *std.MemPackage GetMemFile(path string, name string) *std.MemFile IterMemPackage() <-chan *std.MemPackage - ClearObjectCache() + ClearObjectCache() // run before processing a message SetPackageInjector(PackageInjector) // for natives SetNativeStore(NativeStore) // for "new" natives XXX GetNative(pkgPath string, name Name) func(m *Machine) // for "new" natives XXX @@ -241,16 +241,6 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore return ds } -// Unstable. -// This function is used to clear the object cache every transaction. -// It also sets a new allocator. -func (ds *defaultStore) ClearObjectCache() { - ds.alloc.Reset() - ds.cacheObjects = make(map[ObjectID]Object) // new cache. - ds.opslog = nil // new ops log. - ds.SetCachePackage(Uverse()) -} - // If nil baseStore and iavlStore, the baseStores are re-used. func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) TransactionStore { if baseStore == nil { @@ -851,10 +841,14 @@ func (ds *defaultStore) IterMemPackage() <-chan *std.MemPackage { } } -// TODO: consider a better/faster/simpler way of achieving the overall same goal? -func (ds *defaultStore) SwapStores(baseStore, iavlStore store.Store) { - ds.baseStore = baseStore - ds.iavlStore = iavlStore +// Unstable. +// This function is used to clear the object cache every transaction. +// It also sets a new allocator. +func (ds *defaultStore) ClearObjectCache() { + ds.alloc.Reset() + ds.cacheObjects = make(map[ObjectID]Object) // new cache. + ds.opslog = nil // new ops log. + ds.SetCachePackage(Uverse()) } func (ds *defaultStore) SetPackageInjector(inj PackageInjector) { diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 9e664b55ae3..032eeabe81d 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -8,6 +8,8 @@ import ( ) func Test_txLogMap(t *testing.T) { + t.Parallel() + type Value struct{} // Full "integration test" of the txLogMap + mapwrapper. @@ -86,6 +88,8 @@ func Test_txLogMap(t *testing.T) { } func Test_bufferedTxMap(t *testing.T) { + t.Parallel() + type Value struct{} // Full "integration test" of the bufferedTxMap. @@ -165,6 +169,8 @@ func Test_bufferedTxMap(t *testing.T) { } func Test_bufferedTxMap_initErr(t *testing.T) { + t.Parallel() + var b bufferedTxMap[bool, bool] b.init() @@ -175,6 +181,8 @@ func Test_bufferedTxMap_initErr(t *testing.T) { } func Test_bufferedTxMap_bufferedErr(t *testing.T) { + t.Parallel() + var b bufferedTxMap[bool, bool] b.init() buf := b.buffered() diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index f54801b8277..a7701e2f674 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -200,6 +200,8 @@ func TestLoadVersionInvalid(t *testing.T) { } func TestOptionSetters(t *testing.T) { + t.Parallel() + tt := []struct { // Calling BaseApp.[method]([value]) should change BaseApp.[fieldName] to [value]. method string From b859b07357dc5b70a7ef61bf787f0fe99b1cb26e Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 17:41:06 +0200 Subject: [PATCH 26/45] add newGnoTransactionStore --- gno.land/pkg/sdk/vm/keeper.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 1d19c60c906..a32d745aada 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -202,12 +202,15 @@ type gnoStoreContextKeyType struct{} var gnoStoreContextKey gnoStoreContextKeyType -func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { +func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { ms := ctx.MultiStore() base := ms.GetStore(vm.baseKey) iavl := ms.GetStore(vm.iavlKey) - st := vm.gnoStore.BeginTransaction(base, iavl) - return ctx.WithValue(gnoStoreContextKey, st) + return vm.gnoStore.BeginTransaction(base, iavl) +} + +func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + return ctx.WithValue(gnoStoreContextKey, vm.newGnoTransactionStore(ctx)) } func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { @@ -633,8 +636,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { // QueryFuncs returns public facing function signatures. func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) { - ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) - store := vm.getGnoTransactionStore(ctx) + store := vm.newGnoTransactionStore(ctx) // throwaway (never committed) // Ensure pkgPath is realm. if !gno.IsRealmPath(pkgPath) { err = ErrInvalidPkgPath(fmt.Sprintf( @@ -696,9 +698,8 @@ func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionS // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "Eval". func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { - ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoTransactionStore(ctx) + gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -764,9 +765,8 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res // TODO: modify query protocol to allow MsgEval. // TODO: then, rename to "EvalString". func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string) (res string, err error) { - ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) alloc := gno.NewAllocator(maxAllocQuery) - gnostore := vm.getGnoTransactionStore(ctx) + gnostore := vm.newGnoTransactionStore(ctx) // throwaway (never committed) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -827,8 +827,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string } func (vm *VMKeeper) QueryFile(ctx sdk.Context, filepath string) (res string, err error) { - ctx = vm.MakeGnoTransactionStore(ctx) // throwaway (never committed) - store := vm.getGnoTransactionStore(ctx) + store := vm.newGnoTransactionStore(ctx) // throwaway (never committed) dirpath, filename := std.SplitFilepath(filepath) if filename != "" { memFile := store.GetMemFile(dirpath, filename) From 78552f824da1e10ca66205daba31e65f13af098e Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 17:46:30 +0200 Subject: [PATCH 27/45] codereview changes --- tm2/pkg/sdk/baseapp_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index a7701e2f674..c8884533b30 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -222,7 +222,7 @@ func TestOptionSetters(t *testing.T) { for _, tc := range tt { t.Run(tc.method, func(t *testing.T) { - t.Helper() + t.Parallel() var ba BaseApp rv := reflect.ValueOf(&ba) From 73af9c434e1a5ce4f3e66e87975b5195b7101bcd Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 18:56:53 +0200 Subject: [PATCH 28/45] add tests for iterators --- gnovm/pkg/gnolang/store.go | 2 +- gnovm/pkg/gnolang/store_test.go | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 60268542e11..35a26e11c04 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -284,7 +284,7 @@ func (t transactionStore) Write() { } func (transactionStore) SetPackageGetter(pg PackageGetter) { - panic("package getter may not be modified in a transaction store") + panic("SetPackageGetter may not be called in a transaction store") } func (transactionStore) ClearCache() { diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 032eeabe81d..d86df727a06 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -10,11 +10,12 @@ import ( func Test_txLogMap(t *testing.T) { t.Parallel() - type Value struct{} + type Value = struct{} // Full "integration test" of the txLogMap + mapwrapper. source := mapWrapper[int, *Value](map[int]*Value{}) + // create 4 empty values (we'll just use the pointers) vs := [...]*Value{ {}, {}, @@ -35,6 +36,8 @@ func Test_txLogMap(t *testing.T) { v, ok = source.Get(0) assert.False(t, ok, "should be unsuccessful Get") assert.Nil(t, v, "pointer returned should be nil") + + verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[2]}) } saved := maps.Clone(source) @@ -52,6 +55,13 @@ func Test_txLogMap(t *testing.T) { assert.False(t, ok, "should be unsuccessful Get") assert.Nil(t, v, "pointer returned should be nil") + // Update an existing value to another value. + txm.Set(2, vs[0]) + v, ok = txm.Get(2) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + // Add a new value txm.Set(3, vs[3]) v, ok = txm.Get(3) assert.True(t, ok, "should be successful Get") @@ -68,6 +78,10 @@ func Test_txLogMap(t *testing.T) { assert.True(t, ok) assert.Equal(t, saved, source) assert.Equal(t, saved, txm.source) + + // double-check on the iterators. + verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[0]}) + verifyHashMapValues(t, txm, map[int]*Value{2: vs[2], 3: vs[3]}) } { @@ -84,9 +98,27 @@ func Test_txLogMap(t *testing.T) { v, ok = source.Get(1) assert.Nil(t, v) assert.False(t, ok) + + // double-check on the iterators. + verifyHashMapValues(t, source, map[int]*Value{2: vs[0], 3: vs[3]}) + verifyHashMapValues(t, txm, map[int]*Value{2: vs[0], 3: vs[3]}) } } +func verifyHashMapValues(t *testing.T, m hashMap[int, *struct{}], expectedReadonly map[int]*struct{}) { + t.Helper() + + expected := maps.Clone(expectedReadonly) + m.Iterate()(func(k int, v *struct{}) bool { + ev, eok := expected[k] + _ = assert.True(t, eok, "mapping %d:%v should exist in expected map", k, v) && + assert.Equal(t, ev, v, "values should match") + delete(expected, k) + return true + }) + assert.Empty(t, expected, "(some) expected values not found in the hashMap") +} + func Test_bufferedTxMap(t *testing.T) { t.Parallel() From cba4c0ffabb95a159a15daa4fbebd56e2626ef74 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 19:17:21 +0200 Subject: [PATCH 29/45] refactor: move txLogMap into own internal package --- gnovm/pkg/gnolang/internal/txlog/txlog.go | 133 +++++ .../pkg/gnolang/internal/txlog/txlog_test.go | 512 ++++++++++++++++++ gnovm/pkg/gnolang/store.go | 120 +--- gnovm/pkg/gnolang/store_test.go | 511 ----------------- 4 files changed, 654 insertions(+), 622 deletions(-) create mode 100644 gnovm/pkg/gnolang/internal/txlog/txlog.go create mode 100644 gnovm/pkg/gnolang/internal/txlog/txlog_test.go diff --git a/gnovm/pkg/gnolang/internal/txlog/txlog.go b/gnovm/pkg/gnolang/internal/txlog/txlog.go new file mode 100644 index 00000000000..e582cf1b8ec --- /dev/null +++ b/gnovm/pkg/gnolang/internal/txlog/txlog.go @@ -0,0 +1,133 @@ +// Package txlog is an internal package containing data structures that can +// function as "transaction logs" on top of a hash map (or other key/value +// data type implementing [Map]). +// +// A transaction log keeps track of the write operations performed in a +// transaction +package txlog + +// Map is a generic interface to a key/value map, like Go's builtin map. +type Map[K comparable, V any] interface { + Get(K) (V, bool) + Set(K, V) + Delete(K) + Iterate() func(yield func(K, V) bool) +} + +// MapCommitter is a Map which also implements a Commit() method, which writes +// to the underlying (parent) [Map]. +type MapCommitter[K comparable, V any] interface { + Map[K, V] + + // Commit writes the logged operations to the underlying map. + // After calling commit, the underlying tx log is cleared and the + // MapCommitter may be reused. + Commit() +} + +// GoMap is a simple implementation of [Map], which wraps the operations of +// Go's map builtin to implement [Map]. +type GoMap[K comparable, V any] map[K]V + +// Get implements [Map]. +func (m GoMap[K, V]) Get(k K) (V, bool) { + v, ok := m[k] + return v, ok +} + +// Set implements [Map]. +func (m GoMap[K, V]) Set(k K, v V) { + m[k] = v +} + +// Delete implements [Map]. +func (m GoMap[K, V]) Delete(k K) { + delete(m, k) +} + +// Iterate implements [Map]. +func (m GoMap[K, V]) Iterate() func(yield func(K, V) bool) { + return func(yield func(K, V) bool) { + for k, v := range m { + if !yield(k, v) { + return + } + } + } +} + +// Wrap wraps the map m into a data structure to keep a transaction log. +// To write data to m, use MapCommitter.Commit. +func Wrap[K comparable, V any](m Map[K, V]) MapCommitter[K, V] { + return &txLog[K, V]{source: m, dirty: make(map[K]deletable[V])} +} + +type txLog[K comparable, V any] struct { + source Map[K, V] + dirty map[K]deletable[V] +} + +func (b *txLog[K, V]) Commit() { + for k, v := range b.dirty { + if v.deleted { + b.source.Delete(k) + } else { + b.source.Set(k, v.v) + } + } + b.dirty = make(map[K]deletable[V]) +} + +func (b txLog[K, V]) Get(k K) (V, bool) { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + + return b.source.Get(k) +} + +func (b txLog[K, V]) Set(k K, v V) { + b.dirty[k] = deletable[V]{v: v} +} + +func (b txLog[K, V]) Delete(k K) { + b.dirty[k] = deletable[V]{deleted: true} +} + +func (b txLog[K, V]) Iterate() func(yield func(K, V) bool) { + return func(yield func(K, V) bool) { + b.source.Iterate()(func(k K, v V) bool { + if dirty, ok := b.dirty[k]; ok { + if dirty.deleted { + return true + } + return yield(k, dirty.v) + } + + // not in dirty + return yield(k, v) + }) + // yield for new values + for k, v := range b.dirty { + if v.deleted { + continue + } + _, ok := b.source.Get(k) + if ok { + continue + } + if !yield(k, v.v) { + break + } + } + } +} + +type deletable[V any] struct { + v V + deleted bool +} diff --git a/gnovm/pkg/gnolang/internal/txlog/txlog_test.go b/gnovm/pkg/gnolang/internal/txlog/txlog_test.go new file mode 100644 index 00000000000..cc68df159c3 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/txlog/txlog_test.go @@ -0,0 +1,512 @@ +package txlog + +import ( + "maps" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_txLog(t *testing.T) { + t.Parallel() + + type Value = struct{} + + // Full "integration test" of the txLog + mapwrapper. + source := GoMap[int, *Value](map[int]*Value{}) + + // create 4 empty values (we'll just use the pointers) + vs := [...]*Value{ + {}, + {}, + {}, + {}, + } + source.Set(0, vs[0]) + source.Set(1, vs[1]) + source.Set(2, vs[2]) + + { + // Attempt getting, and deleting an item. + v, ok := source.Get(0) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + source.Delete(0) + v, ok = source.Get(0) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + + verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[2]}) + } + + saved := maps.Clone(source) + txm := Wrap(source).(*txLog[int, *Value]) + + { + // Attempt getting, deleting an item on a buffered map; + // then creating a new one. + v, ok := txm.Get(1) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[1] == v, "pointer returned should be ==") + + txm.Delete(1) + v, ok = txm.Get(1) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + + // Update an existing value to another value. + txm.Set(2, vs[0]) + v, ok = txm.Get(2) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + // Add a new value + txm.Set(3, vs[3]) + v, ok = txm.Get(3) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[3] == v, "pointer returned should be ==") + + // The original bufferedTxMap should still not know about the + // new value, and the internal "source" map should still be the + // same. + v, ok = source.Get(3) + assert.Nil(t, v) + assert.False(t, ok) + v, ok = source.Get(1) + assert.True(t, vs[1] == v) + assert.True(t, ok) + assert.Equal(t, saved, source) + assert.Equal(t, saved, txm.source) + + // double-check on the iterators. + verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[0]}) + verifyHashMapValues(t, txm, map[int]*Value{2: vs[2], 3: vs[3]}) + } + + { + // Using Commit() should cause txm's internal buffer to be cleared; + // and for all changes to show up on the source map. + txm.Commit() + assert.Empty(t, txm.dirty) + assert.Equal(t, source, txm.source) + assert.NotEqual(t, saved, source) + + v, ok := source.Get(3) + assert.True(t, vs[3] == v) + assert.True(t, ok) + v, ok = source.Get(1) + assert.Nil(t, v) + assert.False(t, ok) + + // double-check on the iterators. + verifyHashMapValues(t, source, map[int]*Value{2: vs[0], 3: vs[3]}) + verifyHashMapValues(t, txm, map[int]*Value{2: vs[0], 3: vs[3]}) + } +} + +func verifyHashMapValues(t *testing.T, m Map[int, *struct{}], expectedReadonly map[int]*struct{}) { + t.Helper() + + expected := maps.Clone(expectedReadonly) + m.Iterate()(func(k int, v *struct{}) bool { + ev, eok := expected[k] + _ = assert.True(t, eok, "mapping %d:%v should exist in expected map", k, v) && + assert.Equal(t, ev, v, "values should match") + delete(expected, k) + return true + }) + assert.Empty(t, expected, "(some) expected values not found in the Map") +} + +func Test_bufferedTxMap(t *testing.T) { + t.Parallel() + + type Value struct{} + + // Full "integration test" of the bufferedTxMap. + var m bufferedTxMap[int, *Value] + m.init() + + vs := [...]*Value{ + {}, + {}, + {}, + {}, + } + m.Set(0, vs[0]) + m.Set(1, vs[1]) + m.Set(2, vs[2]) + + { + // Attempt getting, and deleting an item. + v, ok := m.Get(0) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[0] == v, "pointer returned should be ==") + + m.Delete(0) + v, ok = m.Get(0) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + } + + saved := maps.Clone(m.source) + bm := m.buffered() + + { + // Attempt getting, deleting an item on a buffered map; + // then creating a new one. + v, ok := bm.Get(1) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[1] == v, "pointer returned should be ==") + + bm.Delete(1) + v, ok = bm.Get(1) + assert.False(t, ok, "should be unsuccessful Get") + assert.Nil(t, v, "pointer returned should be nil") + + bm.Set(3, vs[3]) + v, ok = bm.Get(3) + assert.True(t, ok, "should be successful Get") + assert.True(t, vs[3] == v, "pointer returned should be ==") + + // The original bufferedTxMap should still not know about the + // new value, and the internal "source" map should still be the + // same. + v, ok = m.Get(3) + assert.Nil(t, v) + assert.False(t, ok) + v, ok = m.Get(1) + assert.True(t, vs[1] == v) + assert.True(t, ok) + assert.Equal(t, saved, m.source) + assert.Equal(t, saved, bm.source) + } + + { + // Using write() should cause bm's internal buffer to be cleared; + // and for all changes to show up on the source map. + bm.write() + assert.Empty(t, bm.dirty) + assert.Equal(t, m.source, bm.source) + assert.NotEqual(t, saved, m.source) + + v, ok := m.Get(3) + assert.True(t, vs[3] == v) + assert.True(t, ok) + v, ok = m.Get(1) + assert.Nil(t, v) + assert.False(t, ok) + } +} + +func Test_bufferedTxMap_initErr(t *testing.T) { + t.Parallel() + + var b bufferedTxMap[bool, bool] + b.init() + + assert.PanicsWithValue(t, "cannot init with a dirty buffer", func() { + buf := b.buffered() + buf.init() + }) +} + +func Test_bufferedTxMap_bufferedErr(t *testing.T) { + t.Parallel() + + var b bufferedTxMap[bool, bool] + b.init() + buf := b.buffered() + + assert.PanicsWithValue(t, "cannot stack multiple bufferedTxMap", func() { + buf.buffered() + }) +} + +// bufferedTxMap is a wrapper around the map type, supporting regular Get, Set +// and Delete operations. Additionally, it can create a "buffered" version of +// itself, which will keep track of all write (set and delete) operations to the +// map; so that they can all be atomically committed when calling "write". +type bufferedTxMap[K comparable, V any] struct { + source map[K]V + dirty map[K]deletable[V] +} + +// init should be called when creating the bufferedTxMap, in a non-buffered +// context. +func (b *bufferedTxMap[K, V]) init() { + if b.dirty != nil { + panic("cannot init with a dirty buffer") + } + b.source = make(map[K]V) +} + +// buffered creates a copy of b, which has a usable dirty map. +func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { + if b.dirty != nil { + panic("cannot stack multiple bufferedTxMap") + } + return bufferedTxMap[K, V]{ + source: b.source, + dirty: make(map[K]deletable[V]), + } +} + +// write commits the data in dirty to the map in source. +func (b *bufferedTxMap[K, V]) write() { + for k, v := range b.dirty { + if v.deleted { + delete(b.source, k) + } else { + b.source[k] = v.v + } + } + b.dirty = make(map[K]deletable[V]) +} + +func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { + if b.dirty != nil { + if bufValue, ok := b.dirty[k]; ok { + if bufValue.deleted { + var zeroV V + return zeroV, false + } + return bufValue.v, true + } + } + v, ok := b.source[k] + return v, ok +} + +func (b bufferedTxMap[K, V]) Set(k K, v V) { + if b.dirty == nil { + b.source[k] = v + return + } + b.dirty[k] = deletable[V]{v: v} +} + +func (b bufferedTxMap[K, V]) Delete(k K) { + if b.dirty == nil { + delete(b.source, k) + return + } + b.dirty[k] = deletable[V]{deleted: true} +} + +func Benchmark_txLogRead(b *testing.B) { + const maxValues = (1 << 10) * 9 // must be multiple of 9 + + var ( + baseMap = make(map[int]int) // all values filled + wrapped = GoMap[int, int](baseMap) // wrapper around baseMap + stack1 = Wrap(wrapped) // n+1, n+4, n+7 values filled (n%9 == 0) + stack2 = Wrap(stack1) // n'th values filled (n%9 == 0) + ) + + for i := 0; i < maxValues; i++ { + baseMap[i] = i + switch i % 9 { + case 1, 4, 7: + stack1.Set(i, i+1_000_000) + case 0: + stack2.Set(i, i+10_000_000) + } + } + + var v int + var ok bool + _, _ = v, ok + + // through closure, so func calls have to go through "indirection". + runbench := func(b *testing.B, src Map[int, int]) { //nolint:thelper + for i := 0; i < b.N; i++ { + v, ok = src.Get(i % maxValues) + } + } + + b.Run("stack2", func(b *testing.B) { runbench(b, stack2) }) + b.Run("stack1", func(b *testing.B) { runbench(b, stack1) }) + b.Run("wrapped", func(b *testing.B) { runbench(b, wrapped) }) + b.Run("baseline", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = baseMap[i%maxValues] + } + }) +} + +func Benchmark_txLogWrite(b *testing.B) { + // after this amount of values, the maps are re-initialized. + // you can tweak this to see how the benchmarks behave on a variety of + // values. + // NOTE: setting this too high will skew the benchmark in favour those which + // have a smaller N, as those with a higher N have to allocate more in a + // single map. + const maxValues = 1 << 15 // 32768 + + var v int + var ok bool + _, _ = v, ok + + b.Run("stack1", func(b *testing.B) { + src := GoMap[int, int](make(map[int]int)) + st := Wrap(src) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + st.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = st.Get(k) + + if k == maxValues-1 { + st = Wrap(src) + } + } + }) + b.Run("wrapped", func(b *testing.B) { + src := GoMap[int, int](make(map[int]int)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + src.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = src.Get(k) + + if k == maxValues-1 { + src = GoMap[int, int](make(map[int]int)) + } + } + }) + b.Run("baseline", func(b *testing.B) { + // this serves to have a baseline value in the benchmark results + // for when we just use a map directly. + m := make(map[int]int) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + m[k] = i + v, ok = m[k] + + if k == maxValues-1 { + m = make(map[int]int) + } + } + }) +} + +func Benchmark_bufferedTxMapRead(b *testing.B) { + const maxValues = (1 << 10) * 9 // must be multiple of 9 + + var ( + baseMap = make(map[int]int) // all values filled + wrapped = bufferedTxMap[int, int]{source: baseMap} + stack1 = wrapped.buffered() // n, n+1, n+4, n+7 values filled (n%9 == 0) + // this test doesn't have stack2 as bufferedTxMap + // does not support stacking + ) + + for i := 0; i < maxValues; i++ { + baseMap[i] = i + switch i % 9 { + case 0, 1, 4, 7: + stack1.Set(i, i+1_000_000) + } + } + + var v int + var ok bool + _, _ = v, ok + + b.Run("stack1", func(b *testing.B) { + for i := 0; i < b.N; i++ { + // use assignment to avoid the compiler optimizing out the loops + v, ok = stack1.Get(i % maxValues) + } + }) + b.Run("wrapped", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = wrapped.Get(i % maxValues) + } + }) + b.Run("baseline", func(b *testing.B) { + for i := 0; i < b.N; i++ { + v, ok = baseMap[i%maxValues] + } + }) +} + +func Benchmark_bufferedTxMapWrite(b *testing.B) { + // after this amount of values, the maps are re-initialized. + // you can tweak this to see how the benchmarks behave on a variety of + // values. + // NOTE: setting this too high will skew the benchmark in favour those which + // have a smaller N, as those with a higher N have to allocate more in a + // single map. + const maxValues = 1 << 15 // 32768 + + var v int + var ok bool + _, _ = v, ok + + b.Run("buffered", func(b *testing.B) { + var orig bufferedTxMap[int, int] + orig.init() + txm := orig.buffered() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + txm.Set(k, i) + // we use this assignment to prevent the compiler from optimizing + // out code, especially in the baseline case. + v, ok = txm.Get(k) + + if k == maxValues-1 { + txm = orig.buffered() + } + } + }) + b.Run("unbuffered", func(b *testing.B) { + var txm bufferedTxMap[int, int] + txm.init() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + txm.Set(k, i) + v, ok = txm.Get(k) + + if k == maxValues-1 { + txm.init() + } + } + }) + b.Run("baseline", func(b *testing.B) { + // this serves to have a baseline value in the benchmark results + // for when we just use a map directly. + m := make(map[int]int) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + k := i % maxValues + + m[k] = i + v, ok = m[k] + + if k == maxValues-1 { + m = make(map[int]int) + } + } + }) +} diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 35a26e11c04..155dce6088f 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/txlog" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/colors" "github.com/gnolang/gno/tm2/pkg/std" @@ -116,109 +117,6 @@ type hashMap[K comparable, V any] interface { Iterate() func(yield func(K, V) bool) } -type txLogMap[K comparable, V any] struct { - source hashMap[K, V] - dirty map[K]deletable[V] -} - -func newTxLog[K comparable, V any](source hashMap[K, V]) *txLogMap[K, V] { - return &txLogMap[K, V]{ - source: source, - dirty: make(map[K]deletable[V]), - } -} - -// write commits the data in dirty to the map in source. -func (b *txLogMap[K, V]) write() { - for k, v := range b.dirty { - if v.deleted { - b.source.Delete(k) - } else { - b.source.Set(k, v.v) - } - } - b.dirty = make(map[K]deletable[V]) -} - -func (b txLogMap[K, V]) Get(k K) (V, bool) { - if bufValue, ok := b.dirty[k]; ok { - if bufValue.deleted { - var zeroV V - return zeroV, false - } - return bufValue.v, true - } - - return b.source.Get(k) -} - -func (b txLogMap[K, V]) Set(k K, v V) { - b.dirty[k] = deletable[V]{v: v} -} - -func (b txLogMap[K, V]) Delete(k K) { - b.dirty[k] = deletable[V]{deleted: true} -} - -func (b txLogMap[K, V]) Iterate() func(yield func(K, V) bool) { - return func(yield func(K, V) bool) { - b.source.Iterate()(func(k K, v V) bool { - if dirty, ok := b.dirty[k]; ok { - if dirty.deleted { - return true - } - return yield(k, dirty.v) - } - - // not in dirty - return yield(k, v) - }) - // yield for new values - for k, v := range b.dirty { - if v.deleted { - continue - } - _, ok := b.source.Get(k) - if ok { - continue - } - if !yield(k, v.v) { - break - } - } - } -} - -type mapWrapper[K comparable, V any] map[K]V - -func (m mapWrapper[K, V]) Get(k K) (V, bool) { - v, ok := m[k] - return v, ok -} - -func (m mapWrapper[K, V]) Set(k K, v V) { - m[k] = v -} - -func (m mapWrapper[K, V]) Delete(k K) { - delete(m, k) -} - -func (m mapWrapper[K, V]) Iterate() func(yield func(K, V) bool) { - return func(yield func(K, V) bool) { - for k, v := range m { - if !yield(k, v) { - return - } - } - } -} - -type deletable[V any] struct { - v V - deleted bool -} - func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore { ds := &defaultStore{ baseStore: baseStore, @@ -227,8 +125,8 @@ func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore // cacheObjects is set; objects in the store will be copied over for any transaction. cacheObjects: make(map[ObjectID]Object), - cacheTypes: mapWrapper[TypeID, Type](map[TypeID]Type{}), - cacheNodes: mapWrapper[Location, BlockNode](map[Location]BlockNode{}), + cacheTypes: txlog.GoMap[TypeID, Type](map[TypeID]Type{}), + cacheNodes: txlog.GoMap[Location, BlockNode](map[Location]BlockNode{}), // store configuration pkgGetter: nil, @@ -256,8 +154,8 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans // transaction-scoped cacheObjects: make(map[ObjectID]Object), - cacheTypes: newTxLog(ds.cacheTypes), - cacheNodes: newTxLog(ds.cacheNodes), + cacheTypes: txlog.Wrap(ds.cacheTypes), + cacheNodes: txlog.Wrap(ds.cacheNodes), alloc: ds.alloc.Fork().Reset(), // store configuration @@ -279,8 +177,8 @@ func (ds *defaultStore) BeginTransaction(baseStore, iavlStore store.Store) Trans type transactionStore struct{ *defaultStore } func (t transactionStore) Write() { - t.cacheTypes.(*txLogMap[TypeID, Type]).write() - t.cacheNodes.(*txLogMap[Location, BlockNode]).write() + t.cacheTypes.(txlog.MapCommitter[TypeID, Type]).Commit() + t.cacheNodes.(txlog.MapCommitter[Location, BlockNode]).Commit() } func (transactionStore) SetPackageGetter(pg PackageGetter) { @@ -936,8 +834,8 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { func (ds *defaultStore) ClearCache() { ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes = mapWrapper[TypeID, Type](map[TypeID]Type{}) - ds.cacheNodes = mapWrapper[Location, BlockNode](map[Location]BlockNode{}) + ds.cacheTypes = txlog.GoMap[TypeID, Type](map[TypeID]Type{}) + ds.cacheNodes = txlog.GoMap[Location, BlockNode](map[Location]BlockNode{}) ds.cacheNativeTypes = make(map[reflect.Type]Type) // restore builtin types to cache. InitStoreCaches(ds) diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index d86df727a06..94255b1e0ef 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -1,512 +1 @@ package gnolang - -import ( - "maps" - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_txLogMap(t *testing.T) { - t.Parallel() - - type Value = struct{} - - // Full "integration test" of the txLogMap + mapwrapper. - source := mapWrapper[int, *Value](map[int]*Value{}) - - // create 4 empty values (we'll just use the pointers) - vs := [...]*Value{ - {}, - {}, - {}, - {}, - } - source.Set(0, vs[0]) - source.Set(1, vs[1]) - source.Set(2, vs[2]) - - { - // Attempt getting, and deleting an item. - v, ok := source.Get(0) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[0] == v, "pointer returned should be ==") - - source.Delete(0) - v, ok = source.Get(0) - assert.False(t, ok, "should be unsuccessful Get") - assert.Nil(t, v, "pointer returned should be nil") - - verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[2]}) - } - - saved := maps.Clone(source) - txm := newTxLog(source) - - { - // Attempt getting, deleting an item on a buffered map; - // then creating a new one. - v, ok := txm.Get(1) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[1] == v, "pointer returned should be ==") - - txm.Delete(1) - v, ok = txm.Get(1) - assert.False(t, ok, "should be unsuccessful Get") - assert.Nil(t, v, "pointer returned should be nil") - - // Update an existing value to another value. - txm.Set(2, vs[0]) - v, ok = txm.Get(2) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[0] == v, "pointer returned should be ==") - - // Add a new value - txm.Set(3, vs[3]) - v, ok = txm.Get(3) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[3] == v, "pointer returned should be ==") - - // The original bufferedTxMap should still not know about the - // new value, and the internal "source" map should still be the - // same. - v, ok = source.Get(3) - assert.Nil(t, v) - assert.False(t, ok) - v, ok = source.Get(1) - assert.True(t, vs[1] == v) - assert.True(t, ok) - assert.Equal(t, saved, source) - assert.Equal(t, saved, txm.source) - - // double-check on the iterators. - verifyHashMapValues(t, source, map[int]*Value{1: vs[1], 2: vs[0]}) - verifyHashMapValues(t, txm, map[int]*Value{2: vs[2], 3: vs[3]}) - } - - { - // Using write() should cause bm's internal buffer to be cleared; - // and for all changes to show up on the source map. - txm.write() - assert.Empty(t, txm.dirty) - assert.Equal(t, source, txm.source) - assert.NotEqual(t, saved, source) - - v, ok := source.Get(3) - assert.True(t, vs[3] == v) - assert.True(t, ok) - v, ok = source.Get(1) - assert.Nil(t, v) - assert.False(t, ok) - - // double-check on the iterators. - verifyHashMapValues(t, source, map[int]*Value{2: vs[0], 3: vs[3]}) - verifyHashMapValues(t, txm, map[int]*Value{2: vs[0], 3: vs[3]}) - } -} - -func verifyHashMapValues(t *testing.T, m hashMap[int, *struct{}], expectedReadonly map[int]*struct{}) { - t.Helper() - - expected := maps.Clone(expectedReadonly) - m.Iterate()(func(k int, v *struct{}) bool { - ev, eok := expected[k] - _ = assert.True(t, eok, "mapping %d:%v should exist in expected map", k, v) && - assert.Equal(t, ev, v, "values should match") - delete(expected, k) - return true - }) - assert.Empty(t, expected, "(some) expected values not found in the hashMap") -} - -func Test_bufferedTxMap(t *testing.T) { - t.Parallel() - - type Value struct{} - - // Full "integration test" of the bufferedTxMap. - var m bufferedTxMap[int, *Value] - m.init() - - vs := [...]*Value{ - {}, - {}, - {}, - {}, - } - m.Set(0, vs[0]) - m.Set(1, vs[1]) - m.Set(2, vs[2]) - - { - // Attempt getting, and deleting an item. - v, ok := m.Get(0) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[0] == v, "pointer returned should be ==") - - m.Delete(0) - v, ok = m.Get(0) - assert.False(t, ok, "should be unsuccessful Get") - assert.Nil(t, v, "pointer returned should be nil") - } - - saved := maps.Clone(m.source) - bm := m.buffered() - - { - // Attempt getting, deleting an item on a buffered map; - // then creating a new one. - v, ok := bm.Get(1) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[1] == v, "pointer returned should be ==") - - bm.Delete(1) - v, ok = bm.Get(1) - assert.False(t, ok, "should be unsuccessful Get") - assert.Nil(t, v, "pointer returned should be nil") - - bm.Set(3, vs[3]) - v, ok = bm.Get(3) - assert.True(t, ok, "should be successful Get") - assert.True(t, vs[3] == v, "pointer returned should be ==") - - // The original bufferedTxMap should still not know about the - // new value, and the internal "source" map should still be the - // same. - v, ok = m.Get(3) - assert.Nil(t, v) - assert.False(t, ok) - v, ok = m.Get(1) - assert.True(t, vs[1] == v) - assert.True(t, ok) - assert.Equal(t, saved, m.source) - assert.Equal(t, saved, bm.source) - } - - { - // Using write() should cause bm's internal buffer to be cleared; - // and for all changes to show up on the source map. - bm.write() - assert.Empty(t, bm.dirty) - assert.Equal(t, m.source, bm.source) - assert.NotEqual(t, saved, m.source) - - v, ok := m.Get(3) - assert.True(t, vs[3] == v) - assert.True(t, ok) - v, ok = m.Get(1) - assert.Nil(t, v) - assert.False(t, ok) - } -} - -func Test_bufferedTxMap_initErr(t *testing.T) { - t.Parallel() - - var b bufferedTxMap[bool, bool] - b.init() - - assert.PanicsWithValue(t, "cannot init with a dirty buffer", func() { - buf := b.buffered() - buf.init() - }) -} - -func Test_bufferedTxMap_bufferedErr(t *testing.T) { - t.Parallel() - - var b bufferedTxMap[bool, bool] - b.init() - buf := b.buffered() - - assert.PanicsWithValue(t, "cannot stack multiple bufferedTxMap", func() { - buf.buffered() - }) -} - -// bufferedTxMap is a wrapper around the map type, supporting regular Get, Set -// and Delete operations. Additionally, it can create a "buffered" version of -// itself, which will keep track of all write (set and delete) operations to the -// map; so that they can all be atomically committed when calling "write". -type bufferedTxMap[K comparable, V any] struct { - source map[K]V - dirty map[K]deletable[V] -} - -// init should be called when creating the bufferedTxMap, in a non-buffered -// context. -func (b *bufferedTxMap[K, V]) init() { - if b.dirty != nil { - panic("cannot init with a dirty buffer") - } - b.source = make(map[K]V) -} - -// buffered creates a copy of b, which has a usable dirty map. -func (b bufferedTxMap[K, V]) buffered() bufferedTxMap[K, V] { - if b.dirty != nil { - panic("cannot stack multiple bufferedTxMap") - } - return bufferedTxMap[K, V]{ - source: b.source, - dirty: make(map[K]deletable[V]), - } -} - -// write commits the data in dirty to the map in source. -func (b *bufferedTxMap[K, V]) write() { - for k, v := range b.dirty { - if v.deleted { - delete(b.source, k) - } else { - b.source[k] = v.v - } - } - b.dirty = make(map[K]deletable[V]) -} - -func (b bufferedTxMap[K, V]) Get(k K) (V, bool) { - if b.dirty != nil { - if bufValue, ok := b.dirty[k]; ok { - if bufValue.deleted { - var zeroV V - return zeroV, false - } - return bufValue.v, true - } - } - v, ok := b.source[k] - return v, ok -} - -func (b bufferedTxMap[K, V]) Set(k K, v V) { - if b.dirty == nil { - b.source[k] = v - return - } - b.dirty[k] = deletable[V]{v: v} -} - -func (b bufferedTxMap[K, V]) Delete(k K) { - if b.dirty == nil { - delete(b.source, k) - return - } - b.dirty[k] = deletable[V]{deleted: true} -} - -func Benchmark_txLogMapRead(b *testing.B) { - const maxValues = (1 << 10) * 9 // must be multiple of 9 - - var ( - baseMap = make(map[int]int) // all values filled - wrapped = mapWrapper[int, int](baseMap) // wrapper around baseMap - stack1 = newTxLog(wrapped) // n+1, n+4, n+7 values filled (n%9 == 0) - stack2 = newTxLog(stack1) // n'th values filled (n%9 == 0) - ) - - for i := 0; i < maxValues; i++ { - baseMap[i] = i - switch i % 9 { - case 1, 4, 7: - stack1.Set(i, i+1_000_000) - case 0: - stack2.Set(i, i+10_000_000) - } - } - - var v int - var ok bool - _, _ = v, ok - - // through closure, so func calls have to go through "indirection". - runbench := func(b *testing.B, src hashMap[int, int]) { //nolint:thelper - for i := 0; i < b.N; i++ { - v, ok = src.Get(i % maxValues) - } - } - - b.Run("stack2", func(b *testing.B) { runbench(b, stack2) }) - b.Run("stack1", func(b *testing.B) { runbench(b, stack1) }) - b.Run("wrapped", func(b *testing.B) { runbench(b, wrapped) }) - b.Run("baseline", func(b *testing.B) { - for i := 0; i < b.N; i++ { - v, ok = baseMap[i%maxValues] - } - }) -} - -func Benchmark_txLogMapWrite(b *testing.B) { - // after this amount of values, the maps are re-initialized. - // you can tweak this to see how the benchmarks behave on a variety of - // values. - // NOTE: setting this too high will skew the benchmark in favour those which - // have a smaller N, as those with a higher N have to allocate more in a - // single map. - const maxValues = 1 << 15 // 32768 - - var v int - var ok bool - _, _ = v, ok - - b.Run("stack1", func(b *testing.B) { - src := mapWrapper[int, int](make(map[int]int)) - st := newTxLog(src) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - st.Set(k, i) - // we use this assignment to prevent the compiler from optimizing - // out code, especially in the baseline case. - v, ok = st.Get(k) - - if k == maxValues-1 { - st = newTxLog(src) - } - } - }) - b.Run("wrapped", func(b *testing.B) { - src := mapWrapper[int, int](make(map[int]int)) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - src.Set(k, i) - // we use this assignment to prevent the compiler from optimizing - // out code, especially in the baseline case. - v, ok = src.Get(k) - - if k == maxValues-1 { - src = mapWrapper[int, int](make(map[int]int)) - } - } - }) - b.Run("baseline", func(b *testing.B) { - // this serves to have a baseline value in the benchmark results - // for when we just use a map directly. - m := make(map[int]int) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - m[k] = i - v, ok = m[k] - - if k == maxValues-1 { - m = make(map[int]int) - } - } - }) -} - -func Benchmark_bufferedTxMapRead(b *testing.B) { - const maxValues = (1 << 10) * 9 // must be multiple of 9 - - var ( - baseMap = make(map[int]int) // all values filled - wrapped = bufferedTxMap[int, int]{source: baseMap} - stack1 = wrapped.buffered() // n, n+1, n+4, n+7 values filled (n%9 == 0) - // this test doesn't have stack2 as bufferedTxMap - // does not support stacking - ) - - for i := 0; i < maxValues; i++ { - baseMap[i] = i - switch i % 9 { - case 0, 1, 4, 7: - stack1.Set(i, i+1_000_000) - } - } - - var v int - var ok bool - _, _ = v, ok - - b.Run("stack1", func(b *testing.B) { - for i := 0; i < b.N; i++ { - // use assignment to avoid the compiler optimizing out the loops - v, ok = stack1.Get(i % maxValues) - } - }) - b.Run("wrapped", func(b *testing.B) { - for i := 0; i < b.N; i++ { - v, ok = wrapped.Get(i % maxValues) - } - }) - b.Run("baseline", func(b *testing.B) { - for i := 0; i < b.N; i++ { - v, ok = baseMap[i%maxValues] - } - }) -} - -func Benchmark_bufferedTxMapWrite(b *testing.B) { - // after this amount of values, the maps are re-initialized. - // you can tweak this to see how the benchmarks behave on a variety of - // values. - // NOTE: setting this too high will skew the benchmark in favour those which - // have a smaller N, as those with a higher N have to allocate more in a - // single map. - const maxValues = 1 << 15 // 32768 - - var v int - var ok bool - _, _ = v, ok - - b.Run("buffered", func(b *testing.B) { - var orig bufferedTxMap[int, int] - orig.init() - txm := orig.buffered() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - txm.Set(k, i) - // we use this assignment to prevent the compiler from optimizing - // out code, especially in the baseline case. - v, ok = txm.Get(k) - - if k == maxValues-1 { - txm = orig.buffered() - } - } - }) - b.Run("unbuffered", func(b *testing.B) { - var txm bufferedTxMap[int, int] - txm.init() - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - txm.Set(k, i) - v, ok = txm.Get(k) - - if k == maxValues-1 { - txm.init() - } - } - }) - b.Run("baseline", func(b *testing.B) { - // this serves to have a baseline value in the benchmark results - // for when we just use a map directly. - m := make(map[int]int) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - k := i % maxValues - - m[k] = i - v, ok = m[k] - - if k == maxValues-1 { - m = make(map[int]int) - } - } - }) -} From cf760811254f243e489d3057dd4d29a855f1ae61 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 19:38:33 +0200 Subject: [PATCH 30/45] some consistency --- tm2/pkg/sdk/abci.go | 3 ++- tm2/pkg/sdk/baseapp.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/sdk/abci.go b/tm2/pkg/sdk/abci.go index 4857541ea41..0b86518f0b9 100644 --- a/tm2/pkg/sdk/abci.go +++ b/tm2/pkg/sdk/abci.go @@ -18,7 +18,8 @@ type BeginBlocker func(ctx Context, req abci.RequestBeginBlock) abci.ResponseBeg type EndBlocker func(ctx Context, req abci.RequestEndBlock) abci.ResponseEndBlock // BeginTxHook is a BaseApp-specific hook, called to modify the context with any -// additional application-specific information. +// additional application-specific information, before running the messages in a +// transaction. type BeginTxHook func(ctx Context) Context // EndTxHook is a BaseApp-specific hook, called after all the messages in a diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 386a5fb206f..867a38d680a 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -43,7 +43,7 @@ type BaseApp struct { endBlocker EndBlocker // logic to run after all txs, and to determine valset changes beginTxHook BeginTxHook // BaseApp-specific hook run before running transaction messages. - endTxHook EndTxHook // BaseApp-specific hook run after a transaction + endTxHook EndTxHook // BaseApp-specific hook run after running transaction messages. // -------------------- // Volatile state From 00297d1188ff66bdcec2daea372d4e8c95da15f4 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 19:40:04 +0200 Subject: [PATCH 31/45] remove hashMap data type --- gnovm/pkg/gnolang/store.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 155dce6088f..58e1bb156b9 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -93,10 +93,10 @@ type defaultStore struct { iavlStore store.Store // for escaped object hashes // transaction-scoped - cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. - cacheTypes hashMap[TypeID, Type] // this re-uses the parent store's. - cacheNodes hashMap[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. - alloc *Allocator // for accounting for cached items + cacheObjects map[ObjectID]Object // this is a real cache, reset with every transaction. + cacheTypes txlog.Map[TypeID, Type] // this re-uses the parent store's. + cacheNodes txlog.Map[Location, BlockNode] // until BlockNode persistence is implemented, this is an actual store. + alloc *Allocator // for accounting for cached items // store configuration; cannot be modified in a transaction pkgGetter PackageGetter // non-realm packages @@ -110,13 +110,6 @@ type defaultStore struct { opslog []StoreOp // for debugging and testing. } -type hashMap[K comparable, V any] interface { - Get(K) (V, bool) - Set(K, V) - Delete(K) - Iterate() func(yield func(K, V) bool) -} - func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore { ds := &defaultStore{ baseStore: baseStore, From bd3bf2192c277f05685c81a27fb66cf59f1dfdc9 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 23 Aug 2024 20:29:53 +0200 Subject: [PATCH 32/45] more tests --- gnovm/pkg/gnolang/store.go | 1 - gnovm/pkg/gnolang/store_test.go | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 58e1bb156b9..3f2ce30e637 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -503,7 +503,6 @@ func (ds *defaultStore) DelObject(oo Object) { func (ds *defaultStore) GetType(tid TypeID) Type { tt := ds.GetTypeSafe(tid) if tt == nil { - ds.Print() panic(fmt.Sprintf("unexpected type with id %s", tid.String())) } return tt diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 94255b1e0ef..8d480de4b58 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -1 +1,65 @@ package gnolang + +import ( + "io" + "testing" + + "github.com/gnolang/gno/tm2/pkg/db/memdb" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/store/dbadapter" + "github.com/gnolang/gno/tm2/pkg/store/types" + "github.com/stretchr/testify/assert" +) + +func TestTransactionStore(t *testing.T) { + db := memdb.NewMemDB() + tm2Store := dbadapter.StoreConstructor(db, types.StoreOptions{}) + + st := NewStore(nil, tm2Store, tm2Store) + wrappedTm2Store := tm2Store.CacheWrap() + txSt := st.BeginTransaction(wrappedTm2Store, wrappedTm2Store) + m := NewMachineWithOptions(MachineOptions{ + PkgPath: "hello", + Store: txSt, + Output: io.Discard, + }) + _, pv := m.RunMemPackage(&std.MemPackage{ + Name: "hello", + Path: "hello", + Files: []*std.MemFile{ + {Name: "hello.gno", Body: "package hello; func main() { println(A(11)); }; type A int"}, + }, + }, true) + m.SetActivePackage(pv) + m.RunMain() + + // mem package should only exist in txSt + // (check both memPackage and types - one is stored directly in the db, + // the other uses txlog) + assert.Nil(t, st.GetMemPackage("hello")) + assert.NotNil(t, txSt.GetMemPackage("hello")) + assert.PanicsWithValue(t, "unexpected type with id hello.A", func() { st.GetType("hello.A") }) + assert.NotNil(t, txSt.GetType("hello.A")) + + // use write on the stores + txSt.Write() + wrappedTm2Store.Write() + + // mem package should exist and be ==. + res := st.GetMemPackage("hello") + assert.NotNil(t, res) + assert.Equal(t, txSt.GetMemPackage("hello"), res) + helloA := st.GetType("hello.A") + assert.NotNil(t, helloA) + assert.Equal(t, txSt.GetType("hello.A"), helloA) +} + +func TestTransactionStore_blockedMethods(t *testing.T) { + // These methods should panic as they modify store settings, which should + // only be changed in the root store. + assert.Panics(t, func() { transactionStore{}.SetPackageGetter(nil) }) + assert.Panics(t, func() { transactionStore{}.ClearCache() }) + assert.Panics(t, func() { transactionStore{}.SetPackageInjector(nil) }) + assert.Panics(t, func() { transactionStore{}.SetNativeStore(nil) }) + assert.Panics(t, func() { transactionStore{}.SetStrictGo2GnoMapping(false) }) +} From 5b9d8d5f1ace014ca969925f982fe074f9aece1a Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 26 Aug 2024 18:33:59 +0200 Subject: [PATCH 33/45] more comments / examples on txlog --- gnovm/pkg/gnolang/internal/txlog/txlog.go | 18 +++++--- .../pkg/gnolang/internal/txlog/txlog_test.go | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/gnovm/pkg/gnolang/internal/txlog/txlog.go b/gnovm/pkg/gnolang/internal/txlog/txlog.go index e582cf1b8ec..cda11083672 100644 --- a/gnovm/pkg/gnolang/internal/txlog/txlog.go +++ b/gnovm/pkg/gnolang/internal/txlog/txlog.go @@ -3,7 +3,8 @@ // data type implementing [Map]). // // A transaction log keeps track of the write operations performed in a -// transaction +// transaction, so that they can be committed together, atomically, +// when calling [MapCommitter.Commit]. package txlog // Map is a generic interface to a key/value map, like Go's builtin map. @@ -59,15 +60,19 @@ func (m GoMap[K, V]) Iterate() func(yield func(K, V) bool) { // Wrap wraps the map m into a data structure to keep a transaction log. // To write data to m, use MapCommitter.Commit. func Wrap[K comparable, V any](m Map[K, V]) MapCommitter[K, V] { - return &txLog[K, V]{source: m, dirty: make(map[K]deletable[V])} + return &txLog[K, V]{ + source: m, + dirty: make(map[K]deletable[V]), + } } type txLog[K comparable, V any] struct { - source Map[K, V] - dirty map[K]deletable[V] + source Map[K, V] // read-only until Commit() + dirty map[K]deletable[V] // pending writes on source } func (b *txLog[K, V]) Commit() { + // copy from b.dirty into b.source; clean b.dirty for k, v := range b.dirty { if v.deleted { b.source.Delete(k) @@ -100,6 +105,8 @@ func (b txLog[K, V]) Delete(k K) { func (b txLog[K, V]) Iterate() func(yield func(K, V) bool) { return func(yield func(K, V) bool) { + // go through b.source; skip deleted values, and use updated values + // for those which exist in b.dirty. b.source.Iterate()(func(k K, v V) bool { if dirty, ok := b.dirty[k]; ok { if dirty.deleted { @@ -111,7 +118,8 @@ func (b txLog[K, V]) Iterate() func(yield func(K, V) bool) { // not in dirty return yield(k, v) }) - // yield for new values + + // iterate over all "new" values (ie. exist in b.dirty but not b.source). for k, v := range b.dirty { if v.deleted { continue diff --git a/gnovm/pkg/gnolang/internal/txlog/txlog_test.go b/gnovm/pkg/gnolang/internal/txlog/txlog_test.go index cc68df159c3..b0780fc8380 100644 --- a/gnovm/pkg/gnolang/internal/txlog/txlog_test.go +++ b/gnovm/pkg/gnolang/internal/txlog/txlog_test.go @@ -1,12 +1,53 @@ package txlog import ( + "fmt" "maps" "testing" "github.com/stretchr/testify/assert" ) +func ExampleWrap() { + type User struct { + ID int + Name string + } + m := GoMap[int, User](map[int]User{ + 1: {ID: 1, Name: "alice"}, + 2: {ID: 2, Name: "bob"}, + }) + + // Wrap m in a transaction log. + txl := Wrap(m) + txl.Set(2, User{ID: 2, Name: "carl"}) + + // m will still have bob, while txl will have carl. + fmt.Println("m.Get(2):") + fmt.Println(m.Get(2)) + fmt.Println("txl.Get(2):") + fmt.Println(txl.Get(2)) + + // after txl.Commit(), the transaction log will be committed to m. + txl.Commit() + fmt.Println("--- commit ---") + fmt.Println("m.Get(2):") + fmt.Println(m.Get(2)) + fmt.Println("txl.Get(2):") + fmt.Println(txl.Get(2)) + + // Output: + // m.Get(2): + // {2 bob} true + // txl.Get(2): + // {2 carl} true + // --- commit --- + // m.Get(2): + // {2 carl} true + // txl.Get(2): + // {2 carl} true +} + func Test_txLog(t *testing.T) { t.Parallel() From 8eb0ce4e6833becbcd3a2bdd342289a72fbcf867 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 26 Aug 2024 19:16:59 +0200 Subject: [PATCH 34/45] add gnoland start -non-validator; and failing test. --- .../cmd/gnoland/testdata/restart_nonval.txtar | 5 +++++ gno.land/pkg/integration/doc.go | 1 + .../pkg/integration/testing_integration.go | 20 +++++++++++++++++ gno.land/pkg/integration/testing_node.go | 17 ++++++++++---- tm2/pkg/sdk/context.go | 22 +++++-------------- 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/restart_nonval.txtar diff --git a/gno.land/cmd/gnoland/testdata/restart_nonval.txtar b/gno.land/cmd/gnoland/testdata/restart_nonval.txtar new file mode 100644 index 00000000000..87b4ad4ecb9 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/restart_nonval.txtar @@ -0,0 +1,5 @@ +# This txtar tests for starting up a non-validator node; then also restarting it. +loadpkg gno.land/p/demo/avl + +gnoland start -non-validator +gnoland restart diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go index 9be4be0af07..ef3ed9923da 100644 --- a/gno.land/pkg/integration/doc.go +++ b/gno.land/pkg/integration/doc.go @@ -13,6 +13,7 @@ // pre-configuration or pass custom arguments to the start command. // - `gnoland restart` will simulate restarting a node, as in stopping and // starting it again, recovering state from the persisted database data. +// - `gnoland start -non-validator` can be used to start a node as a non-validator node. // // 2. `gnokey`: // - Supports most of the common commands. diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 56c2a655888..6cf936518f8 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -3,6 +3,7 @@ package integration import ( "context" "errors" + "flag" "fmt" "hash/crc32" "log/slog" @@ -172,6 +173,13 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { break } + // parse flags + fs := flag.NewFlagSet("start", flag.ContinueOnError) + nonVal := fs.Bool("non-validator", false, "set up node as a non-validator") + if err := fs.Parse(args); err != nil { + ts.Fatalf("unable to parse `gnoland start` flags: %s", err) + } + // get packages pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) // grab logger creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 @@ -191,6 +199,18 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // setup genesis state cfg.Genesis.AppState = *genesis + if *nonVal { + pv := gnoland.NewMockedPrivValidator() + // remove address from cfg.Genesis.Validators, to start as a non-val. + cfg.Genesis.Validators = []bft.GenesisValidator{ + { + Address: pv.GetPubKey().Address(), + PubKey: pv.GetPubKey(), + Power: 10, + Name: "none", + }, + } + } cfg.DB = memdb.NewMemDB() // so it can be reused when restarting. n, remoteAddr := TestingInMemoryNode(t, logger, cfg) diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 64a7c59ffc1..db412b516a9 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -3,6 +3,7 @@ package integration import ( "log/slog" "path/filepath" + "slices" "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -32,10 +33,18 @@ func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMem err = node.Start() require.NoError(t, err) - select { - case <-node.Ready(): - case <-time.After(time.Second * 10): - require.FailNow(t, "timeout while waiting for the node to start") + ourAddress := config.PrivValidator.GetPubKey().Address() + isValidator := slices.ContainsFunc(config.Genesis.Validators, func(val bft.GenesisValidator) bool { + return val.Address == ourAddress + }) + + // Wait for first block if we are a validator. + if isValidator { + select { + case <-node.Ready(): + case <-time.After(time.Second * 10): + require.FailNow(t, "timeout while waiting for the node to start") + } } return node, node.Config().RPC.ListenAddress diff --git a/tm2/pkg/sdk/context.go b/tm2/pkg/sdk/context.go index 0e1021e0174..63c5a50f8eb 100644 --- a/tm2/pkg/sdk/context.go +++ b/tm2/pkg/sdk/context.go @@ -147,31 +147,21 @@ func (c Context) WithEventLogger(em *EventLogger) Context { return c } -// WithValue is deprecated, provided for backwards compatibility -// Please use +// WithValue is shorthand for: // -// ctx = ctx.WithContext(context.WithValue(ctx.Context(), key, false)) +// c.WithContext(context.WithValue(c.Context(), key, value)) // -// instead of -// -// ctx = ctx.WithValue(key, false) -// -// NOTE: why? +// It adds a value to the [context.Context]. func (c Context) WithValue(key, value interface{}) Context { c.ctx = context.WithValue(c.ctx, key, value) return c } -// Value is deprecated, provided for backwards compatibility -// Please use -// -// ctx.Context().Value(key) -// -// instead of +// Value is shorthand for: // -// ctx.Value(key) +// c.Context().Value(key) // -// NOTE: why? +// It retrieves a value from the [context.Context]. func (c Context) Value(key interface{}) interface{} { return c.ctx.Value(key) } From 7109b8a3491a96b7966508dcee64f630c75e7cce Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 26 Aug 2024 19:29:01 +0200 Subject: [PATCH 35/45] fix bug reported by milos --- gno.land/pkg/gnoland/app.go | 13 ++++++++++--- gno.land/pkg/integration/testing_integration.go | 3 ++- gno.land/pkg/integration/testing_node.go | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 84326f4d793..f2efaa81aa6 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -229,16 +229,23 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh { // load standard libraries - // need to write to the MultiStore directly - so that the standard - // libraries are available when we process genesis txs + // we write it to store right after loading the libs so that they are + // available when loading genesis txs. + // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, + // this is done in BaseApp.cacheTxContext; so we replicate it here. + ms := ctx.MultiStore() + msCache := ms.MultiCacheWrap() + stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) + stdlibCtx = stdlibCtx.WithMultiStore(msCache) if cfg.CacheStdlibLoad { cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) } else { cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) } cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) - stdlibCtx.MultiStore().MultiWrite() + + msCache.MultiWrite() } ctx.Logger().Debug("InitChainer: standard libraries loaded", diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 6cf936518f8..d3f55cfadf7 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -200,8 +200,9 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // setup genesis state cfg.Genesis.AppState = *genesis if *nonVal { + // re-create cfg.Genesis.Validators with a throwaway pv, so we start as a + // non-validator. pv := gnoland.NewMockedPrivValidator() - // remove address from cfg.Genesis.Validators, to start as a non-val. cfg.Genesis.Validators = []bft.GenesisValidator{ { Address: pv.GetPubKey().Address(), diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index db412b516a9..5e9e2272049 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -39,6 +39,7 @@ func TestingInMemoryNode(t TestingTS, logger *slog.Logger, config *gnoland.InMem }) // Wait for first block if we are a validator. + // If we are not a validator, we don't produce blocks, so node.Ready() hangs. if isValidator { select { case <-node.Ready(): From b00508f601e45370b5734b5ec64112c00f7116a5 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 26 Aug 2024 20:01:03 +0200 Subject: [PATCH 36/45] add test for CopyFromCachedStore --- gnovm/pkg/gnolang/store_test.go | 38 +++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go index 8d480de4b58..8114291d1b6 100644 --- a/gnovm/pkg/gnolang/store_test.go +++ b/gnovm/pkg/gnolang/store_test.go @@ -7,13 +7,13 @@ import ( "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/types" + storetypes "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/stretchr/testify/assert" ) func TestTransactionStore(t *testing.T) { db := memdb.NewMemDB() - tm2Store := dbadapter.StoreConstructor(db, types.StoreOptions{}) + tm2Store := dbadapter.StoreConstructor(db, storetypes.StoreOptions{}) st := NewStore(nil, tm2Store, tm2Store) wrappedTm2Store := tm2Store.CacheWrap() @@ -63,3 +63,37 @@ func TestTransactionStore_blockedMethods(t *testing.T) { assert.Panics(t, func() { transactionStore{}.SetNativeStore(nil) }) assert.Panics(t, func() { transactionStore{}.SetStrictGo2GnoMapping(false) }) } + +func TestCopyFromCachedStore(t *testing.T) { + // Create cached store, with a type and a mempackage. + c1 := memdb.NewMemDB() + c1s := dbadapter.StoreConstructor(c1, storetypes.StoreOptions{}) + c2 := memdb.NewMemDB() + c2s := dbadapter.StoreConstructor(c2, storetypes.StoreOptions{}) + cachedStore := NewStore(nil, c1s, c2s) + cachedStore.SetType(&DeclaredType{ + PkgPath: "io", + Name: "Reader", + Base: BoolType, + }) + cachedStore.AddMemPackage(&std.MemPackage{ + Name: "math", + Path: "math", + Files: []*std.MemFile{ + {Name: "math.gno", Body: "package math"}, + }, + }) + + // Create dest store and copy. + d1, d2 := memdb.NewMemDB(), memdb.NewMemDB() + d1s := dbadapter.StoreConstructor(d1, storetypes.StoreOptions{}) + d2s := dbadapter.StoreConstructor(d2, storetypes.StoreOptions{}) + destStore := NewStore(nil, d1s, d2s) + destStoreTx := destStore.BeginTransaction(nil, nil) // CopyFromCachedStore requires a tx store. + CopyFromCachedStore(destStoreTx, cachedStore, c1s, c2s) + destStoreTx.Write() + + assert.Equal(t, c1, d1, "cached baseStore and dest baseStore should match") + assert.Equal(t, c2, d2, "cached iavlStore and dest iavlStore should match") + assert.Equal(t, cachedStore.cacheTypes, destStore.cacheTypes, "cacheTypes should match") +} From d16d560383fdb755efbe2e03e8f973838c4de9bd Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 26 Aug 2024 20:10:42 +0200 Subject: [PATCH 37/45] add a test for NewApp --- gno.land/pkg/gnoland/app_test.go | 30 +++++++++++++++++++++------ gno.land/pkg/gnoland/node_inmemory.go | 16 ++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index c34b467de26..4bd7c2eb0ae 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -50,12 +50,7 @@ func TestNewAppWithOptions(t *testing.T) { Time: time.Now(), ChainID: "dev", ConsensusParams: &abci.ConsensusParams{ - Block: &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms - }, + Block: defaultBlockParams(), }, Validators: []abci.ValidatorUpdate{}, AppState: GnoGenesisState{ @@ -107,6 +102,29 @@ func TestNewAppWithOptions_ErrNoDB(t *testing.T) { assert.ErrorContains(t, err, "no db provided") } +func TestNewApp(t *testing.T) { + // NewApp should have good defaults and manage to run InitChain. + td := t.TempDir() + + app, err := NewApp(td, true, nil, nil) + require.NoError(t, err, "NewApp should be successful") + + resp := app.InitChain(abci.RequestInitChain{ + RequestBase: abci.RequestBase{}, + Time: time.Time{}, + ChainID: "dev", + ConsensusParams: &abci.ConsensusParams{ + Block: defaultBlockParams(), + Validator: &abci.ValidatorParams{ + PubKeyTypeURLs: []string{}, + }, + }, + Validators: []abci.ValidatorUpdate{}, + AppState: nil, + }) + assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) +} + // Test whether InitChainer calls to load the stdlibs correctly. func TestInitChainer_LoadStdlib(t *testing.T) { t.Parallel() diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index edc9a24118a..d168c955607 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -41,12 +41,7 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { GenesisTime: time.Now(), ChainID: chainid, ConsensusParams: abci.ConsensusParams{ - Block: &abci.BlockParams{ - MaxTxBytes: 1_000_000, // 1MB, - MaxDataBytes: 2_000_000, // 2MB, - MaxGas: 100_000_000, // 100M gas - TimeIotaMS: 100, // 100ms - }, + Block: defaultBlockParams(), }, AppState: &GnoGenesisState{ Balances: []Balance{}, @@ -55,6 +50,15 @@ func NewDefaultGenesisConfig(chainid string) *bft.GenesisDoc { } } +func defaultBlockParams() *abci.BlockParams { + return &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 100_000_000, // 100M gas + TimeIotaMS: 100, // 100ms + } +} + func NewDefaultTMConfig(rootdir string) *tmcfg.Config { // We use `TestConfig` here otherwise ChainID will be empty, and // there is no other way to update it than using a config file From df9d70805f1c9c4aafd2f4e1ab47b8df4eaeb933 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 27 Aug 2024 23:07:29 +0200 Subject: [PATCH 38/45] split up --- gno.land/pkg/gnoland/app.go | 124 ++++++++++++++++++++---------------- gnovm/pkg/gnolang/store.go | 3 +- 2 files changed, 71 insertions(+), 56 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index f2efaa81aa6..9fd8b1b07cb 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -227,68 +227,29 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh start := time.Now() ctx.Logger().Debug("InitChainer: started") - { - // load standard libraries - // we write it to store right after loading the libs so that they are - // available when loading genesis txs. - // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, - // this is done in BaseApp.cacheTxContext; so we replicate it here. - ms := ctx.MultiStore() - msCache := ms.MultiCacheWrap() - - stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) - stdlibCtx = stdlibCtx.WithMultiStore(msCache) - if cfg.CacheStdlibLoad { - cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) - } else { - cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) - } - cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) - - msCache.MultiWrite() - } - + // load standard libraries; immediately committed to store so that they are + // available for use when processing the genesis transactions below. + cfg.loadStdlibs(ctx) ctx.Logger().Debug("InitChainer: standard libraries loaded", "elapsed", time.Since(start)) - txResponses := []abci.ResponseDeliverTx{} - + // load app state. AppState may be nil mostly in some minimal testing setups; + // so log a warning when that happens. + var txResponses []abci.ResponseDeliverTx if req.AppState != nil { - // Get genesis state - genState := req.AppState.(GnoGenesisState) - - // Parse and set genesis state balances - for _, bal := range genState.Balances { - acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) - cfg.acctKpr.SetAccount(ctx, acc) - err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount) - if err != nil { - panic(err) + genState, ok := req.AppState.(GnoGenesisState) + if !ok { + return abci.ResponseInitChain{ + ResponseBase: abci.ResponseBase{ + Error: abci.StringError(fmt.Sprintf("invalid AppState of type %T", req.AppState)), + }, } } - - // Run genesis txs - for _, tx := range genState.Txs { - res := cfg.baseApp.Deliver(tx) - if res.IsErr() { - ctx.Logger().Error( - "Unable to deliver genesis tx", - "log", res.Log, - "error", res.Error, - "gas-used", res.GasUsed, - ) - } - - txResponses = append(txResponses, abci.ResponseDeliverTx{ - ResponseBase: res.ResponseBase, - GasWanted: res.GasWanted, - GasUsed: res.GasUsed, - }) - - cfg.GenesisTxResultHandler(ctx, tx, res) - } + txResponses = cfg.loadAppState(ctx, genState) + } else { + ctx.Logger().Warn("InitChainer: initializing chain without AppState (no genesis transactions or balances have been loaded)") + txResponses = make([]abci.ResponseDeliverTx, 0) } - ctx.Logger().Debug("InitChainer: genesis transactions loaded", "elapsed", time.Since(start)) @@ -299,6 +260,59 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh } } +func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { + // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, + // this is done using BaseApp.cacheTxContext; so we replicate it here. + ms := ctx.MultiStore() + msCache := ms.MultiCacheWrap() + + stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) + stdlibCtx = stdlibCtx.WithMultiStore(msCache) + if cfg.CacheStdlibLoad { + cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) + } else { + cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) + } + cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) + + msCache.MultiWrite() +} + +func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, state GnoGenesisState) []abci.ResponseDeliverTx { + // Parse and set genesis state balances + for _, bal := range state.Balances { + acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) + cfg.acctKpr.SetAccount(ctx, acc) + err := cfg.bankKpr.SetCoins(ctx, bal.Address, bal.Amount) + if err != nil { + panic(err) + } + } + + txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) + // Run genesis txs + for _, tx := range state.Txs { + res := cfg.baseApp.Deliver(tx) + if res.IsErr() { + ctx.Logger().Error( + "Unable to deliver genesis tx", + "log", res.Log, + "error", res.Error, + "gas-used", res.GasUsed, + ) + } + + txResponses = append(txResponses, abci.ResponseDeliverTx{ + ResponseBase: res.ResponseBase, + GasWanted: res.GasWanted, + GasUsed: res.GasUsed, + }) + + cfg.GenesisTxResultHandler(ctx, tx, res) + } + return txResponses +} + // endBlockerApp is the app abstraction required by any EndBlocker type endBlockerApp interface { // LastBlockHeight returns the latest app height diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 3f2ce30e637..8a1743ddf53 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -106,8 +106,8 @@ type defaultStore struct { go2gnoStrict bool // if true, native->gno type conversion must be registered. // transient - current []string // for detecting import cycles. opslog []StoreOp // for debugging and testing. + current []string // for detecting import cycles. } func NewStore(alloc *Allocator, baseStore, iavlStore store.Store) *defaultStore { @@ -185,6 +185,7 @@ func (transactionStore) ClearCache() { // XXX: we should block Go2GnoType, because it uses a global cache map; // but it's called during preprocess and thus breaks some testing code. // let's wait until we remove Go2Gno entirely. +// https://github.com/gnolang/gno/issues/1361 // func (transactionStore) Go2GnoType(reflect.Type) Type { // panic("Go2GnoType may not be called in a transaction store") // } From 2b2c7da1b19646fb98388998e2c37a928d073f7f Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 28 Aug 2024 14:35:41 +0200 Subject: [PATCH 39/45] correctly use gas stores --- gno.land/pkg/sdk/vm/common_test.go | 6 +++--- gno.land/pkg/sdk/vm/gas_test.go | 18 ++++++++++++------ gno.land/pkg/sdk/vm/keeper.go | 6 +++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index cb184718c52..4dd3134457c 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -51,12 +51,12 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) - ctx = vmk.MakeGnoTransactionStore(ctx) + stdlibCtx := vmk.MakeGnoTransactionStore(ctx) stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") if cacheStdlibs { - vmk.LoadStdlibCached(ctx, stdlibsDir) + vmk.LoadStdlibCached(stdlibCtx, stdlibsDir) } else { - vmk.LoadStdlib(ctx, stdlibsDir) + vmk.LoadStdlib(stdlibCtx, stdlibsDir) } mcw.MultiWrite() diff --git a/gno.land/pkg/sdk/vm/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index 55ebc1f9d02..4171b1cdbc3 100644 --- a/gno.land/pkg/sdk/vm/gas_test.go +++ b/gno.land/pkg/sdk/vm/gas_test.go @@ -27,6 +27,9 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) { simulate := false tx.Fee.GasWanted = 3000 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) + // Has to be set up after gas meter in the context; so the stores are + // correctly wrapped in gas stores. + gctx = vmHandler.vm.MakeGnoTransactionStore(gctx) var res sdk.Result abort := false @@ -43,7 +46,7 @@ func TestAddPkgDeliverTxInsuffGas(t *testing.T) { assert.True(t, abort) assert.False(t, res.IsOK()) gasCheck := gctx.GasMeter().GasConsumed() - assert.Equal(t, int64(3462), gasCheck) + assert.Equal(t, int64(3231), gasCheck) } else { t.Errorf("should panic") } @@ -63,6 +66,7 @@ func TestAddPkgDeliverTx(t *testing.T) { simulate = false tx.Fee.GasWanted = 500000 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) + gctx = vmHandler.vm.MakeGnoTransactionStore(gctx) msgs := tx.GetMsgs() res := vmHandler.Process(gctx, msgs[0]) gasDeliver := gctx.GasMeter().GasConsumed() @@ -70,7 +74,7 @@ func TestAddPkgDeliverTx(t *testing.T) { assert.True(t, res.IsOK()) // NOTE: let's try to keep this bellow 100_000 :) - assert.Equal(t, int64(16989), gasDeliver) + assert.Equal(t, int64(92825), gasDeliver) } // Enough gas for a failed transaction. @@ -84,12 +88,13 @@ func TestAddPkgDeliverTxFailed(t *testing.T) { simulate = false tx.Fee.GasWanted = 500000 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) + gctx = vmHandler.vm.MakeGnoTransactionStore(gctx) msgs := tx.GetMsgs() res := vmHandler.Process(gctx, msgs[0]) gasDeliver := gctx.GasMeter().GasConsumed() assert.False(t, res.IsOK()) - assert.Equal(t, int64(1231), gasDeliver) + assert.Equal(t, int64(2231), gasDeliver) } // Not enough gas for a failed transaction. @@ -101,8 +106,9 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { ctx = ctx.WithMode(sdk.RunTxModeDeliver) simulate = false - tx.Fee.GasWanted = 1230 + tx.Fee.GasWanted = 2230 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) + gctx = vmHandler.vm.MakeGnoTransactionStore(gctx) var res sdk.Result abort := false @@ -119,7 +125,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { assert.True(t, abort) assert.False(t, res.IsOK()) gasCheck := gctx.GasMeter().GasConsumed() - assert.Equal(t, int64(1231), gasCheck) + assert.Equal(t, int64(2231), gasCheck) } else { t.Errorf("should panic") } @@ -129,7 +135,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { res = vmHandler.Process(gctx, msgs[0]) } -// Set up a test env for both a successful and a failed tx +// Set up a test env for both a successful and a failed tx. func setupAddPkg(success bool) (sdk.Context, sdk.Tx, vmHandler) { // setup env := setupTestEnv() diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index a32d745aada..40d253ed456 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -203,9 +203,9 @@ type gnoStoreContextKeyType struct{} var gnoStoreContextKey gnoStoreContextKeyType func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { - ms := ctx.MultiStore() - base := ms.GetStore(vm.baseKey) - iavl := ms.GetStore(vm.iavlKey) + base := ctx.Store(vm.baseKey) + iavl := ctx.Store(vm.iavlKey) + return vm.gnoStore.BeginTransaction(base, iavl) } From 26afca5090112c162dbafb0d6f382e40a1083192 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 28 Aug 2024 14:44:48 +0200 Subject: [PATCH 40/45] fix more tests --- gno.land/pkg/sdk/vm/common_test.go | 3 ++- gno.land/pkg/sdk/vm/keeper_test.go | 25 ++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 4dd3134457c..43a8fe1fbec 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -51,13 +51,14 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { mcw := ms.MultiCacheWrap() vmk.Initialize(log.NewNoopLogger(), mcw) - stdlibCtx := vmk.MakeGnoTransactionStore(ctx) + stdlibCtx := vmk.MakeGnoTransactionStore(ctx.WithMultiStore(mcw)) stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") if cacheStdlibs { vmk.LoadStdlibCached(stdlibCtx, stdlibsDir) } else { vmk.LoadStdlib(stdlibCtx, stdlibsDir) } + vmk.CommitGnoTransactionStore(stdlibCtx) mcw.MultiWrite() return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck} diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index afbc13fcf8b..d6a9703ac7d 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -25,8 +25,7 @@ var coinsString = ugnot.ValueString(10000000) func TestVMKeeperAddPackage(t *testing.T) { env := setupTestEnv() - ctx := env.ctx - vmk := env.vmk + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -58,7 +57,7 @@ func Echo() string {return "hello world"}`, assert.True(t, errors.Is(err, InvalidPkgPathError{})) // added package is formatted - store := vmk.getGnoTransactionStore(ctx) + store := env.vmk.getGnoTransactionStore(ctx) memFile := store.GetMemFile("gno.land/r/test", "test.gno") assert.NotNil(t, memFile) expected := `package test @@ -71,7 +70,7 @@ func Echo() string { return "hello world" } // Sending total send amount succeeds. func TestVMKeeperOrigSend1(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -116,7 +115,7 @@ func Echo(msg string) string { // Sending too much fails func TestVMKeeperOrigSend2(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -170,7 +169,7 @@ func GetAdmin() string { // Sending more than tx send fails. func TestVMKeeperOrigSend3(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -214,7 +213,7 @@ func Echo(msg string) string { // Sending realm package coins succeeds. func TestVMKeeperRealmSend1(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -258,7 +257,7 @@ func Echo(msg string) string { // Sending too much realm package coins fails. func TestVMKeeperRealmSend2(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -302,7 +301,7 @@ func Echo(msg string) string { // Assign admin as OrigCaller on deploying the package. func TestVMKeeperOrigCallerInit(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -356,7 +355,7 @@ func GetAdmin() string { // Call Run without imports, without variables. func TestVMKeeperRunSimple(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -395,7 +394,7 @@ func TestVMKeeperRunImportStdlibsColdStdlibLoad(t *testing.T) { func testVMKeeperRunImportStdlibs(t *testing.T, env testEnv) { t.Helper() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -425,7 +424,7 @@ func main() { func TestNumberOfArgsError(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) @@ -464,7 +463,7 @@ func Echo(msg string) string { func TestVMKeeperReinitialize(t *testing.T) { env := setupTestEnv() - ctx := env.ctx + ctx := env.vmk.MakeGnoTransactionStore(env.ctx) // Give "addr1" some gnots. addr := crypto.AddressFromPreimage([]byte("addr1")) From c02706715acbe7daf850bc0237a54129f803abc7 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Wed, 28 Aug 2024 21:01:54 +0200 Subject: [PATCH 41/45] return error in the case of nil AppStates --- gno.land/pkg/gnoland/app.go | 29 ++++++++++++++--------------- gno.land/pkg/gnoland/app_test.go | 6 ++++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 9fd8b1b07cb..24e8add2f87 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -235,21 +235,15 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh // load app state. AppState may be nil mostly in some minimal testing setups; // so log a warning when that happens. - var txResponses []abci.ResponseDeliverTx - if req.AppState != nil { - genState, ok := req.AppState.(GnoGenesisState) - if !ok { - return abci.ResponseInitChain{ - ResponseBase: abci.ResponseBase{ - Error: abci.StringError(fmt.Sprintf("invalid AppState of type %T", req.AppState)), - }, - } + txResponses, err := cfg.loadAppState(ctx, req.AppState) + if err != nil { + return abci.ResponseInitChain{ + ResponseBase: abci.ResponseBase{ + Error: abci.StringError(err.Error()), + }, } - txResponses = cfg.loadAppState(ctx, genState) - } else { - ctx.Logger().Warn("InitChainer: initializing chain without AppState (no genesis transactions or balances have been loaded)") - txResponses = make([]abci.ResponseDeliverTx, 0) } + ctx.Logger().Debug("InitChainer: genesis transactions loaded", "elapsed", time.Since(start)) @@ -278,7 +272,12 @@ func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { msCache.MultiWrite() } -func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, state GnoGenesisState) []abci.ResponseDeliverTx { +func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { + state, ok := appState.(GnoGenesisState) + if !ok { + return nil, fmt.Errorf("invalid AppState of type %T", appState) + } + // Parse and set genesis state balances for _, bal := range state.Balances { acc := cfg.acctKpr.NewAccountWithAddress(ctx, bal.Address) @@ -310,7 +309,7 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, state GnoGenesisState cfg.GenesisTxResultHandler(ctx, tx, res) } - return txResponses + return txResponses, nil } // endBlockerApp is the app abstraction required by any EndBlocker diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 4bd7c2eb0ae..de125b56d65 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -120,7 +120,7 @@ func TestNewApp(t *testing.T) { }, }, Validators: []abci.ValidatorUpdate{}, - AppState: nil, + AppState: GnoGenesisState{}, }) assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } @@ -176,7 +176,9 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper vmKpr: mock, CacheStdlibLoad: cached, } - cfg.InitChainer(testCtx, abci.RequestInitChain{}) + cfg.InitChainer(testCtx, abci.RequestInitChain{ + AppState: GnoGenesisState{}, + }) exp := map[string]int{ "MakeGnoTransactionStore": 1, "CommitGnoTransactionStore": 1, From 7c67a144d5aee6c014123078dcb9d8857568decb Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 2 Sep 2024 20:34:02 +0200 Subject: [PATCH 42/45] feat(gno.land): support setting custom height/timestamp for genesis txs --- gno.land/pkg/gnoland/app.go | 37 ++++++++- gno.land/pkg/gnoland/app_test.go | 127 ++++++++++++++++++++++------- gno.land/pkg/gnoland/types.go | 3 + gno.land/pkg/sdk/vm/keeper.go | 136 ++++++++++++++----------------- gnovm/stdlibs/std/context.go | 1 - 5 files changed, 194 insertions(+), 110 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 24e8add2f87..35512ebbfbe 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -2,6 +2,7 @@ package gnoland import ( + "errors" "fmt" "log/slog" "path/filepath" @@ -112,6 +113,9 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // the tx - in other words, data from a failing transaction won't be persisted // to the gno store caches. baseApp.SetBeginTxHook(func(ctx sdk.Context) sdk.Context { + if icc.beginTxHook != nil && ctx.BlockHeight() == 0 { + ctx = icc.beginTxHook(ctx) + } // Create Gno transaction store. return vmk.MakeGnoTransactionStore(ctx) }) @@ -220,10 +224,16 @@ type InitChainerConfig struct { vmKpr vm.VMKeeperI acctKpr auth.AccountKeeperI bankKpr bank.BankKeeperI + + // This is used by InitChainer, to set a different beginTxHook on each + // transaction. This allows the InitChainer to pass additional data down + // to the underlying app, such as custom Time/Height for genesis. + // (See [GnoGenesisState]). + beginTxHook func(sdk.Context) sdk.Context } // InitChainer is the function that can be used as a [sdk.InitChainer]. -func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { +func (cfg *InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { start := time.Now() ctx.Logger().Debug("InitChainer: started") @@ -254,7 +264,7 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh } } -func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { +func (cfg *InitChainerConfig) loadStdlibs(ctx sdk.Context) { // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, // this is done using BaseApp.cacheTxContext; so we replicate it here. ms := ctx.MultiStore() @@ -272,7 +282,12 @@ func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { msCache.MultiWrite() } -func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { +func (cfg *InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { + defer func() { + // Ensure to reset beginTxHook to nil when wrapping up. + cfg.beginTxHook = nil + }() + state, ok := appState.(GnoGenesisState) if !ok { return nil, fmt.Errorf("invalid AppState of type %T", appState) @@ -288,9 +303,23 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci } } + if len(state.TxContexts) != 0 && len(state.Txs) != len(state.TxContexts) { + return nil, errors.New("genesis state tx_contexts, if given, should be of same length as txs") + } + txResponses := make([]abci.ResponseDeliverTx, 0, len(state.Txs)) // Run genesis txs - for _, tx := range state.Txs { + for idx, tx := range state.Txs { + if len(state.TxContexts) > 0 { + customExecCtx := state.TxContexts[idx] + if customExecCtx.Timestamp == 0 { + customExecCtx.Timestamp = ctx.BlockTime().Unix() + } + cfg.beginTxHook = func(ctx sdk.Context) sdk.Context { + return vm.InjectExecContextCustom(ctx, customExecCtx) + } + } + res := cfg.baseApp.Deliver(tx) if res.IsErr() { ctx.Logger().Error( diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index de125b56d65..65cf36f85b9 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -28,52 +28,60 @@ import ( "github.com/stretchr/testify/require" ) -// Tests that NewAppWithOptions works even when only providing a simple DB. -func TestNewAppWithOptions(t *testing.T) { - t.Parallel() - - app, err := NewAppWithOptions(&AppOptions{ +func testAppOptions() *AppOptions { + return &AppOptions{ DB: memdb.NewMemDB(), InitChainerConfig: InitChainerConfig{ CacheStdlibLoad: true, GenesisTxResultHandler: PanicOnFailingTxResultHandler, StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), }, - }) - require.NoError(t, err) - bapp := app.(*sdk.BaseApp) - assert.Equal(t, "dev", bapp.AppVersion()) - assert.Equal(t, "gnoland", bapp.Name()) + } +} - addr := crypto.AddressFromPreimage([]byte("test1")) - resp := bapp.InitChain(abci.RequestInitChain{ +func testRequestInitChain(genState GnoGenesisState) abci.RequestInitChain { + return abci.RequestInitChain{ Time: time.Now(), ChainID: "dev", ConsensusParams: &abci.ConsensusParams{ Block: defaultBlockParams(), }, Validators: []abci.ValidatorUpdate{}, - AppState: GnoGenesisState{ - Balances: []Balance{ - { - Address: addr, - Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, - }, + AppState: genState, + } +} + +// Tests that NewAppWithOptions works even when only providing a simple DB. +func TestNewAppWithOptions(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(testAppOptions()) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + assert.Equal(t, "dev", bapp.AppVersion()) + assert.Equal(t, "gnoland", bapp.Name()) + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, }, - Txs: []std.Tx{ - { - Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.MemFile{ - { - Name: "demo.gno", - Body: "package demo; func Hello() string { return `hello`; }", - }, - })}, - Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, - Signatures: []std.Signature{{}}, // one empty signature - }, + }, + Txs: []std.Tx{ + { + Msgs: []std.Msg{vm.NewMsgAddPackage(addr, "gno.land/r/demo", []*std.MemFile{ + { + Name: "demo.gno", + Body: "package demo; func Hello() string { return `hello`; }", + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature }, }, - }) + })) require.True(t, resp.IsOK(), "InitChain response: %v", resp) tx := amino.MustMarshal(std.Tx{ @@ -125,6 +133,65 @@ func TestNewApp(t *testing.T) { assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } +func TestNewApp_WithTxContexts(t *testing.T) { + t.Parallel() + + appOpts := testAppOptions() + called := 0 + appOpts.GenesisTxResultHandler = func(_ sdk.Context, _ std.Tx, res sdk.Result) { + if !res.IsOK() { + t.Fatal(res) + } + called++ + assert.Equal(t, string(res.Data), "time.Now() 7331\nstd.GetHeight() -1337\n") + } + app, err := NewAppWithOptions(appOpts) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + + const pkgFile = `package main + +import ( + "std" + "time" +) + +func main() { + println("time.Now()", time.Now().Unix()) + println("std.GetHeight()", std.GetHeight()) +} +` + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + }, + Txs: []std.Tx{ + { + Msgs: []std.Msg{vm.NewMsgRun(addr, std.Coins{}, []*std.MemFile{ + { + Name: "demo.gno", + Body: pkgFile, + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, + }, + TxContexts: []vm.ExecContextCustom{ + { + Height: -1337, + Timestamp: 7331, + }, + }, + })) + require.True(t, resp.IsOK(), "InitChain response: %v", resp) +} + // Test whether InitChainer calls to load the stdlibs correctly. func TestInitChainer_LoadStdlib(t *testing.T) { t.Parallel() diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 016f3279dbd..c12bea05d83 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -3,6 +3,7 @@ package gnoland import ( "errors" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -22,4 +23,6 @@ func ProtoGnoAccount() std.Account { type GnoGenesisState struct { Balances []Balance `json:"balances"` Txs []std.Tx `json:"txs"` + // Should match len(Txs), or be null + TxContexts []vm.ExecContextCustom `json:"tx_contexts"` } diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 40d253ed456..628d970c7d5 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -198,9 +198,12 @@ func loadStdlibPackage(pkgPath, stdlibDir string, store gno.Store) { m.RunMemPackage(memPkg, true) } -type gnoStoreContextKeyType struct{} +type keeperContextKey string -var gnoStoreContextKey gnoStoreContextKeyType +const ( + keeperKeyGnoStore keeperContextKey = "keeper:gno_store" + keeperKeyCustomContext keeperContextKey = "keeper:exec_context" +) func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { base := ctx.Store(vm.baseKey) @@ -210,7 +213,7 @@ func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore } func (vm *VMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { - return ctx.WithValue(gnoStoreContextKey, vm.newGnoTransactionStore(ctx)) + return ctx.WithValue(keeperKeyGnoStore, vm.newGnoTransactionStore(ctx)) } func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { @@ -218,7 +221,7 @@ func (vm *VMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { } func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { - txStore := ctx.Value(gnoStoreContextKey).(gno.TransactionStore) + txStore := ctx.Value(keeperKeyGnoStore).(gno.TransactionStore) txStore.ClearObjectCache() return txStore } @@ -226,6 +229,49 @@ func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore // Namespace can be either a user or crypto address. var reNamespace = regexp.MustCompile(`^gno.land/(?:r|p)/([\.~_a-zA-Z0-9]+)`) +// ExecContextCustom is a subset of [stdlibs.ExecContext] which can be used to +// inject custom fields at genesis. +type ExecContextCustom struct { + // Height is the BlockHeight: it is not recommended to set the same + // BlockHeight as the one used by the previous iteration of the chain, as + // from the realm's point of view it will eventually be as if a height + // occurred twice at different times: instead, use negative heights. + // (Last block should be -1, as 0 is the genesis block). + Height int64 `json:"height"` + Timestamp int64 `json:"timestamp"` +} + +// InjectExecContextCustom returns a new sdk.Context, which contains the given +// [ExecContextCustom]. This can be used to modify Height and timestamp in +// genesis transactions. +func InjectExecContextCustom(ctx sdk.Context, e ExecContextCustom) sdk.Context { + return ctx.WithValue(keeperKeyCustomContext, e) +} + +func (vm *VMKeeper) newExecContext(ctx sdk.Context, creator, pkgAddr crypto.Address) stdlibs.ExecContext { + execCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + OrigCaller: creator.Bech32(), + OrigSendSpent: new(std.Coins), + OrigPkgAddr: pkgAddr.Bech32(), + Banker: NewSDKBanker(vm, ctx), + EventLogger: ctx.EventLogger(), + } + + // if we're at genesis, ctx.Context() can change Height and Timestamp. + if ctx.BlockHeight() == 0 { + v, ok := ctx.Value(keeperKeyCustomContext).(ExecContextCustom) + if ok { + execCtx.Height = v.Height + execCtx.Timestamp = v.Timestamp + } + } + + return execCtx +} + // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { const sysUsersPkg = "gno.land/r/sys/users" @@ -253,17 +299,8 @@ func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Add // Parse and run the files, construct *PV. pkgAddr := gno.DerivePkgAddr(pkgPath) - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - OrigCaller: creator.Bech32(), - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - // XXX: should we remove the banker ? - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, creator, pkgAddr) + // XXX: should we remove the banker from this ExecContext? m := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -353,18 +390,8 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { } // Parse and run the files, construct *PV. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: creator.Bech32(), - OrigSend: deposit, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, creator, pkgAddr) + msgCtx.OrigSend = deposit // Parse and run the files, construct *PV. m2 := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -454,18 +481,8 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { // Make context. // NOTE: if this is too expensive, // could it be safely partially memoized? - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: caller.Bech32(), - OrigSend: send, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, caller, pkgAddr) + msgCtx.OrigSend = send // Construct machine and evaluate. m := gno.NewMachineWithOptions( gno.MachineOptions{ @@ -553,18 +570,8 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { } // Parse and run the files, construct *PV. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - Msg: msg, - OrigCaller: caller.Bech32(), - OrigSend: send, - OrigSendSpent: new(std.Coins), - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, caller, pkgAddr) + msgCtx.OrigSend = send // Parse and run the files, construct *PV. buf := new(bytes.Buffer) m := gno.NewMachineWithOptions( @@ -714,18 +721,8 @@ func (vm *VMKeeper) QueryEval(ctx sdk.Context, pkgPath string, expr string) (res return "", err } // Construct new machine. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, - // OrigCaller: caller, - // OrigSend: send, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - EventLogger: ctx.EventLogger(), - } + // safe to pass ctx as long as it is a fork to be discarded. + msgCtx := vm.newExecContext(ctx, crypto.Address{}, pkgAddr) m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, @@ -781,18 +778,7 @@ func (vm *VMKeeper) QueryEvalString(ctx sdk.Context, pkgPath string, expr string return "", err } // Construct new machine. - msgCtx := stdlibs.ExecContext{ - ChainID: ctx.ChainID(), - Height: ctx.BlockHeight(), - Timestamp: ctx.BlockTime().Unix(), - // Msg: msg, - // OrigCaller: caller, - // OrigSend: jsend, - // OrigSendSpent: nil, - OrigPkgAddr: pkgAddr.Bech32(), - Banker: NewSDKBanker(vm, ctx), // safe as long as ctx is a fork to be discarded. - EventLogger: ctx.EventLogger(), - } + msgCtx := vm.newExecContext(ctx, crypto.Address{}, pkgAddr) m := gno.NewMachineWithOptions( gno.MachineOptions{ PkgPath: pkgPath, diff --git a/gnovm/stdlibs/std/context.go b/gnovm/stdlibs/std/context.go index ff5c91a14eb..966173b6d72 100644 --- a/gnovm/stdlibs/std/context.go +++ b/gnovm/stdlibs/std/context.go @@ -12,7 +12,6 @@ type ExecContext struct { Height int64 Timestamp int64 // seconds TimestampNano int64 // nanoseconds, only used for testing. - Msg sdk.Msg OrigCaller crypto.Bech32Address OrigPkgAddr crypto.Bech32Address OrigSend std.Coins From 41b441dc8ee9f9ff76e8a2b182309724b81ccbc1 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 2 Sep 2024 21:11:48 +0200 Subject: [PATCH 43/45] fix ci --- gnovm/tests/file.go | 1 - 1 file changed, 1 deletion(-) diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index f6bd789f1bf..a5a70a251d9 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -60,7 +60,6 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { ChainID: "dev", Height: 123, Timestamp: 1234567890, - Msg: nil, OrigCaller: caller.Bech32(), OrigPkgAddr: pkgAddr.Bech32(), OrigSend: send, From 18698554bdee7d1ae03250c71d2ec8c96d962a28 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Mon, 2 Sep 2024 21:32:13 +0200 Subject: [PATCH 44/45] remove calls from mock --- gno.land/pkg/gnoland/app_test.go | 41 +++++++++++++++++++++---------- gno.land/pkg/gnoland/mock_test.go | 16 ------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index de125b56d65..ae282f8e114 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -152,25 +152,39 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper ms.LoadLatestVersion() testCtx := sdk.NewContext(sdk.RunTxModeDeliver, ms.MultiCacheWrap(), &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) + // mock set-up + var ( + makeCalls int + commitCalls int + loadStdlibCalls int + loadStdlibCachedCalls int + ) containsGnoStore := func(ctx sdk.Context) bool { return ctx.Context().Value(gnoStoreKey) == gnoStoreValue } - loadStdlib := func(ctx sdk.Context, dir string) { - assert.Equal(t, stdlibDir, dir, "stdlibDir should match provided dir") - assert.True(t, containsGnoStore(ctx), "should contain gno store") + // ptr is pointer to either loadStdlibCalls or loadStdlibCachedCalls + loadStdlib := func(ptr *int) func(ctx sdk.Context, dir string) { + return func(ctx sdk.Context, dir string) { + assert.Equal(t, stdlibDir, dir, "stdlibDir should match provided dir") + assert.True(t, containsGnoStore(ctx), "should contain gno store") + *ptr++ + } } mock := &mockVMKeeper{ makeGnoTransactionStoreFn: func(ctx sdk.Context) sdk.Context { + makeCalls++ assert.False(t, containsGnoStore(ctx), "should not already contain gno store") return ctx.WithContext(context.WithValue(ctx.Context(), gnoStoreKey, gnoStoreValue)) }, commitGnoTransactionStoreFn: func(ctx sdk.Context) { + commitCalls++ assert.True(t, containsGnoStore(ctx), "should contain gno store") }, - loadStdlibFn: loadStdlib, - loadStdlibCachedFn: loadStdlib, - calls: make(map[string]int), + loadStdlibFn: loadStdlib(&loadStdlibCalls), + loadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls), } + + // call initchainer cfg := InitChainerConfig{ StdlibDir: stdlibDir, vmKpr: mock, @@ -179,16 +193,17 @@ func testInitChainerLoadStdlib(t *testing.T, cached bool) { //nolint:thelper cfg.InitChainer(testCtx, abci.RequestInitChain{ AppState: GnoGenesisState{}, }) - exp := map[string]int{ - "MakeGnoTransactionStore": 1, - "CommitGnoTransactionStore": 1, - } + + // assert number of calls + assert.Equal(t, 1, makeCalls, "should call MakeGnoTransactionStore once") + assert.Equal(t, 1, commitCalls, "should call CommitGnoTransactionStore once") if cached { - exp["LoadStdlibCached"] = 1 + assert.Equal(t, 0, loadStdlibCalls, "should call LoadStdlib never") + assert.Equal(t, 1, loadStdlibCachedCalls, "should call LoadStdlibCached once") } else { - exp["LoadStdlib"] = 1 + assert.Equal(t, 1, loadStdlibCalls, "should call LoadStdlib once") + assert.Equal(t, 0, loadStdlibCachedCalls, "should call LoadStdlibCached never") } - assert.Equal(t, mock.calls, exp) } // generateValidatorUpdates generates dummy validator updates diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index f22c453f018..62aecaf5278 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -54,18 +54,9 @@ type mockVMKeeper struct { loadStdlibCachedFn func(sdk.Context, string) makeGnoTransactionStoreFn func(ctx sdk.Context) sdk.Context commitGnoTransactionStoreFn func(ctx sdk.Context) - - calls map[string]int -} - -func (m *mockVMKeeper) call(name string) { - if m.calls != nil { - m.calls[name]++ - } } func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { - m.call("AddPackage") if m.addPackageFn != nil { return m.addPackageFn(ctx, msg) } @@ -74,7 +65,6 @@ func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { } func (m *mockVMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err error) { - m.call("Call") if m.callFn != nil { return m.callFn(ctx, msg) } @@ -83,7 +73,6 @@ func (m *mockVMKeeper) Call(ctx sdk.Context, msg vm.MsgCall) (res string, err er } func (m *mockVMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res string, err error) { - m.call("QueryEval") if m.queryFn != nil { return m.queryFn(ctx, pkgPath, expr) } @@ -92,7 +81,6 @@ func (m *mockVMKeeper) QueryEval(ctx sdk.Context, pkgPath, expr string) (res str } func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err error) { - m.call("Run") if m.runFn != nil { return m.runFn(ctx, msg) } @@ -101,21 +89,18 @@ func (m *mockVMKeeper) Run(ctx sdk.Context, msg vm.MsgRun) (res string, err erro } func (m *mockVMKeeper) LoadStdlib(ctx sdk.Context, stdlibDir string) { - m.call("LoadStdlib") if m.loadStdlibFn != nil { m.loadStdlibFn(ctx, stdlibDir) } } func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { - m.call("LoadStdlibCached") if m.loadStdlibCachedFn != nil { m.loadStdlibCachedFn(ctx, stdlibDir) } } func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { - m.call("MakeGnoTransactionStore") if m.makeGnoTransactionStoreFn != nil { return m.makeGnoTransactionStoreFn(ctx) } @@ -123,7 +108,6 @@ func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { } func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { - m.call("CommitGnoTransactionStore") if m.commitGnoTransactionStoreFn != nil { m.commitGnoTransactionStoreFn(ctx) } From 55a8d4ae9ef2d67027c7be72e2df9bcd77f57dfc Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Tue, 3 Sep 2024 12:20:08 +0200 Subject: [PATCH 45/45] more coverage --- gno.land/pkg/gnoland/app_test.go | 57 ++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 6605e9852aa..895f11b9792 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path/filepath" + "strconv" "strings" "testing" "time" @@ -133,17 +134,22 @@ func TestNewApp(t *testing.T) { assert.True(t, resp.IsOK(), "resp is not OK: %v", resp) } -func TestNewApp_WithTxContexts(t *testing.T) { +func TestNewAppWithOptions_WithTxContexts(t *testing.T) { t.Parallel() appOpts := testAppOptions() called := 0 - appOpts.GenesisTxResultHandler = func(_ sdk.Context, _ std.Tx, res sdk.Result) { + appOpts.GenesisTxResultHandler = func(ctx sdk.Context, _ std.Tx, res sdk.Result) { if !res.IsOK() { t.Fatal(res) } called++ - assert.Equal(t, string(res.Data), "time.Now() 7331\nstd.GetHeight() -1337\n") + switch called { + case 1: + assert.Equal(t, string(res.Data), "time.Now() 7331\nstd.GetHeight() -1337\n") + case 2: + assert.Equal(t, string(res.Data), "time.Now() "+strconv.FormatInt(ctx.BlockTime().Unix(), 10)+"\nstd.GetHeight() 13377331\n") + } } app, err := NewAppWithOptions(appOpts) require.NoError(t, err) @@ -181,17 +187,62 @@ func main() { Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, Signatures: []std.Signature{{}}, // one empty signature }, + { + // same as previous, but will have different TxContext. + Msgs: []std.Msg{vm.NewMsgRun(addr, std.Coins{}, []*std.MemFile{ + { + Name: "demo.gno", + Body: pkgFile, + }, + })}, + Fee: std.Fee{GasWanted: 1e6, GasFee: std.Coin{Amount: 1e6, Denom: "ugnot"}}, + Signatures: []std.Signature{{}}, // one empty signature + }, }, TxContexts: []vm.ExecContextCustom{ { Height: -1337, Timestamp: 7331, }, + { + // different height; timestamp=0 will mean that InitChain will auto-set it + // to the block time. + Height: 13377331, + Timestamp: 0, + }, }, })) require.True(t, resp.IsOK(), "InitChain response: %v", resp) } +func TestNewAppWithOptions_InvalidTxContexts(t *testing.T) { + t.Parallel() + + app, err := NewAppWithOptions(testAppOptions()) + require.NoError(t, err) + bapp := app.(*sdk.BaseApp) + + addr := crypto.AddressFromPreimage([]byte("test1")) + resp := bapp.InitChain(testRequestInitChain(GnoGenesisState{ + Balances: []Balance{ + { + Address: addr, + Amount: []std.Coin{{Amount: 1e15, Denom: "ugnot"}}, + }, + }, + Txs: []std.Tx{ + // one tx + {}, + }, + TxContexts: []vm.ExecContextCustom{ + // two contexts + {}, {}, + }, + })) + assert.True(t, resp.IsErr()) + assert.ErrorContains(t, resp.Error, "genesis state tx_contexts, if given, should be of same length as txs") +} + // Test whether InitChainer calls to load the stdlibs correctly. func TestInitChainer_LoadStdlib(t *testing.T) { t.Parallel()