diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 0502c03c86f..572e147a263 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -17,6 +17,7 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gno.land/pkg/integration" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/amino" tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -98,6 +99,9 @@ type Node struct { var DefaultFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { + stdlibsDeployer := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1, FIXME: replace + stdlibsTxs := gnoland.LoadEmbeddedStdlibs(stdlibsDeployer, DefaultFee) + mpkgs, err := NewPackagesMap(cfg.PackagesPathList) if err != nil { return nil, fmt.Errorf("unable map pkgs list: %w", err) @@ -126,7 +130,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) { // generate genesis state genesis := gnoland.GnoGenesisState{ Balances: cfg.BalancesList, - Txs: append(pkgsTxs, cfg.InitialTxs...), + Txs: append(stdlibsTxs, append(pkgsTxs, cfg.InitialTxs...)...), } if err := devnode.rebuildNode(ctx, genesis); err != nil { @@ -280,6 +284,10 @@ func (n *Node) Reset(ctx context.Context) error { // Reset starting time startTime := time.Now() + // Load stdlibs + stdlibsDeployer := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1, FIXME: replace + stdlibsTxs := gnoland.LoadEmbeddedStdlibs(stdlibsDeployer, DefaultFee) + // Generate a new genesis state based on the current packages pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime) if err != nil { @@ -287,7 +295,7 @@ func (n *Node) Reset(ctx context.Context) error { } // Append initialTxs - txs := append(pkgsTxs, n.initialState...) + txs := append(stdlibsTxs, append(pkgsTxs, n.initialState...)...) genesis := gnoland.GnoGenesisState{ Balances: n.config.BalancesList, Txs: txs, @@ -370,7 +378,9 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata // get current genesis state genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState) - initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages + numStdLibs := len(stdlibs.InitOrder()) - 1 // stdlibs count minus testing lib + initialTxs := genesis.Txs[n.loadedPackages+numStdLibs:] // ignore previously loaded packages + state := append([]gnoland.TxWithMetadata{}, initialTxs...) lastBlock := n.getLatestBlockNumber() @@ -491,8 +501,6 @@ func (n *Node) rebuildNode(ctx context.Context, genesis gnoland.GnoGenesisState) // Setup node config nodeConfig := newNodeConfig(n.config.TMConfig, n.config.ChainID, n.config.ChainDomain, genesis) nodeConfig.GenesisTxResultHandler = n.genesisTxResultHandler - // Speed up stdlib loading after first start (saves about 2-3 seconds on each reload). - nodeConfig.CacheStdlibLoad = true nodeConfig.Genesis.ConsensusParams.Block.MaxGas = n.config.MaxGasPerBlock // recoverFromError handles panics and converts them to errors. diff --git a/contribs/gnodev/pkg/dev/node_state.go b/contribs/gnodev/pkg/dev/node_state.go index 73362a5f1c8..0d17b179264 100644 --- a/contribs/gnodev/pkg/dev/node_state.go +++ b/contribs/gnodev/pkg/dev/node_state.go @@ -8,6 +8,7 @@ import ( "github.com/gnolang/gno/contribs/gnodev/pkg/events" "github.com/gnolang/gno/gno.land/pkg/gnoland" bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" ) var ErrEmptyState = errors.New("empty state") @@ -83,6 +84,10 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { return nil } + // Load stdlibs + stdlibsDeployer := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1, FIXME: replace + stdlibsTxs := gnoland.LoadEmbeddedStdlibs(stdlibsDeployer, DefaultFee) + // Load genesis packages pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime) if err != nil { @@ -94,7 +99,7 @@ func (n *Node) MoveBy(ctx context.Context, x int) error { // Create genesis with loaded pkgs + previous state genesis := gnoland.GnoGenesisState{ Balances: n.config.BalancesList, - Txs: append(pkgsTxs, newState...), + Txs: append(stdlibsTxs, append(pkgsTxs, newState...)...), } // Reset the node with the new genesis state. diff --git a/contribs/gnodev/pkg/dev/node_state_test.go b/contribs/gnodev/pkg/dev/node_state_test.go index efaeb979693..8983669f423 100644 --- a/contribs/gnodev/pkg/dev/node_state_test.go +++ b/contribs/gnodev/pkg/dev/node_state_test.go @@ -192,7 +192,7 @@ func Render(_ string) string { return strconv.Itoa(value) } func testingContext(t *testing.T) context.Context { t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*7) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) t.Cleanup(cancel) return ctx } diff --git a/contribs/gnodev/pkg/dev/packages.go b/contribs/gnodev/pkg/dev/packages.go index 62c1907b8c9..500000e2c7b 100644 --- a/contribs/gnodev/pkg/dev/packages.go +++ b/contribs/gnodev/pkg/dev/packages.go @@ -139,7 +139,7 @@ func (pm PackagesMap) Load(fee std.Fee, start time.Time) ([]gnoland.TxWithMetada // Open files in directory as MemPackage. memPkg := gno.MustReadMemPackage(modPkg.Dir, modPkg.Name) - if err := memPkg.Validate(); err != nil { + if err := memPkg.Validate(true); err != nil { return nil, fmt.Errorf("invalid package: %w", err) } diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 77d7e20b8ef..5e2537b778c 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -394,6 +394,9 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro return fmt.Errorf("unable to load genesis balances file %q: %w", c.genesisBalancesFile, err) } + // Load embedded stdlibs + stdlibsTxs := gnoland.LoadEmbeddedStdlibs(genesisDeployAddress, genesisDeployFee) + // Load examples folder examplesDir := filepath.Join(c.gnoRootDir, "examples") pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, genesisDeployAddress, genesisDeployFee) @@ -407,7 +410,7 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro return fmt.Errorf("unable to load genesis txs file: %w", err) } - genesisTxs = append(pkgsTxs, genesisTxs...) + genesisTxs = append(stdlibsTxs, append(pkgsTxs, genesisTxs...)...) // Construct genesis AppState. gen.AppState = gnoland.GnoGenesisState{ diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 2f620fcd360..445d0dee315 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -116,7 +116,7 @@ func TestStart_Lazy(t *testing.T) { io.SetErr(commands.WriteNopCloser(mockErr)) // Create and run the command - ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancelFn := context.WithTimeout(context.Background(), 20*time.Second) defer cancelFn() // Set up the command ctx @@ -128,7 +128,7 @@ func TestStart_Lazy(t *testing.T) { }) // Set up the retry ctx - retryCtx, retryCtxCancelFn := context.WithTimeout(ctx, 5*time.Second) + retryCtx, retryCtxCancelFn := context.WithTimeout(ctx, 10*time.Second) defer retryCtxCancelFn() // This is a very janky way to verify the node has started. diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index e0c93f6194f..23f7a51c5d8 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -10,7 +10,6 @@ import ( "time" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" - "github.com/gnolang/gno/gnovm/pkg/gnoenv" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" bft "github.com/gnolang/gno/tm2/pkg/bft/types" @@ -50,8 +49,6 @@ func TestAppOptions(db dbm.DB) *AppOptions { EventSwitch: events.NewEventSwitch(), InitChainerConfig: InitChainerConfig{ GenesisTxResultHandler: PanicOnFailingTxResultHandler, - StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), - CacheStdlibLoad: true, }, } } @@ -183,7 +180,6 @@ func NewApp( EventSwitch: evsw, InitChainerConfig: InitChainerConfig{ GenesisTxResultHandler: PanicOnFailingTxResultHandler, - StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"), }, } if skipFailingGenesisTxs { @@ -221,14 +217,6 @@ type InitChainerConfig struct { // Handles the results of each genesis transaction. GenesisTxResultHandler - // Standard library directory. - StdlibDir string - // Whether to keep a record of the DB operations to load standard libraries, - // so they can be quickly replicated on additional genesis executions. - // This should be used for integration testing, where InitChainer will be - // called several times. - CacheStdlibLoad bool - // These fields are passed directly by NewAppWithOptions, and should not be // configurable by end-users. baseApp *sdk.BaseApp @@ -243,12 +231,6 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh 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) @@ -270,24 +252,6 @@ func (cfg InitChainerConfig) InitChainer(ctx sdk.Context, req abci.RequestInitCh } } -func (cfg InitChainerConfig) loadStdlibs(ctx sdk.Context) { - // cache-wrapping is necessary for non-validator nodes; in the tm2 BaseApp, - // this is done using BaseApp.cacheTxContext; so we replicate it here. - ms := ctx.MultiStore() - msCache := ms.MultiCacheWrap() - - stdlibCtx := cfg.vmKpr.MakeGnoTransactionStore(ctx) - stdlibCtx = stdlibCtx.WithMultiStore(msCache) - if cfg.CacheStdlibLoad { - cfg.vmKpr.LoadStdlibCached(stdlibCtx, cfg.StdlibDir) - } else { - cfg.vmKpr.LoadStdlib(stdlibCtx, cfg.StdlibDir) - } - cfg.vmKpr.CommitGnoTransactionStore(stdlibCtx) - - msCache.MultiWrite() -} - func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci.ResponseDeliverTx, error) { state, ok := appState.(GnoGenesisState) if !ok { @@ -351,6 +315,10 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci GasUsed: res.GasUsed, }) + if res.IsErr() { + fmt.Println("failed to exec genesis tx, height", ctx.BlockHeight(), "tx", stdTx, "res", res) + } + cfg.GenesisTxResultHandler(ctx, stdTx, res) } return txResponses, nil diff --git a/gno.land/pkg/gnoland/app_test.go b/gno.land/pkg/gnoland/app_test.go index 999e04b2c4b..c53af425243 100644 --- a/gno.land/pkg/gnoland/app_test.go +++ b/gno.land/pkg/gnoland/app_test.go @@ -1,7 +1,6 @@ package gnoland import ( - "context" "errors" "fmt" "strings" @@ -20,9 +19,6 @@ import ( "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" ) @@ -147,87 +143,6 @@ func TestNewApp(t *testing.T) { 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() @@ -287,30 +202,31 @@ func TestInitChainer_MetadataTxs(t *testing.T) { var ( currentTimestamp = time.Now() laterTimestamp = currentTimestamp.Add(10 * 24 * time.Hour) // 10 days + stdlibsDeployFee = std.NewFee(50000, std.NewCoin("ugnot", 1000000)) getMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState { + stdlibs := LoadEmbeddedStdlibs(balances[0].Address, stdlibsDeployFee) + return GnoGenesisState{ // Set the package deployment as the genesis tx - Txs: []TxWithMetadata{ - { - Tx: tx, - Metadata: &GnoTxMetadata{ - Timestamp: laterTimestamp.Unix(), - }, + Txs: append(stdlibs, TxWithMetadata{ + Tx: tx, + Metadata: &GnoTxMetadata{ + Timestamp: laterTimestamp.Unix(), }, - }, + }), // Make sure the deployer account has a balance Balances: balances, } } getNonMetadataState = func(tx std.Tx, balances []Balance) GnoGenesisState { + stdlibs := LoadEmbeddedStdlibs(balances[0].Address, stdlibsDeployFee) + return GnoGenesisState{ - Txs: []TxWithMetadata{ - { - Tx: tx, - }, - }, + Txs: append(stdlibs, TxWithMetadata{ + Tx: tx, + }), Balances: balances, } } @@ -396,7 +312,7 @@ func TestInitChainer_MetadataTxs(t *testing.T) { { // Make sure the deployer account has a balance Address: key.PubKey().Address(), - Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)), + Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000_000)), }, }), }) diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go index 778121d59ed..a69cfe86cea 100644 --- a/gno.land/pkg/gnoland/genesis.go +++ b/gno.land/pkg/gnoland/genesis.go @@ -8,6 +8,7 @@ import ( vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" + "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/amino" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -169,7 +170,7 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C // Open files in directory as MemPackage. memPkg := gno.MustReadMemPackage(pkg.Dir, pkg.Name) - err := memPkg.Validate() + err := memPkg.Validate(true) if err != nil { return tx, fmt.Errorf("invalid package: %w", err) } @@ -187,3 +188,29 @@ func LoadPackage(pkg gnomod.Pkg, creator bft.Address, fee std.Fee, deposit std.C return tx, nil } + +func LoadEmbeddedStdlibs(deployer crypto.Address, fee std.Fee) []TxWithMetadata { + pkgs := stdlibs.EmbeddedMemPackages() + stdlibsTxs := make([]TxWithMetadata, 0, len(pkgs)-1) + + for _, memPkg := range pkgs { + if memPkg.Path == stdlibs.TestingLib { + continue + } + + tx := TxWithMetadata{Tx: std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: deployer, + Package: memPkg, + }, + }, + }} + + tx.Tx.Signatures = make([]std.Signature, len(tx.Tx.GetSigners())) + stdlibsTxs = append(stdlibsTxs, tx) + } + + return stdlibsTxs +} diff --git a/gno.land/pkg/gnoland/mock_test.go b/gno.land/pkg/gnoland/mock_test.go index 62aecaf5278..bf11d0ccee5 100644 --- a/gno.land/pkg/gnoland/mock_test.go +++ b/gno.land/pkg/gnoland/mock_test.go @@ -50,8 +50,6 @@ type mockVMKeeper struct { 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) } @@ -88,18 +86,6 @@ 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) diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index f42166411c8..4089b54a217 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "log/slog" - "path/filepath" "time" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -26,7 +25,6 @@ type InMemoryNodeConfig struct { DB *memdb.MemDB // will be initialized if nil VMOutput io.Writer // optional - // If StdlibDir not set, then it's filepath.Join(TMConfig.RootDir, "gnovm", "stdlibs") InitChainerConfig } @@ -102,9 +100,6 @@ 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() diff --git a/gno.land/pkg/integration/load_bench_test.go b/gno.land/pkg/integration/load_bench_test.go new file mode 100644 index 00000000000..5e9cbce7e50 --- /dev/null +++ b/gno.land/pkg/integration/load_bench_test.go @@ -0,0 +1,48 @@ +package integration + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" + "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db/memdb" + tm2Log "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" +) + +func BenchmarkTestingNodeInit(b *testing.B) { + b.StopTimer() + + gnoRootDir := gnoenv.RootDir() + genesis := &gnoland.GnoGenesisState{ + Balances: LoadDefaultGenesisBalanceFile(b, gnoRootDir), + Params: LoadDefaultGenesisParamFile(b, gnoRootDir), + Txs: []gnoland.TxWithMetadata{}, + } + logger := tm2Log.NewNoopLogger() + pkgs := newPkgsLoader() + + b.StartTimer() + + creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + + // get packages + pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil) + if err != nil { + b.Fatalf("unable to load packages txs: %s", err) + } + + // Generate config and node + cfg := TestingMinimalNodeConfig(b, gnoRootDir) + genesis.Txs = pkgsTxs + + // setup genesis state + cfg.Genesis.AppState = *genesis + + cfg.DB = memdb.NewMemDB() // so it can be reused when restarting. + + TestingInMemoryNode(b, logger, cfg) +} diff --git a/gno.land/pkg/integration/testdata/adduserfrom.txtar b/gno.land/pkg/integration/testdata/adduserfrom.txtar index 47ec70b00e6..e6e2da8797c 100644 --- a/gno.land/pkg/integration/testdata/adduserfrom.txtar +++ b/gno.land/pkg/integration/testdata/adduserfrom.txtar @@ -1,14 +1,14 @@ # adduserfrom just mnemonic -adduserfrom user1 'source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast' -stdout 'g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5' +adduserfrom user1 'potato office wash simple fence distance wrist october idea rain resist grit soup margin kangaroo relief warrior wild flash source agree panic pumpkin chapter' +stdout 'g1tklzzh98af35u3czdl5tdvhtf8mxak09x920ka' # adduserfrom mnemonic and account -adduserfrom user2 'source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast' 1 -stdout 'g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4' +adduserfrom user2 'potato office wash simple fence distance wrist october idea rain resist grit soup margin kangaroo relief warrior wild flash source agree panic pumpkin chapter' 1 +stdout 'g16svw053j5fqlq7tpjug6x87ezyn5qy2nzmdlal' # adduserfrom mnemonic, account and index -adduserfrom user3 'source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast' 1 1 -stdout 'g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp' +adduserfrom user3 'potato office wash simple fence distance wrist october idea rain resist grit soup margin kangaroo relief warrior wild flash source agree panic pumpkin chapter' 1 1 +stdout 'g1myq5smwnzr0qcsa6kecqcy4pjjwt776yp6hmqv' ## start a new node gnoland start @@ -17,14 +17,14 @@ gnoland start gnokey query bank/balances/${USER_ADDR_user1} stdout '10000000ugnot' -gnokey query bank/balances/g18e22n23g462drp4pyszyl6e6mwxkaylthgeeq4 +gnokey query bank/balances/g16svw053j5fqlq7tpjug6x87ezyn5qy2nzmdlal stdout '10000000ugnot' gnokey query auth/accounts/${USER_ADDR_user3} stdout 'height: 0' stdout 'data: {' stdout ' "BaseAccount": {' -stdout ' "address": "g1mtmrdmqfu0aryqfl4aw65n35haw2wdjkh5p4cp",' +stdout ' "address": "g1myq5smwnzr0qcsa6kecqcy4pjjwt776yp6hmqv",' stdout ' "coins": "10000000ugnot",' stdout ' "public_key": null,' stdout ' "account_number": "59",' diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index 2a0a4cf1106..786e9b105a0 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -5,10 +5,13 @@ import ( "errors" "flag" "fmt" + "go/parser" + "go/token" "hash/crc32" "log/slog" "os" "path/filepath" + "slices" "strconv" "strings" "testing" @@ -143,6 +146,8 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // test1 must be created outside of the loop below because it is already included in genesis so // attempting to recreate results in it getting overwritten and breaking existing tests that // rely on its address being static. + // NOTE: recreating test1 will break tests because it's used to deploy stdlibs and needs a greater balance than the default + // maybe we should deploy stdlibs with a dedicated account kb.CreateAccount(DefaultAccount_Name, DefaultAccount_Seed, "", "", 0, 0) env.Setenv("USER_SEED_"+DefaultAccount_Name, DefaultAccount_Seed) env.Setenv("USER_ADDR_"+DefaultAccount_Name, DefaultAccount_Address) @@ -183,10 +188,11 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { 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 defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) + + // get packages + pkgs := ts.Value(envKeyPkgsLoader).(*pkgsLoader) // grab logger pkgsTxs, err := pkgs.LoadPackages(creator, defaultFee, nil) if err != nil { ts.Fatalf("unable to load packages txs: %s", err) @@ -198,7 +204,7 @@ func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // Generate config and node cfg := TestingMinimalNodeConfig(t, gnoRootDir) genesis := ts.Value(envKeyGenesis).(*gnoland.GnoGenesisState) - genesis.Txs = append(pkgsTxs, genesis.Txs...) + genesis.Txs = pkgsTxs // setup genesis state cfg.Genesis.AppState = *genesis @@ -640,6 +646,7 @@ func createAccountFrom(env envSetter, kb keys.Keybase, accountName, mnemonic str type pkgsLoader struct { pkgs []gnomod.Pkg + deps map[string][]string // direct dependencies by pkg visited map[string]struct{} // list of occurrences to patchs with the given value @@ -650,13 +657,45 @@ type pkgsLoader struct { func newPkgsLoader() *pkgsLoader { return &pkgsLoader{ pkgs: make([]gnomod.Pkg, 0), + deps: make(map[string][]string), visited: make(map[string]struct{}), patchs: make(map[string]string), } } func (pl *pkgsLoader) List() gnomod.PkgList { - return pl.pkgs + // TODO: probably can be optimized with graph theory + toSort := pl.pkgs[:] + loaded := make(map[string]struct{}) + sorted := []gnomod.Pkg{} + for len(toSort) != 0 { + toRemove := []string{} + for _, elem := range toSort { + deps := pl.deps[elem.Name] + foundUnloaded := false + for _, dep := range deps { + if _, ok := loaded[dep]; ok { + continue + } + foundUnloaded = true + break + } + if !foundUnloaded { + loaded[elem.Name] = struct{}{} + sorted = append(sorted, elem) + toRemove = append(toRemove, elem.Name) + } + } + if len(toRemove) == 0 { + // we can't find a way to correctly order deps, maybe due to a cyclic or missing dep + // return the sorted list and remaining packages + return append(sorted, toSort...) + } + toSort = slices.DeleteFunc(toSort, func(elem gnomod.Pkg) bool { + return slices.Contains(toRemove, elem.Name) + }) + } + return sorted } func (pl *pkgsLoader) SetPatch(replace, with string) { @@ -705,6 +744,8 @@ func (pl *pkgsLoader) LoadPackages(creator bft.Address, fee std.Fee, deposit std } func (pl *pkgsLoader) LoadAllPackagesFromDir(path string) error { + // FIXME: stdlibs are not added here + // list all packages from target path pkgslist, err := gnomod.ListPkgs(path) if err != nil { @@ -713,7 +754,7 @@ func (pl *pkgsLoader) LoadAllPackagesFromDir(path string) error { for _, pkg := range pkgslist { if !pl.exist(pkg) { - pl.add(pkg) + pl.add(pkg, nil) } } @@ -769,21 +810,69 @@ func (pl *pkgsLoader) LoadPackage(modroot string, path, name string) error { if pl.exist(currentPkg) { continue } - pl.add(currentPkg) + + // deps list to correctly order MsgAddPackage list + deps := []string{} + addDep := func(dep string) { + if slices.Contains(deps, dep) { + return + } + deps = append(deps, dep) + } + + // Add stdlibs requirements to the queue and add deps outside of gno.mod to deps list + pkgDir := currentPkg.Dir + entries, err := os.ReadDir(pkgDir) + if err != nil { + return err + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + filename := entry.Name() + if !strings.HasSuffix(filename, ".gno") || strings.HasSuffix(filename, "_test.gno") || strings.HasSuffix(filename, "_filetest.gno") { + continue + } + + filePath := filepath.Join(pkgDir, filename) + fset := token.NewFileSet() + parsed, err := parser.ParseFile(fset, filePath, nil, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("parse %q: %w", filePath, err) + } + for _, imp := range parsed.Imports { + impPkgPath, err := strconv.Unquote(imp.Path.Value) + if err != nil { + return fmt.Errorf("unquote %q: %w", imp.Path.Value, err) + } + addDep(impPkgPath) + if !gnolang.IsStdlib(impPkgPath) { + continue + } + dir := filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs", filepath.FromSlash(impPkgPath)) + queue = append(queue, gnomod.Pkg{Dir: dir, Name: impPkgPath}) + } + } // Add requirements to the queue for _, pkgPath := range currentPkg.Imports { fullPath := filepath.Join(modroot, pkgPath) queue = append(queue, gnomod.Pkg{Dir: fullPath}) + addDep(pkgPath) } + + // fmt.Println("addpkg", currentPkg.Name, currentPkg.Dir) + pl.add(currentPkg, deps) } return nil } -func (pl *pkgsLoader) add(pkg gnomod.Pkg) { +func (pl *pkgsLoader) add(pkg gnomod.Pkg, deps []string) { pl.visited[pkg.Name] = struct{}{} pl.pkgs = append(pl.pkgs, pkg) + pl.deps[pkg.Name] = deps } func (pl *pkgsLoader) exist(pkg gnomod.Pkg) (ok bool) { diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go index 7e34049d352..6f818630359 100644 --- a/gno.land/pkg/integration/testing_node.go +++ b/gno.land/pkg/integration/testing_node.go @@ -91,7 +91,6 @@ func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNode DB: memdb.NewMemDB(), InitChainerConfig: gnoland.InitChainerConfig{ GenesisTxResultHandler: gnoland.PanicOnFailingTxResultHandler, - CacheStdlibLoad: true, }, } } @@ -134,10 +133,13 @@ func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []gno examplesDir := filepath.Join(gnoroot, "examples") defaultFee := std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000))) - txs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee) + + stdlibsTxs := gnoland.LoadEmbeddedStdlibs(creator, defaultFee) + + examplesTxs, err := gnoland.LoadPackagesFromDir(examplesDir, creator, defaultFee) require.NoError(t, err) - return txs + return append(stdlibsTxs, examplesTxs...) } // LoadDefaultGenesisBalanceFile loads the default genesis balance file for testing. diff --git a/gno.land/pkg/sdk/vm/common_test.go b/gno.land/pkg/sdk/vm/common_test.go index 8b1b7d909c1..e0bc56f009b 100644 --- a/gno.land/pkg/sdk/vm/common_test.go +++ b/gno.land/pkg/sdk/vm/common_test.go @@ -3,8 +3,10 @@ package vm // DONTCOVER import ( - "path/filepath" + "fmt" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/log" @@ -27,14 +29,6 @@ type testEnv struct { } func setupTestEnv() testEnv { - return _setupTestEnv(true) -} - -func setupTestEnvCold() testEnv { - return _setupTestEnv(false) -} - -func _setupTestEnv(cacheStdlibs bool) testEnv { db := memdb.NewMemDB() baseCapKey := store.NewStoreKey("baseCapKey") @@ -55,15 +49,38 @@ func _setupTestEnv(cacheStdlibs bool) testEnv { mcw := ms.MultiCacheWrap() 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) - } + loadStdlibs(vmk.gnoStore) vmk.CommitGnoTransactionStore(stdlibCtx) mcw.MultiWrite() vmh := NewHandler(vmk) return testEnv{ctx: ctx, vmk: vmk, bank: bank, acck: acck, vmh: vmh} } + +func loadStdlibs(store gno.Store) { + stdlibInitList := stdlibs.InitOrder() + for _, lib := range stdlibInitList { + if lib == "testing" { + // XXX: testing is skipped for now while it uses testing-only packages + // like fmt and encoding/json + continue + } + loadStdlibPackage(lib, store) + } +} + +func loadStdlibPackage(pkgPath string, store gno.Store) { + memPkg := stdlibs.EmbeddedMemPackage(pkgPath) + if memPkg == nil || memPkg.IsEmpty() { + // no gno files are present + panic(fmt.Sprintf("failed loading stdlib %q: not a valid MemPackage", pkgPath)) + } + + m := gno.NewMachineWithOptions(gno.MachineOptions{ + PkgPath: "gno.land/r/stdlibs/" + pkgPath, + // PkgPath: pkgPath, XXX why? + Store: store, + }) + defer m.Release() + m.RunMemPackage(memPkg, true) +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 00a0544cad6..1982ff204e4 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -8,27 +8,21 @@ import ( "fmt" "io" "log/slog" - "path/filepath" "regexp" "strings" - "sync" "time" "github.com/gnolang/gno/gnovm" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/stdlibs" "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/db/memdb" "github.com/gnolang/gno/tm2/pkg/errors" - osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/sdk" "github.com/gnolang/gno/tm2/pkg/sdk/auth" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/sdk/params" "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/types" "github.com/gnolang/gno/tm2/pkg/telemetry" "github.com/gnolang/gno/tm2/pkg/telemetry/metrics" "go.opentelemetry.io/otel/attribute" @@ -47,8 +41,6 @@ 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) } @@ -125,83 +117,6 @@ func (vm *VMKeeper) Initialize( } } -type stdlibCache struct { - dir string - base store.Store - iavl store.Store - gno 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() { - cachedStdlib = stdlibCache{ - dir: stdlibDir, - base: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), - iavl: dbadapter.StoreConstructor(memdb.NewMemDB(), types.StoreOptions{}), - } - - gs := gno.NewStore(nil, cachedStdlib.base, cachedStdlib.iavl) - gs.SetNativeResolver(stdlibs.NativeResolver) - loadStdlib(gs, stdlibDir) - cachedStdlib.gno = gs - }) - - if stdlibDir != cachedStdlib.dir { - panic(fmt.Sprintf( - "cannot load cached stdlib: cached stdlib is in dir %q; wanted to load stdlib in dir %q", - cachedStdlib.dir, stdlibDir)) - } - - gs := vm.getGnoTransactionStore(ctx) - gno.CopyFromCachedStore(gs, cachedStdlib.gno, cachedStdlib.base, cachedStdlib.iavl) -} - -// 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(store gno.Store, stdlibDir string) { - stdlibInitList := stdlibs.InitOrder() - for _, lib := range stdlibInitList { - if lib == "testing" { - // XXX: testing is skipped for now while it uses testing-only packages - // like fmt and encoding/json - continue - } - loadStdlibPackage(lib, stdlibDir, store) - } -} - -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)) - } - memPkg := gno.MustReadMemPackage(stdlibPath, pkgPath) - if memPkg.IsEmpty() { - // no gno files are present - panic(fmt.Sprintf("failed loading stdlib %q: not a valid MemPackage", pkgPath)) - } - - m := gno.NewMachineWithOptions(gno.MachineOptions{ - // XXX: gno.land, vm.domain, other? - PkgPath: "gno.land/r/stdlibs/" + pkgPath, - // PkgPath: pkgPath, XXX why? - Store: store, - }) - defer m.Release() - m.RunMemPackage(memPkg, true) -} - type gnoStoreContextKeyType struct{} var gnoStoreContextKey gnoStoreContextKeyType @@ -232,6 +147,11 @@ var reNamespace = regexp.MustCompile(`^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/(?:r|p)/([\. // checkNamespacePermission check if the user as given has correct permssion to on the given pkg path func (vm *VMKeeper) checkNamespacePermission(ctx sdk.Context, creator crypto.Address, pkgPath string) error { + // disable check during genesis for stdlibs. TODO: MsgAddStdlib? + if ctx.BlockHeight() == 0 && gno.IsStdlib(pkgPath) { + return nil + } + sysUsersPkg := vm.getSysUsersPkgParam(ctx) if sysUsersPkg == "" { return nil @@ -332,10 +252,11 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) (err error) { if creatorAcc == nil { return std.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", creator)) } - if err := msg.Package.Validate(); err != nil { + allowStdlib := ctx.BlockHeight() == 0 + if err := msg.Package.Validate(allowStdlib); err != nil { return ErrInvalidPkgPath(err.Error()) } - if !strings.HasPrefix(pkgPath, chainDomain+"/") { + if !gno.IsStdlib(pkgPath) && !strings.HasPrefix(pkgPath, chainDomain+"/") { return ErrInvalidPkgPath("invalid domain: " + pkgPath) } if pv := gnostore.GetPackage(pkgPath, false); pv != nil { @@ -552,7 +473,7 @@ func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { if callerAcc == nil { return "", std.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", caller)) } - if err := msg.Package.Validate(); err != nil { + if err := msg.Package.Validate(false); err != nil { return "", ErrInvalidPkgPath(err.Error()) } diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index f8144988c44..0684aecdf12 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -13,13 +13,9 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot" "github.com/gnolang/gno/gnovm" - "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(10_000_000) @@ -479,7 +475,7 @@ func TestVMKeeperRunImportStdlibs(t *testing.T) { // Call Run with stdlibs, "cold" loading the standard libraries func TestVMKeeperRunImportStdlibsColdStdlibLoad(t *testing.T) { - env := setupTestEnvCold() + env := setupTestEnv() testVMKeeperRunImportStdlibs(t, env) } @@ -595,16 +591,3 @@ func Echo(msg string) string { 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/gnovm/cmd/gno/doc.go b/gnovm/cmd/gno/doc.go index 794dd1ba7bb..778316020d1 100644 --- a/gnovm/cmd/gno/doc.go +++ b/gnovm/cmd/gno/doc.go @@ -104,6 +104,7 @@ func execDoc(cfg *docCfg, args []string, io commands.IO) error { } // select dirs from which to gather directories + // TODO: use embedded stdlibs dirs := []string{filepath.Join(cfg.rootDir, "gnovm/stdlibs")} res, err := doc.ResolveDocumentable(dirs, modDirs, args, cfg.unexported) if res == nil { diff --git a/gnovm/cmd/gno/fmt.go b/gnovm/cmd/gno/fmt.go index de6c28c4df0..a096a9a6ccf 100644 --- a/gnovm/cmd/gno/fmt.go +++ b/gnovm/cmd/gno/fmt.go @@ -221,6 +221,7 @@ func fmtFormatFileImports(cfg *fmtCfg, io commands.IO) (fmtProcessFileFunc, erro } // Load stdlibs + // TODO: use embedded stdlibs stdlibs := filepath.Join(gnoroot, "gnovm", "stdlibs") if err := r.LoadPackages(stdlibs, pkgHandler); err != nil { return nil, fmt.Errorf("unable to load %q: %w", stdlibs, err) diff --git a/gnovm/memfile.go b/gnovm/memfile.go index 6988c893dd7..a24cf8cba1b 100644 --- a/gnovm/memfile.go +++ b/gnovm/memfile.go @@ -40,15 +40,16 @@ func (mempkg *MemPackage) IsEmpty() bool { const pathLengthLimit = 256 var ( - rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) - rePkgOrRlmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) - reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`) + rePkgName = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) + rePkgOrRlmPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+$`) + rePkgOrRlmOrStdlibPath = regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}\/(?:p|r)(?:\/_?[a-z]+[a-z0-9_]*)+|(?:[a-z]+[a-z0-9_]*)(?:\/_?[a-z]+[a-z0-9_]*)*$`) + reFileName = regexp.MustCompile(`^([a-zA-Z0-9_]*\.[a-z0-9_\.]*|LICENSE|README)$`) ) // path must not contain any dots after the first domain component. // file names must contain dots. // NOTE: this is to prevent conflicts with nested paths. -func (mempkg *MemPackage) Validate() error { +func (mempkg *MemPackage) Validate(allowStdlib bool) error { // add assertion that MemPkg contains at least 1 file if len(mempkg.Files) <= 0 { return fmt.Errorf("no files found within package %q", mempkg.Name) @@ -62,9 +63,16 @@ func (mempkg *MemPackage) Validate() error { return fmt.Errorf("invalid package name %q, failed to match %q", mempkg.Name, rePkgName) } - if !rePkgOrRlmPath.MatchString(mempkg.Path) { - return fmt.Errorf("invalid package/realm path %q, failed to match %q", mempkg.Path, rePkgOrRlmPath) + if allowStdlib { + if !rePkgOrRlmOrStdlibPath.MatchString(mempkg.Path) { + return fmt.Errorf("invalid package/realm/stdlib path %q, failed to match %q", mempkg.Path, rePkgOrRlmOrStdlibPath) + } + } else { + if !rePkgOrRlmPath.MatchString(mempkg.Path) { + return fmt.Errorf("invalid package/realm path %q, failed to match %q", mempkg.Path, rePkgOrRlmPath) + } } + // enforce sorting files based on Go conventions for predictability sorted := sort.SliceIsSorted( mempkg.Files, diff --git a/gnovm/memfile_test.go b/gnovm/memfile_test.go index 5ef70e9e868..cb292b82b47 100644 --- a/gnovm/memfile_test.go +++ b/gnovm/memfile_test.go @@ -261,7 +261,7 @@ func TestMemPackage_Validate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := tc.mpkg.Validate() + err := tc.mpkg.Validate(false) if tc.errContains != "" { assert.ErrorContains(t, err, tc.errContains) } else { diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 8d3d6d8a2cc..7ade6f7af26 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -1,9 +1,11 @@ package gnolang import ( + "errors" "fmt" "go/parser" "go/token" + "io/fs" "math" "os" "path/filepath" @@ -1151,18 +1153,23 @@ func MustPackageNameFromFileBody(name, body string) Name { return pkgName } -// ReadMemPackage initializes a new MemPackage by reading the OS directory +// ReadMemPackage is a wrapper around [ReadMemPackageFromFS] using an OS filesystem rooted at dir +func ReadMemPackage(dir string, pkgPath string) (*gnovm.MemPackage, error) { + return ReadMemPackageFromFS(os.DirFS(dir), ".", pkgPath) +} + +// ReadMemPackageFromFS initializes a new MemPackage by reading the [fs.FS] directory // at dir, and saving it with the given pkgPath (import path). // The resulting MemPackage will contain the names and content of all *.gno files, // and additionally README.md, LICENSE. // -// ReadMemPackage does not perform validation aside from the package's name; +// ReadMemPackageFromFS does not perform validation aside from the package's name; // the files are not parsed but their contents are merely stored inside a MemFile. // // NOTE: panics if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackage(dir string, pkgPath string) (*gnovm.MemPackage, error) { - files, err := os.ReadDir(dir) +func ReadMemPackageFromFS(fsys fs.FS, dir string, pkgPath string) (*gnovm.MemPackage, error) { + files, err := fs.ReadDir(fsys, dir) if err != nil { return nil, err } @@ -1192,7 +1199,7 @@ func ReadMemPackage(dir string, pkgPath string) (*gnovm.MemPackage, error) { } list = append(list, filepath.Join(dir, file.Name())) } - return ReadMemPackageFromList(list, pkgPath) + return ReadMemPackageFromList([]fs.FS{fsys}, list, pkgPath) } // MustReadMemPackage is a wrapper around [ReadMemPackage] that panics on error. @@ -1210,14 +1217,27 @@ func MustReadMemPackage(dir string, pkgPath string) *gnovm.MemPackage { // // NOTE: errors out if package name is invalid (characters must be alphanumeric or _, // lowercase, and must start with a letter). -func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, error) { +func ReadMemPackageFromList(fss []fs.FS, list []string, pkgPath string) (*gnovm.MemPackage, error) { + if len(fss) == 0 { + return nil, errors.New("no filesystems provided") + } memPkg := &gnovm.MemPackage{Path: pkgPath} var pkgName Name for _, fpath := range list { fname := filepath.Base(fpath) - bz, err := os.ReadFile(fpath) - if err != nil { - return nil, err + var ( + bz []byte + err error + ) + for i, fsys := range fss { + bz, err = fs.ReadFile(fsys, fpath) + if i != len(fss)-1 && os.IsNotExist(err) { + continue + } + if err != nil { + return nil, fmt.Errorf("read %q in fsys #%d: %w", fpath, i, err) + } + break } // XXX: should check that all pkg names are the same (else package is invalid) if pkgName == "" && strings.HasSuffix(fname, ".gno") { @@ -1248,8 +1268,8 @@ func ReadMemPackageFromList(list []string, pkgPath string) (*gnovm.MemPackage, e } // MustReadMemPackageFromList is a wrapper around [ReadMemPackageFromList] that panics on error. -func MustReadMemPackageFromList(list []string, pkgPath string) *gnovm.MemPackage { - pkg, err := ReadMemPackageFromList(list, pkgPath) +func MustReadMemPackageFromList(fss []fs.FS, list []string, pkgPath string) *gnovm.MemPackage { + pkg, err := ReadMemPackageFromList(fss, list, pkgPath) if err != nil { panic(err) } diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index 4cbc2948f43..58330e694d9 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -598,7 +598,7 @@ func (ds *defaultStore) incGetPackageIndexCounter() uint64 { } func (ds *defaultStore) AddMemPackage(memPkg *gnovm.MemPackage) { - memPkg.Validate() // NOTE: duplicate validation. + memPkg.Validate(false) // NOTE: duplicate validation. ctr := ds.incGetPackageIndexCounter() idxkey := []byte(backendPackageIndexKey(ctr)) bz := amino.MustMarshal(memPkg) diff --git a/gnovm/pkg/test/imports.go b/gnovm/pkg/test/imports.go index 731bf9756dd..14cc1ef94d4 100644 --- a/gnovm/pkg/test/imports.go +++ b/gnovm/pkg/test/imports.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "math/big" - "os" "path/filepath" "runtime/debug" "strings" @@ -126,8 +125,8 @@ func Store( return pkg, pkg.NewPackage() } - // Load normal stdlib. - pn, pv = loadStdlib(rootDir, pkgPath, store, stdout) + // load normal stdlib. + pn, pv = loadStdlib(pkgPath, store, stdout) if pn != nil { return } @@ -162,36 +161,12 @@ func Store( return } -func loadStdlib(rootDir, pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { - dirs := [...]string{ - // Normal stdlib path. - filepath.Join(rootDir, "gnovm", "stdlibs", pkgPath), - // Override path. Definitions here override the previous if duplicate. - filepath.Join(rootDir, "gnovm", "tests", "stdlibs", pkgPath), - } - files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files - for _, path := range dirs { - dl, err := os.ReadDir(path) - if err != nil { - if os.IsNotExist(err) { - continue - } - panic(fmt.Errorf("could not access dir %q: %w", path, err)) - } - - for _, f := range dl { - // NOTE: RunMemPackage has other rules; those should be mostly useful - // for on-chain packages (ie. include README and gno.mod). - if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") { - files = append(files, filepath.Join(path, f.Name())) - } - } - } - if len(files) == 0 { +func loadStdlib(pkgPath string, store gno.Store, stdout io.Writer) (*gno.PackageNode, *gno.PackageValue) { + memPkg := teststdlibs.EmbeddedMemPackage(pkgPath) + if memPkg == nil || memPkg.IsEmpty() { return nil, nil } - memPkg := gno.MustReadMemPackageFromList(files, pkgPath) m2 := gno.NewMachineWithOptions(gno.MachineOptions{ // NOTE: see also pkgs/sdk/vm/builtins.go // Needs PkgPath != its name because TestStore.getPackage is the package diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 3ea3d4bc9bd..1d96502ee81 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -314,7 +314,7 @@ func (opts *TestOptions) runTestFiles( m.Alloc = alloc m.SetActivePackage(pv) - testingpv := m.Store.GetPackage("testing", false) + testingpv := m.Store.GetPackage(stdlibs.TestingLib, false) testingtv := gno.TypedValue{T: &gno.PackageType{}, V: testingpv} testingcx := &gno.ConstExpr{TypedValue: testingtv} diff --git a/gnovm/pkg/transpiler/transpiler.go b/gnovm/pkg/transpiler/transpiler.go index bd4bb1b1bc9..502e6999a25 100644 --- a/gnovm/pkg/transpiler/transpiler.go +++ b/gnovm/pkg/transpiler/transpiler.go @@ -89,6 +89,7 @@ func Transpile(source, tags, filename string) (*Result, error) { ctx := &transpileCtx{ rootDir: gnoenv.RootDir(), } + // NOTE: this should be the only place where we get stdlibs from the filesystem stdlibPrefix := filepath.Join(ctx.rootDir, "gnovm", "stdlibs") if isTestFile { // XXX(morgan): this disables checking that a package exists (in examples or stdlibs) diff --git a/gnovm/stdlibs/embedded.go b/gnovm/stdlibs/embedded.go new file mode 100644 index 00000000000..1c045f3e93f --- /dev/null +++ b/gnovm/stdlibs/embedded.go @@ -0,0 +1,52 @@ +package stdlibs + +import ( + "embed" + "sync" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +// embeddedSources embeds the stdlibs. +// Be careful to remove transpile artifacts before building release binaries or they will be included +// +//go:embed */* +var embeddedSources embed.FS + +// EmbeddedSources returns embedded stdlibs sources +func EmbeddedSources() embed.FS { + return embeddedSources +} + +// EmbeddedMemPackages returns a slice of [gnovm.MemPackage] generated from embedded stdlibs sources +func EmbeddedMemPackages() []*gnovm.MemPackage { + return embeddedMemPackages() +} + +// EmbeddedMemPackage returns a mapping of pkg paths to [gnovm.MemPackage] generated from embedded stdlibs sources +func EmbeddedMemPackage(pkgPath string) *gnovm.MemPackage { + return embeddedMemPackagesMap()[pkgPath] +} + +var embeddedMemPackages = sync.OnceValue(func() []*gnovm.MemPackage { + pkgsMap := embeddedMemPackagesMap() + pkgs := make([]*gnovm.MemPackage, len(pkgsMap)) + for i, pkgPath := range initOrder { + pkgs[i] = pkgsMap[pkgPath] + } + return pkgs +}) + +var embeddedMemPackagesMap = sync.OnceValue(func() map[string]*gnovm.MemPackage { + pkgPaths := initOrder + pkgs := make(map[string]*gnovm.MemPackage, len(pkgPaths)) + for _, pkgPath := range pkgPaths { + pkg, err := gnolang.ReadMemPackageFromFS(embeddedSources, pkgPath, pkgPath) + if err != nil { + panic(err) + } + pkgs[pkgPath] = pkg + } + return pkgs +}) diff --git a/gnovm/stdlibs/embedded_test.go b/gnovm/stdlibs/embedded_test.go new file mode 100644 index 00000000000..76aafeca9ac --- /dev/null +++ b/gnovm/stdlibs/embedded_test.go @@ -0,0 +1,316 @@ +package stdlibs + +import ( + "embed" + "fmt" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEmbedTree(t *testing.T) { + actualEmbedTree := dumpEmbedFS(t, embeddedSources, 0, ".") + require.Equal(t, expectedEmbedTree, "\n"+actualEmbedTree) +} + +func dumpEmbedFS(t *testing.T, efs embed.FS, level int, p string) string { + t.Helper() + + s := "" + + dir, err := efs.ReadDir(p) + require.NoError(t, err) + + for _, entry := range dir { + s += fmt.Sprintf("%s%s\n", strings.Repeat(" ", level), entry.Name()) + if entry.IsDir() { + s += dumpEmbedFS(t, efs, level+1, path.Join(p, entry.Name())) + } + } + + return s +} + +const expectedEmbedTree = ` +bufio + bufio.gno + example_test.gno + export_test.gno + scan.gno + scan_test.gno +bytes + boundary_test.gno + buffer.gno + buffer_test.gno + bytes.gno + bytes_test.gno + compare_test.gno + example_test.gno + export_test.gno + reader.gno + reader_test.gno +crypto + chacha20 + chacha + README.md + chacha.gno + chacha_generic.gno + chacha_ref.gno + chacha_test.gno + chacha20.gno + chacha20_test.gno + rand + rand.gno + cipher + README.md + cipher.gno + ed25519 + ed25519.gno + ed25519.go + ed25519_test.gno + sha256 + sha256.gno + sha256.go + sha256_test.gno +encoding + base64 + base64.gno + base64_test.gno + binary + binary.gno + binary_test.gno + encoding.gno + hex + hex.gno + hex_test.gno +errors + README.md + errors.gno + errors_test.gno + example_test.gno +hash + adler32 + adler32.gno + hash.gno + marshal_test.gno +html + entity.gno + entity_test.gno + escape.gno + escape_test.gno +internal + bytealg + bytealg.gno + compare_generic.gno + count_generic.gno + equal_generic.gno + index_generic.gno + indexbyte_generic.gno +io + example_test.gno + io.gno + io_test.gno + multi.gno + multi_test.gno +math + abs.gno + acosh.gno + all_test.gno + asin.gno + asinh.gno + atan.gno + atan2.gno + atanh.gno + bits + bits.gno + bits_errors.gno + bits_tables.gno + bits_test.gno + export_test.gno + bits.gno + cbrt.gno + const.gno + const_test.gno + copysign.gno + dim.gno + erf.gno + erfinv.gno + exp.gno + expm1.gno + export_test.gno + floor.gno + fma.gno + frexp.gno + gamma.gno + hypot.gno + j0.gno + j1.gno + jn.gno + ldexp.gno + lgamma.gno + log.gno + log10.gno + log1p.gno + logb.gno + mod.gno + modf.gno + native.gno + native.go + nextafter.gno + overflow + overflow.gno + overflow_test.gno + pow.gno + pow10.gno + rand + auto_test.gno + example_test.gno + exp.gno + normal.gno + pcg.gno + pcg_test.gno + rand.gno + rand_test.gno + regress_test.gno + zipf.gno + remainder.gno + signbit.gno + sin.gno + sincos.gno + sinh.gno + sqrt.gno + tan.gno + tanh.gno + trig_reduce.gno +net + url + url.gno + url_test.gno +path + match.gno + match_test.gno + path.gno + path_test.gno +regexp + all_test.gno + backtrack.gno + example_test.gno + exec.gno + exec_test.gno + find_test.gno + onepass.gno + onepass_test.gno + regexp.gno + syntax + compile.gno + doc.gno + make_perl_groups.pl + op_string.gno + parse.gno + parse_test.gno + perl_groups.gno + prog.gno + prog_test.gno + regexp.gno + simplify.gno + simplify_test.gno +sort + search.gno + search_test.gno + sort.gno + sort_test.gno +std + addr_set.gno + banker.gno + banker.go + coins.gno + context.go + crypto.gno + crypto_test.gno + emit_event.gno + emit_event.go + emit_event_test.go + frame.gno + native.gno + native.go + native_test.go + package.go + params.gno + params.go +strconv + atob.gno + atob_test.gno + atof.gno + atof_test.gno + atoi.gno + atoi_test.gno + bytealg.gno + decimal.gno + decimal_test.gno + doc.gno + eisel_lemire.gno + example_test.gno + export_test.gno + fp_test.gno + ftoa.gno + ftoa_test.gno + ftoaryu.gno + ftoaryu_test.gno + internal_test.gno + isprint.gno + itoa.gno + itoa_test.gno + quote.gno + quote_test.gno + strconv_test.gno +strings + builder.gno + builder_test.gno + compare.gno + example_test.gno + export_test.gno + printtrie_test.gno + reader.gno + reader_test.gno + replace.gno + replace_test.gno + search.gno + strings.gno +testing + fuzz.gno + fuzz_test.gno + match.gno + random.gno + random_test.gno + testing.gno + testing.go +time + format.gno + time.gno + time.go + timezoneinfo.gno + tzdata.go + zoneinfo_read.gno + zzipdata.go +unicode + README.md + casetables.gno + digit.gno + digit_test.gno + example_test.gno + graphic.gno + graphic_test.gno + letter.gno + letter_test.gno + script_test.gno + tables.gno + utf16 + utf16.gno + utf16_test.gno + utf8 + example_test.gno + utf8.gno + utf8_test.gno +` diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index 3b8b88c1fde..ffa5393032c 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -7,6 +7,8 @@ import ( libsstd "github.com/gnolang/gno/gnovm/stdlibs/std" ) +const TestingLib = "testing" + type ExecContext = libsstd.ExecContext func GetContext(m *gno.Machine) ExecContext { diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2baba6b5005..c7775db5c05 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,7 +13,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) // std/native.gno:45 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 4f749c3a6e1..f826f43851b 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,7 +23,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) // std/native.gno:45 diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 16a374164d5..a982a316491 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -69,7 +69,7 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:8" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:4" // }, // "FileName": "native.gno", // "IsMethod": false, diff --git a/gnovm/tests/stdlibs/embedded.go b/gnovm/tests/stdlibs/embedded.go new file mode 100644 index 00000000000..e8fcd983120 --- /dev/null +++ b/gnovm/tests/stdlibs/embedded.go @@ -0,0 +1,67 @@ +package stdlibs + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + "sync" + + "github.com/gnolang/gno/gnovm" + "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/stdlibs" +) + +// embeddedSources embeds the testing stdlibs. +// Be careful to remove transpile artifacts before building release binaries or they will be included +// +//go:embed */* +var embeddedSources embed.FS + +// EmbeddedMemPackage returns the [gnovm.MemPackage] designated by pkgPath generated from embedded stdlibs sources +func EmbeddedMemPackage(pkgPath string) *gnovm.MemPackage { + return embeddedMemPackages()[pkgPath] +} + +var embeddedMemPackages = sync.OnceValue(func() map[string]*gnovm.MemPackage { + initOrder := stdlibs.InitOrder() + memPkgs := make(map[string]*gnovm.MemPackage, len(initOrder)) + + filesystems := []fs.FS{embeddedSources, stdlibs.EmbeddedSources()} + filesystemsNames := []string{"test", "normal"} + + for _, pkgPath := range initOrder { + files := make([]string, 0, 32) // pre-alloc 32 as a likely high number of files + for i, fsys := range filesystems { + entries, err := fs.ReadDir(fsys, pkgPath) + if err != nil { + if os.IsNotExist(err) { + continue + } + panic(fmt.Errorf("failed to read embedded stdlib %q in %q fsys: %w", pkgPath, filesystemsNames[i], err)) + } + for _, f := range entries { + // NOTE: RunMemPackage has other rules; those should be mostly useful + // for on-chain packages (ie. include README and gno.mod). + fp := filepath.Join(pkgPath, f.Name()) + if !f.IsDir() && strings.HasSuffix(f.Name(), ".gno") && !slices.Contains(files, fp) { + files = append(files, fp) + } + } + } + if len(files) == 0 { + return nil + } + + pkg, err := gnolang.ReadMemPackageFromList(filesystems, files, pkgPath) + if err != nil { + panic(err) + } + memPkgs[pkgPath] = pkg + } + + return memPkgs +}) diff --git a/gnovm/tests/stdlibs/embedded_test.go b/gnovm/tests/stdlibs/embedded_test.go new file mode 100644 index 00000000000..ef412f9185b --- /dev/null +++ b/gnovm/tests/stdlibs/embedded_test.go @@ -0,0 +1,47 @@ +package stdlibs + +import ( + "embed" + "fmt" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEmbedTree(t *testing.T) { + actualEmbedTree := dumpEmbedFS(t, embeddedSources, 0, ".") + require.Equal(t, expectedEmbedTree, "\n"+actualEmbedTree) +} + +func dumpEmbedFS(t *testing.T, efs embed.FS, level int, p string) string { + t.Helper() + + s := "" + + dir, err := efs.ReadDir(p) + require.NoError(t, err) + + for _, entry := range dir { + s += fmt.Sprintf("%s%s\n", strings.Repeat(" ", level), entry.Name()) + if entry.IsDir() { + s += dumpEmbedFS(t, efs, level+1, path.Join(p, entry.Name())) + } + } + + return s +} + +const expectedEmbedTree = ` +std + frame_testing.gno + std.gno + std.go +testing + native_testing.gno + native_testing.go +unicode + natives.gno + natives.go +` diff --git a/misc/genstd/package_sort.go b/misc/genstd/package_sort.go index 575f56d9506..47fcac1abab 100644 --- a/misc/genstd/package_sort.go +++ b/misc/genstd/package_sort.go @@ -5,6 +5,7 @@ import ( "slices" "strings" + "github.com/gnolang/gno/gnovm/stdlibs" "golang.org/x/exp/maps" ) @@ -37,7 +38,7 @@ func sortPackages(pkgs []*pkgData) []string { if slices.Contains(res, imp) { continue } - if pkg.importPath == "testing" && + if pkg.importPath == stdlibs.TestingLib && slices.Contains(nativeInjections, imp) { continue }