diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 0b25234f350..e1e90a71838 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -116,6 +116,23 @@ func DefaultNodeConfig(rootdir, domain string) *NodeConfig { } } +type CachePath struct { + data map[string]bool + mu sync.RWMutex +} + +func (cp *CachePath) Set(key string) { + cp.mu.Lock() + defer cp.mu.Unlock() + cp.data[key] = true +} + +func (cp *CachePath) Get(key string) bool { + cp.mu.RLock() + defer cp.mu.RUnlock() + return cp.data[key] +} + // Node is not thread safe type Node struct { *node.Node @@ -129,7 +146,7 @@ type Node struct { pkgs []packages.Package pkgsModifier map[string]QueryPath // path -> QueryPath paths []string - + cachepath *CachePath // keep track of number of loaded package to be able to skip them on restore loadedPackages int @@ -163,6 +180,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig, pkgpaths ...string) (*Node currentStateIndex: len(cfg.InitialTxs), paths: pkgpaths, pkgsModifier: pkgsModifier, + cachepath: &CachePath{data: make(map[string]bool)}, } // XXX: MOVE THIS, passing context here can be confusing @@ -280,6 +298,11 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil { return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr) } + for _, msg := range tx.Msgs { + if addpkg, ok := msg.(vm.MsgAddPackage); ok && addpkg.Package != nil { + n.cachepath.Set(addpkg.Package.Path) + } + } metaTxs = append(metaTxs, gnoland.TxWithMetadata{ Tx: tx, @@ -423,6 +446,11 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata func (n *Node) generateTxs(fee std.Fee, pkgs []packages.Package) []gnoland.TxWithMetadata { metatxs := make([]gnoland.TxWithMetadata, 0, len(pkgs)) for _, pkg := range pkgs { + if n.cachepath.Get(pkg.Path) { + n.logger.Error("Can't reload package, package conflict in ", "path", pkg.Path) + continue + } + msg := vm.MsgAddPackage{ Creator: n.config.DefaultCreator, Deposit: n.config.DefaultDeposit, @@ -508,7 +536,6 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // Generate txs pkgsTxs := n.generateTxs(DefaultFee, pkgs) genesis.Txs = append(pkgsTxs, state...) - // Reset the node with the new genesis state. err = n.rebuildNode(ctx, genesis) n.logger.Info("reload done", @@ -520,7 +547,6 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error { // Update node infos n.pkgs = pkgs n.loadedPackages = len(pkgsTxs) - // Emit reload event n.emitter.Emit(&events.Reload{}) return nil diff --git a/contribs/gnodev/pkg/dev/node_test.go b/contribs/gnodev/pkg/dev/node_test.go index 1a9c041d37e..44defa6c9b6 100644 --- a/contribs/gnodev/pkg/dev/node_test.go +++ b/contribs/gnodev/pkg/dev/node_test.go @@ -1,8 +1,10 @@ package dev import ( + "bytes" "context" "encoding/json" + "log/slog" "testing" "time" @@ -78,6 +80,89 @@ func Render(_ string) string { return "foo" } require.NoError(t, node.Close()) } +// TestNewNode_WithPackage tests the NewDevNode with a single package. +func TestNewNode_ConflictPackage(t *testing.T) { + // Define 2 packages with same path + fooPkg := std.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*std.MemFile{ + { + Name: "foo.gno", + Body: `package foo +func Render(_ string) string { return "foo" } +`, + }, + }, + } + + conflictPkg := std.MemPackage{ + Name: "foo", + Path: "gno.land/r/dev/foo", + Files: []*std.MemFile{ + { + Name: "foo.gno", + Body: `package foo + func Render(_ string) string { return "conflict" } + `, + }, + }, + } + + node, emitter := newTestingDevNode(t) + + // Create a message to add the package + pkg := vm.MsgAddPackage{ + Package: &fooPkg, + Deposit: std.Coins{{Denom: "ugnot", Amount: 1000}}, + } + + // Add the realm to the node + res, err := testingAddRealm(t, node, pkg) + require.NoError(t, err) + require.NoError(t, res.CheckTx.Error) + require.NoError(t, res.DeliverTx.Error) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult) + + // Check that the realm has been added + render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "foo", render) + + // Update the node's loader with the conflicting package + conflictLoader := packages.NewLoader(packages.NewMockResolver(&conflictPkg)) + node.loader = conflictLoader + + node.AddPackagePaths(conflictPkg.Path) + + // Create a buffer to capture logs + var logBuffer bytes.Buffer + + // Create a custom logger that writes to the buffer + customLogger := slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{ + Level: slog.LevelDebug, + })) + + node.logger = customLogger + + // Reload the node to apply changes from the new loader + err = node.Reload(context.Background()) + require.Equal(t, true, node.cachepath.Get("gno.land/r/dev/foo")) + + require.NoError(t, err) + assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload) + + // Check the log buffer to confirm the error was logged + logOutput := logBuffer.String() + assert.Contains(t, logOutput, "Can't reload package, package conflict in") + assert.Contains(t, logOutput, "gno.land/r/dev/foo") + + // Check the realm after reload + render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo") + require.NoError(t, err) + require.Equal(t, "foo", render) +} + func TestNodeAddPackage(t *testing.T) { // Setup a Node instance fooPkg := std.MemPackage{ @@ -569,6 +654,42 @@ func testingCallRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxC return cli.Call(bcfg, vmMsgs...) } +func testingAddRealm(t *testing.T, node *Node, msgs ...vm.MsgAddPackage) (*core_types.ResultBroadcastTxCommit, error) { + t.Helper() + + defaultCfg := gnoclient.BaseTxCfg{ + GasFee: ugnot.ValueString(1000000), // Gas fee + GasWanted: 3_000_000, // Gas wanted + } + + return testingAddRealmWithConfig(t, node, defaultCfg, msgs...) +} + +func testingAddRealmWithConfig(t *testing.T, node *Node, bcfg gnoclient.BaseTxCfg, msgs ...vm.MsgAddPackage) (*core_types.ResultBroadcastTxCommit, error) { + t.Helper() + + signer := newInMemorySigner(t, node.Config().ChainID()) + cli := gnoclient.Client{ + Signer: signer, + RPCClient: node.Client(), + } + + // Set Creator in the msgs + caller, err := signer.Info() + require.NoError(t, err) + + // Create new messages with the caller set as Creator + newMsgs := make([]vm.MsgAddPackage, 0, len(msgs)) + for _, msg := range msgs { + newMsg := msg + newMsg.Creator = caller.GetAddress() + newMsgs = append(newMsgs, newMsg) + } + + // Call AddPackage + return cli.AddPackage(bcfg, newMsgs...) +} + func newTestingNodeConfig(pkgs ...*std.MemPackage) *NodeConfig { var loader packages.BaseLoader gnoroot := gnoenv.RootDir()