From 01d9e8d6337a341344b83002950b42fa05989946 Mon Sep 17 00:00:00 2001 From: Sergio Maria Matone Date: Mon, 30 Sep 2024 15:57:14 +0200 Subject: [PATCH 01/10] feat(ops/gnobro): introducing Release workflow for the Gnobro tool (#2872) Adding support for releasing the image of Gnobro Facilitates #2807
Contributors' checklist... - [*] Added new tests, or not needed, or not feasible - [*] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [*] Updated the official documentation or not needed - [*] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [*] Added references to related issues and PRs - [*] Provided any useful hints for running manual tests
--- .github/goreleaser.yaml | 97 +++++++++++++++++++++++++++++++++++++++++ Dockerfile.release | 10 ++++- 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/.github/goreleaser.yaml b/.github/goreleaser.yaml index 1984493d36f..cd3c62c2ae6 100644 --- a/.github/goreleaser.yaml +++ b/.github/goreleaser.yaml @@ -86,6 +86,21 @@ builds: goarm: - 6 - 7 + - id: gnobro + dir: ./contribs/gnodev/cmd/gnobro + binary: gnobro + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + - arm + goarm: + - 6 + - 7 gomod: proxy: true @@ -489,6 +504,74 @@ dockers: ids: - gnofaucet + # gnobro + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: amd64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64" + build_flag_templates: + - "--target=gnobro" + - "--platform=linux/amd64" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm64 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8" + build_flag_templates: + - "--target=gnobro" + - "--platform=linux/arm64/v8" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm + goarm: 6 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6" + build_flag_templates: + - "--target=gnobro" + - "--platform=linux/arm/v6" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + - use: buildx + dockerfile: Dockerfile.release + goos: linux + goarch: arm + goarm: 7 + image_templates: + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7" + - "ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7" + build_flag_templates: + - "--target=gnobro" + - "--platform=linux/arm/v7" + - "--label=org.opencontainers.image.created={{.Date}}" + - "--label=org.opencontainers.image.title={{.ProjectName}}/gnobro" + - "--label=org.opencontainers.image.revision={{.FullCommit}}" + - "--label=org.opencontainers.image.version={{.Version}}" + ids: + - gnobro + docker_manifests: # https://goreleaser.com/customization/docker_manifest/ @@ -562,6 +645,20 @@ docker_manifests: - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv6 - ghcr.io/gnolang/{{ .ProjectName }}/gnofaucet:{{ .Env.TAG_VERSION }}-armv7 + # gnobro + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Version }}-armv7 + - name_template: ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }} + image_templates: + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-amd64 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-arm64v8 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv6 + - ghcr.io/gnolang/{{ .ProjectName }}/gnobro:{{ .Env.TAG_VERSION }}-armv7 + docker_signs: - cmd: cosign env: diff --git a/Dockerfile.release b/Dockerfile.release index 644f8cb5de9..4887857b5c2 100644 --- a/Dockerfile.release +++ b/Dockerfile.release @@ -18,7 +18,6 @@ EXPOSE 26656 26657 ENTRYPOINT [ "/usr/bin/gnoland" ] - # ## ghcr.io/gnolang/gno/gnokey FROM base as gnokey @@ -26,7 +25,6 @@ FROM base as gnokey COPY ./gnokey /usr/bin/gnokey ENTRYPOINT [ "/usr/bin/gnokey" ] - # ## ghcr.io/gnolang/gno/gnoweb FROM base as gnoweb @@ -43,6 +41,14 @@ COPY ./gnofaucet /usr/bin/gnofaucet EXPOSE 5050 ENTRYPOINT [ "/usr/bin/gnofaucet" ] +# +## ghcr.io/gnolang/gno/gnobro +FROM base as gnobro + +COPY ./gnobro /usr/bin/gnobro +EXPOSE 22 +ENTRYPOINT [ "/usr/bin/gnobro" ] + # ## ghcr.io/gnolang/gno FROM base as gno From 0e84846bc75915e1e8e3321be252484983947daf Mon Sep 17 00:00:00 2001 From: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:19:18 +0700 Subject: [PATCH 02/10] fix(tm2): Improve hash comparison messages in consensus error handling (#2859) Relates to https://github.com/gnolang/gno/issues/2773 **Description:** This PR enhances the error messaging in the **validateBlock** function by formatting the hash values as a hexadecimal string instead of a byte array. This change improves readability during consensus failures
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- tm2/pkg/bft/state/validation.go | 10 +-- tm2/pkg/bft/state/validation_test.go | 122 +++++++++++++++++++++------ 2 files changed, 103 insertions(+), 29 deletions(-) diff --git a/tm2/pkg/bft/state/validation.go b/tm2/pkg/bft/state/validation.go index 10628a5be84..13274b6a38c 100644 --- a/tm2/pkg/bft/state/validation.go +++ b/tm2/pkg/bft/state/validation.go @@ -63,31 +63,31 @@ func validateBlock(stateDB dbm.DB, state State, block *types.Block) error { // Validate app info if !bytes.Equal(block.AppHash, state.AppHash) { - return fmt.Errorf("wrong Block.Header.AppHash. Expected %X, got %v", + return fmt.Errorf("wrong Block.Header.AppHash. Expected %X, got %X", state.AppHash, block.AppHash, ) } if !bytes.Equal(block.ConsensusHash, state.ConsensusParams.Hash()) { - return fmt.Errorf("wrong Block.Header.ConsensusHash. Expected %X, got %v", + return fmt.Errorf("wrong Block.Header.ConsensusHash. Expected %X, got %X", state.ConsensusParams.Hash(), block.ConsensusHash, ) } if !bytes.Equal(block.LastResultsHash, state.LastResultsHash) { - return fmt.Errorf("wrong Block.Header.LastResultsHash. Expected %X, got %v", + return fmt.Errorf("wrong Block.Header.LastResultsHash. Expected %X, got %X", state.LastResultsHash, block.LastResultsHash, ) } if !bytes.Equal(block.ValidatorsHash, state.Validators.Hash()) { - return fmt.Errorf("wrong Block.Header.ValidatorsHash. Expected %X, got %v", + return fmt.Errorf("wrong Block.Header.ValidatorsHash. Expected %X, got %X", state.Validators.Hash(), block.ValidatorsHash, ) } if !bytes.Equal(block.NextValidatorsHash, state.NextValidators.Hash()) { - return fmt.Errorf("wrong Block.Header.NextValidatorsHash. Expected %X, got %v", + return fmt.Errorf("wrong Block.Header.NextValidatorsHash. Expected %X, got %X", state.NextValidators.Hash(), block.NextValidatorsHash, ) diff --git a/tm2/pkg/bft/state/validation_test.go b/tm2/pkg/bft/state/validation_test.go index 7ab9d1035ee..0eadd076be9 100644 --- a/tm2/pkg/bft/state/validation_test.go +++ b/tm2/pkg/bft/state/validation_test.go @@ -1,9 +1,11 @@ package state_test import ( + "fmt" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/bft/mempool/mock" @@ -29,51 +31,123 @@ func TestValidateBlockHeader(t *testing.T) { blockExec := sm.NewBlockExecutor(stateDB, log.NewTestingLogger(t), proxyApp.Consensus(), mock.Mempool{}) lastCommit := types.NewCommit(types.BlockID{}, nil) - // some bad values + validHash := tmhash.Sum([]byte("this hash is valid")) wrongHash := tmhash.Sum([]byte("this hash is wrong")) + wrongAddress := ed25519.GenPrivKey().PubKey().Address() + invalidAddress := crypto.Address{} + // Manipulation of any header field causes failure. testCases := []struct { name string malleateBlock func(block *types.Block) + expectedError string }{ - {"BlockVersion wrong", func(block *types.Block) { block.Version += "-wrong" }}, - {"AppVersion wrong", func(block *types.Block) { block.AppVersion += "-wrong" }}, - {"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }}, - {"Height wrong", func(block *types.Block) { block.Height += 10 }}, - {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }}, - {"NumTxs wrong", func(block *types.Block) { block.NumTxs += 10 }}, - {"TotalTxs wrong", func(block *types.Block) { block.TotalTxs += 10 }}, - - {"LastBlockID wrong", func(block *types.Block) { block.LastBlockID.PartsHeader.Total += 10 }}, - {"LastCommitHash wrong", func(block *types.Block) { block.LastCommitHash = wrongHash }}, - {"DataHash wrong", func(block *types.Block) { block.DataHash = wrongHash }}, - - {"ValidatorsHash wrong", func(block *types.Block) { block.ValidatorsHash = wrongHash }}, - {"NextValidatorsHash wrong", func(block *types.Block) { block.NextValidatorsHash = wrongHash }}, - {"ConsensusHash wrong", func(block *types.Block) { block.ConsensusHash = wrongHash }}, - {"AppHash wrong", func(block *types.Block) { block.AppHash = wrongHash }}, - {"LastResultsHash wrong", func(block *types.Block) { block.LastResultsHash = wrongHash }}, - - {"Proposer wrong", func(block *types.Block) { block.ProposerAddress = ed25519.GenPrivKey().PubKey().Address() }}, - {"Proposer invalid", func(block *types.Block) { block.ProposerAddress = crypto.Address{} /* zero */ }}, + { + "BlockVersion wrong", + func(block *types.Block) { block.Version += "-wrong" }, + "wrong Block.Header.Version", + }, + { + "AppVersion wrong", + func(block *types.Block) { block.AppVersion += "-wrong" }, + "wrong Block.Header.AppVersion", + }, + { + "ChainID wrong", + func(block *types.Block) { block.ChainID = "not-the-real-one" }, + "wrong Block.Header.ChainID", + }, + { + "Height wrong", + func(block *types.Block) { block.Height += 10 }, + "", + }, + { + "Time wrong", + func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }, + "", + }, + { + "NumTxs wrong", + func(block *types.Block) { block.NumTxs += 10 }, + "wrong Header.NumTxs", + }, + { + "TotalTxs wrong", + func(block *types.Block) { block.TotalTxs += 10 }, + "wrong Block.Header.TotalTxs", + }, + { + "LastBlockID wrong", + func(block *types.Block) { block.LastBlockID.PartsHeader.Total += 10 }, + "wrong Block.Header.LastBlockID", + }, + { + "LastCommitHash wrong", + func(block *types.Block) { block.LastCommitHash = wrongHash }, + "wrong Header.LastCommitHash", + }, + { + "DataHash wrong", + func(block *types.Block) { block.DataHash = wrongHash }, + "wrong Header.DataHash", + }, + { + "ValidatorsHash wrong", + func(block *types.Block) { block.ValidatorsHash = wrongHash }, + "wrong Block.Header.ValidatorsHash", + }, + { + "NextValidatorsHash wrong", + func(block *types.Block) { block.NextValidatorsHash = wrongHash }, + "wrong Block.Header.NextValidatorsHash", + }, + { + "ConsensusHash wrong", + func(block *types.Block) { block.ConsensusHash = wrongHash }, + "wrong Block.Header.ConsensusHash", + }, + { + "AppHash mismatch", + func(block *types.Block) { block.AppHash = wrongHash }, + fmt.Sprintf("wrong Block.Header.AppHash. Expected %X, got %X", validHash, wrongHash), + }, + { + "LastResultsHash wrong", + func(block *types.Block) { block.LastResultsHash = wrongHash }, + fmt.Sprintf("wrong Block.Header.LastResultsHash. Expected %X, got %X", validHash, wrongHash), + }, + { + "Proposer wrong", + func(block *types.Block) { block.ProposerAddress = wrongAddress }, + fmt.Sprintf("Block.Header.ProposerAddress, %X, is not a validator", wrongAddress), + }, + { + "Proposer invalid", + func(block *types.Block) { block.ProposerAddress = invalidAddress /* zero */ }, + fmt.Sprintf("Block.Header.ProposerAddress, %X, is not a validator", invalidAddress), + }, } // Build up state for multiple heights for height := int64(1); height < validationTestsStopHeight; height++ { proposerAddr := state.Validators.GetProposer().Address + state.AppHash = validHash + state.LastResultsHash = validHash + /* - Invalid blocks don't pass + Invalid blocks don't pass */ for _, tc := range testCases { block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, proposerAddr) tc.malleateBlock(block) err := blockExec.ValidateBlock(state, block) - require.Error(t, err, tc.name) + assert.ErrorContains(t, err, tc.expectedError, tc.name) } /* - A good block passes + A good block passes */ var err error state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals) From fb85d0c12913d3c07e751e7c372d6049ee6e370d Mon Sep 17 00:00:00 2001 From: sunspirit <167175638+linhpn99@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:59:56 +0700 Subject: [PATCH 03/10] feat(examples): Implement a two-player Dice Roller game (#2768) ## Description: This PR introduces a basic dice rolling game : **_Dice Roller_** ## Game Rules: > 1. Two players each roll a dice once > 2. Each roll results in a value between 1 and 6 > 3. The player with the highest score will win the game > 4. If both players roll the same value, the game is a draw > 5. No points or stats changes are awarded if you play against yourself ## Purpose: - This package serves to illustrate the application of on-chain randomness using Gno's `p/demo/entropy` and `rand/math` packages. While these packages provide randomness, they are not entirely unpredictable, and their usage in this game is intended to showcase their practical implementation in Gno's realms - Designed with a minimalistic realm to ensure ease of understanding and accessibility for newcomers to Gno realm development ![Screenshot from 2024-09-10 21-56-59](https://github.com/user-attachments/assets/aa3e4c70-2db4-4949-9a50-2e349d51d9a5) You guys can test this game at : https://test4.gno.land/r/g1w6886hdj2tet0seyw6kn8fl92sx06prgd9w9j8/game/v3/diceroller
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Morgan --- .../r/demo/games/dice_roller/dice_roller.gno | 309 ++++++++++++++++++ .../games/dice_roller/dice_roller_test.gno | 139 ++++++++ .../gno.land/r/demo/games/dice_roller/gno.mod | 11 + .../r/demo/games/dice_roller/icon.gno | 55 ++++ 4 files changed, 514 insertions(+) create mode 100644 examples/gno.land/r/demo/games/dice_roller/dice_roller.gno create mode 100644 examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno create mode 100644 examples/gno.land/r/demo/games/dice_roller/gno.mod create mode 100644 examples/gno.land/r/demo/games/dice_roller/icon.gno diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno new file mode 100644 index 00000000000..9dcd67f0dcb --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller.gno @@ -0,0 +1,309 @@ +package dice_roller + +import ( + "errors" + "math/rand" + "sort" + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" + "gno.land/p/demo/entropy" + "gno.land/p/demo/seqid" + "gno.land/p/demo/ufmt" + "gno.land/r/demo/users" +) + +type ( + // game represents a Dice Roller game between two players + game struct { + player1, player2 std.Address + roll1, roll2 int + } + + // player holds the information about each player including their stats + player struct { + addr std.Address + wins, losses, draws, points int + } + + // leaderBoard is a slice of players, used to sort players by rank + leaderBoard []player +) + +const ( + // Constants to represent game result outcomes + ongoing = iota + win + draw + loss +) + +var ( + games avl.Tree // AVL tree for storing game states + gameId seqid.ID // Sequence ID for games + + players avl.Tree // AVL tree for storing player data + + seed = uint64(entropy.New().Seed()) + r = rand.New(rand.NewPCG(seed, 0xdeadbeef)) +) + +// rollDice generates a random dice roll between 1 and 6 +func rollDice() int { + return r.IntN(6) + 1 +} + +// NewGame initializes a new game with the provided opponent's address +func NewGame(addr std.Address) int { + if !addr.IsValid() { + panic("invalid opponent's address") + } + + games.Set(gameId.Next().String(), &game{ + player1: std.PrevRealm().Addr(), + player2: addr, + }) + + return int(gameId) +} + +// Play allows a player to roll the dice and updates the game state accordingly +func Play(idx int) int { + g, err := getGame(idx) + if err != nil { + panic(err) + } + + roll := rollDice() // Random the player's dice roll + + // Play the game and update the player's roll + if err := g.play(std.PrevRealm().Addr(), roll); err != nil { + panic(err) + } + + // If both players have rolled, update the results and leaderboard + if g.isFinished() { + // If the player is playing against themselves, no points are awarded + if g.player1 == g.player2 { + return roll + } + + player1 := getPlayer(g.player1) + player2 := getPlayer(g.player2) + + if g.roll1 > g.roll2 { + player1.updateStats(win) + player2.updateStats(loss) + } else if g.roll2 > g.roll1 { + player2.updateStats(win) + player1.updateStats(loss) + } else { + player1.updateStats(draw) + player2.updateStats(draw) + } + } + + return roll +} + +// play processes a player's roll and updates their score +func (g *game) play(player std.Address, roll int) error { + if player != g.player1 && player != g.player2 { + return errors.New("invalid player") + } + + if g.isFinished() { + return errors.New("game over") + } + + if player == g.player1 && g.roll1 == 0 { + g.roll1 = roll + return nil + } + + if player == g.player2 && g.roll2 == 0 { + g.roll2 = roll + return nil + } + + return errors.New("already played") +} + +// isFinished checks if the game has ended +func (g *game) isFinished() bool { + return g.roll1 != 0 && g.roll2 != 0 +} + +// checkResult returns the game status as a formatted string +func (g *game) status() string { + if !g.isFinished() { + return resultIcon(ongoing) + " Game still in progress" + } + + if g.roll1 > g.roll2 { + return resultIcon(win) + " Player1 Wins !" + } else if g.roll2 > g.roll1 { + return resultIcon(win) + " Player2 Wins !" + } else { + return resultIcon(draw) + " It's a Draw !" + } +} + +// Render provides a summary of the current state of games and leader board +func Render(path string) string { + var sb strings.Builder + + sb.WriteString(`# 🎲 **Dice Roller Game** + +Welcome to Dice Roller! Challenge your friends to a simple yet exciting dice rolling game. Roll the dice and see who gets the highest score ! + +--- + +## **How to Play**: +1. **Create a game**: Challenge an opponent using [NewGame](./dice_roller?help&__func=NewGame) +2. **Roll the dice**: Play your turn by rolling a dice using [Play](./dice_roller?help&__func=Play) + +--- + +## **Scoring Rules**: +- **Win** πŸ†: +3 points +- **Draw** 🀝: +1 point each +- **Lose** ❌: No points +- **Playing against yourself**: No points or stats changes for you + +--- + +## **Recent Games**: +Below are the results from the most recent games. Up to 10 recent games are displayed + +| Game | Player 1 | 🎲 Roll 1 | Player 2 | 🎲 Roll 2 | πŸ† Winner | +|------|----------|-----------|----------|-----------|-----------| +`) + + maxGames := 10 + for n := int(gameId); n > 0 && int(gameId)-n < maxGames; n-- { + g, err := getGame(n) + if err != nil { + continue + } + + sb.WriteString(strconv.Itoa(n) + " | " + + "" + shortName(g.player1) + "" + " | " + diceIcon(g.roll1) + " | " + + "" + shortName(g.player2) + "" + " | " + diceIcon(g.roll2) + " | " + + g.status() + "\n") + } + + sb.WriteString(` +--- + +## **Leaderboard**: +The top players are ranked by performance. Games played against oneself are not counted in the leaderboard + +| Rank | Player | Wins | Losses | Draws | Points | +|------|-----------------------|------|--------|-------|--------| +`) + + for i, player := range getLeaderBoard() { + sb.WriteString(ufmt.Sprintf("| %s | **%s** | %d | %d | %d | %d |\n", + rankIcon(i+1), + shortName(player.addr), + player.wins, + player.losses, + player.draws, + player.points, + )) + } + + sb.WriteString("\n---\n**Good luck and have fun !** πŸŽ‰") + return sb.String() +} + +// shortName returns a shortened name for the given address +func shortName(addr std.Address) string { + user := users.GetUserByAddress(addr) + if user != nil { + return user.Name + } + if len(addr) < 10 { + return string(addr) + } + return string(addr)[:10] + "..." +} + +// getGame retrieves the game state by its ID +func getGame(idx int) (*game, error) { + v, ok := games.Get(seqid.ID(idx).String()) + if !ok { + return nil, errors.New("game not found") + } + return v.(*game), nil +} + +// updateResult updates the player's stats and points based on the game outcome +func (p *player) updateStats(result int) { + switch result { + case win: + p.wins++ + p.points += 3 + case loss: + p.losses++ + case draw: + p.draws++ + p.points++ + } +} + +// getPlayer retrieves a player or initializes a new one if they don't exist +func getPlayer(addr std.Address) *player { + v, ok := players.Get(addr.String()) + if !ok { + player := &player{ + addr: addr, + } + players.Set(addr.String(), player) + return player + } + + return v.(*player) +} + +// getLeaderBoard generates a leaderboard sorted by points +func getLeaderBoard() leaderBoard { + board := leaderBoard{} + players.Iterate("", "", func(key string, value interface{}) bool { + player := value.(*player) + board = append(board, *player) + return false + }) + + sort.Sort(board) + + return board +} + +// Methods for sorting the leaderboard +func (r leaderBoard) Len() int { + return len(r) +} + +func (r leaderBoard) Less(i, j int) bool { + if r[i].points != r[j].points { + return r[i].points > r[j].points + } + + if r[i].wins != r[j].wins { + return r[i].wins > r[j].wins + } + + if r[i].draws != r[j].draws { + return r[i].draws > r[j].draws + } + + return false +} + +func (r leaderBoard) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} diff --git a/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno new file mode 100644 index 00000000000..2f6770a366f --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/dice_roller_test.gno @@ -0,0 +1,139 @@ +package dice_roller + +import ( + "std" + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" + "gno.land/p/demo/testutils" + "gno.land/p/demo/urequire" +) + +var ( + player1 = testutils.TestAddress("alice") + player2 = testutils.TestAddress("bob") + unknownPlayer = testutils.TestAddress("unknown") +) + +// resetGameState resets the game state for testing +func resetGameState() { + games = avl.Tree{} + gameId = seqid.ID(0) + players = avl.Tree{} +} + +// TestNewGame tests the initialization of a new game +func TestNewGame(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Verify that the game has been correctly initialized + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, player1.String(), g.player1.String()) + urequire.Equal(t, player2.String(), g.player2.String()) + urequire.Equal(t, 0, g.roll1) + urequire.Equal(t, 0, g.roll2) +} + +// TestPlay tests the dice rolling functionality for both players +func TestPlay(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + g, err := getGame(gameID) + urequire.NoError(t, err) + + // Simulate rolling dice for player 1 + roll1 := Play(gameID) + + // Verify player 1's roll + urequire.NotEqual(t, 0, g.roll1) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, 0, g.roll2) // Player 2 hasn't rolled yet + + // Simulate rolling dice for player 2 + std.TestSetOrigCaller(player2) + roll2 := Play(gameID) + + // Verify player 2's roll + urequire.NotEqual(t, 0, g.roll2) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) +} + +// TestPlayAgainstSelf tests the scenario where a player plays against themselves +func TestPlayAgainstSelf(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1) + + // Simulate rolling dice twice by the same player + roll1 := Play(gameID) + roll2 := Play(gameID) + + g, err := getGame(gameID) + urequire.NoError(t, err) + urequire.Equal(t, g.roll1, roll1) + urequire.Equal(t, g.roll2, roll2) +} + +// TestPlayInvalidPlayer tests the scenario where an invalid player tries to play +func TestPlayInvalidPlayer(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player1) + + // Attempt to play as an invalid player + std.TestSetOrigCaller(unknownPlayer) + urequire.PanicsWithMessage(t, "invalid player", func() { + Play(gameID) + }) +} + +// TestPlayAlreadyPlayed tests the scenario where a player tries to play again after already playing +func TestPlayAlreadyPlayed(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Player 1 rolls + Play(gameID) + + // Player 1 tries to roll again + urequire.PanicsWithMessage(t, "already played", func() { + Play(gameID) + }) +} + +// TestPlayBeyondGameEnd tests that playing after both players have finished their rolls fails +func TestPlayBeyondGameEnd(t *testing.T) { + resetGameState() + + std.TestSetOrigCaller(player1) + gameID := NewGame(player2) + + // Play for both players + std.TestSetOrigCaller(player1) + Play(gameID) + std.TestSetOrigCaller(player2) + Play(gameID) + + // Check if the game is over + g, err := getGame(gameID) + urequire.NoError(t, err) + + // Attempt to play more should fail + std.TestSetOrigCaller(player1) + urequire.PanicsWithMessage(t, "game over", func() { + Play(gameID) + }) +} diff --git a/examples/gno.land/r/demo/games/dice_roller/gno.mod b/examples/gno.land/r/demo/games/dice_roller/gno.mod new file mode 100644 index 00000000000..75c6473fa3e --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/gno.mod @@ -0,0 +1,11 @@ +module gno.land/r/demo/games/dice_roller + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/entropy v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/games/dice_roller/icon.gno b/examples/gno.land/r/demo/games/dice_roller/icon.gno new file mode 100644 index 00000000000..3417253e7b1 --- /dev/null +++ b/examples/gno.land/r/demo/games/dice_roller/icon.gno @@ -0,0 +1,55 @@ +package dice_roller + +import ( + "strconv" +) + +// diceIcon returns an icon of the dice roll +func diceIcon(roll int) string { + switch roll { + case 1: + return "🎲1" + case 2: + return "🎲2" + case 3: + return "🎲3" + case 4: + return "🎲4" + case 5: + return "🎲5" + case 6: + return "🎲6" + default: + return "❓" + } +} + +// resultIcon returns the icon representing the result of a game +func resultIcon(result int) string { + switch result { + case ongoing: + return "πŸ”„" + case win: + return "πŸ†" + case loss: + return "❌" + case draw: + return "🀝" + default: + return "❓" + } +} + +// rankIcon returns the icon for a player's rank +func rankIcon(rank int) string { + switch rank { + case 1: + return "πŸ₯‡" + case 2: + return "πŸ₯ˆ" + case 3: + return "πŸ₯‰" + default: + return strconv.Itoa(rank) + } +} From 4f6ca96975f6ae9e3dff0d35d9dcc36a40d1cf85 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 2 Oct 2024 21:02:34 +0900 Subject: [PATCH 04/10] chore(p/grc20): Distinct Event Types for GRC20 Functions (#2749) # Description Currently, the event type for grc20 is uniformly set to `TrasferEvent` (execpt for `Approval` function), which necessitated making RPC calls to check every block. Therefore, I have modified it by adding event types for each function to distinguish them from one another. In this case, we can reduce the number of RPC calls by retrieving data only when the target event type occurs.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
Co-authored-by: Morgan --- examples/gno.land/p/demo/grc/grc20/banker.gno | 8 +++----- examples/gno.land/p/demo/grc/grc20/types.gno | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/p/demo/grc/grc20/banker.gno b/examples/gno.land/p/demo/grc/grc20/banker.gno index f643d3e2635..7a3ebb18ef5 100644 --- a/examples/gno.land/p/demo/grc/grc20/banker.gno +++ b/examples/gno.land/p/demo/grc/grc20/banker.gno @@ -64,7 +64,7 @@ func (b *Banker) Mint(address std.Address, amount uint64) error { b.balances.Set(string(address), newBalance) std.Emit( - TransferEvent, + MintEvent, "from", "", "to", string(address), "value", strconv.Itoa(int(amount)), @@ -90,7 +90,7 @@ func (b *Banker) Burn(address std.Address, amount uint64) error { b.balances.Set(string(address), newBalance) std.Emit( - TransferEvent, + BurnEvent, "from", string(address), "to", "", "value", strconv.Itoa(int(amount)), @@ -146,9 +146,6 @@ func (b *Banker) Transfer(from, to std.Address, amount uint64) error { toBalance := b.BalanceOf(to) fromBalance := b.BalanceOf(from) - // debug. - // println("from", from, "to", to, "amount", amount, "fromBalance", fromBalance, "toBalance", toBalance) - if fromBalance < amount { return ErrInsufficientBalance } @@ -165,6 +162,7 @@ func (b *Banker) Transfer(from, to std.Address, amount uint64) error { "to", to.String(), "value", strconv.Itoa(int(amount)), ) + return nil } diff --git a/examples/gno.land/p/demo/grc/grc20/types.gno b/examples/gno.land/p/demo/grc/grc20/types.gno index fe3aef349d9..201c6638914 100644 --- a/examples/gno.land/p/demo/grc/grc20/types.gno +++ b/examples/gno.land/p/demo/grc/grc20/types.gno @@ -56,6 +56,8 @@ type Token interface { } const ( + MintEvent = "Mint" + BurnEvent = "Burn" TransferEvent = "Transfer" ApprovalEvent = "Approval" ) From 11a5027e724b2c82aac3a1a769b2ad9aa07d27dd Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 2 Oct 2024 21:21:20 +0900 Subject: [PATCH 05/10] feat(p/ufmt): Support more formatting verbs (#2351) # Description Support for several new formatting verbs to the `ufmt` package: `%x`: Outputs byte values as hexadecimal strings. Supports uint8, []uint8, and [32]uint8 types. `%q`: Outputs quoted strings with proper escaping. Supports string type. `%T`: Outputs the type of the value. Supports various built-in types using type switching with interface{}. Particularly in the case of byte slices, previously need to use a loop to convert each element to a string one by one, but now there is no need for that anymore. --------- Co-authored-by: Marc Vertes --- examples/gno.land/p/demo/ufmt/ufmt.gno | 53 +++++++++++++++++++++ examples/gno.land/p/demo/ufmt/ufmt_test.gno | 12 +++++ 2 files changed, 65 insertions(+) diff --git a/examples/gno.land/p/demo/ufmt/ufmt.gno b/examples/gno.land/p/demo/ufmt/ufmt.gno index 55494e32cec..c2abf43c85a 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt.gno @@ -57,6 +57,12 @@ func Println(args ...interface{}) { // %d: formats an integer value using package "strconv". // Currently supports only uint, uint64, int, int64. // %t: formats a boolean value to "true" or "false". +// %x: formats an integer value as a hexadecimal string. +// Currently supports only uint8, []uint8, [32]uint8. +// %c: formats a rune value as a string. +// Currently supports only rune, int. +// %q: formats a string value as a quoted string. +// %T: formats the type of the value. // %%: outputs a literal %. Does not consume an argument. func Sprintf(format string, args ...interface{}) string { // we use runes to handle multi-byte characters @@ -158,6 +164,53 @@ func Sprintf(format string, args ...interface{}) string { default: buf += fallback(verb, v) } + case "x": + switch v := arg.(type) { + case uint8: + buf += strconv.FormatUint(uint64(v), 16) + default: + buf += "(unhandled)" + } + case "q": + switch v := arg.(type) { + case string: + buf += strconv.Quote(v) + default: + buf += "(unhandled)" + } + case "T": + switch arg.(type) { + case bool: + buf += "bool" + case int: + buf += "int" + case int8: + buf += "int8" + case int16: + buf += "int16" + case int32: + buf += "int32" + case int64: + buf += "int64" + case uint: + buf += "uint" + case uint8: + buf += "uint8" + case uint16: + buf += "uint16" + case uint32: + buf += "uint32" + case uint64: + buf += "uint64" + case string: + buf += "string" + case []byte: + buf += "[]byte" + case []rune: + buf += "[]rune" + default: + buf += "unknown" + } // % handled before, as it does not consume an argument default: buf += "(unhandled verb: %" + verb + ")" diff --git a/examples/gno.land/p/demo/ufmt/ufmt_test.gno b/examples/gno.land/p/demo/ufmt/ufmt_test.gno index d53fb39bc44..2a583202a93 100644 --- a/examples/gno.land/p/demo/ufmt/ufmt_test.gno +++ b/examples/gno.land/p/demo/ufmt/ufmt_test.gno @@ -41,6 +41,18 @@ func TestSprintf(t *testing.T) { {"Ò", nil, "Ò"}, {"Hello, World! 😊", nil, "Hello, World! 😊"}, {"unicode formatting: %s", []interface{}{"😊"}, "unicode formatting: 😊"}, + {"invalid hex [%x]", []interface{}{"invalid"}, "invalid hex [(unhandled)]"}, + {"rune as character [%c]", []interface{}{rune('A')}, "rune as character [A]"}, + {"int as character [%c]", []interface{}{int('B')}, "int as character [B]"}, + {"quoted string [%q]", []interface{}{"hello"}, "quoted string [\"hello\"]"}, + {"quoted string with escape [%q]", []interface{}{"\thello\nworld\\"}, "quoted string with escape [\"\\thello\\nworld\\\\\"]"}, + {"invalid quoted string [%q]", []interface{}{123}, "invalid quoted string [(unhandled)]"}, + {"type of bool [%T]", []interface{}{true}, "type of bool [bool]"}, + {"type of int [%T]", []interface{}{123}, "type of int [int]"}, + {"type of string [%T]", []interface{}{"hello"}, "type of string [string]"}, + {"type of []byte [%T]", []interface{}{[]byte{1, 2, 3}}, "type of []byte [[]byte]"}, + {"type of []rune [%T]", []interface{}{[]rune{'a', 'b', 'c'}}, "type of []rune [[]rune]"}, + {"type of unknown [%T]", []interface{}{struct{}{}}, "type of unknown [unknown]"}, // mismatch printing {"%s", []interface{}{nil}, "%!s()"}, {"%s", []interface{}{421}, "%!s(int=421)"}, From bdd91ce102c867b50d425c2f52966b046a061d9f Mon Sep 17 00:00:00 2001 From: grepsuzette <350354+grepsuzette@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:42:17 +0800 Subject: [PATCH 06/10] test(r/demo/tests): add filetest for PrevRealm (#1705) PrevRealm was tested from `gno/examples/gno.land/r/demo/tests/tests_test.gnotest`. As far as I can see in r/demo/tests however, it was not tested for *filetests*. Although `gno test .` passes, depending on whether #1704 is confirmed, there may be more filetests to add in this folder (in a different PR I guess). --------- Co-authored-by: grepsuzette Co-authored-by: Morgan --- .../gno.land/r/demo/tests/z2_filetest.gno | 24 ++++++++++++++++ .../gno.land/r/demo/tests/z3_filetest.gno | 28 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 examples/gno.land/r/demo/tests/z2_filetest.gno create mode 100644 examples/gno.land/r/demo/tests/z3_filetest.gno diff --git a/examples/gno.land/r/demo/tests/z2_filetest.gno b/examples/gno.land/r/demo/tests/z2_filetest.gno new file mode 100644 index 00000000000..147d2c12c6c --- /dev/null +++ b/examples/gno.land/r/demo/tests/z2_filetest.gno @@ -0,0 +1,24 @@ +package main + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" +) + +// When a single realm in the frames, PrevRealm returns the user +// When 2 or more realms in the frames, PrevRealm returns the second to last +func main() { + var ( + eoa = testutils.TestAddress("someone") + rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") + ) + std.TestSetOrigCaller(eoa) + println("tests.GetPrevRealm().Addr(): ", tests.GetPrevRealm().Addr()) + println("tests.GetRSubtestsPrevRealm().Addr(): ", tests.GetRSubtestsPrevRealm().Addr()) +} + +// Output: +// tests.GetPrevRealm().Addr(): g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk +// tests.GetRSubtestsPrevRealm().Addr(): g1gz4ycmx0s6ln2wdrsh4e00l9fsel2wskqa3snq diff --git a/examples/gno.land/r/demo/tests/z3_filetest.gno b/examples/gno.land/r/demo/tests/z3_filetest.gno new file mode 100644 index 00000000000..5430e7f7151 --- /dev/null +++ b/examples/gno.land/r/demo/tests/z3_filetest.gno @@ -0,0 +1,28 @@ +// PKGPATH: gno.land/r/demo/test_test +package test_test + +import ( + "std" + + "gno.land/p/demo/testutils" + "gno.land/r/demo/tests" +) + +func main() { + var ( + eoa = testutils.TestAddress("someone") + rTestsAddr = std.DerivePkgAddr("gno.land/r/demo/tests") + ) + std.TestSetOrigCaller(eoa) + // Contrarily to z2_filetest.gno we EXPECT GetPrevRealms != eoa (#1704) + if addr := tests.GetPrevRealm().Addr(); addr != eoa { + println("want tests.GetPrevRealm().Addr ==", eoa, "got", addr) + } + // When 2 or more realms in the frames, it is also different + if addr := tests.GetRSubtestsPrevRealm().Addr(); addr != rTestsAddr { + println("want GetRSubtestsPrevRealm().Addr ==", rTestsAddr, "got", addr) + } +} + +// Output: +// want tests.GetPrevRealm().Addr == g1wdhk6et0dej47h6lta047h6lta047h6lrnerlk got g1xufrdvnfk6zc9r0nqa23ld3tt2r5gkyvw76q63 From 09b624170ee18b466c09712843d343e3b804a737 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 2 Oct 2024 21:42:39 +0900 Subject: [PATCH 07/10] chore(gnovm/gnofmt): Replace usage of ast.Object with ast.Ident In `gnofmt` (#2546) # Description Removes the usage of the deprecated `ast.Object` type and replaces it with `ast.Ident`. Since `ast.Ident` contains name and location information, I think it would provide the information that needed in most cases. --- gnovm/pkg/gnofmt/processor.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/gnovm/pkg/gnofmt/processor.go b/gnovm/pkg/gnofmt/processor.go index c6484fe6784..3487e3d1598 100644 --- a/gnovm/pkg/gnofmt/processor.go +++ b/gnovm/pkg/gnofmt/processor.go @@ -16,10 +16,15 @@ import ( const tabWidth = 8 +type ( + declMap map[*ast.Ident]ast.Decl + fileMap map[string]*ast.File +) + type parsedPackage struct { error error - files map[string]*ast.File - decls map[*ast.Object]ast.Decl + files fileMap + decls declMap } type Processor struct { @@ -52,7 +57,7 @@ func (p *Processor) FormatImportFromSource(filename string, src any) ([]byte, er } // Collect top level declarations within the source - pkgDecls := make(map[*ast.Object]ast.Decl) + pkgDecls := make(declMap) collectTopDeclaration(nodefile, pkgDecls) // Process and format the parsed node. @@ -129,7 +134,7 @@ func (p *Processor) parseFile(path string, src any) (file *ast.File, err error) } // Helper function to process and format a parsed AST node. -func (p *Processor) processAndFormat(file *ast.File, filename string, topDecls map[*ast.Object]ast.Decl) ([]byte, error) { +func (p *Processor) processAndFormat(file *ast.File, filename string, topDecls declMap) ([]byte, error) { // Collect unresolved unresolved := collectUnresolved(file, topDecls) @@ -167,8 +172,8 @@ func (p *Processor) processPackageFiles(path string, pkg Package) *parsedPackage } pkgc = &parsedPackage{ - decls: make(map[*ast.Object]ast.Decl), - files: map[string]*ast.File{}, + decls: make(declMap), + files: make(fileMap), } pkgc.error = ReadWalkPackage(pkg, func(filename string, r io.Reader, err error) error { if err != nil { @@ -190,31 +195,31 @@ func (p *Processor) processPackageFiles(path string, pkg Package) *parsedPackage } // collectTopDeclaration collects top-level declarations from a single file. -func collectTopDeclaration(file *ast.File, topDecls map[*ast.Object]ast.Decl) { +func collectTopDeclaration(file *ast.File, topDecls declMap) { for _, decl := range file.Decls { switch d := decl.(type) { case *ast.GenDecl: for _, spec := range d.Specs { switch s := spec.(type) { case *ast.TypeSpec: - topDecls[s.Name.Obj] = d + topDecls[s.Name] = d case *ast.ValueSpec: for _, name := range s.Names { - topDecls[name.Obj] = d + topDecls[name] = d } } } case *ast.FuncDecl: // Check for top-level function if d.Recv == nil && d.Name != nil && d.Name.Obj != nil { - topDecls[d.Name.Obj] = d + topDecls[d.Name] = d } } } } // collectUnresolved collects unresolved identifiers and declarations. -func collectUnresolved(file *ast.File, topDecls map[*ast.Object]ast.Decl) map[string]map[string]bool { +func collectUnresolved(file *ast.File, topDecls declMap) map[string]map[string]bool { unresolved := map[string]map[string]bool{} unresolvedList := []*ast.Ident{} for _, u := range file.Unresolved { @@ -233,7 +238,7 @@ func collectUnresolved(file *ast.File, topDecls map[*ast.Object]ast.Decl) map[st ast.Inspect(file, func(n ast.Node) bool { switch e := n.(type) { case *ast.Ident: - if d := topDecls[e.Obj]; d != nil { + if _, ok := topDecls[e]; ok { delete(unresolved, e.Name) } case *ast.SelectorExpr: @@ -260,7 +265,7 @@ func collectUnresolved(file *ast.File, topDecls map[*ast.Object]ast.Decl) map[st } // cleanupPreviousImports removes resolved imports from the unresolved list. -func (p *Processor) cleanupPreviousImports(node *ast.File, knownDecls map[*ast.Object]ast.Decl, unresolved map[string]map[string]bool) { +func (p *Processor) cleanupPreviousImports(node *ast.File, knownDecls declMap, unresolved map[string]map[string]bool) { imports := astutil.Imports(p.fset, node) for _, imps := range imports { for _, imp := range imps { @@ -290,8 +295,8 @@ func (p *Processor) cleanupPreviousImports(node *ast.File, knownDecls map[*ast.O } // Mark knownDecls as resolved - for obj := range knownDecls { - delete(unresolved, obj.Name) + for ident := range knownDecls { + delete(unresolved, ident.Name) } } From 7f39d04ca837304128298dcdc4755d5aad778b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lker=20G=2E=20=C3=96zt=C3=BCrk?= Date: Wed, 2 Oct 2024 18:09:40 +0300 Subject: [PATCH 08/10] chore(codeowners): add codeowners for boardsv2 (#2883) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f13ce49ef45..3870ff30539 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,7 @@ /examples/gno.land/p/demo/avl/ @jaekwon /examples/gno.land/p/demo/bf/ @moul /examples/gno.land/p/demo/blog/ @gnolang/devrels +/examples/gno.land/p/demo/boardsv2/ @ilgooz @jeronimoalbi @moul /examples/gno.land/p/demo/cford32/ @thehowl /examples/gno.land/p/demo/memeland/ @leohhhn /examples/gno.land/p/demo/seqid/ @thehowl @@ -36,6 +37,7 @@ /examples/gno.land/p/demo/ui/ @moul /examples/gno.land/r/demo/ @gnolang/tech-staff @gnolang/devrels /examples/gno.land/r/demo/art/ @moul +/examples/gno.land/r/demo/boardsv2/ @ilgooz @jeronimoalbi @moul /examples/gno.land/r/demo/memeland/ @leohhhn /examples/gno.land/r/demo/tamagotchi/ @moul /examples/gno.land/r/demo/userbook/ @leohhhn From ee2b1fa13189e728e410cbe42809caae8ec57efa Mon Sep 17 00:00:00 2001 From: SunSpirit <48086732+sunspirit99@users.noreply.github.com> Date: Thu, 3 Oct 2024 00:03:27 +0700 Subject: [PATCH 09/10] test(gno.land/sdk/vm): add unit tests for `Msg*.ValidateBasic` (#2855) The `vm Msg` is not yet covered by unit tests
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- gno.land/pkg/sdk/vm/msg_test.go | 271 ++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 gno.land/pkg/sdk/vm/msg_test.go diff --git a/gno.land/pkg/sdk/vm/msg_test.go b/gno.land/pkg/sdk/vm/msg_test.go new file mode 100644 index 00000000000..eaaaa0f0ab2 --- /dev/null +++ b/gno.land/pkg/sdk/vm/msg_test.go @@ -0,0 +1,271 @@ +package vm + +import ( + "testing" + + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/stretchr/testify/assert" +) + +func TestMsgAddPackage_ValidateBasic(t *testing.T) { + t.Parallel() + + creator := crypto.AddressFromPreimage([]byte("addr1")) + pkgName := "test" + pkgPath := "gno.land/r/namespace/test" + files := []*std.MemFile{ + { + Name: "test.gno", + Body: `package test + func Echo() string {return "hello world"}`, + }, + } + + tests := []struct { + name string + msg MsgAddPackage + expectSignBytes string + expectErr error + }{ + { + name: "valid message", + msg: NewMsgAddPackage(creator, pkgPath, files), + expectSignBytes: `{"creator":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt","deposit":"",` + + `"package":{"files":[{"body":"package test\n\t\tfunc Echo() string {return \"hello world\"}",` + + `"name":"test.gno"}],"name":"test","path":"gno.land/r/namespace/test"}}`, + expectErr: nil, + }, + { + name: "missing creator address", + msg: MsgAddPackage{ + Creator: crypto.Address{}, + Package: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: files, + }, + Deposit: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: std.InvalidAddressError{}, + }, + { + name: "missing package path", + msg: MsgAddPackage{ + Creator: creator, + Package: &std.MemPackage{ + Name: pkgName, + Path: "", + Files: files, + }, + Deposit: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidPkgPathError{}, + }, + { + name: "invalid deposit coins", + msg: MsgAddPackage{ + Creator: creator, + Package: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: files, + }, + Deposit: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: -1000, // invalid amount + }}, + }, + expectErr: std.InvalidCoinsError{}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if err := tc.msg.ValidateBasic(); err != nil { + assert.ErrorIs(t, err, tc.expectErr) + } else { + assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes())) + } + }) + } +} + +func TestMsgCall_ValidateBasic(t *testing.T) { + t.Parallel() + + caller := crypto.AddressFromPreimage([]byte("addr1")) + pkgPath := "gno.land/r/namespace/test" + funcName := "MyFunction" + args := []string{"arg1", "arg2"} + + tests := []struct { + name string + msg MsgCall + expectSignBytes string + expectErr error + }{ + { + name: "valid message", + msg: NewMsgCall(caller, std.NewCoins(std.NewCoin("ugnot", 1000)), pkgPath, funcName, args), + expectSignBytes: `{"args":["arg1","arg2"],"caller":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt",` + + `"func":"MyFunction","pkg_path":"gno.land/r/namespace/test","send":"1000ugnot"}`, + expectErr: nil, + }, + { + name: "invalid caller address", + msg: MsgCall{ + Caller: crypto.Address{}, + PkgPath: pkgPath, + Func: funcName, + Args: args, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: std.InvalidAddressError{}, + }, + { + name: "missing package path", + msg: MsgCall{ + Caller: caller, + PkgPath: "", + Func: funcName, + Args: args, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidPkgPathError{}, + }, + { + name: "pkgPath should not be a realm path", + msg: MsgCall{ + Caller: caller, + PkgPath: "gno.land/p/namespace/test", // this is not a valid realm path + Func: funcName, + Args: args, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidPkgPathError{}, + }, + { + name: "missing function name to call", + msg: MsgCall{ + Caller: caller, + PkgPath: pkgPath, + Func: "", + Args: args, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidExprError{}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if err := tc.msg.ValidateBasic(); err != nil { + assert.ErrorIs(t, err, tc.expectErr) + } else { + assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes())) + } + }) + } +} + +func TestMsgRun_ValidateBasic(t *testing.T) { + t.Parallel() + + caller := crypto.AddressFromPreimage([]byte("addr1")) + pkgName := "main" + pkgPath := "gno.land/r/" + caller.String() + "/run" + pkgFiles := []*std.MemFile{ + { + Name: "main.gno", + Body: `package main + func Echo() string {return "hello world"}`, + }, + } + + tests := []struct { + name string + msg MsgRun + expectSignBytes string + expectErr error + }{ + { + name: "valid message", + msg: NewMsgRun(caller, std.NewCoins(std.NewCoin("ugnot", 1000)), pkgFiles), + expectSignBytes: `{"caller":"g14ch5q26mhx3jk5cxl88t278nper264ces4m8nt",` + + `"package":{"files":[{"body":"package main\n\t\tfunc Echo() string {return \"hello world\"}",` + + `"name":"main.gno"}],"name":"main","path":""},` + + `"send":"1000ugnot"}`, + expectErr: nil, + }, + { + name: "invalid caller address", + msg: MsgRun{ + Caller: crypto.Address{}, + Package: &std.MemPackage{ + Name: pkgName, + Path: pkgPath, + Files: pkgFiles, + }, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: std.InvalidAddressError{}, + }, + { + name: "invalid package path", + msg: MsgRun{ + Caller: caller, + Package: &std.MemPackage{ + Name: pkgName, + Path: "gno.land/r/namespace/test", // this is not a valid run path + Files: pkgFiles, + }, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidPkgPathError{}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if err := tc.msg.ValidateBasic(); err != nil { + assert.ErrorIs(t, err, tc.expectErr) + } else { + assert.Equal(t, tc.expectSignBytes, string(tc.msg.GetSignBytes())) + } + }) + } +} From a2b4d4b39349474b5d1b61be018aa820d644a009 Mon Sep 17 00:00:00 2001 From: Miguel Victoria Villaquiran Date: Wed, 2 Oct 2024 23:31:31 +0200 Subject: [PATCH 10/10] feat(stdlibs): add strings.Replacer (#2816) prerequisite of #2802 This pull request ports the files: - replace.go - replace_test.go from the Golang standard library. I added some tags on the code with the hope it will help to review the code and to launch discussion if neccessary. I could after remove these changes ```go // Custom code: XXX_Some_Explanation ( code not present on the original go file) . . . // End of custom code ```
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--- gnovm/stdlibs/strings/printtrie_test.gno | 102 ++++ gnovm/stdlibs/strings/replace.gno | 587 +++++++++++++++++++++++ gnovm/stdlibs/strings/replace_test.gno | 511 ++++++++++++++++++++ 3 files changed, 1200 insertions(+) create mode 100644 gnovm/stdlibs/strings/printtrie_test.gno create mode 100644 gnovm/stdlibs/strings/replace.gno create mode 100644 gnovm/stdlibs/strings/replace_test.gno diff --git a/gnovm/stdlibs/strings/printtrie_test.gno b/gnovm/stdlibs/strings/printtrie_test.gno new file mode 100644 index 00000000000..b5b387b9bca --- /dev/null +++ b/gnovm/stdlibs/strings/printtrie_test.gno @@ -0,0 +1,102 @@ +package strings + +import ( + "testing" +) + +func (r *Replacer) PrintTrie() string { + r.buildOnce() + gen := r.r.(*genericReplacer) + return gen.printNode(&gen.root, 0) +} + +func (r *genericReplacer) printNode(t *trieNode, depth int) (s string) { + if t.priority > 0 { + s += "+" + } else { + s += "-" + } + s += "\n" + + if t.prefix != "" { + s += Repeat(".", depth) + t.prefix + s += r.printNode(t.next, depth+len(t.prefix)) + } else if t.table != nil { + for b, m := range r.mapping { + if int(m) != r.tableSize && t.table[m] != nil { + s += Repeat(".", depth) + string([]byte{byte(b)}) + s += r.printNode(t.table[m], depth+1) + } + } + } + return +} + +func TestGenericTrieBuilding(t *testing.T) { + testCases := []struct{ in, out string }{ + {"abc;abdef;abdefgh;xx;xy;z", `- + a- + .b- + ..c+ + ..d- + ...ef+ + .....gh+ + x- + .x+ + .y+ + z+ + `}, + {"abracadabra;abracadabrakazam;abraham;abrasion", `- + a- + .bra- + ....c- + .....adabra+ + ...........kazam+ + ....h- + .....am+ + ....s- + .....ion+ + `}, + {"aaa;aa;a;i;longerst;longer;long;xx;x;X;Y", `- + X+ + Y+ + a+ + .a+ + ..a+ + i+ + l- + .ong+ + ....er+ + ......st+ + x+ + .x+ + `}, + {"foo;;foo;foo1", `+ + f- + .oo+ + ...1+ + `}, + } + + for _, tc := range testCases { + keys := Split(tc.in, ";") + args := make([]string, len(keys)*2) + for i, key := range keys { + args[i*2] = key + } + + got := NewReplacer(args...).PrintTrie() + // Remove tabs from tc.out + wantbuf := make([]byte, 0, len(tc.out)) + for i := 0; i < len(tc.out); i++ { + if tc.out[i] != '\t' { + wantbuf = append(wantbuf, tc.out[i]) + } + } + want := string(wantbuf) + + if got != want { + t.Errorf("PrintTrie(%q)\ngot\n%swant\n%s", tc.in, got, want) + } + } +} diff --git a/gnovm/stdlibs/strings/replace.gno b/gnovm/stdlibs/strings/replace.gno new file mode 100644 index 00000000000..98a47ad3f81 --- /dev/null +++ b/gnovm/stdlibs/strings/replace.gno @@ -0,0 +1,587 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package strings + +import ( + "io" +) + +// Replacer replaces a list of strings with replacements. +// It is safe for concurrent use by multiple goroutines. +type Replacer struct { + // Custom code: remove variable once of type sync.Once on golang package + // End of custom code + r replacer + oldnew []string +} + +// replacer is the interface that a replacement algorithm needs to implement. +type replacer interface { + Replace(s string) string + WriteString(w io.Writer, s string) (n int, err error) +} + +// NewReplacer returns a new [Replacer] from a list of old, new string +// pairs. Replacements are performed in the order they appear in the +// target string, without overlapping matches. The old string +// comparisons are done in argument order. +// +// NewReplacer panics if given an odd number of arguments. +func NewReplacer(oldnew ...string) *Replacer { + if len(oldnew)%2 == 1 { + panic("strings.NewReplacer: odd argument count") + } + return &Replacer{oldnew: append([]string(nil), oldnew...)} +} + +func (r *Replacer) buildOnce() { + // Custom code: check replacer is null instead of call sync.Once + if r.r != nil { + return + } + // End of custom code + r.r = r.build() + r.oldnew = nil +} + +func (b *Replacer) build() replacer { + oldnew := b.oldnew + if len(oldnew) == 2 && len(oldnew[0]) > 1 { + return makeSingleStringReplacer(oldnew[0], oldnew[1]) + } + + allNewBytes := true + for i := 0; i < len(oldnew); i += 2 { + if len(oldnew[i]) != 1 { + return makeGenericReplacer(oldnew) + } + if len(oldnew[i+1]) != 1 { + allNewBytes = false + } + } + + if allNewBytes { + r := byteReplacer{} + for i := range r { + r[i] = byte(i) + } + // The first occurrence of old->new map takes precedence + // over the others with the same old string. + for i := len(oldnew) - 2; i >= 0; i -= 2 { + o := oldnew[i][0] + n := oldnew[i+1][0] + r[o] = n + } + return &r + } + + r := byteStringReplacer{toReplace: make([]string, 0, len(oldnew)/2)} + // The first occurrence of old->new map takes precedence + // over the others with the same old string. + for i := len(oldnew) - 2; i >= 0; i -= 2 { + o := oldnew[i][0] + n := oldnew[i+1] + // To avoid counting repetitions multiple times. + if r.replacements[o] == nil { + // We need to use string([]byte{o}) instead of string(o), + // to avoid utf8 encoding of o. + // E. g. byte(150) produces string of length 2. + r.toReplace = append(r.toReplace, string([]byte{o})) + } + r.replacements[o] = []byte(n) + + } + return &r +} + +// Replace returns a copy of s with all replacements performed. +func (r *Replacer) Replace(s string) string { + // Custom code: adaptation without sync.Once + r.buildOnce() + // End of custom code + return r.r.Replace(s) +} + +// WriteString writes s to w with all replacements performed. +func (r *Replacer) WriteString(w io.Writer, s string) (n int, err error) { + // Custom code: adaptation without sync.Once + r.buildOnce() + // End of custom code + return r.r.WriteString(w, s) +} + +// trieNode is a node in a lookup trie for prioritized key/value pairs. Keys +// and values may be empty. For example, the trie containing keys "ax", "ay", +// "bcbc", "x" and "xy" could have eight nodes: +// +// n0 - +// n1 a- +// n2 .x+ +// n3 .y+ +// n4 b- +// n5 .cbc+ +// n6 x+ +// n7 .y+ +// +// n0 is the root node, and its children are n1, n4 and n6; n1's children are +// n2 and n3; n4's child is n5; n6's child is n7. Nodes n0, n1 and n4 (marked +// with a trailing "-") are partial keys, and nodes n2, n3, n5, n6 and n7 +// (marked with a trailing "+") are complete keys. +type trieNode struct { + // value is the value of the trie node's key/value pair. It is empty if + // this node is not a complete key. + value string + // priority is the priority (higher is more important) of the trie node's + // key/value pair; keys are not necessarily matched shortest- or longest- + // first. Priority is positive if this node is a complete key, and zero + // otherwise. In the example above, positive/zero priorities are marked + // with a trailing "+" or "-". + priority int + + // A trie node may have zero, one or more child nodes: + // * if the remaining fields are zero, there are no children. + // * if prefix and next are non-zero, there is one child in next. + // * if table is non-zero, it defines all the children. + // + // Prefixes are preferred over tables when there is one child, but the + // root node always uses a table for lookup efficiency. + + // prefix is the difference in keys between this trie node and the next. + // In the example above, node n4 has prefix "cbc" and n4's next node is n5. + // Node n5 has no children and so has zero prefix, next and table fields. + prefix string + next *trieNode + + // table is a lookup table indexed by the next byte in the key, after + // remapping that byte through genericReplacer.mapping to create a dense + // index. In the example above, the keys only use 'a', 'b', 'c', 'x' and + // 'y', which remap to 0, 1, 2, 3 and 4. All other bytes remap to 5, and + // genericReplacer.tableSize will be 5. Node n0's table will be + // []*trieNode{ 0:n1, 1:n4, 3:n6 }, where the 0, 1 and 3 are the remapped + // 'a', 'b' and 'x'. + table []*trieNode +} + +func (t *trieNode) add(key, val string, priority int, r *genericReplacer) { + if key == "" { + if t.priority == 0 { + t.value = val + t.priority = priority + } + return + } + + if t.prefix != "" { + // Need to split the prefix among multiple nodes. + var n int // length of the longest common prefix + for ; n < len(t.prefix) && n < len(key); n++ { + if t.prefix[n] != key[n] { + break + } + } + if n == len(t.prefix) { + t.next.add(key[n:], val, priority, r) + } else if n == 0 { + // First byte differs, start a new lookup table here. Looking up + // what is currently t.prefix[0] will lead to prefixNode, and + // looking up key[0] will lead to keyNode. + var prefixNode *trieNode + if len(t.prefix) == 1 { + prefixNode = t.next + } else { + prefixNode = &trieNode{ + prefix: t.prefix[1:], + next: t.next, + } + } + keyNode := new(trieNode) + t.table = make([]*trieNode, r.tableSize) + t.table[r.mapping[t.prefix[0]]] = prefixNode + t.table[r.mapping[key[0]]] = keyNode + t.prefix = "" + t.next = nil + keyNode.add(key[1:], val, priority, r) + } else { + // Insert new node after the common section of the prefix. + next := &trieNode{ + prefix: t.prefix[n:], + next: t.next, + } + t.prefix = t.prefix[:n] + t.next = next + next.add(key[n:], val, priority, r) + } + } else if t.table != nil { + // Insert into existing table. + m := r.mapping[key[0]] + if t.table[m] == nil { + t.table[m] = new(trieNode) + } + t.table[m].add(key[1:], val, priority, r) + } else { + t.prefix = key + t.next = new(trieNode) + t.next.add("", val, priority, r) + } +} + +func (r *genericReplacer) lookup(s string, ignoreRoot bool) (val string, keylen int, found bool) { + // Iterate down the trie to the end, and grab the value and keylen with + // the highest priority. + bestPriority := 0 + node := &r.root + n := 0 + for node != nil { + if node.priority > bestPriority && !(ignoreRoot && node == &r.root) { + bestPriority = node.priority + val = node.value + keylen = n + found = true + } + + if s == "" { + break + } + if node.table != nil { + index := r.mapping[s[0]] + if int(index) == r.tableSize { + break + } + node = node.table[index] + s = s[1:] + n++ + } else if node.prefix != "" && HasPrefix(s, node.prefix) { + n += len(node.prefix) + s = s[len(node.prefix):] + node = node.next + } else { + break + } + } + return +} + +// genericReplacer is the fully generic algorithm. +// It's used as a fallback when nothing faster can be used. +type genericReplacer struct { + root trieNode + // tableSize is the size of a trie node's lookup table. It is the number + // of unique key bytes. + tableSize int + // mapping maps from key bytes to a dense index for trieNode.table. + mapping [256]byte +} + +func makeGenericReplacer(oldnew []string) *genericReplacer { + r := new(genericReplacer) + // Find each byte used, then assign them each an index. + for i := 0; i < len(oldnew); i += 2 { + key := oldnew[i] + for j := 0; j < len(key); j++ { + r.mapping[key[j]] = 1 + } + } + + for _, b := range r.mapping { + r.tableSize += int(b) + } + + var index byte + for i, b := range r.mapping { + if b == 0 { + r.mapping[i] = byte(r.tableSize) + } else { + r.mapping[i] = index + index++ + } + } + // Ensure root node uses a lookup table (for performance). + r.root.table = make([]*trieNode, r.tableSize) + + for i := 0; i < len(oldnew); i += 2 { + r.root.add(oldnew[i], oldnew[i+1], len(oldnew)-i, r) + } + return r +} + +type appendSliceWriter []byte + +// Write writes to the buffer to satisfy [io.Writer]. +func (w *appendSliceWriter) Write(p []byte) (int, error) { + *w = append(*w, p...) + return len(p), nil +} + +// WriteString writes to the buffer without string->[]byte->string allocations. +func (w *appendSliceWriter) WriteString(s string) (int, error) { + *w = append(*w, s...) + return len(s), nil +} + +type stringWriter struct { + w io.Writer +} + +func (w stringWriter) WriteString(s string) (int, error) { + return w.w.Write([]byte(s)) +} + +func getStringWriter(w io.Writer) io.StringWriter { + sw, ok := w.(io.StringWriter) + if !ok { + sw = stringWriter{w} + } + return sw +} + +func (r *genericReplacer) Replace(s string) string { + buf := make(appendSliceWriter, 0, len(s)) + r.WriteString(&buf, s) + return string(buf) +} + +func (r *genericReplacer) WriteString(w io.Writer, s string) (n int, err error) { + sw := getStringWriter(w) + var last, wn int + var prevMatchEmpty bool + for i := 0; i <= len(s); { + // Fast path: s[i] is not a prefix of any pattern. + if i != len(s) && r.root.priority == 0 { + index := int(r.mapping[s[i]]) + if index == r.tableSize || r.root.table[index] == nil { + i++ + continue + } + } + + // Ignore the empty match iff the previous loop found the empty match. + val, keylen, match := r.lookup(s[i:], prevMatchEmpty) + prevMatchEmpty = match && keylen == 0 + if match { + wn, err = sw.WriteString(s[last:i]) + n += wn + if err != nil { + return + } + wn, err = sw.WriteString(val) + n += wn + if err != nil { + return + } + i += keylen + last = i + continue + } + i++ + } + if last != len(s) { + wn, err = sw.WriteString(s[last:]) + n += wn + } + return +} + +// singleStringReplacer is the implementation that's used when there is only +// one string to replace (and that string has more than one byte). +type singleStringReplacer struct { + finder *stringFinder + // value is the new string that replaces that pattern when it's found. + value string +} + +func makeSingleStringReplacer(pattern string, value string) *singleStringReplacer { + return &singleStringReplacer{finder: makeStringFinder(pattern), value: value} +} + +func (r *singleStringReplacer) Replace(s string) string { + var buf Builder + i, matched := 0, false + for { + match := r.finder.next(s[i:]) + if match == -1 { + break + } + matched = true + buf.Grow(match + len(r.value)) + buf.WriteString(s[i : i+match]) + buf.WriteString(r.value) + i += match + len(r.finder.pattern) + } + if !matched { + return s + } + buf.WriteString(s[i:]) + return buf.String() +} + +func (r *singleStringReplacer) WriteString(w io.Writer, s string) (n int, err error) { + sw := getStringWriter(w) + var i, wn int + for { + match := r.finder.next(s[i:]) + if match == -1 { + break + } + wn, err = sw.WriteString(s[i : i+match]) + n += wn + if err != nil { + return + } + wn, err = sw.WriteString(r.value) + n += wn + if err != nil { + return + } + i += match + len(r.finder.pattern) + } + wn, err = sw.WriteString(s[i:]) + n += wn + return +} + +// byteReplacer is the implementation that's used when all the "old" +// and "new" values are single ASCII bytes. +// The array contains replacement bytes indexed by old byte. +type byteReplacer [256]byte + +func (r *byteReplacer) Replace(s string) string { + var buf []byte // lazily allocated + for i := 0; i < len(s); i++ { + b := s[i] + if r[b] != b { + if buf == nil { + buf = []byte(s) + } + buf[i] = r[b] + } + } + if buf == nil { + return s + } + return string(buf) +} + +func (r *byteReplacer) WriteString(w io.Writer, s string) (n int, err error) { + sw := getStringWriter(w) + last := 0 + for i := 0; i < len(s); i++ { + b := s[i] + if r[b] == b { + continue + } + if last != i { + wn, err := sw.WriteString(s[last:i]) + n += wn + if err != nil { + return n, err + } + } + last = i + 1 + nw, err := w.Write(r[b : int(b)+1]) + n += nw + if err != nil { + return n, err + } + } + if last != len(s) { + nw, err := sw.WriteString(s[last:]) + n += nw + if err != nil { + return n, err + } + } + return n, nil +} + +// byteStringReplacer is the implementation that's used when all the +// "old" values are single ASCII bytes but the "new" values vary in size. +type byteStringReplacer struct { + // replacements contains replacement byte slices indexed by old byte. + // A nil []byte means that the old byte should not be replaced. + replacements [256][]byte + // toReplace keeps a list of bytes to replace. Depending on length of toReplace + // and length of target string it may be faster to use Count, or a plain loop. + // We store single byte as a string, because Count takes a string. + toReplace []string +} + +// countCutOff controls the ratio of a string length to a number of replacements +// at which (*byteStringReplacer).Replace switches algorithms. +// For strings with higher ration of length to replacements than that value, +// we call Count, for each replacement from toReplace. +// For strings, with a lower ratio we use simple loop, because of Count overhead. +// countCutOff is an empirically determined overhead multiplier. +// TODO(tocarip) revisit once we have register-based abi/mid-stack inlining. +const countCutOff = 8 + +func (r *byteStringReplacer) Replace(s string) string { + newSize := len(s) + anyChanges := false + // Is it faster to use Count? + if len(r.toReplace)*countCutOff <= len(s) { + for _, x := range r.toReplace { + if c := Count(s, x); c != 0 { + // The -1 is because we are replacing 1 byte with len(replacements[b]) bytes. + newSize += c * (len(r.replacements[x[0]]) - 1) + anyChanges = true + } + + } + } else { + for i := 0; i < len(s); i++ { + b := s[i] + if r.replacements[b] != nil { + // See above for explanation of -1 + newSize += len(r.replacements[b]) - 1 + anyChanges = true + } + } + } + if !anyChanges { + return s + } + buf := make([]byte, newSize) + j := 0 + for i := 0; i < len(s); i++ { + b := s[i] + if r.replacements[b] != nil { + j += copy(buf[j:], r.replacements[b]) + } else { + buf[j] = b + j++ + } + } + return string(buf) +} + +func (r *byteStringReplacer) WriteString(w io.Writer, s string) (n int, err error) { + sw := getStringWriter(w) + last := 0 + for i := 0; i < len(s); i++ { + b := s[i] + if r.replacements[b] == nil { + continue + } + if last != i { + nw, err := sw.WriteString(s[last:i]) + n += nw + if err != nil { + return n, err + } + } + last = i + 1 + nw, err := w.Write(r.replacements[b]) + n += nw + if err != nil { + return n, err + } + } + if last != len(s) { + var nw int + nw, err = sw.WriteString(s[last:]) + n += nw + } + return +} diff --git a/gnovm/stdlibs/strings/replace_test.gno b/gnovm/stdlibs/strings/replace_test.gno new file mode 100644 index 00000000000..dc4858dcc5c --- /dev/null +++ b/gnovm/stdlibs/strings/replace_test.gno @@ -0,0 +1,511 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package strings_test + +import ( + "bytes" + "fmt" + "strings" + "testing" +) + +var htmlEscaper = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + `"`, """, + "'", "'", +) + +var htmlUnescaper = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + """, `"`, + "'", "'", +) + +// The http package's old HTML escaping function. +func oldHTMLEscape(s string) string { + s = strings.Replace(s, "&", "&", -1) + s = strings.Replace(s, "<", "<", -1) + s = strings.Replace(s, ">", ">", -1) + s = strings.Replace(s, `"`, """, -1) + s = strings.Replace(s, "'", "'", -1) + return s +} + +var capitalLetters = strings.NewReplacer("a", "A", "b", "B") + +// TestReplacer tests the replacer implementations. +func TestReplacer(t *testing.T) { + type testCase struct { + r *strings.Replacer + in, out string + } + var testCases []testCase + + // str converts 0xff to "\xff". This isn't just string(b) since that converts to UTF-8. + str := func(b byte) string { + return string([]byte{b}) + } + var s []string + + // inc maps "\x00"->"\x01", ..., "a"->"b", "b"->"c", ..., "\xff"->"\x00". + s = nil + for i := 0; i < 256; i++ { + s = append(s, str(byte(i)), str(byte(i+1))) + } + inc := strings.NewReplacer(s...) + + // Test cases with 1-byte old strings, 1-byte new strings. + testCases = append(testCases, + testCase{capitalLetters, "brad", "BrAd"}, + testCase{capitalLetters, strings.Repeat("a", (32<<10)+123), strings.Repeat("A", (32<<10)+123)}, + testCase{capitalLetters, "", ""}, + + testCase{inc, "brad", "csbe"}, + testCase{inc, "\x00\xff", "\x01\x00"}, + testCase{inc, "", ""}, + + testCase{strings.NewReplacer("a", "1", "a", "2"), "brad", "br1d"}, + ) + + // repeat maps "a"->"a", "b"->"bb", "c"->"ccc", ... + s = nil + for i := 0; i < 256; i++ { + n := i + 1 - 'a' + if n < 1 { + n = 1 + } + s = append(s, str(byte(i)), strings.Repeat(str(byte(i)), n)) + } + repeat := strings.NewReplacer(s...) + + // Test cases with 1-byte old strings, variable length new strings. + testCases = append(testCases, + testCase{htmlEscaper, "No changes", "No changes"}, + testCase{htmlEscaper, "I <3 escaping & stuff", "I <3 escaping & stuff"}, + testCase{htmlEscaper, "&&&", "&&&"}, + testCase{htmlEscaper, "", ""}, + + testCase{repeat, "brad", "bbrrrrrrrrrrrrrrrrrradddd"}, + testCase{repeat, "abba", "abbbba"}, + testCase{repeat, "", ""}, + + testCase{strings.NewReplacer("a", "11", "a", "22"), "brad", "br11d"}, + ) + + // The remaining test cases have variable length old strings. + + testCases = append(testCases, + testCase{htmlUnescaper, "&amp;", "&"}, + testCase{htmlUnescaper, "<b>HTML's neat</b>", "HTML's neat"}, + testCase{htmlUnescaper, "", ""}, + + testCase{strings.NewReplacer("a", "1", "a", "2", "xxx", "xxx"), "brad", "br1d"}, + + testCase{strings.NewReplacer("a", "1", "aa", "2", "aaa", "3"), "aaaa", "1111"}, + + testCase{strings.NewReplacer("aaa", "3", "aa", "2", "a", "1"), "aaaa", "31"}, + ) + + // gen1 has multiple old strings of variable length. There is no + // overall non-empty common prefix, but some pairwise common prefixes. + gen1 := strings.NewReplacer( + "aaa", "3[aaa]", + "aa", "2[aa]", + "a", "1[a]", + "i", "i", + "longerst", "most long", + "longer", "medium", + "long", "short", + "xx", "xx", + "x", "X", + "X", "Y", + "Y", "Z", + ) + testCases = append(testCases, + testCase{gen1, "fooaaabar", "foo3[aaa]b1[a]r"}, + testCase{gen1, "long, longerst, longer", "short, most long, medium"}, + testCase{gen1, "xxxxx", "xxxxX"}, + testCase{gen1, "XiX", "YiY"}, + testCase{gen1, "", ""}, + ) + + // gen2 has multiple old strings with no pairwise common prefix. + gen2 := strings.NewReplacer( + "roses", "red", + "violets", "blue", + "sugar", "sweet", + ) + testCases = append(testCases, + testCase{gen2, "roses are red, violets are blue...", "red are red, blue are blue..."}, + testCase{gen2, "", ""}, + ) + + // gen3 has multiple old strings with an overall common prefix. + gen3 := strings.NewReplacer( + "abracadabra", "poof", + "abracadabrakazam", "splat", + "abraham", "lincoln", + "abrasion", "scrape", + "abraham", "isaac", + ) + testCases = append(testCases, + testCase{gen3, "abracadabrakazam abraham", "poofkazam lincoln"}, + testCase{gen3, "abrasion abracad", "scrape abracad"}, + testCase{gen3, "abba abram abrasive", "abba abram abrasive"}, + testCase{gen3, "", ""}, + ) + + // foo{1,2,3,4} have multiple old strings with an overall common prefix + // and 1- or 2- byte extensions from the common prefix. + foo1 := strings.NewReplacer( + "foo1", "A", + "foo2", "B", + "foo3", "C", + ) + foo2 := strings.NewReplacer( + "foo1", "A", + "foo2", "B", + "foo31", "C", + "foo32", "D", + ) + foo3 := strings.NewReplacer( + "foo11", "A", + "foo12", "B", + "foo31", "C", + "foo32", "D", + ) + foo4 := strings.NewReplacer( + "foo12", "B", + "foo32", "D", + ) + testCases = append(testCases, + testCase{foo1, "fofoofoo12foo32oo", "fofooA2C2oo"}, + testCase{foo1, "", ""}, + + testCase{foo2, "fofoofoo12foo32oo", "fofooA2Doo"}, + testCase{foo2, "", ""}, + + testCase{foo3, "fofoofoo12foo32oo", "fofooBDoo"}, + testCase{foo3, "", ""}, + + testCase{foo4, "fofoofoo12foo32oo", "fofooBDoo"}, + testCase{foo4, "", ""}, + ) + + // genAll maps "\x00\x01\x02...\xfe\xff" to "[all]", amongst other things. + allBytes := make([]byte, 256) + for i := range allBytes { + allBytes[i] = byte(i) + } + allString := string(allBytes) + genAll := strings.NewReplacer( + allString, "[all]", + "\xff", "[ff]", + "\x00", "[00]", + ) + testCases = append(testCases, + testCase{genAll, allString, "[all]"}, + testCase{genAll, "a\xff" + allString + "\x00", "a[ff][all][00]"}, + testCase{genAll, "", ""}, + ) + + // Test cases with empty old strings. + + blankToX1 := strings.NewReplacer("", "X") + blankToX2 := strings.NewReplacer("", "X", "", "") + blankHighPriority := strings.NewReplacer("", "X", "o", "O") + blankLowPriority := strings.NewReplacer("o", "O", "", "X") + blankNoOp1 := strings.NewReplacer("", "") + blankNoOp2 := strings.NewReplacer("", "", "", "A") + blankFoo := strings.NewReplacer("", "X", "foobar", "R", "foobaz", "Z") + testCases = append(testCases, + testCase{blankToX1, "foo", "XfXoXoX"}, + testCase{blankToX1, "", "X"}, + + testCase{blankToX2, "foo", "XfXoXoX"}, + testCase{blankToX2, "", "X"}, + + testCase{blankHighPriority, "oo", "XOXOX"}, + testCase{blankHighPriority, "ii", "XiXiX"}, + testCase{blankHighPriority, "oiio", "XOXiXiXOX"}, + testCase{blankHighPriority, "iooi", "XiXOXOXiX"}, + testCase{blankHighPriority, "", "X"}, + + testCase{blankLowPriority, "oo", "OOX"}, + testCase{blankLowPriority, "ii", "XiXiX"}, + testCase{blankLowPriority, "oiio", "OXiXiOX"}, + testCase{blankLowPriority, "iooi", "XiOOXiX"}, + testCase{blankLowPriority, "", "X"}, + + testCase{blankNoOp1, "foo", "foo"}, + testCase{blankNoOp1, "", ""}, + + testCase{blankNoOp2, "foo", "foo"}, + testCase{blankNoOp2, "", ""}, + + testCase{blankFoo, "foobarfoobaz", "XRXZX"}, + testCase{blankFoo, "foobar-foobaz", "XRX-XZX"}, + testCase{blankFoo, "", "X"}, + ) + + // single string replacer + + abcMatcher := strings.NewReplacer("abc", "[match]") + + testCases = append(testCases, + testCase{abcMatcher, "", ""}, + testCase{abcMatcher, "ab", "ab"}, + testCase{abcMatcher, "abc", "[match]"}, + testCase{abcMatcher, "abcd", "[match]d"}, + testCase{abcMatcher, "cabcabcdabca", "c[match][match]d[match]a"}, + ) + + // Issue 6659 cases (more single string replacer) + + noHello := strings.NewReplacer("Hello", "") + testCases = append(testCases, + testCase{noHello, "Hello", ""}, + testCase{noHello, "Hellox", "x"}, + testCase{noHello, "xHello", "x"}, + testCase{noHello, "xHellox", "xx"}, + ) + + // No-arg test cases. + + nop := strings.NewReplacer() + testCases = append(testCases, + testCase{nop, "abc", "abc"}, + testCase{nop, "", ""}, + ) + + // Run the test cases. + + for i, tc := range testCases { + if s := tc.r.Replace(tc.in); s != tc.out { + t.Errorf("%d. Replace(%q) = %q, want %q", i, tc.in, s, tc.out) + } + var buf bytes.Buffer + n, err := tc.r.WriteString(&buf, tc.in) + if err != nil { + t.Errorf("%d. WriteString: %v", i, err) + continue + } + got := buf.String() + if got != tc.out { + t.Errorf("%d. WriteString(%q) wrote %q, want %q", i, tc.in, got, tc.out) + continue + } + if n != len(tc.out) { + t.Errorf("%d. WriteString(%q) wrote correct string but reported %d bytes; want %d (%q)", + i, tc.in, n, len(tc.out), tc.out) + } + } +} + +var algorithmTestCases = []struct { + r *strings.Replacer + want string +}{ + {capitalLetters, "*strings.byteReplacer"}, + {htmlEscaper, "*strings.byteStringReplacer"}, + {strings.NewReplacer("12", "123"), "*strings.singleStringReplacer"}, + {strings.NewReplacer("1", "12"), "*strings.byteStringReplacer"}, + {strings.NewReplacer("", "X"), "*strings.genericReplacer"}, + {strings.NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"}, +} + +//// TestPickAlgorithm tests that strings.NewReplacer picks the correct algorithm. +//func TestPickAlgorithm(t *testing.T) { +// for i, tc := range algorithmTestCases { +// got := fmt.Sprintf("%T", tc.r.Replacer()) +// if got != tc.want { +// t.Errorf("%d. algorithm = %s, want %s", i, got, tc.want) +// } +// } +//} + +type errWriter struct{} + +func (errWriter) Write(p []byte) (n int, err error) { + return 0, fmt.Errorf("unwritable") +} + +// TestWriteStringError tests that WriteString returns an error +// received from the underlying io.Writer. +func TestWriteStringError(t *testing.T) { + for i, tc := range algorithmTestCases { + n, err := tc.r.WriteString(errWriter{}, "abc") + if n != 0 || err == nil || err.Error() != "unwritable" { + t.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err) + } + } +} + +func BenchmarkGenericNoMatch(b *testing.B) { + str := strings.Repeat("A", 100) + strings.Repeat("B", 100) + generic := strings.NewReplacer("a", "A", "b", "B", "12", "123") // varying lengths forces generic + for i := 0; i < b.N; i++ { + generic.Replace(str) + } +} + +func BenchmarkGenericMatch1(b *testing.B) { + str := strings.Repeat("a", 100) + strings.Repeat("b", 100) + generic := strings.NewReplacer("a", "A", "b", "B", "12", "123") + for i := 0; i < b.N; i++ { + generic.Replace(str) + } +} + +func BenchmarkGenericMatch2(b *testing.B) { + str := strings.Repeat("It's <b>HTML</b>!", 100) + for i := 0; i < b.N; i++ { + htmlUnescaper.Replace(str) + } +} + +func benchmarkSingleString(b *testing.B, pattern, text string) { + r := strings.NewReplacer(pattern, "[match]") + b.SetBytes(int64(len(text))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + r.Replace(text) + } +} + +func BenchmarkSingleMaxSkipping(b *testing.B) { + benchmarkSingleString(b, strings.Repeat("b", 25), strings.Repeat("a", 10000)) +} + +func BenchmarkSingleLongSuffixFail(b *testing.B) { + benchmarkSingleString(b, "b"+strings.Repeat("a", 500), strings.Repeat("a", 1002)) +} + +func BenchmarkSingleMatch(b *testing.B) { + benchmarkSingleString(b, "abcdef", strings.Repeat("abcdefghijklmno", 1000)) +} + +func BenchmarkByteByteNoMatch(b *testing.B) { + str := strings.Repeat("A", 100) + strings.Repeat("B", 100) + for i := 0; i < b.N; i++ { + capitalLetters.Replace(str) + } +} + +func BenchmarkByteByteMatch(b *testing.B) { + str := strings.Repeat("a", 100) + strings.Repeat("b", 100) + for i := 0; i < b.N; i++ { + capitalLetters.Replace(str) + } +} + +func BenchmarkByteStringMatch(b *testing.B) { + str := "<" + strings.Repeat("a", 99) + strings.Repeat("b", 99) + ">" + for i := 0; i < b.N; i++ { + htmlEscaper.Replace(str) + } +} + +func BenchmarkHTMLEscapeNew(b *testing.B) { + str := "I <3 to escape HTML & other text too." + for i := 0; i < b.N; i++ { + htmlEscaper.Replace(str) + } +} + +func BenchmarkHTMLEscapeOld(b *testing.B) { + str := "I <3 to escape HTML & other text too." + for i := 0; i < b.N; i++ { + oldHTMLEscape(str) + } +} + +func BenchmarkByteStringReplacerWriteString(b *testing.B) { + str := strings.Repeat("I <3 to escape HTML & other text too.", 100) + buf := new(bytes.Buffer) + for i := 0; i < b.N; i++ { + htmlEscaper.WriteString(buf, str) + buf.Reset() + } +} + +func BenchmarkByteReplacerWriteString(b *testing.B) { + str := strings.Repeat("abcdefghijklmnopqrstuvwxyz", 100) + buf := new(bytes.Buffer) + for i := 0; i < b.N; i++ { + capitalLetters.WriteString(buf, str) + buf.Reset() + } +} + +// BenchmarkByteByteReplaces compares byteByteImpl against multiple Replaces. +func BenchmarkByteByteReplaces(b *testing.B) { + str := strings.Repeat("a", 100) + strings.Repeat("b", 100) + for i := 0; i < b.N; i++ { + strings.Replace(strings.Replace(str, "a", "A", -1), "b", "B", -1) + } +} + +// BenchmarkByteByteMap compares byteByteImpl against Map. +func BenchmarkByteByteMap(b *testing.B) { + str := strings.Repeat("a", 100) + strings.Repeat("b", 100) + fn := func(r rune) rune { + switch r { + case 'a': + return 'A' + case 'b': + return 'B' + } + return r + } + for i := 0; i < b.N; i++ { + strings.Map(fn, str) + } +} + +var mapdata = []struct{ name, data string }{ + {"ASCII", "a b c d e f g h i j k l m n o p q r s t u v w x y z"}, + {"Greek", "Ξ± Ξ² Ξ³ Ξ΄ Ξ΅ ΞΆ Ξ· ΞΈ ΞΉ ΞΊ Ξ» ΞΌ Ξ½ ΞΎ ΞΏ Ο€ ρ Ο‚ Οƒ Ο„ Ο… Ο† Ο‡ ψ Ο‰"}, +} + +func BenchmarkMap(b *testing.B) { + mapidentity := func(r rune) rune { + return r + } + + b.Run("identity", func(b *testing.B) { + for _, md := range mapdata { + b.Run(md.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + strings.Map(mapidentity, md.data) + } + }) + } + }) + + mapchange := func(r rune) rune { + if 'a' <= r && r <= 'z' { + return r + 'A' - 'a' + } + if 'Ξ±' <= r && r <= 'Ο‰' { + return r + 'Ξ‘' - 'Ξ±' + } + return r + } + + b.Run("change", func(b *testing.B) { + for _, md := range mapdata { + b.Run(md.name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + strings.Map(mapchange, md.data) + } + }) + } + }) +}