From c1a3341d49a1d2661ddc2649ee340c6ff5e8760f Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 5 Sep 2024 12:17:01 +0200 Subject: [PATCH] fix(gno.land): make gno store cache respect rollbacks (#2319) This pull request modifies the current `defaultStore` to support transactionality in the same way as our tm2 stores. A few new concepts are introduced: - A `TransactionStore` interface is added, which extends the `gnolang.Store` interface to support a Write() method. - Together with corresponding implementations allowing for transactionality on its cache maps, this means that the Gno store retained by the VM keeper is only modified atomically after a transaction has completed. - The tm2 `BaseApp` has the new "hooks" `BeginTxHook` and `EndTxHook`. The former is called right before starting a transaction, and the latter is called right after finishing it, together with the transaction result. - This allows us to plug in the Gno `TransactionalStore` in the `sdk.Context` through the `BeginTxHook`; and commit the result, if successful, in the `EndTxHook`. ## Overview of changes - `gno.land` - `pkg/gnoland/app.go`: the InitChainer is now additionally responsible of loading standard libraries. To separate the options related to app startup globally, and those to genesis, the InitChainer is now a method of its config struct, `InitChainerConfig`, embedded into the `AppOptions`. - `pkg/gnoland/app.go`: `NewAppOptions` is only used in `NewApp`, where most of its fields were modified, anyway. I replaced it by changing the `validate()` method to write default values. - `pkg/gnoland/node_inmemory.go`, `pkg/integration/testing_integration.go`: these changes were made necessary to support `gnoland restart` in our txtars, and supporting fast reloading of standard libraries (`LoadStdlibCached`). - `pkg/sdk/vm/keeper.go`: removed all the code to load standard libraries on Initialize, as it's now done in the InitChainer. The hack introduced in #2568 is no longer necessary as well. Other changes show how the Gno Store is injected and retrieved from the `sdk.Context`. - `gnovm/pkg/gnolang/store.go` - Fork and SwapStores have been removed, in favour of BeginTransaction. BeginTransaction creates a `TransactionalStore`; the "transaction-scoped" fields of the defaultStore are swapped with "transactional" versions (including the allocator, cacheObjects and cacheTypes/cacheNodes - the latter write to a `txLogMap`). - ClearObjectCache is still necessary for the case of a transaction with multiple messages. - The `Map` interface in `txlog` allows us to have a `txLog` data type stacked on top of another. This is useful for the cases where we use `BeginTransaction` in preprocess. (We previously used `Fork`.) See later in the "open questions" section. - I added an Iterator method on the `txlog.Map` interface - this will be compatible with [RangeFunc](https://go.dev/wiki/RangefuncExperiment), once we switch over to [go1.23](https://go.dev/doc/go1.23). - `tm2/pkg/sdk` - As previously mentioned, two hooks were added on the BaseApp to support injecting application code right before starting a transaction and then right after ending it; allowing us to inject the code relating to the Gno.land store while maintaining the modules decoupled - Other - `gnovm/pkg/gnolang/machine.go` has a one-line fix for a bug printing the machine, whereby it would panic if len(blocks) == 0. ## Open questions / notes - TransactionalStore should be a different implementation altogether; but decoupling the logic which is used within the stores without doing a massive copy-and-paste of the entire defaultStore implementation is non-trivial. See [this comment](https://github.com/gnolang/gno/pull/2319#issuecomment-2265817755), and [this PR](https://github.com/gnolang/gno/pull/2655) for a draft proposed store refactor, which would render the store more modular and testable. - There is an alternative implementation, which in micro-benchmarks would be somewhat faster, in place of the `txLog`; it's still in `gnolang/internal/txlog/txlog_test.go` where it also has benchmarks. See [1347c5f](https://github.com/gnolang/gno/pull/2319/commits/1347c5ff467e8a68c1cb8158d47538ff2677d8bf) for the solution which uses `bufferedTxMap`, without using the `txlog.Map` interface. The main problem with this approach is that it does not support stacking bufferedTxMaps, thus there is a different method to be used in preprocess - `preprocessFork()`. --- contribs/gnodev/pkg/dev/node.go | 6 +- .../testdata/issue_2283_cacheTypes.txtar | 1 - .../testdata/restart_missing_type.txtar | 202 +++++++ .../cmd/gnoland/testdata/restart_nonval.txtar | 5 + gno.land/pkg/gnoland/app.go | 268 ++++++--- gno.land/pkg/gnoland/app_test.go | 204 ++++++- gno.land/pkg/gnoland/mock_test.go | 44 +- gno.land/pkg/gnoland/node_inmemory.go | 46 +- gno.land/pkg/integration/doc.go | 5 +- .../pkg/integration/testdata/gnoland.txtar | 2 +- .../pkg/integration/testdata/restart.txtar | 24 + .../pkg/integration/testing_integration.go | 48 +- gno.land/pkg/integration/testing_node.go | 31 +- gno.land/pkg/sdk/vm/common_test.go | 14 +- gno.land/pkg/sdk/vm/gas_test.go | 8 +- gno.land/pkg/sdk/vm/keeper.go | 234 +++----- gno.land/pkg/sdk/vm/keeper_test.go | 89 ++- .../pkg/sdk/vm/testdata/emptystdlib/README | 1 + gnovm/pkg/gnolang/internal/txlog/txlog.go | 141 +++++ .../pkg/gnolang/internal/txlog/txlog_test.go | 553 ++++++++++++++++++ gnovm/pkg/gnolang/machine.go | 3 +- gnovm/pkg/gnolang/preprocess.go | 4 +- gnovm/pkg/gnolang/store.go | 244 +++++--- gnovm/pkg/gnolang/store_test.go | 99 ++++ tm2/pkg/sdk/abci.go | 9 + tm2/pkg/sdk/baseapp.go | 12 + tm2/pkg/sdk/baseapp_test.go | 50 +- tm2/pkg/sdk/context.go | 22 +- tm2/pkg/sdk/options.go | 14 + 29 files changed, 1958 insertions(+), 425 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/restart_missing_type.txtar create mode 100644 gno.land/cmd/gnoland/testdata/restart_nonval.txtar create mode 100644 gno.land/pkg/integration/testdata/restart.txtar create mode 100644 gno.land/pkg/sdk/vm/testdata/emptystdlib/README create mode 100644 gnovm/pkg/gnolang/internal/txlog/txlog.go create mode 100644 gnovm/pkg/gnolang/internal/txlog/txlog_test.go create mode 100644 gnovm/pkg/gnolang/store_test.go diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 5b7c4fe08da..c3e70366fb2 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -469,7 +469,9 @@ 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 + // 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 // recoverFromError handles panics and converts them to errors. @@ -512,7 +514,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/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar index 38b0c8fe865..95bd48c0144 100644 --- a/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar +++ b/gno.land/cmd/gnoland/testdata/issue_2283_cacheTypes.txtar @@ -101,4 +101,3 @@ import ( func Call(s string) { base64.StdEncoding.DecodeString("hey") } - 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..7592693eeff --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/restart_missing_type.txtar @@ -0,0 +1,202 @@ +# 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 + +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": "1000000", + "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/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/gnoland/app.go b/gno.land/pkg/gnoland/app.go index f4d353411f8..2380658c6e9 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 ( @@ -5,6 +6,7 @@ import ( "log/slog" "path/filepath" "strconv" + "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" @@ -25,48 +27,46 @@ 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 - // `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 // required + Logger *slog.Logger // required + EventSwitch events.EventSwitch // required + MaxCycles int64 // hard limit for cycles in GnoVM + InitChainerConfig // options related to InitChainer } -func NewAppOptions() *AppOptions { +// DefaultAppOptions provides a "ready" default [AppOptions] for use with +// [NewAppWithOptions], using the provided db. +func TestAppOptions(db dbm.DB) *AppOptions { return &AppOptions{ - GenesisTxHandler: PanicOnFailingTxHandler, - Logger: log.NewNoopLogger(), - DB: memdb.NewMemDB(), - GnoRootDir: gnoenv.RootDir(), - EventSwitch: events.NilEventSwitch(), + DB: db, + Logger: log.NewNoopLogger(), + EventSwitch: events.NewEventSwitch(), + InitChainerConfig: InitChainerConfig{ + GenesisTxResultHandler: PanicOnFailingTxResultHandler, + StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), + CacheStdlibLoad: true, + }, } } -func (c *AppOptions) validate() error { - if c.Logger == nil { - return fmt.Errorf("no logger provided") - } - - if c.DB == nil { +func (c AppOptions) validate() error { + // Required fields + switch { + case c.DB == nil: return fmt.Errorf("no db provided") + case c.Logger == nil: + return fmt.Errorf("no logger provided") + case c.EventSwitch == nil: + return fmt.Errorf("no event switch provided") } - 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 @@ -88,13 +88,13 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - - // XXX: Embed this ? - stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") - vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) + vmk := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, cfg.MaxCycles) // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.GenesisTxHandler)) + icc := cfg.InitChainerConfig + icc.baseApp = baseApp + icc.acctKpr, icc.bankKpr, icc.vmKpr = acctKpr, bankKpr, vmk + baseApp.SetInitChainer(icc.InitChainer) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -108,14 +108,28 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { newCtx sdk.Context, res sdk.Result, abort bool, ) { // 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 }, ) + // 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) + }) + baseApp.SetEndTxHook(func(ctx sdk.Context, result sdk.Result) { + if result.IsOK() { + vmk.CommitGnoTransactionStore(ctx) + } + }) + // Set up the event collector c := newCollector[validatorUpdate]( cfg.EventSwitch, // global event switch filled by the node @@ -143,13 +157,13 @@ 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 } -// NewApp creates the GnoLand application. +// NewApp creates the gno.land application. func NewApp( dataRootDir string, skipFailingGenesisTxs bool, @@ -158,9 +172,16 @@ 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.GenesisTxHandler = NoopGenesisTxHandler + cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler } // Get main DB. @@ -169,74 +190,135 @@ 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) } -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, - acctKpr auth.AccountKeeperI, - bankKpr bank.BankKeeperI, - resHandler GenesisTxHandler, -) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { - return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - 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) - } - } - - // 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) - } - } +// 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 + + // 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 - // Done! + // 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; 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)) + + // load app state. AppState may be nil mostly in some minimal testing setups; + // so log a warning when that happens. + txResponses, err := cfg.loadAppState(ctx, req.AppState) + if err != nil { return abci.ResponseInitChain{ - Validators: req.Validators, - TxResponses: txResponses, + ResponseBase: abci.ResponseBase{ + Error: abci.StringError(err.Error()), + }, } } + + ctx.Logger().Debug("InitChainer: genesis transactions loaded", + "elapsed", time.Since(start)) + + // Done! + return abci.ResponseInitChain{ + Validators: req.Validators, + TxResponses: txResponses, + } +} + +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, 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) + 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, 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 852d090f3af..193ff0b0b14 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -1,20 +1,202 @@ package gnoland import ( + "context" "errors" "fmt" "strings" "testing" + "time" - "github.com/gnolang/gno/gnovm/stdlibs/std" + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + 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" - "github.com/gnolang/gno/tm2/pkg/bft/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" "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) { + t.Parallel() + + app, err := NewAppWithOptions(TestAppOptions(memdb.NewMemDB())) + 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: defaultBlockParams(), + }, + 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 + }, + }, + }, + }) + 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)}, + 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, + }) + 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") +} + +func TestNewApp(t *testing.T) { + // NewApp should have good defaults and manage to run InitChain. + td := t.TempDir() + + app, err := NewApp(td, true, events.NewEventSwitch(), log.NewNoopLogger()) + 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: GnoGenesisState{}, + }) + 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() + + 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" + 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()) + + // mock set-up + var ( + makeCalls int + commitCalls int + loadStdlibCalls int + loadStdlibCachedCalls int + ) + containsGnoStore := func(ctx sdk.Context) bool { + return ctx.Context().Value(gnoStoreKey) == gnoStoreValue + } + // 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(&loadStdlibCalls), + loadStdlibCachedFn: loadStdlib(&loadStdlibCachedCalls), + } + + // call initchainer + cfg := InitChainerConfig{ + StdlibDir: stdlibDir, + vmKpr: mock, + CacheStdlibLoad: cached, + } + cfg.InitChainer(testCtx, abci.RequestInitChain{ + AppState: GnoGenesisState{}, + }) + + // assert number of calls + assert.Equal(t, 1, makeCalls, "should call MakeGnoTransactionStore once") + assert.Equal(t, 1, commitCalls, "should call CommitGnoTransactionStore once") + if cached { + assert.Equal(t, 0, loadStdlibCalls, "should call LoadStdlib never") + assert.Equal(t, 1, loadStdlibCachedCalls, "should call LoadStdlibCached once") + } else { + assert.Equal(t, 1, loadStdlibCalls, "should call LoadStdlib once") + assert.Equal(t, 0, loadStdlibCachedCalls, "should call LoadStdlibCached never") + } +} + // generateValidatorUpdates generates dummy validator updates func generateValidatorUpdates(t *testing.T, count int) []abci.ValidatorUpdate { t.Helper() @@ -81,7 +263,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 +284,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 } @@ -126,7 +308,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{}) @@ -145,7 +327,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 } @@ -169,7 +351,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{}) @@ -208,7 +390,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, } @@ -217,7 +399,7 @@ func TestEndBlocker(t *testing.T) { if index%2 == 0 { changes[index].Power = 0 - event = std.GnoEvent{ + event = gnostd.GnoEvent{ Type: validatorRemovedEvent, PkgPath: valRealm, } @@ -227,8 +409,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 1ff9f168bd1..62aecaf5278 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -45,18 +45,15 @@ 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 { - 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) } func (m *mockVMKeeper) AddPackage(ctx sdk.Context, msg vm.MsgAddPackage) error { @@ -91,6 +88,31 @@ 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) { + if m.loadStdlibFn != nil { + m.loadStdlibFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) LoadStdlibCached(ctx sdk.Context, stdlibDir string) { + if m.loadStdlibCachedFn != nil { + m.loadStdlibCachedFn(ctx, stdlibDir) + } +} + +func (m *mockVMKeeper) MakeGnoTransactionStore(ctx sdk.Context) sdk.Context { + if m.makeGnoTransactionStoreFn != nil { + return m.makeGnoTransactionStoreFn(ctx) + } + return ctx +} + +func (m *mockVMKeeper) CommitGnoTransactionStore(ctx sdk.Context) { + if m.commitGnoTransactionStoreFn != nil { + m.commitGnoTransactionStoreFn(ctx) + } +} + type ( lastBlockHeightDelegate func() int64 loggerDelegate func() *slog.Logger diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index 02691f89c3e..d168c955607 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,11 @@ type InMemoryNodeConfig struct { PrivValidator bft.PrivValidator // identity of the validator Genesis *bft.GenesisDoc TMConfig *tmcfg.Config - GenesisTxHandler GenesisTxHandler GenesisMaxVMCycles int64 + DB *memdb.MemDB // will be initialized if nil + + // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") + InitChainerConfig } // NewMockedPrivValidator generate a new key @@ -37,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{}, @@ -51,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 @@ -70,7 +78,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 +95,21 @@ 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 db if nil + if cfg.DB == nil { + cfg.DB = memdb.NewMemDB() + } + // 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: cfg.DB, + EventSwitch: evsw, + InitChainerConfig: cfg.InitChainerConfig, }) if err != nil { return nil, fmt.Errorf("error initializing new app: %w", err) @@ -114,7 +128,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/doc.go b/gno.land/pkg/integration/doc.go index 2b6d24c23b8..ef3ed9923da 100644 --- a/gno.land/pkg/integration/doc.go +++ b/gno.land/pkg/integration/doc.go @@ -8,9 +8,12 @@ // // 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. +// - `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/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 -- diff --git a/gno.land/pkg/integration/testdata/restart.txtar b/gno.land/pkg/integration/testdata/restart.txtar new file mode 100644 index 00000000000..8d50dd15814 --- /dev/null +++ b/gno.land/pkg/integration/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/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index d525591f51e..d3f55cfadf7 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" @@ -26,6 +27,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" @@ -72,6 +74,7 @@ func RunGnolandTestscripts(t *testing.T, txtarDir string) { type testNode struct { *node.Node + cfg *gnoland.InMemoryNodeConfig nGnoKeyExec uint // Counter for execution of gnokey. } @@ -152,7 +155,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 } @@ -170,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 @@ -189,16 +199,50 @@ 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() + 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) // 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": + 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/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index f3baf55b0dd..5e9e2272049 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" @@ -12,6 +13,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" ) @@ -31,10 +33,19 @@ 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 we are not a validator, we don't produce blocks, so node.Ready() hangs. + 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 @@ -72,10 +83,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, + DB: memdb.NewMemDB(), + InitChainerConfig: gnoland.InitChainerConfig{ + GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler, + CacheStdlibLoad: true, + }, } } diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 6dd8050d6b6..43a8fe1fbec 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -38,6 +38,7 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { baseCapKey := store.NewStoreKey("baseCapKey") iavlCapKey := store.NewStoreKey("iavlCapKey") + // Mount db store and iavlstore ms := store.NewCommitMultiStore(db) ms.MountStoreWithDB(baseCapKey, dbadapter.StoreConstructor, db) ms.MountStoreWithDB(iavlCapKey, iavl.StoreConstructor, db) @@ -46,11 +47,18 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { ctx := sdk.NewContext(sdk.RunTxModeDeliver, ms, &bft.Header{ChainID: "test-chain-id"}, log.NewNoopLogger()) acck := authm.NewAccountKeeper(iavlCapKey, std.ProtoBaseAccount) bank := bankm.NewBankKeeper(acck) - stdlibsDir := filepath.Join("..", "..", "..", "..", "gnovm", "stdlibs") - vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, stdlibsDir, 100_000_000) + vmk := NewVMKeeper(baseCapKey, iavlCapKey, acck, bank, 100_000_000) mcw := ms.MultiCacheWrap() - vmk.Initialize(log.NewNoopLogger(), mcw, cacheStdlibs) + vmk.Initialize(log.NewNoopLogger(), mcw) + 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/gas_test.go b/gno.land/pkg/sdk/vm/gas_test.go index de647c8735a..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 @@ -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() @@ -84,6 +88,7 @@ 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() @@ -103,6 +108,7 @@ func TestAddPkgDeliverTxFailedNoGas(t *testing.T) { simulate = false tx.Fee.GasWanted = 2230 gctx := auth.SetGasMeter(simulate, ctx, tx.Fee.GasWanted) + gctx = vmHandler.vm.MakeGnoTransactionStore(gctx) var res sdk.Result abort := false @@ -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 615be2029fe..40d253ed456 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -34,8 +34,8 @@ import ( ) const ( - maxAllocTx = 500 * 1000 * 1000 - maxAllocQuery = 1500 * 1000 * 1000 // higher limit for queries + maxAllocTx = 500_000_000 + maxAllocQuery = 1_500_000_000 // higher limit for queries ) // vm.VMKeeperI defines a module interface that supports Gno @@ -45,17 +45,20 @@ 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) + 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 { - baseKey store.StoreKey - iavlKey store.StoreKey - acck auth.AccountKeeper - bank bank.BankKeeper - stdlibsDir string + baseKey store.StoreKey + iavlKey store.StoreKey + acck auth.AccountKeeper + bank bank.BankKeeper // cached, the DeliverTx persistent state. gnoStore gno.Store @@ -69,17 +72,15 @@ func NewVMKeeper( iavlKey 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, + baseKey: baseKey, + iavlKey: iavlKey, + acck: acck, + bank: bank, + maxCycles: maxCycles, } return vmk } @@ -87,69 +88,28 @@ func NewVMKeeper( func (vm *VMKeeper) Initialize( logger *slog.Logger, ms store.MultiStore, - cacheStdlibLoad bool, ) { 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 { - // No packages in the store; set up the stdlibs. - start := time.Now() + vm.gnoStore = gno.NewStore(alloc, baseStore, iavlStore) + vm.gnoStore.SetNativeStore(stdlibs.NativeStore) - 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 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. 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: "", Output: os.Stdout, // XXX - Store: gnoStore, + Store: vm.gnoStore, }) defer m2.Release() gno.DisableDebug() @@ -159,57 +119,52 @@ func uncachedPackageLoad( logger.Debug("GnoVM packages preprocessed", "elapsed", time.Since(start)) } - return gnoStore } -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 +type stdlibCache struct { + dir string + base store.Store + iavl store.Store + gno gno.Store } -func cachedStdlibLoad(stdlibsDir string, baseStore, iavlStore store.Store) gno.Store { +var ( + cachedStdlibOnce sync.Once + 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 = memdb.NewMemDB() - cachedStdlibIavl = memdb.NewMemDB() - - cachedGnoStore = gno.NewStore(nil, - dbadapter.StoreConstructor(cachedStdlibBase, types.StoreOptions{}), - dbadapter.StoreConstructor(cachedStdlibIavl, types.StoreOptions{})) - cachedGnoStore.SetNativeStore(stdlibs.NativeStore) - loadStdlib(stdlibsDir, cachedGnoStore) - }) + cachedStdlib = stdlibCache{ + dir: stdlibDir, + base: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), + iavl: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), + } - itr := cachedStdlibBase.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - baseStore.Set(itr.Key(), itr.Value()) - } + gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl) + gs.SetNativeStore(stdlibs.NativeStore) + loadStdlib(gs, stdlibDir) + cachedStdlib.gno = gs + }) - itr = cachedStdlibIavl.Iterator(nil, nil) - for ; itr.Valid(); itr.Next() { - iavlStore.Set(itr.Key(), itr.Value()) + 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)) } - alloc := gno.NewAllocator(maxAllocTx) - gs := gno.NewStore(alloc, baseStore, iavlStore) - gs.SetNativeStore(stdlibs.NativeStore) - gno.CopyCachesFromStore(gs, cachedGnoStore) - return gs + gs := vm.getGnoTransactionStore(ctx) + gno.CopyFromCachedStore(gs, cachedStdlib.gno, cachedStdlib.base, cachedStdlib.iavl) } -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.getGnoTransactionStore(ctx) + loadStdlib(gs, stdlibDir) +} -func loadStdlib(stdlibsDir string, store gno.Store) { +func loadStdlib(store gno.Store, stdlibDir string) { stdlibInitList := stdlibs.InitOrder() for _, lib := range stdlibInitList { if lib == "testing" { @@ -217,12 +172,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)) @@ -243,40 +198,29 @@ func loadStdlibPackage(pkgPath, stdlibsDir string, store gno.Store) { m.RunMemPackage(memPkg, true) } -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") - } +type gnoStoreContextKeyType struct{} + +var gnoStoreContextKey gnoStoreContextKeyType + +func (vm *VMKeeper) newGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { + base := ctx.Store(vm.baseKey) + iavl := ctx.Store(vm.iavlKey) + + 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) { + vm.getGnoTransactionStore(ctx).Write() +} + +func (vm *VMKeeper) getGnoTransactionStore(ctx sdk.Context) gno.TransactionStore { + txStore := ctx.Value(gnoStoreContextKey).(gno.TransactionStore) + txStore.ClearObjectCache() + return txStore } // Namespace can be either a user or crypto address. @@ -286,7 +230,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) { @@ -367,7 +311,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() { @@ -464,7 +408,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) @@ -578,7 +522,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 @@ -692,7 +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) { - store := vm.getGnoStore(ctx) + store := vm.newGnoTransactionStore(ctx) // throwaway (never committed) // Ensure pkgPath is realm. if !gno.IsRealmPath(pkgPath) { err = ErrInvalidPkgPath(fmt.Sprintf( @@ -755,7 +699,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.newGnoTransactionStore(ctx) // throwaway (never committed) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -822,7 +766,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.newGnoTransactionStore(ctx) // throwaway (never committed) pkgAddr := gno.DerivePkgAddr(pkgPath) // Get Package. pv := gnostore.GetPackage(pkgPath, false) @@ -883,7 +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) { - store := vm.getGnoStore(ctx) + store := vm.newGnoTransactionStore(ctx) // throwaway (never committed) dirpath, filename := std.SplitFilepath(filepath) if filename != "" { memFile := store.GetMemFile(dirpath, filename) diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 75b55c3174a..d6a9703ac7d 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -9,18 +9,23 @@ 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) 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")) @@ -39,12 +44,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.getGnoTransactionStore(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.getGnoTransactionStore(ctx).GetPackage(pkgPath, false)) err = env.vmk.AddPackage(ctx, msg1) @@ -52,7 +57,7 @@ func Echo() string {return "hello world"}`, assert.True(t, errors.Is(err, InvalidPkgPathError{})) // added package is formatted - store := vmk.getGnoStore(ctx) + store := env.vmk.getGnoTransactionStore(ctx) memFile := store.GetMemFile("gno.land/r/test", "test.gno") assert.NotNil(t, memFile) expected := `package test @@ -65,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")) @@ -110,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")) @@ -164,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")) @@ -208,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")) @@ -252,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")) @@ -296,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")) @@ -350,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")) @@ -389,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")) @@ -419,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")) @@ -455,3 +460,59 @@ func Echo(msg string) string { }, ) } + +func TestVMKeeperReinitialize(t *testing.T) { + env := setupTestEnv() + ctx := env.vmk.MakeGnoTransactionStore(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 diff --git a/gnovm/pkg/gnolang/internal/txlog/txlog.go b/gnovm/pkg/gnolang/internal/txlog/txlog.go new file mode 100644 index 00000000000..cda11083672 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/txlog/txlog.go @@ -0,0 +1,141 @@ +// 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, 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. +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] // 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) + } 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) { + // 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 { + return true + } + return yield(k, dirty.v) + } + + // not in dirty + return yield(k, v) + }) + + // iterate over all "new" values (ie. exist in b.dirty but not b.source). + 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..b0780fc8380 --- /dev/null +++ b/gnovm/pkg/gnolang/internal/txlog/txlog_test.go @@ -0,0 +1,553 @@ +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() + + 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/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/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ba60ead28f6..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.Fork() + 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.Fork() + 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 038f4ba894b..8a1743ddf53 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -2,17 +2,16 @@ package gnolang import ( "fmt" - "maps" "reflect" "slices" "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" "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,7 @@ 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. + 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 @@ -73,20 +74,33 @@ type Store interface { LogSwitchRealm(rlmpath string) // to mark change of realm boundaries ClearCache() Print() +} + +// 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 + 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 + 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. @@ -98,28 +112,119 @@ type defaultStore struct { 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), + cacheTypes: txlog.GoMap[TypeID, Type](map[TypeID]Type{}), + cacheNodes: txlog.GoMap[Location, BlockNode](map[Location]BlockNode{}), + + // 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, } 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 + cacheObjects: make(map[ObjectID]Object), + cacheTypes: txlog.Wrap(ds.cacheTypes), + cacheNodes: txlog.Wrap(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 transactionStore{ds2} +} + +type transactionStore struct{ *defaultStore } + +func (t transactionStore) Write() { + t.cacheTypes.(txlog.MapCommitter[TypeID, Type]).Commit() + t.cacheNodes.(txlog.MapCommitter[Location, BlockNode]).Commit() +} + +func (transactionStore) SetPackageGetter(pg PackageGetter) { + panic("SetPackageGetter may not be called 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. +// https://github.com/gnolang/gno/issues/1361 +// 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. -func CopyCachesFromStore(dst, src Store) { - ds, ss := dst.(*defaultStore), src.(*defaultStore) - ds.cacheObjects = maps.Clone(ss.cacheObjects) - ds.cacheTypes = maps.Clone(ss.cacheTypes) - ds.cacheNodes = maps.Clone(ss.cacheNodes) +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()) + } + + ss.cacheTypes.Iterate()(func(k TypeID, v Type) bool { + ds.cacheTypes.Set(k, v) + return true + }) + ss.cacheNodes.Iterate()(func(k Location, v BlockNode) bool { + ds.cacheNodes.Set(k, v) + return true + }) } func (ds *defaultStore) GetAllocator() *Allocator { @@ -399,7 +504,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 @@ -407,7 +511,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. @@ -424,7 +528,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 @@ -435,7 +539,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") @@ -443,14 +547,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. @@ -465,7 +569,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 { @@ -478,7 +582,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. @@ -494,7 +598,7 @@ func (ds *defaultStore) GetBlockNodeSafe(loc Location) BlockNode { loc, bn.GetLocation())) } } - ds.cacheNodes[loc] = bn + ds.cacheNodes.Set(loc, bn) return bn } } @@ -513,7 +617,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 } @@ -582,6 +686,7 @@ func (ds *defaultStore) getMemPackage(path string, isRetry bool) *std.MemPackage } return nil } + var memPkg *std.MemPackage amino.MustUnmarshal(bz, &memPkg) return memPkg @@ -637,47 +742,6 @@ func (ds *defaultStore) ClearObjectCache() { 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 - ds.iavlStore = iavlStore -} - func (ds *defaultStore) SetPackageInjector(inj PackageInjector) { ds.pkgInjector = inj } @@ -693,18 +757,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 @@ -775,8 +827,8 @@ func (ds *defaultStore) LogSwitchRealm(rlmpath string) { func (ds *defaultStore) ClearCache() { ds.cacheObjects = make(map[ObjectID]Object) - ds.cacheTypes = make(map[TypeID]Type) - ds.cacheNodes = make(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) @@ -792,16 +844,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 { + ds.cacheTypes.Iterate()(func(tid TypeID, typ Type) bool { fmt.Printf("- %v: %v\n", tid, stringz.TrimN(fmt.Sprintf("%v", typ), 50)) - } + return true + }) fmt.Println(colors.Yellow("//----------------------------------------")) fmt.Println(colors.Green("defaultStore:cacheNodes...")) - for loc, bn := range ds.cacheNodes { + ds.cacheNodes.Iterate()(func(loc Location, bn BlockNode) bool { fmt.Printf("- %v: %v\n", loc, stringz.TrimN(fmt.Sprintf("%v", bn), 50)) - } + return true + }) fmt.Println(colors.Red("//----------------------------------------")) } diff --git a/gnovm/pkg/gnolang/store_test.go b/gnovm/pkg/gnolang/store_test.go new file mode 100644 index 00000000000..8114291d1b6 --- /dev/null +++ b/gnovm/pkg/gnolang/store_test.go @@ -0,0 +1,99 @@ +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" + 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, storetypes.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) }) +} + +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") +} diff --git a/tm2/pkg/sdk/abci.go b/tm2/pkg/sdk/abci.go index a9cd14e9ed3..0b86518f0b9 100644 --- a/tm2/pkg/sdk/abci.go +++ b/tm2/pkg/sdk/abci.go @@ -16,3 +16,12 @@ 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, 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 +// 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..867a38d680a 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 running transaction messages. + // -------------------- // Volatile state // checkState is set on initialization and reset on Commit. @@ -820,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 @@ -828,6 +836,10 @@ func (app *BaseApp) runTx(mode RunTxMode, txBytes []byte, tx Tx) (result Result) return result } + if app.endTxHook != nil { + app.endTxHook(runMsgCtx, result) + } + // only update state if all messages pass if result.IsOK() { msCache.MultiWrite() diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 1680b99a5c6..c8884533b30 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,47 @@ func TestLoadVersionInvalid(t *testing.T) { require.Error(t, err) } +func TestOptionSetters(t *testing.T) { + t.Parallel() + + 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.Parallel() + + 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 +313,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 +974,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 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) } diff --git a/tm2/pkg/sdk/options.go b/tm2/pkg/sdk/options.go index f174b5501a2..b9840a7510b 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(beginTx BeginTxHook) { + if app.sealed { + panic("SetBeginTxHook() on sealed BaseApp") + } + app.beginTxHook = beginTx +} + +func (app *BaseApp) SetEndTxHook(endTx EndTxHook) { + if app.sealed { + panic("SetEndTxHook() on sealed BaseApp") + } + app.endTxHook = endTx +}