From cfefb3b223de30a7db8b75c82c9853398caa2983 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:23:47 +0200 Subject: [PATCH 01/93] ci: tell codecov to wait_for_ci to avoid flappy reports (#1160) I've conducted tests on my fork, which you can find at https://github.com/moul/gno. The 'if' condition draws inspiration from @ajnavarro's contributions, so a shoutout to him for that. Additionally, @ajnavarro pointed out gaps in our testing for certain packages. We need to strategize on expanding our test coverage to address these overlooked areas. I suggest waiting for his alternative work before considering merging mine. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/codecov.yml | 26 +++++++++++++++++++++++--- .github/workflows/benchmark.yml | 1 + .github/workflows/db-tests.yml | 7 ++++--- .github/workflows/examples.yml | 18 +++++++++--------- .github/workflows/gh-pages.yml | 2 ++ .github/workflows/gnoland.yml | 23 ++++++++++++++--------- .github/workflows/gnovm.yml | 24 +++++++++++++++--------- .github/workflows/misc.yml | 22 +++++++++++----------- .github/workflows/tm2.yml | 25 ++++++++++++++++--------- 9 files changed, 95 insertions(+), 53 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index cd8860c3a68..65609743a74 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,7 +1,7 @@ codecov: require_ci_to_pass: false notify: - wait_for_ci: false + wait_for_ci: true ignore: - misc @@ -12,6 +12,25 @@ comment: coverage: round: down precision: 2 + status: + project: + default: + target: auto + threshold: 10 # Let's decrease this later. + base: parent + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false + patch: + default: + target: auto + threshold: 100 # Allows PRs without tests, overall stats count. + base: auto + if_no_uploads: error + if_not_found: success + if_ci_failed: error + only_pulls: false flag_management: default_rules: @@ -19,9 +38,10 @@ flag_management: statuses: - type: project target: auto - threshold: 0.5% + threshold: 10 # Let's decrease this later. - type: patch - target: auto + target: auto # Allows PRs without tests, overall stats count. + threshold: 100 individual_flags: - name: tm2 paths: diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 8b8c6f618c5..63490c0bfa1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -10,6 +10,7 @@ on: type: string jobs: benchmarks: + if: ${{ github.repository == 'gnolang/gno' }} runs-on: [self-hosted, Linux, X64, benchmark-v1] steps: - name: checkout diff --git a/.github/workflows/db-tests.yml b/.github/workflows/db-tests.yml index 081a7517725..f8da78cc3d8 100644 --- a/.github/workflows/db-tests.yml +++ b/.github/workflows/db-tests.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" tags: @@ -29,10 +29,12 @@ jobs: - fsdb - boltdb steps: + - uses: actions/checkout@v4 + # golang - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ matrix.goversion }} # leveldb - name: install leveldb @@ -46,6 +48,5 @@ jobs: sudo dpkg -i *.deb # test ./pkgs/db - - uses: actions/checkout@v4 - name: test ./tm2/pkg/db run: go test -tags ${{ matrix.tags }} ./tm2/pkg/db/... diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 4353682122a..d4b3079d612 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -21,16 +21,16 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" runs-on: ubuntu-latest timeout-minutes: 30 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno - run: go run ./gnovm/cmd/gno precompile --verbose ./examples - run: go run ./gnovm/cmd/gno build --verbose ./examples @@ -38,34 +38,34 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" # unittests: TODO: matrix with contracts runs-on: ubuntu-latest timeout-minutes: 30 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno - run: go run ./gnovm/cmd/gno test --verbose ./examples lint: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" # unittests: TODO: matrix with contracts runs-on: ubuntu-latest timeout-minutes: 10 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno # testing official directories, basically examples/ minus examples/.../x/. - run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/p diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 2233132e745..8f57bec80a1 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -17,6 +17,7 @@ concurrency: jobs: build: + if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -30,6 +31,7 @@ jobs: path: ./misc/gendocs/godoc deploy: + if: ${{ github.repository == 'gnolang/gno' }} # Alternatively, validate based on provided tokens and permissions. runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 902d0f41954..9fdfa3011cc 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -9,6 +9,12 @@ on: - "tm2/**.go" - "gno.land/**" - ".github/workflows/gnovm.yml" + # Until the codecov issue is resolved, it's essential to run the tests for gnovm, tm2, and gno.land concurrently. + - "gnovm/**" + - "tm2/**" + - "gno.land/**" + - "examples/**" + - ".github/workflows/**" push: branches: [ "master" ] @@ -21,7 +27,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" goarch: [ "amd64" ] @@ -35,10 +41,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: go install working-directory: gno.land run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go install ./cmd/${{ matrix.program }} @@ -47,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" args: @@ -58,25 +64,24 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: test working-directory: gno.land run: | export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} - - if: runner.os == 'Linux' + - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} name: gno.land flags: gno.land-${{matrix.args}} files: ./gno.land/coverage.out - #fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} - fail_ci_if_error: false # temporarily + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: strategy: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index e545e4ac6db..034aa109767 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -11,6 +11,12 @@ on: - "gnovm/Makefile" - "tm2/**.go" - ".github/workflows/gnovm.yml" + # Until the codecov issue is resolved, it's essential to run the tests for gnovm, tm2, and gno.land concurrently. + - "gnovm/**" + - "tm2/**" + - "gno.land/**" + - "examples/**" + - ".github/workflows/**" push: branches: [ "master" ] @@ -23,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: # two latest versions + goversion: # two latest versions - "1.20.x" - "1.21.x" goenv: # TODO: replace with pairs, so it's easier to read in the GH interface. @@ -34,10 +40,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: go install working-directory: gnovm run: ${{ matrix.goenv }} go install ./cmd/${{ matrix.program }} @@ -46,7 +52,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" args: @@ -62,22 +68,22 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: test working-directory: gnovm run: | export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} - - if: runner.os == 'Linux' + - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} name: gnovm + verbose: true flags: gnovm-${{matrix.args}} files: ./gnovm/coverage.out - #fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} - fail_ci_if_error: false # temporarily + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index 7ece5b5158d..a7c20d61ffd 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -13,14 +13,14 @@ jobs: lint: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v4 with: go-version: 1.21.x - - name: Checkout code - uses: actions/checkout@v4 - - name: Lint uses: golangci/golangci-lint-action@v3 with: @@ -31,17 +31,17 @@ jobs: fmt: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v4 with: - go-version: 1.20.x + go-version: 1.21.x - name: Install make run: sudo apt-get install -y make - - name: Checkout code - uses: actions/checkout@v4 - # prefill dependencies so that mod messages don't show up in make output - name: Fetch dependencies run: go mod download -modfile ./misc/devdeps/go.mod -x @@ -61,13 +61,13 @@ jobs: modtidy: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Install Go uses: actions/setup-go@v4 with: - go-version: 1.20.x - - - name: Checkout code - uses: actions/checkout@v4 + go-version: 1.21.x - name: Check go.mods run: | diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 1be5bedc6c9..fc744ccc2ef 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -7,6 +7,12 @@ on: - "tm2/Makefile" - "tm2/**.go" - ".github/workflows/tm2.yml" + # Until the codecov issue is resolved, it's essential to run the tests for gnovm, tm2, and gno.land concurrently. + - "gnovm/**" + - "tm2/**" + - "gno.land/**" + - "examples/**" + - ".github/workflows/**" push: branches: [ "master" ] @@ -19,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" goarch: [ "amd64" ] @@ -28,10 +34,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: go install working-directory: tm2 run: GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go install ${{ matrix.program }} @@ -40,7 +46,7 @@ jobs: strategy: fail-fast: false matrix: - go-version: + goversion: - "1.20.x" - "1.21.x" args: @@ -51,22 +57,23 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v4 + go-version: ${{ matrix.goversion }} - name: test working-directory: tm2 run: | export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} - - if: runner.os == 'Linux' + touch coverage.out + - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} name: tm2 + verbose: true flags: tm2-${{matrix.args}} files: ./tm2/coverage.out - #fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} - fail_ci_if_error: false # temporarily + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} From e10c0c77a80b0ef6940c41e2476f78bfd2fd483c Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Fri, 22 Sep 2023 07:38:39 -0700 Subject: [PATCH 02/93] fix: print declared type in output (#1143) This the first PR required for this feature #1141 --------- Co-authored-by: piux2 <> Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/pkg/gnolang/values_string.go | 4 ++++ gnovm/tests/files/addr0b_stdlibs.gno | 2 +- gnovm/tests/files/assign21.gno | 2 +- gnovm/tests/files/composite2.gno | 2 +- gnovm/tests/files/composite5.gno | 2 +- gnovm/tests/files/composite6.gno | 2 +- gnovm/tests/files/const12.gno | 2 +- gnovm/tests/files/const15.gno | 2 +- gnovm/tests/files/fun6b.gno | 2 +- gnovm/tests/files/fun9.gno | 2 +- gnovm/tests/files/interface39b.gno | 2 +- gnovm/tests/files/map19b.gno | 2 +- gnovm/tests/files/ptr0.gno | 2 +- gnovm/tests/files/ptr7.gno | 2 +- gnovm/tests/files/recurse0.gno | 4 ++-- gnovm/tests/files/struct28b.gno | 2 +- gnovm/tests/files/struct50b.gno | 4 ++-- gnovm/tests/files/struct52b.gno | 4 ++-- gnovm/tests/files/struct53b.gno | 2 +- gnovm/tests/files/type0.gno | 2 +- gnovm/tests/files/zpersist_valids.gno | 8 ++++---- gnovm/tests/files/zrealm10.gno | 4 ++-- gnovm/tests/files/zrealm11.gno | 4 ++-- gnovm/tests/files/zrealm8.gno | 4 ++-- gnovm/tests/files/zrealm9.gno | 4 ++-- gnovm/tests/files/zrealm_crossrealm0.gno | 2 +- gnovm/tests/files/zrealm_crossrealm1.gno | 2 +- gnovm/tests/files/zrealm_crossrealm6.gno | 2 +- gnovm/tests/files/zrealm_std1.gno | 2 +- gnovm/tests/files/zrealm_std2.gno | 2 +- 30 files changed, 43 insertions(+), 39 deletions(-) diff --git a/gnovm/pkg/gnolang/values_string.go b/gnovm/pkg/gnolang/values_string.go index 4819c2064e8..f9a0128d7f9 100644 --- a/gnovm/pkg/gnolang/values_string.go +++ b/gnovm/pkg/gnolang/values_string.go @@ -176,6 +176,10 @@ func (tv *TypedValue) Sprint(m *Machine) string { res := m.Eval(Call(Sel(&ConstExpr{TypedValue: *tv}, "Error"))) return res[0].GetString() } + // print declared type + if _, ok := tv.T.(*DeclaredType); ok { + return tv.String() + } // otherwise, default behavior. switch bt := baseOf(tv.T).(type) { case PrimitiveType: diff --git a/gnovm/tests/files/addr0b_stdlibs.gno b/gnovm/tests/files/addr0b_stdlibs.gno index e2848c2321b..2ec6782c7f0 100644 --- a/gnovm/tests/files/addr0b_stdlibs.gno +++ b/gnovm/tests/files/addr0b_stdlibs.gno @@ -20,4 +20,4 @@ func main() { } // Output: -// struct{(struct{( string),( string),(0 int),(0 int),(nil github.com/gnolang/gno/_test/net/http.Header),(undefined),(0 int64),(nil []string),(false bool),( string),(nil github.com/gnolang/gno/_test/net/http.Values),(nil github.com/gnolang/gno/_test/net/http.Values),(nil github.com/gnolang/gno/_test/net/http.Header),( string),( string),(nil *github.com/gnolang/gno/_test/net/http.Response)} github.com/gnolang/gno/_test/net/http.Request),( string)} +// (struct{(struct{( string),( string),(0 int),(0 int),(nil github.com/gnolang/gno/_test/net/http.Header),(undefined),(0 int64),(nil []string),(false bool),( string),(nil github.com/gnolang/gno/_test/net/http.Values),(nil github.com/gnolang/gno/_test/net/http.Values),(nil github.com/gnolang/gno/_test/net/http.Header),( string),( string),(nil *github.com/gnolang/gno/_test/net/http.Response)} github.com/gnolang/gno/_test/net/http.Request),( string)} main.extendedRequest) diff --git a/gnovm/tests/files/assign21.gno b/gnovm/tests/files/assign21.gno index 9d3595e95bd..c24d902c0c0 100644 --- a/gnovm/tests/files/assign21.gno +++ b/gnovm/tests/files/assign21.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// 1 true +// (1 main.thing) true diff --git a/gnovm/tests/files/composite2.gno b/gnovm/tests/files/composite2.gno index 5e8dc6afc1b..e6882e03b8b 100644 --- a/gnovm/tests/files/composite2.gno +++ b/gnovm/tests/files/composite2.gno @@ -9,4 +9,4 @@ func main() { } // Output: -// struct{("hello" string)} +// (struct{("hello" string)} main.T) diff --git a/gnovm/tests/files/composite5.gno b/gnovm/tests/files/composite5.gno index 15c4aa530e7..906f9d4f930 100644 --- a/gnovm/tests/files/composite5.gno +++ b/gnovm/tests/files/composite5.gno @@ -11,4 +11,4 @@ func main() { } // Output: -// struct{(12 uint16)} +// (struct{(12 uint16)} main.T) diff --git a/gnovm/tests/files/composite6.gno b/gnovm/tests/files/composite6.gno index 122665aa9d7..25817cde400 100644 --- a/gnovm/tests/files/composite6.gno +++ b/gnovm/tests/files/composite6.gno @@ -15,4 +15,4 @@ func main() { } // Output: -// struct{(2 uint16)} +// (struct{(2 uint16)} main.T) diff --git a/gnovm/tests/files/const12.gno b/gnovm/tests/files/const12.gno index 84510bf16b5..86dde62eb3d 100644 --- a/gnovm/tests/files/const12.gno +++ b/gnovm/tests/files/const12.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// 0 2 4 6 +// (0 main.Kind) (2 main.Kind) (4 main.Kind) (6 main.Kind) diff --git a/gnovm/tests/files/const15.gno b/gnovm/tests/files/const15.gno index ed55754726c..a2af6c6a849 100644 --- a/gnovm/tests/files/const15.gno +++ b/gnovm/tests/files/const15.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// 3 +// (3 main.T1) diff --git a/gnovm/tests/files/fun6b.gno b/gnovm/tests/files/fun6b.gno index 9ff57036333..17b0473b33b 100644 --- a/gnovm/tests/files/fun6b.gno +++ b/gnovm/tests/files/fun6b.gno @@ -17,4 +17,4 @@ func main() { } // Output: -// struct{(gonative{} gonative{*sync.Pool})} +// (struct{(gonative{} gonative{*sync.Pool})} main.Pool) diff --git a/gnovm/tests/files/fun9.gno b/gnovm/tests/files/fun9.gno index 5b5ec1bfe36..18c49a3a226 100644 --- a/gnovm/tests/files/fun9.gno +++ b/gnovm/tests/files/fun9.gno @@ -9,4 +9,4 @@ func main() { } // Output: -// 1 +// (1 main.myint) diff --git a/gnovm/tests/files/interface39b.gno b/gnovm/tests/files/interface39b.gno index 9a49c0abdef..a87b9ff1d30 100644 --- a/gnovm/tests/files/interface39b.gno +++ b/gnovm/tests/files/interface39b.gno @@ -18,4 +18,4 @@ func main() { } // Output: -// struct{("bar" string)} +// (struct{("bar" string)} main.foo) diff --git a/gnovm/tests/files/map19b.gno b/gnovm/tests/files/map19b.gno index dbbb6876cc6..77361016591 100644 --- a/gnovm/tests/files/map19b.gno +++ b/gnovm/tests/files/map19b.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// struct{(nil map[int64]*main.server)} +// (struct{(nil map[int64]*main.server)} main.cmap) diff --git a/gnovm/tests/files/ptr0.gno b/gnovm/tests/files/ptr0.gno index 2c61280b2ee..dd459e2d29f 100644 --- a/gnovm/tests/files/ptr0.gno +++ b/gnovm/tests/files/ptr0.gno @@ -9,4 +9,4 @@ func main() { } // Output: -// 2 +// (2 main.myint) diff --git a/gnovm/tests/files/ptr7.gno b/gnovm/tests/files/ptr7.gno index 1c6d7ae4040..b8f7fc9cf88 100644 --- a/gnovm/tests/files/ptr7.gno +++ b/gnovm/tests/files/ptr7.gno @@ -23,4 +23,4 @@ func main() { } // Output: -// struct{(nil github.com/gnolang/gno/_test/net.IP),(nil github.com/gnolang/gno/_test/net.IPMask)} +// (struct{(nil github.com/gnolang/gno/_test/net.IP),(nil github.com/gnolang/gno/_test/net.IPMask)} main.ipNetValue) diff --git a/gnovm/tests/files/recurse0.gno b/gnovm/tests/files/recurse0.gno index 2bf8f539f5a..fe5a997b19f 100644 --- a/gnovm/tests/files/recurse0.gno +++ b/gnovm/tests/files/recurse0.gno @@ -33,5 +33,5 @@ func main() { } // Output: -// struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T)),(struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T))} main.U)} -// struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T))} +// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T)),(struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T))} main.U)} main.T) +// (struct{(nil []main.T),(nil []*main.T),(nil map[string]main.T),(nil map[string]*main.T),(nil chan main.T),(nil chan *main.T),(nil *main.T),(nil func(.arg_0 main.T)( main.T)),(nil func(.arg_0 *main.T)( *main.T))} main.U) diff --git a/gnovm/tests/files/struct28b.gno b/gnovm/tests/files/struct28b.gno index dc61b00bb6f..e426fe490e2 100644 --- a/gnovm/tests/files/struct28b.gno +++ b/gnovm/tests/files/struct28b.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// struct{(struct{(nil *main.T1)} main.T2)} +// (struct{(struct{(nil *main.T1)} main.T2)} main.T1) diff --git a/gnovm/tests/files/struct50b.gno b/gnovm/tests/files/struct50b.gno index 62558c5b69f..212238c5cd7 100644 --- a/gnovm/tests/files/struct50b.gno +++ b/gnovm/tests/files/struct50b.gno @@ -14,5 +14,5 @@ func main() { } // Output: -// struct{("hello" string),(slice[(struct{("world" string),(nil []main.Node)} main.Node)] []main.Node)} -// struct{("hello" string),(slice[(struct{("world" string),(slice[(struct{("sunshine" string),(nil []main.Node)} main.Node)] []main.Node)} main.Node)] []main.Node)} +// (struct{("hello" string),(slice[(struct{("world" string),(nil []main.Node)} main.Node)] []main.Node)} main.Node) +// (struct{("hello" string),(slice[(struct{("world" string),(slice[(struct{("sunshine" string),(nil []main.Node)} main.Node)] []main.Node)} main.Node)] []main.Node)} main.Node) diff --git a/gnovm/tests/files/struct52b.gno b/gnovm/tests/files/struct52b.gno index 167e45d10a6..f1b26641149 100644 --- a/gnovm/tests/files/struct52b.gno +++ b/gnovm/tests/files/struct52b.gno @@ -14,5 +14,5 @@ func main() { } // Output: -// struct{("hello" string),(map{("1" string):(struct{("world" string),(map{} map[string]main.Node)} main.Node)} map[string]main.Node)} -// struct{("hello" string),(map{("1" string):(struct{("world" string),(map{("1" string):(struct{("sunshine" string),(map{} map[string]main.Node)} main.Node)} map[string]main.Node)} main.Node)} map[string]main.Node)} +// (struct{("hello" string),(map{("1" string):(struct{("world" string),(map{} map[string]main.Node)} main.Node)} map[string]main.Node)} main.Node) +// (struct{("hello" string),(map{("1" string):(struct{("world" string),(map{("1" string):(struct{("sunshine" string),(map{} map[string]main.Node)} main.Node)} map[string]main.Node)} main.Node)} map[string]main.Node)} main.Node) diff --git a/gnovm/tests/files/struct53b.gno b/gnovm/tests/files/struct53b.gno index b0f9be2659d..a367b109d30 100644 --- a/gnovm/tests/files/struct53b.gno +++ b/gnovm/tests/files/struct53b.gno @@ -18,4 +18,4 @@ func main() { } // Output: -// struct{(nil *main.T)} +// (struct{(nil *main.T)} main.T2) diff --git a/gnovm/tests/files/type0.gno b/gnovm/tests/files/type0.gno index 739cd711f93..38e385a04d9 100644 --- a/gnovm/tests/files/type0.gno +++ b/gnovm/tests/files/type0.gno @@ -8,4 +8,4 @@ func main() { } // Output: -// 0 +// (0 main.newInt) diff --git a/gnovm/tests/files/zpersist_valids.gno b/gnovm/tests/files/zpersist_valids.gno index 33280b54e57..2709a243adf 100644 --- a/gnovm/tests/files/zpersist_valids.gno +++ b/gnovm/tests/files/zpersist_valids.gno @@ -108,7 +108,7 @@ func printVars(phase string) { } // Output: -// preinit 16 true 22 16.16 16.16 16 16 16 16 16 97 hello slice[("A" string)] A struct{(16 int),("A" string)} 16 16 16 16 16 struct{(16 float32)} A -// postinit 32 false 44 32.32 32.32 32 32 32 32 32 66 helloB slice[("A" string),("B" string)] A struct{(32 int),("B" string)} 32 32 32 32 32 struct{("B" string)} B -// premain 32 false 44 32.32 32.32 32 32 32 32 32 66 helloB slice[("A" string),("B" string)] A struct{(32 int),("B" string)} 32 32 32 32 32 struct{("B" string)} B -// postmain 64 true 88 64.64 64.64 64 64 64 64 64 67 helloBC slice[("A" string),("B" string),("C" string)] A struct{(64 int),("C" string)} 64 64 64 64 64 struct{("C" string)} C +// preinit 16 true 22 16.16 16.16 16 16 16 16 16 97 hello slice[("A" string)] A (struct{(16 int),("A" string)} gno.land/r/demo/tests_test.myStruct) 16 16 16 16 16 struct{(16 float32)} A +// postinit 32 false 44 32.32 32.32 32 32 32 32 32 66 helloB slice[("A" string),("B" string)] A (struct{(32 int),("B" string)} gno.land/r/demo/tests_test.myStruct) 32 32 32 32 32 struct{("B" string)} B +// premain 32 false 44 32.32 32.32 32 32 32 32 32 66 helloB slice[("A" string),("B" string)] A (struct{(32 int),("B" string)} gno.land/r/demo/tests_test.myStruct) 32 32 32 32 32 struct{("B" string)} B +// postmain 64 true 88 64.64 64.64 64 64 64 64 64 67 helloBC slice[("A" string),("B" string),("C" string)] A (struct{(64 int),("C" string)} gno.land/r/demo/tests_test.myStruct) 64 64 64 64 64 struct{("C" string)} C diff --git a/gnovm/tests/files/zrealm10.gno b/gnovm/tests/files/zrealm10.gno index 12925ddd680..97bb9d439e2 100644 --- a/gnovm/tests/files/zrealm10.gno +++ b/gnovm/tests/files/zrealm10.gno @@ -18,8 +18,8 @@ func main() { } // Output: -// struct{(1 int)} -// struct{(3 int)} +// (struct{(1 int)} gno.land/r/test.MyStruct) +// (struct{(3 int)} gno.land/r/test.MyStruct) // Realm: // switchrealm["gno.land/r/test"] diff --git a/gnovm/tests/files/zrealm11.gno b/gnovm/tests/files/zrealm11.gno index 0f1616db14d..0f4d26a44c0 100644 --- a/gnovm/tests/files/zrealm11.gno +++ b/gnovm/tests/files/zrealm11.gno @@ -18,8 +18,8 @@ func main() { } // Output: -// struct{(1 int)} -// struct{(-1 int)} +// (struct{(1 int)} gno.land/r/test.MyStruct) +// (struct{(-1 int)} gno.land/r/test.MyStruct) // Realm: // switchrealm["gno.land/r/test"] diff --git a/gnovm/tests/files/zrealm8.gno b/gnovm/tests/files/zrealm8.gno index e8ca92c8b69..1452301dac6 100644 --- a/gnovm/tests/files/zrealm8.gno +++ b/gnovm/tests/files/zrealm8.gno @@ -18,8 +18,8 @@ func main() { } // Output: -// struct{(1 int)} -// struct{(2 int)} +// (struct{(1 int)} gno.land/r/test.MyStruct) +// (struct{(2 int)} gno.land/r/test.MyStruct) // Realm: // switchrealm["gno.land/r/test"] diff --git a/gnovm/tests/files/zrealm9.gno b/gnovm/tests/files/zrealm9.gno index 52e2da3565e..306ca02a92a 100644 --- a/gnovm/tests/files/zrealm9.gno +++ b/gnovm/tests/files/zrealm9.gno @@ -18,8 +18,8 @@ func main() { } // Output: -// struct{(1 int)} -// struct{(0 int)} +// (struct{(1 int)} gno.land/r/test.MyStruct) +// (struct{(0 int)} gno.land/r/test.MyStruct) // Realm: // switchrealm["gno.land/r/test"] diff --git a/gnovm/tests/files/zrealm_crossrealm0.gno b/gnovm/tests/files/zrealm_crossrealm0.gno index 053e2258465..5bf34c2c852 100644 --- a/gnovm/tests/files/zrealm_crossrealm0.gno +++ b/gnovm/tests/files/zrealm_crossrealm0.gno @@ -14,4 +14,4 @@ func main() { } // Output: -// struct{("test" string)} +// (struct{("test" string)} gno.land/r/demo/tests.TestRealmObject) diff --git a/gnovm/tests/files/zrealm_crossrealm1.gno b/gnovm/tests/files/zrealm_crossrealm1.gno index 59b2d317b8a..686468b40c7 100644 --- a/gnovm/tests/files/zrealm_crossrealm1.gno +++ b/gnovm/tests/files/zrealm_crossrealm1.gno @@ -17,4 +17,4 @@ func main() { } // Output: -// struct{("test" string)} +// (struct{("test" string)} gno.land/r/demo/tests.TestRealmObject) diff --git a/gnovm/tests/files/zrealm_crossrealm6.gno b/gnovm/tests/files/zrealm_crossrealm6.gno index c17e317f2eb..d2e7a4b096a 100644 --- a/gnovm/tests/files/zrealm_crossrealm6.gno +++ b/gnovm/tests/files/zrealm_crossrealm6.gno @@ -18,4 +18,4 @@ func main() { } // Output: -// struct{("modified" string)} +// (struct{("modified" string)} gno.land/p/demo/tests.TestRealmObject2) diff --git a/gnovm/tests/files/zrealm_std1.gno b/gnovm/tests/files/zrealm_std1.gno index 85391bd26b1..87f75bcb871 100644 --- a/gnovm/tests/files/zrealm_std1.gno +++ b/gnovm/tests/files/zrealm_std1.gno @@ -25,7 +25,7 @@ func main() { } // Output: -// slice[ref(1ed29bd278d735e20e296bd4afe927501941392f:4)] +// (slice[ref(1ed29bd278d735e20e296bd4afe927501941392f:4)] std.AddressList) // error: address already exists // has: true // has: false diff --git a/gnovm/tests/files/zrealm_std2.gno b/gnovm/tests/files/zrealm_std2.gno index 68c09a741d8..1ae1fb4a881 100644 --- a/gnovm/tests/files/zrealm_std2.gno +++ b/gnovm/tests/files/zrealm_std2.gno @@ -26,7 +26,7 @@ func main() { } // Output: -// slice[ref(1ed29bd278d735e20e296bd4afe927501941392f:4)] +// (slice[ref(1ed29bd278d735e20e296bd4afe927501941392f:4)] std.AddressList) // error: address already exists // has: true // has: false From 8eeff73eb760d6f14e80ce00bd827831c8035b63 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 22 Sep 2023 20:47:32 +0200 Subject: [PATCH 03/93] docs(contributing): change instructions for vim, reference gnols (#1113) Setting syntax=go instead of filetype=go allows us to have syntax highlighting without triggering vim-go. Also added a reference to @jdkato's great [gnols](https://github.com/jdkato/gnols) --------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- CONTRIBUTING.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 521f5d14c21..d1e23f18273 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,14 +79,15 @@ Add to your `.vimrc` file: ```vim function! GnoFmt() - cexpr system('gofmt -e -w ' . expand('%')) "or replace with gofumpt, see below - edit! + cexpr system('gofmt -e -w ' . expand('%')) " or replace with gofumpt, see below + edit! + set syntax=go endfunction command! GnoFmt call GnoFmt() augroup gno_autocmd - autocmd! - autocmd BufNewFile,BufRead *.gno set filetype=go - autocmd BufWritePost *.gno GnoFmt + autocmd! + autocmd BufNewFile,BufRead *.gno set syntax=go + autocmd BufWritePost *.gno GnoFmt augroup END ``` @@ -96,6 +97,9 @@ To use *gofumpt* instead of *gofmt*, as hinted in the comment, you may either ha cexpr system('go run -modfile /misc/devdeps/go.mod mvdan.cc/gofumpt -w ' . expand('%')) ``` +There is an experimental and unofficial [Gno Language Server](https://github.com/jdkato/gnols) +developed by the community, with an installation guide for Neovim. + #### Emacs Support 1. Install [go-mode.el](https://github.com/dominikh/go-mode.el). @@ -105,6 +109,11 @@ cexpr system('go run -modfile /misc/devdeps/go.mod mvdan.cc/gofump (add-to-list 'auto-mode-alist '("\\.gno\\'" . go-mode)) ``` +#### Sublime Text + +There is an experimental and unofficial [Gno Language Server](https://github.com/jdkato/gnols) +developed by the community, with an installation guide for Sublime Text. + ### Local Setup To get started with Gno development, the process is relatively straightforward. From b449e89efe5f32d214afdcadd4c6bf461d28ef69 Mon Sep 17 00:00:00 2001 From: grepsuzette <350354+grepsuzette@users.noreply.github.com> Date: Sat, 23 Sep 2023 02:58:56 +0800 Subject: [PATCH 04/93] fix: fix vuln demonstrated by #583 (#584) This is a fix for the 2022-10-07 Binance vuln demonstrated in #583. **Original fix** was simply (https://github.com/cosmos/iavl/pull/582): ``` if len(pin.Left) > 0 && len(pin.Right) > 0 { return nil, errors.New("both left and right child hashes are set") } ``` Our iavl functions however don't return errors. Proposing to use `panic()` instead, as it does in other parts of this file. --- More about the vuln, for comments and archival: * https://twitter.com/buchmanster/status/1578879225574350848 * https://medium.com/@Beosin_com/how-did-the-bnb-chain-exploiter-pass-iavl-proof-verification-an-in-depth-analysis-by-beosin-c925b77bc13e --------- Co-authored-by: grepsuzette Co-authored-by: Antonio Navarro Perez Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- tm2/pkg/iavl/proof.go | 5 ++ tm2/pkg/iavl/proof_forgery_test.go | 103 +++++++++++++++++++++++++++++ tm2/pkg/iavl/proof_test.go | 13 +++- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tm2/pkg/iavl/proof_forgery_test.go diff --git a/tm2/pkg/iavl/proof.go b/tm2/pkg/iavl/proof.go index 47568361da8..b098fd0cd3f 100644 --- a/tm2/pkg/iavl/proof.go +++ b/tm2/pkg/iavl/proof.go @@ -22,6 +22,7 @@ var ( //---------------------------------------- +// Contract: Left and Right can never both be set. Will result in a empty `[]` roothash type proofInnerNode struct { Height int8 `json:"height"` Size int64 `json:"size"` @@ -62,6 +63,10 @@ func (pin proofInnerNode) Hash(childHash []byte) []byte { err = amino.EncodeVarint(buf, pin.Version) } + if len(pin.Left) > 0 && len(pin.Right) > 0 { + panic(fmt.Sprintf("both left and right child hashes are set")) + } + if len(pin.Left) == 0 { if err == nil { err = amino.EncodeByteSlice(buf, childHash) diff --git a/tm2/pkg/iavl/proof_forgery_test.go b/tm2/pkg/iavl/proof_forgery_test.go new file mode 100644 index 00000000000..a742ad40fcc --- /dev/null +++ b/tm2/pkg/iavl/proof_forgery_test.go @@ -0,0 +1,103 @@ +package iavl_test + +import ( + "encoding/hex" + "math/rand" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gnolang/gno/tm2/pkg/crypto/tmhash" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/iavl" +) + +func TestProofForgery(t *testing.T) { + source := rand.NewSource(0) + r := rand.New(source) + cacheSize := 0 + tree := iavl.NewMutableTree(db.NewMemDB(), cacheSize) + + // two keys only + keys := []byte{0x11, 0x32} + values := make([][]byte, len(keys)) + // make random values and insert into tree + for i, ikey := range keys { + key := []byte{ikey} + v := r.Intn(255) + values[i] = []byte{byte(v)} + tree.Set(key, values[i]) + } + + // get root + root := tree.WorkingHash() + // use the rightmost kv pair in the tree so the inner nodes will populate left + k := []byte{keys[1]} + v := values[1] + + val, proof, err := tree.GetWithProof(k) + require.NoError(t, err) + + err = proof.Verify(root) + require.NoError(t, err) + err = proof.VerifyItem(k, val) + require.NoError(t, err) + + // ------------------- FORGE PROOF ------------------- + + forgedPayloadBytes := decodeHex(t, "0xabcd") + forgedValueHash := tmhash.Sum(forgedPayloadBytes) + // make a forgery of the proof by adding: + // - a new leaf node to the right + // - an empty inner node + // - a right entry in the path + _, proof2, _ := tree.GetWithProof(k) + forgedNode := proof2.Leaves[0] + forgedNode.Key = []byte{0xFF} + forgedNode.ValueHash = forgedValueHash + proof2.Leaves = append(proof2.Leaves, forgedNode) + proof2.InnerNodes = append(proof2.InnerNodes, iavl.PathToLeaf{}) + // figure out what hash we need via https://twitter.com/samczsun/status/1578181160345034752 + proof2.LeftPath[0].Right = decodeHex(t, "82C36CED85E914DAE8FDF6DD11FD5833121AA425711EB126C470CE28FF6623D5") + + rootHashValid := proof.ComputeRootHash() + verifyErr := proof.Verify(rootHashValid) + require.NoError(t, verifyErr, "should verify") + + // forged proofs now should make ComputeRootHash() and Verify() panic + var rootHashForged []byte + require.Panics(t, func() { rootHashForged = proof2.ComputeRootHash() }, "ComputeRootHash must panic if both left and right are set") + require.Panics(t, func() { proof2.Verify(rootHashForged) }, "forged proof should not verify") + require.Panics(t, func() { proof2.Verify(rootHashValid) }, "verify (tentatively forged) proof2 two fails with valid proof") + + { + // legit node verifies against legit proof (expected) + verifyErr = proof.VerifyItem(k, v) + require.NoError(t, verifyErr, "valid proof should verify") + // forged node fails to verify against legit proof (expected) + verifyErr = proof.VerifyItem(forgedNode.Key, forgedPayloadBytes) + require.Error(t, verifyErr, "forged proof should fail to verify") + } + { + // legit node fails to verify against forged proof (expected) + verifyErr = proof2.VerifyItem(k, v) + require.Error(t, verifyErr, "valid proof should verify, but has a forged sister node") + + // forged node fails to verify against forged proof (previously this succeeded!) + verifyErr = proof2.VerifyItem(forgedNode.Key, forgedPayloadBytes) + require.Error(t, verifyErr, "forged proof should fail verify") + } +} + +func decodeHex(t *testing.T, str string) []byte { + t.Helper() + if strings.HasPrefix(str, "0x") { + str = str[2:] + } + b, err := hex.DecodeString(str) + if err != nil { + t.Fatalf("unable to decode string, %v", err) + } + return b +} diff --git a/tm2/pkg/iavl/proof_test.go b/tm2/pkg/iavl/proof_test.go index 87f3329bb80..f67ea45b00a 100644 --- a/tm2/pkg/iavl/proof_test.go +++ b/tm2/pkg/iavl/proof_test.go @@ -2,6 +2,7 @@ package iavl import ( "bytes" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -230,7 +231,7 @@ func verifyProof(t *testing.T, proof *RangeProof, root []byte) { } // may be invalid... errors are okay if err == nil { - assert.Errorf(t, badProof.Verify(root), + assert.Errorf(t, rangeProofVerify(badProof, root), "Proof was still valid after a random mutation:\n%X\n%X", proofBytes, badProofBytes) } @@ -239,6 +240,16 @@ func verifyProof(t *testing.T, proof *RangeProof, root []byte) { // ---------------------------------------- +func rangeProofVerify(rangeProof *RangeProof, root []byte) (namedError error) { + defer func() { + if e := recover(); e != nil { + namedError = errors.New(e.(string)) + } + }() + namedError = rangeProof.Verify(root) + return +} + func flatten(bzz [][]byte) (res []byte) { for _, bz := range bzz { res = append(res, bz...) From 2ecbb6df21c509941da157c85dd6914a63c5b8f3 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:12:40 +0200 Subject: [PATCH 05/93] feat(stdlib): add `net/url` (#1066) Added the `net/url` package. This package will be beneficial for manipulating URLs, for instance, parsing query parameters or for URL muxing in realms. depends: - [x] #1076 - [x] #1065
Contributors' checklist... - [X] 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 - [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] 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).
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/docs/go-gno-compatibility.md | 2 +- gnovm/stdlibs/net/url/url.gno | 1265 ++++++++++++++++ gnovm/stdlibs/net/url/url_test.gno | 2222 ++++++++++++++++++++++++++++ gnovm/stdlibs/strings/strings.gno | 33 + 4 files changed, 3521 insertions(+), 1 deletion(-) create mode 100644 gnovm/stdlibs/net/url/url.gno create mode 100644 gnovm/stdlibs/net/url/url_test.gno diff --git a/gnovm/docs/go-gno-compatibility.md b/gnovm/docs/go-gno-compatibility.md index d5448ab2779..98f42aa9f29 100644 --- a/gnovm/docs/go-gno-compatibility.md +++ b/gnovm/docs/go-gno-compatibility.md @@ -283,7 +283,7 @@ Additional native types: | net/rpc/jsonrpc | TBD | | net/smtp | TBD | | net/textproto | TBD | -| net/url | TBD | +| net/url | full | | os | TBD | | os/exec | TBD | | os/signal | TBD | diff --git a/gnovm/stdlibs/net/url/url.gno b/gnovm/stdlibs/net/url/url.gno new file mode 100644 index 00000000000..501b263e873 --- /dev/null +++ b/gnovm/stdlibs/net/url/url.gno @@ -0,0 +1,1265 @@ +// 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 url parses URLs and implements query escaping. +package url + +// See RFC 3986. This package generally follows RFC 3986, except where +// it deviates for compatibility reasons. When sending changes, first +// search old issues for history on decisions. Unit tests should also +// contain references to issue numbers with details. + +import ( + "errors" + "fmt" + "path" + "sort" + "strconv" + "strings" +) + +// Error reports an error and the operation and URL that caused it. +type Error struct { + Op string + URL string + Err error +} + +func (e *Error) Unwrap() error { return e.Err } +func (e *Error) Error() string { return fmt.Sprintf("%s %q: %s", e.Op, e.URL, e.Err) } + +func (e *Error) Timeout() bool { + t, ok := e.Err.(interface { + Timeout() bool + }) + return ok && t.Timeout() +} + +func (e *Error) Temporary() bool { + t, ok := e.Err.(interface { + Temporary() bool + }) + return ok && t.Temporary() +} + +const upperhex = "0123456789ABCDEF" + +func ishex(c byte) bool { + switch { + case '0' <= c && c <= '9': + return true + case 'a' <= c && c <= 'f': + return true + case 'A' <= c && c <= 'F': + return true + } + return false +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +type encoding int + +const ( + encodePath encoding = 1 + iota + encodePathSegment + encodeHost + encodeZone + encodeUserPassword + encodeQueryComponent + encodeFragment +) + +type EscapeError string + +func (e EscapeError) Error() string { + return "invalid URL escape " + strconv.Quote(string(e)) +} + +type InvalidHostError string + +func (e InvalidHostError) Error() string { + return "invalid character " + strconv.Quote(string(e)) + " in host name" +} + +// Return true if the specified character should be escaped when +// appearing in a URL string, according to RFC 3986. +// +// Please be informed that for now shouldEscape does not check all +// reserved characters correctly. See golang.org/issue/5684. +func shouldEscape(c byte, mode encoding) bool { + // §2.3 Unreserved characters (alphanum) + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { + return false + } + + if mode == encodeHost || mode == encodeZone { + // §3.2.2 Host allows + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" + // as part of reg-name. + // We add : because we include :port as part of host. + // We add [ ] because we include [ipv6]:port as part of host. + // We add < > because they're the only characters left that + // we could possibly allow, and Parse will reject them if we + // escape them (because hosts can't use %-encoding for + // ASCII bytes). + switch c { + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"': + return false + } + } + + switch c { + case '-', '_', '.', '~': // §2.3 Unreserved characters (mark) + return false + + case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved) + // Different sections of the URL allow a few of + // the reserved characters to appear unescaped. + switch mode { + case encodePath: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. This package + // only manipulates the path as a whole, so we allow those + // last three as well. That leaves only ? to escape. + return c == '?' + + case encodePathSegment: // §3.3 + // The RFC allows : @ & = + $ but saves / ; , for assigning + // meaning to individual path segments. + return c == '/' || c == ';' || c == ',' || c == '?' + + case encodeUserPassword: // §3.2.1 + // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in + // userinfo, so we must escape only '@', '/', and '?'. + // The parsing of userinfo treats ':' as special so we must escape + // that too. + return c == '@' || c == '/' || c == '?' || c == ':' + + case encodeQueryComponent: // §3.4 + // The RFC reserves (so we must escape) everything. + return true + + case encodeFragment: // §4.1 + // The RFC text is silent but the grammar allows + // everything, so escape nothing. + return false + } + } + + if mode == encodeFragment { + // RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are + // included in reserved from RFC 2396 §2.2. The remaining sub-delims do not + // need to be escaped. To minimize potential breakage, we apply two restrictions: + // (1) we always escape sub-delims outside of the fragment, and (2) we always + // escape single quote to avoid breaking callers that had previously assumed that + // single quotes would be escaped. See issue #19917. + switch c { + case '!', '(', ')', '*': + return false + } + } + + // Everything else must be escaped. + return true +} + +// QueryUnescape does the inverse transformation of QueryEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. +// It returns an error if any % is not followed by two hexadecimal +// digits. +func QueryUnescape(s string) (string, error) { + return unescape(s, encodeQueryComponent) +} + +// PathUnescape does the inverse transformation of PathEscape, +// converting each 3-byte encoded substring of the form "%AB" into the +// hex-decoded byte 0xAB. It returns an error if any % is not followed +// by two hexadecimal digits. +// +// PathUnescape is identical to QueryUnescape except that it does not +// unescape '+' to ' ' (space). +func PathUnescape(s string) (string, error) { + return unescape(s, encodePathSegment) +} + +// unescape unescapes a string; the mode specifies +// which section of the URL string is being unescaped. +func unescape(s string, mode encoding) (string, error) { + // Count %, check that they're well-formed. + n := 0 + hasPlus := false + for i := 0; i < len(s); { + switch s[i] { + case '%': + n++ + if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) { + s = s[i:] + if len(s) > 3 { + s = s[:3] + } + return "", EscapeError(s) + } + // Per https://tools.ietf.org/html/rfc3986#page-21 + // in the host component %-encoding can only be used + // for non-ASCII bytes. + // But https://tools.ietf.org/html/rfc6874#section-2 + // introduces %25 being allowed to escape a percent sign + // in IPv6 scoped-address literals. Yay. + if mode == encodeHost && unhex(s[i+1]) < 8 && s[i:i+3] != "%25" { + return "", EscapeError(s[i : i+3]) + } + if mode == encodeZone { + // RFC 6874 says basically "anything goes" for zone identifiers + // and that even non-ASCII can be redundantly escaped, + // but it seems prudent to restrict %-escaped bytes here to those + // that are valid host name bytes in their unescaped form. + // That is, you can use escaping in the zone identifier but not + // to introduce bytes you couldn't just write directly. + // But Windows puts spaces here! Yay. + v := unhex(s[i+1])<<4 | unhex(s[i+2]) + if s[i:i+3] != "%25" && v != ' ' && shouldEscape(v, encodeHost) { + return "", EscapeError(s[i : i+3]) + } + } + i += 3 + case '+': + hasPlus = mode == encodeQueryComponent + i++ + default: + if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) { + return "", InvalidHostError(s[i : i+1]) + } + i++ + } + } + + if n == 0 && !hasPlus { + return s, nil + } + + var t strings.Builder + t.Grow(len(s) - 2*n) + for i := 0; i < len(s); i++ { + switch s[i] { + case '%': + t.WriteByte(unhex(s[i+1])<<4 | unhex(s[i+2])) + i += 2 + case '+': + if mode == encodeQueryComponent { + t.WriteByte(' ') + } else { + t.WriteByte('+') + } + default: + t.WriteByte(s[i]) + } + } + return t.String(), nil +} + +// QueryEscape escapes the string so it can be safely placed +// inside a URL query. +func QueryEscape(s string) string { + return escape(s, encodeQueryComponent) +} + +// PathEscape escapes the string so it can be safely placed inside a URL path segment, +// replacing special characters (including /) with %XX sequences as needed. +func PathEscape(s string) string { + return escape(s, encodePathSegment) +} + +func escape(s string, mode encoding) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if shouldEscape(c, mode) { + if c == ' ' && mode == encodeQueryComponent { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var buf [64]byte + var t []byte + + required := len(s) + 2*hexCount + if required <= len(buf) { + t = buf[:required] + } else { + t = make([]byte, required) + } + + if hexCount == 0 { + copy(t, s) + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + t[i] = '+' + } + } + return string(t) + } + + j := 0 + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ' && mode == encodeQueryComponent: + t[j] = '+' + j++ + case shouldEscape(c, mode): + t[j] = '%' + t[j+1] = upperhex[c>>4] + t[j+2] = upperhex[c&15] + j += 3 + default: + t[j] = s[i] + j++ + } + } + return string(t) +} + +// A URL represents a parsed URL (technically, a URI reference). +// +// The general form represented is: +// +// [scheme:][//[userinfo@]host][/]path[?query][#fragment] +// +// URLs that do not start with a slash after the scheme are interpreted as: +// +// scheme:opaque[?query][#fragment] +// +// Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/. +// A consequence is that it is impossible to tell which slashes in the Path were +// slashes in the raw URL and which were %2f. This distinction is rarely important, +// but when it is, the code should use the EscapedPath method, which preserves +// the original encoding of Path. +// +// The RawPath field is an optional field which is only set when the default +// encoding of Path is different from the escaped path. See the EscapedPath method +// for more details. +// +// URL's String method uses the EscapedPath method to obtain the path. +type URL struct { + Scheme string + Opaque string // encoded opaque data + User *Userinfo // username and password information + Host string // host or host:port + Path string // path (relative paths may omit leading slash) + RawPath string // encoded path hint (see EscapedPath method) + OmitHost bool // do not emit empty host (authority) + ForceQuery bool // append a query ('?') even if RawQuery is empty + RawQuery string // encoded query values, without '?' + Fragment string // fragment for references, without '#' + RawFragment string // encoded fragment hint (see EscapedFragment method) +} + +// User returns a Userinfo containing the provided username +// and no password set. +func User(username string) *Userinfo { + return &Userinfo{username, "", false} +} + +// UserPassword returns a Userinfo containing the provided username +// and password. +// +// This functionality should only be used with legacy web sites. +// RFC 2396 warns that interpreting Userinfo this way +// “is NOT RECOMMENDED, because the passing of authentication +// information in clear text (such as URI) has proven to be a +// security risk in almost every case where it has been used.” +func UserPassword(username, password string) *Userinfo { + return &Userinfo{username, password, true} +} + +// The Userinfo type is an immutable encapsulation of username and +// password details for a URL. An existing Userinfo value is guaranteed +// to have a username set (potentially empty, as allowed by RFC 2396), +// and optionally a password. +type Userinfo struct { + username string + password string + passwordSet bool +} + +// Username returns the username. +func (u *Userinfo) Username() string { + if u == nil { + return "" + } + return u.username +} + +// Password returns the password in case it is set, and whether it is set. +func (u *Userinfo) Password() (string, bool) { + if u == nil { + return "", false + } + return u.password, u.passwordSet +} + +// String returns the encoded userinfo information in the standard form +// of "username[:password]". +func (u *Userinfo) String() string { + if u == nil { + return "" + } + s := escape(u.username, encodeUserPassword) + if u.passwordSet { + s += ":" + escape(u.password, encodeUserPassword) + } + return s +} + +// Maybe rawURL is of the form scheme:path. +// (Scheme must be [a-zA-Z][a-zA-Z0-9+.-]*) +// If so, return scheme, path; else return "", rawURL. +func getScheme(rawURL string) (scheme, path string, err error) { + for i := 0; i < len(rawURL); i++ { + c := rawURL[i] + switch { + case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z': + // do nothing + case '0' <= c && c <= '9' || c == '+' || c == '-' || c == '.': + if i == 0 { + return "", rawURL, nil + } + case c == ':': + if i == 0 { + return "", "", errors.New("missing protocol scheme") + } + return rawURL[:i], rawURL[i+1:], nil + default: + // we have encountered an invalid character, + // so there is no valid scheme + return "", rawURL, nil + } + } + return "", rawURL, nil +} + +// Parse parses a raw url into a URL structure. +// +// The url may be relative (a path, without a host) or absolute +// (starting with a scheme). Trying to parse a hostname and path +// without a scheme is invalid but may not necessarily return an +// error, due to parsing ambiguities. +func Parse(rawURL string) (*URL, error) { + // Cut off #frag + u, frag, _ := strings.Cut(rawURL, "#") + url, err := parse(u, false) + if err != nil { + return nil, &Error{"parse", u, err} + } + if frag == "" { + return url, nil + } + if err = url.setFragment(frag); err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// ParseRequestURI parses a raw url into a URL structure. It assumes that +// url was received in an HTTP request, so the url is interpreted +// only as an absolute URI or an absolute path. +// The string url is assumed not to have a #fragment suffix. +// (Web browsers strip #fragment before sending the URL to a web server.) +func ParseRequestURI(rawURL string) (*URL, error) { + url, err := parse(rawURL, true) + if err != nil { + return nil, &Error{"parse", rawURL, err} + } + return url, nil +} + +// parse parses a URL from a string in one of two contexts. If +// viaRequest is true, the URL is assumed to have arrived via an HTTP request, +// in which case only absolute URLs or path-absolute relative URLs are allowed. +// If viaRequest is false, all forms of relative URLs are allowed. +func parse(rawURL string, viaRequest bool) (*URL, error) { + var rest string + var err error + + if stringContainsCTLByte(rawURL) { + return nil, errors.New("net/url: invalid control character in URL") + } + + if rawURL == "" && viaRequest { + return nil, errors.New("empty url") + } + url := new(URL) + + if rawURL == "*" { + url.Path = "*" + return url, nil + } + + // Split off possible leading "http:", "mailto:", etc. + // Cannot contain escaped characters. + if url.Scheme, rest, err = getScheme(rawURL); err != nil { + return nil, err + } + url.Scheme = strings.ToLower(url.Scheme) + + if strings.HasSuffix(rest, "?") && strings.Count(rest, "?") == 1 { + url.ForceQuery = true + rest = rest[:len(rest)-1] + } else { + rest, url.RawQuery, _ = strings.Cut(rest, "?") + } + + if !strings.HasPrefix(rest, "/") { + if url.Scheme != "" { + // We consider rootless paths per RFC 3986 as opaque. + url.Opaque = rest + return url, nil + } + if viaRequest { + return nil, errors.New("invalid URI for request") + } + + // Avoid confusion with malformed schemes, like cache_object:foo/bar. + // See golang.org/issue/16822. + // + // RFC 3986, §3.3: + // In addition, a URI reference (Section 4.1) may be a relative-path reference, + // in which case the first path segment cannot contain a colon (":") character. + if segment, _, _ := strings.Cut(rest, "/"); strings.Contains(segment, ":") { + // First path segment has colon. Not allowed in relative URL. + return nil, errors.New("first path segment in URL cannot contain colon") + } + } + + if (url.Scheme != "" || !viaRequest && !strings.HasPrefix(rest, "///")) && strings.HasPrefix(rest, "//") { + var authority string + authority, rest = rest[2:], "" + if i := strings.Index(authority, "/"); i >= 0 { + authority, rest = authority[:i], authority[i:] + } + url.User, url.Host, err = parseAuthority(authority) + if err != nil { + return nil, err + } + } else if url.Scheme != "" && strings.HasPrefix(rest, "/") { + // OmitHost is set to true when rawURL has an empty host (authority). + // See golang.org/issue/46059. + url.OmitHost = true + } + + // Set Path and, optionally, RawPath. + // RawPath is a hint of the encoding of Path. We don't want to set it if + // the default escaping of Path is equivalent, to help make sure that people + // don't rely on it in general. + if err := url.setPath(rest); err != nil { + return nil, err + } + return url, nil +} + +func parseAuthority(authority string) (user *Userinfo, host string, err error) { + i := strings.LastIndex(authority, "@") + if i < 0 { + host, err = parseHost(authority) + } else { + host, err = parseHost(authority[i+1:]) + } + if err != nil { + return nil, "", err + } + if i < 0 { + return nil, host, nil + } + userinfo := authority[:i] + if !validUserinfo(userinfo) { + return nil, "", errors.New("net/url: invalid userinfo") + } + if !strings.Contains(userinfo, ":") { + if userinfo, err = unescape(userinfo, encodeUserPassword); err != nil { + return nil, "", err + } + user = User(userinfo) + } else { + username, password, _ := strings.Cut(userinfo, ":") + if username, err = unescape(username, encodeUserPassword); err != nil { + return nil, "", err + } + if password, err = unescape(password, encodeUserPassword); err != nil { + return nil, "", err + } + user = UserPassword(username, password) + } + return user, host, nil +} + +// parseHost parses host as an authority without user +// information. That is, as host[:port]. +func parseHost(host string) (string, error) { + if strings.HasPrefix(host, "[") { + // Parse an IP-Literal in RFC 3986 and RFC 6874. + // E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80". + i := strings.LastIndex(host, "]") + if i < 0 { + return "", errors.New("missing ']' in host") + } + colonPort := host[i+1:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } + + // RFC 6874 defines that %25 (%-encoded percent) introduces + // the zone identifier, and the zone identifier can use basically + // any %-encoding it likes. That's different from the host, which + // can only %-encode non-ASCII bytes. + // We do impose some restrictions on the zone, to avoid stupidity + // like newlines. + zone := strings.Index(host[:i], "%25") + if zone >= 0 { + host1, err := unescape(host[:zone], encodeHost) + if err != nil { + return "", err + } + host2, err := unescape(host[zone:i], encodeZone) + if err != nil { + return "", err + } + host3, err := unescape(host[i:], encodeHost) + if err != nil { + return "", err + } + return host1 + host2 + host3, nil + } + } else if i := strings.LastIndex(host, ":"); i != -1 { + colonPort := host[i:] + if !validOptionalPort(colonPort) { + return "", fmt.Errorf("invalid port %q after host", colonPort) + } + } + + var err error + if host, err = unescape(host, encodeHost); err != nil { + return "", err + } + return host, nil +} + +// setPath sets the Path and RawPath fields of the URL based on the provided +// escaped path p. It maintains the invariant that RawPath is only specified +// when it differs from the default encoding of the path. +// For example: +// - setPath("/foo/bar") will set Path="/foo/bar" and RawPath="" +// - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar" +// setPath will return an error only if the provided path contains an invalid +// escaping. +func (u *URL) setPath(p string) error { + path, err := unescape(p, encodePath) + if err != nil { + return err + } + u.Path = path + if escp := escape(path, encodePath); p == escp { + // Default encoding is fine. + u.RawPath = "" + } else { + u.RawPath = p + } + return nil +} + +// EscapedPath returns the escaped form of u.Path. +// In general there are multiple possible escaped forms of any path. +// EscapedPath returns u.RawPath when it is a valid escaping of u.Path. +// Otherwise EscapedPath ignores u.RawPath and computes an escaped +// form on its own. +// The String and RequestURI methods use EscapedPath to construct +// their results. +// In general, code should call EscapedPath instead of +// reading u.RawPath directly. +func (u *URL) EscapedPath() string { + if u.RawPath != "" && validEncoded(u.RawPath, encodePath) { + p, err := unescape(u.RawPath, encodePath) + if err == nil && p == u.Path { + return u.RawPath + } + } + if u.Path == "*" { + return "*" // don't escape (Issue 11202) + } + return escape(u.Path, encodePath) +} + +// validEncoded reports whether s is a valid encoded path or fragment, +// according to mode. +// It must not contain any bytes that require escaping during encoding. +func validEncoded(s string, mode encoding) bool { + for i := 0; i < len(s); i++ { + // RFC 3986, Appendix A. + // pchar = unreserved / pct-encoded / sub-delims / ":" / "@". + // shouldEscape is not quite compliant with the RFC, + // so we check the sub-delims ourselves and let + // shouldEscape handle the others. + switch s[i] { + case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '@': + // ok + case '[', ']': + // ok - not specified in RFC 3986 but left alone by modern browsers + case '%': + // ok - percent encoded, will decode + default: + if shouldEscape(s[i], mode) { + return false + } + } + } + return true +} + +// setFragment is like setPath but for Fragment/RawFragment. +func (u *URL) setFragment(f string) error { + frag, err := unescape(f, encodeFragment) + if err != nil { + return err + } + u.Fragment = frag + if escf := escape(frag, encodeFragment); f == escf { + // Default encoding is fine. + u.RawFragment = "" + } else { + u.RawFragment = f + } + return nil +} + +// EscapedFragment returns the escaped form of u.Fragment. +// In general there are multiple possible escaped forms of any fragment. +// EscapedFragment returns u.RawFragment when it is a valid escaping of u.Fragment. +// Otherwise EscapedFragment ignores u.RawFragment and computes an escaped +// form on its own. +// The String method uses EscapedFragment to construct its result. +// In general, code should call EscapedFragment instead of +// reading u.RawFragment directly. +func (u *URL) EscapedFragment() string { + if u.RawFragment != "" && validEncoded(u.RawFragment, encodeFragment) { + f, err := unescape(u.RawFragment, encodeFragment) + if err == nil && f == u.Fragment { + return u.RawFragment + } + } + return escape(u.Fragment, encodeFragment) +} + +// validOptionalPort reports whether port is either an empty string +// or matches /^:\d*$/ +func validOptionalPort(port string) bool { + if port == "" { + return true + } + if port[0] != ':' { + return false + } + for _, b := range port[1:] { + if b < '0' || b > '9' { + return false + } + } + return true +} + +// String reassembles the URL into a valid URL string. +// The general form of the result is one of: +// +// scheme:opaque?query#fragment +// scheme://userinfo@host/path?query#fragment +// +// If u.Opaque is non-empty, String uses the first form; +// otherwise it uses the second form. +// Any non-ASCII characters in host are escaped. +// To obtain the path, String uses u.EscapedPath(). +// +// In the second form, the following rules apply: +// - if u.Scheme is empty, scheme: is omitted. +// - if u.User is nil, userinfo@ is omitted. +// - if u.Host is empty, host/ is omitted. +// - if u.Scheme and u.Host are empty and u.User is nil, +// the entire scheme://userinfo@host/ is omitted. +// - if u.Host is non-empty and u.Path begins with a /, +// the form host/path does not add its own /. +// - if u.RawQuery is empty, ?query is omitted. +// - if u.Fragment is empty, #fragment is omitted. +func (u *URL) String() string { + var buf strings.Builder + if u.Scheme != "" { + buf.WriteString(u.Scheme) + buf.WriteByte(':') + } + if u.Opaque != "" { + buf.WriteString(u.Opaque) + } else { + if u.Scheme != "" || u.Host != "" || u.User != nil { + if u.OmitHost && u.Host == "" && u.User == nil { + // omit empty host + } else { + if u.Host != "" || u.Path != "" || u.User != nil { + buf.WriteString("//") + } + if ui := u.User; ui != nil { + buf.WriteString(ui.String()) + buf.WriteByte('@') + } + if h := u.Host; h != "" { + buf.WriteString(escape(h, encodeHost)) + } + } + } + path := u.EscapedPath() + if path != "" && path[0] != '/' && u.Host != "" { + buf.WriteByte('/') + } + if buf.Len() == 0 { + // RFC 3986 §4.2 + // A path segment that contains a colon character (e.g., "this:that") + // cannot be used as the first segment of a relative-path reference, as + // it would be mistaken for a scheme name. Such a segment must be + // preceded by a dot-segment (e.g., "./this:that") to make a relative- + // path reference. + if segment, _, _ := strings.Cut(path, "/"); strings.Contains(segment, ":") { + buf.WriteString("./") + } + } + buf.WriteString(path) + } + if u.ForceQuery || u.RawQuery != "" { + buf.WriteByte('?') + buf.WriteString(u.RawQuery) + } + if u.Fragment != "" { + buf.WriteByte('#') + buf.WriteString(u.EscapedFragment()) + } + return buf.String() +} + +// Redacted is like String but replaces any password with "xxxxx". +// Only the password in u.User is redacted. +func (u *URL) Redacted() string { + if u == nil { + return "" + } + + ru := *u + if _, has := ru.User.Password(); has { + ru.User = UserPassword(ru.User.Username(), "xxxxx") + } + return ru.String() +} + +// Values maps a string key to a list of values. +// It is typically used for query parameters and form values. +// Unlike in the http.Header map, the keys in a Values map +// are case-sensitive. +type Values map[string][]string + +// Get gets the first value associated with the given key. +// If there are no values associated with the key, Get returns +// the empty string. To access multiple values, use the map +// directly. +func (v Values) Get(key string) string { + vs := v[key] + if len(vs) == 0 { + return "" + } + return vs[0] +} + +// Set sets the key to value. It replaces any existing +// values. +func (v Values) Set(key, value string) { + v[key] = []string{value} +} + +// Add adds the value to key. It appends to any existing +// values associated with key. +func (v Values) Add(key, value string) { + v[key] = append(v[key], value) +} + +// Del deletes the values associated with key. +func (v Values) Del(key string) { + delete(v, key) +} + +// Has checks whether a given key is set. +func (v Values) Has(key string) bool { + _, ok := v[key] + return ok +} + +// ParseQuery parses the URL-encoded query string and returns +// a map listing the values specified for each key. +// ParseQuery always returns a non-nil map containing all the +// valid query parameters found; err describes the first decoding error +// encountered, if any. +// +// Query is expected to be a list of key=value settings separated by ampersands. +// A setting without an equals sign is interpreted as a key set to an empty +// value. +// Settings containing a non-URL-encoded semicolon are considered invalid. +func ParseQuery(query string) (Values, error) { + m := make(Values) + err := parseQuery(m, query) + return m, err +} + +func parseQuery(m Values, query string) (err error) { + for query != "" { + var key string + key, query, _ = strings.Cut(query, "&") + if strings.Contains(key, ";") { + err = fmt.Errorf("invalid semicolon separator in query") + continue + } + if key == "" { + continue + } + key, value, _ := strings.Cut(key, "=") + key, err1 := QueryUnescape(key) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + value, err1 = QueryUnescape(value) + if err1 != nil { + if err == nil { + err = err1 + } + continue + } + m[key] = append(m[key], value) + } + return err +} + +// Encode encodes the values into “URL encoded” form +// ("bar=baz&foo=quux") sorted by key. +func (v Values) Encode() string { + if v == nil { + return "" + } + var buf strings.Builder + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vs := v[k] + keyEscaped := QueryEscape(k) + for _, v := range vs { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(keyEscaped) + buf.WriteByte('=') + buf.WriteString(QueryEscape(v)) + } + } + return buf.String() +} + +// resolvePath applies special path segments from refs and applies +// them to base, per RFC 3986. +func resolvePath(base, ref string) string { + var full string + if ref == "" { + full = base + } else if ref[0] != '/' { + i := strings.LastIndex(base, "/") + full = base[:i+1] + ref + } else { + full = ref + } + if full == "" { + return "" + } + + var ( + elem string + dst strings.Builder + ) + first := true + remaining := full + // We want to return a leading '/', so write it now. + dst.WriteByte('/') + found := true + for found { + elem, remaining, found = strings.Cut(remaining, "/") + if elem == "." { + first = false + // drop + continue + } + + if elem == ".." { + // Ignore the leading '/' we already wrote. + str := dst.String()[1:] + index := strings.LastIndexByte(str, '/') + + dst.Reset() + dst.WriteByte('/') + if index == -1 { + first = true + } else { + dst.WriteString(str[:index]) + } + } else { + if !first { + dst.WriteByte('/') + } + dst.WriteString(elem) + first = false + } + } + + if elem == "." || elem == ".." { + dst.WriteByte('/') + } + + // We wrote an initial '/', but we don't want two. + r := dst.String() + if len(r) > 1 && r[1] == '/' { + r = r[1:] + } + return r +} + +// IsAbs reports whether the URL is absolute. +// Absolute means that it has a non-empty scheme. +func (u *URL) IsAbs() bool { + return u.Scheme != "" +} + +// Parse parses a URL in the context of the receiver. The provided URL +// may be relative or absolute. Parse returns nil, err on parse +// failure, otherwise its return value is the same as ResolveReference. +func (u *URL) Parse(ref string) (*URL, error) { + refURL, err := Parse(ref) + if err != nil { + return nil, err + } + return u.ResolveReference(refURL), nil +} + +// ResolveReference resolves a URI reference to an absolute URI from +// an absolute base URI u, per RFC 3986 Section 5.2. The URI reference +// may be relative or absolute. ResolveReference always returns a new +// URL instance, even if the returned URL is identical to either the +// base or reference. If ref is an absolute URL, then ResolveReference +// ignores base and returns a copy of ref. +func (u *URL) ResolveReference(ref *URL) *URL { + url := *ref + if ref.Scheme == "" { + url.Scheme = u.Scheme + } + if ref.Scheme != "" || ref.Host != "" || ref.User != nil { + // The "absoluteURI" or "net_path" cases. + // We can ignore the error from setPath since we know we provided a + // validly-escaped path. + url.setPath(resolvePath(ref.EscapedPath(), "")) + return &url + } + if ref.Opaque != "" { + url.User = nil + url.Host = "" + url.Path = "" + return &url + } + if ref.Path == "" && !ref.ForceQuery && ref.RawQuery == "" { + url.RawQuery = u.RawQuery + if ref.Fragment == "" { + url.Fragment = u.Fragment + url.RawFragment = u.RawFragment + } + } + // The "abs_path" or "rel_path" cases. + url.Host = u.Host + url.User = u.User + url.setPath(resolvePath(u.EscapedPath(), ref.EscapedPath())) + return &url +} + +// Query parses RawQuery and returns the corresponding values. +// It silently discards malformed value pairs. +// To check errors use ParseQuery. +func (u *URL) Query() Values { + v, _ := ParseQuery(u.RawQuery) + return v +} + +// RequestURI returns the encoded path?query or opaque?query +// string that would be used in an HTTP request for u. +func (u *URL) RequestURI() string { + result := u.Opaque + if result == "" { + result = u.EscapedPath() + if result == "" { + result = "/" + } + } else { + if strings.HasPrefix(result, "//") { + result = u.Scheme + ":" + result + } + } + if u.ForceQuery || u.RawQuery != "" { + result += "?" + u.RawQuery + } + return result +} + +// Hostname returns u.Host, stripping any valid port number if present. +// +// If the result is enclosed in square brackets, as literal IPv6 addresses are, +// the square brackets are removed from the result. +func (u *URL) Hostname() string { + host, _ := splitHostPort(u.Host) + return host +} + +// Port returns the port part of u.Host, without the leading colon. +// +// If u.Host doesn't contain a valid numeric port, Port returns an empty string. +func (u *URL) Port() string { + _, port := splitHostPort(u.Host) + return port +} + +// splitHostPort separates host and port. If the port is not valid, it returns +// the entire input as host, and it doesn't check the validity of the host. +// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. +func splitHostPort(hostPort string) (host, port string) { + host = hostPort + + colon := strings.LastIndexByte(host, ':') + if colon != -1 && validOptionalPort(host[colon:]) { + host, port = host[:colon], host[colon+1:] + } + + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + + return +} + +// Marshaling interface implementations. +// Would like to implement MarshalText/UnmarshalText but that will change the JSON representation of URLs. + +func (u *URL) MarshalBinary() (text []byte, err error) { + return []byte(u.String()), nil +} + +func (u *URL) UnmarshalBinary(text []byte) error { + u1, err := Parse(string(text)) + if err != nil { + return err + } + *u = *u1 + return nil +} + +// JoinPath returns a new URL with the provided path elements joined to +// any existing path and the resulting path cleaned of any ./ or ../ elements. +// Any sequences of multiple / characters will be reduced to a single /. +func (u *URL) JoinPath(elem ...string) *URL { + elem = append([]string{u.EscapedPath()}, elem...) + var p string + if !strings.HasPrefix(elem[0], "/") { + // Return a relative path if u is relative, + // but ensure that it contains no ../ elements. + elem[0] = "/" + elem[0] + p = path.Join(elem...)[1:] + } else { + p = path.Join(elem...) + } + // path.Join will remove any trailing slashes. + // Preserve at least one. + if strings.HasSuffix(elem[len(elem)-1], "/") && !strings.HasSuffix(p, "/") { + p += "/" + } + url := *u + url.setPath(p) + return &url +} + +// validUserinfo reports whether s is a valid userinfo string per RFC 3986 +// Section 3.2.1: +// +// userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) +// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" +// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" +// / "*" / "+" / "," / ";" / "=" +// +// It doesn't validate pct-encoded. The caller does that via func unescape. +func validUserinfo(s string) bool { + for _, r := range s { + if 'A' <= r && r <= 'Z' { + continue + } + if 'a' <= r && r <= 'z' { + continue + } + if '0' <= r && r <= '9' { + continue + } + switch r { + case '-', '.', '_', ':', '~', '!', '$', '&', '\'', + '(', ')', '*', '+', ',', ';', '=', '%', '@': + continue + default: + return false + } + } + return true +} + +// stringContainsCTLByte reports whether s contains any ASCII control character. +func stringContainsCTLByte(s string) bool { + for i := 0; i < len(s); i++ { + b := s[i] + if b < ' ' || b == 0x7f { + return true + } + } + return false +} + +// JoinPath returns a URL string with the provided path elements joined to +// the existing path of base and the resulting path cleaned of any ./ or ../ elements. +func JoinPath(base string, elem ...string) (result string, err error) { + url, err := Parse(base) + if err != nil { + return + } + result = url.JoinPath(elem...).String() + return +} diff --git a/gnovm/stdlibs/net/url/url_test.gno b/gnovm/stdlibs/net/url/url_test.gno new file mode 100644 index 00000000000..0471a0a9e85 --- /dev/null +++ b/gnovm/stdlibs/net/url/url_test.gno @@ -0,0 +1,2222 @@ +// 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 url + +import ( + "bytes" + // encodingPkg "encoding" + // "encoding/gob" + "encoding/json" + "fmt" + "io" + // "net" + "strings" + "testing" +) + +type URLTest struct { + in string + out *URL // expected parse + roundtrip string // expected result of reserializing the URL; empty means same as "in". +} + +var urltests = []URLTest{ + // no path + { + "http://www.google.com", + &URL{ + Scheme: "http", + Host: "www.google.com", + }, + "", + }, + // path + { + "http://www.google.com/", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + }, + "", + }, + // path with hex escaping + { + "http://www.google.com/file%20one%26two", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/file one&two", + RawPath: "/file%20one%26two", + }, + "", + }, + // fragment with hex escaping + { + "http://www.google.com/#file%20one%26two", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + Fragment: "file one&two", + RawFragment: "file%20one%26two", + }, + "", + }, + // user + { + "ftp://webmaster@www.google.com/", + &URL{ + Scheme: "ftp", + User: User("webmaster"), + Host: "www.google.com", + Path: "/", + }, + "", + }, + // escape sequence in username + { + "ftp://john%20doe@www.google.com/", + &URL{ + Scheme: "ftp", + User: User("john doe"), + Host: "www.google.com", + Path: "/", + }, + "ftp://john%20doe@www.google.com/", + }, + // empty query + { + "http://www.google.com/?", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + ForceQuery: true, + }, + "", + }, + // query ending in question mark (Issue 14573) + { + "http://www.google.com/?foo=bar?", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "foo=bar?", + }, + "", + }, + // query + { + "http://www.google.com/?q=go+language", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + }, + "", + }, + // query with hex escaping: NOT parsed + { + "http://www.google.com/?q=go%20language", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go%20language", + }, + "", + }, + // %20 outside query + { + "http://www.google.com/a%20b?q=c+d", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/a b", + RawQuery: "q=c+d", + }, + "", + }, + // path without leading /, so no parsing + { + "http:www.google.com/?q=go+language", + &URL{ + Scheme: "http", + Opaque: "www.google.com/", + RawQuery: "q=go+language", + }, + "http:www.google.com/?q=go+language", + }, + // path without leading /, so no parsing + { + "http:%2f%2fwww.google.com/?q=go+language", + &URL{ + Scheme: "http", + Opaque: "%2f%2fwww.google.com/", + RawQuery: "q=go+language", + }, + "http:%2f%2fwww.google.com/?q=go+language", + }, + // non-authority with path; see golang.org/issue/46059 + { + "mailto:/webmaster@golang.org", + &URL{ + Scheme: "mailto", + Path: "/webmaster@golang.org", + OmitHost: true, + }, + "", + }, + // non-authority + { + "mailto:webmaster@golang.org", + &URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + "", + }, + // unescaped :// in query should not create a scheme + { + "/foo?query=http://bad", + &URL{ + Path: "/foo", + RawQuery: "query=http://bad", + }, + "", + }, + // leading // without scheme should create an authority + { + "//foo", + &URL{ + Host: "foo", + }, + "", + }, + // leading // without scheme, with userinfo, path, and query + { + "//user@foo/path?a=b", + &URL{ + User: User("user"), + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + "", + }, + // Three leading slashes isn't an authority, but doesn't return an error. + // (We can't return an error, as this code is also used via + // ServeHTTP -> ReadRequest -> Parse, which is arguably a + // different URL parsing context, but currently shares the + // same codepath) + { + "///threeslashes", + &URL{ + Path: "///threeslashes", + }, + "", + }, + { + "http://user:password@google.com", + &URL{ + Scheme: "http", + User: UserPassword("user", "password"), + Host: "google.com", + }, + "http://user:password@google.com", + }, + // unescaped @ in username should not confuse host + { + "http://j@ne:password@google.com", + &URL{ + Scheme: "http", + User: UserPassword("j@ne", "password"), + Host: "google.com", + }, + "http://j%40ne:password@google.com", + }, + // unescaped @ in password should not confuse host + { + "http://jane:p@ssword@google.com", + &URL{ + Scheme: "http", + User: UserPassword("jane", "p@ssword"), + Host: "google.com", + }, + "http://jane:p%40ssword@google.com", + }, + { + "http://j@ne:password@google.com/p@th?q=@go", + &URL{ + Scheme: "http", + User: UserPassword("j@ne", "password"), + Host: "google.com", + Path: "/p@th", + RawQuery: "q=@go", + }, + "http://j%40ne:password@google.com/p@th?q=@go", + }, + { + "http://www.google.com/?q=go+language#foo", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo", + }, + "", + }, + { + "http://www.google.com/?q=go+language#foo&bar", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo&bar", + }, + "http://www.google.com/?q=go+language#foo&bar", + }, + { + "http://www.google.com/?q=go+language#foo%26bar", + &URL{ + Scheme: "http", + Host: "www.google.com", + Path: "/", + RawQuery: "q=go+language", + Fragment: "foo&bar", + RawFragment: "foo%26bar", + }, + "http://www.google.com/?q=go+language#foo%26bar", + }, + { + "file:///home/adg/rabbits", + &URL{ + Scheme: "file", + Host: "", + Path: "/home/adg/rabbits", + }, + "file:///home/adg/rabbits", + }, + // "Windows" paths are no exception to the rule. + // See golang.org/issue/6027, especially comment #9. + { + "file:///C:/FooBar/Baz.txt", + &URL{ + Scheme: "file", + Host: "", + Path: "/C:/FooBar/Baz.txt", + }, + "file:///C:/FooBar/Baz.txt", + }, + // case-insensitive scheme + { + "MaIlTo:webmaster@golang.org", + &URL{ + Scheme: "mailto", + Opaque: "webmaster@golang.org", + }, + "mailto:webmaster@golang.org", + }, + // Relative path + { + "a/b/c", + &URL{ + Path: "a/b/c", + }, + "a/b/c", + }, + // escaped '?' in username and password + { + "http://%3Fam:pa%3Fsword@google.com", + &URL{ + Scheme: "http", + User: UserPassword("?am", "pa?sword"), + Host: "google.com", + }, + "", + }, + // host subcomponent; IPv4 address in RFC 3986 + { + "http://192.168.0.1/", + &URL{ + Scheme: "http", + Host: "192.168.0.1", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv4 address in RFC 3986 + { + "http://192.168.0.1:8080/", + &URL{ + Scheme: "http", + Host: "192.168.0.1:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address in RFC 3986 + { + "http://[fe80::1]/", + &URL{ + Scheme: "http", + Host: "[fe80::1]", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv6 address in RFC 3986 + { + "http://[fe80::1]:8080/", + &URL{ + Scheme: "http", + Host: "[fe80::1]:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25en0]/", // alphanum zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en0]", + Path: "/", + }, + "", + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25en0]:8080/", // alphanum zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en0]:8080", + Path: "/", + }, + "", + }, + // host subcomponent; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25%65%6e%301-._~]/", // percent-encoded+unreserved zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]", + Path: "/", + }, + "http://[fe80::1%25en01-._~]/", + }, + // host and port subcomponents; IPv6 address with zone identifier in RFC 6874 + { + "http://[fe80::1%25%65%6e%301-._~]:8080/", // percent-encoded+unreserved zone identifier + &URL{ + Scheme: "http", + Host: "[fe80::1%en01-._~]:8080", + Path: "/", + }, + "http://[fe80::1%25en01-._~]:8080/", + }, + // alternate escapings of path survive round trip + { + "http://rest.rsc.io/foo%2fbar/baz%2Fquux?alt=media", + &URL{ + Scheme: "http", + Host: "rest.rsc.io", + Path: "/foo/bar/baz/quux", + RawPath: "/foo%2fbar/baz%2Fquux", + RawQuery: "alt=media", + }, + "", + }, + // issue 12036 + { + "mysql://a,b,c/bar", + &URL{ + Scheme: "mysql", + Host: "a,b,c", + Path: "/bar", + }, + "", + }, + // worst case host, still round trips + { + "scheme://!$&'()*+,;=hello!:1/path", + &URL{ + Scheme: "scheme", + Host: "!$&'()*+,;=hello!:1", + Path: "/path", + }, + "", + }, + // worst case path, still round trips + { + "http://host/!$&'()*+,;=:@[hello]", + &URL{ + Scheme: "http", + Host: "host", + Path: "/!$&'()*+,;=:@[hello]", + RawPath: "/!$&'()*+,;=:@[hello]", + }, + "", + }, + // golang.org/issue/5684 + { + "http://example.com/oid/[order_id]", + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/oid/[order_id]", + RawPath: "/oid/[order_id]", + }, + "", + }, + // golang.org/issue/12200 (colon with empty port) + { + "http://192.168.0.2:8080/foo", + &URL{ + Scheme: "http", + Host: "192.168.0.2:8080", + Path: "/foo", + }, + "", + }, + { + "http://192.168.0.2:/foo", + &URL{ + Scheme: "http", + Host: "192.168.0.2:", + Path: "/foo", + }, + "", + }, + { + // Malformed IPv6 but still accepted. + "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080/foo", + &URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:8080", + Path: "/foo", + }, + "", + }, + { + // Malformed IPv6 but still accepted. + "http://2b01:e34:ef40:7730:8e70:5aff:fefe:edac:/foo", + &URL{ + Scheme: "http", + Host: "2b01:e34:ef40:7730:8e70:5aff:fefe:edac:", + Path: "/foo", + }, + "", + }, + { + "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080/foo", + &URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:8080", + Path: "/foo", + }, + "", + }, + { + "http://[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:/foo", + &URL{ + Scheme: "http", + Host: "[2b01:e34:ef40:7730:8e70:5aff:fefe:edac]:", + Path: "/foo", + }, + "", + }, + // golang.org/issue/7991 and golang.org/issue/12719 (non-ascii %-encoded in host) + { + "http://hello.世界.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + }, + { + "http://hello.%e4%b8%96%e7%95%8c.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + }, + { + "http://hello.%E4%B8%96%E7%95%8C.com/foo", + &URL{ + Scheme: "http", + Host: "hello.世界.com", + Path: "/foo", + }, + "", + }, + // golang.org/issue/10433 (path beginning with //) + { + "http://example.com//foo", + &URL{ + Scheme: "http", + Host: "example.com", + Path: "//foo", + }, + "", + }, + // test that we can reparse the host names we accept. + { + "myscheme://authority<\"hi\">/foo", + &URL{ + Scheme: "myscheme", + Host: "authority<\"hi\">", + Path: "/foo", + }, + "", + }, + // spaces in hosts are disallowed but escaped spaces in IPv6 scope IDs are grudgingly OK. + // This happens on Windows. + // golang.org/issue/14002 + { + "tcp://[2020::2020:20:2020:2020%25Windows%20Loves%20Spaces]:2020", + &URL{ + Scheme: "tcp", + Host: "[2020::2020:20:2020:2020%Windows Loves Spaces]:2020", + }, + "", + }, + // test we can roundtrip magnet url + // fix issue https://golang.org/issue/20054 + { + "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + &URL{ + Scheme: "magnet", + Host: "", + Path: "", + RawQuery: "xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + "magnet:?xt=urn:btih:c12fe1c06bba254a9dc9f519b335aa7c1367a88a&dn", + }, + { + "mailto:?subject=hi", + &URL{ + Scheme: "mailto", + Host: "", + Path: "", + RawQuery: "subject=hi", + }, + "mailto:?subject=hi", + }, +} + +// XXX: need `fmt` package +// more useful string for debugging than fmt's struct printer +// func ufmt(u *URL) string { +// var user, pass any +// if u.User != nil { +// user = u.User.Username() +// if p, ok := u.User.Password(); ok { +// pass = p +// } +// } +// return fmt.Sprintf("opaque=%q, scheme=%q, user=%#v, pass=%#v, host=%q, path=%q, rawpath=%q, rawq=%q, frag=%q, rawfrag=%q, forcequery=%v, omithost=%t", +// u.Opaque, u.Scheme, user, pass, u.Host, u.Path, u.RawPath, u.RawQuery, u.Fragment, u.RawFragment, u.ForceQuery, u.OmitHost) +// } + +func BenchmarkString(b *testing.B) { + b.StopTimer() + b.ReportAllocs() + for _, tt := range urltests { + u, err := Parse(tt.in) + if err != nil { + b.Errorf("Parse(%q) returned error %s", tt.in, err) + continue + } + if tt.roundtrip == "" { + continue + } + b.StartTimer() + var g string + for i := 0; i < b.N; i++ { + g = u.String() + } + b.StopTimer() + if w := tt.roundtrip; b.N > 0 && g != w { + b.Errorf("Parse(%q).String() == %q, want %q", tt.in, g, w) + } + } +} + +// XXX: need reflect +// func TestParse(t *testing.T) { +// for _, tt := range urltests { +// u, err := Parse(tt.in) +// if err != nil { +// t.Errorf("Parse(%q) returned error %v", tt.in, err) +// continue +// } +// if !reflect.DeepEqual(u, tt.out) { +// t.Errorf("Parse(%q):\n\tgot %v\n\twant %v\n", tt.in, ufmt(u), ufmt(tt.out)) +// } +// } +// } + +const pathThatLooksSchemeRelative = "//not.a.user@not.a.host/just/a/path" + +var parseRequestURLTests = []struct { + url string + expectedValid bool +}{ + {"http://foo.com", true}, + {"http://foo.com/", true}, + {"http://foo.com/path", true}, + {"/", true}, + {pathThatLooksSchemeRelative, true}, + {"//not.a.user@%66%6f%6f.com/just/a/path/also", true}, + {"*", true}, + {"http://192.168.0.1/", true}, + {"http://192.168.0.1:8080/", true}, + {"http://[fe80::1]/", true}, + {"http://[fe80::1]:8080/", true}, + + // Tests exercising RFC 6874 compliance: + {"http://[fe80::1%25en0]/", true}, // with alphanum zone identifier + {"http://[fe80::1%25en0]:8080/", true}, // with alphanum zone identifier + {"http://[fe80::1%25%65%6e%301-._~]/", true}, // with percent-encoded+unreserved zone identifier + {"http://[fe80::1%25%65%6e%301-._~]:8080/", true}, // with percent-encoded+unreserved zone identifier + + {"foo.html", false}, + {"../dir/", false}, + {" http://foo.com", false}, + {"http://192.168.0.%31/", false}, + {"http://192.168.0.%31:8080/", false}, + {"http://[fe80::%31]/", false}, + {"http://[fe80::%31]:8080/", false}, + {"http://[fe80::%31%25en0]/", false}, + {"http://[fe80::%31%25en0]:8080/", false}, + + // These two cases are valid as textual representations as + // described in RFC 4007, but are not valid as address + // literals with IPv6 zone identifiers in URIs as described in + // RFC 6874. + {"http://[fe80::1%en0]/", false}, + {"http://[fe80::1%en0]:8080/", false}, +} + +func TestParseRequestURI(t *testing.T) { + for _, test := range parseRequestURLTests { + _, err := ParseRequestURI(test.url) + if test.expectedValid && err != nil { + t.Errorf("ParseRequestURI(%q) gave err %v; want no error", test.url, err) + } else if !test.expectedValid && err == nil { + t.Errorf("ParseRequestURI(%q) gave nil error; want some error", test.url) + } + } + + url, err := ParseRequestURI(pathThatLooksSchemeRelative) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if url.Path != pathThatLooksSchemeRelative { + t.Errorf("ParseRequestURI path:\ngot %q\nwant %q", url.Path, pathThatLooksSchemeRelative) + } +} + +var stringURLTests = []struct { + url URL + want string +}{ + // No leading slash on path should prepend slash on String() call + { + url: URL{ + Scheme: "http", + Host: "www.google.com", + Path: "search", + }, + want: "http://www.google.com/search", + }, + // Relative path with first element containing ":" should be prepended with "./", golang.org/issue/17184 + { + url: URL{ + Path: "this:that", + }, + want: "./this:that", + }, + // Relative path with second element containing ":" should not be prepended with "./" + { + url: URL{ + Path: "here/this:that", + }, + want: "here/this:that", + }, + // Non-relative path with first element containing ":" should not be prepended with "./" + { + url: URL{ + Scheme: "http", + Host: "www.google.com", + Path: "this:that", + }, + want: "http://www.google.com/this:that", + }, +} + +func TestURLString(t *testing.T) { + for _, tt := range urltests { + u, err := Parse(tt.in) + if err != nil { + t.Errorf("Parse(%q) returned error %s", tt.in, err) + continue + } + expected := tt.in + if tt.roundtrip != "" { + expected = tt.roundtrip + } + s := u.String() + if s != expected { + t.Errorf("Parse(%q).String() == %q (expected %q)", tt.in, s, expected) + } + } + + for _, tt := range stringURLTests { + if got := tt.url.String(); got != tt.want { + t.Errorf("%+v.String() = %q; want %q", tt.url, got, tt.want) + } + } +} + +func TestURLRedacted(t *testing.T) { + cases := []struct { + name string + url *URL + want string + }{ + { + name: "non-blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: UserPassword("user", "password"), + }, + want: "http://user:xxxxx@host.tld/this:that", + }, + { + name: "blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: User("user"), + }, + want: "http://user@host.tld/this:that", + }, + { + name: "nil User", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + User: UserPassword("", "password"), + }, + want: "http://:xxxxx@host.tld/this:that", + }, + { + name: "blank Username, blank Password", + url: &URL{ + Scheme: "http", + Host: "host.tld", + Path: "this:that", + }, + want: "http://host.tld/this:that", + }, + { + name: "empty URL", + url: &URL{}, + want: "", + }, + { + name: "nil URL", + url: nil, + want: "", + }, + } + + for _, tt := range cases { + t := t + t.Run(tt.name, func(t *testing.T) { + if g, w := tt.url.Redacted(), tt.want; g != w { + t.Fatalf("got: %q\nwant: %q", g, w) + } + }) + } +} + +type EscapeTest struct { + in string + out string + err error +} + +var unescapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "1%41", + "1A", + nil, + }, + { + "1%41%42%43", + "1ABC", + nil, + }, + { + "%4a", + "J", + nil, + }, + { + "%6F", + "o", + nil, + }, + { + "%", // not enough characters after % + "", + EscapeError("%"), + }, + { + "%a", // not enough characters after % + "", + EscapeError("%a"), + }, + { + "%1", // not enough characters after % + "", + EscapeError("%1"), + }, + { + "123%45%6", // not enough characters after % + "", + EscapeError("%6"), + }, + { + "%zzzzz", // invalid hex digits + "", + EscapeError("%zz"), + }, + { + "a+b", + "a b", + nil, + }, + { + "a%20b", + "a b", + nil, + }, +} + +func TestUnescape(t *testing.T) { + for _, tt := range unescapeTests { + actual, err := QueryUnescape(tt.in) + if actual != tt.out || (err != nil) != (tt.err != nil) { + t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", tt.in, actual, err, tt.out, tt.err) + } + + in := tt.in + out := tt.out + if strings.Contains(tt.in, "+") { + in = strings.ReplaceAll(tt.in, "+", "%20") + actual, err := PathUnescape(in) + if actual != tt.out || (err != nil) != (tt.err != nil) { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, tt.out, tt.err) + } + if tt.err == nil { + s, err := QueryUnescape(strings.ReplaceAll(tt.in, "+", "XXX")) + if err != nil { + continue + } + in = tt.in + out = strings.ReplaceAll(s, "XXX", "+") + } + } + + actual, err = PathUnescape(in) + if actual != out || (err != nil) != (tt.err != nil) { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", in, actual, err, out, tt.err) + } + } +} + +var queryEscapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "one two", + "one+two", + nil, + }, + { + "10%", + "10%25", + nil, + }, + { + " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", + "+%3F%26%3D%23%2B%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09%3A%2F%40%24%27%28%29%2A%2C%3B", + nil, + }, +} + +func TestQueryEscape(t *testing.T) { + for _, tt := range queryEscapeTests { + actual := QueryEscape(tt.in) + if tt.out != actual { + t.Errorf("QueryEscape(%q) = %q, want %q", tt.in, actual, tt.out) + } + + // for bonus points, verify that escape:unescape is an identity. + roundtrip, err := QueryUnescape(actual) + if roundtrip != tt.in || err != nil { + t.Errorf("QueryUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") + } + } +} + +var pathEscapeTests = []EscapeTest{ + { + "", + "", + nil, + }, + { + "abc", + "abc", + nil, + }, + { + "abc+def", + "abc+def", + nil, + }, + { + "a/b", + "a%2Fb", + nil, + }, + { + "one two", + "one%20two", + nil, + }, + { + "10%", + "10%25", + nil, + }, + { + " ?&=#+%!<>#\"{}|\\^[]`☺\t:/@$'()*,;", + "%20%3F&=%23+%25%21%3C%3E%23%22%7B%7D%7C%5C%5E%5B%5D%60%E2%98%BA%09:%2F@$%27%28%29%2A%2C%3B", + nil, + }, +} + +func TestPathEscape(t *testing.T) { + for _, tt := range pathEscapeTests { + actual := PathEscape(tt.in) + if tt.out != actual { + t.Errorf("PathEscape(%q) = %q, want %q", tt.in, actual, tt.out) + } + + // for bonus points, verify that escape:unescape is an identity. + roundtrip, err := PathUnescape(actual) + if roundtrip != tt.in || err != nil { + t.Errorf("PathUnescape(%q) = %q, %s; want %q, %s", actual, roundtrip, err, tt.in, "[no error]") + } + } +} + +// NOTE: this part was already commented +// var userinfoTests = []UserinfoTest{ +// {"user", "password", "user:password"}, +// { +// "foo:bar", "~!@#$%^&*()_+{}|[]\\-=`:;'\"<>?,./", +// "foo%3Abar:~!%40%23$%25%5E&*()_+%7B%7D%7C%5B%5D%5C-=%60%3A;'%22%3C%3E?,.%2F", +// }, +// } + +type EncodeQueryTest struct { + m Values + expected string +} + +var encodeQueryTests = []EncodeQueryTest{ + {nil, ""}, + {Values{"q": {"puppies"}, "oe": {"utf8"}}, "oe=utf8&q=puppies"}, + {Values{"q": {"dogs", "&", "7"}}, "q=dogs&q=%26&q=7"}, + {Values{ + "a": {"a1", "a2", "a3"}, + "b": {"b1", "b2", "b3"}, + "c": {"c1", "c2", "c3"}, + }, "a=a1&a=a2&a=a3&b=b1&b=b2&b=b3&c=c1&c=c2&c=c3"}, +} + +func TestEncodeQuery(t *testing.T) { + for _, tt := range encodeQueryTests { + if q := tt.m.Encode(); q != tt.expected { + t.Errorf(`EncodeQuery(%+v) = %q, want %q`, tt.m, q, tt.expected) + } + } +} + +var resolvePathTests = []struct { + base, ref, expected string +}{ + {"a/b", ".", "/a/"}, + {"a/b", "c", "/a/c"}, + {"a/b", "..", "/"}, + {"a/", "..", "/"}, + {"a/", "../..", "/"}, + {"a/b/c", "..", "/a/"}, + {"a/b/c", "../d", "/a/d"}, + {"a/b/c", ".././d", "/a/d"}, + {"a/b", "./..", "/"}, + {"a/./b", ".", "/a/"}, + {"a/../", ".", "/"}, + {"a/.././b", "c", "/c"}, +} + +func TestResolvePath(t *testing.T) { + for _, test := range resolvePathTests { + got := resolvePath(test.base, test.ref) + if got != test.expected { + t.Errorf("For %q + %q got %q; expected %q", test.base, test.ref, got, test.expected) + } + } +} + +func BenchmarkResolvePath(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resolvePath("a/b/c", ".././d") + } +} + +var resolveReferenceTests = []struct { + base, rel, expected string +}{ + // Absolute URL references + {"http://foo.com?a=b", "https://bar.com/", "https://bar.com/"}, + {"http://foo.com/", "https://bar.com/?a=b", "https://bar.com/?a=b"}, + {"http://foo.com/", "https://bar.com/?", "https://bar.com/?"}, + {"http://foo.com/bar", "mailto:foo@example.com", "mailto:foo@example.com"}, + + // Path-absolute references + {"http://foo.com/bar", "/baz", "http://foo.com/baz"}, + {"http://foo.com/bar?a=b#f", "/baz", "http://foo.com/baz"}, + {"http://foo.com/bar?a=b", "/baz?", "http://foo.com/baz?"}, + {"http://foo.com/bar?a=b", "/baz?c=d", "http://foo.com/baz?c=d"}, + + // Multiple slashes + {"http://foo.com/bar", "http://foo.com//baz", "http://foo.com//baz"}, + {"http://foo.com/bar", "http://foo.com///baz/quux", "http://foo.com///baz/quux"}, + + // Scheme-relative + {"https://foo.com/bar?a=b", "//bar.com/quux", "https://bar.com/quux"}, + + // Path-relative references: + + // ... current directory + {"http://foo.com", ".", "http://foo.com/"}, + {"http://foo.com/bar", ".", "http://foo.com/"}, + {"http://foo.com/bar/", ".", "http://foo.com/bar/"}, + + // ... going down + {"http://foo.com", "bar", "http://foo.com/bar"}, + {"http://foo.com/", "bar", "http://foo.com/bar"}, + {"http://foo.com/bar/baz", "quux", "http://foo.com/bar/quux"}, + + // ... going up + {"http://foo.com/bar/baz", "../quux", "http://foo.com/quux"}, + {"http://foo.com/bar/baz", "../../../../../quux", "http://foo.com/quux"}, + {"http://foo.com/bar", "..", "http://foo.com/"}, + {"http://foo.com/bar/baz", "./..", "http://foo.com/"}, + // ".." in the middle (issue 3560) + {"http://foo.com/bar/baz", "quux/dotdot/../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/.././tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/./../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/././../../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/./.././../tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/dotdot/dotdot/./../../.././././tail", "http://foo.com/bar/quux/tail"}, + {"http://foo.com/bar/baz", "quux/./dotdot/../dotdot/../dot/./tail/..", "http://foo.com/bar/quux/dot/"}, + + // Remove any dot-segments prior to forming the target URI. + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4 + {"http://foo.com/dot/./dotdot/../foo/bar", "../baz", "http://foo.com/dot/baz"}, + + // Triple dot isn't special + {"http://foo.com/bar", "...", "http://foo.com/..."}, + + // Fragment + {"http://foo.com/bar", ".#frag", "http://foo.com/#frag"}, + {"http://example.org/", "#!$&%27()*+,;=", "http://example.org/#!$&%27()*+,;="}, + + // Paths with escaping (issue 16947). + {"http://foo.com/foo%2fbar/", "../baz", "http://foo.com/baz"}, + {"http://foo.com/1/2%2f/3%2f4/5", "../../a/b/c", "http://foo.com/1/a/b/c"}, + {"http://foo.com/1/2/3", "./a%2f../../b/..%2fc", "http://foo.com/1/2/b/..%2fc"}, + {"http://foo.com/1/2%2f/3%2f4/5", "./a%2f../b/../c", "http://foo.com/1/2%2f/3%2f4/a%2f../c"}, + {"http://foo.com/foo%20bar/", "../baz", "http://foo.com/baz"}, + {"http://foo.com/foo", "../bar%2fbaz", "http://foo.com/bar%2fbaz"}, + {"http://foo.com/foo%2dbar/", "./baz-quux", "http://foo.com/foo%2dbar/baz-quux"}, + + // RFC 3986: Normal Examples + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.1 + {"http://a/b/c/d;p?q", "g:h", "g:h"}, + {"http://a/b/c/d;p?q", "g", "http://a/b/c/g"}, + {"http://a/b/c/d;p?q", "./g", "http://a/b/c/g"}, + {"http://a/b/c/d;p?q", "g/", "http://a/b/c/g/"}, + {"http://a/b/c/d;p?q", "/g", "http://a/g"}, + {"http://a/b/c/d;p?q", "//g", "http://g"}, + {"http://a/b/c/d;p?q", "?y", "http://a/b/c/d;p?y"}, + {"http://a/b/c/d;p?q", "g?y", "http://a/b/c/g?y"}, + {"http://a/b/c/d;p?q", "#s", "http://a/b/c/d;p?q#s"}, + {"http://a/b/c/d;p?q", "g#s", "http://a/b/c/g#s"}, + {"http://a/b/c/d;p?q", "g?y#s", "http://a/b/c/g?y#s"}, + {"http://a/b/c/d;p?q", ";x", "http://a/b/c/;x"}, + {"http://a/b/c/d;p?q", "g;x", "http://a/b/c/g;x"}, + {"http://a/b/c/d;p?q", "g;x?y#s", "http://a/b/c/g;x?y#s"}, + {"http://a/b/c/d;p?q", "", "http://a/b/c/d;p?q"}, + {"http://a/b/c/d;p?q", ".", "http://a/b/c/"}, + {"http://a/b/c/d;p?q", "./", "http://a/b/c/"}, + {"http://a/b/c/d;p?q", "..", "http://a/b/"}, + {"http://a/b/c/d;p?q", "../", "http://a/b/"}, + {"http://a/b/c/d;p?q", "../g", "http://a/b/g"}, + {"http://a/b/c/d;p?q", "../..", "http://a/"}, + {"http://a/b/c/d;p?q", "../../", "http://a/"}, + {"http://a/b/c/d;p?q", "../../g", "http://a/g"}, + + // RFC 3986: Abnormal Examples + // https://datatracker.ietf.org/doc/html/rfc3986#section-5.4.2 + {"http://a/b/c/d;p?q", "../../../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "../../../../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "/./g", "http://a/g"}, + {"http://a/b/c/d;p?q", "/../g", "http://a/g"}, + {"http://a/b/c/d;p?q", "g.", "http://a/b/c/g."}, + {"http://a/b/c/d;p?q", ".g", "http://a/b/c/.g"}, + {"http://a/b/c/d;p?q", "g..", "http://a/b/c/g.."}, + {"http://a/b/c/d;p?q", "..g", "http://a/b/c/..g"}, + {"http://a/b/c/d;p?q", "./../g", "http://a/b/g"}, + {"http://a/b/c/d;p?q", "./g/.", "http://a/b/c/g/"}, + {"http://a/b/c/d;p?q", "g/./h", "http://a/b/c/g/h"}, + {"http://a/b/c/d;p?q", "g/../h", "http://a/b/c/h"}, + {"http://a/b/c/d;p?q", "g;x=1/./y", "http://a/b/c/g;x=1/y"}, + {"http://a/b/c/d;p?q", "g;x=1/../y", "http://a/b/c/y"}, + {"http://a/b/c/d;p?q", "g?y/./x", "http://a/b/c/g?y/./x"}, + {"http://a/b/c/d;p?q", "g?y/../x", "http://a/b/c/g?y/../x"}, + {"http://a/b/c/d;p?q", "g#s/./x", "http://a/b/c/g#s/./x"}, + {"http://a/b/c/d;p?q", "g#s/../x", "http://a/b/c/g#s/../x"}, + + // Extras. + {"https://a/b/c/d;p?q", "//g?q", "https://g?q"}, + {"https://a/b/c/d;p?q", "//g#s", "https://g#s"}, + {"https://a/b/c/d;p?q", "//g/d/e/f?y#s", "https://g/d/e/f?y#s"}, + {"https://a/b/c/d;p#s", "?y", "https://a/b/c/d;p?y"}, + {"https://a/b/c/d;p?q#s", "?y", "https://a/b/c/d;p?y"}, + + // Empty path and query but with ForceQuery (issue 46033). + {"https://a/b/c/d;p?q#s", "?", "https://a/b/c/d;p?"}, +} + +func TestResolveReference(t *testing.T) { + mustParse := func(url string) *URL { + u, err := Parse(url) + if err != nil { + t.Fatalf("Parse(%q) got err %v", url, err) + } + return u + } + opaque := &URL{Scheme: "scheme", Opaque: "opaque"} + for _, test := range resolveReferenceTests { + base := mustParse(test.base) + rel := mustParse(test.rel) + url := base.ResolveReference(rel) + if got := url.String(); got != test.expected { + t.Errorf("URL(%q).ResolveReference(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) + } + // Ensure that new instances are returned. + if base == url { + t.Errorf("Expected URL.ResolveReference to return new URL instance.") + } + // Test the convenience wrapper too. + url, err := base.Parse(test.rel) + if err != nil { + t.Errorf("URL(%q).Parse(%q) failed: %v", test.base, test.rel, err) + } else if got := url.String(); got != test.expected { + t.Errorf("URL(%q).Parse(%q)\ngot %q\nwant %q", test.base, test.rel, got, test.expected) + } else if base == url { + // Ensure that new instances are returned for the wrapper too. + t.Errorf("Expected URL.Parse to return new URL instance.") + } + // Ensure Opaque resets the URL. + url = base.ResolveReference(opaque) + if *url != *opaque { + t.Errorf("ResolveReference failed to resolve opaque URL:\ngot %#v\nwant %#v", url, opaque) + } + // Test the convenience wrapper with an opaque URL too. + url, err = base.Parse("scheme:opaque") + if err != nil { + t.Errorf(`URL(%q).Parse("scheme:opaque") failed: %v`, test.base, err) + } else if *url != *opaque { + t.Errorf("Parse failed to resolve opaque URL:\ngot %#v\nwant %#v", opaque, url) + } else if base == url { + // Ensure that new instances are returned, again. + t.Errorf("Expected URL.Parse to return new URL instance.") + } + } +} + +func TestQueryValues(t *testing.T) { + u, _ := Parse("http://x.com?foo=bar&bar=1&bar=2&baz") + v := u.Query() + if len(v) != 3 { + t.Errorf("got %d keys in Query values, want 3", len(v)) + } + if g, e := v.Get("foo"), "bar"; g != e { + t.Errorf("Get(foo) = %q, want %q", g, e) + } + // Case sensitive: + if g, e := v.Get("Foo"), ""; g != e { + t.Errorf("Get(Foo) = %q, want %q", g, e) + } + if g, e := v.Get("bar"), "1"; g != e { + t.Errorf("Get(bar) = %q, want %q", g, e) + } + if g, e := v.Get("baz"), ""; g != e { + t.Errorf("Get(baz) = %q, want %q", g, e) + } + if h, e := v.Has("foo"), true; h != e { + t.Errorf("Has(foo) = %t, want %t", h, e) + } + if h, e := v.Has("bar"), true; h != e { + t.Errorf("Has(bar) = %t, want %t", h, e) + } + if h, e := v.Has("baz"), true; h != e { + t.Errorf("Has(baz) = %t, want %t", h, e) + } + if h, e := v.Has("noexist"), false; h != e { + t.Errorf("Has(noexist) = %t, want %t", h, e) + } + v.Del("bar") + if g, e := v.Get("bar"), ""; g != e { + t.Errorf("second Get(bar) = %q, want %q", g, e) + } +} + +type parseTest struct { + query string + out Values + ok bool +} + +var parseTests = []parseTest{ + { + query: "a=1", + out: Values{"a": []string{"1"}}, + ok: true, + }, + { + query: "a=1&b=2", + out: Values{"a": []string{"1"}, "b": []string{"2"}}, + ok: true, + }, + { + query: "a=1&a=2&a=banana", + out: Values{"a": []string{"1", "2", "banana"}}, + ok: true, + }, + { + query: "ascii=%3Ckey%3A+0x90%3E", + out: Values{"ascii": []string{""}}, + ok: true, + }, + { + query: "a=1;b=2", + out: Values{}, + ok: false, + }, + { + query: "a;b=1", + out: Values{}, + ok: false, + }, + { + query: "a=%3B", // hex encoding for semicolon + out: Values{"a": []string{";"}}, + ok: true, + }, + { + query: "a%3Bb=1", + out: Values{"a;b": []string{"1"}}, + ok: true, + }, + { + query: "a=1&a=2;a=banana", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: "a;b&c=1", + out: Values{"c": []string{"1"}}, + ok: false, + }, + { + query: "a=1&b=2;a=3&c=4", + out: Values{"a": []string{"1"}, "c": []string{"4"}}, + ok: false, + }, + { + query: "a=1&b=2;c=3", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: ";", + out: Values{}, + ok: false, + }, + { + query: "a=1;", + out: Values{}, + ok: false, + }, + { + query: "a=1&;", + out: Values{"a": []string{"1"}}, + ok: false, + }, + { + query: ";a=1&b=2", + out: Values{"b": []string{"2"}}, + ok: false, + }, + { + query: "a=1&b=2;", + out: Values{"a": []string{"1"}}, + ok: false, + }, +} + +func TestParseQuery(t *testing.T) { + for _, test := range parseTests { + t.Run(test.query, func(t *testing.T) { + form, err := ParseQuery(test.query) + if test.ok != (err == nil) { + want := "" + if test.ok { + want = "" + } + t.Errorf("Unexpected error: %v, want %v", err, want) + } + if len(form) != len(test.out) { + t.Errorf("len(form) = %d, want %d", len(form), len(test.out)) + } + for k, evs := range test.out { + vs, ok := form[k] + if !ok { + t.Errorf("Missing key %q", k) + continue + } + if len(vs) != len(evs) { + t.Errorf("len(form[%q]) = %d, want %d", k, len(vs), len(evs)) + continue + } + for j, ev := range evs { + if v := vs[j]; v != ev { + t.Errorf("form[%q][%d] = %q, want %q", k, j, v, ev) + } + } + } + }) + } +} + +type RequestURITest struct { + url *URL + out string +} + +var requritests = []RequestURITest{ + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "", + }, + "/", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + }, + "/a%20b", + }, + // golang.org/issue/4860 variant 1 + { + &URL{ + Scheme: "http", + Host: "example.com", + Opaque: "/%2F/%2F/", + }, + "/%2F/%2F/", + }, + // golang.org/issue/4860 variant 2 + { + &URL{ + Scheme: "http", + Host: "example.com", + Opaque: "//other.example.com/%2F/%2F/", + }, + "http://other.example.com/%2F/%2F/", + }, + // better fix for issue 4860 + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/////", + RawPath: "/%2F/%2F/", + }, + "/%2F/%2F/", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/////", + RawPath: "/WRONG/", // ignored because doesn't match Path + }, + "/////", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + RawQuery: "q=go+language", + }, + "/a%20b?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a b", + RawPath: "/a b", // ignored because invalid + RawQuery: "q=go+language", + }, + "/a%20b?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/a?b", + RawPath: "/a?b", // ignored because invalid + RawQuery: "q=go+language", + }, + "/a%3Fb?q=go+language", + }, + { + &URL{ + Scheme: "myschema", + Opaque: "opaque", + }, + "opaque", + }, + { + &URL{ + Scheme: "myschema", + Opaque: "opaque", + RawQuery: "q=go+language", + }, + "opaque?q=go+language", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "//foo", + }, + "//foo", + }, + { + &URL{ + Scheme: "http", + Host: "example.com", + Path: "/foo", + ForceQuery: true, + }, + "/foo?", + }, +} + +func TestRequestURI(t *testing.T) { + for _, tt := range requritests { + s := tt.url.RequestURI() + if s != tt.out { + t.Errorf("%#v.RequestURI() == %q (expected %q)", tt.url, s, tt.out) + } + } +} + +func TestParseFailure(t *testing.T) { + // Test that the first parse error is returned. + const url = "%gh&%ij" + _, err := ParseQuery(url) + errStr := fmt.Sprint(err) + if !strings.Contains(errStr, "%gh") { + t.Errorf(`ParseQuery(%q) returned error %q, want something containing %q"`, url, errStr, "%gh") + } +} + +func TestParseErrors(t *testing.T) { + tests := []struct { + in string + wantErr bool + }{ + {"http://[::1]", false}, + {"http://[::1]:80", false}, + {"http://[::1]:namedport", true}, // rfc3986 3.2.3 + {"http://x:namedport", true}, // rfc3986 3.2.3 + {"http://[::1]/", false}, + {"http://[::1]a", true}, + {"http://[::1]%23", true}, + {"http://[::1%25en0]", false}, // valid zone id + {"http://[::1]:", false}, // colon, but no port OK + {"http://x:", false}, // colon, but no port OK + {"http://[::1]:%38%30", true}, // not allowed: % encoding only for non-ASCII + {"http://[::1%25%41]", false}, // RFC 6874 allows over-escaping in zone + {"http://[%10::1]", true}, // no %xx escapes in IP address + {"http://[::1]/%48", false}, // %xx in path is fine + {"http://%41:8080/", true}, // not allowed: % encoding only for non-ASCII + {"mysql://x@y(z:123)/foo", true}, // not well-formed per RFC 3986, golang.org/issue/33646 + {"mysql://x@y(1.2.3.4:123)/foo", true}, + + {" http://foo.com", true}, // invalid character in schema + {"ht tp://foo.com", true}, // invalid character in schema + {"ahttp://foo.com", false}, // valid schema characters + {"1http://foo.com", true}, // invalid character in schema + + {"http://[]%20%48%54%54%50%2f%31%2e%31%0a%4d%79%48%65%61%64%65%72%3a%20%31%32%33%0a%0a/", true}, // golang.org/issue/11208 + {"http://a b.com/", true}, // no space in host name please + {"cache_object://foo", true}, // scheme cannot have _, relative path cannot have : in first segment + {"cache_object:foo", true}, + {"cache_object:foo/bar", true}, + {"cache_object/:foo/bar", false}, + } + for _, tt := range tests { + u, err := Parse(tt.in) + if tt.wantErr { + if err == nil { + t.Errorf("Parse(%q) = %#v; want an error", tt.in, u) + } + continue + } + if err != nil { + t.Errorf("Parse(%q) = %v; want no error", tt.in, err) + } + } +} + +// Issue 11202 +func TestStarRequest(t *testing.T) { + u, err := Parse("*") + if err != nil { + t.Fatal(err) + } + if got, want := u.RequestURI(), "*"; got != want { + t.Errorf("RequestURI = %q; want %q", got, want) + } +} + +type shouldEscapeTest struct { + in byte + mode encoding + escape bool +} + +var shouldEscapeTests = []shouldEscapeTest{ + // Unreserved characters (§2.3) + {'a', encodePath, false}, + {'a', encodeUserPassword, false}, + {'a', encodeQueryComponent, false}, + {'a', encodeFragment, false}, + {'a', encodeHost, false}, + {'z', encodePath, false}, + {'A', encodePath, false}, + {'Z', encodePath, false}, + {'0', encodePath, false}, + {'9', encodePath, false}, + {'-', encodePath, false}, + {'-', encodeUserPassword, false}, + {'-', encodeQueryComponent, false}, + {'-', encodeFragment, false}, + {'.', encodePath, false}, + {'_', encodePath, false}, + {'~', encodePath, false}, + + // User information (§3.2.1) + {':', encodeUserPassword, true}, + {'/', encodeUserPassword, true}, + {'?', encodeUserPassword, true}, + {'@', encodeUserPassword, true}, + {'$', encodeUserPassword, false}, + {'&', encodeUserPassword, false}, + {'+', encodeUserPassword, false}, + {',', encodeUserPassword, false}, + {';', encodeUserPassword, false}, + {'=', encodeUserPassword, false}, + + // Host (IP address, IPv6 address, registered name, port suffix; §3.2.2) + {'!', encodeHost, false}, + {'$', encodeHost, false}, + {'&', encodeHost, false}, + {'\'', encodeHost, false}, + {'(', encodeHost, false}, + {')', encodeHost, false}, + {'*', encodeHost, false}, + {'+', encodeHost, false}, + {',', encodeHost, false}, + {';', encodeHost, false}, + {'=', encodeHost, false}, + {':', encodeHost, false}, + {'[', encodeHost, false}, + {']', encodeHost, false}, + {'0', encodeHost, false}, + {'9', encodeHost, false}, + {'A', encodeHost, false}, + {'z', encodeHost, false}, + {'_', encodeHost, false}, + {'-', encodeHost, false}, + {'.', encodeHost, false}, +} + +func TestShouldEscape(t *testing.T) { + for _, tt := range shouldEscapeTests { + if shouldEscape(tt.in, tt.mode) != tt.escape { + t.Errorf("shouldEscape(%q, %v) returned %v; expected %v", tt.in, tt.mode, !tt.escape, tt.escape) + } + } +} + +type timeoutError struct { + timeout bool +} + +func (e *timeoutError) Error() string { return "timeout error" } +func (e *timeoutError) Timeout() bool { return e.timeout } + +type temporaryError struct { + temporary bool +} + +func (e *temporaryError) Error() string { return "temporary error" } +func (e *temporaryError) Temporary() bool { return e.temporary } + +type timeoutTemporaryError struct { + timeoutError + temporaryError +} + +func (e *timeoutTemporaryError) Error() string { return "timeout/temporary error" } + +var netErrorTests = []struct { + err error + timeout bool + temporary bool +}{{ + err: &Error{"Get", "http://google.com/", &timeoutError{timeout: true}}, + timeout: true, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutError{timeout: false}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &temporaryError{temporary: true}}, + timeout: false, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &temporaryError{temporary: false}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: true}}}, + timeout: true, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: true}}}, + timeout: false, + temporary: true, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: true}, temporaryError{temporary: false}}}, + timeout: true, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", &timeoutTemporaryError{timeoutError{timeout: false}, temporaryError{temporary: false}}}, + timeout: false, + temporary: false, +}, { + err: &Error{"Get", "http://google.com/", io.EOF}, + timeout: false, + temporary: false, +}} + +// XXX: panic: not yet supported +// XXX: need `net` package +// Test that Error implements net.Error and that it forwards +// func TestURLErrorImplementsNetError(t *testing.T) { +// for i, tt := range netErrorTests { +// err, ok := tt.err.(net.Error) +// if !ok { +// t.Errorf("%d: %T does not implement net.Error", i+1, tt.err) +// continue +// } +// if err.Timeout() != tt.timeout { +// t.Errorf("%d: err.Timeout(): got %v, want %v", i+1, err.Timeout(), tt.timeout) +// continue +// } +// if err.Temporary() != tt.temporary { +// t.Errorf("%d: err.Temporary(): got %v, want %v", i+1, err.Temporary(), tt.temporary) +// } +// } +// } + +func TestURLHostnameAndPort(t *testing.T) { + tests := []struct { + in string // URL.Host field + host string + port string + }{ + {"foo.com:80", "foo.com", "80"}, + {"foo.com", "foo.com", ""}, + {"foo.com:", "foo.com", ""}, + {"FOO.COM", "FOO.COM", ""}, // no canonicalization + {"1.2.3.4", "1.2.3.4", ""}, + {"1.2.3.4:80", "1.2.3.4", "80"}, + {"[1:2:3:4]", "1:2:3:4", ""}, + {"[1:2:3:4]:80", "1:2:3:4", "80"}, + {"[::1]:80", "::1", "80"}, + {"[::1]", "::1", ""}, + {"[::1]:", "::1", ""}, + {"localhost", "localhost", ""}, + {"localhost:443", "localhost", "443"}, + {"some.super.long.domain.example.org:8080", "some.super.long.domain.example.org", "8080"}, + {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:17000", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", "17000"}, + {"[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", ""}, + + // Ensure that even when not valid, Host is one of "Hostname", + // "Hostname:Port", "[Hostname]" or "[Hostname]:Port". + // See https://golang.org/issue/29098. + {"[google.com]:80", "google.com", "80"}, + {"google.com]:80", "google.com]", "80"}, + {"google.com:80_invalid_port", "google.com:80_invalid_port", ""}, + {"[::1]extra]:80", "::1]extra", "80"}, + {"google.com]extra:extra", "google.com]extra:extra", ""}, + } + for _, tt := range tests { + u := &URL{Host: tt.in} + host, port := u.Hostname(), u.Port() + if host != tt.host { + t.Errorf("Hostname for Host %q = %q; want %q", tt.in, host, tt.host) + } + if port != tt.port { + t.Errorf("Port for Host %q = %q; want %q", tt.in, port, tt.port) + } + } +} + +// XXX: this require `encoding` package +// var ( +// _ encodingPkg.BinaryMarshaler = (*URL)(nil) +// _ encodingPkg.BinaryUnmarshaler = (*URL)(nil) +// ) + +func TestJSON(t *testing.T) { + u, err := Parse("https://www.google.com/x?y=z") + if err != nil { + t.Fatal(err) + } + js, err := json.Marshal(u) + if err != nil { + t.Fatal(err) + } + + // If only we could implement TextMarshaler/TextUnmarshaler, + // this would work: + // + // if string(js) != strconv.Quote(u.String()) { + // t.Errorf("json encoding: %s\nwant: %s\n", js, strconv.Quote(u.String())) + // } + + u1 := new(URL) + err = json.Unmarshal(js, u1) + if err != nil { + t.Fatal(err) + } + if u1.String() != u.String() { + t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) + } +} + +// XXX: this require `encoding/gob` +// func TestGob(t *testing.T) { +// u, err := Parse("https://www.google.com/x?y=z") +// if err != nil { +// t.Fatal(err) +// } +// var w bytes.Buffer +// err = gob.NewEncoder(&w).Encode(u) +// if err != nil { +// t.Fatal(err) +// } + +// u1 := new(URL) +// err = gob.NewDecoder(&w).Decode(u1) +// if err != nil { +// t.Fatal(err) +// } +// if u1.String() != u.String() { +// t.Errorf("json decoded to: %s\nwant: %s\n", u1, u) +// } +// } + +func TestNilUser(t *testing.T) { + defer func() { + if v := recover(); v != nil { + t.Fatalf("unexpected panic: %v", v) + } + }() + + u, err := Parse("http://foo.com/") + if err != nil { + t.Fatalf("parse err: %v", err) + } + + if v := u.User.Username(); v != "" { + t.Fatalf("expected empty username, got %s", v) + } + + if v, ok := u.User.Password(); v != "" || ok { + t.Fatalf("expected empty password, got %s (%v)", v, ok) + } + + if v := u.User.String(); v != "" { + t.Fatalf("expected empty string, got %s", v) + } +} + +// XXX: panic: not yet supported +// func TestInvalidUserPassword(t *testing.T) { +// t.Skip("panic: not yet supported") + +// _, err := Parse("http://user^:passwo^rd@foo.com/") +// got, wantsub := fmt.Sprint(err), "net/url: invalid userinfo" // panic +// if !strings.Contains(got, wantsub) { +// t.Errorf("error = %q; want substring %q", got, wantsub) +// } +// } + +// XXX: panic: not yet supported +// func TestRejectControlCharacters(t *testing.T) { +// tests := []string{ +// "http://foo.com/?foo\nbar", +// "http\r://foo.com/", +// "http://foo\x7f.com/", +// } +// for _, s := range tests { +// _, err := Parse(s) +// const wantSub = "net/url: invalid control character in URL" +// if got := fmt.Sprint(err); !strings.Contains(got, wantSub) { // panic +// t.Errorf("Parse(%q) error = %q; want substring %q", s, got, wantSub) +// } +// } + +// // But don't reject non-ASCII CTLs, at least for now: +// if _, err := Parse("http://foo.com/ctl\x80"); err != nil { +// t.Errorf("error parsing URL with non-ASCII control byte: %v", err) +// } +// } + +var escapeBenchmarks = []struct { + unescaped string + query string + path string +}{ + { + unescaped: "one two", + query: "one+two", + path: "one%20two", + }, + { + unescaped: "Фотки собак", + query: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8+%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", + path: "%D0%A4%D0%BE%D1%82%D0%BA%D0%B8%20%D1%81%D0%BE%D0%B1%D0%B0%D0%BA", + }, + + { + unescaped: "shortrun(break)shortrun", + query: "shortrun%28break%29shortrun", + path: "shortrun%28break%29shortrun", + }, + + { + unescaped: "longerrunofcharacters(break)anotherlongerrunofcharacters", + query: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", + path: "longerrunofcharacters%28break%29anotherlongerrunofcharacters", + }, + + { + unescaped: strings.Repeat("padded/with+various%characters?that=need$some@escaping+paddedsowebreak/256bytes", 4), + query: strings.Repeat("padded%2Fwith%2Bvarious%25characters%3Fthat%3Dneed%24some%40escaping%2Bpaddedsowebreak%2F256bytes", 4), + path: strings.Repeat("padded%2Fwith+various%25characters%3Fthat=need$some@escaping+paddedsowebreak%2F256bytes", 4), + }, +} + +func BenchmarkQueryEscape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g = QueryEscape(tc.unescaped) + } + b.StopTimer() + if g != tc.query { + b.Errorf("QueryEscape(%q) == %q, want %q", tc.unescaped, g, tc.query) + } + }) + } +} + +func BenchmarkPathEscape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g = PathEscape(tc.unescaped) + } + b.StopTimer() + if g != tc.path { + b.Errorf("PathEscape(%q) == %q, want %q", tc.unescaped, g, tc.path) + } + }) + } +} + +func BenchmarkQueryUnescape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g, _ = QueryUnescape(tc.query) + } + b.StopTimer() + if g != tc.unescaped { + b.Errorf("QueryUnescape(%q) == %q, want %q", tc.query, g, tc.unescaped) + } + }) + } +} + +func BenchmarkPathUnescape(b *testing.B) { + for _, tc := range escapeBenchmarks { + b.Run("", func(b *testing.B) { + b.ReportAllocs() + var g string + for i := 0; i < b.N; i++ { + g, _ = PathUnescape(tc.path) + } + b.StopTimer() + if g != tc.unescaped { + b.Errorf("PathUnescape(%q) == %q, want %q", tc.path, g, tc.unescaped) + } + }) + } +} + +func TestJoinPath(t *testing.T) { + tests := []struct { + base string + elem []string + out string + }{ + { + base: "https://go.googlesource.com", + elem: []string{"go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/a/b/c", + elem: []string{"../../../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/", + elem: []string{"../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com", + elem: []string{"../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com", + elem: []string{"../go", "../../go", "../../../go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/../go", + elem: nil, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com/", + elem: []string{"./go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com//", + elem: []string{"/go"}, + out: "https://go.googlesource.com/go", + }, + { + base: "https://go.googlesource.com//", + elem: []string{"/go", "a", "b", "c"}, + out: "https://go.googlesource.com/go/a/b/c", + }, + { + base: "http://[fe80::1%en0]:8080/", + elem: []string{"/go"}, + }, + { + base: "https://go.googlesource.com", + elem: []string{"go/"}, + out: "https://go.googlesource.com/go/", + }, + { + base: "https://go.googlesource.com", + elem: []string{"go//"}, + out: "https://go.googlesource.com/go/", + }, + { + base: "https://go.googlesource.com", + elem: nil, + out: "https://go.googlesource.com/", + }, + { + base: "https://go.googlesource.com/", + elem: nil, + out: "https://go.googlesource.com/", + }, + { + base: "https://go.googlesource.com/a%2fb", + elem: []string{"c"}, + out: "https://go.googlesource.com/a%2fb/c", + }, + { + base: "https://go.googlesource.com/a%2fb", + elem: []string{"c%2fd"}, + out: "https://go.googlesource.com/a%2fb/c%2fd", + }, + { + base: "https://go.googlesource.com/a/b", + elem: []string{"/go"}, + out: "https://go.googlesource.com/a/b/go", + }, + { + base: "/", + elem: nil, + out: "/", + }, + { + base: "a", + elem: nil, + out: "a", + }, + { + base: "a", + elem: []string{"b"}, + out: "a/b", + }, + { + base: "a", + elem: []string{"../b"}, + out: "b", + }, + { + base: "a", + elem: []string{"../../b"}, + out: "b", + }, + { + base: "", + elem: []string{"a"}, + out: "a", + }, + { + base: "", + elem: []string{"../a"}, + out: "a", + }, + } + for _, tt := range tests { + wantErr := "nil" + if tt.out == "" { + wantErr = "non-nil error" + } + if out, err := JoinPath(tt.base, tt.elem...); out != tt.out || (err == nil) != (tt.out != "") { + t.Errorf("JoinPath(%q, %q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) + } + var out string + u, err := Parse(tt.base) + if err == nil { + u = u.JoinPath(tt.elem...) + out = u.String() + } + if out != tt.out || (err == nil) != (tt.out != "") { + t.Errorf("Parse(%q).JoinPath(%q) = %q, %v, want %q, %v", tt.base, tt.elem, out, err, tt.out, wantErr) + } + } +} diff --git a/gnovm/stdlibs/strings/strings.gno b/gnovm/stdlibs/strings/strings.gno index 21ef6282cb5..968c44fb1a0 100644 --- a/gnovm/stdlibs/strings/strings.gno +++ b/gnovm/stdlibs/strings/strings.gno @@ -1104,3 +1104,36 @@ func Index(s, substr string) int { } return -1 } + +// Cut slices s around the first instance of sep, +// returning the text before and after sep. +// The found result reports whether sep appears in s. +// If sep does not appear in s, cut returns s, "", false. +func Cut(s, sep string) (before, after string, found bool) { + if i := Index(s, sep); i >= 0 { + return s[:i], s[i+len(sep):], true + } + return s, "", false +} + +// CutPrefix returns s without the provided leading prefix string +// and reports whether it found the prefix. +// If s doesn't start with prefix, CutPrefix returns s, false. +// If prefix is the empty string, CutPrefix returns s, true. +func CutPrefix(s, prefix string) (after string, found bool) { + if !HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} + +// CutSuffix returns s without the provided ending suffix string +// and reports whether it found the suffix. +// If s doesn't end with suffix, CutSuffix returns s, false. +// If suffix is the empty string, CutSuffix returns s, true. +func CutSuffix(s, suffix string) (before string, found bool) { + if !HasSuffix(s, suffix) { + return s, false + } + return s[:len(s)-len(suffix)], true +} From 78466ce9ac3f6014047c46eda9073775ac793008 Mon Sep 17 00:00:00 2001 From: Antonio Navarro Perez Date: Fri, 22 Sep 2023 21:52:50 +0200 Subject: [PATCH 06/93] fix(vm): Release VM properly (#1116) Properly clean slices using make (the internal logic is expecting slices to not be nil). It closes #1033 - [X] Added new tests, or not needed, or not feasible - [X] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [X] Updated the official documentation or not needed - [X] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [X] Added references to related issues and PRs - [X] Provided any useful hints for running manual tests - [X] 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). --------- Signed-off-by: Antonio Navarro Perez Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/pkg/gnolang/machine.go | 42 +++++++++++++------------------ gnovm/pkg/gnolang/machine_test.go | 10 ++++++++ 2 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 gnovm/pkg/gnolang/machine_test.go diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 60008d3c34d..94232e014d2 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -81,8 +81,8 @@ type MachineOptions struct { var machinePool = sync.Pool{ New: func() interface{} { return &Machine{ - Ops: make([]Op, OpSize), - Values: make([]TypedValue, ValueSize), + Ops: make([]Op, VMSliceSize), + Values: make([]TypedValue, VMSliceSize), } }, } @@ -117,20 +117,14 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { } context := opts.Context mm := machinePool.Get().(*Machine) - *mm = Machine{ - Ops: mm.Ops, - NumOps: 0, - Values: mm.Values, - NumValues: 0, - Package: pv, - Alloc: alloc, - CheckTypes: checkTypes, - ReadOnly: readOnly, - MaxCycles: maxCycles, - Output: output, - Store: store, - Context: context, - } + mm.Package = pv + mm.Alloc = alloc + mm.CheckTypes = checkTypes + mm.ReadOnly = readOnly + mm.MaxCycles = maxCycles + mm.Output = output + mm.Store = store + mm.Context = context if pv != nil { mm.SetActivePackage(pv) @@ -139,13 +133,12 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { } const ( - OpSize = 1024 - ValueSize = 1024 + VMSliceSize = 1024 ) var ( - opZeroed [OpSize]Op - valueZeroed [ValueSize]TypedValue + opZeroed [VMSliceSize]Op + valueZeroed [VMSliceSize]TypedValue ) // m should not be used after this call @@ -154,13 +147,14 @@ var ( // and prevent objects that were not taken from // the pool, to call Release func (m *Machine) Release() { - // copy() // here we zero in the values for the next user m.NumOps = 0 m.NumValues = 0 - // this is the fastest way to zero-in a slice in Go - copy(m.Ops, opZeroed[:]) - copy(m.Values, valueZeroed[:]) + + ops, values := m.Ops[:VMSliceSize:VMSliceSize], m.Values[:VMSliceSize:VMSliceSize] + copy(ops, opZeroed[:]) + copy(values, valueZeroed[:]) + *m = Machine{Ops: ops, Values: values} machinePool.Put(m) } diff --git a/gnovm/pkg/gnolang/machine_test.go b/gnovm/pkg/gnolang/machine_test.go new file mode 100644 index 00000000000..e37fc508cb6 --- /dev/null +++ b/gnovm/pkg/gnolang/machine_test.go @@ -0,0 +1,10 @@ +package gnolang + +import "testing" + +func BenchmarkCreateNewMachine(b *testing.B) { + for i := 0; i < b.N; i++ { + m := NewMachineWithOptions(MachineOptions{}) + m.Release() + } +} From 5a7d005fc14ff2b31483dfe0b6d5990653707f5f Mon Sep 17 00:00:00 2001 From: Blake <104744707+r3v4s@users.noreply.github.com> Date: Sat, 23 Sep 2023 04:57:28 +0900 Subject: [PATCH 07/93] chore: use std.PrevRealm() in grc721 package (#992) update (missing part of) grc721 package implementation to use `std.PrevRealm()` not `std.GetOrigCaller()` ## related #667 implementation of `std.PrevRealm()` #895 update grc20, 721 to use `std.PrevRealm()`
Checklists... ## Contributors Checklist - [x] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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](../.benchmarks/README.md).
Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/grc/grc721/basic_nft.gno | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno index cbce05d379c..b707527c6a4 100644 --- a/examples/gno.land/p/demo/grc/grc721/basic_nft.gno +++ b/examples/gno.land/p/demo/grc/grc721/basic_nft.gno @@ -95,7 +95,7 @@ func (s *basicNFT) Approve(to std.Address, tid TokenID) error { return ErrApprovalToCurrentOwner } - caller := std.GetOrigCaller() + caller := std.PrevRealm().Addr() if caller != owner && !s.IsApprovedForAll(owner, caller) { return ErrCallerIsNotOwnerOrApproved } @@ -123,7 +123,7 @@ func (s *basicNFT) SetApprovalForAll(operator std.Address, approved bool) error return ErrInvalidAddress } - caller := std.GetOrigCaller() + caller := std.PrevRealm().Addr() return s.setApprovalForAll(caller, operator, approved) } @@ -131,7 +131,7 @@ func (s *basicNFT) SetApprovalForAll(operator std.Address, approved bool) error // contract recipients are aware of the GRC721 protocol to prevent // tokens from being forever locked. func (s *basicNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error { - caller := std.GetOrigCaller() + caller := std.PrevRealm().Addr() if !s.isApprovedOrOwner(caller, tid) { return ErrCallerIsNotOwnerOrApproved } @@ -150,7 +150,7 @@ func (s *basicNFT) SafeTransferFrom(from, to std.Address, tid TokenID) error { // Transfers `tokenId` token from `from` to `to`. func (s *basicNFT) TransferFrom(from, to std.Address, tid TokenID) error { - caller := std.GetOrigCaller() + caller := std.PrevRealm().Addr() if !s.isApprovedOrOwner(caller, tid) { return ErrCallerIsNotOwnerOrApproved } From fd133bb998806d8f6035df1fe3ce46db9b5bfe3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:22:02 +0200 Subject: [PATCH 08/93] chore(deps): Bump github.com/rs/cors from 1.10.0 to 1.10.1 (#1181) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.10.0 to 1.10.1.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/rs/cors&package-manager=go_modules&previous-version=1.10.0&new-version=1.10.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d7544ddcdf5..06329b2f02d 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/peterbourgon/ff/v3 v3.4.0 github.com/pmezard/go-difflib v1.0.0 github.com/rogpeppe/go-internal v1.11.0 - github.com/rs/cors v1.10.0 + github.com/rs/cors v1.10.1 github.com/stretchr/testify v1.8.4 github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c go.etcd.io/bbolt v1.3.7 diff --git a/go.sum b/go.sum index b038376393c..77c72cfab69 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8= -github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= From 28455d8ffc5b9861ce030a58b265e0671bde3205 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:42:45 +0200 Subject: [PATCH 09/93] chore: configure fossa license detector (#1183)
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).
--- .fossa.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .fossa.yml diff --git a/.fossa.yml b/.fossa.yml new file mode 100644 index 00000000000..cb80534e571 --- /dev/null +++ b/.fossa.yml @@ -0,0 +1,5 @@ +version: 3 + +paths: + exclude: + - ./misc/ From 39ab2dab729c97595433e65536c059f03d614f09 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:20:25 +0200 Subject: [PATCH 10/93] fix(codecov): increase codecov upload attempt (#1186) multiple workflows were failing randomly with `Unable to locate build via Github Actions API` error like: - https://github.com/gnolang/gno/actions/runs/6349517658/job/17247838334?pr=1179 - https://github.com/gnolang/gno/actions/runs/6378366330/job/17308780300?pr=1117 - ... based on this [issue#3954](https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954) from `community.codecov.com` adding `codecov upload token` should resolve most issues. However, in some rare instances where API limits are still reached, a re-upload attempt should be made. This PR introduces a retry action to allow codecov to reattempt the upload if it fails the first time. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- .github/workflows/gnoland.yml | 20 ++++++++++++++------ .github/workflows/gnovm.yml | 22 +++++++++++++++------- .github/workflows/tm2.yml | 22 +++++++++++++++------- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 9fdfa3011cc..09896aaee7b 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -74,14 +74,22 @@ jobs: export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} + + # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. + # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - uses: codecov/codecov-action@v3 + name: Upload coverage to Codecov.io + uses: Wandalen/wretry.action@v1.3.0 with: - token: ${{ secrets.CODECOV_TOKEN }} - name: gno.land - flags: gno.land-${{matrix.args}} - files: ./gno.land/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + attempt_limit: 3 + attempt_delay: 30000 + action: codecov/codecov-action@v3 + with: | + token: ${{ secrets.CODECOV_TOKEN }} + name: gno.land + flags: gno.land-${{matrix.args}} + files: ./gno.land/coverage.out + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: strategy: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 034aa109767..5ac11805fae 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -78,12 +78,20 @@ jobs: export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} + + # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. + # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - uses: codecov/codecov-action@v3 + name: Upload coverage to Codecov.io + uses: Wandalen/wretry.action@v1.3.0 with: - token: ${{ secrets.CODECOV_TOKEN }} - name: gnovm - verbose: true - flags: gnovm-${{matrix.args}} - files: ./gnovm/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + attempt_limit: 3 + attempt_delay: 30000 + action: codecov/codecov-action@v3 + with: | + token: ${{ secrets.CODECOV_TOKEN }} + name: gnovm + verbose: true + flags: gnovm-${{matrix.args}} + files: ./gnovm/coverage.out + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index fc744ccc2ef..2c116d80f87 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -68,12 +68,20 @@ jobs: export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} touch coverage.out + + # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. + # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - uses: codecov/codecov-action@v3 + name: Upload coverage to Codecov.io + uses: Wandalen/wretry.action@v1.3.0 with: - token: ${{ secrets.CODECOV_TOKEN }} - name: tm2 - verbose: true - flags: tm2-${{matrix.args}} - files: ./tm2/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + attempt_limit: 3 + attempt_delay: 30000 + action: codecov/codecov-action@v3 + with: | + token: ${{ secrets.CODECOV_TOKEN }} + name: tm2 + verbose: true + flags: tm2-${{matrix.args}} + files: ./tm2/coverage.out + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} From c315db647cc4b236fc4485aa6c4fb69b7240c31f Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 3 Oct 2023 19:07:29 +0200 Subject: [PATCH 11/93] fix(codecov): merge coverage for a single upload by workflow (#1190) It appears that the retry system introduced in #1186 was not sufficient, as we still hit API limits when running multiple checks concurrently. This PR merges all coverage files into a single upload at the of each testing workflows. As a result, we now have 3 uploads instead of 16, which should drastically reduce the number of Codecov upload failures. Note: It still appears to fail randomly, but we might need to wait some time until our API rate decreases. I believe it's still preferable to have 3 uploads rather than 16, which seemed to overwhelm Codecov. --------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> --- .github/workflows/gnoland.yml | 32 ++++++++++++++++++-------------- .github/workflows/gnovm.yml | 34 +++++++++++++++++++--------------- .github/workflows/tm2.yml | 34 +++++++++++++++++++--------------- 3 files changed, 56 insertions(+), 44 deletions(-) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 09896aaee7b..e2b099c2b92 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -74,22 +74,26 @@ jobs: export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} + - uses: actions/upload-artifact@v3 + if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} + with: + name: ${{runner.os}}-coverage-gnoland-${{ matrix.args}}-${{matrix.goversion}} + path: ./gno.land/coverage.out - # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. - # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.3.0 + upload-coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Download all previous coverage artifacts + uses: actions/download-artifact@v3 + with: + path: ${{ runner.temp }}/coverage + - name: Upload combined coverage to Codecov + uses: codecov/codecov-action@v3 with: - attempt_limit: 3 - attempt_delay: 30000 - action: codecov/codecov-action@v3 - with: | - token: ${{ secrets.CODECOV_TOKEN }} - name: gno.land - flags: gno.land-${{matrix.args}} - files: ./gno.land/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + directory: ${{ runner.temp }}/coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: strategy: diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 5ac11805fae..71b03b5ca05 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -78,20 +78,24 @@ jobs: export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} + - uses: actions/upload-artifact@v3 + if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} + with: + name: ${{runner.os}}-coverage-gnovm-${{ matrix.args}}-${{matrix.goversion}} + path: ./gnovm/coverage.out - # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. - # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.3.0 + upload-coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Download all previous coverage artifacts + uses: actions/download-artifact@v3 with: - attempt_limit: 3 - attempt_delay: 30000 - action: codecov/codecov-action@v3 - with: | - token: ${{ secrets.CODECOV_TOKEN }} - name: gnovm - verbose: true - flags: gnovm-${{matrix.args}} - files: ./gnovm/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + path: ${{ runner.temp }}/coverage + - name: Upload combined coverage to Codecov + uses: codecov/codecov-action@v3 + with: + directory: ${{ runner.temp }}/coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 2c116d80f87..7b78ccb1e0f 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -68,20 +68,24 @@ jobs: export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} touch coverage.out + - uses: actions/upload-artifact@v3 + if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} + with: + name: ${{runner.os}}-coverage-tm2-${{ matrix.args}}-${{matrix.goversion}} + path: ./tm2/coverage.out - # NOTE: Using retry action to manage occasional upload failures to codecov.io, due to API limits. - # Refer to issue#3954: https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 - - if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.3.0 + upload-coverage: + needs: test + runs-on: ubuntu-latest + steps: + - name: Download all previous coverage artifacts + uses: actions/download-artifact@v3 with: - attempt_limit: 3 - attempt_delay: 30000 - action: codecov/codecov-action@v3 - with: | - token: ${{ secrets.CODECOV_TOKEN }} - name: tm2 - verbose: true - flags: tm2-${{matrix.args}} - files: ./tm2/coverage.out - fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + path: ${{ runner.temp }}/coverage + - name: Upload combined coverage to Codecov + uses: codecov/codecov-action@v3 + with: + directory: ${{ runner.temp }}/coverage + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} + From f942d04af59465e6f928fc4db0dbe10ccc4451fd Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:44:10 +0200 Subject: [PATCH 12/93] chore: update fossa config (#1184) Signed-off-by: moul <94029+moul@users.noreply.github.com>
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).
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> --- .fossa.yml | 5 ---- .github/.fossa.yml | 15 +++++++++++ .github/workflows/fossa.yml | 50 +++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) delete mode 100644 .fossa.yml create mode 100644 .github/.fossa.yml create mode 100644 .github/workflows/fossa.yml diff --git a/.fossa.yml b/.fossa.yml deleted file mode 100644 index cb80534e571..00000000000 --- a/.fossa.yml +++ /dev/null @@ -1,5 +0,0 @@ -version: 3 - -paths: - exclude: - - ./misc/ diff --git a/.github/.fossa.yml b/.github/.fossa.yml new file mode 100644 index 00000000000..d639b393e98 --- /dev/null +++ b/.github/.fossa.yml @@ -0,0 +1,15 @@ +version: 3 + +# https://github.com/fossas/fossa-cli/blob/master/docs/references/files/fossa-yml.md + +project: + id: github.com/gnolang/gno + name: gno + +targets: + only: + - type: gomod + +paths: + exclude: + - ./misc/ diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml new file mode 100644 index 00000000000..17d8bb07930 --- /dev/null +++ b/.github/workflows/fossa.yml @@ -0,0 +1,50 @@ +name: Dependency License Scanning + +on: + workflow_dispatch: + pull_request: + paths: + - ".github/.fossa.yml" + - ".github/workflows/fossa.yml" + schedule: + - cron: '0 0 * * 6' # At 00:00 on saturdays + +permissions: + contents: read + +jobs: + fossa: + name: Fossa + runs-on: ubuntu-latest + if: github.repository == 'gnolang/gno' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # we don't know what commit the last tag was it's safer to get entire repo so previousStableVersion resolves + fetch-depth: 0 + + - name: Move .fossa.yml to root dir + run: mv .github/.fossa.yml . + + - name: Cache Coursier cache + uses: coursier/cache-action@v6.4.0 + + - name: Set up JDK 17 + uses: coursier/setup-action@v1.3.0 + with: + jvm: temurin:1.17 + + - name: Set up fossa CLI + run: curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install-latest.sh | bash + + - name: FOSSA analyze + run: fossa analyze + env: + FOSSA_API_KEY: "${{secrets.FOSSA_API_KEY}}" + + - name: FOSSA test + run: fossa test + env: + FOSSA_API_KEY: "${{secrets.FOSSA_API_KEY}}" + From ce258b16cf27d10ac8e62abde2675c3b4ce76b6f Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 3 Oct 2023 21:07:52 +0200 Subject: [PATCH 13/93] chore: fix fossa action (#1192) --- .github/workflows/fossa.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 17d8bb07930..3036720f6eb 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -36,7 +36,7 @@ jobs: jvm: temurin:1.17 - name: Set up fossa CLI - run: curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install-latest.sh | bash + run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install-latest.sh | bash" - name: FOSSA analyze run: fossa analyze From 924d62d141fc0f936b2bb69ff55db76fb73ee051 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:23:57 +0200 Subject: [PATCH 14/93] feat: add txtar driver for gnoland integration test (#1117) Resolves #1101. This PR establishes a base for creating `txtar` tests using a partial in-memory `gnoland` node. It supports: - `gnoland [start|stop]`: The node doesn't start automatically, which enables users to perform pre-configuration or pass custom arguments to the start command. - `gnokey`: Most of the commands should work. The `--remote`, `--insecure-password-stdin`, and `--home` flags are automatically set with the appropriate values to communicate with the node. - `sleep`: A simple helper to introduce a delay between actions, ensuring specific tasks complete before proceeding. Currently, only the "test1" user is automatically created in the default keybase directory (without a password). Default `gnoland` genesis balance and genesis transaction files are also load on start. You can find some examples of `txtar` in this directory: https://github.com/gfanton/gno/tree/feat/gnoland-txtar-driver/gno.land/cmd/gnoland/testdata `gnoland` logs aren't forwarded to stdout to avoid overwhelming informations in the tests. Instead, you can specify a log directory using the `LOG_DIR` environment variable or set `TESTWORK=true` to enable persistence in the `txtar` working directory. In either case, the path to the log file will be printed at the start if one of these environment variables is set. This also enables storing logs as artifacts on the CI for later examination. There is still a lot to do, but I believe this provides a good base for future iterations.
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).
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/workflows/gnoland.yml | 7 + Dockerfile | 3 +- docs/testing_guide.md | 86 +++++ gno.land/Makefile | 11 +- gno.land/cmd/gnokey/main.go | 3 +- gno.land/cmd/gnoland/integration_test.go | 12 + gno.land/cmd/gnoland/testdata/addpkg.txtar | 26 ++ gno.land/pkg/gnoland/app.go | 98 ++++- gno.land/pkg/integration/doc.go | 87 +++++ gno.land/pkg/integration/gnoland.go | 336 ++++++++++++++++++ gno.land/pkg/integration/integration_test.go | 11 + .../pkg/integration/testdata/gnokey.txtar | 32 ++ .../pkg/integration/testdata/gnoland.txtar | 43 +++ .../pkg/integration/testing_integration.go | 282 +++++++++++++++ gno.land/pkg/sdk/vm/keeper.go | 5 +- tm2/pkg/crypto/keys/client/add.go | 4 +- tm2/pkg/crypto/keys/client/addpkg.go | 4 +- tm2/pkg/crypto/keys/client/broadcast.go | 2 +- tm2/pkg/crypto/keys/client/call.go | 4 +- tm2/pkg/crypto/keys/client/delete.go | 4 +- tm2/pkg/crypto/keys/client/export.go | 4 +- tm2/pkg/crypto/keys/client/generate.go | 4 +- tm2/pkg/crypto/keys/client/import.go | 4 +- tm2/pkg/crypto/keys/client/list.go | 4 +- tm2/pkg/crypto/keys/client/maketx.go | 8 +- tm2/pkg/crypto/keys/client/query.go | 12 +- tm2/pkg/crypto/keys/client/root.go | 24 +- tm2/pkg/crypto/keys/client/send.go | 4 +- tm2/pkg/crypto/keys/client/sign.go | 4 +- tm2/pkg/crypto/keys/client/verify.go | 4 +- 30 files changed, 1065 insertions(+), 67 deletions(-) create mode 100644 docs/testing_guide.md create mode 100644 gno.land/cmd/gnoland/integration_test.go create mode 100644 gno.land/cmd/gnoland/testdata/addpkg.txtar create mode 100644 gno.land/pkg/integration/doc.go create mode 100644 gno.land/pkg/integration/gnoland.go create mode 100644 gno.land/pkg/integration/integration_test.go create mode 100644 gno.land/pkg/integration/testdata/gnokey.txtar create mode 100644 gno.land/pkg/integration/testdata/gnoland.txtar create mode 100644 gno.land/pkg/integration/testing_integration.go diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index e2b099c2b92..2b38f254a13 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -73,7 +73,14 @@ jobs: run: | export GOPATH=$HOME/go export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" + export LOG_DIR="${{ runner.temp }}/logs/test-${{ matrix.goversion }}-gnoland" make ${{ matrix.args }} + - name: Upload Test Log + if: always() + uses: actions/upload-artifact@v3 + with: + name: logs-test-gnoland-go${{ matrix.goversion }} + path: ${{ runner.temp }}/logs/**/*.log - uses: actions/upload-artifact@v3 if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} with: diff --git a/Dockerfile b/Dockerfile index 70e2d01bf04..9e7fc48dcb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,8 @@ RUN rm -rf /opt/gno/src/.git # runtime-base + runtime-tls FROM debian:stable-slim AS runtime-base -ENV PATH="${PATH}:/opt/gno/bin" +ENV PATH="${PATH}:/opt/gno/bin" \ + GNOROOT="/opt/gno/src" WORKDIR /opt/gno/src FROM runtime-base AS runtime-tls RUN apt-get update && apt-get install -y expect ca-certificates && update-ca-certificates diff --git a/docs/testing_guide.md b/docs/testing_guide.md new file mode 100644 index 00000000000..6019f238f01 --- /dev/null +++ b/docs/testing_guide.md @@ -0,0 +1,86 @@ +# Gnoland Testing Guide + +This guide provides an overview of our testing practices and conventions. While most of our testing aligns with typical Go practices, there are exceptions and specifics you should be aware of. + +## Standard Package Testing + +For most packages, tests are written and executed in the standard Go manner: + +- Tests are located alongside the code they test. +- The `go test` command can be used to execute tests. + +However, as mentioned earlier, there are some exceptions. In the following sections, we will explore our specialized tests and how to work with them. + +## Gno Filetests + +**Location:** `gnovm/test/files` + +These are our custom file-based tests tailored specifically for this project. + +**Execution:** + +From the gnovm directory, There are two main commands to run Gno filetests: + +1. To test native files, use: +``` +make _test.gnolang.native +``` + +2. To test standard libraries, use: +``` +make _test.gnolang.stdlibs +``` + +**Golden Files Update:** + +Golden files are references for expected outputs. Sometimes, after certain updates, these need to be synchronized. To do so: + +1. For native tests: +``` +make _test.gnolang.native.sync +``` + +2. For standard library tests: +``` +make _test.gnolang.stdlibs.sync +``` + +## Integration Tests + +**Location:** `gno.land/**/testdata` + +From the gno.land directory, Integration tests are designed to ensure different parts of the project work cohesively. Specifically: + +1. **InMemory Node Integration Testing:** + Found in `gno.land/cmd/gnoland/testdata`, these are dedicated to running integration tests against a genuine `gnoland` node. + +2. **Integration Features Testing:** + Located in `gno.land/pkg/integration/testdata`, these tests target integrations specific commands. + +These integration tests utilize the `testscript` package and follow the `txtar` file specifications. + +**Documentation:** + +- For general `testscript` package documentation, refer to: [testscript documentation](https://github.com/rogpeppe/go-internal/blob/v1.11.0/testscript/doc.go) + +- For more specific details about our integration tests, consult our extended documentation: [gnoland integration documentation](https://github.com/gnolang/gno/blob/master/gno.land/pkg/integration/doc.go) + +**Execution:** + +To run the integration tests (alongside other packages): + +``` +make _test.pkgs +``` + +**Golden Files Update within txtar:** + +For tests utilizing the `cmp` command inside `txtar` files, golden files can be synchronized using: + +``` +make _test.pkgs.sync +``` + +--- + +As the project evolves, this guide might be updated. diff --git a/gno.land/Makefile b/gno.land/Makefile index 117de18fc46..e794bb58174 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -47,7 +47,10 @@ test: _test.gnoland _test.gnoweb _test.gnokey _test.pkgs GOTEST_FLAGS ?= -v -p 1 -timeout=30m -_test.gnoland:; go test $(GOTEST_FLAGS) ./cmd/gnoland -_test.gnoweb:; go test $(GOTEST_FLAGS) ./cmd/gnoweb -_test.gnokey:; go test $(GOTEST_FLAGS) ./cmd/gnokey -_test.pkgs:; go test $(GOTEST_FLAGS) ./pkg/... +_test.gnoland:; go test $(GOTEST_FLAGS) ./cmd/gnoland +_test.gnoweb:; go test $(GOTEST_FLAGS) ./cmd/gnoweb +_test.gnokey:; go test $(GOTEST_FLAGS) ./cmd/gnokey +_test.pkgs:; go test $(GOTEST_FLAGS) ./pkg/... +_test.pkgs.sync:; UPDATE_SCRIPTS=true go test $(GOTEST_FLAGS) ./pkg/... + + diff --git a/gno.land/cmd/gnokey/main.go b/gno.land/cmd/gnokey/main.go index 1f3414dc893..28cb665eac1 100644 --- a/gno.land/cmd/gnokey/main.go +++ b/gno.land/cmd/gnokey/main.go @@ -5,11 +5,12 @@ import ( "fmt" "os" + "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" ) func main() { - cmd := client.NewRootCmd() + cmd := client.NewRootCmd(commands.NewDefaultIO()) if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go new file mode 100644 index 00000000000..78c8e94fa09 --- /dev/null +++ b/gno.land/cmd/gnoland/integration_test.go @@ -0,0 +1,12 @@ +package main + +import ( + "testing" + + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/rogpeppe/go-internal/testscript" +) + +func TestTestdata(t *testing.T) { + testscript.Run(t, integration.SetupGnolandTestScript(t, "testdata")) +} diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar new file mode 100644 index 00000000000..5e871b058ac --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -0,0 +1,26 @@ +# test for add package + +## start a new node +gnoland start + +## add bar.gno package located in $WORK directory as gno.land/r/foobar/bar +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 + +## execute Render +gnokey maketx call -pkgpath gno.land/r/foobar/bar -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 + +## compare render +cmp stdout stdout.golden + +-- bar.gno -- +package bar + +func Render(path string) string { + return "hello from foo" +} + +-- stdout.golden -- +("hello from foo" string) +OK! +GAS WANTED: 2000000 +GAS USED: 69163 \ No newline at end of file diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index b10f251b115..3585f99d7de 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -2,6 +2,8 @@ package gnoland import ( "fmt" + "os" + "os/exec" "path/filepath" "strings" @@ -20,12 +22,40 @@ import ( "github.com/gnolang/gno/tm2/pkg/store/iavl" ) +type AppOptions struct { + DB dbm.DB + // `gnoRootDir` should point to the local location of the gno repository. + // It serves as the gno equivalent of GOROOT. + GnoRootDir string + SkipFailingGenesisTxs bool + Logger log.Logger + MaxCycles int64 +} + +func NewAppOptions() *AppOptions { + return &AppOptions{ + Logger: log.NewNopLogger(), + DB: dbm.NewMemDB(), + GnoRootDir: GuessGnoRootDir(), + } +} + +func (c *AppOptions) validate() error { + if c.Logger == nil { + return fmt.Errorf("no logger provided") + } + + if c.DB == nil { + return fmt.Errorf("no db provided") + } + + return nil +} + // NewApp creates the GnoLand application. -func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCycles int64) (abci.Application, error) { - // Get main DB. - db, err := dbm.NewDB("gnolang", dbm.GoLevelDBBackend, filepath.Join(rootDir, "data")) - if err != nil { - return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, rootDir, err) +func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { + if err := cfg.validate(); err != nil { + return nil, err } // Capabilities keys. @@ -33,21 +63,21 @@ func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCy baseKey := store.NewStoreKey("base") // Create BaseApp. - baseApp := sdk.NewBaseApp("gnoland", logger, db, baseKey, mainKey) + baseApp := sdk.NewBaseApp("gnoland", cfg.Logger, cfg.DB, baseKey, mainKey) baseApp.SetAppVersion("dev") // Set mounts for BaseApp's MultiStore. - baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, db) - baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, db) + baseApp.MountStoreWithDB(mainKey, iavl.StoreConstructor, cfg.DB) + baseApp.MountStoreWithDB(baseKey, dbadapter.StoreConstructor, cfg.DB) // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) - stdlibsDir := filepath.Join("..", "gnovm", "stdlibs") - vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, maxCycles) + stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") + vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) // Set InitChainer - baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, skipFailingGenesisTxs)) + baseApp.SetInitChainer(InitChainer(baseApp, acctKpr, bankKpr, cfg.SkipFailingGenesisTxs)) // Set AnteHandler authOptions := auth.AnteOptions{ @@ -88,6 +118,23 @@ func NewApp(rootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCy return baseApp, nil } +// NewApp creates the GnoLand application. +func NewApp(dataRootDir string, skipFailingGenesisTxs bool, logger log.Logger, maxCycles int64) (abci.Application, error) { + var err error + + cfg := NewAppOptions() + + // Get main DB. + cfg.DB, err = dbm.NewDB("gnolang", dbm.GoLevelDBBackend, filepath.Join(dataRootDir, "data")) + if err != nil { + return nil, fmt.Errorf("error initializing database %q using path %q: %w", dbm.GoLevelDBBackend, dataRootDir, err) + } + + cfg.Logger = logger + + return NewAppWithOptions(cfg) +} + // InitChainer returns a function that can initialize the chain with genesis. func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank.BankKeeperI, skipFailingGenesisTxs bool) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { @@ -107,14 +154,15 @@ func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank for i, tx := range genState.Txs { res := baseApp.Deliver(tx) if res.IsErr() { - fmt.Println("ERROR LOG:", res.Log) - fmt.Println("#", i, string(amino.MustMarshalJSON(tx))) + ctx.Logger().Error("LOG", res.Log) + ctx.Logger().Error("#", i, string(amino.MustMarshalJSON(tx))) + // NOTE: comment out to ignore. if !skipFailingGenesisTxs { panic(res.Error) } } else { - fmt.Println("SUCCESS:", string(amino.MustMarshalJSON(tx))) + ctx.Logger().Info("SUCCESS:", string(amino.MustMarshalJSON(tx))) } } // Done! @@ -146,3 +194,25 @@ func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock return abci.ResponseEndBlock{} } } + +func GuessGnoRootDir() string { + var rootdir string + + // First try to get the root directory from the GNOROOT environment variable. + if rootdir = os.Getenv("GNOROOT"); rootdir != "" { + return filepath.Clean(rootdir) + } + + if gobin, err := exec.LookPath("go"); err == nil { + // If GNOROOT is not set, try to guess the root directory using the `go list` command. + cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") + out, err := cmd.CombinedOutput() + if err != nil { + panic(fmt.Errorf("invalid gno directory %q: %w", rootdir, err)) + } + + return strings.TrimSpace(string(out)) + } + + panic("no go binary available, unable to determine gno root-dir path") +} diff --git a/gno.land/pkg/integration/doc.go b/gno.land/pkg/integration/doc.go new file mode 100644 index 00000000000..a8b40f9c321 --- /dev/null +++ b/gno.land/pkg/integration/doc.go @@ -0,0 +1,87 @@ +// Package integration offers utilities to run txtar-based tests against the gnoland system +// by extending the functionalities provided by the standard testscript package. This package is +// currently in an experimental phase and may undergo significant changes in the future. +// +// SetupGnolandTestScript, sets up the environment for running txtar tests, introducing additional +// commands like "gnoland" and "gnokey" into the test script ecosystem. Specifically, it allows the +// user to initiate an in-memory gnoland node and interact with it via the `gnokey` command. +// +// Additional Command Overview: +// +// 1. `gnoland [start|stop]`: +// - The gnoland node doesn't start automatically. This enables the user to do some +// pre-configuration or pass custom arguments to the start command. +// +// 2. `gnokey`: +// - Supports most of the common commands. +// - `--remote`, `--insecure-password-stdin`, and `--home` flags are set automatically to +// communicate with the gnoland node. +// +// Logging: +// +// Gnoland logs aren't forwarded to stdout to avoid overwhelming the tests with too much +// information. Instead, a log directory can be specified with `LOG_DIR`, or you +// can set `TESTWORK=true` +// to persist logs in the txtar working directory. In any case, the log file should be printed +// on start if one of these environment variables is set. +// +// Accounts: +// +// By default, only the test1 user will be created in the default keybase directory, +// with no password set. The default gnoland genesis balance file and the genesis +// transaction file are also registered by default. +// +// Examples: +// +// Examples can be found in the `testdata` directory of this package. +// +// Environment Variables: +// +// Input: +// +// - LOG_LEVEL: +// The logging level to be used, which can be one of "error", "debug", "info", or an empty string. +// If empty, the log level defaults to "debug". +// +// - LOG_DIR: +// If set, logs will be directed to the specified directory. +// +// - TESTWORK: +// A boolean that, when enabled, retains working directories after tests for +// inspection. If enabled, gnoland logs will be persisted inside this +// folder. +// +// - UPDATE_SCRIPTS: +// A boolean that, when enabled, updates the test scripts if a `cmp` command +// fails and its second argument refers to a file inside the testscript +// file. The content will be quoted with txtar.Quote if needed, requiring +// manual edits if it's not unquoted in the script. +// +// Output (available inside testscripts files): +// +// - WORK: +// The path to the temporary work directory tree created for each script. +// +// - GNOROOT: +// Points to the local location of the gno repository, serving as the GOROOT equivalent for gno. +// +// - GNOHOME: +// Refers to the local directory where gnokey stores its keys. +// +// - GNODATA: +// The path where the gnoland node stores its configuration and data. It's +// set only if the node has started. +// +// - USER_SEED_test1: +// Contains the seed for the test1 account. +// +// - USER_ADDR_test1: +// Contains the address for the test1 account. +// +// - RPC_ADDR: +// Points to the gnoland node's remote address. It's set only if the node has started. +// +// For a more comprehensive guide on original behaviors, additional commands and environment +// variables, refer to the original documentation of testscripts available here: +// https://github.com/rogpeppe/go-internal/blob/master/testscript/doc.go +package integration diff --git a/gno.land/pkg/integration/gnoland.go b/gno.land/pkg/integration/gnoland.go new file mode 100644 index 00000000000..c4fee341bfc --- /dev/null +++ b/gno.land/pkg/integration/gnoland.go @@ -0,0 +1,336 @@ +package integration + +import ( + "flag" + "fmt" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + 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/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/privval" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/log" + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/rogpeppe/go-internal/testscript" +) + +type IntegrationConfig struct { + SkipFailingGenesisTxs bool + SkipStart bool + GenesisBalancesFile string + GenesisTxsFile string + ChainID string + GenesisRemote string + RootDir string + GenesisMaxVMCycles int64 + Config string +} + +// NOTE: This is a copy of gnoland actual flags. +// XXX: A lot this make no sense for integration. +func (c *IntegrationConfig) RegisterFlags(fs *flag.FlagSet) { + fs.BoolVar( + &c.SkipFailingGenesisTxs, + "skip-failing-genesis-txs", + false, + "don't panic when replaying invalid genesis txs", + ) + fs.BoolVar( + &c.SkipStart, + "skip-start", + false, + "quit after initialization, don't start the node", + ) + + fs.StringVar( + &c.GenesisBalancesFile, + "genesis-balances-file", + "./genesis/genesis_balances.txt", + "initial distribution file", + ) + + fs.StringVar( + &c.GenesisTxsFile, + "genesis-txs-file", + "./genesis/genesis_txs.txt", + "initial txs to replay", + ) + + fs.StringVar( + &c.ChainID, + "chainid", + "dev", + "the ID of the chain", + ) + + fs.StringVar( + &c.RootDir, + "root-dir", + "testdir", + "directory for config and data", + ) + + fs.StringVar( + &c.GenesisRemote, + "genesis-remote", + "localhost:26657", + "replacement for '%%REMOTE%%' in genesis", + ) + + fs.Int64Var( + &c.GenesisMaxVMCycles, + "genesis-max-vm-cycles", + 10_000_000, + "set maximum allowed vm cycles per operation. Zero means no limit.", + ) +} + +func execTestingGnoland(t *testing.T, logger log.Logger, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) { + t.Helper() + + // Setup start config. + icfg := &IntegrationConfig{} + { + fs := flag.NewFlagSet("start", flag.ExitOnError) + icfg.RegisterFlags(fs) + + // Override default value for flags. + fs.VisitAll(func(f *flag.Flag) { + switch f.Name { + case "root-dir": + f.DefValue = gnoDataDir + case "chainid": + f.DefValue = "tendermint_test" + case "genesis-balances-file": + f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_balances.txt") + case "genesis-txs-file": + f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_txs.txt") + default: + return + } + + f.Value.Set(f.DefValue) + }) + + if err := fs.Parse(args); err != nil { + return nil, fmt.Errorf("unable to parse flags: %w", err) + } + } + + // Setup testing config. + cfg := config.TestConfig().SetRootDir(gnoDataDir) + { + cfg.EnsureDirs() + cfg.Consensus.CreateEmptyBlocks = true + cfg.Consensus.CreateEmptyBlocksInterval = time.Duration(0) + cfg.RPC.ListenAddress = "tcp://127.0.0.1:0" + cfg.P2P.ListenAddress = "tcp://127.0.0.1:0" + } + + // Prepare genesis. + if err := setupTestingGenesis(gnoDataDir, cfg, icfg, gnoRootDir); err != nil { + return nil, err + } + + // Create application and node. + return createAppAndNode(cfg, logger, gnoRootDir, icfg) +} + +func setupTestingGenesis(gnoDataDir string, cfg *config.Config, icfg *IntegrationConfig, gnoRootDir string) error { + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + + genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis) + genesisDirPath := filepath.Dir(genesisFilePath) + if err := osm.EnsureDir(genesisDirPath, 0o700); err != nil { + return fmt.Errorf("unable to ensure directory %q: %w", genesisDirPath, err) + } + + genesisTxs := loadGenesisTxs(icfg.GenesisTxsFile, icfg.ChainID, icfg.GenesisRemote) + pvPub := priv.GetPubKey() + + gen := &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: icfg.ChainID, + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + // TODO: update limits. + MaxTxBytes: 1000000, // 1MB, + MaxDataBytes: 2000000, // 2MB, + MaxGas: 10000000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: pvPub.Address(), + PubKey: pvPub, + Power: 10, + Name: "testvalidator", + }, + }, + } + + // Load distribution. + balances := loadGenesisBalances(icfg.GenesisBalancesFile) + + // Load initial packages from examples. + // XXX: We should be able to config this. + test1 := crypto.MustAddressFromString(test1Addr) + txs := []std.Tx{} + + // List initial packages to load from examples. + // println(filepath.Join(gnoRootDir, "examples")) + pkgs, err := gnomod.ListPkgs(filepath.Join(gnoRootDir, "examples")) + if err != nil { + return fmt.Errorf("listing gno packages: %w", err) + } + + // Sort packages by dependencies. + sortedPkgs, err := pkgs.Sort() + if err != nil { + return fmt.Errorf("sorting packages: %w", err) + } + + // Filter out draft packages. + nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() + + for _, pkg := range nonDraftPkgs { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + + var tx std.Tx + tx.Msgs = []std.Msg{ + vmm.MsgAddPackage{ + Creator: test1, + Package: memPkg, + Deposit: nil, + }, + } + + // XXX: Add fee flag ? + // Or maybe reduce fee to the minimum ? + tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + // Load genesis txs from file. + txs = append(txs, genesisTxs...) + + // Construct genesis AppState. + gen.AppState = gnoland.GnoGenesisState{ + Balances: balances, + Txs: txs, + } + + writeGenesisFile(gen, genesisFilePath) + + return nil +} + +func createAppAndNode(cfg *config.Config, logger log.Logger, gnoRootDir string, icfg *IntegrationConfig) (*node.Node, error) { + gnoApp, err := gnoland.NewAppWithOptions(&gnoland.AppOptions{ + Logger: logger, + GnoRootDir: gnoRootDir, + SkipFailingGenesisTxs: icfg.SkipFailingGenesisTxs, + MaxCycles: icfg.GenesisMaxVMCycles, + DB: db.NewMemDB(), + }) + if err != nil { + return nil, fmt.Errorf("error in creating new app: %w", err) + } + + cfg.LocalApp = gnoApp + node, err := node.DefaultNewNode(cfg, logger) + if err != nil { + return nil, fmt.Errorf("error in creating node: %w", err) + } + + return node, node.Start() +} + +func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { + if err != nil { + fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) + if !neg { + ts.Fatalf("unexpected %q command failure: %s", cmd, err) + } + } else { + if neg { + ts.Fatalf("unexpected %s command success", cmd) + } + } +} + +func loadGenesisTxs( + path string, + chainID string, + genesisRemote string, +) []std.Tx { + txs := []std.Tx{} + txsBz := osm.MustReadFile(path) + txsLines := strings.Split(string(txsBz), "\n") + for _, txLine := range txsLines { + if txLine == "" { + continue // Skip empty line. + } + + // Patch the TX. + txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) + txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) + + var tx std.Tx + amino.MustUnmarshalJSON([]byte(txLine), &tx) + txs = append(txs, tx) + } + + return txs +} + +func loadGenesisBalances(path string) []string { + // Each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot. + balances := []string{} + content := osm.MustReadFile(path) + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + + // Remove comments. + line = strings.Split(line, "#")[0] + line = strings.TrimSpace(line) + + // Skip empty lines. + if line == "" { + continue + } + + parts := strings.Split(line, "=") + if len(parts) != 2 { + panic("invalid genesis_balance line: " + line) + } + + balances = append(balances, line) + } + return balances +} + +func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { + err := gen.SaveAs(filePath) + if err != nil { + panic(err) + } +} diff --git a/gno.land/pkg/integration/integration_test.go b/gno.land/pkg/integration/integration_test.go new file mode 100644 index 00000000000..3c22a190d64 --- /dev/null +++ b/gno.land/pkg/integration/integration_test.go @@ -0,0 +1,11 @@ +package integration + +import ( + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +func TestTestdata(t *testing.T) { + testscript.Run(t, SetupGnolandTestScript(t, "testdata")) +} diff --git a/gno.land/pkg/integration/testdata/gnokey.txtar b/gno.land/pkg/integration/testdata/gnokey.txtar new file mode 100644 index 00000000000..e4d2c93c0c9 --- /dev/null +++ b/gno.land/pkg/integration/testdata/gnokey.txtar @@ -0,0 +1,32 @@ +# test basic gnokey integrations commands +# golden files have been generated using UPDATE_SCRIPTS=true + +# start gnoland +gnoland start + +## test1 account should be available on default +gnokey query auth/accounts/${USER_ADDR_test1} +cmp stdout gnokey-query-valid.stdout.golden +cmp stderr gnokey-query-valid.stderr.golden + +## invalid gnokey command should raise an error +! gnokey query foo/bar +cmp stdout gnokey-query-invalid.stdout.golden +cmp stderr gnokey-query-invalid.stderr.golden + +-- gnokey-query-valid.stdout.golden -- +height: 0 +data: { + "BaseAccount": { + "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", + "coins": "9999892000000ugnot", + "public_key": null, + "account_number": "0", + "sequence": "0" + } +} +-- gnokey-query-valid.stderr.golden -- +-- gnokey-query-invalid.stdout.golden -- +Log: +-- gnokey-query-invalid.stderr.golden -- +"gnokey" error: unknown request error diff --git a/gno.land/pkg/integration/testdata/gnoland.txtar b/gno.land/pkg/integration/testdata/gnoland.txtar new file mode 100644 index 00000000000..c675e7578b6 --- /dev/null +++ b/gno.land/pkg/integration/testdata/gnoland.txtar @@ -0,0 +1,43 @@ +# test basic gnoland integrations commands +# golden files have been generated using UPDATE_SCRIPTS=true + +## no arguments should fail +! gnoland +cmp stdout gnoland-no-arguments.stdout.golden +cmp stderr gnoland-no-arguments.stderr.golden + +## should be able to start +gnoland start +cmp stdout gnoland-start.stdout.golden +cmp stderr gnoland-start.stderr.golden + +## should not be able to start a node twice +! gnoland start +cmp stdout gnoland-already-start.stdout.golden +cmp stderr gnoland-already-start.stderr.golden + +## should be able to stop default +gnoland stop +cmp stdout gnoland-stop.stdout.golden +cmp stderr gnoland-stop.stderr.golden + +## should not be able to stop a node twice +! gnoland stop +cmp stdout gnoland-already-stop.stdout.golden +cmp stderr gnoland-already-stop.stderr.golden + +-- gnoland-no-arguments.stdout.golden -- +-- gnoland-no-arguments.stderr.golden -- +"gnoland" error: syntax: gnoland [start|stop] +-- gnoland-start.stdout.golden -- +node started successfully +-- gnoland-start.stderr.golden -- +-- gnoland-already-start.stdout.golden -- +-- gnoland-already-start.stderr.golden -- +"gnoland start" error: node already started +-- gnoland-stop.stdout.golden -- +node stopped successfully +-- gnoland-stop.stderr.golden -- +-- gnoland-already-stop.stdout.golden -- +-- gnoland-already-stop.stderr.golden -- +"gnoland stop" error: node not started cannot be stopped diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go new file mode 100644 index 00000000000..f0a696ddd85 --- /dev/null +++ b/gno.land/pkg/integration/testing_integration.go @@ -0,0 +1,282 @@ +package integration + +import ( + "context" + "fmt" + "hash/crc32" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/rogpeppe/go-internal/testscript" +) + +// XXX: This should be centralize somewhere. +const ( + test1Addr = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + test1Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +) + +type testNode struct { + *node.Node + logger log.Logger + nGnoKeyExec uint // Counter for execution of gnokey. +} + +// SetupGnolandTestScript prepares the test environment to execute txtar tests +// using a partial InMemory gnoland node. It initializes key storage, sets up the gnoland node, +// and provides custom commands like "gnoland" and "gnokey" for txtar script execution. +// +// The function returns testscript.Params which contain the test setup and command +// executions to be used with the testscript package. +// +// For a detailed explanation of the commands and their behaviors, as well as +// example txtar scripts, refer to the package documentation in doc.go. +func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { + t.Helper() + + tmpdir := t.TempDir() + + // `gnoRootDir` should point to the local location of the gno repository. + // It serves as the gno equivalent of GOROOT. + gnoRootDir := gnoland.GuessGnoRootDir() + + // `gnoHomeDir` should be the local directory where gnokey stores keys. + gnoHomeDir := filepath.Join(tmpdir, "gno") + + // `gnoDataDir` should refer to the local location where the gnoland node + // stores its configuration and data. + gnoDataDir := filepath.Join(tmpdir, "data") + + // Testscripts run concurrently by default, so we need to be prepared for that. + var muNodes sync.Mutex + nodes := map[string]*testNode{} + + updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) + persistWorkDir, _ := strconv.ParseBool(os.Getenv("TESTWORK")) + return testscript.Params{ + UpdateScripts: updateScripts, + TestWork: persistWorkDir, + Dir: txtarDir, + Setup: func(env *testscript.Env) error { + kb, err := keys.NewKeyBaseFromDir(gnoHomeDir) + if err != nil { + return err + } + + // XXX: Add a command to add custom account. + kb.CreateAccount("test1", test1Seed, "", "", 0, 0) + env.Setenv("USER_SEED_test1", test1Seed) + env.Setenv("USER_ADDR_test1", test1Addr) + + env.Setenv("GNOROOT", gnoRootDir) + env.Setenv("GNOHOME", gnoHomeDir) + + return nil + }, + Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "gnoland": func(ts *testscript.TestScript, neg bool, args []string) { + muNodes.Lock() + defer muNodes.Unlock() + + if len(args) == 0 { + tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop]")) + return + } + + sid := getSessionID(ts) + + var cmd string + cmd, args = args[0], args[1:] + + var err error + switch cmd { + case "start": + if _, ok := nodes[sid]; ok { + err = fmt.Errorf("node already started") + break + } + + logger := log.NewNopLogger() + if persistWorkDir || os.Getenv("LOG_DIR") != "" { + logname := fmt.Sprintf("gnoland-%s.log", sid) + logger = getTestingLogger(ts, logname) + } + + dataDir := filepath.Join(gnoDataDir, sid) + var node *node.Node + if node, err = execTestingGnoland(t, logger, dataDir, gnoRootDir, args); err == nil { + nodes[sid] = &testNode{ + Node: node, + logger: logger, + } + ts.Defer(func() { + muNodes.Lock() + defer muNodes.Unlock() + + if n := nodes[sid]; n != nil { + if err := n.Stop(); err != nil { + panic(fmt.Errorf("node %q was unable to stop: %w", sid, err)) + } + } + }) + + // Get listen address environment. + // It should have been updated with the right port on start. + laddr := node.Config().RPC.ListenAddress + + // Add default environements. + ts.Setenv("RPC_ADDR", laddr) + ts.Setenv("GNODATA", gnoDataDir) + + const listenerID = "testing_listener" + + // Wait for first block by waiting for `EventNewBlock` event. + nb := make(chan struct{}, 1) + node.EventSwitch().AddListener(listenerID, func(ev events.Event) { + if _, ok := ev.(types.EventNewBlock); ok { + select { + case nb <- struct{}{}: + default: + } + } + }) + + if node.BlockStore().Height() == 0 { + select { + case <-nb: // ok + case <-time.After(time.Second * 6): + ts.Fatalf("timeout while waiting for the node to start") + } + } + + node.EventSwitch().RemoveListener(listenerID) + + fmt.Fprintln(ts.Stdout(), "node started successfully") + } + case "stop": + n, ok := nodes[sid] + if !ok { + err = fmt.Errorf("node not started cannot be stopped") + break + } + + if err = n.Stop(); err == nil { + delete(nodes, sid) + + // Unset gnoland environements. + ts.Setenv("RPC_ADDR", "") + ts.Setenv("GNODATA", "") + fmt.Fprintln(ts.Stdout(), "node stopped successfully") + } + default: + err = fmt.Errorf("invalid gnoland subcommand: %q", cmd) + } + + tsValidateError(ts, "gnoland "+cmd, neg, err) + }, + "gnokey": func(ts *testscript.TestScript, neg bool, args []string) { + muNodes.Lock() + defer muNodes.Unlock() + + sid := getSessionID(ts) + + // Setup IO command. + io := commands.NewTestIO() + io.SetOut(commands.WriteNopCloser(ts.Stdout())) + io.SetErr(commands.WriteNopCloser(ts.Stderr())) + cmd := client.NewRootCmd(io) + + io.SetIn(strings.NewReader("\n")) // Inject empty password to stdin. + defaultArgs := []string{ + "-home", gnoHomeDir, + "-insecure-password-stdin=true", // There no use to not have this param by default. + } + + if n, ok := nodes[sid]; ok { + if raddr := n.Config().RPC.ListenAddress; raddr != "" { + defaultArgs = append(defaultArgs, "-remote", raddr) + } + + n.nGnoKeyExec++ + headerlog := fmt.Sprintf("%.02d!EXEC_GNOKEY", n.nGnoKeyExec) + // Log the command inside gnoland logger, so we can better scope errors. + n.logger.Info(headerlog, strings.Join(args, " ")) + defer n.logger.Info(headerlog, "END") + } + + // Inject default argument, if duplicate + // arguments, it should be override by the ones + // user provided. + args = append(defaultArgs, args...) + + err := cmd.ParseAndRun(context.Background(), args) + + tsValidateError(ts, "gnokey", neg, err) + }, + }, + } +} + +func getSessionID(ts *testscript.TestScript) string { + works := ts.Getenv("WORK") + sum := crc32.ChecksumIEEE([]byte(works)) + return strconv.FormatUint(uint64(sum), 16) +} + +func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { + var path string + if logdir := os.Getenv("LOG_DIR"); logdir != "" { + if err := os.MkdirAll(logdir, 0o755); err != nil { + ts.Fatalf("unable to make log directory %q", logdir) + } + + var err error + if path, err = filepath.Abs(filepath.Join(logdir, logname)); err != nil { + ts.Fatalf("uanble to get absolute path of logdir %q", logdir) + } + } else if workdir := ts.Getenv("WORK"); workdir != "" { + path = filepath.Join(workdir, logname) + } else { + return log.NewNopLogger() + } + + f, err := os.Create(path) + if err != nil { + ts.Fatalf("unable to create log file %q: %s", path, err.Error()) + } + + ts.Defer(func() { + if err := f.Close(); err != nil { + panic(fmt.Errorf("unable to close log file %q: %w", path, err)) + } + }) + + logger := log.NewTMLogger(f) + switch level := os.Getenv("LOG_LEVEL"); strings.ToLower(level) { + case "error": + logger.SetLevel(log.LevelError) + case "debug": + logger.SetLevel(log.LevelDebug) + case "info": + logger.SetLevel(log.LevelInfo) + case "": + default: + ts.Fatalf("invalid log level %q", level) + } + + ts.Logf("starting logger: %q", path) + return logger +} diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 28531f0a773..6f695e98558 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -188,7 +188,8 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { }) defer m2.Release() m2.RunMemPackage(memPkg, true) - fmt.Println("CPUCYCLES addpkg", m2.Cycles) + + ctx.Logger().Info("CPUCYCLES", "addpkg", m2.Cycles) return nil } @@ -270,7 +271,7 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { m.Release() }() rtvs := m.Eval(xn) - fmt.Println("CPUCYCLES call", m.Cycles) + ctx.Logger().Info("CPUCYCLES call: ", m.Cycles) for i, rtv := range rtvs { res = res + rtv.String() if i < len(rtvs)-1 { diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index 5f90a9f874e..30b612a9de2 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -29,7 +29,7 @@ type addCfg struct { index uint64 } -func newAddCmd(rootCfg *baseCfg) *commands.Command { +func newAddCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &addCfg{ rootCfg: rootCfg, } @@ -42,7 +42,7 @@ func newAddCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execAdd(cfg, args, commands.NewDefaultIO()) + return execAdd(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/addpkg.go b/tm2/pkg/crypto/keys/client/addpkg.go index 885e1f123d7..3de9a6de546 100644 --- a/tm2/pkg/crypto/keys/client/addpkg.go +++ b/tm2/pkg/crypto/keys/client/addpkg.go @@ -24,7 +24,7 @@ type addPkgCfg struct { deposit string } -func newAddPkgCmd(rootCfg *makeTxCfg) *commands.Command { +func newAddPkgCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &addPkgCfg{ rootCfg: rootCfg, } @@ -37,7 +37,7 @@ func newAddPkgCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execAddPkg(cfg, args, commands.NewDefaultIO()) + return execAddPkg(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/broadcast.go b/tm2/pkg/crypto/keys/client/broadcast.go index 039a9557c38..f1d448495a6 100644 --- a/tm2/pkg/crypto/keys/client/broadcast.go +++ b/tm2/pkg/crypto/keys/client/broadcast.go @@ -23,7 +23,7 @@ type broadcastCfg struct { tx *std.Tx } -func newBroadcastCmd(rootCfg *baseCfg) *commands.Command { +func newBroadcastCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &broadcastCfg{ rootCfg: rootCfg, } diff --git a/tm2/pkg/crypto/keys/client/call.go b/tm2/pkg/crypto/keys/client/call.go index bcb7be3e550..29fe9739a36 100644 --- a/tm2/pkg/crypto/keys/client/call.go +++ b/tm2/pkg/crypto/keys/client/call.go @@ -22,7 +22,7 @@ type callCfg struct { args commands.StringArr } -func newCallCmd(rootCfg *makeTxCfg) *commands.Command { +func newCallCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &callCfg{ rootCfg: rootCfg, } @@ -35,7 +35,7 @@ func newCallCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execCall(cfg, args, commands.NewDefaultIO()) + return execCall(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/delete.go b/tm2/pkg/crypto/keys/client/delete.go index 0f216d3467c..e22ac30988c 100644 --- a/tm2/pkg/crypto/keys/client/delete.go +++ b/tm2/pkg/crypto/keys/client/delete.go @@ -16,7 +16,7 @@ type deleteCfg struct { force bool } -func newDeleteCmd(rootCfg *baseCfg) *commands.Command { +func newDeleteCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &deleteCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newDeleteCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execDelete(cfg, args, commands.NewDefaultIO()) + return execDelete(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/export.go b/tm2/pkg/crypto/keys/client/export.go index eda04a5c92f..6eff8aa97b3 100644 --- a/tm2/pkg/crypto/keys/client/export.go +++ b/tm2/pkg/crypto/keys/client/export.go @@ -18,7 +18,7 @@ type exportCfg struct { unsafe bool } -func newExportCmd(rootCfg *baseCfg) *commands.Command { +func newExportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &exportCfg{ rootCfg: rootCfg, } @@ -31,7 +31,7 @@ func newExportCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execExport(cfg, commands.NewDefaultIO()) + return execExport(cfg, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/generate.go b/tm2/pkg/crypto/keys/client/generate.go index b721e6704ce..d209bd70bd3 100644 --- a/tm2/pkg/crypto/keys/client/generate.go +++ b/tm2/pkg/crypto/keys/client/generate.go @@ -16,7 +16,7 @@ type generateCfg struct { customEntropy bool } -func newGenerateCmd(rootCfg *baseCfg) *commands.Command { +func newGenerateCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &generateCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newGenerateCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execGenerate(cfg, args, commands.NewDefaultIO()) + return execGenerate(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/import.go b/tm2/pkg/crypto/keys/client/import.go index 5e0eeecabb5..e1d8af55861 100644 --- a/tm2/pkg/crypto/keys/client/import.go +++ b/tm2/pkg/crypto/keys/client/import.go @@ -18,7 +18,7 @@ type importCfg struct { unsafe bool } -func newImportCmd(rootCfg *baseCfg) *commands.Command { +func newImportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &importCfg{ rootCfg: rootCfg, } @@ -31,7 +31,7 @@ func newImportCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, _ []string) error { - return execImport(cfg, commands.NewDefaultIO()) + return execImport(cfg, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/list.go b/tm2/pkg/crypto/keys/client/list.go index 50be35cef43..cb86feb2395 100644 --- a/tm2/pkg/crypto/keys/client/list.go +++ b/tm2/pkg/crypto/keys/client/list.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) -func newListCmd(rootCfg *baseCfg) *commands.Command { +func newListCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "list", @@ -17,7 +17,7 @@ func newListCmd(rootCfg *baseCfg) *commands.Command { }, nil, func(_ context.Context, args []string) error { - return execList(rootCfg, args, commands.NewDefaultIO()) + return execList(rootCfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index cbcc6def0de..36214a5a983 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -17,7 +17,7 @@ type makeTxCfg struct { chainID string } -func newMakeTxCmd(rootCfg *baseCfg) *commands.Command { +func newMakeTxCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &makeTxCfg{ rootCfg: rootCfg, } @@ -33,9 +33,9 @@ func newMakeTxCmd(rootCfg *baseCfg) *commands.Command { ) cmd.AddSubCommands( - newAddPkgCmd(cfg), - newSendCmd(cfg), - newCallCmd(cfg), + newAddPkgCmd(cfg, io), + newSendCmd(cfg, io), + newCallCmd(cfg, io), ) return cmd diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 58923f8787c..8cc5757aba7 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -3,7 +3,6 @@ package client import ( "context" "flag" - "fmt" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" @@ -22,7 +21,7 @@ type queryCfg struct { path string } -func newQueryCmd(rootCfg *baseCfg) *commands.Command { +func newQueryCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &queryCfg{ rootCfg: rootCfg, } @@ -35,7 +34,7 @@ func newQueryCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execQuery(cfg, args) + return execQuery(cfg, args, io) }, ) } @@ -63,7 +62,7 @@ func (c *queryCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execQuery(cfg *queryCfg, args []string) error { +func execQuery(cfg *queryCfg, args []string, io *commands.IO) error { if len(args) != 1 { return flag.ErrHelp } @@ -76,15 +75,16 @@ func execQuery(cfg *queryCfg, args []string) error { } if qres.Response.Error != nil { - fmt.Printf("Log: %s\n", + io.Printf("Log: %s\n", qres.Response.Log) return qres.Response.Error } + resdata := qres.Response.Data // XXX in general, how do we know what to show? // proof := qres.Response.Proof height := qres.Response.Height - fmt.Printf("height: %d\ndata: %s\n", + io.Printf("height: %d\ndata: %s\n", height, string(resdata)) return nil diff --git a/tm2/pkg/crypto/keys/client/root.go b/tm2/pkg/crypto/keys/client/root.go index ad8983f2bb9..550dd408b77 100644 --- a/tm2/pkg/crypto/keys/client/root.go +++ b/tm2/pkg/crypto/keys/client/root.go @@ -18,7 +18,7 @@ type baseCfg struct { BaseOptions } -func NewRootCmd() *commands.Command { +func NewRootCmd(io *commands.IO) *commands.Command { cfg := &baseCfg{} cmd := commands.NewCommand( @@ -35,17 +35,17 @@ func NewRootCmd() *commands.Command { ) cmd.AddSubCommands( - newAddCmd(cfg), - newDeleteCmd(cfg), - newGenerateCmd(cfg), - newExportCmd(cfg), - newImportCmd(cfg), - newListCmd(cfg), - newSignCmd(cfg), - newVerifyCmd(cfg), - newQueryCmd(cfg), - newBroadcastCmd(cfg), - newMakeTxCmd(cfg), + newAddCmd(cfg, io), + newDeleteCmd(cfg, io), + newGenerateCmd(cfg, io), + newExportCmd(cfg, io), + newImportCmd(cfg, io), + newListCmd(cfg, io), + newSignCmd(cfg, io), + newVerifyCmd(cfg, io), + newQueryCmd(cfg, io), + newBroadcastCmd(cfg, io), + newMakeTxCmd(cfg, io), ) return cmd diff --git a/tm2/pkg/crypto/keys/client/send.go b/tm2/pkg/crypto/keys/client/send.go index 8f82778b1e3..6d19ffcb393 100644 --- a/tm2/pkg/crypto/keys/client/send.go +++ b/tm2/pkg/crypto/keys/client/send.go @@ -21,7 +21,7 @@ type sendCfg struct { to string } -func newSendCmd(rootCfg *makeTxCfg) *commands.Command { +func newSendCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { cfg := &sendCfg{ rootCfg: rootCfg, } @@ -34,7 +34,7 @@ func newSendCmd(rootCfg *makeTxCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execSend(cfg, args, commands.NewDefaultIO()) + return execSend(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/sign.go b/tm2/pkg/crypto/keys/client/sign.go index bfc39647141..761e0d7a563 100644 --- a/tm2/pkg/crypto/keys/client/sign.go +++ b/tm2/pkg/crypto/keys/client/sign.go @@ -28,7 +28,7 @@ type signCfg struct { pass string } -func newSignCmd(rootCfg *baseCfg) *commands.Command { +func newSignCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &signCfg{ rootCfg: rootCfg, } @@ -41,7 +41,7 @@ func newSignCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execSign(cfg, args, commands.NewDefaultIO()) + return execSign(cfg, args, io) }, ) } diff --git a/tm2/pkg/crypto/keys/client/verify.go b/tm2/pkg/crypto/keys/client/verify.go index 3dcc5f35dee..bb486c1a8fa 100644 --- a/tm2/pkg/crypto/keys/client/verify.go +++ b/tm2/pkg/crypto/keys/client/verify.go @@ -16,7 +16,7 @@ type verifyCfg struct { docPath string } -func newVerifyCmd(rootCfg *baseCfg) *commands.Command { +func newVerifyCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { cfg := &verifyCfg{ rootCfg: rootCfg, } @@ -29,7 +29,7 @@ func newVerifyCmd(rootCfg *baseCfg) *commands.Command { }, cfg, func(_ context.Context, args []string) error { - return execVerify(cfg, args, commands.NewDefaultIO()) + return execVerify(cfg, args, io) }, ) } From 740e7f72a4861b2c9a153746a609c374662e75bb Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 5 Oct 2023 18:44:44 +0200 Subject: [PATCH 15/93] fix(tm2): make HTTPClient support https (#1158) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- tm2/pkg/bft/rpc/lib/client/http_client.go | 12 ++--- .../bft/rpc/lib/client/http_client_test.go | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 tm2/pkg/bft/rpc/lib/client/http_client_test.go diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go index d5708f97d40..5a9da9ec052 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client.go @@ -78,12 +78,6 @@ func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) } - // accept http(s) as an alias for tcp - switch protocol { - case protoHTTP, protoHTTPS: - protocol = protoTCP - } - return protocol, address, nil } @@ -99,6 +93,12 @@ func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { return makeErrorDialer(err) } + // net.Dial doesn't understand http/https, so change it to TCP + switch protocol { + case protoHTTP, protoHTTPS: + protocol = protoTCP + } + return func(proto, addr string) (net.Conn, error) { return net.Dial(protocol, address) } diff --git a/tm2/pkg/bft/rpc/lib/client/http_client_test.go b/tm2/pkg/bft/rpc/lib/client/http_client_test.go new file mode 100644 index 00000000000..9546a0c1d72 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/http_client_test.go @@ -0,0 +1,52 @@ +package rpcclient + +import ( + "testing" + + "github.com/jaekwon/testify/assert" +) + +func Test_parseRemoteAddr(t *testing.T) { + tt := []struct { + remoteAddr string + network, s, errContains string + }{ + {"127.0.0.1", "tcp", "127.0.0.1", ""}, + {"https://example.com", "https", "example.com", ""}, + {"wss://[::1]", "wss", "[::1]", ""}, + // no error cases - they cannot happen! + } + + for _, tc := range tt { + n, s, err := parseRemoteAddr(tc.remoteAddr) + if tc.errContains != "" { + _ = assert.NotNil(t, err) && assert.Contains(t, err.Error(), tc.errContains) + } + assert.NoError(t, err) + assert.Equal(t, n, tc.network) + assert.Equal(t, s, tc.s) + } +} + +// Following tests check that we correctly translate http/https to tcp, +// and other protocols are left intact from parseRemoteAddr() + +func Test_makeHTTPDialer(t *testing.T) { + dl := makeHTTPDialer("https://.") + _, err := dl("hello", "world") + if assert.NotNil(t, err) { + e := err.Error() + assert.Contains(t, e, "dial tcp:", "should convert https to tcp") + assert.Contains(t, e, "address .:", "should have parsed the address (as incorrect)") + } +} + +func Test_makeHTTPDialer_noConvert(t *testing.T) { + dl := makeHTTPDialer("udp://.") + _, err := dl("hello", "world") + if assert.NotNil(t, err) { + e := err.Error() + assert.Contains(t, e, "dial udp:", "udp protocol should remain the same") + assert.Contains(t, e, "address .:", "should have parsed the address (as incorrect)") + } +} From 4af3eb217ef4234a7bfed485d513d93276b153b3 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 5 Oct 2023 19:32:18 +0200 Subject: [PATCH 16/93] fix(gnolang): allow comparisons using uninitialized string values (#1132) The provided additional test, without the patch, fails with the following error: ``` === RUN TestFiles/comp3.gno Machine.RunMain() panic: interface conversion: gnolang.Value is nil, not gnolang.StringValue [...] goroutine 180 [running]: runtime/debug.Stack() /usr/lib/go/src/runtime/debug/stack.go:24 +0x65 [...] panic({0xb91680, 0xc001fa7bf0}) /usr/lib/go/src/runtime/panic.go:884 +0x213 github.com/gnolang/gno/gnovm/pkg/gnolang.isLss(0xc0054b4050, 0xc0054b4078) /home/howl/oc/gno2/gnovm/pkg/gnolang/op_binary.go:492 +0x431 ``` This seems to be because internally, the string value is "uninitialized" (hopefully right word here). This is in opposition to an initialised string, as would happen for the statement `x := ""`. This PR changes the behaviour for comparisons inside of `op_binary` (<, >, <=, >=) to use `GetString` instead of a type assertion of `TypedValue.V` to a `StringValue`. This is in line with what is already done inside of `isEql`, introduced in this commit: https://github.com/gnolang/gno/commit/da6520f3de0fe5d70602b8652528b96a17dc15f0#diff-7cc5c6bc5496b6ad9ed55e04e1cdf2f0d1d5954af21be5bc38ef3c46389149a9L358 `git blame` points instead this part of code inside of the comparisons functions to go back to the initial commit. Additionally, I looked for other cases where we are currently doing type assertions directly into a `StringValue`, and there was one in the implementation of `append`. Since this is a special case and requires having a native value as the first argument, I haven't written a test for it, but the change should be safe as `GetString()` internally just does the type assertion, but checks for `tv.V == nil` first.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] 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: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/pkg/gnolang/op_binary.go | 8 ++++---- gnovm/pkg/gnolang/uverse.go | 2 +- gnovm/tests/files/append5.gno | 10 ++++++++++ gnovm/tests/files/comp3.gno | 21 +++++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 gnovm/tests/files/append5.gno create mode 100644 gnovm/tests/files/comp3.gno diff --git a/gnovm/pkg/gnolang/op_binary.go b/gnovm/pkg/gnolang/op_binary.go index 13156133e38..ddab8302d2d 100644 --- a/gnovm/pkg/gnolang/op_binary.go +++ b/gnovm/pkg/gnolang/op_binary.go @@ -489,7 +489,7 @@ func isEql(store Store, lv, rv *TypedValue) bool { func isLss(lv, rv *TypedValue) bool { switch lv.T.Kind() { case StringKind: - return (lv.V.(StringValue) < rv.V.(StringValue)) + return (lv.GetString() < rv.GetString()) case IntKind: return (lv.GetInt() < rv.GetInt()) case Int8Kind: @@ -533,7 +533,7 @@ func isLss(lv, rv *TypedValue) bool { func isLeq(lv, rv *TypedValue) bool { switch lv.T.Kind() { case StringKind: - return (lv.V.(StringValue) <= rv.V.(StringValue)) + return (lv.GetString() <= rv.GetString()) case IntKind: return (lv.GetInt() <= rv.GetInt()) case Int8Kind: @@ -577,7 +577,7 @@ func isLeq(lv, rv *TypedValue) bool { func isGtr(lv, rv *TypedValue) bool { switch lv.T.Kind() { case StringKind: - return (lv.V.(StringValue) > rv.V.(StringValue)) + return (lv.GetString() > rv.GetString()) case IntKind: return (lv.GetInt() > rv.GetInt()) case Int8Kind: @@ -621,7 +621,7 @@ func isGtr(lv, rv *TypedValue) bool { func isGeq(lv, rv *TypedValue) bool { switch lv.T.Kind() { case StringKind: - return (lv.V.(StringValue) >= rv.V.(StringValue)) + return (lv.GetString() >= rv.GetString()) case IntKind: return (lv.GetInt() >= rv.GetInt()) case Int8Kind: diff --git a/gnovm/pkg/gnolang/uverse.go b/gnovm/pkg/gnolang/uverse.go index 80f8a751e57..57f8f6d393d 100644 --- a/gnovm/pkg/gnolang/uverse.go +++ b/gnovm/pkg/gnolang/uverse.go @@ -550,7 +550,7 @@ func UverseNode() *PackageNode { if xt.Elem().Kind() == Uint8Kind { // TODO this might be faster if reflect supports // appending this way without first converting to a slice. - argrv := reflect.ValueOf([]byte(arg1.TV.V.(StringValue))) + argrv := reflect.ValueOf([]byte(arg1.TV.GetString())) resrv := reflect.AppendSlice(sv, argrv) m.PushValue(TypedValue{ T: xt, diff --git a/gnovm/tests/files/append5.gno b/gnovm/tests/files/append5.gno new file mode 100644 index 00000000000..b1fdae852b1 --- /dev/null +++ b/gnovm/tests/files/append5.gno @@ -0,0 +1,10 @@ +package main + +func main() { + var x string + y := append([]byte{'X'}, x...) + println(string(y)) +} + +// Output: +// X diff --git a/gnovm/tests/files/comp3.gno b/gnovm/tests/files/comp3.gno new file mode 100644 index 00000000000..d412e8f0e48 --- /dev/null +++ b/gnovm/tests/files/comp3.gno @@ -0,0 +1,21 @@ +package main + +func main() { + // test against uninitialized value: https://github.com/gnolang/gno/pull/1132 + var x string + y := "Hello" + results := [...]bool{ + x < y, + x <= y, + x >= y, + x > y, + + x == y, + x == "", + y == "", + } + println(results) +} + +// Output: +// array[(true bool),(true bool),(false bool),(false bool),(false bool),(true bool),(false bool)] From d86509507221897b25c2707040c688b748e7d363 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Fri, 6 Oct 2023 01:31:58 +0530 Subject: [PATCH 17/93] feat: use `modfile` package to write modfile (#1077) The previous implementation manually iterates over the `Require` and `Replace` and writes them in the string var to construct the modfile, which is very inefficient and doesn't handles comments and other cases. Changed it use `modfile` package to write modfile(gno.mod/go.mod). It uses `*modfile.FileSyntax`. Copied few methods from `modfile` package to manipulate `*modfile.FileSyntax`.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] 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: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gno/mod.go | 2 +- gnovm/pkg/gnomod/file.go | 162 +++++++--- gnovm/pkg/gnomod/gnomod.go | 37 +-- gnovm/pkg/gnomod/read.go | 209 ++++++++++++- gnovm/pkg/gnomod/read_test.go | 541 ++++++++++++++++++++++++++++++++++ 5 files changed, 875 insertions(+), 76 deletions(-) create mode 100644 gnovm/pkg/gnomod/read_test.go diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 09194232fec..267b7d99237 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -126,7 +126,7 @@ func execModDownload(cfg *modDownloadCfg, args []string, io *commands.IO) error } // write go.mod file - err = gomod.WriteToPath(filepath.Join(path, "go.mod")) + err = gomod.Write(filepath.Join(path, "go.mod")) if err != nil { return fmt.Errorf("write go.mod file: %w", err) } diff --git a/gnovm/pkg/gnomod/file.go b/gnovm/pkg/gnomod/file.go index 71d98b2d14b..25189ebc71d 100644 --- a/gnovm/pkg/gnomod/file.go +++ b/gnovm/pkg/gnomod/file.go @@ -1,3 +1,12 @@ +// Some part of file is copied and modified from +// golang.org/x/mod/modfile/read.go +// +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in here[1]. +// +// [1]: https://cs.opensource.google/go/x/mod/+/master:LICENSE + package gnomod import ( @@ -24,6 +33,100 @@ type File struct { Syntax *modfile.FileSyntax } +// AddRequire sets the first require line for path to version vers, +// preserving any existing comments for that line and removing all +// other lines for path. +// +// If no line currently exists for path, AddRequire adds a new line +// at the end of the last require block. +func (f *File) AddRequire(path, vers string) error { + need := true + for _, r := range f.Require { + if r.Mod.Path == path { + if need { + r.Mod.Version = vers + updateLine(r.Syntax, "require", modfile.AutoQuote(path), vers) + need = false + } else { + markLineAsRemoved(r.Syntax) + *r = modfile.Require{} + } + } + } + + if need { + f.AddNewRequire(path, vers, false) + } + return nil +} + +// AddNewRequire adds a new require line for path at version vers at the end of +// the last require block, regardless of any existing require lines for path. +func (f *File) AddNewRequire(path, vers string, indirect bool) { + line := addLine(f.Syntax, nil, "require", modfile.AutoQuote(path), vers) + r := &modfile.Require{ + Mod: module.Version{Path: path, Version: vers}, + Syntax: line, + } + setIndirect(r, indirect) + f.Require = append(f.Require, r) +} + +func (f *File) AddModuleStmt(path string) error { + if f.Syntax == nil { + f.Syntax = new(modfile.FileSyntax) + } + if f.Module == nil { + f.Module = &modfile.Module{ + Mod: module.Version{Path: path}, + Syntax: addLine(f.Syntax, nil, "module", modfile.AutoQuote(path)), + } + } else { + f.Module.Mod.Path = path + updateLine(f.Module.Syntax, "module", modfile.AutoQuote(path)) + } + return nil +} + +func (f *File) AddComment(text string) { + if f.Syntax == nil { + f.Syntax = new(modfile.FileSyntax) + } + f.Syntax.Stmt = append(f.Syntax.Stmt, &modfile.CommentBlock{ + Comments: modfile.Comments{ + Before: []modfile.Comment{ + { + Token: text, + }, + }, + }, + }) +} + +func (f *File) AddReplace(oldPath, oldVers, newPath, newVers string) error { + return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers) +} + +func (f *File) DropRequire(path string) error { + for _, r := range f.Require { + if r.Mod.Path == path { + markLineAsRemoved(r.Syntax) + *r = modfile.Require{} + } + } + return nil +} + +func (f *File) DropReplace(oldPath, oldVers string) error { + for _, r := range f.Replace { + if r.Old.Path == oldPath && r.Old.Version == oldVers { + markLineAsRemoved(r.Syntax) + *r = modfile.Replace{} + } + } + return nil +} + // Validate validates gno.mod func (f *File) Validate() error { if f.Module == nil { @@ -73,13 +176,8 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { return fmt.Errorf("writepackage: %w", err) } - modFile := &File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: mod.Path, - }, - }, - } + modFile := new(File) + modFile.AddModuleStmt(mod.Path) for _, req := range requirements { path := req[1 : len(req)-1] // trim leading and trailing `"` if strings.HasSuffix(path, modFile.Module.Mod.Path) { @@ -92,13 +190,7 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { if strings.HasPrefix(path, gnolang.ImportPrefix) { path = strings.TrimPrefix(path, gnolang.ImportPrefix+"/examples/") - modFile.Require = append(modFile.Require, &modfile.Require{ - Mod: module.Version{ - Path: path, - Version: "v0.0.0", // TODO: Use latest? - }, - Indirect: true, - }) + modFile.AddNewRequire(path, "v0.0.0-latest", true) } } @@ -112,7 +204,7 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { } pkgPath := PackageDir(path, mod) goModFilePath := filepath.Join(pkgPath, "go.mod") - err = goMod.WriteToPath(goModFilePath) + err = goMod.Write(goModFilePath) if err != nil { return err } @@ -121,42 +213,14 @@ func (f *File) FetchDeps(path string, remote string, verbose bool) error { return nil } -// WriteToPath writes file to the given absolute file path -// TODO: Find better way to do this. Try to use `modfile` -// package to manage this. -func (f *File) WriteToPath(absFilePath string) error { - if f.Module == nil { - return errors.New("writing go.mod: module not found") - } - - data := "module " + f.Module.Mod.Path + "\n" - - if f.Go != nil { - data += "\ngo " + f.Go.Version + "\n" - } - - if f.Require != nil { - data += "\nrequire (" + "\n" - for _, req := range f.Require { - data += "\t" + req.Mod.Path + " " + req.Mod.Version + "\n" - } - data += ")\n" - } - - if f.Replace != nil { - data += "\nreplace (" + "\n" - for _, rep := range f.Replace { - data += "\t" + rep.Old.Path + " " + rep.Old.Version + - " => " + rep.New.Path + "\n" - } - data += ")\n" - } - - err := os.WriteFile(absFilePath, []byte(data), 0o644) +// writes file to the given absolute file path +func (f *File) Write(fname string) error { + f.Syntax.Cleanup() + data := modfile.Format(f.Syntax) + err := os.WriteFile(fname, data, 0o644) if err != nil { - return fmt.Errorf("writefile %q: %w", absFilePath, err) + return fmt.Errorf("writefile %q: %w", fname, err) } - return nil } diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index aa41c5aa00c..7bb51d6558a 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -100,7 +100,7 @@ func GnoToGoMod(f File) (*File, error) { if strings.HasPrefix(f.Module.Mod.Path, gnolang.GnoRealmPkgsPrefixBefore) || strings.HasPrefix(f.Module.Mod.Path, gnolang.GnoPackagePrefixBefore) { - f.Module.Mod.Path = gnolang.ImportPrefix + "/examples/" + f.Module.Mod.Path + f.AddModuleStmt(gnolang.ImportPrefix + "/examples/" + f.Module.Mod.Path) } for i := range f.Require { @@ -113,20 +113,17 @@ func GnoToGoMod(f File) (*File, error) { path := f.Require[i].Mod.Path if strings.HasPrefix(f.Require[i].Mod.Path, gnolang.GnoRealmPkgsPrefixBefore) || strings.HasPrefix(f.Require[i].Mod.Path, gnolang.GnoPackagePrefixBefore) { - f.Require[i].Mod.Path = gnolang.ImportPrefix + "/examples/" + f.Require[i].Mod.Path + // Add dependency with a modified import path + f.AddRequire(gnolang.ImportPrefix+"/examples/"+f.Require[i].Mod.Path, f.Require[i].Mod.Version) } - - f.Replace = append(f.Replace, &modfile.Replace{ - Old: module.Version{ - Path: f.Require[i].Mod.Path, - Version: f.Require[i].Mod.Version, - }, - New: module.Version{ - Path: filepath.Join(gnoModPath, path), - }, - }) + f.AddReplace(f.Require[i].Mod.Path, f.Require[i].Mod.Version, filepath.Join(gnoModPath, path), "") + // Remove the old require since the new dependency was added above + f.DropRequire(f.Require[i].Mod.Path) } + // Remove replacements that are not replaced by directories. + // + // Explanation: // By this stage every replacement should be replace by dir. // If not replaced by dir, remove it. // @@ -153,14 +150,11 @@ func GnoToGoMod(f File) (*File, error) { // ``` // // Remove `gno.land/p/demo/avl v1.2.3 => gno.land/p/demo/avl v3.2.1`. - repl := make([]*modfile.Replace, 0, len(f.Replace)) for _, r := range f.Replace { if !modfile.IsDirectoryPath(r.New.Path) { - continue + f.DropReplace(r.Old.Path, r.Old.Version) } - repl = append(repl, r) } - f.Replace = repl return &f, nil } @@ -215,14 +209,9 @@ func CreateGnoModFile(rootDir, modPath string) error { return err } - modFile := &File{ - Module: &modfile.Module{ - Mod: module.Version{ - Path: modPath, - }, - }, - } - modFile.WriteToPath(filepath.Join(rootDir, "gno.mod")) + modfile := new(File) + modfile.AddModuleStmt(modPath) + modfile.Write(filepath.Join(rootDir, "gno.mod")) return nil } diff --git a/gnovm/pkg/gnomod/read.go b/gnovm/pkg/gnomod/read.go index 206c843f86a..9bbed3c4651 100644 --- a/gnovm/pkg/gnomod/read.go +++ b/gnovm/pkg/gnomod/read.go @@ -3,9 +3,10 @@ // license that can be found in here[1]. // // [1]: https://cs.opensource.google/go/x/mod/+/master:LICENSE -// Original Filepath: golang.org/x/mod/modfile/read.go // -// Note: This file may contain some modifications. +// Mostly copied and modified from: +// - golang.org/x/mod/modfile/read.go +// - golang.org/x/mod/modfile/rule.go package gnomod @@ -845,3 +846,207 @@ func parseDraft(block *modfile.CommentBlock) bool { } return true } + +// markLineAsRemoved modifies line so that it (and its end-of-line comment, if any) +// will be dropped by (*FileSyntax).Cleanup. +func markLineAsRemoved(line *modfile.Line) { + line.Token = nil + line.Comments.Suffix = nil +} + +func updateLine(line *modfile.Line, tokens ...string) { + if line.InBlock { + tokens = tokens[1:] + } + line.Token = tokens +} + +// setIndirect sets line to have (or not have) a "// indirect" comment. +func setIndirect(r *modfile.Require, indirect bool) { + r.Indirect = indirect + line := r.Syntax + if isIndirect(line) == indirect { + return + } + if indirect { + // Adding comment. + if len(line.Suffix) == 0 { + // New comment. + line.Suffix = []modfile.Comment{{Token: "// indirect", Suffix: true}} + return + } + + com := &line.Suffix[0] + text := strings.TrimSpace(strings.TrimPrefix(com.Token, string(slashSlash))) + if text == "" { + // Empty comment. + com.Token = "// indirect" + return + } + + // Insert at beginning of existing comment. + com.Token = "// indirect; " + text + return + } + + // Removing comment. + f := strings.TrimSpace(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) + if f == "indirect" { + // Remove whole comment. + line.Suffix = nil + return + } + + // Remove comment prefix. + com := &line.Suffix[0] + i := strings.Index(com.Token, "indirect;") + com.Token = "//" + com.Token[i+len("indirect;"):] +} + +// isIndirect reports whether line has a "// indirect" comment, +// meaning it is in go.mod only for its effect on indirect dependencies, +// so that it can be dropped entirely once the effective version of the +// indirect dependency reaches the given minimum version. +func isIndirect(line *modfile.Line) bool { + if len(line.Suffix) == 0 { + return false + } + f := strings.Fields(strings.TrimPrefix(line.Suffix[0].Token, string(slashSlash))) + return (len(f) == 1 && f[0] == "indirect" || len(f) > 1 && f[0] == "indirect;") +} + +// addLine adds a line containing the given tokens to the file. +// +// If the first token of the hint matches the first token of the +// line, the new line is added at the end of the block containing hint, +// extracting hint into a new block if it is not yet in one. +// +// If the hint is non-nil buts its first token does not match, +// the new line is added after the block containing hint +// (or hint itself, if not in a block). +// +// If no hint is provided, addLine appends the line to the end of +// the last block with a matching first token, +// or to the end of the file if no such block exists. +func addLine(x *modfile.FileSyntax, hint modfile.Expr, tokens ...string) *modfile.Line { + if hint == nil { + // If no hint given, add to the last statement of the given type. + Loop: + for i := len(x.Stmt) - 1; i >= 0; i-- { + stmt := x.Stmt[i] + switch stmt := stmt.(type) { + case *modfile.Line: + if stmt.Token != nil && stmt.Token[0] == tokens[0] { + hint = stmt + break Loop + } + case *modfile.LineBlock: + if stmt.Token[0] == tokens[0] { + hint = stmt + break Loop + } + } + } + } + + newLineAfter := func(i int) *modfile.Line { + newl := &modfile.Line{Token: tokens} + if i == len(x.Stmt) { + x.Stmt = append(x.Stmt, newl) + } else { + x.Stmt = append(x.Stmt, nil) + copy(x.Stmt[i+2:], x.Stmt[i+1:]) + x.Stmt[i+1] = newl + } + return newl + } + + if hint != nil { + for i, stmt := range x.Stmt { + switch stmt := stmt.(type) { + case *modfile.Line: + if stmt == hint { + if stmt.Token == nil || stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + // Convert line to line block. + stmt.InBlock = true + block := &modfile.LineBlock{Token: stmt.Token[:1], Line: []*modfile.Line{stmt}} + stmt.Token = stmt.Token[1:] + x.Stmt[i] = block + newl := &modfile.Line{Token: tokens[1:], InBlock: true} + block.Line = append(block.Line, newl) + return newl + } + + case *modfile.LineBlock: + if stmt == hint { + if stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + newl := &modfile.Line{Token: tokens[1:], InBlock: true} + stmt.Line = append(stmt.Line, newl) + return newl + } + + for j, line := range stmt.Line { + if line == hint { + if stmt.Token[0] != tokens[0] { + return newLineAfter(i) + } + + // Add new line after hint within the block. + stmt.Line = append(stmt.Line, nil) + copy(stmt.Line[j+2:], stmt.Line[j+1:]) + newl := &modfile.Line{Token: tokens[1:], InBlock: true} + stmt.Line[j+1] = newl + return newl + } + } + } + } + } + + newl := &modfile.Line{Token: tokens} + x.Stmt = append(x.Stmt, newl) + return newl +} + +func addReplace(syntax *modfile.FileSyntax, replace *[]*modfile.Replace, oldPath, oldVers, newPath, newVers string) error { + need := true + oldv := module.Version{Path: oldPath, Version: oldVers} + newv := module.Version{Path: newPath, Version: newVers} + tokens := []string{"replace", modfile.AutoQuote(oldPath)} + if oldVers != "" { + tokens = append(tokens, oldVers) + } + tokens = append(tokens, "=>", modfile.AutoQuote(newPath)) + if newVers != "" { + tokens = append(tokens, newVers) + } + + var hint *modfile.Line + for _, r := range *replace { + if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) { + if need { + // Found replacement for old; update to use new. + r.New = newv + updateLine(r.Syntax, tokens...) + need = false + continue + } + // Already added; delete other replacements for same. + markLineAsRemoved(r.Syntax) + *r = modfile.Replace{} + } + if r.Old.Path == oldPath { + hint = r.Syntax + } + } + if need { + *replace = append(*replace, &modfile.Replace{Old: oldv, New: newv, Syntax: addLine(syntax, hint, tokens...)}) + } + return nil +} diff --git a/gnovm/pkg/gnomod/read_test.go b/gnovm/pkg/gnomod/read_test.go new file mode 100644 index 00000000000..cf3b6f59076 --- /dev/null +++ b/gnovm/pkg/gnomod/read_test.go @@ -0,0 +1,541 @@ +// Copyright 2018 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 gnomod + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "golang.org/x/mod/modfile" +) + +// TestParsePunctuation verifies that certain ASCII punctuation characters +// (brackets, commas) are lexed as separate tokens, even when they're +// surrounded by identifier characters. +func TestParsePunctuation(t *testing.T) { + for _, test := range []struct { + desc, src, want string + }{ + {"paren", "require ()", "require ( )"}, + {"brackets", "require []{},", "require [ ] { } ,"}, + {"mix", "require a[b]c{d}e,", "require a [ b ] c { d } e ,"}, + {"block_mix", "require (\n\ta[b]\n)", "require ( a [ b ] )"}, + {"interval", "require [v1.0.0, v1.1.0)", "require [ v1.0.0 , v1.1.0 )"}, + } { + t.Run(test.desc, func(t *testing.T) { + f, err := parse("gno.mod", []byte(test.src)) + if err != nil { + t.Fatalf("parsing %q: %v", test.src, err) + } + var tokens []string + for _, stmt := range f.Stmt { + switch stmt := stmt.(type) { + case *modfile.Line: + tokens = append(tokens, stmt.Token...) + case *modfile.LineBlock: + tokens = append(tokens, stmt.Token...) + tokens = append(tokens, "(") + for _, line := range stmt.Line { + tokens = append(tokens, line.Token...) + } + tokens = append(tokens, ")") + default: + t.Fatalf("parsing %q: unexpected statement of type %T", test.src, stmt) + } + } + got := strings.Join(tokens, " ") + if got != test.want { + t.Errorf("parsing %q: got %q, want %q", test.src, got, test.want) + } + }) + } +} + +var modulePathTests = []struct { + input []byte + expected string +}{ + {input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"}, + {input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"}, + {input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"}, + {input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"}, + {input: []byte("module `github.com/rsc/vgotest`"), expected: "github.com/rsc/vgotest"}, + {input: []byte("module \"github.com/rsc/vgotest/v2\""), expected: "github.com/rsc/vgotest/v2"}, + {input: []byte("module github.com/rsc/vgotest/v2"), expected: "github.com/rsc/vgotest/v2"}, + {input: []byte("module \"gopkg.in/yaml.v2\""), expected: "gopkg.in/yaml.v2"}, + {input: []byte("module gopkg.in/yaml.v2"), expected: "gopkg.in/yaml.v2"}, + {input: []byte("module \"gopkg.in/check.v1\"\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module \"gopkg.in/check.v1\n\""), expected: ""}, + {input: []byte("module gopkg.in/check.v1\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module \"gopkg.in/check.v1\"\r\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module gopkg.in/check.v1\r\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module \"gopkg.in/check.v1\"\n\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module gopkg.in/check.v1\n\n"), expected: "gopkg.in/check.v1"}, + {input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""}, + {input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""}, + {input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""}, + {input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""}, + {input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""}, + {input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""}, + {input: []byte("module \nmodule a/b/c "), expected: "a/b/c"}, + {input: []byte("module \" \""), expected: " "}, + {input: []byte("module "), expected: ""}, + {input: []byte("module \" a/b/c \""), expected: " a/b/c "}, + {input: []byte("module \"github.com/rsc/vgotest1\" // with a comment"), expected: "github.com/rsc/vgotest1"}, +} + +func TestModulePath(t *testing.T) { + for _, test := range modulePathTests { + t.Run(string(test.input), func(t *testing.T) { + result := ModulePath(test.input) + if result != test.expected { + t.Fatalf("ModulePath(%q): %s, want %s", string(test.input), result, test.expected) + } + }) + } +} + +func TestParseVersions(t *testing.T) { + tests := []struct { + desc, input string + ok bool + }{ + // go lines + {desc: "empty", input: "module m\ngo \n", ok: false}, + {desc: "one", input: "module m\ngo 1\n", ok: false}, + {desc: "two", input: "module m\ngo 1.22\n", ok: true}, + {desc: "three", input: "module m\ngo 1.22.333", ok: true}, + {desc: "before", input: "module m\ngo v1.2\n", ok: false}, + {desc: "after", input: "module m\ngo 1.2rc1\n", ok: true}, + {desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false}, + {desc: "alt1", input: "module m\ngo 1.2.3\n", ok: true}, + {desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: true}, + {desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: true}, + {desc: "alt4", input: "module m\ngo 1.2.beta1\n", ok: false}, + } + t.Run("Strict", func(t *testing.T) { + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + if _, err := Parse("gno.mod", []byte(test.input)); err == nil && !test.ok { + t.Error("unexpected success") + } else if err != nil && test.ok { + t.Errorf("unexpected error: %v", err) + } + }) + } + }) +} + +func TestComments(t *testing.T) { + for _, test := range []struct { + desc, input, want string + }{ + { + desc: "comment_only", + input: ` +// a +// b +`, + want: ` +comments before "// a" +comments before "// b" +`, + }, { + desc: "line", + input: ` +// a + +// b +module m // c +// d + +// e +`, + want: ` +comments before "// a" +line before "// b" +line suffix "// c" +comments before "// d" +comments before "// e" +`, + }, { + desc: "cr_removed", + input: "// a\r\r\n", + want: `comments before "// a\r"`, + }, + } { + t.Run(test.desc, func(t *testing.T) { + f, err := Parse("gno.mod", []byte(test.input)) + if err != nil { + t.Fatal(err) + } + + if test.desc == "block" { + panic("hov") + } + + buf := &bytes.Buffer{} + printComments := func(prefix string, cs *modfile.Comments) { + for _, c := range cs.Before { + fmt.Fprintf(buf, "%s before %q\n", prefix, c.Token) + } + for _, c := range cs.Suffix { + fmt.Fprintf(buf, "%s suffix %q\n", prefix, c.Token) + } + for _, c := range cs.After { + fmt.Fprintf(buf, "%s after %q\n", prefix, c.Token) + } + } + + printComments("file", &f.Syntax.Comments) + for _, stmt := range f.Syntax.Stmt { + switch stmt := stmt.(type) { + case *modfile.CommentBlock: + printComments("comments", stmt.Comment()) + case *modfile.Line: + printComments("line", stmt.Comment()) + } + } + + got := strings.TrimSpace(buf.String()) + want := strings.TrimSpace(test.want) + if got != want { + t.Errorf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} + +var addRequireTests = []struct { + desc string + in string + path string + vers string + out string +}{ + { + `existing`, + ` + module m + require x.y/z v1.2.3 + `, + "x.y/z", "v1.5.6", + ` + module m + require x.y/z v1.5.6 + `, + }, + { + `existing2`, + ` + module m + require ( + x.y/z v1.2.3 // first + x.z/a v0.1.0 // first-a + ) + require x.y/z v1.4.5 // second + require ( + x.y/z v1.6.7 // third + x.z/a v0.2.0 // third-a + ) + `, + "x.y/z", "v1.8.9", + ` + module m + + require ( + x.y/z v1.8.9 // first + x.z/a v0.1.0 // first-a + ) + + require x.z/a v0.2.0 // third-a + `, + }, + { + `new`, + ` + module m + require x.y/z v1.2.3 + `, + "x.y/w", "v1.5.6", + ` + module m + require ( + x.y/z v1.2.3 + x.y/w v1.5.6 + ) + `, + }, + { + `new2`, + ` + module m + require x.y/z v1.2.3 + require x.y/q/v2 v2.3.4 + `, + "x.y/w", "v1.5.6", + ` + module m + require x.y/z v1.2.3 + require ( + x.y/q/v2 v2.3.4 + x.y/w v1.5.6 + ) + `, + }, +} + +var addModuleStmtTests = []struct { + desc string + in string + path string + out string +}{ + { + `existing`, + ` + module m + require x.y/z v1.2.3 + `, + "n", + ` + module n + require x.y/z v1.2.3 + `, + }, + { + `new`, + ``, + "m", + ` + module m + `, + }, +} + +var addReplaceTests = []struct { + desc string + in string + oldPath string + oldVers string + newPath string + newVers string + out string +}{ + { + `replace_with_module`, + ` + module m + require x.y/z v1.2.3 + `, + "x.y/z", + "v1.5.6", + "a.b/c", + "v1.5.6", + ` + module m + require x.y/z v1.2.3 + replace x.y/z v1.5.6 => a.b/c v1.5.6 + `, + }, + { + `replace_with_dir`, + ` + module m + require x.y/z v1.2.3 + `, + "x.y/z", + "v1.5.6", + "/path/to/dir", + "", + ` + module m + require x.y/z v1.2.3 + replace x.y/z v1.5.6 => /path/to/dir + `, + }, +} + +var dropRequireTests = []struct { + desc string + in string + path string + out string +}{ + { + `existing`, + ` + module m + require x.y/z v1.2.3 + `, + "x.y/z", + ` + module m + `, + }, + { + `existing2`, + ` + module m + require ( + x.y/z v1.2.3 // first + x.z/a v0.1.0 // first-a + ) + require x.y/z v1.4.5 // second + require ( + x.y/z v1.6.7 // third + x.z/a v0.2.0 // third-a + ) + `, + "x.y/z", + ` + module m + + require x.z/a v0.1.0 // first-a + + require x.z/a v0.2.0 // third-a + `, + }, + { + `not_exists`, + ` + module m + require x.y/z v1.2.3 + `, + "a.b/c", + ` + module m + require x.y/z v1.2.3 + `, + }, +} + +var dropReplaceTests = []struct { + desc string + in string + path string + vers string + out string +}{ + { + `existing`, + ` + module m + require x.y/z v1.2.3 + + replace x.y/z v1.2.3 => a.b/c v1.5.6 + `, + "x.y/z", + "v1.2.3", + ` + module m + require x.y/z v1.2.3 + `, + }, + { + `not_exists`, + ` + module m + require x.y/z v1.2.3 + + replace x.y/z v1.2.3 => a.b/c v1.5.6 + `, + "a.b/c", + "v3.2.1", + ` + module m + require x.y/z v1.2.3 + + replace x.y/z v1.2.3 => a.b/c v1.5.6 + `, + }, +} + +func TestAddRequire(t *testing.T) { + for _, tt := range addRequireTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, func(f *File) error { + err := f.AddRequire(tt.path, tt.vers) + f.Syntax.Cleanup() + return err + }) + }) + } +} + +func TestAddModuleStmt(t *testing.T) { + for _, tt := range addModuleStmtTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, func(f *File) error { + err := f.AddModuleStmt(tt.path) + f.Syntax.Cleanup() + return err + }) + }) + } +} + +func TestAddReplace(t *testing.T) { + for _, tt := range addReplaceTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, func(f *File) error { + f.AddReplace(tt.oldPath, tt.oldVers, tt.newPath, tt.newVers) + f.Syntax.Cleanup() + return nil + }) + }) + } +} + +func TestDropRequire(t *testing.T) { + for _, tt := range dropRequireTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, func(f *File) error { + err := f.DropRequire(tt.path) + f.Syntax.Cleanup() + return err + }) + }) + } +} + +func TestDropReplace(t *testing.T) { + for _, tt := range dropReplaceTests { + t.Run(tt.desc, func(t *testing.T) { + testEdit(t, tt.in, tt.out, func(f *File) error { + err := f.DropReplace(tt.path, tt.vers) + f.Syntax.Cleanup() + return err + }) + }) + } +} + +func testEdit(t *testing.T, in, want string, transform func(f *File) error) *File { + t.Helper() + f, err := Parse("in", []byte(in)) + if err != nil { + t.Fatal(err) + } + g, err := Parse("out", []byte(want)) + if err != nil { + t.Fatal(err) + } + golden := modfile.Format(g.Syntax) + if err := transform(f); err != nil { + t.Fatal(err) + } + out := modfile.Format(f.Syntax) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(out, golden) { + t.Errorf("have:\n%s\nwant:\n%s", out, golden) + } + + return f +} From fa8eb7753dc5396144dd4166123234af3a3a4da1 Mon Sep 17 00:00:00 2001 From: Albert Le Batteux Date: Fri, 6 Oct 2023 17:24:10 +0100 Subject: [PATCH 18/93] chore(amino): improve error readability (#1179) --- tm2/pkg/amino/codec.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm2/pkg/amino/codec.go b/tm2/pkg/amino/codec.go index 6300ae04673..602c7090d91 100644 --- a/tm2/pkg/amino/codec.go +++ b/tm2/pkg/amino/codec.go @@ -532,7 +532,7 @@ func (cdc *Codec) getTypeInfoFromFullnameRLock(fullname string, fopts FieldOptio info, ok := cdc.fullnameToTypeInfo[fullname] if !ok { - err = fmt.Errorf("unrecognized concrete type full name %s of %v", fullname, cdc.fullnameToTypeInfo) + err = fmt.Errorf("amino: unrecognized concrete type full name %s", fullname) cdc.mtx.RUnlock() return } From e144d2697bf7b8c702db99241c02ab32ddb39390 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 02:23:34 +0200 Subject: [PATCH 19/93] chore(deps): Bump golang.org/x/mod from 0.12.0 to 0.13.0 (#1207) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.12.0 to 0.13.0.
Commits
  • 5b69280 modfile: use new go version string format in error message
  • 273ef6c go.mod: update to go 1.18 and x/tools v0.13.0
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/mod&package-manager=go_modules&previous-version=0.12.0&new-version=0.13.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 06329b2f02d..dbb62a34e2d 100644 --- a/go.mod +++ b/go.mod @@ -33,10 +33,10 @@ require ( go.etcd.io/bbolt v1.3.7 go.uber.org/multierr v1.9.0 golang.org/x/crypto v0.13.0 - golang.org/x/mod v0.12.0 + golang.org/x/mod v0.13.0 golang.org/x/net v0.15.0 golang.org/x/term v0.12.0 - golang.org/x/tools v0.6.0 + golang.org/x/tools v0.13.0 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 77c72cfab69..3e9aac9b2e2 100644 --- a/go.sum +++ b/go.sum @@ -197,8 +197,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -242,8 +242,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From b34816b491bbc570a7737d8c70df9bf63f19ca96 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:10:11 +0200 Subject: [PATCH 20/93] chore: improve tests (#1232) --- .../pkg/integration/testdata/gnokey.txtar | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/gno.land/pkg/integration/testdata/gnokey.txtar b/gno.land/pkg/integration/testdata/gnokey.txtar index e4d2c93c0c9..123a0ce291c 100644 --- a/gno.land/pkg/integration/testdata/gnokey.txtar +++ b/gno.land/pkg/integration/testdata/gnokey.txtar @@ -6,27 +6,19 @@ gnoland start ## test1 account should be available on default gnokey query auth/accounts/${USER_ADDR_test1} -cmp stdout gnokey-query-valid.stdout.golden -cmp stderr gnokey-query-valid.stderr.golden +stdout 'height: 0' +stdout 'data: {' +stdout ' "BaseAccount": {' +stdout ' "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5",' +stdout ' "coins": "[0-9]*ugnot",' # dynamic +stdout ' "public_key": null,' +stdout ' "account_number": "0",' +stdout ' "sequence": "0"' +stdout ' }' +stdout '}' +! stderr '.+' # empty ## invalid gnokey command should raise an error ! gnokey query foo/bar -cmp stdout gnokey-query-invalid.stdout.golden -cmp stderr gnokey-query-invalid.stderr.golden - --- gnokey-query-valid.stdout.golden -- -height: 0 -data: { - "BaseAccount": { - "address": "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", - "coins": "9999892000000ugnot", - "public_key": null, - "account_number": "0", - "sequence": "0" - } -} --- gnokey-query-valid.stderr.golden -- --- gnokey-query-invalid.stdout.golden -- -Log: --- gnokey-query-invalid.stderr.golden -- -"gnokey" error: unknown request error +stdout 'Log:' +stderr '"gnokey" error: unknown request error' From dce345f31b8fc6be3e831778983345d36f01e31f Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 11 Oct 2023 13:08:33 -0700 Subject: [PATCH 21/93] fix(make): disable _test.gnoweb temporarily (#1223) --- .github/workflows/gnoland.yml | 8 +++++--- gno.land/Makefile | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 2b38f254a13..95cb5fa8ce0 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -60,7 +60,9 @@ jobs: - _test.gnoland - _test.gnokey - _test.pkgs - #- _test.gnoweb # this test should be rewritten to run an inmemory localnode + # XXX: test broken, should be rewritten to run an inmemory localnode + # Re-add to makefile when fixed. Tracked here: https://github.com/gnolang/gno/issues/1222 + #- _test.gnoweb runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -76,7 +78,7 @@ jobs: export LOG_DIR="${{ runner.temp }}/logs/test-${{ matrix.goversion }}-gnoland" make ${{ matrix.args }} - name: Upload Test Log - if: always() + if: always() uses: actions/upload-artifact@v3 with: name: logs-test-gnoland-go${{ matrix.goversion }} @@ -99,7 +101,7 @@ jobs: uses: codecov/codecov-action@v3 with: directory: ${{ runner.temp }}/coverage - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: diff --git a/gno.land/Makefile b/gno.land/Makefile index e794bb58174..1fd1aaa1f78 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -43,7 +43,9 @@ fmt: ######################################## # Test suite .PHONY: test -test: _test.gnoland _test.gnoweb _test.gnokey _test.pkgs +test: _test.gnoland _test.gnokey _test.pkgs +# XXX: _test.gnoweb is currently disabled. If fixed, re-enable here and in CI. +# https://github.com/gnolang/gno/issues/1222 GOTEST_FLAGS ?= -v -p 1 -timeout=30m From eb27a8f51766bf82f74d47b70b8ac0e29b0c8cf8 Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Thu, 12 Oct 2023 18:12:56 +0200 Subject: [PATCH 22/93] test(gnovm): migrate 'gno build' test to testscript (#1103) Like done for 'gno test', use testscript and txtar files to define the different test cases. The previous test was only testing `gno build` without arguments and files, so this PR adds more cases. Interestingly, the gno files are only used to determine the directories where the 'go build' command will be passed. This means only go file syntax is checked (gno file syntax is ignored, as pictured in `invalid_gno_files.txtar` case). Also a `go.mod` is required or else the command fails. Like the previous test, the new test can be run via ``` $ go test ./gnovm/cmd/gno -v -run Build ```
Contributors' checklist... - [x] 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 - [x] 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/cmd/gno/build_test.go | 18 ++++------- .../gno/testdata/gno_build/empty_dir.txtar | 6 ++++ .../gno_build/invalid_gno_files.txtar | 27 +++++++++++++++++ .../testdata/gno_build/invalid_go_files.txtar | 30 +++++++++++++++++++ .../cmd/gno/testdata/gno_build/no_args.txtar | 6 ++++ .../gno/testdata/gno_build/no_gno_files.txtar | 12 ++++++++ .../gno/testdata/gno_build/no_go_files.txtar | 19 ++++++++++++ .../cmd/gno/testdata/gno_build/no_gomod.txtar | 16 ++++++++++ gnovm/cmd/gno/testdata/gno_build/ok.txtar | 23 ++++++++++++++ 9 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 gnovm/cmd/gno/testdata/gno_build/empty_dir.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/invalid_gno_files.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/no_args.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/no_gno_files.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_build/ok.txtar diff --git a/gnovm/cmd/gno/build_test.go b/gnovm/cmd/gno/build_test.go index 89339ee8a6e..5bb03ef0d35 100644 --- a/gnovm/cmd/gno/build_test.go +++ b/gnovm/cmd/gno/build_test.go @@ -1,17 +1,11 @@ package main -import "testing" +import ( + "testing" -func TestBuildApp(t *testing.T) { - tc := []testMainCase{ - { - args: []string{"build"}, - errShouldBe: "flag: help requested", - }, + "github.com/rogpeppe/go-internal/testscript" +) - // {args: []string{"build", "..."}, stdoutShouldContain: "..."}, - // TODO: auto precompilation - // TODO: error handling - } - testMainCaseRun(t, tc) +func TestBuild(t *testing.T) { + testscript.Run(t, setupTestScript(t, "testdata/gno_build")) } diff --git a/gnovm/cmd/gno/testdata/gno_build/empty_dir.txtar b/gnovm/cmd/gno/testdata/gno_build/empty_dir.txtar new file mode 100644 index 00000000000..d346b6ad46f --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/empty_dir.txtar @@ -0,0 +1,6 @@ +# Run gno build on an empty dir + +gno build . + +! stdout .+ +! stderr .+ diff --git a/gnovm/cmd/gno/testdata/gno_build/invalid_gno_files.txtar b/gnovm/cmd/gno/testdata/gno_build/invalid_gno_files.txtar new file mode 100644 index 00000000000..617e12291be --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/invalid_gno_files.txtar @@ -0,0 +1,27 @@ +# Run gno build with invalid gno files (still success) + +gno build . + +! stdout .+ +! stderr .+ + +-- go.mod -- +module gnobuild + +-- file1.go -- +package file1 + +-- main.gno -- +package main + +invalid + +func main() {} + +-- sub/sub.gno -- +package sub + +invalid + +-- sub/file2.go -- +package file2 diff --git a/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar b/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar new file mode 100644 index 00000000000..6093bfdad00 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar @@ -0,0 +1,30 @@ +# Run gno build with invalid go files + +! gno build . + +! stdout .+ +stderr '\./file1\.go:3:1: syntax error: non-declaration statement outside function body' +stderr '\./\.: build pkg: std go compiler: exit status 1' +stderr 'sub/file2\.go:3:1: syntax error: non-declaration statement outside function body' +stderr '\./sub: build pkg: std go compiler: exit status 1' + +-- go.mod -- +module gnobuild + +-- file1.go -- +package file1 + +invalid1 + +-- main.gno -- +package main + +func main() {} + +-- sub/sub.gno -- +package sub + +-- sub/file2.go -- +package file2 + +invalid2 diff --git a/gnovm/cmd/gno/testdata/gno_build/no_args.txtar b/gnovm/cmd/gno/testdata/gno_build/no_args.txtar new file mode 100644 index 00000000000..b3f68676588 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/no_args.txtar @@ -0,0 +1,6 @@ +# Run gno build without args + +! gno build + +! stdout .+ +stderr 'flag: help requested' diff --git a/gnovm/cmd/gno/testdata/gno_build/no_gno_files.txtar b/gnovm/cmd/gno/testdata/gno_build/no_gno_files.txtar new file mode 100644 index 00000000000..58261e77cda --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/no_gno_files.txtar @@ -0,0 +1,12 @@ +# Run gno build on a dir w/o gno files + +gno build . + +! stdout .+ +! stderr .+ + +-- README -- +Hello world + +-- sub/README -- +Hello world diff --git a/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar b/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar new file mode 100644 index 00000000000..ddc6aec4555 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar @@ -0,0 +1,19 @@ +# Run gno build in a dir without go files + +! gno build . + +! stdout .+ +stderr -count=2 'no Go files in '$WORK +stderr '\./\.: build pkg: std go compiler: exit status 1' +stderr '\./sub: build pkg: std go compiler: exit status 1' + +-- go.mod -- +module gnobuild + +-- main.gno -- +package main + +func main() {} + +-- sub/sub.gno -- +package sub diff --git a/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar b/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar new file mode 100644 index 00000000000..5eb8edeaad9 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar @@ -0,0 +1,16 @@ +# Run gno build on a dir without go.mod + +! gno build . + +! stdout .+ +stderr -count=2 'go: go.mod file not found in current directory or any parent directory' +stderr './.: build pkg: std go compiler: exit status 1' +stderr './sub: build pkg: std go compiler: exit status 1' + +-- main.gno -- +package main + +func main() {} + +-- sub/sub.gno -- +package sub diff --git a/gnovm/cmd/gno/testdata/gno_build/ok.txtar b/gnovm/cmd/gno/testdata/gno_build/ok.txtar new file mode 100644 index 00000000000..9d70fd97904 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_build/ok.txtar @@ -0,0 +1,23 @@ +# Run gno build successfully + +gno build . + +! stdout .+ +! stderr .+ + +-- go.mod -- +module gnobuild + +-- file1.go -- +package file1 + +-- main.gno -- +package main + +func main() {} + +-- sub/sub.gno -- +package sub + +-- sub/file2.go -- +package file2 From a2971bf011b2239cc4d368f586cfe9650c538ea7 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Thu, 12 Oct 2023 22:09:52 +0530 Subject: [PATCH 23/93] feat: `gno test` support `/...` pattern (#1078) BREAKING CHANGE: Altered behavior of the `gno test` command Adds support for `/...` pattern in `gno test` command. Now args can have `/...` pattern in the directory path. Using `gno test ./path/to/pkg` would trigger the execution of test files solely within the specified package directory, excluding any subdirectories like `./path/to/pkg/subpkg`. To execute test files within subdirectories as well, use `gno test ./path/to/pkg/...` It supports all variations of `/...` such as `./path/.../pkg`, `./.../pkg`, ,`./.../path/...` and more
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] 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).
--- .github/workflows/examples.yml | 2 +- examples/Makefile | 4 +- gnovm/cmd/gno/test.go | 6 +- .../cmd/gno/testdata/gno_test/empty_dir.txtar | 5 + .../gno/testdata/gno_test/empty_gno1.txtar | 2 +- .../gno/testdata/gno_test/error_correct.txtar | 2 +- .../testdata/gno_test/failing_filetest.txtar | 4 +- gnovm/cmd/gno/testdata/gno_test/minim1.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/minim2.txtar | 2 +- gnovm/cmd/gno/testdata/gno_test/minim3.txtar | 2 +- .../testdata/gno_test/output_correct.txtar | 2 +- .../gno/testdata/gno_test/output_sync.txtar | 2 +- .../gno/testdata/gno_test/realm_correct.txtar | 2 +- .../gno/testdata/gno_test/realm_sync.txtar | 2 +- .../testdata/gno_test/valid_filetest.txtar | 4 +- .../gno/testdata/gno_test/valid_test.txtar | 7 +- gnovm/cmd/gno/util.go | 84 +++++ gnovm/cmd/gno/util_test.go | 297 ++++++++++++++++++ 18 files changed, 411 insertions(+), 20 deletions(-) create mode 100644 gnovm/cmd/gno/util_test.go diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d4b3079d612..b17c66d8e5a 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -50,7 +50,7 @@ jobs: with: go-version: ${{ matrix.goversion }} - run: go install -v ./gnovm/cmd/gno - - run: go run ./gnovm/cmd/gno test --verbose ./examples + - run: go run ./gnovm/cmd/gno test --verbose ./examples/... lint: strategy: fail-fast: false diff --git a/examples/Makefile b/examples/Makefile index 5075df198ac..f20072d9df2 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -19,7 +19,7 @@ build: precompile .PHONY: test test: - go run ../gnovm/cmd/gno test --verbose . + go run ../gnovm/cmd/gno test --verbose ./... .PHONY: lint lint: @@ -27,7 +27,7 @@ lint: .PHONY: test.sync test.sync: - go run ../gnovm/cmd/gno test --verbose --update-golden-tests . + go run ../gnovm/cmd/gno test --verbose --update-golden-tests ./... .PHONY: clean clean: diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 8cacfd5623b..85fe3f7ee7d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -50,7 +50,7 @@ func newTestCmd(io *commands.IO) *commands.Command { 'gno test' recompiles each package along with any files with names matching the file pattern "*_test.gno" or "*_filetest.gno". -The only supported for now is a directory (relative or absolute). +The can be directory or file path (relative or absolute). - "*_test.gno" files work like "*_test.go" files, but they contain only test functions. Benchmark and fuzz functions aren't supported yet. Similarly, only @@ -182,9 +182,9 @@ func execTest(cfg *testCfg, args []string, io *commands.IO) error { cfg.rootDir = guessRootDir() } - paths, err := gnoPackagesFromArgs(args) + paths, err := targetsFromPatterns(args) if err != nil { - return fmt.Errorf("list package paths from args: %w", err) + return fmt.Errorf("list targets from patterns: %w", err) } if len(paths) == 0 { io.ErrPrintln("no packages to test") diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar index 73f0da72dfe..ffed64ab9c7 100644 --- a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar @@ -2,5 +2,10 @@ gno test . +! stdout .+ +stderr '[no test files]' + +gno test ./... + ! stdout .+ stderr 'no packages to test' diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar index ae73b3ab275..cc673bb38ff 100644 --- a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr '\? \./\. \[no test files\]' +stderr '\? \. \[no test files\]' ! gno test --precompile . diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar index c7d3187424c..a66d831b48c 100644 --- a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . stdout 'Machine\.RunMain\(\) panic: oups' stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar index bc3efc1a8c9..c739c1ce328 100644 --- a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar @@ -9,8 +9,8 @@ stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop' ! gno test -verbose --precompile . stdout 'Machine.RunMain\(\) panic: beep boop' -stderr '=== PREC \./\.' -stderr '=== BUILD \./\.' +stderr '=== PREC \.' +stderr '=== BUILD \.' stderr '=== RUN file/failing_filetest.gno' stderr 'panic: fail on failing_filetest.gno: got unexpected error: beep boop' diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/gno_test/minim1.txtar index 231ef4a270c..b0a77186086 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim1.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr '\? \./\. \[no test files\]' +stderr '\? \. \[no test files\]' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/gno_test/minim2.txtar index 038dfd19289..3c4d1d085f0 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim2.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/gno_test/minim3.txtar index 8e657104801..ac8ae0c41d4 100644 --- a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/minim3.txtar @@ -3,7 +3,7 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- minim.gno -- package minim diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar index ce12874f669..4e5495ab839 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- package main diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar index 85fec4ab316..b21db788924 100644 --- a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar @@ -5,7 +5,7 @@ gno test -verbose . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar index f376e61d9a4..c3d3b983e34 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar @@ -5,7 +5,7 @@ gno test -verbose . ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- x_filetest.gno -- // PKGPATH: gno.land/r/x diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar index 918fb0b88c9..236e69f8641 100644 --- a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar @@ -5,7 +5,7 @@ gno test -verbose . -update-golden-tests ! stdout .+ # stdout should be empty stderr '=== RUN file/x_filetest.gno' stderr '--- PASS: file/x_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' cmp x_filetest.gno x_filetest.gno.golden diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar index 51b5f323654..545607a9082 100644 --- a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar @@ -3,14 +3,14 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' gno test -verbose . ! stdout .+ stderr '=== RUN file/valid_filetest.gno' stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar index cb5f7286f60..9590626776c 100644 --- a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar +++ b/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar @@ -3,7 +3,12 @@ gno test . ! stdout .+ -stderr 'ok \./\. \d\.\d\ds' +stderr 'ok \. \d\.\d\ds' + +gno test ./... + +! stdout .+ +stderr 'ok \. \d\.\d\ds' -- valid.gno -- package valid diff --git a/gnovm/cmd/gno/util.go b/gnovm/cmd/gno/util.go index 8288539c97b..73ee0f0323b 100644 --- a/gnovm/cmd/gno/util.go +++ b/gnovm/cmd/gno/util.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "regexp" "strings" "time" @@ -105,6 +106,89 @@ func gnoPackagesFromArgs(args []string) ([]string, error) { return paths, nil } +// targetsFromPatterns returns a list of target paths that match the patterns. +// Each pattern can represent a file or a directory, and if the pattern +// includes "/...", the "..." is treated as a wildcard, matching any string. +// Intended to be used by gno commands such as `gno test`. +func targetsFromPatterns(patterns []string) ([]string, error) { + paths := []string{} + for _, p := range patterns { + var match func(string) bool + patternLookup := false + dirToSearch := p + + // Check if the pattern includes `/...` + if strings.Contains(p, "/...") { + index := strings.Index(p, "/...") + if index != -1 { + dirToSearch = p[:index] // Extract the directory path to search + } + match = matchPattern(strings.TrimPrefix(p, "./")) + patternLookup = true + } + + info, err := os.Stat(dirToSearch) + if err != nil { + return nil, fmt.Errorf("invalid file or package path: %w", err) + } + + // If the pattern is a file or a directory + // without `/...`, add it to the list. + if !info.IsDir() || !patternLookup { + paths = append(paths, p) + continue + } + + // the pattern is a dir containing `/...`, walk the dir recursively and + // look for directories containing at least one .gno file and match pattern. + visited := map[string]bool{} // used to run the builder only once per folder. + err = filepath.WalkDir(dirToSearch, func(curpath string, f fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("%s: walk dir: %w", dirToSearch, err) + } + // Skip directories and non ".gno" files. + if f.IsDir() || !isGnoFile(f) { + return nil + } + + parentDir := filepath.Dir(curpath) + if _, found := visited[parentDir]; found { + return nil + } + + visited[parentDir] = true + if match(parentDir) { + paths = append(paths, parentDir) + } + + return nil + }) + if err != nil { + return nil, err + } + } + return paths, nil +} + +// matchPattern(pattern)(name) reports whether +// name matches pattern. Pattern is a limited glob +// pattern in which '...' means 'any string' and there +// is no other special syntax. +// Simplified version of go source's matchPatternInternal +// (see $GOROOT/src/cmd/internal/pkgpattern) +func matchPattern(pattern string) func(name string) bool { + re := regexp.QuoteMeta(pattern) + re = strings.Replace(re, `\.\.\.`, `.*`, -1) + // Special case: foo/... matches foo too. + if strings.HasSuffix(re, `/.*`) { + re = re[:len(re)-len(`/.*`)] + `(/.*)?` + } + reg := regexp.MustCompile(`^` + re + `$`) + return func(name string) bool { + return reg.MatchString(name) + } +} + func fmtDuration(d time.Duration) string { return fmt.Sprintf("%.2fs", d.Seconds()) } diff --git a/gnovm/cmd/gno/util_test.go b/gnovm/cmd/gno/util_test.go new file mode 100644 index 00000000000..9e9659bfe4f --- /dev/null +++ b/gnovm/cmd/gno/util_test.go @@ -0,0 +1,297 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMatchPattern(t *testing.T) { + tests := []struct { + pattern string + names []string + expected []bool + }{ + { + pattern: "foo", + names: []string{"foo", "bar", "baz", "foo/bar"}, + expected: []bool{true, false, false, false}, + }, + { + pattern: "foo/...", + names: []string{"foo", "foo/bar", "foo/bar/baz", "bar", "baz"}, + expected: []bool{true, true, true, false, false}, + }, + { + pattern: "foo/bar/...", + names: []string{"foo/bar", "foo/bar/baz", "foo/baz/bar", "foo", "bar"}, + expected: []bool{true, true, false, false, false}, + }, + { + pattern: "foo/.../baz", + names: []string{"foo/bar", "foo/bar/baz", "foo/baz/bar", "foo", "bar"}, + expected: []bool{false, true, false, false, false}, + }, + { + pattern: "foo/.../baz/...", + names: []string{"foo/bar/baz", "foo/baz/bar", "foo/bar/baz/qux", "foo/baz/bar/qux"}, + expected: []bool{true, false, true, false}, + }, + { + pattern: "...", + names: []string{"foo", "bar", "baz", "foo/bar", "foo/bar/baz"}, + expected: []bool{true, true, true, true, true}, + }, + { + pattern: ".../bar", + names: []string{"foo", "bar", "baz", "foo/bar", "foo/bar/baz"}, + expected: []bool{false, false, false, true, false}, + }, + } + + for _, test := range tests { + t.Run(test.pattern, func(t *testing.T) { + matchFunc := matchPattern(test.pattern) + for i, name := range test.names { + res := matchFunc(name) + assert.Equal(t, test.expected[i], res, "Expected: %v, Got: %v", test.expected[i], res) + } + }) + } +} + +func TestTargetsFromPatterns(t *testing.T) { + tmpDir := t.TempDir() + createGnoPackages(t, tmpDir) + + for _, tc := range []struct { + desc string + in, expected []string + errorShouldContain string + }{ + { + desc: "valid1", + in: []string{ + tmpDir, + }, + expected: []string{ + tmpDir, + }, + }, + { + desc: "valid2", + in: []string{ + tmpDir + "/foo", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + }, + }, + { + desc: "valid_recursive1", + in: []string{ + tmpDir + "/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "baz"), + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "bar", "quux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "valid_recursive2", + in: []string{ + tmpDir + "/foo/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "valid_recursive2", + in: []string{ + tmpDir + "/.../qux", + }, + expected: []string{ + filepath.Join(tmpDir, "foo", "qux"), + }, + }, + { + desc: "valid_recursive3", + in: []string{ + tmpDir + "/.../qux/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo", "qux"), + filepath.Join(tmpDir, "foo", "qux", "corge"), + }, + }, + { + desc: "multiple_input", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar", + tmpDir + "/baz", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "baz"), + }, + }, + { + desc: "mixed_input1", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar/...", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "bar", "quux"), + }, + }, + { + desc: "mixed_input2", + in: []string{ + tmpDir + "/foo", + tmpDir + "/bar/...", + tmpDir + "/baz/baz.gno", + }, + expected: []string{ + filepath.Join(tmpDir, "foo"), + filepath.Join(tmpDir, "bar"), + filepath.Join(tmpDir, "bar", "quux"), + filepath.Join(tmpDir, "baz", "baz.gno"), + }, + }, + { + desc: "not_exists1", + in: []string{ + tmpDir + "/notexists", // dir path + }, + errorShouldContain: "no such file or directory", + }, + { + desc: "not_exists2", + in: []string{ + tmpDir + "/foo/bar.gno", // file path + }, + errorShouldContain: "no such file or directory", + }, + { + desc: "not_exists3", // mixed + in: []string{ + tmpDir + "/foo", // exists + tmpDir + "/notexists", // not exists + }, + errorShouldContain: "no such file or directory", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + targets, err := targetsFromPatterns(tc.in) + if tc.errorShouldContain != "" { + assert.ErrorContains(t, err, tc.errorShouldContain) + return + } + assert.NoError(t, err) + require.Equal(t, len(tc.expected), len(targets)) + for _, tr := range targets { + assert.Contains(t, tc.expected, tr) + } + }) + } +} + +func createGnoPackages(t *testing.T, tmpDir string) { + t.Helper() + + type file struct { + name, data string + } + // Gno pkgs to create + pkgs := []struct { + dir string + files []file + }{ + // pkg 'foo', 'bar' and 'baz' + { + dir: filepath.Join(tmpDir, "foo"), + files: []file{ + { + name: "foo.gno", + data: `package foo`, + }, + }, + }, + { + dir: filepath.Join(tmpDir, "bar"), + files: []file{ + { + name: "bar.gno", + data: `package bar`, + }, + }, + }, + { + dir: filepath.Join(tmpDir, "baz"), + files: []file{ + { + name: "baz.gno", + data: `package baz`, + }, + }, + }, + + // pkg inside 'foo' pkg + { + dir: filepath.Join(tmpDir, "foo", "qux"), + files: []file{ + { + name: "qux.gno", + data: `package qux`, + }, + }, + }, + + // pkg inside 'bar' pkg + { + dir: filepath.Join(tmpDir, "bar", "quux"), + files: []file{ + { + name: "quux.gno", + data: `package quux`, + }, + }, + }, + + // pkg inside 'foo/qux' pkg + { + dir: filepath.Join(tmpDir, "foo", "qux", "corge"), + files: []file{ + { + name: "corge.gno", + data: `package corge`, + }, + }, + }, + } + + // Create pkgs + for _, p := range pkgs { + err := os.MkdirAll(p.dir, 0o700) + require.NoError(t, err) + for _, f := range p.files { + err = os.WriteFile(filepath.Join(p.dir, f.name), []byte(f.data), 0o644) + require.NoError(t, err) + } + } +} From 89428c58b088968e568cab067a7e3ba1b775477f Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 12 Oct 2023 09:52:29 -0700 Subject: [PATCH 24/93] docs: improve README and CONTRIBUTING, add reference to testing guide (#1199) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- CONTRIBUTING.md | 80 ++++++++++++++----- README.md | 85 ++++++++++++++------- docs/{testing_guide.md => testing-guide.md} | 0 3 files changed, 117 insertions(+), 48 deletions(-) rename docs/{testing_guide.md => testing-guide.md} (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1e23f18273..a77557e0a36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ -# Contributing to GNO +# Contributing to Gno -Thank you for looking to contribute to the GNO project. -We appreciate every open-source contribution, as it helps us improve and enhance gno for the benefit of the community. +Thank you for looking to contribute to the Gno project. +We appreciate every open-source contribution, as it helps us improve and enhance Gno for the benefit of the community. This document outlines some basic pointers on making your future contribution a great experience. It outlines basic PR etiquette employed by the core gno team. It lays out coding styles, simple how-to guides and tools to get you up and @@ -20,7 +20,7 @@ Likewise, if you have an idea on how to improve this guide, go for it as well. - [Testing](#testing) - [Running locally](#running-locally) - [Running test workflows](#running-test-workflows) - - [Testing GNO code](#testing-gno-code) + - [Testing Gno code](#testing-gno-code) - [Repository Structure](#repository-structure) - [How do I?](#how-do-i) - [How do I submit changes?](#how-do-i-submit-changes) @@ -41,9 +41,9 @@ Likewise, if you have an idea on how to improve this guide, go for it as well. - **[Discord](https://discord.gg/YFtMjWwUN7)** - we are very active on Discord. Join today and start discussing all things gno with fellow engineers and enthusiasts. -- **[Awesome GNO](https://github.com/gnolang/awesome-gno)** - check out the list of compiled resources for helping you +- **[Awesome Gno](https://github.com/gnolang/awesome-gno)** - check out the list of compiled resources for helping you understand the gno ecosystem -- **[Active Staging](https://gno.land/)** - use the currently available staging environment to play around with a +- **[Active Staging](https://staging.gno.land/)** - use the currently available staging environment to play around with a production network. If you want to interact with a local instance, refer to the [Local Setup](#local-setup) guide. - **[Twitter](https://twitter.com/_gnoland)** - follow us on Twitter to get the latest scoop - **[Telegram](https://t.me/gnoland)** - join our official Telegram group to start a conversation about gno @@ -58,7 +58,6 @@ The primary tech stack for working on the repository: - Go (version 1.20+) - make (for using Makefile configurations) -- Docker (for using the official Docker setup files) It is recommended to work on a Unix environment, as most of the tooling is built around ready-made tools in Unix (WSL2 for Windows / Linux / macOS). @@ -70,8 +69,11 @@ with `make install_gno`. Additionally, you can also configure your editor to recognize `.gno` files as `.go` files, to get the benefit of syntax highlighting. -Currently, we support a [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=harry-hov.gno) extension -(eventually official in the future) for Gnolang. +#### Visual Studio Code + +There currently is an unofficial [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=harry-hov.gno) +extension (primarily developed by a core team member) for working with `*.gno` +files. #### ViM Support @@ -122,9 +124,36 @@ Clone the repo: `git clone https://github.com/gnolang/gno.git` Build / install base commands: -`make build ` +`make install` + +If you haven't already, you may need to add the directory where [`go install` +places its binaries](https://pkg.go.dev/cmd/go#hdr-Compile_and_install_packages_and_dependencies) +to your `PATH`. If you haven't configured `GOBIN` or `GOPATH` differently, this +command should suffice: + +``` +echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.profile +source ~/.profile # reload ~/.profile in the current shell +``` + +After that, you should be good to go to use `gno` and `gnokey`, straight from +your command line! The following commands should list the help messages for +each: -That’s it! +```console +$ gno --help +USAGE + [flags] [...] + +Runs the gno development toolkit +[...] +$ gnokey --help +USAGE + [flags] [...] + +Manages private keys for the node +[...] +``` ### Testing @@ -151,7 +180,7 @@ To run the entire test suite through workflow files, run the following command: act -v -j go-test -#### Testing GNO code +#### Testing Gno code If you wish to test a `.gno` Realm or Package, you can utilize the `gno` tool. @@ -169,24 +198,35 @@ subcommands by running: gno --help +#### Adding new tests + +Most packages will follow the convention established with Go: each package +contains within its file many files suffixed with `_test.go` which test its +functionality. As a general rule, you should follow this convention, and in +every PR you make you should ensure all the code you added is appropriately +covered by tests ([Codecov](https://about.codecov.io/) will loudly complain in +your PR's comments if you don't). + +Additionally, we have a few testing systems that stray from this general rule; +at the time of writing, these are for integration tests and language tests. You +can find more documentation about them [on this guide](gno/docs/testing-guide.md). + ### Repository Structure The repository structure can seem tricky at first, but it’s simple if you consider the philosophy that the gno project -employs (check out [PHILOSOPHY.md](https://github.com/gnolang/gno/blob/master/PHILOSOPHY.md)). +employs (check out [PHILOSOPHY.md](./PHILOSOPHY.md)). The gno project currently favors a mono-repo structure, as it’s easier to manage contributions and keep everyone aligned. In the future, this may change, but in the meantime the majority of gno resources and source code will be centralized here. -- `cmd` - contains the base command implementations for tools like `gnokey`, `gnotxport`, etc. The actual underlying - logic is located within the `pkgs` subdirectories. - `examples` - contains the example `.gno` realms and packages. This is the central point for adding user-defined realms and packages. -- `gnoland` - contains the base source code for bootstrapping the Gnoland node -- `pkgs` - contains the dev-audited packages used throughout the gno codebase -- `stdlibs` - contains the standard library packages used (imported) in `.gno` Smart Contracts. These packages are - themselves `.gno` files. -- `tests` - contains the standard language tests for Gnolang +- `gno.land` - contains the base source code for bootstrapping the Gnoland node, + using `tm2` and `gnovm`. +- `gnovm` - contains the implementation of the Gno programming language and its + Virtual Machine, together with their standard libraries and tests. +- `tm2` - contains a fork of the [Tendermint consensus engine](https://docs.tendermint.com/v0.34/introduction/what-is-tendermint.html) with different expectations. ## How do I? diff --git a/README.md b/README.md index 99634f90a0d..618c3c3f01d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,64 @@ # Gno -At first, there was Bitcoin, out of entropy soup of the greater All. -Then, there was Ethereum, which was created in the likeness of Bitcoin, -but made Turing complete. +> At first, there was Bitcoin, out of entropy soup of the greater All. +> Then, there was Ethereum, which was created in the likeness of Bitcoin, +> but made Turing complete. +> +> Among these were Tendermint and Cosmos to engineer robust PoS and IBC. +> Then came Gno upon Cosmos and there spring forth Gnoland, +> simulated by the Gnomes of the Greater Resistance. + +Gno is an interpreted and fully-deterministic implementation of the Go +programming language, designed to build succint and composable smart contracts. +The first blockchain to use it is Gno.land, a +[Proof of Contribution](./docs/proof-of-contribution.md)-based chain, backed by +a variation of the [Tendermint](https://docs.tendermint.com/v0.34/introduction/what-is-tendermint.html) +consensus engine. -Among these were Tendermint and Cosmos to engineer robust PoS and IBC. -Then came Gno upon Cosmos and there spring forth Gnoland, -simulated by the Gnomes of the Greater Resistance. +## Getting started + +If you haven't already, take a moment to check out our [website](https://gno.land/). + +> The website is a deployment of our [gnoweb](./gno.land/cmd/gnoweb) frontend; you +> can use it to check out [some](https://test3.gno.land/r/demo/boards) [example](https://test3.gno.land/r/gnoland/blog) +> [contracts](https://test3.gno.land/r/demo/users). +> +> Use the `[source]` button in the header to inspect the program's source; use +> the `[help]` button to view how you can use [`gnokey`](./gno.land/cmd/gnokey) +> to interact with the chain from your command line. + +If you have already played around with the website, use our +[Getting Started](https://github.com/gnolang/getting-started) guide to learn how +to write and deploy your first smart contract. No local set-up required! + +Once you're done, learn how to set up your local environment with the +[quickstart guide](./examples/gno.land/r/demo/boards/README.md) and the +[contributing guide](./CONTRIBUTING.md). -## Discover +You can find out more existing tools & documentation for Gno on our +[awesome-gno](https://github.com/gnolang/awesome-gno) repository. +We look forward to seeing your first PR! + +## Repository structure * [examples](./examples) - smart-contract examples and guides for new Gno developers. * [gnovm](./gnovm) - GnoVM and Gnolang. * [gno.land](./gno.land) - Gno.land blockchain and tools. * [tm2](./tm2) - Tendermint2. -## Getting started - -Start your journey with Gno.land by: -- using the [`gnoweb`](./gno.land/cmd/gnoweb) interface on the [latest testnet (test3.gno.land)](https://test3.gno.land/), -- sending transactions with [`gnokey`](./gno.land/cmd/gnokey), -- writing smart-contracts with [`gno` (ex `gnodev`)](./gnovm/cmd/gno). - -Also, see the [quickstart guide](https://github.com/gnolang/gno/blob/master/examples/gno.land/r/demo/boards/README.md). +## Socials & Contact -## Contact - - * Discord: https://discord.gg/YFtMjWwUN7 <-- join now - * Gnoland: https://gno.land/r/demo/boards:testboard - * Telegram: https://t.me/gnoland - * Twitter: https://twitter.com/_gnoland +* [**Discord**](https://discord.gg/YFtMjWwUN7): good for general chat-based + conversations, as well as for asking support on developing with Gno. +* [**Reddit**](https://www.reddit.com/r/gnoland): more "permanent" and + forum-style discussions. Feel free to post anything Gno-related, as well as + any question related to Gno programming! +* [**Telegram**](https://t.me/gnoland): unofficial Telegram group. +* [**Twitter**](https://twitter.com/_gnoland): official Twitter account. Follow + us to know about new developments, events & official announcements about Gno! +* [**YouTube**](https://www.youtube.com/@_gnoland): here we post all of our + video content, like workshops, talks and public development calls. Follow + along on our development journey!
Short doc about all the commands @@ -52,28 +81,28 @@ Also, see the [quickstart guide](https://github.com/gnolang/gno/blob/master/exam
CI/CD/Tools badges and links GitHub Actions: - + * [![gno.land](https://github.com/gnolang/gno/actions/workflows/gnoland.yml/badge.svg)](https://github.com/gnolang/gno/actions/workflows/gnoland.yml) * [![gnovm](https://github.com/gnolang/gno/actions/workflows/gnovm.yml/badge.svg)](https://github.com/gnolang/gno/actions/workflows/gnovm.yml) * [![tm2](https://github.com/gnolang/gno/actions/workflows/tm2.yml/badge.svg)](https://github.com/gnolang/gno/actions/workflows/tm2.yml) * [![examples](https://github.com/gnolang/gno/actions/workflows/examples.yml/badge.svg)](https://github.com/gnolang/gno/actions/workflows/examples.yml) * [![docker](https://github.com/gnolang/gno/actions/workflows/docker.yml/badge.svg)](https://github.com/gnolang/gno/actions/workflows/docker.yml) - + Codecov: - + * General: [![codecov](https://codecov.io/gh/gnolang/gno/branch/master/graph/badge.svg?token=HPP82HR1P4)](https://codecov.io/gh/gnolang/gno) * tm2: [![codecov](https://codecov.io/gh/gnolang/gno/branch/master/graph/badge.svg?token=HPP82HR1P4&flag=tm2)](https://codecov.io/gh/gnolang/gno/tree/master/tm2) * gnovm: [![codecov](https://codecov.io/gh/gnolang/gno/branch/master/graph/badge.svg?token=HPP82HR1P4&flag=gnovm)](https://codecov.io/gh/gnolang/gno/tree/master/gnovm) * gno.land: [![codecov](https://codecov.io/gh/gnolang/gno/branch/master/graph/badge.svg?token=HPP82HR1P4&flag=gno.land)](https://codecov.io/gh/gnolang/gno/tree/master/gno.land) * examples: TODO - + Go Report Card: - + * [![Go Report Card](https://goreportcard.com/badge/github.com/gnolang/gno)](https://goreportcard.com/report/github.com/gnolang/gno) * tm2, gnovm, gno.land: TODO (blocked by tm2 split, because we need go mod workspaces) - + Pkg.go.dev - + * [![Go Reference](https://pkg.go.dev/badge/github.com/gnolang/gno.svg)](https://pkg.go.dev/github.com/gnolang/gno) * TODO: host custom docs on gh-pages, to bypass license limitation
diff --git a/docs/testing_guide.md b/docs/testing-guide.md similarity index 100% rename from docs/testing_guide.md rename to docs/testing-guide.md From 3f2f5a2b9e2446ea648f35f5e0bef51f0eda0520 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 19 Oct 2023 22:10:05 +0100 Subject: [PATCH 25/93] ci: add workflow for automatic monthly snapshots (#1260) Fixes #1130 --- .github/workflows/monthly-snapshots.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/monthly-snapshots.yml diff --git a/.github/workflows/monthly-snapshots.yml b/.github/workflows/monthly-snapshots.yml new file mode 100644 index 00000000000..23eb4629545 --- /dev/null +++ b/.github/workflows/monthly-snapshots.yml @@ -0,0 +1,22 @@ +name: Monthly Snapshots + +on: + schedule: + - cron: '0 0 1 * *' + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Generate tag name + id: tag_name + run: echo "::set-output name=tag_name::v0.0.1-dev.$(date +'%Y.%m.%d')" + - name: Release + uses: softprops/action-gh-release@v1 + with: + generate_release_notes: true + prerelease: true + tag_name: '${{ steps.tag_name.outputs.tag_name }}' From 09dfe6ecb1b9cae4f2c2b9e130395cad834001a1 Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Thu, 19 Oct 2023 23:49:04 +0200 Subject: [PATCH 26/93] test(gno build): remove exit code assertion (#1261) Closes #1258 Can't explain why but Jae has exit code 2 where most people have exit code 1. Decided to remove the code assertion since it doesn't really matter.
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/cmd/gno/testdata/gno_build/invalid_go_files.txtar | 4 ++-- gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar | 4 ++-- gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar b/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar index 6093bfdad00..a7c8b51af49 100644 --- a/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar +++ b/gnovm/cmd/gno/testdata/gno_build/invalid_go_files.txtar @@ -4,9 +4,9 @@ ! stdout .+ stderr '\./file1\.go:3:1: syntax error: non-declaration statement outside function body' -stderr '\./\.: build pkg: std go compiler: exit status 1' +stderr '\./\.: build pkg: std go compiler' stderr 'sub/file2\.go:3:1: syntax error: non-declaration statement outside function body' -stderr '\./sub: build pkg: std go compiler: exit status 1' +stderr '\./sub: build pkg: std go compiler' -- go.mod -- module gnobuild diff --git a/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar b/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar index ddc6aec4555..46559610ccf 100644 --- a/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar +++ b/gnovm/cmd/gno/testdata/gno_build/no_go_files.txtar @@ -4,8 +4,8 @@ ! stdout .+ stderr -count=2 'no Go files in '$WORK -stderr '\./\.: build pkg: std go compiler: exit status 1' -stderr '\./sub: build pkg: std go compiler: exit status 1' +stderr '\./\.: build pkg: std go compiler' +stderr '\./sub: build pkg: std go compiler' -- go.mod -- module gnobuild diff --git a/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar b/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar index 5eb8edeaad9..9e6cad05664 100644 --- a/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar +++ b/gnovm/cmd/gno/testdata/gno_build/no_gomod.txtar @@ -4,8 +4,8 @@ ! stdout .+ stderr -count=2 'go: go.mod file not found in current directory or any parent directory' -stderr './.: build pkg: std go compiler: exit status 1' -stderr './sub: build pkg: std go compiler: exit status 1' +stderr './.: build pkg: std go compiler' +stderr './sub: build pkg: std go compiler' -- main.gno -- package main From 0600d418b4c5a1c970575a1b700d9ab093763b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 19 Oct 2023 18:23:27 -0400 Subject: [PATCH 27/93] feat: add file-based transaction indexing (#546) # Description This PR introduces file-based transaction indexing. As discussed in #275, transaction index parsing should be done as a separate process from the main node, meaning other services can be instantiated to index transactions as readers. The general architecture of the transaction indexers in this PR can be described with the following image: Architecture Each concrete indexer implementation decides how to handle transaction events, and where to store them. Independent processes from the indexers themselves read these events (by parsing files, logs, executing RPC queries...). ## File Indexer The `file` transaction indexer that is included in this PR utilizes `autofile.Group`s to write down transaction events. Users can now specify to use the file-based indexer with the following added flags to the `gnoland` command: - `--tx-indexer-type` - specify the type of indexer (none is default) - `--tx-indexer-path` - path for the file-based tx indexer # Changes include - [ ] Bugfix (non-breaking change that solves an issue) - [ ] Hotfix (change that solves an urgent issue, and requires immediate attention) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (change that is not backwards-compatible and/or changes current functionality) # Checklist (for contributors) - [x] I have assigned this PR to myself - [x] I have added at least 1 reviewer - [x] I have added the relevant labels - [ ] I have updated the official documentation - [x] I have added sufficient documentation in code # Testing - [x] I have tested this code with the official test suite - [x] I have tested this code manually ## Manual tests - Manually executed transactions and verified they were saved to disk. - Added unit tests that cover all added functionality. # Additional comments - [Relevant tendermint2 issue](https://github.com/tendermint/tendermint2/issues/2) - Resolves #275 EDIT: After comments from @jaekwon, this `Indexer` functionality has been renamed to `EventStore`, and work on an independent indexer process (process that can read from the event store) will begin soon that will offer indexing functionality cc @ilgooz --- gno.land/cmd/gnoland/start.go | 62 +++++++ tm2/pkg/autofile/group.go | 21 ++- tm2/pkg/bft/config/config.go | 34 ++-- tm2/pkg/bft/node/node.go | 125 +++++++------- tm2/pkg/bft/rpc/core/pipe.go | 12 +- tm2/pkg/bft/state/eventstore/file/file.go | 92 ++++++++++ .../bft/state/eventstore/file/file_test.go | 140 +++++++++++++++ tm2/pkg/bft/state/eventstore/mock_test.go | 89 ++++++++++ tm2/pkg/bft/state/eventstore/null/null.go | 35 ++++ tm2/pkg/bft/state/eventstore/store.go | 24 +++ tm2/pkg/bft/state/eventstore/store_service.go | 84 +++++++++ .../state/eventstore/store_service_test.go | 159 ++++++++++++++++++ tm2/pkg/bft/state/eventstore/types/config.go | 29 ++++ .../bft/state/eventstore/types/config_test.go | 45 +++++ tm2/pkg/bft/state/txindex/indexer.go | 18 -- tm2/pkg/bft/state/txindex/indexer_service.go | 31 ---- tm2/pkg/bft/state/txindex/null/null.go | 31 ---- tm2/pkg/p2p/node_info.go | 3 +- 18 files changed, 865 insertions(+), 169 deletions(-) create mode 100644 tm2/pkg/bft/state/eventstore/file/file.go create mode 100644 tm2/pkg/bft/state/eventstore/file/file_test.go create mode 100644 tm2/pkg/bft/state/eventstore/mock_test.go create mode 100644 tm2/pkg/bft/state/eventstore/null/null.go create mode 100644 tm2/pkg/bft/state/eventstore/store.go create mode 100644 tm2/pkg/bft/state/eventstore/store_service.go create mode 100644 tm2/pkg/bft/state/eventstore/store_service_test.go create mode 100644 tm2/pkg/bft/state/eventstore/types/config.go create mode 100644 tm2/pkg/bft/state/eventstore/types/config_test.go delete mode 100644 tm2/pkg/bft/state/txindex/indexer.go delete mode 100644 tm2/pkg/bft/state/txindex/indexer_service.go delete mode 100644 tm2/pkg/bft/state/txindex/null/null.go diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index b2134d86ea9..3914cc7775c 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "flag" "fmt" "path/filepath" @@ -17,6 +18,9 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" "github.com/gnolang/gno/tm2/pkg/bft/privval" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/null" + eventstorecfg "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" bft "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -35,6 +39,9 @@ type startCfg struct { rootDir string genesisMaxVMCycles int64 config string + + txEventStoreType string + txEventStorePath string } func newStartCmd(io *commands.IO) *commands.Command { @@ -116,6 +123,29 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "", "config file (optional)", ) + + fs.StringVar( + &c.txEventStoreType, + "tx-event-store-type", + null.EventStoreType, + fmt.Sprintf( + "type of transaction event store [%s]", + strings.Join( + []string{ + null.EventStoreType, + file.EventStoreType, + }, + ", ", + ), + ), + ) + + fs.StringVar( + &c.txEventStorePath, + "tx-event-store-path", + "", + fmt.Sprintf("path for the file tx event store (required if event store is '%s')", file.EventStoreType), + ) } func execStart(c *startCfg, args []string, io *commands.IO) error { @@ -145,6 +175,14 @@ func execStart(c *startCfg, args []string, io *commands.IO) error { writeGenesisFile(genDoc, genesisFilePath) } + // Initialize the indexer config + txEventStoreCfg, err := getTxEventStoreConfig(c) + if err != nil { + return fmt.Errorf("unable to parse indexer config, %w", err) + } + + cfg.TxEventStore = txEventStoreCfg + // create application and node. gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) if err != nil { @@ -180,6 +218,30 @@ func execStart(c *startCfg, args []string, io *commands.IO) error { select {} // run forever } +// getTxEventStoreConfig constructs an event store config from provided user options +func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { + var cfg *eventstorecfg.Config + + switch c.txEventStoreType { + case file.EventStoreType: + if c.txEventStorePath == "" { + return nil, errors.New("unspecified file transaction indexer path") + } + + // Fill out the configuration + cfg = &eventstorecfg.Config{ + EventStoreType: file.EventStoreType, + Params: map[string]any{ + file.Path: c.txEventStorePath, + }, + } + default: + cfg = eventstorecfg.DefaultEventStoreConfig() + } + + return cfg, nil +} + // Makes a local test genesis doc with local privValidator. func makeGenesisDoc( pvPub crypto.PubKey, diff --git a/tm2/pkg/autofile/group.go b/tm2/pkg/autofile/group.go index 3350e1e62c5..189d7818bd8 100644 --- a/tm2/pkg/autofile/group.go +++ b/tm2/pkg/autofile/group.go @@ -125,7 +125,11 @@ func (g *Group) OnStart() error { // OnStop implements service.Service by stopping the goroutine described above. // NOTE: g.Head must be closed separately using Close. func (g *Group) OnStop() { - g.FlushAndSync() + if err := g.FlushAndSync(); err != nil { + g.Logger.Error( + fmt.Sprintf("unable to gracefully flush data, %s", err.Error()), + ) + } } // Wait blocks until all internal goroutines are finished. Supposed to be @@ -136,11 +140,20 @@ func (g *Group) Wait() { // Close closes the head file. The group must be stopped by this moment. func (g *Group) Close() { - g.FlushAndSync() + if err := g.FlushAndSync(); err != nil { + g.Logger.Error( + fmt.Sprintf("unable to gracefully flush data, %s", err.Error()), + ) + } g.mtx.Lock() - _ = g.Head.Close() - g.mtx.Unlock() + defer g.mtx.Unlock() + + if err := g.Head.Close(); err != nil { + g.Logger.Error( + fmt.Sprintf("unable to gracefully close group head, %s", err.Error()), + ) + } } // HeadSizeLimit returns the current head size limit. diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index 6f148c3b5c1..e05f514a284 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -9,6 +9,7 @@ import ( cns "github.com/gnolang/gno/tm2/pkg/bft/consensus/config" mem "github.com/gnolang/gno/tm2/pkg/bft/mempool/config" rpc "github.com/gnolang/gno/tm2/pkg/bft/rpc/config" + eventstore "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" "github.com/gnolang/gno/tm2/pkg/errors" osm "github.com/gnolang/gno/tm2/pkg/os" p2p "github.com/gnolang/gno/tm2/pkg/p2p/config" @@ -20,20 +21,22 @@ type Config struct { BaseConfig `toml:",squash"` // Options for services - RPC *rpc.RPCConfig `toml:"rpc"` - P2P *p2p.P2PConfig `toml:"p2p"` - Mempool *mem.MempoolConfig `toml:"mempool"` - Consensus *cns.ConsensusConfig `toml:"consensus"` + RPC *rpc.RPCConfig `toml:"rpc"` + P2P *p2p.P2PConfig `toml:"p2p"` + Mempool *mem.MempoolConfig `toml:"mempool"` + Consensus *cns.ConsensusConfig `toml:"consensus"` + TxEventStore *eventstore.Config `toml:"tx_event_store"` } // DefaultConfig returns a default configuration for a Tendermint node func DefaultConfig() *Config { return &Config{ - BaseConfig: DefaultBaseConfig(), - RPC: rpc.DefaultRPCConfig(), - P2P: p2p.DefaultP2PConfig(), - Mempool: mem.DefaultMempoolConfig(), - Consensus: cns.DefaultConsensusConfig(), + BaseConfig: DefaultBaseConfig(), + RPC: rpc.DefaultRPCConfig(), + P2P: p2p.DefaultP2PConfig(), + Mempool: mem.DefaultMempoolConfig(), + Consensus: cns.DefaultConsensusConfig(), + TxEventStore: eventstore.DefaultEventStoreConfig(), } } @@ -68,11 +71,12 @@ func LoadOrMakeConfigWithOptions(root string, options ConfigOptions) (cfg *Confi // TestConfig returns a configuration that can be used for testing func TestConfig() *Config { return &Config{ - BaseConfig: TestBaseConfig(), - RPC: rpc.TestRPCConfig(), - P2P: p2p.TestP2PConfig(), - Mempool: mem.TestMempoolConfig(), - Consensus: cns.TestConsensusConfig(), + BaseConfig: TestBaseConfig(), + RPC: rpc.TestRPCConfig(), + P2P: p2p.TestP2PConfig(), + Mempool: mem.TestMempoolConfig(), + Consensus: cns.TestConsensusConfig(), + TxEventStore: eventstore.DefaultEventStoreConfig(), } } @@ -121,7 +125,7 @@ func (cfg *Config) ValidateBasic() error { return nil } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // BaseConfig const ( diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index 23b42cec6b9..bdeb5061540 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/file" "github.com/rs/cors" "github.com/gnolang/gno/tm2/pkg/amino" @@ -25,8 +26,8 @@ import ( _ "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpcserver "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" sm "github.com/gnolang/gno/tm2/pkg/bft/state" - "github.com/gnolang/gno/tm2/pkg/bft/state/txindex" - "github.com/gnolang/gno/tm2/pkg/bft/state/txindex/null" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/null" "github.com/gnolang/gno/tm2/pkg/bft/store" "github.com/gnolang/gno/tm2/pkg/bft/types" tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time" @@ -154,18 +155,18 @@ type Node struct { isListening bool // services - evsw events.EventSwitch - stateDB dbm.DB - blockStore *store.BlockStore // store the blockchain to disk - bcReactor p2p.Reactor // for fast-syncing - mempoolReactor *mempl.Reactor // for gossipping transactions - mempool mempl.Mempool - consensusState *cs.ConsensusState // latest consensus state - consensusReactor *cs.ConsensusReactor // for participating in the consensus - proxyApp proxy.AppConns // connection to the application - rpcListeners []net.Listener // rpc servers - txIndexer txindex.TxIndexer - indexerService *txindex.IndexerService + evsw events.EventSwitch + stateDB dbm.DB + blockStore *store.BlockStore // store the blockchain to disk + bcReactor p2p.Reactor // for fast-syncing + mempoolReactor *mempl.Reactor // for gossipping transactions + mempool mempl.Mempool + consensusState *cs.ConsensusState // latest consensus state + consensusReactor *cs.ConsensusReactor // for participating in the consensus + proxyApp proxy.AppConns // connection to the application + rpcListeners []net.Listener // rpc servers + txEventStore eventstore.TxEventStore + eventStoreService *eventstore.Service } func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { @@ -193,36 +194,36 @@ func createAndStartProxyAppConns(clientCreator proxy.ClientCreator, logger log.L return proxyApp, nil } -func createAndStartIndexerService(config *cfg.Config, dbProvider DBProvider, - evsw events.EventSwitch, logger log.Logger, -) (*txindex.IndexerService, txindex.TxIndexer, error) { - var txIndexer txindex.TxIndexer = &null.TxIndex{} - /* - switch config.TxIndex.Indexer { - case "kv": - store, err := dbProvider(&DBContext{"tx_index", config}) - if err != nil { - return nil, nil, err - } - switch { - case config.TxIndex.IndexTags != "": - txIndexer = kv.NewTxIndex(store, kv.IndexTags(splitAndTrimEmpty(config.TxIndex.IndexTags, ",", " "))) - case config.TxIndex.IndexAllTags: - txIndexer = kv.NewTxIndex(store, kv.IndexAllTags()) - default: - txIndexer = kv.NewTxIndex(store) - } - default: - txIndexer = &null.TxIndex{} +func createAndStartEventStoreService( + cfg *cfg.Config, + evsw events.EventSwitch, + logger log.Logger, +) (*eventstore.Service, eventstore.TxEventStore, error) { + var ( + err error + txEventStore eventstore.TxEventStore + ) + + // Instantiate the event store based on the configuration + switch cfg.TxEventStore.EventStoreType { + case file.EventStoreType: + // Transaction events should be logged to files + txEventStore, err = file.NewTxEventStore(cfg.TxEventStore) + if err != nil { + return nil, nil, fmt.Errorf("unable to create file tx event store, %w", err) } - */ + default: + // Transaction event storing should be omitted + txEventStore = null.NewNullEventStore() + } - indexerService := txindex.NewIndexerService(txIndexer, evsw) - indexerService.SetLogger(logger.With("module", "txindex")) + indexerService := eventstore.NewEventStoreService(txEventStore, evsw) + indexerService.SetLogger(logger.With("module", "eventstore")) if err := indexerService.Start(); err != nil { return nil, nil, err } - return indexerService, txIndexer, nil + + return indexerService, txEventStore, nil } func doHandshake(stateDB dbm.DB, state sm.State, blockStore sm.BlockStore, @@ -431,14 +432,14 @@ func NewNode(config *cfg.Config, return nil, err } - // EventSwitch and IndexerService must be started before the handshake because - // we might need to index the txs of the replayed block as this might not have happened + // EventSwitch and EventStoreService must be started before the handshake because + // we might need to store the txs of the replayed block as this might not have happened // when the node stopped last time (i.e. the node stopped after it saved the block // but before it indexed the txs, or, endblocker panicked) evsw := events.NewEventSwitch() - // Transaction indexing - indexerService, txIndexer, err := createAndStartIndexerService(config, dbProvider, evsw, logger) + // Transaction event storing + eventStoreService, txEventStore, err := createAndStartEventStoreService(config, evsw, logger) if err != nil { return nil, err } @@ -500,7 +501,7 @@ func NewNode(config *cfg.Config, privValidator, fastSync, evsw, consensusLogger, ) - nodeInfo, err := makeNodeInfo(config, nodeKey, txIndexer, genDoc, state) + nodeInfo, err := makeNodeInfo(config, nodeKey, txEventStore, genDoc, state) if err != nil { return nil, errors.Wrap(err, "error making NodeInfo") } @@ -541,17 +542,17 @@ func NewNode(config *cfg.Config, nodeInfo: nodeInfo, nodeKey: nodeKey, - evsw: evsw, - stateDB: stateDB, - blockStore: blockStore, - bcReactor: bcReactor, - mempoolReactor: mempoolReactor, - mempool: mempool, - consensusState: consensusState, - consensusReactor: consensusReactor, - proxyApp: proxyApp, - txIndexer: txIndexer, - indexerService: indexerService, + evsw: evsw, + stateDB: stateDB, + blockStore: blockStore, + bcReactor: bcReactor, + mempoolReactor: mempoolReactor, + mempool: mempool, + consensusState: consensusState, + consensusReactor: consensusReactor, + proxyApp: proxyApp, + txEventStore: txEventStore, + eventStoreService: eventStoreService, } node.BaseService = *service.NewBaseService(logger, "Node", node) @@ -626,7 +627,7 @@ func (n *Node) OnStop() { // first stop the non-reactor services n.evsw.Stop() - n.indexerService.Stop() + n.eventStoreService.Stop() // now stop the reactors n.sw.Stop() @@ -664,7 +665,7 @@ func (n *Node) ConfigureRPC() { rpccore.SetPubKey(pubKey) rpccore.SetGenesisDoc(n.genesisDoc) rpccore.SetProxyAppQuery(n.proxyApp.Query()) - rpccore.SetTxIndexer(n.txIndexer) + rpccore.SetTxEventStore(n.txEventStore) rpccore.SetConsensusReactor(n.consensusReactor) rpccore.SetLogger(n.Logger.With("module", "rpc")) rpccore.SetEventSwitch(n.evsw) @@ -839,15 +840,13 @@ func (n *Node) NodeInfo() p2p.NodeInfo { func makeNodeInfo( config *cfg.Config, nodeKey *p2p.NodeKey, - txIndexer txindex.TxIndexer, + txEventStore eventstore.TxEventStore, genDoc *types.GenesisDoc, state sm.State, ) (p2p.NodeInfo, error) { - txIndexerStatus := "on" - if _, ok := txIndexer.(*null.TxIndex); ok { - txIndexerStatus = "off" - } else if txIndexer == nil { - txIndexerStatus = "none" + txIndexerStatus := eventstore.StatusOff + if txEventStore.GetType() != null.EventStoreType { + txIndexerStatus = eventstore.StatusOn } bcChannel := bc.BlockchainChannel diff --git a/tm2/pkg/bft/rpc/core/pipe.go b/tm2/pkg/bft/rpc/core/pipe.go index fd6c8fc0692..a8b102d9ab7 100644 --- a/tm2/pkg/bft/rpc/core/pipe.go +++ b/tm2/pkg/bft/rpc/core/pipe.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/proxy" cfg "github.com/gnolang/gno/tm2/pkg/bft/rpc/config" sm "github.com/gnolang/gno/tm2/pkg/bft/state" - "github.com/gnolang/gno/tm2/pkg/bft/state/txindex" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -25,7 +25,7 @@ const ( maxPerPage = 100 ) -//---------------------------------------------- +// ---------------------------------------------- // These interfaces are used by RPC and must be thread safe type Consensus interface { @@ -50,7 +50,7 @@ type peers interface { Peers() p2p.IPeerSet } -//---------------------------------------------- +// ---------------------------------------------- // These package level globals come with setters // that are expected to be called only once, on startup @@ -68,7 +68,7 @@ var ( // objects pubKey crypto.PubKey genDoc *types.GenesisDoc // cache the genesis structure - txIndexer txindex.TxIndexer + txEventStore eventstore.TxEventStore consensusReactor *consensus.ConsensusReactor evsw events.EventSwitch gTxDispatcher *txDispatcher @@ -115,8 +115,8 @@ func SetProxyAppQuery(appConn proxy.AppConnQuery) { proxyAppQuery = appConn } -func SetTxIndexer(indexer txindex.TxIndexer) { - txIndexer = indexer +func SetTxEventStore(indexer eventstore.TxEventStore) { + txEventStore = indexer } func SetConsensusReactor(conR *consensus.ConsensusReactor) { diff --git a/tm2/pkg/bft/state/eventstore/file/file.go b/tm2/pkg/bft/state/eventstore/file/file.go new file mode 100644 index 00000000000..f4cd74721f5 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/file/file.go @@ -0,0 +1,92 @@ +package file + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/autofile" + storetypes "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/errors" +) + +const ( + EventStoreType = "file" + Path = "path" +) + +var ( + errMissingPath = errors.New("missing path param") + errInvalidType = errors.New("invalid config for file event store specified") +) + +// TxEventStore is the implementation of a transaction event store +// that outputs to the local filesystem +type TxEventStore struct { + headPath string + group *autofile.Group +} + +// NewTxEventStore creates a new file-based tx event store +func NewTxEventStore(cfg *storetypes.Config) (*TxEventStore, error) { + // Parse config params + if EventStoreType != cfg.EventStoreType { + return nil, errInvalidType + } + + headPath, ok := cfg.GetParam(Path).(string) + if !ok { + return nil, errMissingPath + } + + return &TxEventStore{ + headPath: headPath, + }, nil +} + +// Start starts the file transaction event store, by opening the autofile group +func (t *TxEventStore) Start() error { + // Open the group + group, err := autofile.OpenGroup(t.headPath) + if err != nil { + return fmt.Errorf("unable to open file group for writing, %w", err) + } + + t.group = group + + return nil +} + +// Stop stops the file transaction event store, by closing the autofile group +func (t *TxEventStore) Stop() error { + // Close off the group + t.group.Close() + + return nil +} + +// GetType returns the file transaction event store type +func (t *TxEventStore) GetType() string { + return EventStoreType +} + +// Append marshals the transaction using amino, and writes it to the disk +func (t *TxEventStore) Append(tx types.TxResult) error { + // Serialize the transaction using amino + txRaw, err := amino.MarshalJSON(tx) + if err != nil { + return fmt.Errorf("unable to marshal transaction, %w", err) + } + + // Write the serialized transaction info to the file group + if err = t.group.WriteLine(string(txRaw)); err != nil { + return fmt.Errorf("unable to save transaction event, %w", err) + } + + // Flush output to storage + if err := t.group.FlushAndSync(); err != nil { + return fmt.Errorf("unable to flush and sync transaction event, %w", err) + } + + return nil +} diff --git a/tm2/pkg/bft/state/eventstore/file/file_test.go b/tm2/pkg/bft/state/eventstore/file/file_test.go new file mode 100644 index 00000000000..46d87582ce4 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/file/file_test.go @@ -0,0 +1,140 @@ +package file + +import ( + "bufio" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + storetypes "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" +) + +// generateTestTransactions generates random transaction results +func generateTestTransactions(count int) []types.TxResult { + txs := make([]types.TxResult, count) + + for i := 0; i < count; i++ { + txs[i] = types.TxResult{} + } + + return txs +} + +func TestTxEventStore_New(t *testing.T) { + t.Parallel() + + t.Run("invalid file path specified", func(t *testing.T) { + t.Parallel() + + cfg := &storetypes.Config{ + EventStoreType: "invalid", + } + + i, err := NewTxEventStore(cfg) + + assert.Nil(t, i) + assert.ErrorIs(t, err, errInvalidType) + }) + + t.Run("invalid file path specified", func(t *testing.T) { + t.Parallel() + + cfg := &storetypes.Config{ + EventStoreType: EventStoreType, + Params: nil, + } + + i, err := NewTxEventStore(cfg) + + assert.Nil(t, i) + assert.ErrorIs(t, err, errMissingPath) + }) + + t.Run("valid file path specified", func(t *testing.T) { + t.Parallel() + + headPath := "." + + cfg := &storetypes.Config{ + EventStoreType: EventStoreType, + Params: map[string]any{ + Path: headPath, + }, + } + + i, err := NewTxEventStore(cfg) + if i == nil { + t.Fatalf("unable to create event store") + } + + assert.NoError(t, err) + assert.Equal(t, headPath, i.headPath) + assert.Equal(t, EventStoreType, i.GetType()) + }) +} + +func TestTxEventStore_Append(t *testing.T) { + t.Parallel() + + headFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(func() { + cleanup() + }) + + eventStore, err := NewTxEventStore(&storetypes.Config{ + EventStoreType: EventStoreType, + Params: map[string]any{ + Path: headFile.Name(), + }, + }) + if err != nil { + t.Fatalf("unable to create tx event store, %v", err) + } + + // Start the event store + if err = eventStore.Start(); err != nil { + t.Fatalf("unable to start event store, %v", err) + } + + t.Cleanup(func() { + // Stop the event store + if err = eventStore.Stop(); err != nil { + t.Fatalf("unable to stop event store gracefully, %v", err) + } + }) + + numTxs := 10 + txs := generateTestTransactions(numTxs) + + for _, tx := range txs { + if err = eventStore.Append(tx); err != nil { + t.Fatalf("unable to store transaction, %v", err) + } + } + + // Make sure the file group's size is valid + if eventStore.group.ReadGroupInfo().TotalSize == 0 { + t.Fatalf("invalid group size") + } + + // Open file for reading + scanner := bufio.NewScanner(headFile) + + linesRead := 0 + for scanner.Scan() { + line := scanner.Bytes() + + var txRes types.TxResult + if err = amino.UnmarshalJSON(line, &txRes); err != nil { + t.Fatalf("unable to read store line") + } + + assert.Equal(t, txs[linesRead], txRes) + + linesRead++ + } + + assert.Equal(t, numTxs, linesRead) +} diff --git a/tm2/pkg/bft/state/eventstore/mock_test.go b/tm2/pkg/bft/state/eventstore/mock_test.go new file mode 100644 index 00000000000..087d8f6e3e9 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/mock_test.go @@ -0,0 +1,89 @@ +package eventstore + +import ( + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/service" +) + +// TxEventStore // + +type ( + startDelegate func() error + stopDelegate func() error + getTypeDelegate func() string + appendDelegate func(types.TxResult) error +) + +type mockEventStore struct { + startFn startDelegate + stopFn stopDelegate + getTypeFn getTypeDelegate + appendFn appendDelegate +} + +func (m mockEventStore) Start() error { + if m.startFn != nil { + return m.startFn() + } + + return nil +} + +func (m mockEventStore) Stop() error { + if m.stopFn != nil { + return m.stopFn() + } + + return nil +} + +func (m mockEventStore) GetType() string { + if m.getTypeFn != nil { + return m.getTypeFn() + } + + return "" +} + +func (m mockEventStore) Append(result types.TxResult) error { + if m.appendFn != nil { + return m.appendFn(result) + } + + return nil +} + +// EventSwitch // + +type ( + fireEventDelegate func(events.Event) + addListenerDelegate func(string, events.EventCallback) + removeListenerDelegate func(string) +) + +type mockEventSwitch struct { + service.BaseService + + fireEventFn fireEventDelegate + addListenerFn addListenerDelegate + removeListenerFn removeListenerDelegate +} + +func (m *mockEventSwitch) FireEvent(ev events.Event) { + if m.fireEventFn != nil { + m.fireEventFn(ev) + } +} + +func (m *mockEventSwitch) AddListener(listenerID string, cb events.EventCallback) { + if m.addListenerFn != nil { + m.addListenerFn(listenerID, cb) + } +} + +func (m *mockEventSwitch) RemoveListener(listenerID string) { + if m.removeListenerFn != nil { + m.removeListenerFn(listenerID) + } +} diff --git a/tm2/pkg/bft/state/eventstore/null/null.go b/tm2/pkg/bft/state/eventstore/null/null.go new file mode 100644 index 00000000000..40e3566d89e --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/null/null.go @@ -0,0 +1,35 @@ +package null + +import ( + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" + "github.com/gnolang/gno/tm2/pkg/bft/types" +) + +var _ eventstore.TxEventStore = (*TxEventStore)(nil) + +const ( + EventStoreType = "none" +) + +// TxEventStore acts as a /dev/null +type TxEventStore struct{} + +func NewNullEventStore() *TxEventStore { + return &TxEventStore{} +} + +func (t TxEventStore) Start() error { + return nil +} + +func (t TxEventStore) Stop() error { + return nil +} + +func (t TxEventStore) Append(_ types.TxResult) error { + return nil +} + +func (t TxEventStore) GetType() string { + return EventStoreType +} diff --git a/tm2/pkg/bft/state/eventstore/store.go b/tm2/pkg/bft/state/eventstore/store.go new file mode 100644 index 00000000000..10ef9eefc9b --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/store.go @@ -0,0 +1,24 @@ +package eventstore + +import "github.com/gnolang/gno/tm2/pkg/bft/types" + +const ( + StatusOn = "on" + StatusOff = "off" +) + +// TxEventStore stores transaction events for later processing +type TxEventStore interface { + // Start starts the transaction event store + Start() error + + // Stop stops the transaction event store + Stop() error + + // GetType returns the event store type + GetType() string + + // Append analyzes and appends a single transaction + // to the event store + Append(result types.TxResult) error +} diff --git a/tm2/pkg/bft/state/eventstore/store_service.go b/tm2/pkg/bft/state/eventstore/store_service.go new file mode 100644 index 00000000000..d6ed40c4151 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/store_service.go @@ -0,0 +1,84 @@ +package eventstore + +import ( + "context" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/service" +) + +// Service connects the event bus and event store together in order +// to store events coming from event bus +type Service struct { + service.BaseService + + cancelFn context.CancelFunc + + txEventStore TxEventStore + evsw events.EventSwitch +} + +// NewEventStoreService returns a new service instance +func NewEventStoreService(idr TxEventStore, evsw events.EventSwitch) *Service { + is := &Service{txEventStore: idr, evsw: evsw} + is.BaseService = *service.NewBaseService(nil, "EventStoreService", is) + + return is +} + +func (is *Service) OnStart() error { + // Create a context for the intermediary monitor service + ctx, cancelFn := context.WithCancel(context.Background()) + is.cancelFn = cancelFn + + // Start the event store + if err := is.txEventStore.Start(); err != nil { + return fmt.Errorf("unable to start transaction event store, %w", err) + } + + // Start the intermediary monitor service + go is.monitorTxEvents(ctx) + + return nil +} + +func (is *Service) OnStop() { + // Close off any routines + is.cancelFn() + + // Attempt to gracefully stop the event store + if err := is.txEventStore.Stop(); err != nil { + is.Logger.Error( + fmt.Sprintf("unable to gracefully stop event store, %v", err), + ) + } +} + +// monitorTxEvents acts as an intermediary feed service for the supplied +// event store. It relays transaction events that come from the event stream +func (is *Service) monitorTxEvents(ctx context.Context) { + // Create a subscription for transaction events + subCh := events.SubscribeToEvent(is.evsw, "tx-event-store", types.EventTx{}) + + for { + select { + case <-ctx.Done(): + return + case evRaw := <-subCh: + // Cast the event + ev, ok := evRaw.(types.EventTx) + if !ok { + is.Logger.Error("invalid transaction result type cast") + + continue + } + + // Alert the actual tx event store + if err := is.txEventStore.Append(ev.Result); err != nil { + is.Logger.Error("unable to store transaction", "err", err) + } + } + } +} diff --git a/tm2/pkg/bft/state/eventstore/store_service_test.go b/tm2/pkg/bft/state/eventstore/store_service_test.go new file mode 100644 index 00000000000..3fa5e8a7941 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/store_service_test.go @@ -0,0 +1,159 @@ +package eventstore + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/stretchr/testify/assert" +) + +// generateTxEvents generates random transaction events +func generateTxEvents(count int) []types.EventTx { + txEvents := make([]types.EventTx, count) + + for i := 0; i < count; i++ { + txEvents[i] = types.EventTx{ + Result: types.TxResult{}, + } + } + + return txEvents +} + +func TestEventStoreService_Monitor(t *testing.T) { + t.Parallel() + + const defaultTimeout = 5 * time.Second + + var ( + startCalled = false + stopCalled = false + receivedResults = make([]types.TxResult, 0) + receivedSize atomic.Int64 + + cb events.EventCallback + cbSet atomic.Bool + + mockEventStore = &mockEventStore{ + startFn: func() error { + startCalled = true + + return nil + }, + stopFn: func() error { + stopCalled = true + + return nil + }, + appendFn: func(result types.TxResult) error { + receivedResults = append(receivedResults, result) + + // Atomic because we are accessing this size from a routine + receivedSize.Store(int64(len(receivedResults))) + + return nil + }, + } + mockEventSwitch = &mockEventSwitch{ + fireEventFn: func(event events.Event) { + // Exec the callback on event fire + cb(event) + }, + addListenerFn: func(_ string, callback events.EventCallback) { + // Attach callback + cb = callback + + // Atomic because we are accessing this info from a routine + cbSet.Store(true) + }, + } + ) + + // Create a new event store instance + i := NewEventStoreService(mockEventStore, mockEventSwitch) + if i == nil { + t.Fatal("unable to create event store service") + } + + // Start the event store + if err := i.OnStart(); err != nil { + t.Fatalf("unable to start event store, %v", err) + } + + assert.True(t, startCalled) + + t.Cleanup(func() { + // Stop the event store + i.OnStop() + + assert.True(t, stopCalled) + }) + + // Fire off the events so the event store can catch them + numEvents := 1000 + txEvents := generateTxEvents(numEvents) + + var wg sync.WaitGroup + + // Start a routine that asynchronously pushes events + wg.Add(1) + go func() { + defer wg.Done() + + timeout := time.After(defaultTimeout) + + for { + select { + case <-timeout: + return + default: + // If the callback is set, fire the events + if !cbSet.Load() { + // Listener not set yet + continue + } + + for _, event := range txEvents { + mockEventSwitch.FireEvent(event) + } + + return + } + } + }() + + // Start a routine that monitors received results + wg.Add(1) + go func() { + defer wg.Done() + + timeout := time.After(defaultTimeout) + + for { + select { + case <-timeout: + return + default: + if int(receivedSize.Load()) == numEvents { + return + } + } + } + }() + + wg.Wait() + + // Make sure all results were received + if len(receivedResults) != numEvents { + t.Fatalf("invalid number of results received, %d", len(receivedResults)) + } + + // Make sure all results match + for index, event := range txEvents { + assert.Equal(t, event.Result, receivedResults[index]) + } +} diff --git a/tm2/pkg/bft/state/eventstore/types/config.go b/tm2/pkg/bft/state/eventstore/types/config.go new file mode 100644 index 00000000000..08e25870b4d --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/types/config.go @@ -0,0 +1,29 @@ +package types + +import "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore/null" + +// EventStoreParams defines the arbitrary event store config params +type EventStoreParams map[string]any + +// Config defines the specific event store configuration +type Config struct { + EventStoreType string + Params EventStoreParams +} + +// GetParam fetches the specific config param, if any. +// Returns nil if the param is not present +func (c *Config) GetParam(name string) any { + if c.Params != nil { + return c.Params[name] + } + + return nil +} + +// DefaultEventStoreConfig returns the default event store config +func DefaultEventStoreConfig() *Config { + return &Config{ + EventStoreType: null.EventStoreType, + } +} diff --git a/tm2/pkg/bft/state/eventstore/types/config_test.go b/tm2/pkg/bft/state/eventstore/types/config_test.go new file mode 100644 index 00000000000..0f5683b7c61 --- /dev/null +++ b/tm2/pkg/bft/state/eventstore/types/config_test.go @@ -0,0 +1,45 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_GetParam(t *testing.T) { + t.Parallel() + + const paramName = "param" + + testTable := []struct { + name string + cfg *Config + + expectedParam any + }{ + { + "param not set", + &Config{}, + nil, + }, + { + "valid param set", + &Config{ + Params: map[string]any{ + paramName: 10, + }, + }, + 10, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, testCase.expectedParam, testCase.cfg.GetParam(paramName)) + }) + } +} diff --git a/tm2/pkg/bft/state/txindex/indexer.go b/tm2/pkg/bft/state/txindex/indexer.go deleted file mode 100644 index 2b5b4aae220..00000000000 --- a/tm2/pkg/bft/state/txindex/indexer.go +++ /dev/null @@ -1,18 +0,0 @@ -package txindex - -// TxIndexer interface defines methods to index and search transactions. -type TxIndexer interface { /* - // AddBatch analyzes, indexes and stores a batch of transactions. - AddBatch(b *Batch) error - - // Index analyzes, indexes and stores a single transaction. - Index(result *types.TxResult) error - - // Get returns the transaction specified by hash or nil if the transaction is not indexed - // or stored. - Get(hash []byte) (*types.TxResult, error) - - // Search allows you to query for transactions. - Search(q *query.Query) ([]*types.TxResult, error) - */ -} diff --git a/tm2/pkg/bft/state/txindex/indexer_service.go b/tm2/pkg/bft/state/txindex/indexer_service.go deleted file mode 100644 index fb5c3068ae4..00000000000 --- a/tm2/pkg/bft/state/txindex/indexer_service.go +++ /dev/null @@ -1,31 +0,0 @@ -package txindex - -import ( - "github.com/gnolang/gno/tm2/pkg/events" - "github.com/gnolang/gno/tm2/pkg/service" -) - -// IndexerService connects event bus and transaction indexer together in order -// to index transactions coming from event bus. -type IndexerService struct { - service.BaseService - - idr TxIndexer - evsw events.EventSwitch -} - -// NewIndexerService returns a new service instance. -func NewIndexerService(idr TxIndexer, evsw events.EventSwitch) *IndexerService { - is := &IndexerService{idr: idr, evsw: evsw} - is.BaseService = *service.NewBaseService(nil, "IndexerService", is) - return is -} - -func (is *IndexerService) OnStart() error { - // TODO - return nil -} - -func (is *IndexerService) OnStop() { - // TODO -} diff --git a/tm2/pkg/bft/state/txindex/null/null.go b/tm2/pkg/bft/state/txindex/null/null.go deleted file mode 100644 index ed90013b9a9..00000000000 --- a/tm2/pkg/bft/state/txindex/null/null.go +++ /dev/null @@ -1,31 +0,0 @@ -package null - -import ( - "github.com/gnolang/gno/tm2/pkg/bft/state/txindex" -) - -var _ txindex.TxIndexer = (*TxIndex)(nil) - -// TxIndex acts as a /dev/null. -type TxIndex struct{} - -/* -// Get on a TxIndex is disabled and panics when invoked. -func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) { - return nil, errors.New(`Indexing is disabled (set 'tx_index = "kv"' in config)`) -} - -// AddBatch is a noop and always returns nil. -func (txi *TxIndex) AddBatch(batch *txindex.Batch) error { - return nil -} - -// Index is a noop and always returns nil. -func (txi *TxIndex) Index(result *types.TxResult) error { - return nil -} - -func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { - return []*types.TxResult{}, nil -} -*/ diff --git a/tm2/pkg/p2p/node_info.go b/tm2/pkg/p2p/node_info.go index 9653a83c38a..48ba8f7776b 100644 --- a/tm2/pkg/p2p/node_info.go +++ b/tm2/pkg/p2p/node_info.go @@ -3,6 +3,7 @@ package p2p import ( "fmt" + "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" "github.com/gnolang/gno/tm2/pkg/strings" "github.com/gnolang/gno/tm2/pkg/versionset" ) @@ -100,7 +101,7 @@ func (info NodeInfo) Validate() error { other := info.Other txIndex := other.TxIndex switch txIndex { - case "", "on", "off": + case "", eventstore.StatusOn, eventstore.StatusOff: default: return fmt.Errorf("info.Other.TxIndex should be either 'on', 'off', or empty string, got '%v'", txIndex) } From 9e8fbd37d8142cc6578205b3641ac425d7c8d804 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:33:44 +0200 Subject: [PATCH 28/93] chore(deps): Bump golang.org/x/net from 0.15.0 to 0.17.0 (#1225) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0.
Commits
  • b225e7c http2: limit maximum handler goroutines to MaxConcurrentStreams
  • 88194ad go.mod: update golang.org/x dependencies
  • 2b60a61 quic: fix several bugs in flow control accounting
  • 73d82ef quic: handle DATA_BLOCKED frames
  • 5d5a036 quic: handle streams moving from the data queue to the meta queue
  • 350aad2 quic: correctly extend peer's flow control window after MAX_DATA
  • 21814e7 quic: validate connection id transport parameters
  • a600b35 quic: avoid redundant MAX_DATA updates
  • ea63359 http2: check stream body is present on read timeout
  • ddd8598 quic: version negotiation
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.15.0&new-version=0.17.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index dbb62a34e2d..72c52102547 100644 --- a/go.mod +++ b/go.mod @@ -32,10 +32,10 @@ require ( github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c go.etcd.io/bbolt v1.3.7 go.uber.org/multierr v1.9.0 - golang.org/x/crypto v0.13.0 + golang.org/x/crypto v0.14.0 golang.org/x/mod v0.13.0 - golang.org/x/net v0.15.0 - golang.org/x/term v0.12.0 + golang.org/x/net v0.17.0 + golang.org/x/term v0.13.0 golang.org/x/tools v0.13.0 google.golang.org/protobuf v1.31.0 gopkg.in/yaml.v3 v3.0.1 @@ -66,7 +66,7 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 3e9aac9b2e2..67e2a190feb 100644 --- a/go.sum +++ b/go.sum @@ -189,8 +189,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -208,8 +208,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -227,10 +227,10 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= From d29509e0544021b899fda98e6acb185f446c2c71 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:08:35 -0400 Subject: [PATCH 29/93] feat: portal loop's main contracts and gnoweb improvements (#1176) Addresses #1131 ## Completed Tasks - [x] Completed the home page by adding static content and blocks. - [x] Implemented support for redirects and aliases to improve SEO. - [x] Made some improvements to p/demo/ui, but more work is still needed. - [x] Enhanced p/demo/blog by adding widget support. - [x] Transferred previous static webpages to realms. - [x] Created a new personal realm `p/manfred/present` for a Gno-powered presentation. - [x] Refactored gnoweb to remove static pages, improve maintainability and design consistency, and added optional analytics. ## Next Steps after Merging - Anyone: - Improve r/gnoland/home. - Create and enhance additional `p/demo/*` pages to simplify maintenance of `r/gnoland/home`. - Start writing personal and team realms to incorporate more dynamic data on-chain. Consider adding these realms as widgets on the homepage. - Encourage individuals to create dedicated realms, preferably dynamic ones. Then, import these new realms into the homepage to include widgets such as "upcoming events." - Manfred: - Develop dynamic contracts for Worxdao, including project, team, and people directories. Also, implement DAO features v0 and contributor profiles. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/blog/blog.gno | 51 ++-- examples/gno.land/p/demo/ui/ui.gno | 44 ++- examples/gno.land/r/gnoland/blog/gnoblog.gno | 4 + examples/gno.land/r/gnoland/home/gno.mod | 8 + examples/gno.land/r/gnoland/home/home.gno | 257 ++++++++++++++++++ .../gno.land/r/gnoland/home/home_filetest.gno | 188 +++++++++++++ .../gno.land/r/gnoland/pages/page_about.gno | 12 +- .../r/gnoland/pages/page_ecosystem.gno | 35 +++ .../gno.land/r/gnoland/pages/page_events.gno | 151 ++++++++++ .../gno.land/r/gnoland/pages/page_gnolang.gno | 43 +++ .../gno.land/r/gnoland/pages/page_gor.gno | 221 +++++++++++++++ .../r/gnoland/pages/page_partners.gno | 21 ++ .../gno.land/r/gnoland/pages/page_start.gno | 21 ++ .../r/gnoland/pages/page_testnets.gno | 19 ++ .../r/gnoland/pages/page_tokenomics.gno | 11 + examples/gno.land/r/gnoland/pages/pages.gno | 14 +- .../gno.land/r/gnoland/pages/pages_test.gno | 74 +++-- examples/gno.land/r/manfred/present/admin.gno | 92 +++++++ examples/gno.land/r/manfred/present/gno.mod | 6 + .../r/manfred/present/present_miami23.gno | 44 +++ .../present/present_miami23_filetest.gno | 57 ++++ .../r/manfred/present/presentations.gno | 17 ++ gno.land/Makefile | 3 + gno.land/cmd/gnoweb/main.go | 197 +++++++++----- gno.land/cmd/gnoweb/main_test.go | 71 ++++- gno.land/cmd/gnoweb/pages/GOR.md | 30 -- gno.land/cmd/gnoweb/pages/HOME.md | 50 ---- gno.land/cmd/gnoweb/views/404.html | 4 +- gno.land/cmd/gnoweb/views/faucet.html | 2 +- gno.land/cmd/gnoweb/views/funcs.html | 52 ++-- gno.land/cmd/gnoweb/views/generic.html | 5 +- gno.land/cmd/gnoweb/views/home.html | 21 -- gno.land/cmd/gnoweb/views/package_dir.html | 13 +- gno.land/cmd/gnoweb/views/package_file.html | 6 +- gno.land/cmd/gnoweb/views/realm_help.html | 24 +- gno.land/cmd/gnoweb/views/realm_render.html | 9 +- gno.land/cmd/gnoweb/views/redirect.html | 16 ++ 37 files changed, 1603 insertions(+), 290 deletions(-) create mode 100644 examples/gno.land/r/gnoland/home/gno.mod create mode 100644 examples/gno.land/r/gnoland/home/home.gno create mode 100644 examples/gno.land/r/gnoland/home/home_filetest.gno rename gno.land/cmd/gnoweb/pages/ABOUT.md => examples/gno.land/r/gnoland/pages/page_about.gno (73%) create mode 100644 examples/gno.land/r/gnoland/pages/page_ecosystem.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_events.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_gnolang.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_gor.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_partners.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_start.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_testnets.gno create mode 100644 examples/gno.land/r/gnoland/pages/page_tokenomics.gno create mode 100644 examples/gno.land/r/manfred/present/admin.gno create mode 100644 examples/gno.land/r/manfred/present/gno.mod create mode 100644 examples/gno.land/r/manfred/present/present_miami23.gno create mode 100644 examples/gno.land/r/manfred/present/present_miami23_filetest.gno create mode 100644 examples/gno.land/r/manfred/present/presentations.gno delete mode 100644 gno.land/cmd/gnoweb/pages/GOR.md delete mode 100644 gno.land/cmd/gnoweb/pages/HOME.md delete mode 100644 gno.land/cmd/gnoweb/views/home.html create mode 100644 gno.land/cmd/gnoweb/views/redirect.html diff --git a/examples/gno.land/p/demo/blog/blog.gno b/examples/gno.land/p/demo/blog/blog.gno index 62103b52885..1cf37b7ad3a 100644 --- a/examples/gno.land/p/demo/blog/blog.gno +++ b/examples/gno.land/p/demo/blog/blog.gno @@ -13,13 +13,28 @@ import ( ) type Blog struct { - Title string - Prefix string // i.e. r/gnoland/blog: - Posts avl.Tree // slug -> Post + Title string + Prefix string // i.e. r/gnoland/blog: + Posts avl.Tree // slug -> Post + NoBreadcrumb bool +} + +func (b Blog) RenderLastPostsWidget(limit int) string { + output := "" + i := 0 + b.Posts.Iterate("", "", func(key string, value interface{}) bool { + p := value.(*Post) + output += ufmt.Sprintf("- [%s](%s)\n", p.Title, p.URL()) + i++ + return i >= limit + }) + return output } func (b Blog) RenderHome(res *mux.ResponseWriter, req *mux.Request) { - res.Write(breadcrumb([]string{b.Title})) + if !b.NoBreadcrumb { + res.Write(breadcrumb([]string{b.Title})) + } if b.Posts.Size() == 0 { res.Write("No posts.") @@ -47,12 +62,14 @@ func (b Blog) RenderPost(res *mux.ResponseWriter, req *mux.Request) { } p := post.(*Post) - breadStr := breadcrumb([]string{ - ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), - "p", - p.Title, - }) - res.Write(breadStr) + if !b.NoBreadcrumb { + breadStr := breadcrumb([]string{ + ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), + "p", + p.Title, + }) + res.Write(breadStr) + } // output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) res.Write(p.Body + "\n\n") @@ -75,12 +92,14 @@ func (b Blog) RenderTag(res *mux.ResponseWriter, req *mux.Request) { return } - breadStr := breadcrumb([]string{ - ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), - "t", - slug, - }) - res.Write(breadStr) + if !b.NoBreadcrumb { + breadStr := breadcrumb([]string{ + ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), + "t", + slug, + }) + res.Write(breadStr) + } nb := 0 b.Posts.Iterate("", "", func(key string, value interface{}) bool { diff --git a/examples/gno.land/p/demo/ui/ui.gno b/examples/gno.land/p/demo/ui/ui.gno index efa185914a0..dd21d0510eb 100644 --- a/examples/gno.land/p/demo/ui/ui.gno +++ b/examples/gno.land/p/demo/ui/ui.gno @@ -1,6 +1,9 @@ package ui -import "strings" +import ( + "strconv" + "strings" +) type DOM struct { // metadata @@ -56,6 +59,17 @@ func (dom DOM) String() string { return output } +type Jumbotron []DomStringer + +func (j Jumbotron) String(dom DOM) string { + output := `
` + "\n\n" + for _, elem := range j { + output += elem.String(dom) + "\n" + } + output += `
` + "\n" + return output +} + // XXX: rename Element to Div? type Element []DomStringer @@ -88,6 +102,26 @@ func (b Breadcrumb) String(dom DOM) string { return output } +type Columns struct { + MaxWidth int + Columns []Element +} + +func (c *Columns) Append(elems ...Element) { + c.Columns = append(c.Columns, elems...) +} + +func (c Columns) String(dom DOM) string { + output := `
` + "\n" + for _, entry := range c.Columns { + output += `
` + "\n\n" + output += entry.String(dom) + output += "
\n" + } + output += "
\n" + return output +} + type Link struct { Text string Path string @@ -104,8 +138,14 @@ func (l Link) String(dom DOM) string { case l.Path != "" && l.URL != "": panic("a link should have a path or a URL, not both.") case l.Path != "": + if l.Text == "" { + l.Text = l.Path + } url = dom.Prefix + l.Path case l.URL != "": + if l.Text == "" { + l.Text = l.URL + } url = l.URL } @@ -151,6 +191,7 @@ type ( Italic string Code string Paragraph string + Quote string HR struct{} ) @@ -160,6 +201,7 @@ func (text H3) String(_ DOM) string { return "### " + string(text) + "\n" func (text H4) String(_ DOM) string { return "#### " + string(text) + "\n" } func (text H5) String(_ DOM) string { return "##### " + string(text) + "\n" } func (text H6) String(_ DOM) string { return "###### " + string(text) + "\n" } +func (text Quote) String(_ DOM) string { return "> " + string(text) + "\n" } func (text Bold) String(_ DOM) string { return "**" + string(text) + "**" } func (text Italic) String(_ DOM) string { return "_" + string(text) + "_" } func (text Paragraph) String(_ DOM) string { return "\n" + string(text) + "\n" } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno index 2982ea88489..cad84507614 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno @@ -23,3 +23,7 @@ func AddComment(postSlug, comment string) { func Render(path string) string { return b.Render(path) } + +func RenderLastPostsWidget(limit int) string { + return b.RenderLastPostsWidget(limit) +} diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod new file mode 100644 index 00000000000..9192b4364d0 --- /dev/null +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -0,0 +1,8 @@ +module gno.land/r/gnoland/home + +require ( + "gno.land/r/gnoland/blog" v0.0.0-latest + "gno.land/p/demo/ufmt" v0.0.0-latest + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/ui" v0.0.0-latest +) diff --git a/examples/gno.land/r/gnoland/home/home.gno b/examples/gno.land/r/gnoland/home/home.gno new file mode 100644 index 00000000000..5f2a5b9c4b5 --- /dev/null +++ b/examples/gno.land/r/gnoland/home/home.gno @@ -0,0 +1,257 @@ +package home + +import ( + "std" + + "gno.land/p/demo/ufmt" + "gno.land/p/demo/ui" + blog "gno.land/r/gnoland/blog" +) + +// XXX: p/demo/ui API is crappy, we need to make it more idiomatic +// XXX: use an updatable block system to update content from a DAO +// XXX: var blocks avl.Tree + +func Render(_ string) string { + dom := ui.DOM{Prefix: "r/gnoland/home:"} + dom.Title = "Welcome to Gno.land" + + // body + dom.Body.Append(introSection()...) + dom.Body.Append(ui.Jumbotron(worxDAO())) + dom.Body.Append(packageStaffPicks()...) + dom.Body.Append(ui.HR{}) + dom.Body.Append( + ui.Columns{3, []ui.Element{ + lastBlogposts(4), + upcomingEvents(4), + lastContributions(4), + }}, + ) + dom.Body.Append(ui.Jumbotron(discoverLinks())) + + // footer + dom.Footer.Append( + ui.Columns{2, []ui.Element{ + socialLinks(), + quoteOfTheBlock(), + }}, + ) + + // Testnet disclaimer + dom.Footer.Append( + ui.HR{}, + ui.Bold("This is a testnet."), + ui.Text("Package names are not guaranteed to be available for production."), + ) + + return dom.String() +} + +func lastBlogposts(limit int) ui.Element { + posts := blog.RenderLastPostsWidget(limit) + return ui.Element{ + ui.H3("Last Blogposts"), + ui.Text(posts), + } +} + +func lastContributions(limit int) ui.Element { + return ui.Element{ + ui.H3("Last Contributions"), + ui.Text("TODO: import r/gh"), + ui.Link{Text: "#1134", URL: "https://github.com/gnolang/gno/pull/1134"}, + } +} + +func upcomingEvents(limit int) ui.Element { + return ui.Element{ + ui.H3("Upcoming Events"), + ui.Text("TODO: import r/gnoland/events"), + } +} + +func introSection() ui.Element { + return ui.Element{ + ui.H3("An interpretation of the Golang (Go) programming language for advanced developers and intrepid pioneers to build succinct, composable smart contracts for social coordination."), + ui.Paragraph("If you’re concerned about information censorship and want to contribute to the #GnoWorldOrder, follow our socials to find out how."), + ui.Paragraph("Gno.land is in building mode. If you want to help lay the foundations of a fairer and freer world through innovative ideas and exceptional code, join us today."), + } +} + +func worxDAO() ui.Element { + // WorxDAO + // XXX(manfred): please, let me finish a v0, then we can iterate + // highest level == highest responsibility + // teams are responsible for components they don't owne + // flag : realm maintainers VS facilitators + // teams + // committee of trustees to create the directory + // each directory is a name, has a parent and have groups + // homepage team - blocks aggregating events + // XXX: TODO + /*` + # Directory + + * gno.land (owned by group) + * + * gnovm + * gnolang (language) + * gnovm + - current challenges / concerns / issues + * tm2 + * amino + * + + ## Contributors + ``*/ + return ui.Element{ + ui.H3("WorxDAO (WIP)"), + ui.Text(`- A + - A1 + - A1A + - A1B + - A2 + - A3 + - A3A + - A3A1 +- B +- C`), + } +} + +func quoteOfTheBlock() ui.Element { + quotes := []string{ + "Gno is for Truth.", + "Gno is for Social Coordination.", + "Gno is _not only_ for DeFi.", + "Now, you Gno.", + "Come for the Go, Stay for the Gno.", + } + height := std.GetHeight() + idx := int(height) % len(quotes) + qotb := quotes[idx] + + return ui.Element{ + ui.H3(ufmt.Sprintf("Quote of the ~Day~Block#%d", height)), + ui.Quote(qotb), + } +} + +func socialLinks() ui.Element { + return ui.Element{ + ui.H3("Socials"), + ui.BulletList{ + // XXX: improve UI to support a nice GO api for such links + ui.Text("Check out our [community projects](https://github.com/gnolang/awesome-gno)"), + ui.Text("![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn)"), + ui.Text("![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland)"), + ui.Text("![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland)"), + ui.Text("![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland)"), + }, + } +} + +func packageStaffPicks() ui.Element { + // XXX: make it modifiable from a DAO + return ui.Element{ + ui.H3("Explore New Packages and Realms"), + ui.Columns{ + 3, + []ui.Element{ + { + ui.H4("r/gnoland"), + ui.BulletList{ + ui.Link{URL: "r/gnoland/blog"}, + ui.Link{URL: "r/gnoland/dao"}, + ui.Link{URL: "r/gnoland/faucet"}, + ui.Link{URL: "r/gnoland/home"}, + ui.Link{URL: "r/gnoland/pages"}, + }, + ui.H4("r/system"), + ui.BulletList{ + ui.Link{URL: "r/system/names"}, + ui.Link{URL: "r/system/rewards"}, + ui.Link{URL: "r/system/validators"}, + }, + }, { + ui.H4("r/demo"), + ui.BulletList{ + ui.Link{URL: "r/demo/boards"}, + ui.Link{URL: "r/demo/users"}, + ui.Link{URL: "r/demo/banktest"}, + ui.Link{URL: "r/demo/foo20"}, + ui.Link{URL: "r/demo/foo721"}, + ui.Link{URL: "r/demo/microblog"}, + ui.Link{URL: "r/demo/nft"}, + ui.Link{URL: "r/demo/types"}, + ui.Link{URL: "r/demo/art"}, + ui.Link{URL: "r/demo/groups"}, + ui.Text("..."), + }, + }, { + ui.H4("p/demo"), + ui.BulletList{ + ui.Link{URL: "p/demo/avl"}, + ui.Link{URL: "p/demo/blog"}, + ui.Link{URL: "p/demo/ui"}, + ui.Link{URL: "p/demo/ufmt"}, + ui.Link{URL: "p/demo/merkle"}, + ui.Link{URL: "p/demo/bf"}, + ui.Link{URL: "p/demo/flow"}, + ui.Link{URL: "p/demo/gnode"}, + ui.Link{URL: "p/demo/grc/grc20"}, + ui.Link{URL: "p/demo/grc/grc721"}, + ui.Text("..."), + }, + }, + }, + }, + } +} + +func discoverLinks() ui.Element { + return ui.Element{ + ui.Text(`
+
+ +### Learn about Gno.land + +- [About](/about) +- [GitHub](https://github.com/gnolang) +- [Subscribe](#subscribe) +- [Tokenomics (soon)](#) +- [Blog](/blog) +- [Events](/events) +- [Partners, Fund, Grants](/partners) + +
+ +
+ +### Build with Gnolang + +- [Gno dev with CLI (soon)](#) +- [Explore the Universe](/ecosystem) +- [Test in the browser (soon)](#) +- [About the Gnolang Language](/gnolang) +- [Docs/ Tutorials](https://github.com/gnolang) +- [Gno by example](https://gno-by-example.com/) +- [Getting started video (soon)](#) + +
+
+ +### Explore the universe + +- [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) +- [Install Gno Key instructions](/r/demo/boards:testboard/5) +- [Testnets 3](https://test3.gno.land/) +- [Testnets 2](https://test2.gno.land/) +- [Explorer links(soon)](#) +- [Testnet Tokens (faucet)](https://test3.gno.land/faucet) + +
+
`), + } +} diff --git a/examples/gno.land/r/gnoland/home/home_filetest.gno b/examples/gno.land/r/gnoland/home/home_filetest.gno new file mode 100644 index 00000000000..1fffc11792f --- /dev/null +++ b/examples/gno.land/r/gnoland/home/home_filetest.gno @@ -0,0 +1,188 @@ +package main + +import "gno.land/r/gnoland/home" + +func main() { + println(home.Render("")) +} + +// Output: +// # Welcome to Gno.land +// +// ### An interpretation of the Golang (Go) programming language for advanced developers and intrepid pioneers to build succinct, composable smart contracts for social coordination. +// +// +// If you’re concerned about information censorship and want to contribute to the #GnoWorldOrder, follow our socials to find out how. +// +// +// Gno.land is in building mode. If you want to help lay the foundations of a fairer and freer world through innovative ideas and exceptional code, join us today. +// +//
+// +// ### WorxDAO (WIP) +// +// - A +// - A1 +// - A1A +// - A1B +// - A2 +// - A3 +// - A3A +// - A3A1 +// - B +// - C +//
+// +// ### Explore New Packages and Realms +// +//
+//
+// +// #### r/gnoland +// +// - [r/gnoland/blog](r/gnoland/blog) +// - [r/gnoland/dao](r/gnoland/dao) +// - [r/gnoland/faucet](r/gnoland/faucet) +// - [r/gnoland/home](r/gnoland/home) +// - [r/gnoland/pages](r/gnoland/pages) +// +// #### r/system +// +// - [r/system/names](r/system/names) +// - [r/system/rewards](r/system/rewards) +// - [r/system/validators](r/system/validators) +// +//
+//
+// +// #### r/demo +// +// - [r/demo/boards](r/demo/boards) +// - [r/demo/users](r/demo/users) +// - [r/demo/banktest](r/demo/banktest) +// - [r/demo/foo20](r/demo/foo20) +// - [r/demo/foo721](r/demo/foo721) +// - [r/demo/microblog](r/demo/microblog) +// - [r/demo/nft](r/demo/nft) +// - [r/demo/types](r/demo/types) +// - [r/demo/art](r/demo/art) +// - [r/demo/groups](r/demo/groups) +// - ... +// +//
+//
+// +// #### p/demo +// +// - [p/demo/avl](p/demo/avl) +// - [p/demo/blog](p/demo/blog) +// - [p/demo/ui](p/demo/ui) +// - [p/demo/ufmt](p/demo/ufmt) +// - [p/demo/merkle](p/demo/merkle) +// - [p/demo/bf](p/demo/bf) +// - [p/demo/flow](p/demo/flow) +// - [p/demo/gnode](p/demo/gnode) +// - [p/demo/grc/grc20](p/demo/grc/grc20) +// - [p/demo/grc/grc721](p/demo/grc/grc721) +// - ... +// +//
+//
+// +// +// --- +// +//
+//
+// +// ### Last Blogposts +// +// +//
+//
+// +// ### Upcoming Events +// +// TODO: import r/gnoland/events +//
+//
+// +// ### Last Contributions +// +// TODO: import r/gh +// [#1134](https://github.com/gnolang/gno/pull/1134) +//
+//
+// +//
+// +//
+//
+// +// ### Learn about Gno.land +// +// - [About](/about) +// - [GitHub](https://github.com/gnolang) +// - [Subscribe](#subscribe) +// - [Tokenomics (soon)](#) +// - [Blog](/blog) +// - [Events](/events) +// - [Partners, Fund, Grants](/partners) +// +//
+// +//
+// +// ### Build with Gnolang +// +// - [Gno dev with CLI (soon)](#) +// - [Explore the Universe](/ecosystem) +// - [Test in the browser (soon)](#) +// - [About the Gnolang Language](/gnolang) +// - [Docs/ Tutorials](https://github.com/gnolang) +// - [Gno by example](https://gno-by-example.com/) +// - [Getting started video (soon)](#) +// +//
+//
+// +// ### Explore the universe +// +// - [Discover demo packages](https://github.com/gnolang/gno/tree/master/examples) +// - [Install Gno Key instructions](/r/demo/boards:testboard/5) +// - [Testnets 3](https://test3.gno.land/) +// - [Testnets 2](https://test2.gno.land/) +// - [Explorer links(soon)](#) +// - [Testnet Tokens (faucet)](https://test3.gno.land/faucet) +// +//
+//
+//
+// +// +//
+//
+// +// ### Socials +// +// - Check out our [community projects](https://github.com/gnolang/awesome-gno) +// - ![Discord](static/img/ico-discord.svg) [Discord](https://discord.gg/S8nKUqwkPn) +// - ![Twitter](static/img/ico-twitter.svg) [Twitter](https://twitter.com/_gnoland) +// - ![Youtube](static/img/ico-youtube.svg) [Youtube](https://www.youtube.com/@_gnoland) +// - ![Telegram](static/img/ico-telegram.svg) [Telegram](https://t.me/gnoland) +// +//
+//
+// +// ### Quote of the ~Day~Block#123 +// +// > Now, you Gno. +// +//
+//
+// +// +// --- +// +// **This is a testnet.** +// Package names are not guaranteed to be available for production. diff --git a/gno.land/cmd/gnoweb/pages/ABOUT.md b/examples/gno.land/r/gnoland/pages/page_about.gno similarity index 73% rename from gno.land/cmd/gnoweb/pages/ABOUT.md rename to examples/gno.land/r/gnoland/pages/page_about.gno index a5678a7349a..9aba4e39f76 100644 --- a/gno.land/cmd/gnoweb/pages/ABOUT.md +++ b/examples/gno.land/r/gnoland/pages/page_about.gno @@ -1,4 +1,10 @@ -# About Gno.land +package gnopages + +func init() { + path := "about" + title := "Gno.land Is A Platform To Write Smart Contracts In Gnolang (Gno)" + // XXX: description := "On Gno.land, developers write smart contracts and other blockchain apps using Gnolang (Gno) without learning a language that’s exclusive to a single ecosystem." + body := `# About Gno.land Gno.land is a platform to write smart contracts in Gnolang (Gno). Using an interpreted version of the general-purpose programming language Golang (Go), developers can write smart contracts and other blockchain apps without having to learn a language that’s exclusive to a single ecosystem. @@ -9,4 +15,6 @@ Proof of Contribution rewards contributors from technical and non-technical back This consensus mechanism also achieves higher security with fewer validators, optimizing resources for a greener, more sustainable, and enduring blockchain ecosystem. Any blockchain using Gnolang achieves succinctness, composability, expressivity, and completeness not found in any other smart contract platform. -By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today. +By observing a minimal structure, the design can endure over time and challenge the regime of information censorship we’re living in today.` + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_ecosystem.gno b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno new file mode 100644 index 00000000000..68969c44529 --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_ecosystem.gno @@ -0,0 +1,35 @@ +package gnopages + +func init() { + var ( + path = "ecosystem" + title = "Discover Gno.land Ecosystem Projects & Initiatives" + // XXX: description = "Dive further into the Gno.land ecosystem and discover the core infrastructure, projects, smart contracts, and tooling we’re building." + body = `# Gno Ecosystem + +## Gno.land Space + +For the best onboarding experience, head over to [Gno.land Space](https://www.gnoland.space/) open ecosystem. Here you can set up your Gno wallet, explore existing community-written Gno smart contracts (realms), and become part of our vibrant community by joining [Gno.land Discord](https://discord.com/invite/x76qK4ttHC). + +## Gno Studio (IDE) + +Gno IDE is a web-based application helping builders quickly spin up Gno realms and packages right on their browsers. Offering a smooth and intuitive UX for building on Gno, you’ll find multiple modes for customizability with all the features you’d expect from an IDE, such as auto compilation in the editor, debugging, and extensive testing capability. + +## Gnoscan + +Developed by the Onbloc team, Gnoscan is Gno.land’s blockchain explorer. Anyone can use Gnoscan to easily find information that resides on the Gno.land blockchain, such as wallet addresses, TX hashes, blocks, and contracts. Gnoscan makes our on-chain data easy to read and intuitive to discover. [Go to Gnoscan.](https://gnoscan.io/) + +## Adena + +Adena is a user-friendly non-custodial wallet for Gno.land. Open-source and developed by Onbloc, Adena currently powers all transactions on Gno.land, allowing gnomes to interact easily with the chain. With an emphasis on UX, Adena is built to handle millions of realms and tokens with a high-quality interface, support for NFTs and custom tokens, and seamless integration. [Get started here.](https://adena.app/) + +## Gnoswap + +Gnoswap is currently under development and led by the Onbloc team. Gnoswap will be the first DEX on Gno.land and is an automated market maker (AMM) protocol written in Gnolang that allows for permissionless token exchanges on the platform. + +## Gno.land Developer Portal + +Through the Gno.land Developer Portal, new developers can explore the exciting world of Gnolang (Gno), a novel programming language that powers the Gno.land blockchain. If you want to interact with Gno.land, start writing a realm, build a dApp, or even port a Solidity contract to a Gnolang realm, you’ll find the resources to [get started here](https://docs.onbloc.xyz/).` + ) + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_events.gno b/examples/gno.land/r/gnoland/pages/page_events.gno new file mode 100644 index 00000000000..18e7faeb3d3 --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_events.gno @@ -0,0 +1,151 @@ +package gnopages + +func init() { + var ( + path = "events" + title = "Gno.land Core Team Attends Industry Events & Meetups" + // XXX: description = "If you’re interested in learning more about Gno.land, you can join us at major blockchain industry events throughout the year either in person or virtually." + body = `# Events + +If you’re interested in building web3 with us, catch up with Gno.land in person at one of our industry events. We’re looking to connect with developers and like-minded thinkers who can contribute to the growth of our platform. + +--- + +## Upcoming Events + +
+
+ +### EthCC + +- **Come Meet Us at our Booth** +- Paris, July 17 - 20, 2023 +- Manfred Touron + +[Learn more](https://www.ethcc.io/) + +
+
+ +### Nebular Summit Gno.land for Developers + +- Paris, July 24 - 25, 2023 +- Manfred Touron + +[Learn more](https://www.nebular.builders/) + +
+
+ +### GopherCon EU + +- **Come Meet Us at our Booth** +- Berlin, July 26 - 29, 2023 + +[Learn more](https://gophercon.eu/) + +
+ +
+ +### GopherCon US + +- **Come Meet Us at our Booth** +- San Diego, September 26 - 29, 2023 + +[Learn more](https://www.gophercon.com/) + +
+
+ +--- + +## Past Events + +
+ +
+ +### Eth Seoul + +- **The Evolution of Smart Contracts: A Journey into Gno.land** +- Seoul, June 3, 2023 +- Manfred Touron + +[Learn more](https://2023.ethseoul.org/) + +
+
+ +### BUIDL Asia + +- **Proof of Contribution in Gno.land** +- Seoul, June 6, 2023 +- Manfred Touron + +[Learn more](https://www.buidl.asia/) + +
+
+ +### Game Developer Conference + +- **Side Event: Web3 Gaming Apps Powered by Gno** +- San Francisco, Mach 23, 2023 +- Jae Kwon + +[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) + +
+
+ +### EthDenver + +- **Side Event: Discover Gno.land** +- Denver, Feb 24 - Mar 5, 2023 +- Jae Kwon + +[Watch the talk](https://www.youtube.com/watch?v=IJ0xel8lr4c) + +
+
+ +### Istanbul Blockchain Week + +- Istanbul, Nov 14 - 17, 2022 +- Manfred Touron + +[Watch the talk](https://www.youtube.com/watch?v=JX0gdWT0Cg4) + +
+
+ +### Web Summit Buckle Up and Build with Cosmos + +- Lisbon, Nov 1 - 4, 2022 +- Manfred Touron + +
+
+ +### Cosmoverse + +- Medallin, Sept 26 - 28, 2022 +- Manfred Touron + +[Watch the talk](https://www.youtube.com/watch?v=6s1zG7hgxMk) + +
+
+ +### Berlin Blockchain Week Buckle Up and Build with Cosmos + +- Berlin, Sept 11 - 18, 2022 + +[Watch the talk](https://www.youtube.com/watch?v=hCLErPgnavI) + +
+
` + ) + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_gnolang.gno b/examples/gno.land/r/gnoland/pages/page_gnolang.gno new file mode 100644 index 00000000000..f0c2bfe276d --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_gnolang.gno @@ -0,0 +1,43 @@ +package gnopages + +func init() { + var ( + path = "gnolang" + title = "Gnolang (Gno) Is a Complete Language for Blockchain" + // XXX: description = "Gnolang (Gno) is an interpretation of the popular Golang (Go) language for blockchain created by Tendermint and Cosmos founder Jae Kwon." + body = `# About the Gnolang, the Gno Language + +[Gnolang](https://github.com/gnolang/gno/blob/master/LICENSE.md) (Gno) is an interpretation of the widely-used Golang (Go) programming language for blockchain created by Cosmos co-founder Jae Kwon in 2022 to mark a new era in smart contracting. Gno is ~99% identical to Go, so Go programmers can start coding in Gno right away, with a minimal learning curve. For example, Gno comes with blockchain-specific standard libraries, but any code that doesn’t use blockchain-specific logic can run in Go with minimal processing. Libraries that don’t make sense in the blockchain context, such as network or operating-system access, are not available in Gno. Otherwise, Gno loads and uses many standard libraries that power Go, so most of the parsing of the source code is the same. + +Under the hood, the Gno code is parsed into an abstract syntax tree (AST) and the AST itself is used in the interpreter, rather than bytecode as in many virtual machines such as Java, Python, or Wasm. This makes even the GnoVM accessible to any Go programmer. The novel design of the intuitive GnoVM interpreter allows Gno to freeze and resume the program by persisting and loading the entire memory state. Gno is deterministic, auto-persisted, and auto-Merkle-ized, allowing (smart contract) programs to be succinct, as the programmer doesn’t have to serialize and deserialize objects to persist them into a database (unlike programming applications with the Cosmos SDK). + +## How Gno Differs from Go + +![Gno and Go differences](static/img/gno-language/go-and-gno.jpg) + +The composable nature of Go/Gno allows for type-checked interactions between contracts, making Gno.land safer and more powerful, as well as operationally cheaper and faster. Smart contracts on Gno.land are light, simple, more focused, and easily interoperable—a network of interconnected contracts rather than siloed monoliths that limit interactions with other contracts. + +![Example of Gno code](static/img/gno-language/code-example.jpg) + +## Gno Inherits Go’s Built-in Security Features + +Go supports secure programming through exported/non-exported fields, enabling a “least-authority” design. It is easy to create objects and APIs that expose only what should be accessible to callers while hiding what should not be simply by the capitalization of letters, thus allowing a succinct representation of secure logic that can be called by multiple users. + +Another major advantage of Go is that the language comes with an ecosystem of great tooling, like the compiler and third-party tools that statically analyze code. Gno inherits these advantages from Go directly to create a smart contract programming language that provides embedding, composability, type-check safety, and garbage collection, helping developers to write secure code relying on the compiler, parser, and interpreter to give warning alerts for common mistakes. + +## Gno vs Solidity + +The most widely-adopted smart contract language today is Ethereum’s EVM-compatible Solidity. With bytecode built from the ground up and Turing complete, Solidity opened up a world of possibilities for decentralized applications (dApps) and there are currently more than 10 million contracts deployed on Ethereum. However, Solidity provides limited tooling and its EVM has a stack limit and computational inefficiencies. + +Solidity is designed for one purpose only (writing smart contracts) and is bound by the limitations of the EVM. In addition, developers have to learn several languages if they want to understand the whole stack or work across different ecosystems. Gno aspires to exceed Solidity on multiple fronts (and other smart contract languages like CosmWasm or Substrate) as every part of the stack is written in Gno. It’s easy for developers to understand the entire system just by studying a relatively small code base. + +## Gno Is Essential for the Wider Adoption of Web3 + +Gno makes imports as easy as they are in web2 with runtime-based imports for seamless dependency flow comprehension, and support for complex structs, beyond primitive types. Gno is ultimately cost-effective as dependencies are loaded once, enabling remote function calls as local, and providing automatic and independent per-realm state persistence. + +Using Gno, developers can rapidly accelerate application development and adopt a modular structure by reusing and reassembling existing modules without building from scratch. They can embed one structure inside another in an intuitive way while preserving localism, and the language specification is simple, successfully balancing practicality and minimalism. + +The Go language is so well designed that the Gno smart contract system will become the new gold standard for smart contract development and other blockchain applications. As a programming language that is universally adopted, secure, composable, and complete, Gno is essential for the broader adoption of web3 and its sustainable growth.` + ) + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_gor.gno b/examples/gno.land/r/gnoland/pages/page_gor.gno new file mode 100644 index 00000000000..3a6bb022e09 --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_gor.gno @@ -0,0 +1,221 @@ +package gnopages + +func init() { + path := "gor" + title := "Game of Realms Content For The Best Contributors" + // XXX: description := "Game of Realms is the first high-stakes competition held in two phases to find the best contributors to the Gno.land platform with a 133,700 ATOM prize pool." + body := `# Game of Realms + +
+ +### Game of Realms + +The first high-stakes contest will see participants compete for tiered membership to co-own the Gno.land blockchain. A series of complex technical and non-technical tasks will challenge contributors to create innovative patterns that push the chain to new limits. Start building the foundation for tomorrow through key smart contracts and other contributions that change our understanding of the world. + +
+ +The competition is currently in phase one – for advanced developers only. + +Once the necessary tools to start phase two are ready, we’ll open up the competition to newer devs and non-technical contributors. + +If you want to stack ATOM rewards and play a key role in the success of Gno.land and web3, read more about Game of Realms or open a [PR](https://github.com/gnolang/gno/) today. + +
+ +
+
+
+ +## Phase I. (ongoing) + +- + +- + +- + +
+
+ +## Phase II. (Locked) + +
+
+
+ +
+ +
+ +## Evaluation DAO + +This complex challenge seeks your skills in DAO development and implementation and is one of the most important challenges of phase one. The Evaluation DAO will ensure that contributions in Game of Realms and the Gno.land platform are fairly rewarded. + +
+ + + + + + + +
+ +Game of Realms participants and core contributors are still in discussions, proposing additional ideas, and seeing how the proposal for the Evaluation DAO evolves over time. + +
+ + + +
+ +See [GitHub issue 519](https://github.com/gnolang/gno/issues/519) for the most up-to-date discussion so far on how voting should work for the DAO, what the responsibilities are, how to join, etc. + +
+ + + + + + + + + + + + + + + + + +
+
+ +
+ +## Tutorials + +To progress to phase two of the competition, we need high-quality tutorials, guides, and documentation from phase one participants. Help to create materials that will onboard more contributors to Gno.land. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +## Governance Module + +Can you define and implement a governance contract suite that rivals existing ones, such as the Cosmos Hub? Show us how! We’re looking for the fairest and most efficient governance solution possible. + +
+ + + + + + + +
+ +Game of Realms participants and core contributors have made significant progress teaming up to complete this challenge but discussions and additional ideas are still ongoing. + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +## Register Now + + +
+
+ +
+
+ + +
+ +
+ + +
+ + +
+
+
+ +` + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_partners.gno b/examples/gno.land/r/gnoland/pages/page_partners.gno new file mode 100644 index 00000000000..440302437fa --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_partners.gno @@ -0,0 +1,21 @@ +package gnopages + +func init() { + path := "partners" + title := "Partners" + // XXX: description := """ + body := `## Partnerships + +### Fund and Grants Program + +Are you a builder, tinkerer, or researcher? If you’re looking to create awesome dApps, tooling, infrastructure, or smart contract libraries on Gno.land, you can apply for a grant. The Gno.land Ecosystem Fund and Grants program provides financial contributions for individuals and teams to innovate on the platform. + +
+ +[More information here](https://github.com/gnolang/ecosystem-fund-grants) + +
+` + + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_start.gno b/examples/gno.land/r/gnoland/pages/page_start.gno new file mode 100644 index 00000000000..a36ec6e52b1 --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_start.gno @@ -0,0 +1,21 @@ +package gnopages + +func init() { + path := "start" + title := "Getting Started with Gno" + // XXX: description := "" + + // TODO: codegen to use README files here + + /* TODO: port previous message: This is a demo of Gno smart contract programming. This document was + constructed by Gno onto a smart contract hosted on the data Realm + name ["gno.land/r/demo/boards"](https://gno.land/r/demo/boards/) + ([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)). + */ + body := `## Getting Started with Gno + +- [Install Gno Key](/r/demo/boards:testboard/5) +- TODO: add more links +` + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_testnets.gno b/examples/gno.land/r/gnoland/pages/page_testnets.gno new file mode 100644 index 00000000000..b6c09ab71ee --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_testnets.gno @@ -0,0 +1,19 @@ +package gnopages + +func init() { + path := "testnets" + title := "Gno.land Testnets" + // XXX: description := """ + body := `## Other testnets + +- **[staging.gno.land](https://staging.gno.land) (wiped every commit to master)** +- _[test3.gno.land](https://test3.gno.land) (latest)_ +- _[test2.gno.land](https://test2.gno.land) (archive)_ +- _[test1.gno.land](https://test1.gno.land) (archive)_ + +## Local devnet + +See CONTRIBUTING.md on GitHub. +` + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/page_tokenomics.gno b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno new file mode 100644 index 00000000000..de899ae0a70 --- /dev/null +++ b/examples/gno.land/r/gnoland/pages/page_tokenomics.gno @@ -0,0 +1,11 @@ +package gnopages + +func init() { + var ( + path = "tokenomics" + title = "Gno.land Tokenomics" + // XXX: description = """ + body = `Lorem Ipsum` + ) + _ = b.NewPost("", path, title, body, nil) +} diff --git a/examples/gno.land/r/gnoland/pages/pages.gno b/examples/gno.land/r/gnoland/pages/pages.gno index dbc3d855880..6e1f117d1d5 100644 --- a/examples/gno.land/r/gnoland/pages/pages.gno +++ b/examples/gno.land/r/gnoland/pages/pages.gno @@ -4,16 +4,12 @@ import ( "gno.land/p/demo/blog" ) -var b = &blog.Blog{ - Title: "Gnoland's Pages", - Prefix: "/r/gnoland/pages:", -} +// TODO: switch from p/blog to p/pages -func init() { - _ = b.NewPost("", "gor", "Game of Realms", "Lorem Ipsum", nil) - _ = b.NewPost("", "events", "Events", "Lorem Ipsum", nil) - _ = b.NewPost("", "tokenomics", "Tokenomics", "Lorem Ipsum", nil) - _ = b.NewPost("", "start", "Getting Started", "Lorem Ipsum", nil) +var b = &blog.Blog{ + Title: "Gnoland's Pages", + Prefix: "/r/gnoland/pages:", + NoBreadcrumb: true, } func Render(path string) string { diff --git a/examples/gno.land/r/gnoland/pages/pages_test.gno b/examples/gno.land/r/gnoland/pages/pages_test.gno index 1a43153e2c8..5a6fe84ad38 100644 --- a/examples/gno.land/r/gnoland/pages/pages_test.gno +++ b/examples/gno.land/r/gnoland/pages/pages_test.gno @@ -6,48 +6,42 @@ import ( "testing" ) -func TestPackage(t *testing.T) { - std.TestSetOrigCaller(std.Address("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq")) - - author := std.GetOrigCaller() - - // by default, lorem ipsum posts - { - got := Render("") - expected := ` -# Gnoland's Pages - -
- -## [Events](/r/gnoland/pages:p/events) -**[Learn More](/r/gnoland/pages:p/events)** - -
- -## [Game of Realms](/r/gnoland/pages:p/gor) -**[Learn More](/r/gnoland/pages:p/gor)** - -
- -## [Getting Started](/r/gnoland/pages:p/start) -**[Learn More](/r/gnoland/pages:p/start)** - -
- -## [Tokenomics](/r/gnoland/pages:p/tokenomics) -**[Learn More](/r/gnoland/pages:p/tokenomics)** - -
-` - assertMDEquals(t, got, expected) +func TestHome(t *testing.T) { + printedOnce := false + got := Render("") + expectedSubtrings := []string{ + "/r/gnoland/pages:p/events", + "/r/gnoland/pages:p/tokenomics", + "/r/gnoland/pages:p/start", + "/r/gnoland/pages:p/gor", + "/r/gnoland/pages:p/about", + "/r/gnoland/pages:p/gnolang", + } + for _, substring := range expectedSubtrings { + if !strings.Contains(got, substring) { + if !printedOnce { + println(got) + printedOnce = true + } + t.Errorf("expected %q, but not found.", substring) + } } } -func assertMDEquals(t *testing.T, got, expected string) { - t.Helper() - expected = strings.TrimSpace(expected) - got = strings.TrimSpace(got) - if expected != got { - t.Errorf("invalid render output.\nexpected %q.\ngot %q.", expected, got) +func TestAbout(t *testing.T) { + printedOnce := false + got := Render("p/about") + expectedSubtrings := []string{ + "# About Gno.land", + "Gno.land is a platform to write smart contracts in Gnolang (Gno).", + } + for _, substring := range expectedSubtrings { + if !strings.Contains(got, substring) { + if !printedOnce { + println(got) + printedOnce = true + } + t.Errorf("expected %q, but not found.", substring) + } } } diff --git a/examples/gno.land/r/manfred/present/admin.gno b/examples/gno.land/r/manfred/present/admin.gno new file mode 100644 index 00000000000..ff0cb075656 --- /dev/null +++ b/examples/gno.land/r/manfred/present/admin.gno @@ -0,0 +1,92 @@ +package present + +import ( + "std" + "strings" + + "gno.land/p/demo/avl" +) + +var ( + adminAddr std.Address + moderatorList avl.Tree + inPause bool +) + +func init() { + // adminAddr = std.GetOrigCaller() // FIXME: find a way to use this from the main's genesis. + adminAddr = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" +} + +func AdminSetAdminAddr(addr std.Address) { + assertIsAdmin() + adminAddr = addr +} + +func AdminSetInPause(state bool) { + assertIsAdmin() + inPause = state +} + +func AdminAddModerator(addr std.Address) { + assertIsAdmin() + moderatorList.Set(addr.String(), true) +} + +func AdminRemoveModerator(addr std.Address) { + assertIsAdmin() + moderatorList.Set(addr.String(), false) // XXX: delete instead? +} + +func ModAddPost(slug, title, body, tags string) { + assertIsModerator() + + caller := std.GetOrigCaller() + tagList := strings.Split(tags, ",") + err := b.NewPost(caller, slug, title, body, tagList) + checkErr(err) +} + +func ModEditPost(slug, title, body, tags string) { + assertIsModerator() + + tagList := strings.Split(tags, ",") + err := b.GetPost(slug).Update(title, body, tagList) + checkErr(err) +} + +func isAdmin(addr std.Address) bool { + return addr == adminAddr +} + +func isModerator(addr std.Address) bool { + _, found := moderatorList.Get(addr.String()) + return found +} + +func assertIsAdmin() { + caller := std.GetOrigCaller() + if !isAdmin(caller) { + panic("access restricted.") + } +} + +func assertIsModerator() { + caller := std.GetOrigCaller() + if isAdmin(caller) || isModerator(caller) { + return + } + panic("access restricted") +} + +func assertNotInPause() { + if inPause { + panic("access restricted (pause)") + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/examples/gno.land/r/manfred/present/gno.mod b/examples/gno.land/r/manfred/present/gno.mod new file mode 100644 index 00000000000..9d1ab5b0e56 --- /dev/null +++ b/examples/gno.land/r/manfred/present/gno.mod @@ -0,0 +1,6 @@ +module gno.land/r/manfred/present + +require ( + "gno.land/p/demo/avl" v0.0.0-latest + "gno.land/p/demo/blog" v0.0.0-latest +) diff --git a/examples/gno.land/r/manfred/present/present_miami23.gno b/examples/gno.land/r/manfred/present/present_miami23.gno new file mode 100644 index 00000000000..36b1980bb0b --- /dev/null +++ b/examples/gno.land/r/manfred/present/present_miami23.gno @@ -0,0 +1,44 @@ +package present + +func init() { + path := "miami23" + title := "Portal Loop Demo (Miami 2023)" + body := ` +# Portal Loop Demo (Miami 2023) + +Rendered by Gno. + +[Source (WIP)](https://github.com/gnolang/gno/pull/1176) + +## Portal Loop + +- DONE: Dynamic homepage, key pages, aliases, and redirects. +- TODO: Deploy with history, complete worxdao v0. +- Will replace the static gno.land site. +- Enhances local development. + +[GitHub Issue](https://github.com/gnolang/gno/issues/1108) + +## Roadmap + +- Crafting the roadmap this week, open to collaboration. +- Combining onchain (portal loop) and offchain (GitHub). +- Next week: Unveiling the official v0 roadmap. + +## Teams, DAOs, Projects + +- Developing worxDAO contracts for directories of projects and teams. +- GitHub teams and projects align with this structure. +- CODEOWNER file updates coming. +- Initial teams announced next week. + +## Tech Team Retreat Plan + +- Continue Portal Loop. +- Consider dApp development. +- Explore new topics [here](https://github.com/orgs/gnolang/projects/15/). +- Engage in workshops. +- Connect and have fun with colleagues. +` + _ = b.NewPost(adminAddr, path, title, body, []string{"demo", "portal-loop", "miami"}) +} diff --git a/examples/gno.land/r/manfred/present/present_miami23_filetest.gno b/examples/gno.land/r/manfred/present/present_miami23_filetest.gno new file mode 100644 index 00000000000..05c41905060 --- /dev/null +++ b/examples/gno.land/r/manfred/present/present_miami23_filetest.gno @@ -0,0 +1,57 @@ +package main + +import "gno.land/r/manfred/present" + +func main() { + println(present.Render("")) + println("------------------------------------") + println(present.Render("p/miami23")) +} + +// Output: +//
+// +// ## [Portal Loop Demo (Miami 2023)](/r/manfred/present:p/miami23) +// **[Learn More](/r/manfred/present:p/miami23)** +// +//
+// ------------------------------------ +// # Portal Loop Demo (Miami 2023) +// +// Rendered by Gno. +// +// [Source (WIP)](https://github.com/gnolang/gno/pull/1176) +// +// ## Portal Loop +// +// - DONE: Dynamic homepage, key pages, aliases, and redirects. +// - TODO: Deploy with history, complete worxdao v0. +// - Will replace the static gno.land site. +// - Enhances local development. +// +// [GitHub Issue](https://github.com/gnolang/gno/issues/1108) +// +// ## Roadmap +// +// - Crafting the roadmap this week, open to collaboration. +// - Combining onchain (portal loop) and offchain (GitHub). +// - Next week: Unveiling the official v0 roadmap. +// +// ## Teams, DAOs, Projects +// +// - Developing worxDAO contracts for directories of projects and teams. +// - GitHub teams and projects align with this structure. +// - CODEOWNER file updates coming. +// - Initial teams announced next week. +// +// ## Tech Team Retreat Plan +// +// - Continue Portal Loop. +// - Consider dApp development. +// - Explore new topics [here](https://github.com/orgs/gnolang/projects/15/). +// - Engage in workshops. +// - Connect and have fun with colleagues. +// +// [#demo](/r/manfred/present:t/demo) [#portal-loop](/r/manfred/present:t/portal-loop) [#miami](/r/manfred/present:t/miami) +// +// by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 1970-01-01 12:00am UTC diff --git a/examples/gno.land/r/manfred/present/presentations.gno b/examples/gno.land/r/manfred/present/presentations.gno new file mode 100644 index 00000000000..8a99f502e86 --- /dev/null +++ b/examples/gno.land/r/manfred/present/presentations.gno @@ -0,0 +1,17 @@ +package present + +import ( + "gno.land/p/demo/blog" +) + +// TODO: switch from p/blog to p/present + +var b = &blog.Blog{ + Title: "Manfred's Presentations", + Prefix: "/r/manfred/present:", + NoBreadcrumb: true, +} + +func Render(path string) string { + return b.Render(path) +} diff --git a/gno.land/Makefile b/gno.land/Makefile index 1fd1aaa1f78..be1db280c40 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -5,6 +5,9 @@ help: rundep=go run -modfile ../misc/devdeps/go.mod +.PHONY: gnoland.start +gnoland.start:; go run ./cmd/gnoland start + .PHONY: build build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index e8a2feac0d7..0d9398cb8e2 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -18,7 +18,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - osm "github.com/gnolang/gno/tm2/pkg/os" "github.com/gnolang/gno/tm2/pkg/std" "github.com/gorilla/mux" "github.com/gotuna/gotuna" @@ -32,59 +31,93 @@ const ( qFileStr = "vm/qfile" ) +var startedAt time.Time + var flags struct { - bindAddr string - remoteAddr string - captchaSite string - faucetURL string - viewsDir string - pagesDir string - helpChainID string - helpRemote string + BindAddr string + RemoteAddr string + CaptchaSite string + FaucetURL string + ViewsDir string + HelpChainID string + HelpRemote string + WithAnalytics bool } -var startedAt time.Time - func init() { - flag.StringVar(&flags.remoteAddr, "remote", "127.0.0.1:26657", "remote gnoland node address") - flag.StringVar(&flags.bindAddr, "bind", "127.0.0.1:8888", "server listening address") - flag.StringVar(&flags.captchaSite, "captcha-site", "", "recaptcha site key (if empty, captcha are disabled)") - flag.StringVar(&flags.faucetURL, "faucet-url", "http://localhost:5050", "faucet server URL") - flag.StringVar(&flags.viewsDir, "views-dir", "./cmd/gnoweb/views", "views directory location") - flag.StringVar(&flags.pagesDir, "pages-dir", "./cmd/gnoweb/pages", "pages directory location") - flag.StringVar(&flags.helpChainID, "help-chainid", "dev", "help page's chainid") - flag.StringVar(&flags.helpRemote, "help-remote", "127.0.0.1:26657", "help page's remote addr") + flag.StringVar(&flags.RemoteAddr, "remote", "127.0.0.1:26657", "remote gnoland node address") + flag.StringVar(&flags.BindAddr, "bind", "127.0.0.1:8888", "server listening address") + flag.StringVar(&flags.CaptchaSite, "captcha-site", "", "recaptcha site key (if empty, captcha are disabled)") + flag.StringVar(&flags.FaucetURL, "faucet-url", "http://localhost:5050", "faucet server URL") + flag.StringVar(&flags.ViewsDir, "views-dir", "./cmd/gnoweb/views", "views directory location") // XXX: replace with goembed + flag.StringVar(&flags.HelpChainID, "help-chainid", "dev", "help page's chainid") + flag.StringVar(&flags.HelpRemote, "help-remote", "127.0.0.1:26657", "help page's remote addr") + flag.BoolVar(&flags.WithAnalytics, "with-analytics", false, "enable privacy-first analytics") startedAt = time.Now() } func makeApp() gotuna.App { app := gotuna.App{ - ViewFiles: os.DirFS(flags.viewsDir), + ViewFiles: os.DirFS(flags.ViewsDir), Router: gotuna.NewMuxRouter(), Static: static.EmbeddedStatic, // StaticPrefix: "static/", } - app.Router.Handle("/", handlerHome(app)) - app.Router.Handle("/about", handlerAbout(app)) - app.Router.Handle("/game-of-realms", handlerGor(app)) - app.Router.Handle("/faucet", handlerFaucet(app)) - app.Router.Handle("/r/demo/boards:gnolang/6", handlerRedirect(app)) + + // realm aliases + aliases := map[string]string{ + "/": "/r/gnoland/home", + "/about": "/r/gnoland/pages:p/about", + "/gnolang": "/r/gnoland/pages:p/gnolang", + "/ecosystem": "/r/gnoland/pages:p/ecosystem", + "/partners": "/r/gnoland/pages:p/partners", + "/testnets": "/r/gnoland/pages:p/testnets", + "/start": "/r/gnoland/pages:p/start", + "/game-of-realms": "/r/gnoland/pages:p/gor", // XXX: replace with gor realm + "/events": "/r/gnoland/pages:p/events", // XXX: replace with events realm + } + for from, to := range aliases { + app.Router.Handle(from, handlerRealmAlias(app, to)) + } + // http redirects + redirects := map[string]string{ + "/r/demo/boards:gnolang/6": "/r/demo/boards:gnolang/3", // XXX: temporary + "/blog": "/r/gnoland/blog", + "/gor": "/game-of-realms", + "/grants": "/partners", + "/language": "/gnolang", + "/getting-started": "/start", + } + for from, to := range redirects { + app.Router.Handle(from, handlerRedirect(app, to)) + } + // realm routes // NOTE: see rePathPart. app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}/{filename:(?:.*\\.(?:gno|md|txt)$)?}", handlerRealmFile(app)) app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}", handlerRealmMain(app)) app.Router.Handle("/r/{rlmname:[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)+}:{querystr:.*}", handlerRealmRender(app)) app.Router.Handle("/p/{filepath:.*}", handlerPackageFile(app)) + + // other + app.Router.Handle("/faucet", handlerFaucet(app)) app.Router.Handle("/static/{path:.+}", handlerStaticFile(app)) app.Router.Handle("/favicon.ico", handlerFavicon(app)) + + // api app.Router.Handle("/status.json", handlerStatusJSON(app)) + + app.Router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := r.RequestURI + handleNotFound(app, path, w, r) + }) return app } func main() { flag.Parse() - fmt.Printf("Running on http://%s\n", flags.bindAddr) + fmt.Printf("Running on http://%s\n", flags.BindAddr) server := &http.Server{ - Addr: flags.bindAddr, + Addr: flags.BindAddr, ReadHeaderTimeout: 60 * time.Second, Handler: makeApp().Router, } @@ -94,50 +127,59 @@ func main() { } } -func handlerHome(app gotuna.App) http.Handler { - md := filepath.Join(flags.pagesDir, "HOME.md") - homeContent := osm.MustReadFile(md) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Title", "Gno.land Smart Contract Platform Using Gnolang (Gno)"). - Set("Description", "Gno.land is the only smart contract platform using the Gnolang (Gno) programming language, an interpretation of the widely-used Golang (Go)."). - Set("HomeContent", string(homeContent)). - Render(w, r, "home.html", "funcs.html") - }) -} - -func handlerAbout(app gotuna.App) http.Handler { - md := filepath.Join(flags.pagesDir, "ABOUT.md") - mainContent := osm.MustReadFile(md) - +// handlerRealmAlias is used to render official pages from realms. +// url is intended to be shorter. +// UX is intended to be more minimalistic. +// A link to the realm realm is added. +func handlerRealmAlias(app gotuna.App, rlmpath string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("Title", "Gno.land Is A Platform To Write Smart Contracts In Gnolang (Gno)"). - Set("Description", "On Gno.land, developers write smart contracts and other blockchain apps using Gnolang (Gno) without learning a language that’s exclusive to a single ecosystem."). - Set("MainContent", string(mainContent)). - Render(w, r, "generic.html", "funcs.html") - }) -} + rlmfullpath := "gno.land" + rlmpath + querystr := "" // XXX: "?gnoweb-alias=1" + parts := strings.Split(rlmpath, ":") + switch len(parts) { + case 1: // continue + case 2: // r/realm:querystr + rlmfullpath = "gno.land" + parts[0] + querystr = parts[1] + querystr + default: + panic("should not happen") + } + rlmname := strings.TrimPrefix(rlmfullpath, "gno.land/r/") + qpath := "vm/qrender" + data := []byte(fmt.Sprintf("%s\n%s", rlmfullpath, querystr)) + res, err := makeRequest(qpath, data) + if err != nil { + writeError(w, fmt.Errorf("gnoweb failed to query gnoland: %w", err)) + return + } -func handlerGor(app gotuna.App) http.Handler { - md := filepath.Join(flags.pagesDir, "GOR.md") - mainContent := osm.MustReadFile(md) + queryParts := strings.Split(querystr, "/") + pathLinks := []pathLink{} + for i, part := range queryParts { + pathLinks = append(pathLinks, pathLink{ + URL: "/r/" + rlmname + ":" + strings.Join(queryParts[:i+1], "/"), + Text: part, + }) + } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - app.NewTemplatingEngine(). - Set("MainContent", string(mainContent)). - Set("Title", "Game of Realms Content For The Best Contributors "). - Set("Description", "Game of Realms is the first high-stakes competition held in two phases to find the best contributors to the Gno.land platform with a 133,700 ATOM prize pool."). - Render(w, r, "generic.html", "funcs.html") + tmpl := app.NewTemplatingEngine() + // XXX: extract title from realm's output + // XXX: extract description from realm's output + tmpl.Set("RealmName", rlmname) + tmpl.Set("RealmPath", rlmpath) + tmpl.Set("Query", querystr) + tmpl.Set("PathLinks", pathLinks) + tmpl.Set("Contents", string(res.Data)) + tmpl.Set("Flags", flags) + tmpl.Set("IsAlias", true) + tmpl.Render(w, r, "realm_render.html", "funcs.html") }) } func handlerFaucet(app gotuna.App) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { app.NewTemplatingEngine(). - Set("captchaSite", flags.captchaSite). - Set("faucetURL", flags.faucetURL). + Set("Flags", flags). Render(w, r, "faucet.html", "funcs.html") }) } @@ -192,12 +234,13 @@ func handlerStatusJSON(app gotuna.App) http.Handler { }) } -// XXX temporary. -func handlerRedirect(app gotuna.App) http.Handler { +func handlerRedirect(app gotuna.App, to string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/r/boards:gnolang/3", http.StatusFound) - app.NewTemplatingEngine(). - Render(w, r, "home.html", "funcs.html") + http.Redirect(w, r, to, http.StatusFound) + tmpl := app.NewTemplatingEngine() + tmpl.Set("To", to) + tmpl.Set("Flags", flags) + tmpl.Render(w, r, "redirect.html", "funcs.html") }) } @@ -232,10 +275,9 @@ func handlerRealmMain(app gotuna.App) http.Handler { tmpl := app.NewTemplatingEngine() tmpl.Set("FuncName", funcName) tmpl.Set("RealmPath", rlmpath) - tmpl.Set("Remote", flags.helpRemote) - tmpl.Set("ChainID", flags.helpChainID) tmpl.Set("DirPath", pathOf(rlmpath)) tmpl.Set("FunctionSignatures", fsigs) + tmpl.Set("Flags", flags) tmpl.Render(w, r, "realm_help.html", "funcs.html") } else { // Ensure realm exists. TODO optimize. @@ -297,12 +339,14 @@ func handleRealmRender(app gotuna.App, w http.ResponseWriter, r *http.Request) { } // Render template. tmpl := app.NewTemplatingEngine() - + // XXX: extract title from realm's output + // XXX: extract description from realm's output tmpl.Set("RealmName", rlmname) tmpl.Set("RealmPath", rlmpath) tmpl.Set("Query", querystr) tmpl.Set("PathLinks", pathLinks) tmpl.Set("Contents", string(res.Data)) + tmpl.Set("Flags", flags) tmpl.Render(w, r, "realm_render.html", "funcs.html") } @@ -345,6 +389,7 @@ func renderPackageFile(app gotuna.App, w http.ResponseWriter, r *http.Request, d tmpl.Set("DirURI", diruri) tmpl.Set("DirPath", pathOf(diruri)) tmpl.Set("Files", files) + tmpl.Set("Flags", flags) tmpl.Render(w, r, "package_dir.html", "funcs.html") } else { // Request is for a file. @@ -362,6 +407,7 @@ func renderPackageFile(app gotuna.App, w http.ResponseWriter, r *http.Request, d tmpl.Set("DirPath", pathOf(diruri)) tmpl.Set("FileName", filename) tmpl.Set("FileContents", string(res.Data)) + tmpl.Set("Flags", flags) tmpl.Render(w, r, "package_file.html", "funcs.html") } } @@ -371,7 +417,7 @@ func makeRequest(qpath string, data []byte) (res *abci.ResponseQuery, err error) // Height: height, XXX // Prove: false, XXX } - remote := flags.remoteAddr + remote := flags.RemoteAddr cli := client.NewHTTP(remote, "/websocket") qres, err := cli.ABCIQueryWithOptions( qpath, data, opts2) @@ -432,11 +478,20 @@ func handleNotFound(app gotuna.App, path string, w http.ResponseWriter, r *http. app.NewTemplatingEngine(). Set("title", "Not found"). Set("path", path). + Set("Flags", flags). Render(w, r, "404.html", "funcs.html") } func writeError(w http.ResponseWriter, err error) { + // XXX: writeError should return an error page template. w.WriteHeader(500) + + details := errors.Unwrap(err).Error() + main := err.Error() + + fmt.Println("main", main) + fmt.Println("details", details) + w.Write([]byte(err.Error())) } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 579e1bcd06b..974d3f987b7 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -12,7 +12,11 @@ import ( ) func TestRoutes(t *testing.T) { - ok := http.StatusOK + const ( + ok = http.StatusOK + found = http.StatusFound + notFound = http.StatusNotFound + ) routes := []struct { route string status int @@ -32,6 +36,10 @@ func TestRoutes(t *testing.T) { {"/r/demo/deep/very/deep?help", ok, "exposed"}, {"/r/demo/deep/very/deep/", ok, "render.gno"}, {"/r/demo/deep/very/deep/render.gno", ok, "func Render("}, + {"/game-of-realms", ok, "/r/gnoland/pages:p/gor"}, + {"/gor", found, "/game-of-realms"}, + {"/blog", found, "/r/gnoland/blog"}, + {"/404-not-found", notFound, "/404-not-found"}, } if wd, err := os.Getwd(); err == nil { if strings.HasSuffix(wd, "cmd/gnoweb") { @@ -40,6 +48,14 @@ func TestRoutes(t *testing.T) { } else { panic("os.Getwd() -> err: " + err.Error()) } + + // configure default values + flags.RemoteAddr = "127.0.0.1:26657" + flags.HelpRemote = "127.0.0.1:26657" + flags.HelpChainID = "dev" + flags.CaptchaSite = "" + flags.ViewsDir = "./cmd/gnoweb/views" + flags.WithAnalytics = false app := makeApp() for _, r := range routes { @@ -48,8 +64,57 @@ func TestRoutes(t *testing.T) { response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) assert.Equal(t, r.status, response.Code) - assert.Equal(t, strings.Contains(response.Body.String(), r.substring), true) - println(response.Body.String()) + assert.Contains(t, response.Body.String(), r.substring) + // println(response.Body.String()) }) } } + +func TestAnalytics(t *testing.T) { + routes := []string{ + // special realms + "/", // home + "/about", + "/start", + + // redirects + "/game-of-realms", + "/getting-started", + "/blog", + "/boards", + + // realm, source, help page + "/r/gnoland/blog", + "/r/gnoland/blog/admin.gno", + "/r/demo/users:administrator", + "/r/gnoland/blog?help", + + // special pages + "/404-not-found", + } + + t.Run("with", func(t *testing.T) { + for _, route := range routes { + t.Run(route, func(t *testing.T) { + flags.WithAnalytics = true + app := makeApp() + request := httptest.NewRequest(http.MethodGet, route, nil) + response := httptest.NewRecorder() + app.Router.ServeHTTP(response, request) + assert.Contains(t, response.Body.String(), "simpleanalytics") + }) + } + }) + t.Run("without", func(t *testing.T) { + for _, route := range routes { + t.Run(route, func(t *testing.T) { + flags.WithAnalytics = false + app := makeApp() + request := httptest.NewRequest(http.MethodGet, route, nil) + response := httptest.NewRecorder() + app.Router.ServeHTTP(response, request) + assert.Equal(t, strings.Contains(response.Body.String(), "simpleanalytics"), false) + }) + } + }) +} diff --git a/gno.land/cmd/gnoweb/pages/GOR.md b/gno.land/cmd/gnoweb/pages/GOR.md deleted file mode 100644 index 95a155319fb..00000000000 --- a/gno.land/cmd/gnoweb/pages/GOR.md +++ /dev/null @@ -1,30 +0,0 @@ -# Game of Realms - -The first high-stakes contest will see participants compete for the tiered membership to co-own the Gno.land blockchain. -A series of complex technical and non-technical tasks will challenge contributors to create innovative patterns that push the chain to new limits. -Start building the foundation for tomorrow through key smart contracts and other contributions that change our understanding of the world. - -You can start participating in the co-creation of the Game of Realms now by adding your contributions on the dedicated [GitHub page](https://github.com/gnolang/gno/issues/357) to help us formulate the challenges. We want to release the final challenges from your contributions. - -## Register Now - - -
-
- -
-
- - -
- -
- - -
- - -
-
-
- diff --git a/gno.land/cmd/gnoweb/pages/HOME.md b/gno.land/cmd/gnoweb/pages/HOME.md deleted file mode 100644 index 0f3bfaec685..00000000000 --- a/gno.land/cmd/gnoweb/pages/HOME.md +++ /dev/null @@ -1,50 +0,0 @@ -# Welcome to **Gno.land** - -- [About Gno.land](/about) -- [Blogs](/r/gnoland/blog) -- [Install `gnokey`](https://github.com/gnolang/gno/tree/master/gno.land/cmd/gnokey) -- [Acquire testnet tokens](/faucet) -- [Game of Realms](/game-of-realms) - An open worldwide competition for developers to build the best Gnolang smart-contracts. - -# Explore new packages. - -- r/gnoland - - [/r/gnoland/blog](/r/gnoland/blog) - - [/r/gnoland/faucet](/r/gnoland/faucet) -- r/system - - [/r/system/names](/r/system/names) - - [/r/system/rewards](/r/system/rewards) - - [/r/system/validators](/r/system/validators) -- r/demo - - [/r/demo/banktest](/r/demo/banktest) - - [/r/demo/boards](/r/demo/boards) - - [/r/demo/foo20](/r/demo/foo20) - - [/r/demo/nft](/r/demo/nft) - - [/r/demo/types](/r/demo/types) - - [/r/demo/users](/r/demo/users) - - [/r/demo/groups](/r/demo/groups) -- p/demo - - [/p/demo/avl](/p/demo/avl) - - [/p/demo/blog](/p/demo/blog) - - [/p/demo/flow](/p/demo/flow) - - [/p/demo/gnode](/p/demo/gnode) - - [/p/demo/grc/exts](/p/demo/grc/exts) - - [/p/demo/grc/grc20](/p/demo/grc/grc20) - - [/p/demo/grc/grc721](/p/demo/grc/grc721) - -# Other Testnets - -- **[staging.gno.land](https://staging.gno.land) (wiped every commit to master)** -- _[test3.gno.land](https://test3.gno.land) (latest)_ -- _[test2.gno.land](https://test2.gno.land) (archive)_ -- _[test1.gno.land](https://test1.gno.land) (archive)_ - -**This is a testnet.** -Package names are not guaranteed to be available for production. - -# Social - -Check out our [community projects](https://github.com/gnolang/awesome-gno). - -Official channel: [Discord](https://discord.gg/S8nKUqwkPn)
-Other channels: [Telegram](https://t.me/gnoland) [Twitter](https://twitter.com/_gnoland) [Youtube](https://www.youtube.com/@_gnoland) diff --git a/gno.land/cmd/gnoweb/views/404.html b/gno.land/cmd/gnoweb/views/404.html index c51543ca1cd..fee4fff8689 100644 --- a/gno.land/cmd/gnoweb/views/404.html +++ b/gno.land/cmd/gnoweb/views/404.html @@ -2,8 +2,7 @@ - - + {{ template "html_head" . }} 404 - Not Found @@ -13,6 +12,7 @@

{{.Data.path}}

+ {{ template "analytics" .}} {{- end -}} diff --git a/gno.land/cmd/gnoweb/views/faucet.html b/gno.land/cmd/gnoweb/views/faucet.html index 84bcc6f34e5..85d3d6780c5 100644 --- a/gno.land/cmd/gnoweb/views/faucet.html +++ b/gno.land/cmd/gnoweb/views/faucet.html @@ -2,8 +2,8 @@ + {{ template "html_head" . }} Gno.land - {{ template "html_head" }}
diff --git a/gno.land/cmd/gnoweb/views/funcs.html b/gno.land/cmd/gnoweb/views/funcs.html index 8ac2fe6328b..1f8b7a265dd 100644 --- a/gno.land/cmd/gnoweb/views/funcs.html +++ b/gno.land/cmd/gnoweb/views/funcs.html @@ -1,4 +1,4 @@ -{{ define "header_buttons" }} +{{- define "header_buttons" -}}
-{{ end }} {{ define "html_head" }} +{{- end -}} + +{{- define "html_head" -}} + +{{if .Data.Description}}{{end}} -{{ end }} {{ define "header_logo" }} +{{- end -}} + +{{- define "header_logo" -}} -{{ end }} {{ define "footer" }} +{{- end -}} + +{{- define "footer" -}}
Gno.land
-{{ end }} {{ define "js" }} +{{- end -}} + +{{- define "js" -}} + -{{ end }} {{ define "subscribe" }} +{{ template "analytics" .}} +{{- end -}} + +{{- define "analytics" -}} +{{- if .Data.Flags.WithAnalytics -}} + + + +{{- end -}} +{{- end -}} + +{{- define "subscribe" -}}
-{{ end }} +{{- end -}} diff --git a/gno.land/cmd/gnoweb/views/generic.html b/gno.land/cmd/gnoweb/views/generic.html index 117ea9d59fe..e671625e26a 100644 --- a/gno.land/cmd/gnoweb/views/generic.html +++ b/gno.land/cmd/gnoweb/views/generic.html @@ -3,8 +3,7 @@ Gno.land - {{ .Data.Title }} - - {{ template "html_head" }} + {{ template "html_head" . }}
@@ -16,7 +15,7 @@
{{ template "footer" }}
- {{ template "js" }} + {{ template "js" .}} {{- end -}} diff --git a/gno.land/cmd/gnoweb/views/home.html b/gno.land/cmd/gnoweb/views/home.html deleted file mode 100644 index a2bf78adb96..00000000000 --- a/gno.land/cmd/gnoweb/views/home.html +++ /dev/null @@ -1,21 +0,0 @@ -{{- define "app" -}} - - - - Gno.land - {{ .Data.Title }} - - {{ template "html_head" }} - - -
- -
-
{{ .Data.HomeContent }}
-
-
{{ template "subscribe" }}
- {{ template "footer" }} -
- {{ template "js" }} - - -{{- end -}} diff --git a/gno.land/cmd/gnoweb/views/package_dir.html b/gno.land/cmd/gnoweb/views/package_dir.html index 6b0bf5cfd48..efaf4d7ad0c 100644 --- a/gno.land/cmd/gnoweb/views/package_dir.html +++ b/gno.land/cmd/gnoweb/views/package_dir.html @@ -2,8 +2,8 @@ - Gno.land - {{ template "html_head" }} + {{ template "html_head" . }} + Gno.land - {{.Data.DirPath}}
@@ -12,13 +12,14 @@ {{ .Data.DirPath }}/*
{{ template "dir_contents" . }}
- {{ template "footer" }} - {{ template "js" }} + {{ template "js" . }} -{{- end -}} {{ define "dir_contents" }} +{{- end -}} + +{{- define "dir_contents" -}}
{{ $dirPath := .Data.DirPath }}
    @@ -29,4 +30,4 @@ {{ end }}
-{{ end }} +{{- end -}} diff --git a/gno.land/cmd/gnoweb/views/package_file.html b/gno.land/cmd/gnoweb/views/package_file.html index 7068854d16c..71aa8b68452 100644 --- a/gno.land/cmd/gnoweb/views/package_file.html +++ b/gno.land/cmd/gnoweb/views/package_file.html @@ -2,8 +2,8 @@ - Gno.land - {{ template "html_head" }} + {{ template "html_head" . }} + Gno.land - {{.Data.DirPath}}/{{.Data.FileName}}
@@ -18,7 +18,7 @@ {{ template "footer" }}
- {{ template "js" }} + {{ template "js" .}} - + {{ template "js" . }} -{{- end -}} +{{ define "func_specs" }} +{{- end -}} + +{{- define "func_specs" -}}
{{ $funcName := .Data.FuncName }} {{ $found := false }} {{ if eq $funcName "" }} {{ range .Data.FunctionSignatures }} {{ template "func_spec" . }} {{ end }} {{ else }} {{ range .Data.FunctionSignatures }} {{ if eq .FuncName $funcName }} {{ $found = true }} {{ template "func_spec" . }} {{ end }} {{ end }} {{ if not $found }} {{ $funcName }} not found. {{ end }} {{ end }}
-{{ end }} {{ define "func_spec" }} +{{- end -}} + +{{- define "func_spec" -}}
@@ -67,7 +69,9 @@
-{{ end }} {{ define "func_param" }} +{{- end -}} + +{{- define "func_param" -}} {{ .Name }} @@ -75,7 +79,9 @@ {{ .Type }} -{{ end }} {{ define "func_result" }} +{{- end -}} + +{{- define "func_result" -}} {{ .Name }} {{ .Type }} diff --git a/gno.land/cmd/gnoweb/views/realm_render.html b/gno.land/cmd/gnoweb/views/realm_render.html index 8a2c35adeca..6337d77aafa 100644 --- a/gno.land/cmd/gnoweb/views/realm_render.html +++ b/gno.land/cmd/gnoweb/views/realm_render.html @@ -2,8 +2,8 @@ - Gno.land - {{ template "html_head" }} + {{ template "html_head" . }} + Gno.land - {{.Data.RealmName}}
@@ -26,10 +26,7 @@
{{ template "footer" }} - {{ template "js" }} - - - + {{ template "js" .}} {{- end -}} diff --git a/gno.land/cmd/gnoweb/views/redirect.html b/gno.land/cmd/gnoweb/views/redirect.html new file mode 100644 index 00000000000..6fe43a7138b --- /dev/null +++ b/gno.land/cmd/gnoweb/views/redirect.html @@ -0,0 +1,16 @@ +{{- define "app" -}} + + + + + + + + Redirecting to {{.Data.To}} + + + {{.Data.To}} + {{ template "analytics" .}} + + +{{- end -}} From 235aab6d57f5de8b388a3cfc314587b035236796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 15:47:43 +0200 Subject: [PATCH 30/93] chore(deps): Bump actions/checkout from 3 to 4 (#1264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.1.0

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/monthly-snapshots.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/monthly-snapshots.yml b/.github/workflows/monthly-snapshots.yml index 23eb4629545..85307074218 100644 --- a/.github/workflows/monthly-snapshots.yml +++ b/.github/workflows/monthly-snapshots.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Generate tag name id: tag_name run: echo "::set-output name=tag_name::v0.0.1-dev.$(date +'%Y.%m.%d')" From 902ccc60fbe6bb4d670bfdea4769b95ac5c84204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Fri, 20 Oct 2023 11:00:00 -0400 Subject: [PATCH 31/93] fix: add support for custom node config file (#1240) ## Description This PR adds support for specifying a custom node configuration file using the `--tm2-node-config` flag. Resolves #1234
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [x] 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).
--- gno.land/cmd/gnoland/start.go | 134 +++++++++++++++++++++------- gno.land/pkg/integration/gnoland.go | 2 - tm2/pkg/bft/config/config.go | 34 ++++--- tm2/pkg/bft/config/toml.go | 28 ++++-- tm2/pkg/bft/config/toml_test.go | 62 +++++++++++++ 5 files changed, 206 insertions(+), 54 deletions(-) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 3914cc7775c..a42e1df1bf0 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -1,10 +1,12 @@ package main import ( + "bufio" "context" "errors" "flag" "fmt" + "os" "path/filepath" "strings" "time" @@ -42,6 +44,7 @@ type startCfg struct { txEventStoreType string txEventStorePath string + nodeConfigPath string } func newStartCmd(io *commands.IO) *commands.Command { @@ -54,8 +57,8 @@ func newStartCmd(io *commands.IO) *commands.Command { ShortHelp: "Run the full node", }, cfg, - func(_ context.Context, args []string) error { - return execStart(cfg, args, io) + func(_ context.Context, _ []string) error { + return execStart(cfg, io) }, ) } @@ -121,7 +124,14 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { &c.config, "config", "", - "config file (optional)", + "the flag config file (optional)", + ) + + fs.StringVar( + &c.nodeConfigPath, + "tm2-node-config", + "", + "the node TOML config file path (optional)", ) fs.StringVar( @@ -148,14 +158,28 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execStart(c *startCfg, args []string, io *commands.IO) error { +func execStart(c *startCfg, io *commands.IO) error { logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) rootDir := c.rootDir - cfg := config.LoadOrMakeConfigWithOptions(rootDir, func(cfg *config.Config) { - cfg.Consensus.CreateEmptyBlocks = true - cfg.Consensus.CreateEmptyBlocksInterval = 0 * time.Second - }) + var ( + cfg *config.Config + loadCfgErr error + ) + + // Set the node configuration + if c.nodeConfigPath != "" { + // Load the node configuration + // from the specified path + cfg, loadCfgErr = config.LoadConfigFile(c.nodeConfigPath) + } else { + // Load the default node configuration + cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(rootDir, nil) + } + + if loadCfgErr != nil { + return fmt.Errorf("unable to load node configuration, %w", loadCfgErr) + } // create priv validator first. // need it to generate genesis.json @@ -165,13 +189,23 @@ func execStart(c *startCfg, args []string, io *commands.IO) error { // write genesis file if missing. genesisFilePath := filepath.Join(rootDir, cfg.Genesis) + + genesisTxs, genesisTxsErr := loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote) + if genesisTxsErr != nil { + return fmt.Errorf("unable to load genesis txs, %w", genesisTxsErr) + } + if !osm.FileExists(genesisFilePath) { - genDoc := makeGenesisDoc( + genDoc, err := makeGenesisDoc( priv.GetPubKey(), c.chainID, c.genesisBalancesFile, - loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote), + genesisTxs, ) + if err != nil { + return fmt.Errorf("unable to generate genesis.json, %w", err) + } + writeGenesisFile(genDoc, genesisFilePath) } @@ -248,7 +282,7 @@ func makeGenesisDoc( chainID string, genesisBalancesFile string, genesisTxs []std.Tx, -) *bft.GenesisDoc { +) (*bft.GenesisDoc, error) { gen := &bft.GenesisDoc{} gen.GenesisTime = time.Now() @@ -272,8 +306,10 @@ func makeGenesisDoc( } // Load distribution. - balances := loadGenesisBalances(genesisBalancesFile) - // debug: for _, balance := range balances { fmt.Println(balance) } + balances, err := loadGenesisBalances(genesisBalancesFile) + if err != nil { + return nil, fmt.Errorf("unable to load genesis balances, %w", err) + } // Load initial packages from examples. test1 := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") @@ -319,7 +355,7 @@ func makeGenesisDoc( Balances: balances, Txs: txs, } - return gen + return gen, nil } func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { @@ -333,11 +369,24 @@ func loadGenesisTxs( path string, chainID string, genesisRemote string, -) []std.Tx { - txs := []std.Tx{} - txsBz := osm.MustReadFile(path) - txsLines := strings.Split(string(txsBz), "\n") - for _, txLine := range txsLines { +) ([]std.Tx, error) { + txs := make([]std.Tx, 0) + + if !osm.FileExists(path) { + // No initial transactions + return txs, nil + } + + txsFile, openErr := os.Open(path) + if openErr != nil { + return nil, fmt.Errorf("unable to open genesis txs file, %w", openErr) + } + + scanner := bufio.NewScanner(txsFile) + + for scanner.Scan() { + txLine := scanner.Text() + if txLine == "" { continue // skip empty line } @@ -347,19 +396,40 @@ func loadGenesisTxs( txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) var tx std.Tx - amino.MustUnmarshalJSON([]byte(txLine), &tx) + + if unmarshalErr := amino.UnmarshalJSON([]byte(txLine), &tx); unmarshalErr != nil { + return nil, fmt.Errorf("unable to amino unmarshal tx, %w", unmarshalErr) + } + txs = append(txs, tx) } - return txs + if scanErr := scanner.Err(); scanErr != nil { + return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + } + + return txs, nil } -func loadGenesisBalances(path string) []string { +func loadGenesisBalances(path string) ([]string, error) { // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - balances := []string{} - content := osm.MustReadFile(path) - lines := strings.Split(string(content), "\n") - for _, line := range lines { + balances := make([]string, 0) + + if !osm.FileExists(path) { + // No initial balances + return balances, nil + } + + balancesFile, openErr := os.Open(path) + if openErr != nil { + return nil, fmt.Errorf("unable to open genesis balances file, %w", openErr) + } + + scanner := bufio.NewScanner(balancesFile) + + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) // remove comments. @@ -371,12 +441,16 @@ func loadGenesisBalances(path string) []string { continue } - parts := strings.Split(line, "=") - if len(parts) != 2 { - panic("invalid genesis_balance line: " + line) + if len(strings.Split(line, "=")) != 2 { + return nil, fmt.Errorf("invalid genesis_balance line: %s", line) } balances = append(balances, line) } - return balances + + if scanErr := scanner.Err(); scanErr != nil { + return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + } + + return balances, nil } diff --git a/gno.land/pkg/integration/gnoland.go b/gno.land/pkg/integration/gnoland.go index c4fee341bfc..318d76eea86 100644 --- a/gno.land/pkg/integration/gnoland.go +++ b/gno.land/pkg/integration/gnoland.go @@ -133,8 +133,6 @@ func execTestingGnoland(t *testing.T, logger log.Logger, gnoDataDir, gnoRootDir cfg := config.TestConfig().SetRootDir(gnoDataDir) { cfg.EnsureDirs() - cfg.Consensus.CreateEmptyBlocks = true - cfg.Consensus.CreateEmptyBlocksInterval = time.Duration(0) cfg.RPC.ListenAddress = "tcp://127.0.0.1:0" cfg.P2P.ListenAddress = "tcp://127.0.0.1:0" } diff --git a/tm2/pkg/bft/config/config.go b/tm2/pkg/bft/config/config.go index e05f514a284..3785759c960 100644 --- a/tm2/pkg/bft/config/config.go +++ b/tm2/pkg/bft/config/config.go @@ -40,32 +40,40 @@ func DefaultConfig() *Config { } } -// Like LoadOrMakeConfigWithOptions() but without overriding any defaults. -func LoadOrMakeDefaultConfig(root string) (cfg *Config) { - return LoadOrMakeConfigWithOptions(root, nil) -} - type ConfigOptions func(cfg *Config) -// LoadOrMakeConfigWithOptions() loads configuration or saves one +// LoadOrMakeConfigWithOptions loads configuration or saves one // made by modifying the default config with override options -func LoadOrMakeConfigWithOptions(root string, options ConfigOptions) (cfg *Config) { +func LoadOrMakeConfigWithOptions(root string, options ConfigOptions) (*Config, error) { + var cfg *Config + configPath := join(root, defaultConfigFilePath) if osm.FileExists(configPath) { - cfg = LoadConfigFile(configPath) + var loadErr error + + // Load the configuration + if cfg, loadErr = LoadConfigFile(configPath); loadErr != nil { + return nil, loadErr + } + cfg.SetRootDir(root) cfg.EnsureDirs() } else { cfg = DefaultConfig() - options(cfg) + if options != nil { + options(cfg) + } cfg.SetRootDir(root) cfg.EnsureDirs() WriteConfigFile(configPath, cfg) + + // Validate the configuration + if validateErr := cfg.ValidateBasic(); validateErr != nil { + return nil, fmt.Errorf("unable to validate config, %w", validateErr) + } } - if err := cfg.ValidateBasic(); err != nil { - panic(err) - } - return cfg + + return cfg, nil } // TestConfig returns a configuration that can be used for testing diff --git a/tm2/pkg/bft/config/toml.go b/tm2/pkg/bft/config/toml.go index a35e5674631..fdaa1295342 100644 --- a/tm2/pkg/bft/config/toml.go +++ b/tm2/pkg/bft/config/toml.go @@ -24,17 +24,27 @@ func init() { } } -func LoadConfigFile(configFilePath string) *Config { - bz, err := os.ReadFile(configFilePath) - if err != nil { - panic(err) +// LoadConfigFile loads the TOML node configuration from the specified path +func LoadConfigFile(path string) (*Config, error) { + // Read the config file + content, readErr := os.ReadFile(path) + if readErr != nil { + return nil, readErr } - var config Config - err = toml.Unmarshal(bz, &config) - if err != nil { - panic(err) + + // Parse the node config + var nodeConfig Config + + if unmarshalErr := toml.Unmarshal(content, &nodeConfig); unmarshalErr != nil { + return nil, unmarshalErr + } + + // Validate the config + if validateErr := nodeConfig.ValidateBasic(); validateErr != nil { + return nil, fmt.Errorf("unable to validate config, %w", validateErr) } - return &config + + return &nodeConfig, nil } /****** these are for production settings ***********/ diff --git a/tm2/pkg/bft/config/toml_test.go b/tm2/pkg/bft/config/toml_test.go index 0fe78285997..da6a720ecd4 100644 --- a/tm2/pkg/bft/config/toml_test.go +++ b/tm2/pkg/bft/config/toml_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/pelletier/go-toml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -95,3 +97,63 @@ func checkConfig(configFile string) bool { } return valid } + +func TestTOML_LoadConfig(t *testing.T) { + t.Parallel() + + t.Run("config does not exist", func(t *testing.T) { + t.Parallel() + + cfg, loadErr := LoadConfigFile("dummy-path") + + assert.Error(t, loadErr) + assert.Nil(t, cfg) + }) + + t.Run("config is not valid toml", func(t *testing.T) { + t.Parallel() + + // Create config file + configFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Write invalid TOML + _, writeErr := configFile.WriteString("invalid TOML") + require.NoError(t, writeErr) + + cfg, loadErr := LoadConfigFile(configFile.Name()) + + assert.Error(t, loadErr) + assert.Nil(t, cfg) + }) + + t.Run("valid config", func(t *testing.T) { + t.Parallel() + + // Create config file + configFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Create the default config + defaultConfig := DefaultConfig() + + // Marshal the default config + defaultConfigRaw, marshalErr := toml.Marshal(defaultConfig) + require.NoError(t, marshalErr) + + // Write valid TOML + _, writeErr := configFile.Write(defaultConfigRaw) + require.NoError(t, writeErr) + + cfg, loadErr := LoadConfigFile(configFile.Name()) + require.NoError(t, loadErr) + + assert.EqualValues(t, defaultConfig.BaseConfig, cfg.BaseConfig) + assert.EqualValues(t, defaultConfig.RPC, cfg.RPC) + assert.EqualValues(t, defaultConfig.P2P, cfg.P2P) + assert.EqualValues(t, defaultConfig.Mempool, cfg.Mempool) + assert.EqualValues(t, defaultConfig.Consensus, cfg.Consensus) + assert.Equal(t, defaultConfig.TxEventStore.EventStoreType, cfg.TxEventStore.EventStoreType) + assert.Empty(t, defaultConfig.TxEventStore.Params, cfg.TxEventStore.Params) + }) +} From 2466911a3531f9a9889f59420dfa1d081d011da2 Mon Sep 17 00:00:00 2001 From: Morgan Date: Fri, 20 Oct 2023 12:32:31 -0400 Subject: [PATCH 32/93] docs: publish static pkg-site generated docs for this repo on GH pages (#1251) Preview: https://thehowl.github.io/gno/github.com/gnolang/gno.html --- .github/workflows/gh-pages.yml | 7 +-- CONTRIBUTING.md | 5 +++ README.md | 4 +- misc/devdeps/Makefile | 1 - misc/gendocs/Makefile | 5 ++- misc/gendocs/gendocs.sh | 78 ++++++++++++++++++++-------------- 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 8f57bec80a1..4e812eed5b7 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -21,9 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: "cd misc/devdeps && make install" - - run: "cd misc/gendocs && make gen" - - run: "find docs/ -type f -ls" + - uses: actions/setup-go@v4 + with: + go-version: "1.21.x" + - run: "cd misc/gendocs && make install gen" - uses: actions/configure-pages@v3 id: pages - uses: actions/upload-pages-artifact@v2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a77557e0a36..279b7869152 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,6 +66,11 @@ For Gno, there is no specific tooling that needs to be installed, that’s not a You can utilize the `gno` command to facilitate Gnolang support when writing Smart Contracts in Gno, by installing it with `make install_gno`. +If you are working on Go source code on this repository, `pkg.go.dev` will not +render our documentation as it has a license it does not recognise. Instead, use +the `go doc` command, or use our statically-generated documentation: +https://gnolang.github.io/gno/github.com/gnolang/gno.html + Additionally, you can also configure your editor to recognize `.gno` files as `.go` files, to get the benefit of syntax highlighting. diff --git a/README.md b/README.md index 618c3c3f01d..76f17fc4ae3 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,6 @@ We look forward to seeing your first PR! Pkg.go.dev - * [![Go Reference](https://pkg.go.dev/badge/github.com/gnolang/gno.svg)](https://pkg.go.dev/github.com/gnolang/gno) - * TODO: host custom docs on gh-pages, to bypass license limitation + * [![Go Reference](https://pkg.go.dev/badge/hey/google)](https://gnolang.github.io/gno/github.com/gnolang/gno.html) \ + (pkg.go.dev will not show our repository as it has a license it doesn't recognise)
diff --git a/misc/devdeps/Makefile b/misc/devdeps/Makefile index 99aea512ed0..54df62cc031 100644 --- a/misc/devdeps/Makefile +++ b/misc/devdeps/Makefile @@ -1,4 +1,3 @@ install: go install mvdan.cc/gofumpt go install google.golang.org/protobuf/cmd/protoc-gen-go - go install golang.org/x/tools/cmd/godoc diff --git a/misc/gendocs/Makefile b/misc/gendocs/Makefile index 8a4f3ba2de5..0f56d83693f 100644 --- a/misc/gendocs/Makefile +++ b/misc/gendocs/Makefile @@ -1,5 +1,8 @@ all: clean gen +install: + go install golang.org/x/pkgsite/cmd/pkgsite@latest + gen: ./gendocs.sh @@ -7,4 +10,4 @@ clean: rm -rf godoc kill_zombies: - kill -9 `lsof -t -i tcp:6060 -s TCP:LISTEN` || true + kill -9 `lsof -t -i tcp:8080 -s TCP:LISTEN` || true diff --git a/misc/gendocs/gendocs.sh b/misc/gendocs/gendocs.sh index d7621acd5a0..4336516aba1 100755 --- a/misc/gendocs/gendocs.sh +++ b/misc/gendocs/gendocs.sh @@ -1,35 +1,51 @@ -#!/bin/sh - -GODOC_PORT=${GODOC_PORT:-6060} -GO_MODULE=${GO_MODULE:-github.com/gnolang/gno} -GODOC_OUT=${GODOC_OUT:-godoc} -URL=http://localhost:${GODOC_PORT}/pkg/github.com/gnolang/gno/ - -echo "[+] Starting godoc server..." -go run \ - -modfile ../devdeps/go.mod \ - golang.org/x/tools/cmd/godoc \ - -http="localhost:${GODOC_PORT}" & -PID=$! -# Waiting for godoc server -while ! curl --fail --silent "$URL" > /dev/null 2>&1; do - sleep 0.1 +#!/bin/bash +# Heavily modified version of the following script: +# https://gist.github.com/Kegsay/84ce060f237cb9ab4e0d2d321a91d920 +set -u + +DOC_DIR=godoc +PKG=github.com/gnolang/gno + +# Run a pkgsite server which we will scrape. Use env to run it from our repo's root directory. +env -C ../.. pkgsite & +DOC_PID=$! + +# Wait for the server to init +while : +do + curl -s "http://localhost:8080" > /dev/null + if [ $? -eq 0 ] # exit code is 0 if we connected + then + break + fi done -echo "[+] Downloading godoc pages..." +# Scrape the pkg directory for the API docs. Scrap lib for the CSS/JS. Ignore everything else. wget \ - --recursive \ - --no-verbose \ - --convert-links \ - --page-requisites \ - --adjust-extension \ - --execute=robots=off \ - --include-directories="/lib,/pkg/$GO_MODULE,/src/$GO_MODULE" \ - --exclude-directories="*" \ - --directory-prefix="${GODOC_OUT}" \ - --no-host-directories \ - "$URL?m=all" - -echo "[+] Killing godoc server..." -kill -9 "$PID" + --verbose \ + --recursive \ + --mirror \ + --convert-links \ + --adjust-extension \ + --page-requisites \ + -erobots=off \ + --accept-regex='8080/((search|license-policy|about|)$|(static|images)/|github.com/gnolang/)' \ + http://localhost:8080/ + +# Stop the pkgsite server +kill -9 $DOC_PID + +# Delete the old directory or else mv will put the localhost dir into +# the DOC_DIR if it already exists. +rm -rf $DOC_DIR +mv localhost\:8080 $DOC_DIR + +# Perform various replacements to fix broken links/UI. +# /files/ will point to their github counterparts; we make links to importedby/version go nowhere; +# any other link will point to pkg.go.dev, and fix the /files/... text when viewing a pkg. +find godoc -type f -exec sed -ri 's#http://localhost:8080/files/[^"]*/github.com/gnolang/([^/"]+)/([^"]*)#https://github.com/gnolang/\1/blob/master/\2#g +s#http://localhost:8080/[^"?]*\?tab=(importedby|versions)#\##g +s#http://localhost:8080([^")]*)#https://pkg.go.dev\1#g +s#/files/[^" ]*/(github.com/[^" ]*)/#\1#g' {} + +echo "Docs can be found in $DOC_DIR" From f6d500a79c97caebf53409ffc40ea5c20dbd21ae Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:56:43 -0400 Subject: [PATCH 33/93] chore: repair-staging (#1268) Due to #1176 (removing `-pages-dir` flag from `gnoweb`) Blocks #1108 Signed-off-by: moul <94029+moul@users.noreply.github.com> --- misc/deployments/staging.gno.land/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/misc/deployments/staging.gno.land/docker-compose.yml b/misc/deployments/staging.gno.land/docker-compose.yml index 268002902ab..76d67fdb4e2 100644 --- a/misc/deployments/staging.gno.land/docker-compose.yml +++ b/misc/deployments/staging.gno.land/docker-compose.yml @@ -36,7 +36,6 @@ services: - --faucet-url=https://faucet-staging.gno.land/ - --help-chainid=staging - --help-remote=staging.gno.land:36657 - - --pages-dir=/overlay/pages - --views-dir=./gno.land/cmd/gnoweb/views volumes: - "./overlay:/overlay:ro" From 5cf3c71186dd2ef61387a1685ef8515c96ac4262 Mon Sep 17 00:00:00 2001 From: Antonio Navarro Perez Date: Sat, 21 Oct 2023 12:31:15 -0400 Subject: [PATCH 34/93] fix: allow constant values of infininitesimal non-zero floating points (#1185) Added a unit test and an integration test trying to reproduce #1150 Avoid unhandled errors. - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [x] 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). --------- Signed-off-by: Antonio Navarro Perez Co-authored-by: Morgan Bazalgette --- gnovm/pkg/gnolang/values_conversions.go | 21 +++++++++------- gnovm/pkg/gnolang/values_conversions_test.go | 25 ++++++++++++++++++++ gnovm/tests/files/float6.gno | 13 ++++++++++ gnovm/tests/files/float7.gno | 15 ++++++++++++ 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 gnovm/pkg/gnolang/values_conversions_test.go create mode 100644 gnovm/tests/files/float6.gno create mode 100644 gnovm/tests/files/float7.gno diff --git a/gnovm/pkg/gnolang/values_conversions.go b/gnovm/pkg/gnolang/values_conversions.go index 9fc2ce4a567..cc7c0de9f09 100644 --- a/gnovm/pkg/gnolang/values_conversions.go +++ b/gnovm/pkg/gnolang/values_conversions.go @@ -1240,12 +1240,14 @@ func ConvertUntypedBigdecTo(dst *TypedValue, bv BigdecValue, t Type) { case Float32Kind: dst.T = t dst.V = nil - f64, _ := bd.Float64() + f64, err := bd.Float64() + if err != nil { + panic(fmt.Errorf("cannot convert untyped bigdec to float64: %w", err)) + } + bf := big.NewFloat(f64) - f32, acc := bf.Float32() - if f32 == 0 && (acc == big.Below || acc == big.Above) { - panic("cannot convert untyped bigdec to float32 -- too close to zero") - } else if math.IsInf(float64(f32), 0) { + f32, _ := bf.Float32() + if math.IsInf(float64(f32), 0) { panic("cannot convert untyped bigdec to float32 -- too close to +-Inf") } dst.SetFloat32(f32) @@ -1253,10 +1255,11 @@ func ConvertUntypedBigdecTo(dst *TypedValue, bv BigdecValue, t Type) { case Float64Kind: dst.T = t dst.V = nil - f64, _ := bd.Float64() - if f64 == 0 && !bd.IsZero() { - panic("cannot convert untyped bigdec to float64 -- too close to zero") - } else if math.IsInf(f64, 0) { + f64, err := bd.Float64() + if err != nil { + panic(fmt.Errorf("cannot convert untyped bigdec to float64: %w", err)) + } + if math.IsInf(f64, 0) { panic("cannot convert untyped bigdec to float64 -- too close to +-Inf") } dst.SetFloat64(f64) diff --git a/gnovm/pkg/gnolang/values_conversions_test.go b/gnovm/pkg/gnolang/values_conversions_test.go new file mode 100644 index 00000000000..b9ac9e68d68 --- /dev/null +++ b/gnovm/pkg/gnolang/values_conversions_test.go @@ -0,0 +1,25 @@ +package gnolang + +import ( + "math" + "testing" + + "github.com/cockroachdb/apd" + "github.com/stretchr/testify/require" +) + +func TestConvertUntypedBigdecToFloat(t *testing.T) { + dst := &TypedValue{} + + dec, err := apd.New(-math.MaxInt64, -4).SetFloat64(math.SmallestNonzeroFloat64 / 2) + require.NoError(t, err) + bd := BigdecValue{ + V: dec, + } + + typ := Float64Type + + ConvertUntypedBigdecTo(dst, bd, typ) + + require.Equal(t, float64(0), dst.GetFloat64()) +} diff --git a/gnovm/tests/files/float6.gno b/gnovm/tests/files/float6.gno new file mode 100644 index 00000000000..680b9461975 --- /dev/null +++ b/gnovm/tests/files/float6.gno @@ -0,0 +1,13 @@ +package main + +const ( + SmallestNonzeroFloat64 = 0x1p-1022 * 0x1p-52 // 4.9406564584124654417656879286822137236505980e-324 + DividedByTwo = SmallestNonzeroFloat64 / 2 +) + +func main() { + println(DividedByTwo) +} + +// Output: +// 0 diff --git a/gnovm/tests/files/float7.gno b/gnovm/tests/files/float7.gno new file mode 100644 index 00000000000..f519a963523 --- /dev/null +++ b/gnovm/tests/files/float7.gno @@ -0,0 +1,15 @@ +package main + +const ( + SmallestNonzeroFloat32 = 0x1p-126 * 0x1p-23 // 1.401298464324817070923729583289916131280e-45 + DividedByTwo = SmallestNonzeroFloat32 / 2 +) + +func main() { + var i float32 + i = DividedByTwo + println(i) +} + +// Output: +// 0 From a3bdd2bb25b76b17176c3c59a1ce2522f8a75e53 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 22 Oct 2023 08:18:26 -0400 Subject: [PATCH 35/93] chore: enable analytics on staging (#1279) - enable analytics on staging - switch to custom domain --- gno.land/cmd/gnoweb/views/funcs.html | 4 ++-- misc/deployments/staging.gno.land/docker-compose.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gno.land/cmd/gnoweb/views/funcs.html b/gno.land/cmd/gnoweb/views/funcs.html index 1f8b7a265dd..c8d643ef655 100644 --- a/gno.land/cmd/gnoweb/views/funcs.html +++ b/gno.land/cmd/gnoweb/views/funcs.html @@ -157,8 +157,8 @@ {{- define "analytics" -}} {{- if .Data.Flags.WithAnalytics -}} - - + + {{- end -}} {{- end -}} diff --git a/misc/deployments/staging.gno.land/docker-compose.yml b/misc/deployments/staging.gno.land/docker-compose.yml index 76d67fdb4e2..af5e747e653 100644 --- a/misc/deployments/staging.gno.land/docker-compose.yml +++ b/misc/deployments/staging.gno.land/docker-compose.yml @@ -37,6 +37,7 @@ services: - --help-chainid=staging - --help-remote=staging.gno.land:36657 - --views-dir=./gno.land/cmd/gnoweb/views + - --with-analytics volumes: - "./overlay:/overlay:ro" links: From 4fff7a7b22d8bb7e45223a23597ef3543c829633 Mon Sep 17 00:00:00 2001 From: Peter Lai Date: Tue, 24 Oct 2023 23:23:16 +0800 Subject: [PATCH 36/93] fix: -broadcast true in doc (#1288)
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).
--- examples/gno.land/r/demo/groups/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/groups/README.md b/examples/gno.land/r/demo/groups/README.md index 1db5ae56b51..ecdd5065903 100644 --- a/examples/gno.land/r/demo/groups/README.md +++ b/examples/gno.land/r/demo/groups/README.md @@ -8,7 +8,7 @@ ### - create group - ./build/gnokey maketx call -func "CreateGroup" -args "dao_trinity_ngo" -gas-fee "1000000ugnot" -gas-wanted 4000000 -broadcast true -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 + ./build/gnokey maketx call -func "CreateGroup" -args "dao_trinity_ngo" -gas-fee "1000000ugnot" -gas-wanted 4000000 -broadcast -chainid dev -remote 0.0.0.0:26657 -pkgpath "gno.land/r/demo/groups" test1 ### - add member From 338be197a7c4d8c19899d33c591654dfde3e7dd5 Mon Sep 17 00:00:00 2001 From: xiaolou86 <20718693+xiaolou86@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:24:03 +0800 Subject: [PATCH 37/93] docs: comment typos (#1287) fix some typos in comments. --- examples/gno.land/r/demo/tests/tests.gno | 2 +- .../gno.land/r/x/nir1218_evaluation_proposal/EVALUATION.md | 2 +- .../gno.land/r/x/nir1218_evaluation_proposal/committee.gno | 4 ++-- tm2/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index fb49b2273ae..0094ad2ae35 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -44,7 +44,7 @@ func (t *TestRealmObject) Modify() { } //---------------------------------------- -// Test helpers to test a particualr realm bug. +// Test helpers to test a particular realm bug. type TestNode struct { Name string diff --git a/examples/gno.land/r/x/nir1218_evaluation_proposal/EVALUATION.md b/examples/gno.land/r/x/nir1218_evaluation_proposal/EVALUATION.md index b1dc2de58df..4926713e8fa 100644 --- a/examples/gno.land/r/x/nir1218_evaluation_proposal/EVALUATION.md +++ b/examples/gno.land/r/x/nir1218_evaluation_proposal/EVALUATION.md @@ -51,7 +51,7 @@ An example of a category is a bounty, a chore, a defect, or a document. A contribution is associated with a pull request. A contribution has an evaluation life cycle. A submission time is set when a contribution is added. -A last evaluation time is set when a contribution is evaluated and approved by a memeber. +A last evaluation time is set when a contribution is evaluated and approved by a member. An approval time is set when a contribution is approved by all members (or when a future threshold is reached) #### Submission diff --git a/examples/gno.land/r/x/nir1218_evaluation_proposal/committee.gno b/examples/gno.land/r/x/nir1218_evaluation_proposal/committee.gno index ed939b56fdb..1ec801bb971 100644 --- a/examples/gno.land/r/x/nir1218_evaluation_proposal/committee.gno +++ b/examples/gno.land/r/x/nir1218_evaluation_proposal/committee.gno @@ -9,7 +9,7 @@ import ( type Committee struct { members []std.Address // TODO - use avl tree or address set? - categories avl.Tree // A catagory is mapped to a list of evaluation criteria + categories avl.Tree // A category is mapped to a list of evaluation criteria evaluation *Evaluation } @@ -84,7 +84,7 @@ func (c *Committee) AddContribution(pr *PullRequest, contributor std.Address) (c if !c.isMember(std.GetOrigCaller()) { return -1, false } - // Check the category of the PR matches a catagory this committee evaluates + // Check the category of the PR matches a category this committee evaluates // TODO check the category is an approved category if c.categories.Has(pr.category) { return c.evaluation.AddContribution(pr, contributor) diff --git a/tm2/README.md b/tm2/README.md index 101fa793e82..c4d6aa8d287 100644 --- a/tm2/README.md +++ b/tm2/README.md @@ -22,7 +22,7 @@ * Minimal code - keep total footprint small. * Minimal dependencies - all dependencies must get audited, and become part of the repo. -* Modular dependencies - whereever reasonable, make components modular. +* Modular dependencies - wherever reasonable, make components modular. * Completeness - software projects that don't become finished are projects that are forever vulnerable. One of the primary goals of the Gno language and related works is to become finished within a reasonable timeframe. From f872ca7a37088f18c1be81658ca3614a2e063248 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Thu, 26 Oct 2023 00:29:06 +0900 Subject: [PATCH 38/93] feat: add hash (#1273) Add hash stdlib (especially adler32) and add encoding for dependency ## relate issue #1267 --- gnovm/docs/go-gno-compatibility.md | 4 +- gnovm/stdlibs/encoding/encoding.gno | 54 ++++++++++ gnovm/stdlibs/hash/adler32/adler32.gno | 135 +++++++++++++++++++++++++ gnovm/stdlibs/hash/hash.gno | 58 +++++++++++ gnovm/stdlibs/hash/marshal_test.gno | 87 ++++++++++++++++ 5 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 gnovm/stdlibs/encoding/encoding.gno create mode 100644 gnovm/stdlibs/hash/adler32/adler32.gno create mode 100644 gnovm/stdlibs/hash/hash.gno create mode 100644 gnovm/stdlibs/hash/marshal_test.gno diff --git a/gnovm/docs/go-gno-compatibility.md b/gnovm/docs/go-gno-compatibility.md index 98f42aa9f29..a39cec533f4 100644 --- a/gnovm/docs/go-gno-compatibility.md +++ b/gnovm/docs/go-gno-compatibility.md @@ -234,8 +234,8 @@ Additional native types: | go/types | TBD | | go/types/testdata | TBD | | go/types/testdata/local | TBD | -| hash | TBD | -| hash/adler32 | TBD | +| hash | partial | +| hash/adler32 | full | | hash/crc32 | TBD | | hash/crc64 | TBD | | hash/fnv | TBD | diff --git a/gnovm/stdlibs/encoding/encoding.gno b/gnovm/stdlibs/encoding/encoding.gno new file mode 100644 index 00000000000..50acf3c23a1 --- /dev/null +++ b/gnovm/stdlibs/encoding/encoding.gno @@ -0,0 +1,54 @@ +// Copyright 2013 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 encoding defines interfaces shared by other packages that +// convert data to and from byte-level and textual representations. +// Packages that check for these interfaces include encoding/gob, +// encoding/json, and encoding/xml. As a result, implementing an +// interface once can make a type useful in multiple encodings. +// Standard types that implement these interfaces include time.Time and net.IP. +// The interfaces come in pairs that produce and consume encoded data. +// +// Adding encoding/decoding methods to existing types may constitute a breaking change, +// as they can be used for serialization in communicating with programs +// written with different library versions. +// The policy for packages maintained by the Go project is to only allow +// the addition of marshaling functions if no existing, reasonable marshaling exists. +package encoding + +// BinaryMarshaler is the interface implemented by an object that can +// marshal itself into a binary form. +// +// MarshalBinary encodes the receiver into a binary form and returns the result. +type BinaryMarshaler interface { + MarshalBinary() (data []byte, err error) +} + +// BinaryUnmarshaler is the interface implemented by an object that can +// unmarshal a binary representation of itself. +// +// UnmarshalBinary must be able to decode the form generated by MarshalBinary. +// UnmarshalBinary must copy the data if it wishes to retain the data +// after returning. +type BinaryUnmarshaler interface { + UnmarshalBinary(data []byte) error +} + +// TextMarshaler is the interface implemented by an object that can +// marshal itself into a textual form. +// +// MarshalText encodes the receiver into UTF-8-encoded text and returns the result. +type TextMarshaler interface { + MarshalText() (text []byte, err error) +} + +// TextUnmarshaler is the interface implemented by an object that can +// unmarshal a textual representation of itself. +// +// UnmarshalText must be able to decode the form generated by MarshalText. +// UnmarshalText must copy the text if it wishes to retain the text +// after returning. +type TextUnmarshaler interface { + UnmarshalText(text []byte) error +} diff --git a/gnovm/stdlibs/hash/adler32/adler32.gno b/gnovm/stdlibs/hash/adler32/adler32.gno new file mode 100644 index 00000000000..38d644d1ee5 --- /dev/null +++ b/gnovm/stdlibs/hash/adler32/adler32.gno @@ -0,0 +1,135 @@ +// 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 adler32 implements the Adler-32 checksum. +// +// It is defined in RFC 1950: +// +// Adler-32 is composed of two sums accumulated per byte: s1 is +// the sum of all bytes, s2 is the sum of all s1 values. Both sums +// are done modulo 65521. s1 is initialized to 1, s2 to zero. The +// Adler-32 checksum is stored as s2*65536 + s1 in most- +// significant-byte first (network) order. +package adler32 + +import ( + "errors" + "hash" +) + +const ( + // mod is the largest prime that is less than 65536. + mod = 65521 + // nmax is the largest n such that + // 255 * n * (n+1) / 2 + (n+1) * (mod-1) <= 2^32-1. + // It is mentioned in RFC 1950 (search for "5552"). + nmax = 5552 +) + +// The size of an Adler-32 checksum in bytes. +const Size = 4 + +// digest represents the partial evaluation of a checksum. +// The low 16 bits are s1, the high 16 bits are s2. +type digest uint32 + +func (d *digest) Reset() { *d = 1 } + +// New returns a new hash.Hash32 computing the Adler-32 checksum. Its +// Sum method will lay the value out in big-endian byte order. The +// returned Hash32 also implements encoding.BinaryMarshaler and +// encoding.BinaryUnmarshaler to marshal and unmarshal the internal +// state of the hash. +func New() hash.Hash32 { + d := new(digest) + d.Reset() + return d +} + +func (d *digest) Size() int { return Size } + +func (d *digest) BlockSize() int { return 4 } + +const ( + magic = "adl\x01" + marshaledSize = len(magic) + 4 +) + +func (d *digest) MarshalBinary() ([]byte, error) { + b := make([]byte, 0, marshaledSize) + b = append(b, magic...) + b = appendUint32(b, uint32(*d)) + return b, nil +} + +func (d *digest) UnmarshalBinary(b []byte) error { + if len(b) < len(magic) || string(b[:len(magic)]) != magic { + return errors.New("hash/adler32: invalid hash state identifier") + } + if len(b) != marshaledSize { + return errors.New("hash/adler32: invalid hash state size") + } + *d = digest(readUint32(b[len(magic):])) + return nil +} + +func appendUint32(b []byte, x uint32) []byte { + a := [4]byte{ + byte(x >> 24), + byte(x >> 16), + byte(x >> 8), + byte(x), + } + return append(b, a[:]...) +} + +func readUint32(b []byte) uint32 { + _ = b[3] + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +// Add p to the running checksum d. +func update(d digest, p []byte) digest { + s1, s2 := uint32(d&0xffff), uint32(d>>16) + for len(p) > 0 { + var q []byte + if len(p) > nmax { + p, q = p[:nmax], p[nmax:] + } + for len(p) >= 4 { + s1 += uint32(p[0]) + s2 += s1 + s1 += uint32(p[1]) + s2 += s1 + s1 += uint32(p[2]) + s2 += s1 + s1 += uint32(p[3]) + s2 += s1 + p = p[4:] + } + for _, x := range p { + s1 += uint32(x) + s2 += s1 + } + s1 %= mod + s2 %= mod + p = q + } + return digest(s2<<16 | s1) +} + +func (d *digest) Write(p []byte) (nn int, err error) { + *d = update(*d, p) + return len(p), nil +} + +func (d *digest) Sum32() uint32 { return uint32(*d) } + +func (d *digest) Sum(in []byte) []byte { + s := uint32(*d) + return append(in, byte(s>>24), byte(s>>16), byte(s>>8), byte(s)) +} + +// Checksum returns the Adler-32 checksum of data. +func Checksum(data []byte) uint32 { return uint32(update(1, data)) } diff --git a/gnovm/stdlibs/hash/hash.gno b/gnovm/stdlibs/hash/hash.gno new file mode 100644 index 00000000000..62cf6a45184 --- /dev/null +++ b/gnovm/stdlibs/hash/hash.gno @@ -0,0 +1,58 @@ +// 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 hash provides interfaces for hash functions. +package hash + +import "io" + +// Hash is the common interface implemented by all hash functions. +// +// Hash implementations in the standard library (e.g. hash/crc32 and +// crypto/sha256) implement the encoding.BinaryMarshaler and +// encoding.BinaryUnmarshaler interfaces. Marshaling a hash implementation +// allows its internal state to be saved and used for additional processing +// later, without having to re-write the data previously written to the hash. +// The hash state may contain portions of the input in its original form, +// which users are expected to handle for any possible security implications. +// +// Compatibility: Any future changes to hash or crypto packages will endeavor +// to maintain compatibility with state encoded using previous versions. +// That is, any released versions of the packages should be able to +// decode data written with any previously released version, +// subject to issues such as security fixes. +// See the Go compatibility document for background: https://golang.org/doc/go1compat +type Hash interface { + // Write (via the embedded io.Writer interface) adds more data to the running hash. + // It never returns an error. + io.Writer + + // Sum appends the current hash to b and returns the resulting slice. + // It does not change the underlying hash state. + Sum(b []byte) []byte + + // Reset resets the Hash to its initial state. + Reset() + + // Size returns the number of bytes Sum will return. + Size() int + + // BlockSize returns the hash's underlying block size. + // The Write method must be able to accept any amount + // of data, but it may operate more efficiently if all writes + // are a multiple of the block size. + BlockSize() int +} + +// Hash32 is the common interface implemented by all 32-bit hash functions. +type Hash32 interface { + Hash + Sum32() uint32 +} + +// Hash64 is the common interface implemented by all 64-bit hash functions. +type Hash64 interface { + Hash + Sum64() uint64 +} diff --git a/gnovm/stdlibs/hash/marshal_test.gno b/gnovm/stdlibs/hash/marshal_test.gno new file mode 100644 index 00000000000..b31d35faa77 --- /dev/null +++ b/gnovm/stdlibs/hash/marshal_test.gno @@ -0,0 +1,87 @@ +// Copyright 2017 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. + +// Test that the hashes in the standard library implement +// BinaryMarshaler, BinaryUnmarshaler, +// and lock in the current representations. + +package hash + +import ( + "bytes" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "encoding" + "encoding/hex" + "hash" + "hash/adler32" + "testing" +) + +func fromHex(s string) []byte { + b, err := hex.DecodeString(s) + if err != nil { + panic(err) + } + return b +} + +var marshalTests = []struct { + name string + new func() hash.Hash + golden []byte +}{ + {"adler32", func() hash.Hash { return adler32.New() }, fromHex("61646c01460a789d")}, +} + +func TestMarshalHash(t *testing.T) { + for _, tt := range marshalTests { + t.Run(tt.name, func(t *testing.T) { + buf := make([]byte, 256) + for i := range buf { + buf[i] = byte(i) + } + + h := tt.new() + h.Write(buf[:256]) + sum := h.Sum(nil) + + h2 := tt.new() + h3 := tt.new() + const split = 249 + for i := 0; i < split; i++ { + h2.Write(buf[i : i+1]) + } + h2m, ok := h2.(encoding.BinaryMarshaler) + if !ok { + t.Fatalf("Hash does not implement MarshalBinary") + } + enc, err := h2m.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary: %v", err) + } + if !bytes.Equal(enc, tt.golden) { + t.Errorf("MarshalBinary = %x, want %x", enc, tt.golden) + } + h3u, ok := h3.(encoding.BinaryUnmarshaler) + if !ok { + t.Fatalf("Hash does not implement UnmarshalBinary") + } + if err := h3u.UnmarshalBinary(enc); err != nil { + t.Fatalf("UnmarshalBinary: %v", err) + } + h2.Write(buf[split:]) + h3.Write(buf[split:]) + sum2 := h2.Sum(nil) + sum3 := h3.Sum(nil) + if !bytes.Equal(sum2, sum) { + t.Fatalf("Sum after MarshalBinary = %x, want %x", sum2, sum) + } + if !bytes.Equal(sum3, sum) { + t.Fatalf("Sum after UnmarshalBinary = %x, want %x", sum3, sum) + } + }) + } +} From f39cc4622631276bb3a9a0cdb585950d81f497a5 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 26 Oct 2023 04:54:36 +0200 Subject: [PATCH 39/93] fix: Error string in decryptPrivKey. Use errors.As in IsErrWrongPassword. (#1289) This PR fixes bugs with error handling: * `decryptPrivKey` checks if `DecryptSymmetric` [returns the error "Ciphertext decryption failed"](https://github.com/gnolang/gno/blob/a3bdd2bb25b76b17176c3c59a1ce2522f8a75e53/tm2/pkg/crypto/keys/armor/armor.go#L131) and converts it to `ErrWrongPassword`. The problem is that `DecryptSymmetric` [returns "ciphertext decryption failed"](https://github.com/gnolang/gno/blob/a3bdd2bb25b76b17176c3c59a1ce2522f8a75e53/tm2/pkg/crypto/xsalsa20symmetric/symmetric.go#L53C27-L53C55) (spelled differently). This PR fixes the string in the error check. * `IsErrWrongPassword` checks if the [error type is `keybaseError`](https://github.com/gnolang/gno/blob/a3bdd2bb25b76b17176c3c59a1ce2522f8a75e53/tm2/pkg/crypto/keys/keyerror/errors.go#L75C24-L75C36) . But the error can be wrapped as it is [in `signAndBroadcastTxCommit`](https://github.com/gnolang/gno/blob/60e05e83f57558843c0808f78500b6a51b2a22c1/gno.land/pkg/gnoclient/client_txs.go#L104). Therefore, instead of a simple error type check, this PR updates `IsErrWrongPassword` (and `IsErrKeyNotFound`) to use `errors.As` to check the error type (which unwraps the error if needed). Signed-off-by: Jeff Thompson --- tm2/pkg/crypto/keys/armor/armor.go | 2 +- tm2/pkg/crypto/keys/keyerror/errors.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tm2/pkg/crypto/keys/armor/armor.go b/tm2/pkg/crypto/keys/armor/armor.go index 7233b27123a..22315f8c521 100644 --- a/tm2/pkg/crypto/keys/armor/armor.go +++ b/tm2/pkg/crypto/keys/armor/armor.go @@ -128,7 +128,7 @@ func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privK } key = crypto.Sha256(key) // Get 32 bytes privKeyBytes, err := xsalsa20symmetric.DecryptSymmetric(encBytes, key) - if err != nil && err.Error() == "Ciphertext decryption failed" { + if err != nil && err.Error() == "ciphertext decryption failed" { return privKey, keyerror.NewErrWrongPassword() } else if err != nil { return privKey, err diff --git a/tm2/pkg/crypto/keys/keyerror/errors.go b/tm2/pkg/crypto/keys/keyerror/errors.go index 93eb63d2bf3..f7dc97e972d 100644 --- a/tm2/pkg/crypto/keys/keyerror/errors.go +++ b/tm2/pkg/crypto/keys/keyerror/errors.go @@ -1,6 +1,7 @@ package keyerror import ( + "errors" "fmt" ) @@ -40,7 +41,8 @@ func IsErrKeyNotFound(err error) bool { if err == nil { return false } - if keyErr, ok := err.(keybaseError); ok { + var keyErr keybaseError + if errors.As(err, &keyErr) { if keyErr.Code() == codeKeyNotFound { return true } @@ -72,7 +74,8 @@ func IsErrWrongPassword(err error) bool { if err == nil { return false } - if keyErr, ok := err.(keybaseError); ok { + var keyErr keybaseError + if errors.As(err, &keyErr) { if keyErr.Code() == codeWrongPassword { return true } From eb421578ec72d70d2cbd930576856623376f2daa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 05:09:44 +0200 Subject: [PATCH 40/93] chore(deps): Bump toshimaru/auto-author-assign from 1.6.2 to 2.0.1 (#1175) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [toshimaru/auto-author-assign](https://github.com/toshimaru/auto-author-assign) from 1.6.2 to 2.0.1.
Release notes

Sourced from toshimaru/auto-author-assign's releases.

v2.0.1

What's Changed

Chores

New Contributors

Full Changelog: https://github.com/toshimaru/auto-author-assign/compare/v2.0.0...v2.0.1

v2.0.0

What's Changed

Bump node.js to v20

Dependencies

Full Changelog: https://github.com/toshimaru/auto-author-assign/compare/v1.6.2...v2.0.0

Changelog

Sourced from toshimaru/auto-author-assign's changelog.

2.0.1 (2023-09-26)

2.0.0 (2023-09-24)

Commits
  • c1ffd6f chore(release): 2.0.1
  • 0fc5d8c build: Build script with licenses.txt (#98)
  • 4faf79f Update README supported by ChatGPT (#97)
  • ad9853f Update action version in README.md
  • 293cfe9 chore(release): 2.0.0
  • 52c41c8 Update package.json
  • 4647a30 Bump node.js from v16 to v20
  • 2dc5f32 build(deps): bump @​actions/core from 1.10.0 to 1.10.1 (#94)
  • f2f78d6 build(deps-dev): bump @​vercel/ncc from 0.36.1 to 0.38.0 (#92)
  • e41b643 build(deps): bump actions/checkout from 3 to 4 (#93)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toshimaru/auto-author-assign&package-manager=github_actions&previous-version=1.6.2&new-version=2.0.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-author-assign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index 8902a128b5d..c7f209687c4 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -15,4 +15,4 @@ jobs: assign-author: runs-on: ubuntu-latest steps: - - uses: toshimaru/auto-author-assign@v1.6.2 + - uses: toshimaru/auto-author-assign@v2.0.1 From ccba538d50e1b143ff6045223b60b3c01880797f Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 26 Oct 2023 15:24:10 +0200 Subject: [PATCH 41/93] fix: in TestGnoDoc, fix expected output for `gno doc avl` (#1301) In the root folder, `make test` has one failure: `FAIL: TestGnoDoc/doc_avl (0.01s)` . This is a failure of [this test](https://github.com/gnolang/gno/blob/eb421578ec72d70d2cbd930576856623376f2daa/gnovm/cmd/gno/doc_test.go#L12-L13) which expects "func NewNode". However in folder gnovm/cmd/gno, `go run . doc avl` prints: ``` package avl // import "gno.land/p/demo/avl" type MutTree struct{ ... } func NewMutTree() *MutTree type Tree struct{ ... } func NewTree(key string, value interface{}) *Tree ``` Therefore, this PR changes the test to expect "func NewTree". With this fix, `make test` passes. (Of course, maybe `go run . doc avl` really should have "func NewNode". But I'm assuming that there was an update after the test was written.) Signed-off-by: Jeff Thompson --- gnovm/cmd/gno/doc_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gnovm/cmd/gno/doc_test.go b/gnovm/cmd/gno/doc_test.go index 3eb90e2a329..513862ad2dc 100644 --- a/gnovm/cmd/gno/doc_test.go +++ b/gnovm/cmd/gno/doc_test.go @@ -10,7 +10,7 @@ func TestGnoDoc(t *testing.T) { }, { args: []string{"doc", "avl"}, - stdoutShouldContain: "func NewNode", + stdoutShouldContain: "func NewTree", }, { args: []string{"doc", "-u", "avl.Node"}, From ca5ce0f435968ceca112bde4adb611732b99ec56 Mon Sep 17 00:00:00 2001 From: stalangermin <40028493+stanlagermin@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:38:08 +0700 Subject: [PATCH 42/93] chore: fix typos (#1300) --- README.md | 2 +- tm2/pkg/bft/consensus/ticker.go | 2 +- tm2/pkg/bft/rpc/client/mock/client.go | 2 +- tm2/pkg/bft/types/evidence.go | 2 +- tm2/pkg/db/prefix_db.go | 2 +- tm2/pkg/p2p/conn/secret_connection_test.go | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76f17fc4ae3..71c53b86f19 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ > simulated by the Gnomes of the Greater Resistance. Gno is an interpreted and fully-deterministic implementation of the Go -programming language, designed to build succint and composable smart contracts. +programming language, designed to build succinct and composable smart contracts. The first blockchain to use it is Gno.land, a [Proof of Contribution](./docs/proof-of-contribution.md)-based chain, backed by a variation of the [Tendermint](https://docs.tendermint.com/v0.34/introduction/what-is-tendermint.html) diff --git a/tm2/pkg/bft/consensus/ticker.go b/tm2/pkg/bft/consensus/ticker.go index c4f660aae16..f1a33dd9c38 100644 --- a/tm2/pkg/bft/consensus/ticker.go +++ b/tm2/pkg/bft/consensus/ticker.go @@ -86,7 +86,7 @@ func (t *timeoutTicker) stopTimer() { } // send on tickChan to start a new timer. -// timers are interupted and replaced by new ticks from later steps +// timers are interrupted and replaced by new ticks from later steps // timeouts of 0 on the tickChan will be immediately relayed to the tockChan func (t *timeoutTicker) timeoutRoutine() { t.Logger.Debug("Starting timeout routine") diff --git a/tm2/pkg/bft/rpc/client/mock/client.go b/tm2/pkg/bft/rpc/client/mock/client.go index f7a617da9fe..46db69debb3 100644 --- a/tm2/pkg/bft/rpc/client/mock/client.go +++ b/tm2/pkg/bft/rpc/client/mock/client.go @@ -50,7 +50,7 @@ type Call struct { Error error } -// GetResponse will generate the apporiate response for us, when +// GetResponse will generate the appropriate response for us, when // using the Call struct to configure a Mock handler. // // When configuring a response, if only one of Response or Error is diff --git a/tm2/pkg/bft/types/evidence.go b/tm2/pkg/bft/types/evidence.go index 26cf1aaa8ca..c11021e3976 100644 --- a/tm2/pkg/bft/types/evidence.go +++ b/tm2/pkg/bft/types/evidence.go @@ -65,7 +65,7 @@ const ( ) // MaxEvidencePerBlock returns the maximum number of evidences -// allowed in the block and their maximum total size (limitted to 1/10th +// allowed in the block and their maximum total size (limited to 1/10th // of the maximum block size). // TODO: change to a constant, or to a fraction of the validator set size. // See https://github.com/tendermint/classic/issues/2590 diff --git a/tm2/pkg/db/prefix_db.go b/tm2/pkg/db/prefix_db.go index ced82f922d1..29ed53639e8 100644 --- a/tm2/pkg/db/prefix_db.go +++ b/tm2/pkg/db/prefix_db.go @@ -329,7 +329,7 @@ func stripPrefix(key []byte, prefix []byte) (stripped []byte) { panic("should not happen") } if !bytes.Equal(key[:len(prefix)], prefix) { - panic("should not happne") + panic("should not happen") } return key[len(prefix):] } diff --git a/tm2/pkg/p2p/conn/secret_connection_test.go b/tm2/pkg/p2p/conn/secret_connection_test.go index 521f651e78b..a2560af34eb 100644 --- a/tm2/pkg/p2p/conn/secret_connection_test.go +++ b/tm2/pkg/p2p/conn/secret_connection_test.go @@ -234,7 +234,7 @@ func TestSecretConnectionReadWrite(t *testing.T) { // A helper that will run with (fooConn, fooWrites, fooReads) and vice versa genNodeRunner := func(id string, nodeConn kvstoreConn, nodeWrites []string, nodeReads *[]string) async.Task { return func(_ int) (interface{}, error, bool) { - // Initiate cryptographic private key and secret connection trhough nodeConn. + // Initiate cryptographic private key and secret connection through nodeConn. nodePrvKey := ed25519.GenPrivKey() nodeSecretConn, err := MakeSecretConnection(nodeConn, nodePrvKey) if err != nil { From 34d78b3f9d4afb51d3f53d2d7d73752bb59ff4ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:07:31 +0200 Subject: [PATCH 43/93] chore(deps): bump github.com/gdamore/tcell/v2 from 2.1.0 to 2.6.0 (#862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/gdamore/tcell/v2](https://github.com/gdamore/tcell) from 2.1.0 to 2.6.0.
Release notes

Sourced from github.com/gdamore/tcell/v2's releases.

Version 2.6.0 Feature Release

The main feature introduced in this release is support for web based applications. You can now create applications that run in a browser, and display a simulated terminal emulator in the browser. The initial implementation of this capability was supplied by @​Ahoys123 -- thank you! (We made some follow up bug fixes and improvements.)

More detail about this mode can be found in the README-wasm.md file.

Additionally we added support for alacritty-direct, which was contributed by @​moson-mo.

This version is only tested on go 1.18 and newer. Older versions of go may work, but might also fail, as our dependencies have started using newer compilation flags.

Version 2.5.4 Bug Fix Release

Version 2.5.4 fixed quite a few things in the 2.5 release chain. Arguably it could also have been a minor release due to some quasi-feature updates. It is anticipated that this will be the last release for 2.5.x.

The next minor release (2.6.0) will probably require updating to at least go 1.17 as we move towards updating imports and adopting additional language features.

Fixes:

  • On Windows (and some other platforms) custom TTYs did not work (#580)
  • Default to using narrow for ambiguous characters in East Asian locales (#578) This affected a lot of folks in East Asian locales, and now tcell applications should work by default for them. If overrides to the RUNEWIDTH_EASTASIAN environment are present they will still be honored.
  • Fix for intermittent screen flashes (#576)
  • Encoding sub package now registers all encodings when imported. (Explicit call to Register is no longer required)
  • Tutorial program improved to demonstrate panic handling (thanks to Eric S. Raymond)
  • Fix for mouse-wheel/click-drag conflation (#574)
  • Hyperlink ID support added (#568) (thanks to Tim Culverhouse)
  • Paste support added to views.Application (#552) (thanks to Chris Bradbury)
  • WidgetWatcher is concurrency-safe (thanks to Tim Culverhouse)
  • Fix for CellView.Size() (#553) (thanks to Chris Bradbury)
  • Fix for tput escape sequence errors (#546)
  • Horizontal, Vertical are now type Orientation (#543) (thanks to Zaim Bakar)

Version 2.5.3 Bug Fix Release

Version 2.5.3 only fixed some things related to the documentation.

Version 2.5.2 Bug Fix & Feature Release

(Technically this should probably have been a new minor as a new feature was introduced.)

  • Better handling of monochrome terminals
  • Console resizing support (#462) (this new feature allows applications to specify the window size they want.)
  • Minor mouse demo improvements
  • Added support for terminal hyperlinks (#300)
  • Optimize some output (#526)
  • Documentation fixes

Version 2.5.1 Bug Fix Release

This release fixes #523 - which addresses an unintended behavior when clearing the screen. The regression was introduced in v2.5.0.

Version 2.5.0 Feature Release

Version 2.5.0 is a rollup of a number of bug fixes but also includes some new features:

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gdamore/tcell/v2&package-manager=go_modules&previous-version=2.1.0&new-version=2.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) Dependabot will merge this PR once CI passes on it, as requested by @thehowl. [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 31 ++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 72c52102547..0dc4114f405 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/dgraph-io/badger/v3 v3.2103.4 github.com/fortytw2/leaktest v1.3.0 - github.com/gdamore/tcell/v2 v2.1.0 + github.com/gdamore/tcell/v2 v2.6.0 github.com/gnolang/goleveldb v0.0.9 github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/golang/protobuf v1.5.3 @@ -60,10 +60,10 @@ require ( github.com/klauspost/compress v1.12.3 // indirect github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.7 // indirect - github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.3 // indirect go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect golang.org/x/sys v0.13.0 // indirect diff --git a/go.sum b/go.sum index 67e2a190feb..2b3d717a7e5 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,8 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro= -github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= +github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= +github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE= github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= @@ -122,10 +122,10 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/linxGnu/grocksdb v1.8.4 h1:ZMsBpPpJNtRLHiKKp0mI7gW+NT4s7UgfD5xHxx1jVRo= github.com/linxGnu/grocksdb v1.8.4/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -146,8 +146,9 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= @@ -175,6 +176,7 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= @@ -189,6 +191,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -197,6 +200,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -208,6 +212,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -217,22 +223,32 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -242,6 +258,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 7dee385d5e291c17dbade6f87913d1ba5776cf74 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 26 Oct 2023 14:00:10 -0400 Subject: [PATCH 44/93] fix(cmd/gno): change set_exit_status flag to kebab-case (#1304) consistency fix --- gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/lint_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 158b9d8db5d..7acd9877770 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -38,7 +38,7 @@ func newLintCmd(io *commands.IO) *commands.Command { func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) { fs.BoolVar(&c.verbose, "verbose", false, "verbose output when lintning") fs.StringVar(&c.rootDir, "root-dir", "", "clone location of github.com/gnolang/gno (gno tries to guess it)") - fs.IntVar(&c.setExitStatus, "set_exit_status", 1, "set exit status to 1 if any issues are found") + fs.IntVar(&c.setExitStatus, "set-exit-status", 1, "set exit status to 1 if any issues are found") } func execLint(cfg *lintCfg, args []string, io *commands.IO) error { diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index ce200a1fedd..0a747a03778 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -8,16 +8,16 @@ func TestLintApp(t *testing.T) { args: []string{"lint"}, errShouldBe: "flag: help requested", }, { - args: []string{"lint", "--set_exit_status=0", "../../tests/integ/run-main/"}, + args: []string{"lint", "--set-exit-status=0", "../../tests/integ/run-main/"}, stderrShouldContain: "./../../tests/integ/run-main: missing 'gno.mod' file (code=1).", }, { - args: []string{"lint", "--set_exit_status=0", "../../tests/integ/run-main/"}, + args: []string{"lint", "--set-exit-status=0", "../../tests/integ/run-main/"}, stderrShouldContain: "./../../tests/integ/run-main: missing 'gno.mod' file (code=1).", }, { - args: []string{"lint", "--set_exit_status=0", "../../tests/integ/minimalist-gnomod/"}, + args: []string{"lint", "--set-exit-status=0", "../../tests/integ/minimalist-gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files }, { - args: []string{"lint", "--set_exit_status=0", "../../tests/integ/invalid-module-name/"}, + args: []string{"lint", "--set-exit-status=0", "../../tests/integ/invalid-module-name/"}, // TODO: raise an error because gno.mod is invalid }, // TODO: 'gno mod' is valid? From 7105d00e10209003ea41fbae7a4d7c463c4167c3 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 26 Oct 2023 17:16:17 -0400 Subject: [PATCH 45/93] chore: add misc/list-gnophers and .mailmap (#1265) ## Current Status ```console $ cd ./misc/list-gnophers $ ./main.sh ``` ```csv 1617467419,53785+jaekwon@users.noreply.github.com,./examples/gno.land/p/demo/flow/flow.gno 1651096034,94029+moul@users.noreply.github.com,./examples/gno.land/p/demo/grc/grc721/igrc721.gno 1673524438,hariom.verma@tendermint.com,./examples/gno.land/p/demo/grc/grc721/basic_nft.gno 1677669053,100383075+Jammyaa@users.noreply.github.com,./examples/gno.land/p/demo/grc/grc1155/basic_grc1155_token.gno 1678259597,350354+grepsuzette@users.noreply.github.com,./examples/gno.land/r/demo/deep/very/deep/render.gno 1678709422,pushkarshetye803@gmail.com,./examples/gno.land/r/demo/groups/group.gno 1684921090,contact@albttx.tech,./examples/gno.land/p/demo/merkle/merkle.gno 1687179019,nir1218@users.noreply.github.com,./examples/gno.land/r/x/nir1218_evaluation_proposal/category.gno 1687263124,zack.scholl@gmail.com,./examples/gno.land/p/demo/microblog/microblog.gno ``` ## Future Plans - Translate into GitHub usernames. - Share the gnopher list on `r/gh` (#1134). - Create a new `r/gnoland/gnophers` page with the following features: - Add a widget on `r/gnoland/home` displaying the "latest gnophers." - Create a helper function like `r/gnoland/gnophers.NumberByAddr("g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq`) -> `(2 int)` to use this info on other meta profiles, such as the future "gnolinkedin" mixing facts and personal presentation (https://github.com/gnolang/game-of-realms/pull/5). - Create a `r/gnoland/gnophers:username` route that returns a badge with the username, gnopher number, "gnopher since ," and a `gnoface` (#690). - Stop checking and order t-shirts for the first 100 official gnophers. ### Example Gnopher Badges ```markdown # @jaekwon proudly became the 1st gnopher on Oct 3, 2021. ||||||| ////////\ | | | ~ . | )| X X |. | | | C | | | | __/ | | | \~~~~~~~/ Gnopher#1 ``` ```markdown # @moul proudly became the 2nd gnopher on May 24, 2022. ||||||| ////////\ | | | ~ . | )| X X |. | | | C | | | | __/ | | | \~~~~~~~/ Gnopher#2 ``` cc @gnolang/devrels Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .mailmap | 12 ++++++++++++ misc/list-gnophers/main.sh | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .mailmap create mode 100755 misc/list-gnophers/main.sh diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000..2c81b9938ea --- /dev/null +++ b/.mailmap @@ -0,0 +1,12 @@ +# man 5 gitmailmap +# git log --mailmap --pretty=short | grep ^Author: | sort -u +Jae Kwon <53785+jaekwon@users.noreply.github.com> Jae Kwon +Jae Kwon <53785+jaekwon@users.noreply.github.com> Jae Kwon +Jae Kwon <53785+jaekwon@users.noreply.github.com> jaekwon +Jae Kwon <53785+jaekwon@users.noreply.github.com> Naut Jae +Thomas Bruyelle Thomas Bruyelle +Thomas Bruyelle Thomas Bruyelle +Miloš Živković Miloš Živković +Hariom Verma Hariom Verma +Giancarlos Salas Giancarlos Salas +Morgan Morgan diff --git a/misc/list-gnophers/main.sh b/misc/list-gnophers/main.sh new file mode 100755 index 00000000000..beb90d4c767 --- /dev/null +++ b/misc/list-gnophers/main.sh @@ -0,0 +1,35 @@ +#!/bin/sh + +main() { + cd ../.. + for file in $(list_gno_files); do + extract_file_metadata $file + done > gno_file_commits.csv + echo + cat gno_file_commits.csv | sort_by_date | unique_by_author +} + +list_gno_files() { + # list .gno file in examples/, remove tests and unit tests + find ./examples -name "*.gno" | grep -v _filetest.gno | grep -v _test.gno | grep -v gno.land/r/demo/tests +} + +extract_file_metadata() { + file=$1 + # get the first commit date of the file + first_commit_date=$(git log --pretty=format:%ct --follow $file | tail -n 1) + # get the email of the first contributor of the file + email=$(git log --mailmap --pretty=format:%aE --follow $file | tail -n 1) + # print the file name, first commit date, and email + echo "$first_commit_date,$email,$file" +} + +sort_by_date() { + sort -t, -k1 +} + +unique_by_author() { + awk -F, '!seek[$2]++' +} + +main From e10f811e38c75f830a4a3b5698a42ef2443b2f5b Mon Sep 17 00:00:00 2001 From: Thomas Bruyelle Date: Fri, 27 Oct 2023 20:09:13 +0200 Subject: [PATCH 46/93] docs(contributing): add ViM instructions to setup gnols (#1282) Relates to #1274 What works with this setup: - code completion after `.` but only for stdlibs packages - code hover tooltip with `:LspHover` command (again limited to stdlibs) - format on save (only gofmt for now) - code Lens with `:LspCodeLens` command (only when the buffer is `_test.gno` file) which display a list of possible `gno test` executions with different scopes (package, files or function). Note that test executions are using the LSP `workspace/executeCommand` and actually `vim-lsp` doesn't handle well the output of that. That means, tests are running, but you don't get the ouput. I'm still unsure about how to setup that properly (see https://github.com/prabirshrestha/vim-lsp/issues/1461). The good thing is the limitations can be removed by contributing to [gnols](https://github.com/gno-playground/gnols), for instance : - Expand code completion to all imported types - Expand code hover to all imported types - Add the *go-to-definition* feature - Add the *rename* feature - ... --------- Co-authored-by: Morgan --- CONTRIBUTING.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 279b7869152..1739d50f034 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -80,7 +80,7 @@ There currently is an unofficial [Visual Studio Code](https://marketplace.visual extension (primarily developed by a core team member) for working with `*.gno` files. -#### ViM Support +#### ViM Support (without LSP) Add to your `.vimrc` file: @@ -104,9 +104,68 @@ To use *gofumpt* instead of *gofmt*, as hinted in the comment, you may either ha cexpr system('go run -modfile /misc/devdeps/go.mod mvdan.cc/gofumpt -w ' . expand('%')) ``` +### ViM Support (with LSP) + There is an experimental and unofficial [Gno Language Server](https://github.com/jdkato/gnols) developed by the community, with an installation guide for Neovim. +For ViM purists, you have to install the [`vim-lsp`](https://github.com/prabirshrestha/vim-lsp) +plugin and then register the LSP server in your `.vimrc` file: + +```vim +augroup gno_autocmd + autocmd! + autocmd BufNewFile,BufRead *.gno + \ set filetype=gno | + \ set syntax=go +augroup END + +if (executable('gnols')) + au User lsp_setup call lsp#register_server({ + \ 'name': 'gnols', + \ 'cmd': ['gnols'], + \ 'allowlist': ['gno'], + \ 'config': {}, + \ 'workspace_config': { + \ 'root' : '/path/to/gno_repo', + \ 'gno' : '/path/to/gno_bin', + \ 'precompileOnSave' : v:true, + \ 'buildOnSave' : v:false, + \ }, + \ 'languageId': {server_info->'gno'}, + \ }) +else + echomsg 'gnols binary not found: LSP disabled for Gno files' +endif + +function! s:on_lsp_buffer_enabled() abort + " Autocompletion + setlocal omnifunc=lsp#complete + " Format on save + autocmd BufWritePre LspDocumentFormatSync + " Some optionnal mappings + nmap i (lsp-hover) + " Following mappings are not supported yet by gnols + " nmap gd (lsp-definition) + " nmap rr (lsp-rename) +endfunction +augroup lsp_install + au! + autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled() +augroup END +``` + +Note that unlike the previous ViM setup without LSP, here it is required by +`vim-lsp` to have a specific `filetype=gno`. Syntax highlighting is preserved +thanks to `syntax=go`. + +Inside `lsp#register_server()`, you also have to replace +`workspace_config.root` and `workspace_config.gno` with the correct directories +from your machine. + +Additionaly, it's not possible to use `gofumpt` for code formatting with +`gnols` for now. + #### Emacs Support 1. Install [go-mode.el](https://github.com/dominikh/go-mode.el). From 6ba4f744c316bf8b171dcf229afb97379e3fed8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Sat, 28 Oct 2023 17:29:00 -0400 Subject: [PATCH 47/93] feat: add genesis command suite (#1252) ## Description This PR introduces the genesis.json command suite, as outlined in #1203. Closes #1203 and #1189
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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).
--- gno.land/Makefile | 6 +- gno.land/cmd/genesis/balances.go | 39 + gno.land/cmd/genesis/balances_add.go | 405 ++++++++++ gno.land/cmd/genesis/balances_add_test.go | 719 ++++++++++++++++++ gno.land/cmd/genesis/balances_export.go | 78 ++ gno.land/cmd/genesis/balances_export_test.go | 155 ++++ gno.land/cmd/genesis/balances_remove.go | 103 +++ gno.land/cmd/genesis/balances_remove_test.go | 141 ++++ gno.land/cmd/genesis/generate.go | 153 ++++ gno.land/cmd/genesis/generate_test.go | 245 ++++++ gno.land/cmd/genesis/main.go | 58 ++ gno.land/cmd/genesis/txs.go | 39 + gno.land/cmd/genesis/txs_add.go | 141 ++++ gno.land/cmd/genesis/txs_add_test.go | 266 +++++++ gno.land/cmd/genesis/txs_export.go | 92 +++ gno.land/cmd/genesis/txs_export_test.go | 140 ++++ gno.land/cmd/genesis/txs_remove.go | 108 +++ gno.land/cmd/genesis/txs_remove_test.go | 136 ++++ gno.land/cmd/genesis/types.go | 71 ++ gno.land/cmd/genesis/validator.go | 49 ++ gno.land/cmd/genesis/validator_add.go | 137 ++++ gno.land/cmd/genesis/validator_add_test.go | 293 +++++++ gno.land/cmd/genesis/validator_remove.go | 71 ++ gno.land/cmd/genesis/validator_remove_test.go | 129 ++++ gno.land/cmd/genesis/verify.go | 80 ++ gno.land/cmd/genesis/verify_test.go | 169 ++++ tm2/pkg/bft/types/genesis.go | 64 +- tm2/pkg/bft/types/genesis_test.go | 118 +++ tm2/pkg/bft/types/params.go | 20 +- tm2/pkg/crypto/keys/client/add.go | 2 +- tm2/pkg/crypto/keys/client/export_test.go | 2 +- tm2/pkg/crypto/keys/client/helper.go | 4 +- 32 files changed, 4220 insertions(+), 13 deletions(-) create mode 100644 gno.land/cmd/genesis/balances.go create mode 100644 gno.land/cmd/genesis/balances_add.go create mode 100644 gno.land/cmd/genesis/balances_add_test.go create mode 100644 gno.land/cmd/genesis/balances_export.go create mode 100644 gno.land/cmd/genesis/balances_export_test.go create mode 100644 gno.land/cmd/genesis/balances_remove.go create mode 100644 gno.land/cmd/genesis/balances_remove_test.go create mode 100644 gno.land/cmd/genesis/generate.go create mode 100644 gno.land/cmd/genesis/generate_test.go create mode 100644 gno.land/cmd/genesis/main.go create mode 100644 gno.land/cmd/genesis/txs.go create mode 100644 gno.land/cmd/genesis/txs_add.go create mode 100644 gno.land/cmd/genesis/txs_add_test.go create mode 100644 gno.land/cmd/genesis/txs_export.go create mode 100644 gno.land/cmd/genesis/txs_export_test.go create mode 100644 gno.land/cmd/genesis/txs_remove.go create mode 100644 gno.land/cmd/genesis/txs_remove_test.go create mode 100644 gno.land/cmd/genesis/types.go create mode 100644 gno.land/cmd/genesis/validator.go create mode 100644 gno.land/cmd/genesis/validator_add.go create mode 100644 gno.land/cmd/genesis/validator_add_test.go create mode 100644 gno.land/cmd/genesis/validator_remove.go create mode 100644 gno.land/cmd/genesis/validator_remove_test.go create mode 100644 gno.land/cmd/genesis/verify.go create mode 100644 gno.land/cmd/genesis/verify_test.go diff --git a/gno.land/Makefile b/gno.land/Makefile index be1db280c40..22b9ec24650 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -9,22 +9,24 @@ rundep=go run -modfile ../misc/devdeps/go.mod gnoland.start:; go run ./cmd/gnoland start .PHONY: build -build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync +build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.gnotxsync build.genesis build.gnoland:; go build -o build/gnoland ./cmd/gnoland build.gnoweb:; go build -o build/gnoweb ./cmd/gnoweb build.gnofaucet:; go build -o build/gnofaucet ./cmd/gnofaucet build.gnokey:; go build -o build/gnokey ./cmd/gnokey build.gnotxsync:; go build -o build/gnotxsync ./cmd/gnotxsync +build.genesis:; go build -o build/genesis ./cmd/genesis .PHONY: install -install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync +install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb install.gnofaucet:; go install ./cmd/gnofaucet install.gnokey:; go install ./cmd/gnokey install.gnotxsync:; go install ./cmd/gnotxsync +install.genesis:; go install ./cmd/genesis .PHONY: fclean fclean: clean diff --git a/gno.land/cmd/genesis/balances.go b/gno.land/cmd/genesis/balances.go new file mode 100644 index 00000000000..bb6cd8b532c --- /dev/null +++ b/gno.land/cmd/genesis/balances.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type balancesCfg struct { + commonCfg +} + +// newBalancesCmd creates the genesis balances subcommand +func newBalancesCmd(io *commands.IO) *commands.Command { + cfg := &balancesCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "balances", + ShortUsage: "balances [flags]", + LongHelp: "Manipulates the initial genesis.json account balances (pre-mines)", + ShortHelp: "Manages genesis.json account balances", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newBalancesAddCmd(cfg, io), + newBalancesRemoveCmd(cfg, io), + newBalancesExportCmd(cfg, io), + ) + + return cmd +} + +func (c *balancesCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonCfg.RegisterFlags(fs) +} diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go new file mode 100644 index 00000000000..276e48690a8 --- /dev/null +++ b/gno.land/cmd/genesis/balances_add.go @@ -0,0 +1,405 @@ +package main + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + + _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" +) + +var ( + balanceRegex = regexp.MustCompile(`^(\w+)=(\d+)ugnot$`) + amountRegex = regexp.MustCompile(`^(\d+)ugnot$`) +) + +var ( + errNoBalanceSource = errors.New("at least one balance source must be set") + errBalanceParsingAborted = errors.New("balance parsing aborted") + errInvalidBalanceFormat = errors.New("invalid balance format encountered") + errInvalidAddress = errors.New("invalid address encountered") + errInvalidAmount = errors.New("invalid amount encountered") +) + +type balancesAddCfg struct { + rootCfg *balancesCfg + + balanceSheet string + singleEntries commands.StringArr + parseExport string +} + +// newBalancesAddCmd creates the genesis balances add subcommand +func newBalancesAddCmd(rootCfg *balancesCfg, io *commands.IO) *commands.Command { + cfg := &balancesAddCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "balances add [flags]", + LongHelp: "Adds a new validator to the genesis.json", + }, + cfg, + func(ctx context.Context, _ []string) error { + return execBalancesAdd(ctx, cfg, io) + }, + ) +} + +func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.balanceSheet, + "balance-sheet", + "", + "the path to the balance file containing addresses in the format
=ugnot", + ) + + fs.Var( + &c.singleEntries, + "single", + "the direct balance addition in the format
=ugnot", + ) + + fs.StringVar( + &c.parseExport, + "parse-export", + "", + "the path to the transaction export containing a list of transactions (JSONL)", + ) +} + +func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Validate the source is set correctly + var ( + singleEntriesSet = len(cfg.singleEntries) != 0 + balanceSheetSet = cfg.balanceSheet != "" + txFileSet = cfg.parseExport != "" + ) + + if !singleEntriesSet && !balanceSheetSet && !txFileSet { + return errNoBalanceSource + } + + finalBalances := make(accountBalances) + + // Get the balance sheet from the source + if singleEntriesSet { + balances, err := getBalancesFromEntries(cfg.singleEntries) + if err != nil { + return fmt.Errorf("unable to get balances from entries, %w", err) + } + + finalBalances.leftMerge(balances) + } + + if balanceSheetSet { + // Open the balance sheet + file, loadErr := os.Open(cfg.balanceSheet) + if loadErr != nil { + return fmt.Errorf("unable to open balance sheet, %w", loadErr) + } + + balances, err := getBalancesFromSheet(file) + if err != nil { + return fmt.Errorf("unable to get balances from balance sheet, %w", err) + } + + finalBalances.leftMerge(balances) + } + + if txFileSet { + // Open the transactions file + file, loadErr := os.Open(cfg.parseExport) + if loadErr != nil { + return fmt.Errorf("unable to open transactions file, %w", loadErr) + } + + balances, err := getBalancesFromTransactions(ctx, io, file) + if err != nil { + return fmt.Errorf("unable to get balances from tx file, %w", err) + } + + finalBalances.leftMerge(balances) + } + + // Initialize genesis app state if it is not initialized already + if genesis.AppState == nil { + genesis.AppState = gnoland.GnoGenesisState{} + } + + // Construct the initial genesis balance sheet + state := genesis.AppState.(gnoland.GnoGenesisState) + genesisBalances, err := extractGenesisBalances(state) + if err != nil { + return err + } + + // Merge the two balance sheets, with the input + // having precedence over the genesis balances + finalBalances.leftMerge(genesisBalances) + + // Save the balances + state.Balances = finalBalances.toList() + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "%d pre-mines saved", + len(finalBalances), + ) + + io.Println() + + for address, balance := range finalBalances { + io.Printfln("%s:%dugnot", address.String(), balance) + } + + return nil +} + +// getBalancesFromEntries extracts the balance entries +// from the array of balance +func getBalancesFromEntries(entries []string) (accountBalances, error) { + balances := make(accountBalances) + + for _, entry := range entries { + accountBalance, err := getBalanceFromEntry(entry) + if err != nil { + return nil, fmt.Errorf("unable to extract balance data, %w", err) + } + + balances[accountBalance.address] = accountBalance.amount + } + + return balances, nil +} + +// getBalancesFromSheet extracts the balance sheet from the passed in +// balance sheet file, that has the format of
=ugnot +func getBalancesFromSheet(sheet io.Reader) (accountBalances, error) { + // Parse the balances + balances := make(accountBalances) + scanner := bufio.NewScanner(sheet) + + for scanner.Scan() { + entry := scanner.Text() + + // Remove comments + entry = strings.Split(entry, "#")[0] + entry = strings.TrimSpace(entry) + + // Skip empty lines + if entry == "" { + continue + } + + accountBalance, err := getBalanceFromEntry(entry) + if err != nil { + return nil, fmt.Errorf("unable to extract balance data, %w", err) + } + + balances[accountBalance.address] = accountBalance.amount + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("error encountered while scanning, %w", err) + } + + return balances, nil +} + +// getBalancesFromTransactions constructs a balance map based on MsgSend messages. +// This way of determining the final balance sheet is not valid, since it doesn't take into +// account different message types (ex. MsgCall) that can initialize accounts with some balance values. +// The right way to do this sort of initialization is to spin up an in-memory node +// and execute the entire transaction history to determine touched accounts and final balances, +// and construct a balance sheet based off of this information +func getBalancesFromTransactions( + ctx context.Context, + io *commands.IO, + reader io.Reader, +) (accountBalances, error) { + balances := make(accountBalances) + + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return nil, errBalanceParsingAborted + default: + // Parse the amino JSON + var tx std.Tx + + line := scanner.Bytes() + + if err := amino.UnmarshalJSON(line, &tx); err != nil { + io.ErrPrintfln( + "invalid amino JSON encountered: %s", + string(line), + ) + + continue + } + + feeAmount, err := getAmountFromEntry(tx.Fee.GasFee.String()) + if err != nil { + io.ErrPrintfln( + "invalid gas fee amount encountered: %s", + tx.Fee.GasFee.String(), + ) + + continue + } + + for _, msg := range tx.Msgs { + if msg.Type() != "send" { + continue + } + + msgSend := msg.(bank.MsgSend) + + sendAmount, err := getAmountFromEntry(msgSend.Amount.String()) + if err != nil { + io.ErrPrintfln( + "invalid send amount encountered: %s", + msgSend.Amount.String(), + ) + + continue + } + + // This way of determining final account balances is not really valid, + // because we take into account only the ugnot transfer messages (MsgSend) + // and not other message types (like MsgCall), that can also + // initialize accounts with some balances. Because of this, + // we can run into a situation where a message send amount or fee + // causes an accounts balance to go < 0. In these cases, + // we initialize the account (it is present in the balance sheet), but + // with the balance of 0 + from := balances[msgSend.FromAddress] + to := balances[msgSend.ToAddress] + + to += sendAmount + + if from < sendAmount || from < feeAmount { + // Account cannot cover send amount / fee + // (see message above) + from = 0 + } + + if from > sendAmount { + from -= sendAmount + } + + if from > feeAmount { + from -= feeAmount + } + + balances[msgSend.FromAddress] = from + balances[msgSend.ToAddress] = to + } + } + } + + // Check for scanning errors + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf( + "error encountered while reading file, %w", + err, + ) + } + + return balances, nil +} + +// getAmountFromEntry +func getAmountFromEntry(entry string) (int64, error) { + matches := amountRegex.FindStringSubmatch(entry) + + // Check if there is a match + if len(matches) != 2 { + return 0, fmt.Errorf( + "invalid amount, %s", + entry, + ) + } + + amount, err := strconv.ParseInt(matches[1], 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid amount, %s", matches[1]) + } + + return amount, nil +} + +// getBalanceFromEntry extracts the account balance information +// from a single line in the form of:
=ugnot +func getBalanceFromEntry(entry string) (*accountBalance, error) { + matches := balanceRegex.FindStringSubmatch(entry) + if len(matches) != 3 { + return nil, fmt.Errorf("%w, %s", errInvalidBalanceFormat, entry) + } + + // Validate the address + address, err := crypto.AddressFromString(matches[1]) + if err != nil { + return nil, fmt.Errorf("%w, %w", errInvalidAddress, err) + } + + // Validate the amount + amount, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return nil, fmt.Errorf("%w, %w", errInvalidAmount, err) + } + + return &accountBalance{ + address: address, + amount: amount, + }, nil +} + +// extractGenesisBalances extracts the initial account balances from the +// genesis app state +func extractGenesisBalances(state gnoland.GnoGenesisState) (accountBalances, error) { + // Construct the initial genesis balance sheet + genesisBalances := make(accountBalances) + + for _, entry := range state.Balances { + accountBalance, err := getBalanceFromEntry(entry) + if err != nil { + return nil, fmt.Errorf("invalid genesis balance entry, %w", err) + } + + genesisBalances[accountBalance.address] = accountBalance.amount + } + + return genesisBalances, nil +} diff --git a/gno.land/cmd/genesis/balances_add_test.go b/gno.land/cmd/genesis/balances_add_test.go new file mode 100644 index 00000000000..f986ee85274 --- /dev/null +++ b/gno.land/cmd/genesis/balances_add_test.go @@ -0,0 +1,719 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "math" + "strconv" + "strings" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Balances_Add(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis", func(t *testing.T) { + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("no sources selected", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoBalanceSource.Error()) + }) + + t.Run("invalid genesis path", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("balances from entries", func(t *testing.T) { + t.Parallel() + + dummyKeys := getDummyKeys(t, 2) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + tempGenesis.Name(), + } + + amount := int64(10) + + for _, dummyKey := range dummyKeys { + args = append(args, "--single") + args = append( + args, + fmt.Sprintf( + "%s=%dugnot", + dummyKey.Address().String(), + amount, + ), + ) + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, entry := range state.Balances { + accountBalance, err := getBalanceFromEntry(entry) + require.NoError(t, err) + + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == accountBalance.address.String() { + assert.Equal(t, amount, accountBalance.amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + } + } + }) + + t.Run("balances from sheet", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + dummyKeys := getDummyKeys(t, 10) + amount := int64(10) + + balances := make([]string, len(dummyKeys)) + + // Add a random comment to the balances file output + balances = append(balances, "#comment\n") + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount, + ) + } + + // Write the balance sheet to a file + balanceSheet, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + _, err := balanceSheet.WriteString(strings.Join(balances, "\n")) + require.NoError(t, err) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + tempGenesis.Name(), + "--balance-sheet", + balanceSheet.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, entry := range state.Balances { + accountBalance, err := getBalanceFromEntry(entry) + require.NoError(t, err) + + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == accountBalance.address.String() { + assert.Equal(t, amount, accountBalance.amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + } + } + }) + + t.Run("balances from transactions", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + var ( + dummyKeys = getDummyKeys(t, 10) + amount = int64(10) + amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + gasFee = std.NewCoin("ugnot", 1000000) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + // Write the transactions to a file + txsFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + _, err := txsFile.WriteString(strings.Join(marshalledTxs, "\n")) + require.NoError(t, err) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + tempGenesis.Name(), + "--parse-export", + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, entry := range state.Balances { + accountBalance, err := getBalanceFromEntry(entry) + require.NoError(t, err) + + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for index, dummyKey := range dummyKeys { + checkAmount := amount + if index == 0 { + // the first address should + // have a balance of 0 + checkAmount = 0 + } + + if dummyKey.Address().String() == accountBalance.address.String() { + assert.Equal(t, checkAmount, accountBalance.amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + } + } + }) + + t.Run("balances overwrite", func(t *testing.T) { + t.Parallel() + + dummyKeys := getDummyKeys(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + state := gnoland.GnoGenesisState{ + // Set an initial balance value + Balances: []string{ + fmt.Sprintf( + "%s=%dugnot", + dummyKeys[0].Address().String(), + 100, + ), + }, + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "add", + "--genesis-path", + tempGenesis.Name(), + } + + amount := int64(10) + + for _, dummyKey := range dummyKeys { + args = append(args, "--single") + args = append( + args, + fmt.Sprintf( + "%s=%dugnot", + dummyKey.Address().String(), + amount, + ), + ) + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + require.Equal(t, len(dummyKeys), len(state.Balances)) + + for _, entry := range state.Balances { + accountBalance, err := getBalanceFromEntry(entry) + require.NoError(t, err) + + // Find the appropriate key + // (the genesis is saved with randomized balance order) + found := false + for _, dummyKey := range dummyKeys { + if dummyKey.Address().String() == accountBalance.address.String() { + assert.Equal(t, amount, accountBalance.amount) + + found = true + break + } + } + + if !found { + t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + } + } + }) +} + +func TestBalances_GetBalancesFromEntries(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := int64(10) + + balances := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount, + ) + } + + balanceMap, err := getBalancesFromEntries(balances) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()]) + } + }) + + t.Run("malformed balance, invalid format", func(t *testing.T) { + t.Parallel() + + balances := []string{ + "malformed balance", + } + + balanceMap, err := getBalancesFromEntries(balances) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, errInvalidBalanceFormat.Error()) + }) + + t.Run("malformed balance, invalid address", func(t *testing.T) { + t.Parallel() + + balances := []string{ + "dummyaddress=10ugnot", + } + + balanceMap, err := getBalancesFromEntries(balances) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, errInvalidAddress.Error()) + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + balanceMap, err := getBalancesFromEntries(balances) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, errInvalidAmount.Error()) + }) +} + +func TestBalances_GetBalancesFromSheet(t *testing.T) { + t.Parallel() + + t.Run("valid balances", func(t *testing.T) { + t.Parallel() + + // Generate dummy keys + dummyKeys := getDummyKeys(t, 2) + amount := int64(10) + + balances := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount, + ) + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + balanceMap, err := getBalancesFromSheet(reader) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys { + assert.Equal(t, amount, balanceMap[key.Address()]) + } + }) + + t.Run("malformed balance, invalid amount", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + balances := []string{ + fmt.Sprintf( + "%s=%sugnot", + dummyKey.Address().String(), + strconv.FormatUint(math.MaxUint64, 10), + ), + } + + reader := strings.NewReader(strings.Join(balances, "\n")) + + balanceMap, err := getBalancesFromSheet(reader) + + assert.Nil(t, balanceMap) + assert.ErrorContains(t, err, errInvalidAmount.Error()) + }) +} + +func TestBalances_GetBalancesFromTransactions(t *testing.T) { + t.Parallel() + + t.Run("valid transactions", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = getDummyKeys(t, 10) + amount = int64(10) + amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + gasFee = std.NewCoin("ugnot", 1000000) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + // Validate the balance map + assert.Len(t, balanceMap, len(dummyKeys)) + for _, key := range dummyKeys[1:] { + assert.Equal(t, amount, balanceMap[key.Address()]) + } + + assert.Equal(t, int64(0), balanceMap[sender.Address()]) + }) + + t.Run("malformed transaction, invalid fee amount", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = getDummyKeys(t, 10) + amount = int64(10) + amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + gasFee = std.NewCoin("gnos", 1) // invalid fee + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + assert.NotNil(t, balanceMap) + assert.Contains(t, mockErr.String(), "invalid gas fee amount") + }) + + t.Run("malformed transaction, invalid send amount", func(t *testing.T) { + t.Parallel() + + var ( + dummyKeys = getDummyKeys(t, 10) + amount = int64(10) + amountCoins = std.NewCoins(std.NewCoin("gnogno", amount)) // invalid send amount + gasFee = std.NewCoin("ugnot", 1) + txs = make([]std.Tx, 0) + ) + + sender := dummyKeys[0] + for _, dummyKey := range dummyKeys[1:] { + tx := std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: sender.Address(), + ToAddress: dummyKey.Address(), + Amount: amountCoins, + }, + }, + Fee: std.Fee{ + GasWanted: 10, + GasFee: gasFee, + }, + Signatures: make([]std.Signature, 0), + } + + txs = append(txs, tx) + } + + // Marshal the transactions into amino JSON + marshalledTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + marshalledTx, err := amino.MarshalJSON(tx) + require.NoError(t, err) + + marshalledTxs = append(marshalledTxs, string(marshalledTx)) + } + + mockErr := bytes.NewBufferString("") + io := commands.NewTestIO() + io.SetErr(commands.WriteNopCloser(mockErr)) + + reader := strings.NewReader(strings.Join(marshalledTxs, "\n")) + balanceMap, err := getBalancesFromTransactions(context.Background(), io, reader) + require.NoError(t, err) + + assert.NotNil(t, balanceMap) + assert.Contains(t, mockErr.String(), "invalid send amount") + }) +} diff --git a/gno.land/cmd/genesis/balances_export.go b/gno.land/cmd/genesis/balances_export.go new file mode 100644 index 00000000000..fd5ade26663 --- /dev/null +++ b/gno.land/cmd/genesis/balances_export.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +// newBalancesExportCmd creates the genesis balances export subcommand +func newBalancesExportCmd(balancesCfg *balancesCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "balances export [flags] ", + ShortHelp: "Exports the balances from the genesis.json", + LongHelp: "Exports the balances from the genesis.json to an output file", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execBalancesExport(balancesCfg, io, args) + }, + ) +} + +func execBalancesExport(cfg *balancesCfg, io *commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Load the genesis state + if genesis.AppState == nil { + return errAppStateNotSet + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + if len(state.Balances) == 0 { + io.Println("No genesis balances to export") + + return nil + } + + // Make sure the output file path is specified + if len(args) == 0 { + return errNoOutputFile + } + + // Open output file + outputFile, err := os.OpenFile( + args[0], + os.O_RDWR|os.O_CREATE|os.O_APPEND, + 0o755, + ) + if err != nil { + return fmt.Errorf("unable to create output file, %w", err) + } + + // Save the balances + for _, balance := range state.Balances { + if _, err = outputFile.WriteString( + fmt.Sprintf("%s\n", balance), + ); err != nil { + return fmt.Errorf("unable to write to output, %w", err) + } + } + + io.Printfln( + "Exported %d balances", + len(state.Balances), + ) + + return nil +} diff --git a/gno.land/cmd/genesis/balances_export_test.go b/gno.land/cmd/genesis/balances_export_test.go new file mode 100644 index 00000000000..33e4f7bc800 --- /dev/null +++ b/gno.land/cmd/genesis/balances_export_test.go @@ -0,0 +1,155 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getDummyBalanceLines generates dummy balance lines +func getDummyBalanceLines(t *testing.T, count int) []string { + t.Helper() + + dummyKeys := getDummyKeys(t, count) + amount := int64(10) + + balances := make([]string, len(dummyKeys)) + + for index, key := range dummyKeys { + balances[index] = fmt.Sprintf( + "%s=%dugnot", + key.Address().String(), + amount, + ) + } + + return balances +} + +func TestGenesis_Balances_Export(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "export", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + + t.Run("no output file specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Balances: getDummyBalanceLines(t, 1), + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoOutputFile.Error()) + }) + + t.Run("valid balances export", func(t *testing.T) { + t.Parallel() + + // Generate dummy balances + balances := getDummyBalanceLines(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Balances: balances, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the output file + outputFile, outputCleanup := testutils.NewTestFile(t) + t.Cleanup(outputCleanup) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "export", + "--genesis-path", + tempGenesis.Name(), + outputFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + scanner := bufio.NewScanner(outputFile) + + outputBalances := make([]string, 0) + for scanner.Scan() { + outputBalances = append(outputBalances, scanner.Text()) + } + + require.NoError(t, scanner.Err()) + + assert.Len(t, outputBalances, len(balances)) + + for index, balance := range outputBalances { + assert.Equal(t, balances[index], balance) + } + }) +} diff --git a/gno.land/cmd/genesis/balances_remove.go b/gno.land/cmd/genesis/balances_remove.go new file mode 100644 index 00000000000..f7e9092dc3b --- /dev/null +++ b/gno.land/cmd/genesis/balances_remove.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +var ( + errUnableToLoadGenesis = errors.New("unable to load genesis") + errBalanceNotFound = errors.New("genesis balances entry does not exist") +) + +type balancesRemoveCfg struct { + rootCfg *balancesCfg + + address string +} + +// newBalancesRemoveCmd creates the genesis balances remove subcommand +func newBalancesRemoveCmd(rootCfg *balancesCfg, io *commands.IO) *commands.Command { + cfg := &balancesRemoveCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "balances remove [flags]", + LongHelp: "Removes the balance information of a specific account", + }, + cfg, + func(_ context.Context, _ []string) error { + return execBalancesRemove(cfg, io) + }, + ) +} + +func (c *balancesRemoveCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.address, + "address", + "", + "the address of the account whose balance information should be removed from genesis.json", + ) +} + +func execBalancesRemove(cfg *balancesRemoveCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("%w, %w", errUnableToLoadGenesis, loadErr) + } + + // Validate the address + address, err := crypto.AddressFromString(cfg.address) + if err != nil { + return fmt.Errorf("%w, %w", errInvalidAddress, err) + } + + // Check if the genesis state is set at all + if genesis.AppState == nil { + return errAppStateNotSet + } + + // Construct the initial genesis balance sheet + state := genesis.AppState.(gnoland.GnoGenesisState) + genesisBalances, err := extractGenesisBalances(state) + if err != nil { + return err + } + + // Check if the genesis balance for the account is present + _, exists := genesisBalances[address] + if !exists { + return errBalanceNotFound + } + + // Drop the account pre-mine + delete(genesisBalances, address) + + // Save the balances + state.Balances = genesisBalances.toList() + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Pre-mine information for address %s removed", + address.String(), + ) + + return nil +} diff --git a/gno.land/cmd/genesis/balances_remove_test.go b/gno.land/cmd/genesis/balances_remove_test.go new file mode 100644 index 00000000000..29179c43604 --- /dev/null +++ b/gno.land/cmd/genesis/balances_remove_test.go @@ -0,0 +1,141 @@ +package main + +import ( + "context" + "fmt" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Balances_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis", func(t *testing.T) { + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("genesis app state not set", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = nil // not set + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + + t.Run("address is present", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + state := gnoland.GnoGenesisState{ + // Set an initial balance value + Balances: []string{ + fmt.Sprintf( + "%s=%dugnot", + dummyKey.Address().String(), + 100, + ), + }, + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the genesis was updated + genesis, loadErr := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, loadErr) + + require.NotNil(t, genesis.AppState) + + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + require.True(t, ok) + + assert.Len(t, state.Balances, 0) + }) + + t.Run("address not present", func(t *testing.T) { + t.Parallel() + + dummyKey := getDummyKey(t) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + state := gnoland.GnoGenesisState{ + Balances: []string{}, // Empty initial balance + } + genesis.AppState = state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "balances", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.ErrorContains(t, cmdErr, errBalanceNotFound.Error()) + }) +} diff --git a/gno.land/cmd/genesis/generate.go b/gno.land/cmd/genesis/generate.go new file mode 100644 index 00000000000..93f8553f9e7 --- /dev/null +++ b/gno.land/cmd/genesis/generate.go @@ -0,0 +1,153 @@ +package main + +import ( + "context" + "flag" + "fmt" + "time" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var defaultChainID = "dev" + +type generateCfg struct { + outputPath string + chainID string + genesisTime int64 + blockMaxTxBytes int64 + blockMaxDataBytes int64 + blockMaxGas int64 + blockTimeIota int64 +} + +// newGenerateCmd creates the genesis generate subcommand +func newGenerateCmd(io *commands.IO) *commands.Command { + cfg := &generateCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "generate", + ShortUsage: "generate [flags]", + LongHelp: "Generates a node's genesis.json based on specified parameters", + ShortHelp: "Generates a fresh genesis.json", + }, + cfg, + func(_ context.Context, _ []string) error { + return execGenerate(cfg, io) + }, + ) +} + +func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.outputPath, + "output-path", + "./genesis.json", + "the output path for the genesis.json", + ) + + fs.Int64Var( + &c.genesisTime, + "genesis-time", + time.Now().Unix(), + "the genesis creation time. Defaults to current time", + ) + + fs.StringVar( + &c.chainID, + "chain-id", + defaultChainID, + "the ID of the chain", + ) + + fs.Int64Var( + &c.blockMaxTxBytes, + "block-max-tx-bytes", + types.MaxBlockTxBytes, + "the max size of the block transaction", + ) + + fs.Int64Var( + &c.blockMaxDataBytes, + "block-max-data-bytes", + types.MaxBlockDataBytes, + "the max size of the block data", + ) + + fs.Int64Var( + &c.blockMaxGas, + "block-max-gas", + types.MaxBlockMaxGas, + "the max gas limit for the block", + ) + + fs.Int64Var( + &c.blockTimeIota, + "block-time-iota", + types.BlockTimeIotaMS, + "the block time iota (in ms)", + ) +} + +func execGenerate(cfg *generateCfg, io *commands.IO) error { + // Start with the default configuration + genesis := getDefaultGenesis() + + // Set the genesis time + if cfg.genesisTime > 0 { + genesis.GenesisTime = time.Unix(cfg.genesisTime, 0) + } + + // Set the chain ID + if cfg.chainID != "" { + genesis.ChainID = cfg.chainID + } + + // Set the max tx bytes + if cfg.blockMaxTxBytes > 0 { + genesis.ConsensusParams.Block.MaxTxBytes = cfg.blockMaxTxBytes + } + + // Set the max data bytes + if cfg.blockMaxDataBytes > 0 { + genesis.ConsensusParams.Block.MaxDataBytes = cfg.blockMaxDataBytes + } + + // Set the max block gas + if cfg.blockMaxGas > 0 { + genesis.ConsensusParams.Block.MaxGas = cfg.blockMaxGas + } + + // Set the block time IOTA + if cfg.blockTimeIota > 0 { + genesis.ConsensusParams.Block.TimeIotaMS = cfg.blockTimeIota + } + + // Validate the genesis + if validateErr := genesis.ValidateAndComplete(); validateErr != nil { + return fmt.Errorf("unable to validate genesis, %w", validateErr) + } + + // Save the genesis file to disk + if saveErr := genesis.SaveAs(cfg.outputPath); saveErr != nil { + return fmt.Errorf("unable to save genesis, %w", saveErr) + } + + io.Printfln("Genesis successfully generated at %s\n", cfg.outputPath) + + // Log the empty validator set warning + io.Printfln("WARN: Genesis is generated with an empty validator set") + + return nil +} + +// getDefaultGenesis returns the default genesis config +func getDefaultGenesis() *types.GenesisDoc { + return &types.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: defaultChainID, + ConsensusParams: types.DefaultConsensusParams(), + } +} diff --git a/gno.land/cmd/genesis/generate_test.go b/gno.land/cmd/genesis/generate_test.go new file mode 100644 index 00000000000..ca742e55150 --- /dev/null +++ b/gno.land/cmd/genesis/generate_test.go @@ -0,0 +1,245 @@ +package main + +import ( + "context" + "fmt" + "path/filepath" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Generate(t *testing.T) { + t.Parallel() + + t.Run("default genesis", func(t *testing.T) { + t.Parallel() + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + // Make sure the default configuration is set + defaultGenesis := getDefaultGenesis() + defaultGenesis.GenesisTime = genesis.GenesisTime + + assert.Equal(t, defaultGenesis, genesis) + }) + + t.Run("set chain ID", func(t *testing.T) { + t.Parallel() + + chainID := "example-chain-ID" + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--chain-id", + chainID, + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal(t, genesis.ChainID, chainID) + }) + + t.Run("set block max tx bytes", func(t *testing.T) { + t.Parallel() + + blockMaxTxBytes := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--block-max-tx-bytes", + fmt.Sprintf("%d", blockMaxTxBytes), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxTxBytes, + blockMaxTxBytes, + ) + }) + + t.Run("set block max data bytes", func(t *testing.T) { + t.Parallel() + + blockMaxDataBytes := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--block-max-data-bytes", + fmt.Sprintf("%d", blockMaxDataBytes), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxDataBytes, + blockMaxDataBytes, + ) + }) + + t.Run("set block max gas", func(t *testing.T) { + t.Parallel() + + blockMaxGas := int64(100) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--block-max-gas", + fmt.Sprintf("%d", blockMaxGas), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.MaxGas, + blockMaxGas, + ) + }) + + t.Run("set block time iota", func(t *testing.T) { + t.Parallel() + + blockTimeIota := int64(10) + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--block-time-iota", + fmt.Sprintf("%d", blockTimeIota), + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Load the genesis + genesis, readErr := types.GenesisDocFromFile(genesisPath) + require.NoError(t, readErr) + + assert.Equal( + t, + genesis.ConsensusParams.Block.TimeIotaMS, + blockTimeIota, + ) + }) + + t.Run("invalid genesis config (chain ID)", func(t *testing.T) { + t.Parallel() + + invalidChainID := "thischainidisunusuallylongsoitwillcausethetesttofail" + + tempDir, cleanup := testutils.NewTestCaseDir(t) + t.Cleanup(cleanup) + + genesisPath := filepath.Join(tempDir, "genesis.json") + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "generate", + "--chain-id", + invalidChainID, + "--output-path", + genesisPath, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) +} diff --git a/gno.land/cmd/genesis/main.go b/gno.land/cmd/genesis/main.go new file mode 100644 index 00000000000..c0b043b456a --- /dev/null +++ b/gno.land/cmd/genesis/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +func main() { + io := commands.NewDefaultIO() + cmd := newRootCmd(io) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) + + os.Exit(1) + } +} + +func newRootCmd(io *commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + ShortUsage: " [flags] [...]", + LongHelp: "Gno Genesis manipulation suite", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newGenerateCmd(io), + newValidatorCmd(io), + newVerifyCmd(io), + newBalancesCmd(io), + newTxsCmd(io), + ) + + return cmd +} + +// commonCfg is the common +// configuration for genesis commands +// that require a genesis.json +type commonCfg struct { + genesisPath string +} + +func (c *commonCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.genesisPath, + "genesis-path", + "./genesis.json", + "the path to the genesis.json", + ) +} diff --git a/gno.land/cmd/genesis/txs.go b/gno.land/cmd/genesis/txs.go new file mode 100644 index 00000000000..a7be307c4be --- /dev/null +++ b/gno.land/cmd/genesis/txs.go @@ -0,0 +1,39 @@ +package main + +import ( + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type txsCfg struct { + commonCfg +} + +// newTxsCmd creates the genesis txs subcommand +func newTxsCmd(io *commands.IO) *commands.Command { + cfg := &txsCfg{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "txs", + ShortUsage: "txs [flags]", + ShortHelp: "Manages the initial genesis transactions", + LongHelp: "Manages genesis transactions through input files", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newTxsAddCmd(cfg, io), + newTxsRemoveCmd(cfg, io), + newTxsExportCmd(cfg, io), + ) + + return cmd +} + +func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonCfg.RegisterFlags(fs) +} diff --git a/gno.land/cmd/genesis/txs_add.go b/gno.land/cmd/genesis/txs_add.go new file mode 100644 index 00000000000..027cedae0bd --- /dev/null +++ b/gno.land/cmd/genesis/txs_add.go @@ -0,0 +1,141 @@ +package main + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var ( + errInvalidTxsFile = errors.New("unable to open transactions file") + errNoTxsFileSpecified = errors.New("no txs file specified") + errTxsParsingAborted = errors.New("transaction parsing aborted") +) + +// newTxsAddCmd creates the genesis txs add subcommand +func newTxsAddCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "txs add ", + ShortHelp: "Imports transactions into the genesis.json", + LongHelp: "Imports the transactions from a tx-archive backup to the genesis.json", + }, + commands.NewEmptyConfig(), + func(ctx context.Context, args []string) error { + return execTxsAdd(ctx, txsCfg, io, args) + }, + ) +} + +func execTxsAdd( + ctx context.Context, + cfg *txsCfg, + io *commands.IO, + args []string, +) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Open the transactions files + if len(args) == 0 { + return errNoTxsFileSpecified + } + + parsedTxs := make([]std.Tx, 0) + for _, file := range args { + file, loadErr := os.Open(file) + if loadErr != nil { + return fmt.Errorf("%w, %w", errInvalidTxsFile, loadErr) + } + + txs, err := getTransactionsFromFile(ctx, file) + if err != nil { + return fmt.Errorf("unable to read file, %w", err) + } + + parsedTxs = append(parsedTxs, txs...) + } + + // Initialize the app state if it's not present + if genesis.AppState == nil { + genesis.AppState = gnoland.GnoGenesisState{} + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + + // Left merge the transactions + fileTxStore := txStore(parsedTxs) + genesisTxStore := txStore(state.Txs) + + // The genesis transactions have preference with the order + // in the genesis.json + if err := genesisTxStore.leftMerge(fileTxStore); err != nil { + return err + } + + // Save the state + state.Txs = genesisTxStore + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Saved %d transactions to genesis.json", + len(parsedTxs), + ) + + return nil +} + +// getTransactionsFromFile fetches the transactions from the +// specified reader +func getTransactionsFromFile(ctx context.Context, reader io.Reader) ([]std.Tx, error) { + txs := make([]std.Tx, 0) + + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + select { + case <-ctx.Done(): + return nil, errTxsParsingAborted + default: + // Parse the amino JSON + var tx std.Tx + + if err := amino.UnmarshalJSON(scanner.Bytes(), &tx); err != nil { + return nil, fmt.Errorf( + "unable to unmarshal amino JSON, %w", + err, + ) + } + + txs = append(txs, tx) + } + } + + // Check for scanning errors + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf( + "error encountered while reading file, %w", + err, + ) + } + + return txs, nil +} diff --git a/gno.land/cmd/genesis/txs_add_test.go b/gno.land/cmd/genesis/txs_add_test.go new file mode 100644 index 00000000000..7d194182fb0 --- /dev/null +++ b/gno.land/cmd/genesis/txs_add_test.go @@ -0,0 +1,266 @@ +package main + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/sdk/bank" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateDummyTxs generates dummy transactions +func generateDummyTxs(t *testing.T, count int) []std.Tx { + t.Helper() + + txs := make([]std.Tx, count) + + for i := 0; i < count; i++ { + txs[i] = std.Tx{ + Msgs: []std.Msg{ + bank.MsgSend{ + FromAddress: crypto.Address{byte(i)}, + ToAddress: crypto.Address{byte((i + 1) % count)}, + Amount: std.NewCoins(std.NewCoin("ugnot", 1)), + }, + }, + Fee: std.Fee{ + GasWanted: 1, + GasFee: std.NewCoin("ugnot", 1000000), + }, + Memo: fmt.Sprintf("tx %d", i), + } + } + + return txs +} + +// encodeDummyTxs encodes the transactions into amino JSON +func encodeDummyTxs(t *testing.T, txs []std.Tx) []string { + t.Helper() + + encodedTxs := make([]string, 0, len(txs)) + + for _, tx := range txs { + encodedTx, err := amino.MarshalJSON(tx) + if err != nil { + t.Fatalf("unable to marshal tx, %v", err) + } + + encodedTxs = append(encodedTxs, string(encodedTx)) + } + + return encodedTxs +} + +func TestGenesis_Txs_Add(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + tempGenesis.Name(), + "dummy-tx-file", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidTxsFile.Error()) + }) + + t.Run("no txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoTxsFileSpecified.Error()) + }) + + t.Run("malformed txs file", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + tempGenesis.Name(), + tempGenesis.Name(), // invalid txs file + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "unable to read file") + }) + + t.Run("valid txs file", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the transactions file + txsFile, txsCleanup := testutils.NewTestFile(t) + t.Cleanup(txsCleanup) + + _, err := txsFile.WriteString( + strings.Join( + encodeDummyTxs(t, txs), + "\n", + ), + ) + require.NoError(t, err) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + tempGenesis.Name(), + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)) + + for index, tx := range state.Txs { + assert.Equal(t, txs[index], tx) + } + }) + + t.Run("existing genesis txs", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesisState := gnoland.GnoGenesisState{ + Txs: txs[0 : len(txs)/2], + } + + genesis.AppState = genesisState + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the transactions file + txsFile, txsCleanup := testutils.NewTestFile(t) + t.Cleanup(txsCleanup) + + _, err := txsFile.WriteString( + strings.Join( + encodeDummyTxs(t, txs), + "\n", + ), + ) + require.NoError(t, err) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "add", + "--genesis-path", + tempGenesis.Name(), + txsFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)) + + for index, tx := range state.Txs { + assert.Equal(t, txs[index], tx) + } + }) +} diff --git a/gno.land/cmd/genesis/txs_export.go b/gno.land/cmd/genesis/txs_export.go new file mode 100644 index 00000000000..170166e8a37 --- /dev/null +++ b/gno.land/cmd/genesis/txs_export.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" +) + +var errNoOutputFile = errors.New("no output file path specified") + +// newTxsExportCmd creates the genesis txs export subcommand +func newTxsExportCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "export", + ShortUsage: "txs export [flags] ", + ShortHelp: "Exports the transactions from the genesis.json", + LongHelp: "Exports the transactions from the genesis.json to an output file", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execTxsExport(txsCfg, io, args) + }, + ) +} + +func execTxsExport(cfg *txsCfg, io *commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Load the genesis state + if genesis.AppState == nil { + return errAppStateNotSet + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + if len(state.Txs) == 0 { + io.Println("No genesis transactions to export") + + return nil + } + + // Make sure the output file path is specified + if len(args) == 0 { + return errNoOutputFile + } + + // Open output file + outputFile, err := os.OpenFile( + args[0], + os.O_RDWR|os.O_CREATE|os.O_APPEND, + 0o755, + ) + if err != nil { + return fmt.Errorf("unable to create output file, %w", err) + } + + // Save the transactions + for _, tx := range state.Txs { + // Marshal tx individual tx into JSON + jsonData, err := amino.MarshalJSON(tx) + if err != nil { + return fmt.Errorf("unable to marshal JSON data, %w", err) + } + + // Write the JSON data as a line to the file + if _, err = outputFile.Write(jsonData); err != nil { + return fmt.Errorf("unable to write to output, %w", err) + } + + // Write a newline character to separate JSON objects + if _, err = outputFile.WriteString("\n"); err != nil { + return fmt.Errorf("unable to write newline output, %w", err) + } + } + + io.Printfln( + "Exported %d transactions", + len(state.Txs), + ) + + return nil +} diff --git a/gno.land/cmd/genesis/txs_export_test.go b/gno.land/cmd/genesis/txs_export_test.go new file mode 100644 index 00000000000..bc84bc45f73 --- /dev/null +++ b/gno.land/cmd/genesis/txs_export_test.go @@ -0,0 +1,140 @@ +package main + +import ( + "bufio" + "context" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Txs_Export(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "export", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + + t.Run("no output file specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: generateDummyTxs(t, 1), + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "export", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoOutputFile.Error()) + }) + + t.Run("valid txs export", func(t *testing.T) { + t.Parallel() + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Prepare the output file + outputFile, outputCleanup := testutils.NewTestFile(t) + t.Cleanup(outputCleanup) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "export", + "--genesis-path", + tempGenesis.Name(), + outputFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transactions were written down + scanner := bufio.NewScanner(outputFile) + + outputTxs := make([]std.Tx, 0) + for scanner.Scan() { + var tx std.Tx + + require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx)) + + outputTxs = append(outputTxs, tx) + } + + require.NoError(t, scanner.Err()) + + assert.Len(t, outputTxs, len(txs)) + + for index, tx := range outputTxs { + assert.Equal(t, txs[index], tx) + } + }) +} diff --git a/gno.land/cmd/genesis/txs_remove.go b/gno.land/cmd/genesis/txs_remove.go new file mode 100644 index 00000000000..2aef44fe1e5 --- /dev/null +++ b/gno.land/cmd/genesis/txs_remove.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var ( + errAppStateNotSet = errors.New("genesis app state not set") + errNoTxHashSpecified = errors.New("no transaction hashes specified") + errTxNotFound = errors.New("transaction not present in genesis.json") +) + +// newTxsRemoveCmd creates the genesis txs remove subcommand +func newTxsRemoveCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "txs remove ", + ShortHelp: "Removes the transactions from the genesis.json", + LongHelp: "Removes the transactions using the transaction hash", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execTxsRemove(txsCfg, io, args) + }, + ) +} + +func execTxsRemove(cfg *txsCfg, io *commands.IO, args []string) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check if the genesis state is set at all + if genesis.AppState == nil { + return errAppStateNotSet + } + + // Make sure the transaction hashes are set + if len(args) == 0 { + return errNoTxHashSpecified + } + + state := genesis.AppState.(gnoland.GnoGenesisState) + + for _, inputHash := range args { + index := -1 + + for indx, tx := range state.Txs { + // Find the hash of the transaction + hash, err := getTxHash(tx) + if err != nil { + return fmt.Errorf("unable to generate tx hash, %w", err) + } + + // Check if the hashes match + if strings.ToLower(hash) == strings.ToLower(inputHash) { + index = indx + + break + } + } + + if index < 0 { + return errTxNotFound + } + + state.Txs = append(state.Txs[:index], state.Txs[index+1:]...) + + io.Printfln( + "Transaction %s removed from genesis.json", + inputHash, + ) + } + + genesis.AppState = state + + // Save the updated genesis + if err := genesis.SaveAs(cfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + return nil +} + +// getTxHash returns the hex hash representation of +// the transaction (Amino encoded) +func getTxHash(tx std.Tx) (string, error) { + encodedTx, err := amino.Marshal(tx) + if err != nil { + return "", fmt.Errorf("unable to marshal transaction, %w", err) + } + + txHash := types.Tx(encodedTx).Hash() + + return fmt.Sprintf("%X", txHash), nil +} diff --git a/gno.land/cmd/genesis/txs_remove_test.go b/gno.land/cmd/genesis/txs_remove_test.go new file mode 100644 index 00000000000..b89f2af761a --- /dev/null +++ b/gno.land/cmd/genesis/txs_remove_test.go @@ -0,0 +1,136 @@ +package main + +import ( + "context" + "testing" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Txs_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid genesis app state", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + genesis.AppState = nil // no app state + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "remove", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAppStateNotSet.Error()) + }) + t.Run("no transaction hash specified", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "remove", + "--genesis-path", + tempGenesis.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errNoTxHashSpecified.Error()) + }) + + t.Run("transaction removed", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + // Generate dummy txs + txs := generateDummyTxs(t, 10) + + genesis := getDefaultGenesis() + genesis.AppState = gnoland.GnoGenesisState{ + Txs: txs, + } + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + txHash, err := getTxHash(txs[0]) + require.NoError(t, err) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "txs", + "remove", + "--genesis-path", + tempGenesis.Name(), + txHash, + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + + // Validate the transaction was removed + updatedGenesis, err := types.GenesisDocFromFile(tempGenesis.Name()) + require.NoError(t, err) + require.NotNil(t, updatedGenesis.AppState) + + // Fetch the state + state := updatedGenesis.AppState.(gnoland.GnoGenesisState) + + assert.Len(t, state.Txs, len(txs)-1) + + for _, tx := range state.Txs { + genesisTxHash, err := getTxHash(tx) + require.NoError(t, err) + + assert.NotEqual(t, txHash, genesisTxHash) + } + }) +} diff --git a/gno.land/cmd/genesis/types.go b/gno.land/cmd/genesis/types.go new file mode 100644 index 00000000000..208eaddb6da --- /dev/null +++ b/gno.land/cmd/genesis/types.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// txStore is a wrapper for TM2 transactions +type txStore []std.Tx + +// leftMerge merges the two tx stores, with +// preference to the left +func (i *txStore) leftMerge(b txStore) error { + // Build out the tx hash map + txHashMap := make(map[string]struct{}, len(*i)) + + for _, tx := range *i { + txHash, err := getTxHash(tx) + if err != nil { + return err + } + + txHashMap[txHash] = struct{}{} + } + + for _, tx := range b { + txHash, err := getTxHash(tx) + if err != nil { + return err + } + + if _, exists := txHashMap[txHash]; !exists { + *i = append(*i, tx) + } + } + + return nil +} + +type ( + accountBalances map[types.Address]int64 // address -> balance (ugnot) + accountBalance struct { + address types.Address + amount int64 + } +) + +// toList linearizes the account balances map +func (a accountBalances) toList() []string { + balances := make([]string, 0, len(a)) + + for address, balance := range a { + balances = append( + balances, + fmt.Sprintf("%s=%dugnot", address, balance), + ) + } + + return balances +} + +// leftMerge left-merges the two maps +func (a accountBalances) leftMerge(b accountBalances) { + for key, bVal := range b { + if _, present := (a)[key]; !present { + (a)[key] = bVal + } + } +} diff --git a/gno.land/cmd/genesis/validator.go b/gno.land/cmd/genesis/validator.go new file mode 100644 index 00000000000..a1fee07d070 --- /dev/null +++ b/gno.land/cmd/genesis/validator.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + + "github.com/gnolang/gno/tm2/pkg/commands" +) + +type validatorCfg struct { + commonCfg + + address string +} + +// newValidatorCmd creates the genesis validator subcommand +func newValidatorCmd(io *commands.IO) *commands.Command { + cfg := &validatorCfg{ + commonCfg: commonCfg{}, + } + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "validator", + ShortUsage: "validator [flags]", + LongHelp: "Manipulates the genesis.json validator set", + ShortHelp: "Validator set management in genesis.json", + }, + cfg, + commands.HelpExec, + ) + + cmd.AddSubCommands( + newValidatorAddCmd(cfg, io), + newValidatorRemoveCmd(cfg, io), + ) + + return cmd +} + +func (c *validatorCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonCfg.RegisterFlags(fs) + + fs.StringVar( + &c.address, + "address", + "", + "the output path for the genesis.json", + ) +} diff --git a/gno.land/cmd/genesis/validator_add.go b/gno.land/cmd/genesis/validator_add.go new file mode 100644 index 00000000000..603bfa90caa --- /dev/null +++ b/gno.land/cmd/genesis/validator_add.go @@ -0,0 +1,137 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + _ "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +var ( + errInvalidPower = errors.New("invalid validator power") + errInvalidName = errors.New("invalid validator name") + errPublicKeyMismatch = errors.New("provided public key and address do not match") + errAddressPresent = errors.New("validator with same address already present in genesis.json") +) + +type validatorAddCfg struct { + rootCfg *validatorCfg + + pubKey string + name string + power int64 +} + +// newValidatorAddCmd creates the genesis validator add subcommand +func newValidatorAddCmd(validatorCfg *validatorCfg, io *commands.IO) *commands.Command { + cfg := &validatorAddCfg{ + rootCfg: validatorCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "add", + ShortUsage: "validator add [flags]", + LongHelp: "Adds a new validator to the genesis.json", + }, + cfg, + func(_ context.Context, _ []string) error { + return execValidatorAdd(cfg, io) + }, + ) +} + +func (c *validatorAddCfg) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &c.pubKey, + "pub-key", + "", + "the bech32 string representation of the validator's public key", + ) + + fs.StringVar( + &c.name, + "name", + "", + "the name of the validator (must be unique)", + ) + + fs.Int64Var( + &c.power, + "power", + 1, + "the voting power of the validator (must be > 0)", + ) +} + +func execValidatorAdd(cfg *validatorAddCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check the validator address + address, err := crypto.AddressFromString(cfg.rootCfg.address) + if err != nil { + return fmt.Errorf("invalid validator address, %w", err) + } + + // Check the voting power + if cfg.power < 1 { + return errInvalidPower + } + + // Check the name + if cfg.name == "" { + return errors.New("invalid validator name") + } + + // Check the public key + pubKey, err := crypto.PubKeyFromBech32(cfg.pubKey) + if err != nil { + return fmt.Errorf("invalid validator public key, %w", err) + } + + // Check the public key matches the address + if pubKey.Address() != address { + return errors.New("provided public key and address do not match") + } + + validator := types.GenesisValidator{ + Address: address, + PubKey: pubKey, + Power: cfg.power, + Name: cfg.name, + } + + // Check if the validator exists + for _, genesisValidator := range genesis.Validators { + // There is no need to check if the public keys match + // since the address is derived from it, and the derivation + // is checked already + if validator.Address == genesisValidator.Address { + return errAddressPresent + } + } + + // Add the validator + genesis.Validators = append(genesis.Validators, validator) + + // Save the updated genesis + if err := genesis.SaveAs(cfg.rootCfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Validator with address %s added to genesis file", + cfg.rootCfg.address, + ) + + return nil +} diff --git a/gno.land/cmd/genesis/validator_add_test.go b/gno.land/cmd/genesis/validator_add_test.go new file mode 100644 index 00000000000..37af4157e7c --- /dev/null +++ b/gno.land/cmd/genesis/validator_add_test.go @@ -0,0 +1,293 @@ +package main + +import ( + "context" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/bip39" + "github.com/gnolang/gno/tm2/pkg/crypto/hd" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/gnolang/gno/tm2/pkg/crypto/secp256k1" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// getDummyKey generates a random public key, +// and returns the key info +func getDummyKey(t *testing.T) crypto.PubKey { + t.Helper() + + mnemonic, err := client.GenerateMnemonic(256) + require.NoError(t, err) + + seed := bip39.NewSeed(mnemonic, "") + + return generateKeyFromSeed(seed, 0).PubKey() +} + +// generateKeyFromSeed generates a private key from +// the provided seed and index +func generateKeyFromSeed(seed []byte, index uint32) crypto.PrivKey { + pathParams := hd.NewFundraiserParams(0, crypto.CoinType, index) + + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + + //nolint:errcheck // This derivation can never error out, since the path params + // are always going to be valid + derivedPriv, _ := hd.DerivePrivateKeyForPath(masterPriv, ch, pathParams.String()) + + return secp256k1.PrivKeySecp256k1(derivedPriv) +} + +// getDummyKeys generates random keys for testing +func getDummyKeys(t *testing.T, count int) []crypto.PubKey { + t.Helper() + + dummyKeys := make([]crypto.PubKey, count) + + for i := 0; i < count; i++ { + dummyKeys[i] = getDummyKey(t) + } + + return dummyKeys +} + +func TestGenesis_Validator_Add(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid validator address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + "dummyaddress", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator address") + }) + + t.Run("invalid voting power", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := getDummyKey(t) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--power", + "-1", // invalid voting power + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorIs(t, cmdErr, errInvalidPower) + }) + + t.Run("invalid validator name", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := getDummyKey(t) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "", // invalid validator name + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errInvalidName.Error()) + }) + + t.Run("invalid public key", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + key := getDummyKey(t) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "example", + "--pub-key", + "invalidkey", // invalid pub key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator public key") + }) + + t.Run("public key address mismatch", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + dummyKeys := getDummyKeys(t, 2) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[0].Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(dummyKeys[1]), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errPublicKeyMismatch.Error()) + }) + + t.Run("validator with same address exists", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKeys := getDummyKeys(t, 2) + genesis := getDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKeys[0].Address(), + PubKey: dummyKeys[0], + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[0].Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(dummyKeys[0]), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errAddressPresent.Error()) + }) + + t.Run("valid genesis validator", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + key := getDummyKey(t) + genesis := getDefaultGenesis() + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "add", + "--genesis-path", + tempGenesis.Name(), + "--address", + key.Address().String(), + "--name", + "example", + "--pub-key", + crypto.PubKeyToBech32(key), // another key + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) +} diff --git a/gno.land/cmd/genesis/validator_remove.go b/gno.land/cmd/genesis/validator_remove.go new file mode 100644 index 00000000000..f769b53b0e1 --- /dev/null +++ b/gno.land/cmd/genesis/validator_remove.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "errors" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto" +) + +var errValidatorNotPresent = errors.New("validator not present in genesis.json") + +// newValidatorRemoveCmd creates the genesis validator remove subcommand +func newValidatorRemoveCmd(rootCfg *validatorCfg, io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "remove", + ShortUsage: "validator remove [flags]", + LongHelp: "Removes a validator from the genesis.json", + }, + commands.NewEmptyConfig(), + func(_ context.Context, _ []string) error { + return execValidatorRemove(rootCfg, io) + }, + ) +} + +func execValidatorRemove(cfg *validatorCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Check the validator address + address, err := crypto.AddressFromString(cfg.address) + if err != nil { + return fmt.Errorf("invalid validator address, %w", err) + } + + index := -1 + + for indx, validator := range genesis.Validators { + if validator.Address == address { + index = indx + + break + } + } + + if index < 0 { + return errors.New("validator not present in genesis.json") + } + + // Drop the validator + genesis.Validators = append(genesis.Validators[:index], genesis.Validators[index+1:]...) + + // Save the updated genesis + if err := genesis.SaveAs(cfg.genesisPath); err != nil { + return fmt.Errorf("unable to save genesis.json, %w", err) + } + + io.Printfln( + "Validator with address %s removed from genesis file", + cfg.address, + ) + + return nil +} diff --git a/gno.land/cmd/genesis/validator_remove_test.go b/gno.land/cmd/genesis/validator_remove_test.go new file mode 100644 index 00000000000..953657afe33 --- /dev/null +++ b/gno.land/cmd/genesis/validator_remove_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "testing" + + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Validator_Remove(t *testing.T) { + t.Parallel() + + t.Run("invalid genesis file", func(t *testing.T) { + t.Parallel() + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + "dummy-path", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errUnableToLoadGenesis.Error()) + }) + + t.Run("invalid validator address", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + genesis := getDefaultGenesis() + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + "dummyaddress", + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, "invalid validator address") + }) + + t.Run("validator not found", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKeys := getDummyKeys(t, 2) + genesis := getDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKeys[0].Address(), + PubKey: dummyKeys[0], + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKeys[1].Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.ErrorContains(t, cmdErr, errValidatorNotPresent.Error()) + }) + + t.Run("validator removed", func(t *testing.T) { + t.Parallel() + + tempGenesis, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + dummyKey := getDummyKey(t) + + genesis := getDefaultGenesis() + + // Set an existing validator + genesis.Validators = append(genesis.Validators, types.GenesisValidator{ + Address: dummyKey.Address(), + PubKey: dummyKey, + Power: 1, + Name: "example", + }) + + require.NoError(t, genesis.SaveAs(tempGenesis.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "validator", + "remove", + "--genesis-path", + tempGenesis.Name(), + "--address", + dummyKey.Address().String(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + assert.NoError(t, cmdErr) + }) +} diff --git a/gno.land/cmd/genesis/verify.go b/gno.land/cmd/genesis/verify.go new file mode 100644 index 00000000000..ba51f5801f6 --- /dev/null +++ b/gno.land/cmd/genesis/verify.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" +) + +var errInvalidGenesisState = errors.New("invalid genesis state type") + +type verifyCfg struct { + commonCfg +} + +// newVerifyCmd creates the genesis verify subcommand +func newVerifyCmd(io *commands.IO) *commands.Command { + cfg := &verifyCfg{} + + return commands.NewCommand( + commands.Metadata{ + Name: "verify", + ShortUsage: "verify [flags]", + LongHelp: "Verifies a node's genesis.json", + ShortHelp: "Verifies a genesis.json", + }, + cfg, + func(_ context.Context, _ []string) error { + return execVerify(cfg, io) + }, + ) +} + +func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { + c.commonCfg.RegisterFlags(fs) +} + +func execVerify(cfg *verifyCfg, io *commands.IO) error { + // Load the genesis + genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) + if loadErr != nil { + return fmt.Errorf("unable to load genesis, %w", loadErr) + } + + // Verify it + if validateErr := genesis.Validate(); validateErr != nil { + return fmt.Errorf("unable to verify genesis, %w", validateErr) + } + + // Validate the genesis state + if genesis.AppState != nil { + state, ok := genesis.AppState.(gnoland.GnoGenesisState) + if !ok { + return errInvalidGenesisState + } + + // Validate the initial transactions + for _, tx := range state.Txs { + if validateErr := tx.ValidateBasic(); validateErr != nil { + return fmt.Errorf("invalid transacton, %w", validateErr) + } + } + + // Validate the initial balances + for _, balance := range state.Balances { + if _, parseErr := std.ParseCoins(balance); parseErr != nil { + return fmt.Errorf("invalid balance %s, %w", balance, parseErr) + } + } + } + + io.Printfln("Genesis at %s is valid", cfg.genesisPath) + + return nil +} diff --git a/gno.land/cmd/genesis/verify_test.go b/gno.land/cmd/genesis/verify_test.go new file mode 100644 index 00000000000..fcc5305b9d0 --- /dev/null +++ b/gno.land/cmd/genesis/verify_test.go @@ -0,0 +1,169 @@ +package main + +import ( + "context" + "testing" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/mock" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/require" +) + +func TestGenesis_Verify(t *testing.T) { + t.Parallel() + + getValidTestGenesis := func() *types.GenesisDoc { + key := mock.GenPrivKey().PubKey() + + return &types.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "valid-chain-id", + ConsensusParams: types.DefaultConsensusParams(), + Validators: []types.GenesisValidator{ + { + Address: key.Address(), + PubKey: key, + Power: 1, + Name: "valid validator", + }, + }, + } + } + + t.Run("invalid txs", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + + g.AppState = gnoland.GnoGenesisState{ + Balances: []string{}, + Txs: []std.Tx{ + {}, + }, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) + + t.Run("invalid balances", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + + g.AppState = gnoland.GnoGenesisState{ + Balances: []string{ + "dummybalance", + }, + Txs: []std.Tx{}, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) + + t.Run("valid genesis", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + g.AppState = gnoland.GnoGenesisState{ + Balances: []string{}, + Txs: []std.Tx{}, + } + + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) + + t.Run("valid genesis, no state", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.NoError(t, cmdErr) + }) + + t.Run("invalid genesis state", func(t *testing.T) { + t.Parallel() + + tempFile, cleanup := testutils.NewTestFile(t) + t.Cleanup(cleanup) + + g := getValidTestGenesis() + g.AppState = "Totally invalid state" + require.NoError(t, g.SaveAs(tempFile.Name())) + + // Create the command + cmd := newRootCmd(commands.NewTestIO()) + args := []string{ + "verify", + "--genesis-path", + tempFile.Name(), + } + + // Run the command + cmdErr := cmd.ParseAndRun(context.Background(), args) + require.Error(t, cmdErr) + }) +} diff --git a/tm2/pkg/bft/types/genesis.go b/tm2/pkg/bft/types/genesis.go index f881e068558..c03f7acc09e 100644 --- a/tm2/pkg/bft/types/genesis.go +++ b/tm2/pkg/bft/types/genesis.go @@ -18,7 +18,17 @@ const ( MaxChainIDLen = 50 ) -//------------------------------------------------------------ +var ( + ErrEmptyChainID = errors.New("chain ID is empty") + ErrLongChainID = fmt.Errorf("chain ID cannot be longer than %d chars", MaxChainIDLen) + ErrInvalidGenesisTime = errors.New("invalid genesis time") + ErrNoValidators = errors.New("no validators in set") + ErrInvalidValidatorVotingPower = errors.New("validator has no voting power") + ErrInvalidValidatorAddress = errors.New("invalid validator address") + ErrValidatorPubKeyMismatch = errors.New("validator public key and address mismatch") +) + +// ------------------------------------------------------------ // core types for a genesis definition // NOTE: any changes to the genesis definition should // be reflected in the documentation: @@ -61,6 +71,54 @@ func (genDoc *GenesisDoc) ValidatorHash() []byte { return vset.Hash() } +// Validate validates the genesis doc +func (genDoc *GenesisDoc) Validate() error { + // Make sure the chain ID is not empty + if genDoc.ChainID == "" { + return ErrEmptyChainID + } + + // Make sure the chain ID is < max chain ID length + if len(genDoc.ChainID) > MaxChainIDLen { + return ErrLongChainID + } + + // Make sure the genesis time is valid + if genDoc.GenesisTime.IsZero() { + return ErrInvalidGenesisTime + } + + // Validate the consensus params + if consensusParamsErr := ValidateConsensusParams(genDoc.ConsensusParams); consensusParamsErr != nil { + return consensusParamsErr + } + + // Make sure there are validators in the set + if len(genDoc.Validators) == 0 { + return ErrNoValidators + } + + // Make sure the validators are valid + for _, v := range genDoc.Validators { + // Check the voting power + if v.Power == 0 { + return fmt.Errorf("%w, %s", ErrInvalidValidatorVotingPower, v.Name) + } + + // Check the address + if v.Address.IsZero() { + return fmt.Errorf("%w, %s", ErrInvalidValidatorAddress, v.Name) + } + + // Check the pub key -> address matching + if v.PubKey.Address() != v.Address { + return fmt.Errorf("%w, %s", ErrValidatorPubKeyMismatch, v.Name) + } + } + + return nil +} + // ValidateAndComplete checks that all necessary fields are present // and fills in defaults for optional fields left empty func (genDoc *GenesisDoc) ValidateAndComplete() error { @@ -95,7 +153,7 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error { return nil } -//------------------------------------------------------------ +// ------------------------------------------------------------ // Make genesis state from file // GenesisDocFromJSON unmarshalls JSON data into a GenesisDoc. @@ -126,7 +184,7 @@ func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { return genDoc, nil } -//---------------------------------------- +// ---------------------------------------- // Mock AppState (for testing) type MockAppState struct { diff --git a/tm2/pkg/bft/types/genesis_test.go b/tm2/pkg/bft/types/genesis_test.go index c8886f9bf0a..a8816bed2e7 100644 --- a/tm2/pkg/bft/types/genesis_test.go +++ b/tm2/pkg/bft/types/genesis_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -127,3 +128,120 @@ func randomGenesisDoc() *GenesisDoc { ConsensusParams: DefaultConsensusParams(), } } + +func TestGenesis_Validate(t *testing.T) { + t.Parallel() + + getValidTestGenesis := func() *GenesisDoc { + key := randPubKey() + + return &GenesisDoc{ + GenesisTime: time.Now(), + ChainID: "valid-chain-id", + ConsensusParams: DefaultConsensusParams(), + Validators: []GenesisValidator{ + { + Address: key.Address(), + PubKey: key, + Power: 1, + Name: "valid validator", + }, + }, + } + } + + t.Run("valid genesis", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + + require.NoError(t, g.Validate()) + }) + + t.Run("invalid chain ID (zero)", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ChainID = "" + + assert.ErrorIs(t, g.Validate(), ErrEmptyChainID) + }) + + t.Run("invalid chain ID (long)", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ChainID = "thischainidisunusuallylongsoitwillcausethetesttofail" + + assert.ErrorIs(t, g.Validate(), ErrLongChainID) + }) + + t.Run("invalid genesis time", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.GenesisTime = time.Time{} + + assert.ErrorIs(t, g.Validate(), ErrInvalidGenesisTime) + }) + + t.Run("invalid consensus params", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.ConsensusParams.Block.MaxTxBytes = -1 // invalid value + + assert.ErrorContains(t, g.Validate(), "MaxTxBytes") + }) + + t.Run("no validators", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{} + + assert.ErrorIs(t, g.Validate(), ErrNoValidators) + }) + + t.Run("invalid validator, no voting power", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 0, // no voting power + }, + } + + assert.ErrorIs(t, g.Validate(), ErrInvalidValidatorVotingPower) + }) + + t.Run("invalid validator, zero address", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 1, + Address: Address{}, // zero address + }, + } + + assert.ErrorIs(t, g.Validate(), ErrInvalidValidatorAddress) + }) + + t.Run("invalid validator, public key mismatch", func(t *testing.T) { + t.Parallel() + + g := getValidTestGenesis() + g.Validators = []GenesisValidator{ + { + Power: 1, + Address: Address{1}, + PubKey: randPubKey(), + }, + } + + assert.ErrorIs(t, g.Validate(), ErrValidatorPubKeyMismatch) + }) +} diff --git a/tm2/pkg/bft/types/params.go b/tm2/pkg/bft/types/params.go index e50f5c05b88..461d62a17b3 100644 --- a/tm2/pkg/bft/types/params.go +++ b/tm2/pkg/bft/types/params.go @@ -16,6 +16,18 @@ const ( // MaxBlockPartsCount is the maximum count of block parts. MaxBlockPartsCount = (MaxBlockSizeBytes / BlockPartSizeBytes) + 1 + + // MaxBlockTxBytes is the max size of the block transaction + MaxBlockTxBytes int64 = 1000000 // 1MB + + // MaxBlockDataBytes is the max size of the block data + MaxBlockDataBytes int64 = 2000000 // 2MB + + // MaxBlockMaxGas is the max gas limit for the block + MaxBlockMaxGas int64 = 10000000 // 10M gas + + // BlockTimeIotaMS is the block time iota (in ms) + BlockTimeIotaMS int64 = 100 // ms ) var validatorPubKeyTypeURLs = map[string]struct{}{ @@ -31,10 +43,10 @@ func DefaultConsensusParams() abci.ConsensusParams { func DefaultBlockParams() *abci.BlockParams { return &abci.BlockParams{ - MaxTxBytes: 1024 * 1024, // 1MB - MaxDataBytes: 22020096, // 21MB - MaxGas: -1, - TimeIotaMS: 1000, // 1s + MaxTxBytes: MaxBlockTxBytes, + MaxDataBytes: MaxBlockDataBytes, + MaxGas: MaxBlockMaxGas, + TimeIotaMS: BlockTimeIotaMS, } } diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index 30b612a9de2..c90dfc9f803 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -259,7 +259,7 @@ func execAdd(cfg *addCfg, args []string, io *commands.IO) error { } if len(mnemonic) == 0 { - mnemonic, err = generateMnemonic(mnemonicEntropySize) + mnemonic, err = GenerateMnemonic(mnemonicEntropySize) if err != nil { return err } diff --git a/tm2/pkg/crypto/keys/client/export_test.go b/tm2/pkg/crypto/keys/client/export_test.go index a5f1ec8f48e..7ddbeede993 100644 --- a/tm2/pkg/crypto/keys/client/export_test.go +++ b/tm2/pkg/crypto/keys/client/export_test.go @@ -44,7 +44,7 @@ func addRandomKeyToKeybase( encryptPassword string, ) (keys.Info, error) { // Generate a random mnemonic - mnemonic, err := generateMnemonic(mnemonicEntropySize) + mnemonic, err := GenerateMnemonic(mnemonicEntropySize) if err != nil { return nil, fmt.Errorf( "unable to generate a mnemonic phrase, %w", diff --git a/tm2/pkg/crypto/keys/client/helper.go b/tm2/pkg/crypto/keys/client/helper.go index 42a936910f7..525ad9071f8 100644 --- a/tm2/pkg/crypto/keys/client/helper.go +++ b/tm2/pkg/crypto/keys/client/helper.go @@ -2,9 +2,9 @@ package client import "github.com/gnolang/gno/tm2/pkg/crypto/bip39" -// generateMnemonic generates a new BIP39 mnemonic using the +// GenerateMnemonic generates a new BIP39 mnemonic using the // provided entropy size -func generateMnemonic(entropySize int) (string, error) { +func GenerateMnemonic(entropySize int) (string, error) { // Generate the entropy seed entropySeed, err := bip39.NewEntropy(entropySize) if err != nil { From 199cd29584d44812a0aec3606bbff37a320c609a Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sat, 28 Oct 2023 17:32:25 -0400 Subject: [PATCH 48/93] feat: add simple address validity check (#1303) ## Description This PR adds a simple `Valid()` check to the Address type. It is there to prevent most human errors, and doesn't actually validate the math behind bech32 (for now) - it relies only on the length of the address. Running `gno test .` will run the unit test for the `Valid()` function. Closes: #1298
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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).
--------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/grc/grc1155/util.gno | 2 +- examples/gno.land/p/demo/grc/grc20/util.gno | 2 +- examples/gno.land/p/demo/grc/grc721/util.gno | 2 +- gnovm/stdlibs/std/crypto.gno | 5 ++++ gnovm/stdlibs/std/crypto_test.gno | 30 +++++++++++++++++++ gnovm/stdlibs/stdshim/crypto.gno | 5 ++++ go.mod | 2 +- go.sum | 10 +++---- 8 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 gnovm/stdlibs/std/crypto_test.gno diff --git a/examples/gno.land/p/demo/grc/grc1155/util.gno b/examples/gno.land/p/demo/grc/grc1155/util.gno index 72a5d73561c..2c6452a1066 100644 --- a/examples/gno.land/p/demo/grc/grc1155/util.gno +++ b/examples/gno.land/p/demo/grc/grc1155/util.gno @@ -7,7 +7,7 @@ import ( const zeroAddress std.Address = "" func isValidAddress(addr std.Address) bool { - if addr.String() == "" { + if !addr.IsValid() { return false } return true diff --git a/examples/gno.land/p/demo/grc/grc20/util.gno b/examples/gno.land/p/demo/grc/grc20/util.gno index a70edf421ba..2892b036bbd 100644 --- a/examples/gno.land/p/demo/grc/grc20/util.gno +++ b/examples/gno.land/p/demo/grc/grc20/util.gno @@ -5,7 +5,7 @@ import "std" const zeroAddress = std.Address("") func checkIsValidAddress(addr std.Address) error { - if addr.String() == "" { + if !addr.IsValid() { return ErrInvalidAddress } return nil diff --git a/examples/gno.land/p/demo/grc/grc721/util.gno b/examples/gno.land/p/demo/grc/grc721/util.gno index f82ba98194a..bb6bf24d984 100644 --- a/examples/gno.land/p/demo/grc/grc721/util.gno +++ b/examples/gno.land/p/demo/grc/grc721/util.gno @@ -7,7 +7,7 @@ import ( var zeroAddress = std.Address("") func isValidAddress(addr std.Address) error { - if addr.String() == "" { + if !addr.IsValid() { return ErrInvalidAddress } return nil diff --git a/gnovm/stdlibs/std/crypto.gno b/gnovm/stdlibs/std/crypto.gno index 3ebd802dc3f..8d005dccf5c 100644 --- a/gnovm/stdlibs/std/crypto.gno +++ b/gnovm/stdlibs/std/crypto.gno @@ -6,6 +6,11 @@ func (a Address) String() string { return string(a) } +// IsValid checks if the address is of specific length. Doesn't check prefix or checksum for the address +func (a Address) IsValid() bool { + return len(a) == RawAddressSize*2 // hex length +} + const RawAddressSize = 20 type RawAddress [RawAddressSize]byte diff --git a/gnovm/stdlibs/std/crypto_test.gno b/gnovm/stdlibs/std/crypto_test.gno new file mode 100644 index 00000000000..293f3e06945 --- /dev/null +++ b/gnovm/stdlibs/std/crypto_test.gno @@ -0,0 +1,30 @@ +package std + +import ( + "testing" +) + +func TestValid(t *testing.T) { + type test struct { + inputAddress Address + expected bool + } + + testCases := []test{ + {inputAddress: "g1f4v282mwyhu29afke4vq5r2xzcm6z3ftnugcnv", expected: true}, + {inputAddress: "g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa", expected: true}, + {inputAddress: "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq", expected: true}, + {inputAddress: "g14da4n9hcynyzz83q607uu8keuh9hwlv42ra6fa", expected: true}, + {inputAddress: "", expected: false}, + {inputAddress: "000000000000", expected: false}, + {inputAddress: "0000000000000000000000000000000000000000000000000000000000000000000000", expected: false}, + } + + for _, tc := range testCases { + result := tc.inputAddress.IsValid() + + if result != tc.expected { + t.Fatalf("Expected: %t, got: %t", tc.expected, result) + } + } +} diff --git a/gnovm/stdlibs/stdshim/crypto.gno b/gnovm/stdlibs/stdshim/crypto.gno index 3ebd802dc3f..8d005dccf5c 100644 --- a/gnovm/stdlibs/stdshim/crypto.gno +++ b/gnovm/stdlibs/stdshim/crypto.gno @@ -6,6 +6,11 @@ func (a Address) String() string { return string(a) } +// IsValid checks if the address is of specific length. Doesn't check prefix or checksum for the address +func (a Address) IsValid() bool { + return len(a) == RawAddressSize*2 // hex length +} + const RawAddressSize = 20 type RawAddress [RawAddressSize]byte diff --git a/go.mod b/go.mod index 0dc4114f405..5cc274f82b4 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.7 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/nxadm/tail v1.4.8 // indirect + github.com/nxadm/tail v1.4.11 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.3 // indirect go.opencensus.io v0.22.5 // indirect diff --git a/go.sum b/go.sum index 2b3d717a7e5..0be3496a422 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= @@ -130,8 +130,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= @@ -230,12 +230,12 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= From 0076e4841630bd97687bfc19b46c3f79a17e5e2b Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Thu, 2 Nov 2023 14:06:35 +0100 Subject: [PATCH 49/93] feat: add p/demo/ownable (#1314) --- examples/gno.land/p/demo/ownable/errors.gno | 8 ++ examples/gno.land/p/demo/ownable/gno.mod | 1 + examples/gno.land/p/demo/ownable/ownable.gno | 57 +++++++++ .../gno.land/p/demo/ownable/ownable_test.gno | 113 ++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 examples/gno.land/p/demo/ownable/errors.gno create mode 100644 examples/gno.land/p/demo/ownable/gno.mod create mode 100644 examples/gno.land/p/demo/ownable/ownable.gno create mode 100644 examples/gno.land/p/demo/ownable/ownable_test.gno diff --git a/examples/gno.land/p/demo/ownable/errors.gno b/examples/gno.land/p/demo/ownable/errors.gno new file mode 100644 index 00000000000..ffbf6ab3f6f --- /dev/null +++ b/examples/gno.land/p/demo/ownable/errors.gno @@ -0,0 +1,8 @@ +package ownable + +import "errors" + +var ( + ErrUnauthorized = errors.New("unauthorized; caller is not owner") + ErrInvalidAddress = errors.New("new owner address is invalid") +) diff --git a/examples/gno.land/p/demo/ownable/gno.mod b/examples/gno.land/p/demo/ownable/gno.mod new file mode 100644 index 00000000000..9a9abb1e661 --- /dev/null +++ b/examples/gno.land/p/demo/ownable/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/ownable diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno new file mode 100644 index 00000000000..7f2eac008e1 --- /dev/null +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -0,0 +1,57 @@ +package ownable + +import ( + "std" +) + +// Ownable is meant to be used as a top-level object to make your contract ownable OR +// being embedded in a Gno object to manage per-object ownership. +type Ownable struct { + owner std.Address +} + +func New() *Ownable { + return &Ownable{ + owner: std.GetOrigCaller(), + } +} + +// TransferOwnership transfers ownership of the Ownable struct to a new address +func (o *Ownable) TransferOwnership(newOwner std.Address) error { + err := o.CallerIsOwner() + if err != nil { + return err + } + + if !newOwner.IsValid() { + return ErrInvalidAddress + } + + o.owner = newOwner + return nil +} + +// DropOwnership removes the owner, effectively disabling any owner-related actions +// Top-level usage: disables all only-owner actions/functions, +// Embedded usage: behaves like a burn functionality, removing the owner from the struct +func (o *Ownable) DropOwnership() error { + err := o.CallerIsOwner() + if err != nil { + return err + } + + o.owner = "" + return nil +} + +// CallerIsOwner checks if the caller of the function is the Realm's owner +func (o *Ownable) CallerIsOwner() error { + if std.GetOrigCaller() == o.owner { + return nil + } + return ErrUnauthorized +} + +func (o *Ownable) Owner() std.Address { + return o.owner +} diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno new file mode 100644 index 00000000000..f725795fd47 --- /dev/null +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -0,0 +1,113 @@ +package ownable + +import ( + "std" + "testing" +) + +var ( + firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") + secondCaller = std.Address("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") +) + +func TestNew(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + if firstCaller != result.owner { + t.Fatalf("Expected %s, got: %s\n", firstCaller, result.owner) + } +} + +func TestOwner(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + resultOwner := result.Owner() + + expected := firstCaller + if resultOwner != expected { + t.Fatalf("Expected %s, got: %s\n", expected, result) + } +} + +func TestTransferOwnership(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + o := New() + + err := o.TransferOwnership(secondCaller) + if err != nil { + t.Fatalf("TransferOwnership failed, %v", err) + } + + result := o.Owner() + if secondCaller != result { + t.Fatalf("Expected: %s, got: %s\n", secondCaller, result) + } +} + +func TestCallerIsOwner(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + o := New() + unauthorizedCaller := secondCaller + + std.TestSetOrigCaller(unauthorizedCaller) + + err := o.CallerIsOwner() + if err == nil { + t.Fatalf("Expected %s to not be owner\n", unauthorizedCaller) + } +} + +func TestDropOwnership(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + o := New() + + err := o.DropOwnership() + if err != nil { + t.Fatalf("DropOwnership failed, %v", err) + } + + owner := o.Owner() + if owner != "" { + t.Fatalf("Expected owner to be empty, not %s\n", owner) + } +} + +// Errors + +func TestErrUnauthorized(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + o := New() + + std.TestSetOrigCaller(secondCaller) + + err := o.TransferOwnership(firstCaller) + if err != ErrUnauthorized { + t.Fatalf("Should've been ErrUnauthorized, was %v", err) + } + + err = o.DropOwnership() + if err != ErrUnauthorized { + t.Fatalf("Should've been ErrUnauthorized, was %v", err) + } +} + +func TestErrInvalidAddress(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + o := New() + + err := o.TransferOwnership("") + if err != ErrInvalidAddress { + t.Fatalf("Should've been ErrInvalidAddress, was %v", err) + } + + err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") + if err != ErrInvalidAddress { + t.Fatalf("Should've been ErrInvalidAddress, was %v", err) + } +} From 789f4de1d4940eced86f6311479de0819f3f4b11 Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 2 Nov 2023 14:00:46 -0400 Subject: [PATCH 50/93] fix(misc/gendocs): convert paths for async-loaded scripts (#1307) This should allow js to fully load correctly on gnolang.github.io :) --- misc/gendocs/gendocs.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/misc/gendocs/gendocs.sh b/misc/gendocs/gendocs.sh index 4336516aba1..b50c597bc39 100755 --- a/misc/gendocs/gendocs.sh +++ b/misc/gendocs/gendocs.sh @@ -5,6 +5,8 @@ set -u DOC_DIR=godoc PKG=github.com/gnolang/gno +# Used to load /static content +STATIC_PREFIX=/gno # Run a pkgsite server which we will scrape. Use env to run it from our repo's root directory. env -C ../.. pkgsite & @@ -30,7 +32,11 @@ wget \ --page-requisites \ -erobots=off \ --accept-regex='8080/((search|license-policy|about|)$|(static|images)/|github.com/gnolang/)' \ - http://localhost:8080/ + http://localhost:8080/ \ + http://localhost:8080/static/frontend/frontend.js \ + http://localhost:8080/static/frontend/unit/unit.js \ + http://localhost:8080/static/frontend/unit/main/main.js \ + http://localhost:8080/third_party/dialog-polyfill/dialog-polyfill.js # Stop the pkgsite server kill -9 $DOC_PID @@ -46,6 +52,7 @@ mv localhost\:8080 $DOC_DIR find godoc -type f -exec sed -ri 's#http://localhost:8080/files/[^"]*/github.com/gnolang/([^/"]+)/([^"]*)#https://github.com/gnolang/\1/blob/master/\2#g s#http://localhost:8080/[^"?]*\?tab=(importedby|versions)#\##g s#http://localhost:8080([^")]*)#https://pkg.go.dev\1#g -s#/files/[^" ]*/(github.com/[^" ]*)/#\1#g' {} + +s#/files/[^" ]*/(github.com/[^" ]*)/#\1#g +s#s\.src = src;#s.src = "'"$STATIC_PREFIX"'" + src;#g' {} + echo "Docs can be found in $DOC_DIR" From 47493692c55b18f84bac67ed7b2c58fbda0fbca3 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Mon, 6 Nov 2023 22:07:47 +0530 Subject: [PATCH 51/93] feat: implement `gno mod tidy` (#1035) Contains initial implementation of `gno mod tidy`
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: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/workflows/examples.yml | 19 ++ examples/Makefile | 4 + examples/gno.land/p/demo/acl/gno.mod | 4 +- examples/gno.land/p/demo/blog/gno.mod | 6 +- examples/gno.land/p/demo/dom/gno.mod | 4 +- examples/gno.land/p/demo/flow/gno.mod | 2 +- examples/gno.land/p/demo/gnode/gno.mod | 2 +- .../gno.land/p/demo/grc/exts/vault/gno.mod | 4 +- examples/gno.land/p/demo/grc/grc1155/gno.mod | 6 +- examples/gno.land/p/demo/grc/grc20/gno.mod | 6 +- examples/gno.land/p/demo/grc/grc721/gno.mod | 6 +- examples/gno.land/p/demo/grc/grc777/gno.mod | 4 +- examples/gno.land/p/demo/groups/gno.mod | 5 +- .../gno.land/p/demo/math_eval/int32/gno.mod | 4 +- examples/gno.land/p/demo/microblog/gno.mod | 7 +- examples/gno.land/p/demo/mux/gno.mod | 2 +- examples/gno.land/p/demo/rand/gno.mod | 2 +- examples/gno.land/p/demo/svg/gno.mod | 4 +- examples/gno.land/p/demo/tests/gno.mod | 4 +- .../gno.land/p/demo/tests/subtests/gno.mod | 2 +- examples/gno.land/p/demo/ui/gno.mod | 2 +- examples/gno.land/r/demo/art/gnoface/gno.mod | 4 +- .../gno.land/r/demo/art/millipede/gno.mod | 4 +- examples/gno.land/r/demo/boards/gno.mod | 4 +- examples/gno.land/r/demo/foo1155/gno.mod | 6 +- examples/gno.land/r/demo/foo20/gno.mod | 6 +- examples/gno.land/r/demo/foo721/gno.mod | 6 +- examples/gno.land/r/demo/groups/gno.mod | 4 +- examples/gno.land/r/demo/keystore/gno.mod | 5 +- examples/gno.land/r/demo/math_eval/gno.mod | 4 +- examples/gno.land/r/demo/microblog/gno.mod | 4 +- examples/gno.land/r/demo/nft/gno.mod | 4 +- .../gno.land/r/demo/releases_example/gno.mod | 4 +- examples/gno.land/r/demo/tests/gno.mod | 4 +- .../gno.land/r/demo/tests/subtests/gno.mod | 2 +- examples/gno.land/r/demo/tests_foo/gno.mod | 4 +- examples/gno.land/r/demo/types/gno.mod | 4 +- examples/gno.land/r/demo/ui/gno.mod | 4 +- examples/gno.land/r/demo/users/gno.mod | 5 +- examples/gno.land/r/gnoland/blog/gno.mod | 4 +- examples/gno.land/r/gnoland/faucet/gno.mod | 6 +- examples/gno.land/r/gnoland/pages/gno.mod | 4 +- examples/gno.land/r/system/names/gno.mod | 4 +- examples/gno.land/r/x/manfred_outfmt/gno.mod | 4 +- gnovm/cmd/gno/mod.go | 105 +++++++++++ gnovm/cmd/gno/mod_test.go | 173 +++++++++++++++++- gnovm/pkg/doc/dirs.go | 31 +--- gnovm/pkg/gnomod/parse.go | 28 +++ gnovm/pkg/gnomod/parse_test.go | 57 ++++++ gnovm/tests/integ/invalid-gno-file/gno.mod | 1 + .../tests/integ/invalid-gno-file/invalid.gno | 1 + gnovm/tests/integ/valid2/gno.mod | 3 + gnovm/tests/integ/valid2/valid.gno | 11 ++ gnovm/tests/integ/valid2/valid_test.gno | 11 ++ gnovm/tests/integ/valid2/z_0_filetest.gno | 1 + 55 files changed, 488 insertions(+), 133 deletions(-) create mode 100644 gnovm/tests/integ/invalid-gno-file/gno.mod create mode 100644 gnovm/tests/integ/invalid-gno-file/invalid.gno create mode 100644 gnovm/tests/integ/valid2/gno.mod create mode 100644 gnovm/tests/integ/valid2/valid.gno create mode 100644 gnovm/tests/integ/valid2/valid_test.gno create mode 100644 gnovm/tests/integ/valid2/z_0_filetest.gno diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index b17c66d8e5a..d52ed5c0ba6 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -73,3 +73,22 @@ jobs: - run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/r/gnoland - run: go run ./gnovm/cmd/gno lint --verbose ./examples/gno.land/r/system # TODO: track coverage + mod-tidy: + strategy: + fail-fast: false + matrix: + go-version: [ "1.21.x" ] + # unittests: TODO: matrix with contracts + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + - uses: actions/checkout@v3 + - run: | + GNO_CMD="$(pwd)/gnovm/cmd/gno" + # Find all directories containing gno.mod file + find ./examples -name "gno.mod" -execdir go run "$GNO_CMD" mod tidy \; + # Check if there are changes after running gno mod tidy + git diff --exit-code || (echo "Some gno.mod files are not tidy, please run 'make tidy'." && exit 1) diff --git a/examples/Makefile b/examples/Makefile index f20072d9df2..9b628e01ce5 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -37,3 +37,7 @@ clean: GOFMT_FLAGS ?= -w fmt: go run -modfile ../misc/devdeps/go.mod mvdan.cc/gofumpt $(GOFMT_FLAGS) `find . -name "*.gno"` + +.PHONY: tidy +tidy: + find . -name "gno.mod" -execdir go run github.com/gnolang/gno/gnovm/cmd/gno mod tidy \; diff --git a/examples/gno.land/p/demo/acl/gno.mod b/examples/gno.land/p/demo/acl/gno.mod index 2aabe3a5645..176cde637bd 100644 --- a/examples/gno.land/p/demo/acl/gno.mod +++ b/examples/gno.land/p/demo/acl/gno.mod @@ -1,6 +1,6 @@ module gno.land/p/demo/acl require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/testutils" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/blog/gno.mod b/examples/gno.land/p/demo/blog/gno.mod index c8437af1732..65f58e7a0f6 100644 --- a/examples/gno.land/p/demo/blog/gno.mod +++ b/examples/gno.land/p/demo/blog/gno.mod @@ -1,7 +1,7 @@ module gno.land/p/demo/blog require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/mux" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/mux v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/dom/gno.mod b/examples/gno.land/p/demo/dom/gno.mod index e7d0e5a9e75..83ca827cf66 100644 --- a/examples/gno.land/p/demo/dom/gno.mod +++ b/examples/gno.land/p/demo/dom/gno.mod @@ -1,5 +1,3 @@ module gno.land/p/demo/dom -require ( - "gno.land/p/demo/avl" v0.0.0-latest -) +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/flow/gno.mod b/examples/gno.land/p/demo/flow/gno.mod index 4a4d4fb4d82..5adddbfe021 100644 --- a/examples/gno.land/p/demo/flow/gno.mod +++ b/examples/gno.land/p/demo/flow/gno.mod @@ -1 +1 @@ -module "gno.land/p/demo/flow" +module gno.land/p/demo/flow diff --git a/examples/gno.land/p/demo/gnode/gno.mod b/examples/gno.land/p/demo/gnode/gno.mod index e922821f7fd..a93c2051830 100644 --- a/examples/gno.land/p/demo/gnode/gno.mod +++ b/examples/gno.land/p/demo/gnode/gno.mod @@ -1 +1 @@ -module "gno.land/p/demo/gnode" +module gno.land/p/demo/gnode diff --git a/examples/gno.land/p/demo/grc/exts/vault/gno.mod b/examples/gno.land/p/demo/grc/exts/vault/gno.mod index 8b4d4524366..2720bf09d95 100644 --- a/examples/gno.land/p/demo/grc/exts/vault/gno.mod +++ b/examples/gno.land/p/demo/grc/exts/vault/gno.mod @@ -1,6 +1,6 @@ module gno.land/p/demo/grc/exts/vault require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/grc/grc20" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/grc/grc20 v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc1155/gno.mod b/examples/gno.land/p/demo/grc/grc1155/gno.mod index 33a8e55be71..0b2b85d8e86 100644 --- a/examples/gno.land/p/demo/grc/grc1155/gno.mod +++ b/examples/gno.land/p/demo/grc/grc1155/gno.mod @@ -1,7 +1,7 @@ module gno.land/p/demo/grc/grc1155 require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc20/gno.mod b/examples/gno.land/p/demo/grc/grc20/gno.mod index 5e6e13f834c..fd80766a956 100644 --- a/examples/gno.land/p/demo/grc/grc20/gno.mod +++ b/examples/gno.land/p/demo/grc/grc20/gno.mod @@ -1,7 +1,7 @@ module gno.land/p/demo/grc/grc20 require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/grc/exts" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/grc/exts v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc721/gno.mod b/examples/gno.land/p/demo/grc/grc721/gno.mod index 229fc3f739c..ea8c9c9e52e 100644 --- a/examples/gno.land/p/demo/grc/grc721/gno.mod +++ b/examples/gno.land/p/demo/grc/grc721/gno.mod @@ -1,7 +1,7 @@ module gno.land/p/demo/grc/grc721 require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/grc/grc777/gno.mod b/examples/gno.land/p/demo/grc/grc777/gno.mod index 2fddce3f8f8..9fbf2f2b7cd 100644 --- a/examples/gno.land/p/demo/grc/grc777/gno.mod +++ b/examples/gno.land/p/demo/grc/grc777/gno.mod @@ -1,5 +1,3 @@ module gno.land/p/demo/grc/grc777 -require ( - "gno.land/p/demo/grc/exts" v0.0.0-latest -) +require gno.land/p/demo/grc/exts v0.0.0-latest diff --git a/examples/gno.land/p/demo/groups/gno.mod b/examples/gno.land/p/demo/groups/gno.mod index b52ad8b05b1..0e9f7cf2a7c 100644 --- a/examples/gno.land/p/demo/groups/gno.mod +++ b/examples/gno.land/p/demo/groups/gno.mod @@ -1,7 +1,6 @@ module gno.land/p/demo/groups require ( - "gno.land/r/demo/boards" v0.0.0-latest - "gno.land/p/demo/maths" v0.0.0-latest - "gno.land/p/demo/testutils" v0.0.0-latest + gno.land/p/demo/maths v0.0.0-latest + gno.land/r/demo/boards v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/math_eval/int32/gno.mod b/examples/gno.land/p/demo/math_eval/int32/gno.mod index 9a1e634447c..de57497a699 100644 --- a/examples/gno.land/p/demo/math_eval/int32/gno.mod +++ b/examples/gno.land/p/demo/math_eval/int32/gno.mod @@ -1,5 +1,3 @@ module gno.land/p/demo/math_eval/int32 -require ( - "gno.land/p/demo/ufmt" v0.0.0-latest -) +require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/microblog/gno.mod b/examples/gno.land/p/demo/microblog/gno.mod index fe19b89f777..5964679efa6 100644 --- a/examples/gno.land/p/demo/microblog/gno.mod +++ b/examples/gno.land/p/demo/microblog/gno.mod @@ -1,7 +1,8 @@ module gno.land/p/demo/microblog require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/mux/gno.mod b/examples/gno.land/p/demo/mux/gno.mod index 13e4736bea8..972a531e14c 100644 --- a/examples/gno.land/p/demo/mux/gno.mod +++ b/examples/gno.land/p/demo/mux/gno.mod @@ -1 +1 @@ -module "gno.land/p/demo/mux" +module gno.land/p/demo/mux diff --git a/examples/gno.land/p/demo/rand/gno.mod b/examples/gno.land/p/demo/rand/gno.mod index 66082ea873e..098af152648 100644 --- a/examples/gno.land/p/demo/rand/gno.mod +++ b/examples/gno.land/p/demo/rand/gno.mod @@ -1,3 +1,3 @@ // Draft -module gno.land/p/demo/rand +module gno.land/p/demo/rand diff --git a/examples/gno.land/p/demo/svg/gno.mod b/examples/gno.land/p/demo/svg/gno.mod index adb2acf1350..0af7ba0636d 100644 --- a/examples/gno.land/p/demo/svg/gno.mod +++ b/examples/gno.land/p/demo/svg/gno.mod @@ -1,5 +1,3 @@ module gno.land/p/demo/svg -require ( - "gno.land/p/demo/ufmt" v0.0.0-latest -) +require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/p/demo/tests/gno.mod b/examples/gno.land/p/demo/tests/gno.mod index 229c2f62d9c..5d80e106567 100644 --- a/examples/gno.land/p/demo/tests/gno.mod +++ b/examples/gno.land/p/demo/tests/gno.mod @@ -1,6 +1,6 @@ module gno.land/p/demo/tests require ( - "gno.land/p/demo/tests/subtests" v0.0.0-latest - "gno.land/r/demo/tests" v0.0.0-latest + gno.land/p/demo/tests/subtests v0.0.0-latest + gno.land/r/demo/tests v0.0.0-latest ) diff --git a/examples/gno.land/p/demo/tests/subtests/gno.mod b/examples/gno.land/p/demo/tests/subtests/gno.mod index 26ec7c4879a..c8333722809 100644 --- a/examples/gno.land/p/demo/tests/subtests/gno.mod +++ b/examples/gno.land/p/demo/tests/subtests/gno.mod @@ -1,4 +1,4 @@ module gno.land/p/demo/tests/subtests -// TODO: this file should not exist. +// TODO: this file should not exist. // This is a temporary workaround. Until https://github.com/gnolang/gno/issues/852 diff --git a/examples/gno.land/p/demo/ui/gno.mod b/examples/gno.land/p/demo/ui/gno.mod index e71ee2d1ab1..41f5cb78d83 100644 --- a/examples/gno.land/p/demo/ui/gno.mod +++ b/examples/gno.land/p/demo/ui/gno.mod @@ -1 +1 @@ -module "gno.land/p/demo/ui" +module gno.land/p/demo/ui diff --git a/examples/gno.land/r/demo/art/gnoface/gno.mod b/examples/gno.land/r/demo/art/gnoface/gno.mod index 33d644206d6..bc17ee9df3b 100644 --- a/examples/gno.land/r/demo/art/gnoface/gno.mod +++ b/examples/gno.land/r/demo/art/gnoface/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/art/gnoface require ( - "gno.land/p/demo/rand" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest + gno.land/p/demo/rand v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/art/millipede/gno.mod b/examples/gno.land/r/demo/art/millipede/gno.mod index 10e3d2f1474..346e3a1673c 100644 --- a/examples/gno.land/r/demo/art/millipede/gno.mod +++ b/examples/gno.land/r/demo/art/millipede/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/demo/art/millipede -require ( - "gno.land/p/demo/ufmt" v0.0.0-latest -) +require gno.land/p/demo/ufmt v0.0.0-latest diff --git a/examples/gno.land/r/demo/boards/gno.mod b/examples/gno.land/r/demo/boards/gno.mod index 882d97fec83..434ad019883 100644 --- a/examples/gno.land/r/demo/boards/gno.mod +++ b/examples/gno.land/r/demo/boards/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/boards require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo1155/gno.mod b/examples/gno.land/r/demo/foo1155/gno.mod index 07c05ff5ef3..6fdf18a1658 100644 --- a/examples/gno.land/r/demo/foo1155/gno.mod +++ b/examples/gno.land/r/demo/foo1155/gno.mod @@ -1,7 +1,7 @@ module gno.land/r/demo/foo1155 require ( - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/grc/grc1155" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/grc/grc1155 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo20/gno.mod b/examples/gno.land/r/demo/foo20/gno.mod index 1dbe9e01e4f..516690ee66c 100644 --- a/examples/gno.land/r/demo/foo20/gno.mod +++ b/examples/gno.land/r/demo/foo20/gno.mod @@ -1,7 +1,7 @@ module gno.land/r/demo/foo20 require ( - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/grc/grc20" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/grc/grc20 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/foo721/gno.mod b/examples/gno.land/r/demo/foo721/gno.mod index b34ffd2b3fe..46c19e6ae55 100644 --- a/examples/gno.land/r/demo/foo721/gno.mod +++ b/examples/gno.land/r/demo/foo721/gno.mod @@ -1,7 +1,7 @@ module gno.land/r/demo/foo721 require ( - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/grc/grc721" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/grc/grc721 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/groups/gno.mod b/examples/gno.land/r/demo/groups/gno.mod index d97acbecc7a..fc6756e13e2 100644 --- a/examples/gno.land/r/demo/groups/gno.mod +++ b/examples/gno.land/r/demo/groups/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/groups require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/keystore/gno.mod b/examples/gno.land/r/demo/keystore/gno.mod index 88d59c9ccd6..af0b907c259 100644 --- a/examples/gno.land/r/demo/keystore/gno.mod +++ b/examples/gno.land/r/demo/keystore/gno.mod @@ -1,6 +1,7 @@ module gno.land/r/demo/keystore require ( - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/avl" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/math_eval/gno.mod b/examples/gno.land/r/demo/math_eval/gno.mod index 69d4e8c459b..0e3fcfe6e9b 100644 --- a/examples/gno.land/r/demo/math_eval/gno.mod +++ b/examples/gno.land/r/demo/math_eval/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/math_eval require ( - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/math_eval/int32" v0.0.0-latest + gno.land/p/demo/math_eval/int32 v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/microblog/gno.mod b/examples/gno.land/r/demo/microblog/gno.mod index 3d79a38d5fd..f496b1008ce 100644 --- a/examples/gno.land/r/demo/microblog/gno.mod +++ b/examples/gno.land/r/demo/microblog/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/microblog require ( - "gno.land/p/demo/microblog" v0.0.0-latest - "gno.land/r/demo/users" v0.0.0-latest + gno.land/p/demo/microblog v0.0.0-latest + gno.land/r/demo/users v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/nft/gno.mod b/examples/gno.land/r/demo/nft/gno.mod index 2fefdbd1907..89e0055be51 100644 --- a/examples/gno.land/r/demo/nft/gno.mod +++ b/examples/gno.land/r/demo/nft/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/nft require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/grc/grc721" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/grc/grc721 v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/releases_example/gno.mod b/examples/gno.land/r/demo/releases_example/gno.mod index 85bcd07f232..22f640fe797 100644 --- a/examples/gno.land/r/demo/releases_example/gno.mod +++ b/examples/gno.land/r/demo/releases_example/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/demo/releases_example -require ( - "gno.land/p/demo/releases" v0.0.0-latest -) +require gno.land/p/demo/releases v0.0.0-latest diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index 23dd3760157..9c5162f848e 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/demo/tests require ( - "gno.land/p/demo/testutils" v0.0.0-latest - "gno.land/r/demo/tests/subtests" v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/r/demo/tests/subtests v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/tests/subtests/gno.mod b/examples/gno.land/r/demo/tests/subtests/gno.mod index 80db73b4c15..9f466ff77b9 100644 --- a/examples/gno.land/r/demo/tests/subtests/gno.mod +++ b/examples/gno.land/r/demo/tests/subtests/gno.mod @@ -1,4 +1,4 @@ module gno.land/r/demo/tests/subtests -// TODO: this file should not exist. +// TODO: this file should not exist. // This is a temporary workaround. Until https://github.com/gnolang/gno/issues/852 diff --git a/examples/gno.land/r/demo/tests_foo/gno.mod b/examples/gno.land/r/demo/tests_foo/gno.mod index b19d8a21de1..226271ae4b0 100644 --- a/examples/gno.land/r/demo/tests_foo/gno.mod +++ b/examples/gno.land/r/demo/tests_foo/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/demo/tests_foo -require ( - "gno.land/r/demo/tests" v0.0.0-latest -) +require gno.land/r/demo/tests v0.0.0-latest diff --git a/examples/gno.land/r/demo/types/gno.mod b/examples/gno.land/r/demo/types/gno.mod index 5709668ef2a..0e86e5d5676 100644 --- a/examples/gno.land/r/demo/types/gno.mod +++ b/examples/gno.land/r/demo/types/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/demo/types -require ( - "gno.land/p/demo/avl" v0.0.0-latest -) +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/demo/ui/gno.mod b/examples/gno.land/r/demo/ui/gno.mod index 597c0f388a4..42be8cec3f0 100644 --- a/examples/gno.land/r/demo/ui/gno.mod +++ b/examples/gno.land/r/demo/ui/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/demo/ui -require ( - "gno.land/p/demo/ui" v0.0.0-latest -) +require gno.land/p/demo/ui v0.0.0-latest diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index 055b5816871..edd20eb2721 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -1,6 +1,3 @@ module gno.land/r/demo/users -require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/testutils" v0.0.0-latest -) +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/gnoland/blog/gno.mod b/examples/gno.land/r/gnoland/blog/gno.mod index a8b4f3ceaa9..1d64238cdc8 100644 --- a/examples/gno.land/r/gnoland/blog/gno.mod +++ b/examples/gno.land/r/gnoland/blog/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/gnoland/blog require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/blog" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/blog v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/faucet/gno.mod b/examples/gno.land/r/gnoland/faucet/gno.mod index 75e85df326b..693b0e795cf 100644 --- a/examples/gno.land/r/gnoland/faucet/gno.mod +++ b/examples/gno.land/r/gnoland/faucet/gno.mod @@ -1,7 +1,7 @@ module gno.land/r/gnoland/faucet require ( - "gno.land/p/demo/avl" 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/avl v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/gnoland/pages/gno.mod b/examples/gno.land/r/gnoland/pages/gno.mod index 0f5c4076509..31e9ad2c85b 100644 --- a/examples/gno.land/r/gnoland/pages/gno.mod +++ b/examples/gno.land/r/gnoland/pages/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/gnoland/pages require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/blog" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/blog v0.0.0-latest ) diff --git a/examples/gno.land/r/system/names/gno.mod b/examples/gno.land/r/system/names/gno.mod index 31c456f90e0..cd4fd0aae4a 100644 --- a/examples/gno.land/r/system/names/gno.mod +++ b/examples/gno.land/r/system/names/gno.mod @@ -1,5 +1,3 @@ module gno.land/r/system/names -require ( - "gno.land/p/demo/avl" v0.0.0-latest -) +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/r/x/manfred_outfmt/gno.mod b/examples/gno.land/r/x/manfred_outfmt/gno.mod index eef8ec5956e..e6f705c46b9 100644 --- a/examples/gno.land/r/x/manfred_outfmt/gno.mod +++ b/examples/gno.land/r/x/manfred_outfmt/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/x/manfred_outfmt require ( - "gno.land/p/demo/rand" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest + gno.land/p/demo/rand v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 267b7d99237..9fb5dd704ce 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -4,8 +4,12 @@ import ( "context" "flag" "fmt" + "go/parser" + "go/token" "os" "path/filepath" + "sort" + "strings" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/tm2/pkg/commands" @@ -31,6 +35,7 @@ func newModCmd(io *commands.IO) *commands.Command { cmd.AddSubCommands( newModDownloadCmd(io), newModInitCmd(), + newModTidy(io), ) return cmd @@ -66,6 +71,20 @@ func newModInitCmd() *commands.Command { ) } +func newModTidy(io *commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "tidy", + ShortUsage: "tidy", + ShortHelp: "Add missing and remove unused modules", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execModTidy(args, io) + }, + ) +} + func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.remote, @@ -152,3 +171,89 @@ func execModInit(args []string) error { return nil } + +func execModTidy(args []string, io *commands.IO) error { + if len(args) > 0 { + return flag.ErrHelp + } + + wd, err := os.Getwd() + if err != nil { + return err + } + fname := filepath.Join(wd, "gno.mod") + gm, err := gnomod.ParseGnoMod(fname) + if err != nil { + return err + } + + // Drop all existing requires + for _, r := range gm.Require { + gm.DropRequire(r.Mod.Path) + } + + imports, err := getGnoImports(wd) + if err != nil { + return err + } + for _, im := range imports { + // skip if importpath is modulepath + if im == gm.Module.Mod.Path { + continue + } + gm.AddRequire(im, "v0.0.0-latest") + } + + gm.Write(fname) + return nil +} + +// getGnoImports returns the list of gno imports from a given path. +// Note: It ignores subdirs. Since right now we are still deciding on +// how to handle subdirs. +// See: +// - https://github.com/gnolang/gno/issues/1024 +// - https://github.com/gnolang/gno/issues/852 +// +// TODO: move this to better location. +func getGnoImports(path string) ([]string, error) { + entries, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + allImports := make([]string, 0) + seen := make(map[string]struct{}) + for _, e := range entries { + filename := e.Name() + if ext := filepath.Ext(filename); ext != ".gno" { + continue + } + if strings.HasSuffix(filename, "_filetest.gno") { + continue + } + data, err := os.ReadFile(filepath.Join(path, filename)) + if err != nil { + return nil, err + } + fs := token.NewFileSet() + f, err := parser.ParseFile(fs, filename, data, parser.ImportsOnly) + if err != nil { + return nil, err + } + for _, imp := range f.Imports { + importPath := strings.TrimPrefix(strings.TrimSuffix(imp.Path.Value, `"`), `"`) + if !strings.HasPrefix(importPath, "gno.land/") { + continue + } + if _, ok := seen[importPath]; ok { + continue + } + allImports = append(allImports, importPath) + seen[importPath] = struct{}{} + } + } + sort.Strings(allImports) + + return allImports, nil +} diff --git a/gnovm/cmd/gno/mod_test.go b/gnovm/cmd/gno/mod_test.go index fdae3d12c7a..bbf106c8960 100644 --- a/gnovm/cmd/gno/mod_test.go +++ b/gnovm/cmd/gno/mod_test.go @@ -1,6 +1,13 @@ package main -import "testing" +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) func TestModApp(t *testing.T) { tc := []testMainCase{ @@ -9,7 +16,7 @@ func TestModApp(t *testing.T) { errShouldBe: "flag: help requested", }, - // test gno.mod download + // test `gno mod download` { args: []string{"mod", "download"}, testDir: "../../tests/integ/empty-dir", @@ -73,7 +80,7 @@ func TestModApp(t *testing.T) { errShouldContain: "fetch: writepackage: querychain", }, - // test gno.mod init with no module name + // test `gno mod init` with no module name { args: []string{"mod", "init"}, testDir: "../../tests/integ/valid1", @@ -110,7 +117,7 @@ func TestModApp(t *testing.T) { errShouldBe: "create gno.mod file: gno.mod file already exists", }, - // test gno.mod init with module name + // test `gno mod init` with module name { args: []string{"mod", "init", "gno.land/p/demo/foo"}, testDir: "../../tests/integ/empty-dir", @@ -137,6 +144,164 @@ func TestModApp(t *testing.T) { simulateExternalRepo: true, errShouldBe: "create gno.mod file: gno.mod file already exists", }, + + // test `gno mod tidy` with module name + { + args: []string{"mod", "tidy", "arg1"}, + testDir: "../../tests/integ/minimalist-gnomod", + simulateExternalRepo: true, + errShouldContain: "flag: help requested", + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/empty-dir", + simulateExternalRepo: true, + errShouldContain: "could not read gno.mod file", + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/invalid-module-version1", + simulateExternalRepo: true, + errShouldContain: "error parsing gno.mod file at", + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/minimalist-gnomod", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/require-remote-module", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/valid2", + simulateExternalRepo: true, + }, + { + args: []string{"mod", "tidy"}, + testDir: "../../tests/integ/invalid-gno-file", + simulateExternalRepo: true, + errShouldContain: "expected 'package', found packag", + }, } testMainCaseRun(t, tc) } + +func TestGetGnoImports(t *testing.T) { + workingDir, err := os.Getwd() + require.NoError(t, err) + + // create external dir + tmpDir, cleanUpFn := createTmpDir(t) + defer cleanUpFn() + + // cd to tmp directory + os.Chdir(tmpDir) + defer os.Chdir(workingDir) + + files := []struct { + name, data string + }{ + { + name: "file1.gno", + data: ` + package tmp + + import ( + "std" + + "gno.land/p/demo/pkg1" + ) + `, + }, + { + name: "file2.gno", + data: ` + package tmp + + import ( + "gno.land/p/demo/pkg1" + "gno.land/p/demo/pkg2" + ) + `, + }, + { + name: "file1_test.gno", + data: ` + package tmp + + import ( + "testing" + + "gno.land/p/demo/testpkg" + ) + `, + }, + { + name: "z_0_filetest.gno", + data: ` + package main + + import ( + "gno.land/p/demo/filetestpkg" + ) + `, + }, + + // subpkg files + { + name: filepath.Join("subtmp", "file1.gno"), + data: ` + package subtmp + + import ( + "std" + + "gno.land/p/demo/subpkg1" + ) + `, + }, + { + name: filepath.Join("subtmp", "file2.gno"), + data: ` + package subtmp + + import ( + "gno.land/p/demo/subpkg1" + "gno.land/p/demo/subpkg2" + ) + `, + }, + } + + // Expected list of imports + // - ignore subdirs + // - ignore duplicate + // - ignore *_filetest.gno + // - should be sorted + expected := []string{ + "gno.land/p/demo/pkg1", + "gno.land/p/demo/pkg2", + "gno.land/p/demo/testpkg", + } + + // Create subpkg dir + err = os.Mkdir("subtmp", 0o700) + require.NoError(t, err) + + // Create files + for _, f := range files { + err = os.WriteFile(f.name, []byte(f.data), 0o644) + require.NoError(t, err) + } + + imports, err := getGnoImports(tmpDir) + require.NoError(t, err) + + require.Equal(t, len(expected), len(imports)) + for i := range imports { + assert.Equal(t, expected[i], imports[i]) + } +} diff --git a/gnovm/pkg/doc/dirs.go b/gnovm/pkg/doc/dirs.go index 21216828ce4..19d312f6826 100644 --- a/gnovm/pkg/doc/dirs.go +++ b/gnovm/pkg/doc/dirs.go @@ -5,7 +5,6 @@ package doc import ( - "fmt" "log" "os" "path" @@ -52,7 +51,7 @@ func newDirs(dirs []string, modDirs []string) *bfsDirs { } for _, mdir := range modDirs { - gm, err := parseGnoMod(filepath.Join(mdir, "gno.mod")) + gm, err := gnomod.ParseGnoMod(filepath.Join(mdir, "gno.mod")) if err != nil { log.Printf("%v", err) continue @@ -68,34 +67,6 @@ func newDirs(dirs []string, modDirs []string) *bfsDirs { return d } -// tries to parse gno mod file given the filename, using Parse and Validate from -// the gnomod package -// -// TODO(tb): replace by `gnomod.ParseAt` ? The key difference is the latter -// looks for gno.mod in parent directories, while this function doesn't. -func parseGnoMod(fname string) (*gnomod.File, error) { - file, err := os.Stat(fname) - if err != nil { - return nil, fmt.Errorf("could not read gno.mod file: %w", err) - } - if file.IsDir() { - return nil, fmt.Errorf("invalid gno.mod at %q: is a directory", fname) - } - - b, err := os.ReadFile(fname) - if err != nil { - return nil, fmt.Errorf("could not read gno.mod file: %w", err) - } - gm, err := gnomod.Parse(fname, b) - if err != nil { - return nil, fmt.Errorf("error parsing gno.mod file at %q: %w", fname, err) - } - if err := gm.Validate(); err != nil { - return nil, fmt.Errorf("error validating gno.mod file at %q: %w", fname, err) - } - return gm, nil -} - func getGnoModDirs(gm *gnomod.File) []bfsDir { // cmd/go makes use of the go list command, we don't have that here. diff --git a/gnovm/pkg/gnomod/parse.go b/gnovm/pkg/gnomod/parse.go index 5bda3c31f70..a6314d5729f 100644 --- a/gnovm/pkg/gnomod/parse.go +++ b/gnovm/pkg/gnomod/parse.go @@ -42,6 +42,34 @@ func ParseAt(dir string) (*File, error) { return gm, nil } +// tries to parse gno mod file given the filename, using Parse and Validate from +// the gnomod package +// +// TODO(tb): replace by `gnomod.ParseAt` ? The key difference is the latter +// looks for gno.mod in parent directories, while this function doesn't. +func ParseGnoMod(fname string) (*File, error) { + file, err := os.Stat(fname) + if err != nil { + return nil, fmt.Errorf("could not read gno.mod file: %w", err) + } + if file.IsDir() { + return nil, fmt.Errorf("invalid gno.mod at %q: is a directory", fname) + } + + b, err := os.ReadFile(fname) + if err != nil { + return nil, fmt.Errorf("could not read gno.mod file: %w", err) + } + gm, err := Parse(fname, b) + if err != nil { + return nil, fmt.Errorf("error parsing gno.mod file at %q: %w", fname, err) + } + if err := gm.Validate(); err != nil { + return nil, fmt.Errorf("error validating gno.mod file at %q: %w", fname, err) + } + return gm, nil +} + // Parse parses and returns a gno.mod file. // // - file is the name of the file, used in positions and errors. diff --git a/gnovm/pkg/gnomod/parse_test.go b/gnovm/pkg/gnomod/parse_test.go index 934531e69c7..61aaa83482b 100644 --- a/gnovm/pkg/gnomod/parse_test.go +++ b/gnovm/pkg/gnomod/parse_test.go @@ -1,9 +1,12 @@ package gnomod import ( + "path/filepath" "testing" + "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestModuleDeprecated(t *testing.T) { @@ -167,3 +170,57 @@ func TestParseDraft(t *testing.T) { }) } } + +func TestParseGnoMod(t *testing.T) { + pkgDir := "bar" + for _, tc := range []struct { + desc, modData, modPath, errShouldContain string + }{ + { + desc: "file not exists", + modData: `module foo`, + modPath: filepath.Join(pkgDir, "mod.gno"), + errShouldContain: "could not read gno.mod file:", + }, + { + desc: "file path is dir", + modData: `module foo`, + modPath: pkgDir, + errShouldContain: "is a directory", + }, + { + desc: "valid gno.mod file", + modData: `module foo`, + modPath: filepath.Join(pkgDir, "gno.mod"), + }, + { + desc: "error parsing gno.mod", + modData: `module foo v0.0.0`, + modPath: filepath.Join(pkgDir, "gno.mod"), + errShouldContain: "error parsing gno.mod file at", + }, + { + desc: "error validating gno.mod", + modData: `require bar v0.0.0`, + modPath: filepath.Join(pkgDir, "gno.mod"), + errShouldContain: "error validating gno.mod file at", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + // Create test dir + tempDir, cleanUpFn := testutils.NewTestCaseDir(t) + require.NotNil(t, tempDir) + defer cleanUpFn() + + // Create gno package + createGnoModPkg(t, tempDir, pkgDir, tc.modData) + + _, err := ParseGnoMod(filepath.Join(tempDir, tc.modPath)) + if tc.errShouldContain != "" { + assert.ErrorContains(t, err, tc.errShouldContain) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/gnovm/tests/integ/invalid-gno-file/gno.mod b/gnovm/tests/integ/invalid-gno-file/gno.mod new file mode 100644 index 00000000000..060e28b9dc4 --- /dev/null +++ b/gnovm/tests/integ/invalid-gno-file/gno.mod @@ -0,0 +1 @@ +module test diff --git a/gnovm/tests/integ/invalid-gno-file/invalid.gno b/gnovm/tests/integ/invalid-gno-file/invalid.gno new file mode 100644 index 00000000000..1e4ff406ada --- /dev/null +++ b/gnovm/tests/integ/invalid-gno-file/invalid.gno @@ -0,0 +1 @@ +packag invalid diff --git a/gnovm/tests/integ/valid2/gno.mod b/gnovm/tests/integ/valid2/gno.mod new file mode 100644 index 00000000000..98a5a0dacc1 --- /dev/null +++ b/gnovm/tests/integ/valid2/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/integ/valid + +require gno.land/p/demo/avl v0.0.0-latest diff --git a/gnovm/tests/integ/valid2/valid.gno b/gnovm/tests/integ/valid2/valid.gno new file mode 100644 index 00000000000..4de283f5d87 --- /dev/null +++ b/gnovm/tests/integ/valid2/valid.gno @@ -0,0 +1,11 @@ +package valid + +import ( + "gno.land/p/demo/avl" +) + +const Foo = "foo" + +func DoNothing(t *avl.Tree) { + // noop +} diff --git a/gnovm/tests/integ/valid2/valid_test.gno b/gnovm/tests/integ/valid2/valid_test.gno new file mode 100644 index 00000000000..2394da5c5ae --- /dev/null +++ b/gnovm/tests/integ/valid2/valid_test.gno @@ -0,0 +1,11 @@ +package valid + +import ( + "testing" + + "gno.land/p/integ/valid" +) + +func TestAlwaysValid(t *testing.T) { + _ = valid.Foo +} diff --git a/gnovm/tests/integ/valid2/z_0_filetest.gno b/gnovm/tests/integ/valid2/z_0_filetest.gno new file mode 100644 index 00000000000..06ab7d0f9a3 --- /dev/null +++ b/gnovm/tests/integ/valid2/z_0_filetest.gno @@ -0,0 +1 @@ +package main From ce7a7c66dc637364e5b0abab8eb037cfbea44ce3 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 6 Nov 2023 18:57:57 +0100 Subject: [PATCH 52/93] fix(misc/list-gnophers): make script work when called from any directory (#1308) \+ use a tmpdir for the csv file. @moul
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x ] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests - [x] 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).
--- misc/list-gnophers/main.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/misc/list-gnophers/main.sh b/misc/list-gnophers/main.sh index beb90d4c767..0b230e97948 100755 --- a/misc/list-gnophers/main.sh +++ b/misc/list-gnophers/main.sh @@ -1,12 +1,13 @@ #!/bin/sh main() { + cd "$(dirname "$0")" cd ../.. + fname="$(mktemp --tmpdir gno_file_commits.XXXXXXXXXX.csv)" for file in $(list_gno_files); do extract_file_metadata $file - done > gno_file_commits.csv - echo - cat gno_file_commits.csv | sort_by_date | unique_by_author + done > "$fname" + cat "$fname" | sort_by_date | unique_by_author } list_gno_files() { From 4c7f93c46a54c469f1f0d1ca1d1fb7a8ca1cb3d9 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Tue, 7 Nov 2023 00:42:57 +0530 Subject: [PATCH 53/93] ci(fix): tidy remaining gno.mod files (#1338) CI failing after merging #1035 Seems like some mod files are not tidy. --- examples/gno.land/r/gnoland/home/gno.mod | 7 +++---- examples/gno.land/r/manfred/present/gno.mod | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/gno.land/r/gnoland/home/gno.mod b/examples/gno.land/r/gnoland/home/gno.mod index 9192b4364d0..2864958930c 100644 --- a/examples/gno.land/r/gnoland/home/gno.mod +++ b/examples/gno.land/r/gnoland/home/gno.mod @@ -1,8 +1,7 @@ module gno.land/r/gnoland/home require ( - "gno.land/r/gnoland/blog" v0.0.0-latest - "gno.land/p/demo/ufmt" v0.0.0-latest - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/ui" v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/ui v0.0.0-latest + gno.land/r/gnoland/blog v0.0.0-latest ) diff --git a/examples/gno.land/r/manfred/present/gno.mod b/examples/gno.land/r/manfred/present/gno.mod index 9d1ab5b0e56..5d50447e0e0 100644 --- a/examples/gno.land/r/manfred/present/gno.mod +++ b/examples/gno.land/r/manfred/present/gno.mod @@ -1,6 +1,6 @@ module gno.land/r/manfred/present require ( - "gno.land/p/demo/avl" v0.0.0-latest - "gno.land/p/demo/blog" v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/blog v0.0.0-latest ) From 42c013248e52807bff2712a86f86993b7765c462 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Tue, 7 Nov 2023 09:07:01 +0100 Subject: [PATCH 54/93] feat(repl): improve support of multi-line statements (#1129) ![demo](https://github.com/gnolang/gno/assets/5792239/308e61bc-bdf9-498b-9fa7-cd756835f774) This is a followup of #978. Instead of starting in multi-line mode prior to submit, the line is parsed, and new inputs are appended to it as long as the statment is not complete, as detected by the Go scanner. This is simpler and more general than previous attempt. The secondary prompt is "...", different from primary "gno>", similarly to many REPL programs (node, python, bash, ...). The "/editor" command is removed as not useful anymore. Note also that it is now possible to exit using Ctrl-D. Related issues: #446 #950
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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: Manfred Touron <94029+moul@users.noreply.github.com> --- gnovm/cmd/gno/repl.go | 101 ++++++++++++++++++++--------------------- gnovm/pkg/repl/repl.go | 2 +- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/gnovm/cmd/gno/repl.go b/gnovm/cmd/gno/repl.go index 1acb96c3cb9..0a9d4934ce3 100644 --- a/gnovm/cmd/gno/repl.go +++ b/gnovm/cmd/gno/repl.go @@ -2,10 +2,11 @@ package main import ( "bufio" - "bytes" "context" + "errors" "flag" "fmt" + "go/scanner" "os" "strings" @@ -88,10 +89,10 @@ func execRepl(cfg *replCfg, args []string) error { // gno> import "gno.land/p/demo/avl" // import the p/demo/avl package // gno> func a() string { return "a" } // declare a new function named a // gno> /src // print current generated source -// gno> /editor // enter in editor mode to add several lines +// gno> /editor // enter in multi-line mode, end with ';' // gno> /reset // remove all previously inserted code // gno> println(a()) // print the result of calling a() -// gno> /exit +// gno> /exit // alternative to `) } @@ -99,30 +100,59 @@ func execRepl(cfg *replCfg, args []string) error { } func runRepl(cfg *replCfg) error { - // init repl state r := repl.NewRepl() if cfg.initialCommand != "" { handleInput(r, cfg.initialCommand) } - var multiline bool - for { - fmt.Fprint(os.Stdout, "gno> ") + fmt.Fprint(os.Stdout, "gno> ") - input, err := getInput(multiline) - if err != nil { - return err + inEdit := false + prev := "" + liner := bufio.NewScanner(os.Stdin) + + for liner.Scan() { + line := liner.Text() + + if l := strings.TrimSpace(line); l == ";" { + line, inEdit = "", false + } else if l == "/editor" { + line, inEdit = "", true + fmt.Fprintln(os.Stdout, "// enter a single ';' to quit and commit") + } + if prev != "" { + line = prev + "\n" + line + prev = "" + } + if inEdit { + fmt.Fprint(os.Stdout, "... ") + prev = line + continue } - multiline = handleInput(r, input) + if err := handleInput(r, line); err != nil { + var goScanError scanner.ErrorList + if errors.As(err, &goScanError) { + // We assune that a Go scanner error indicates an incomplete Go statement. + // Append next line and retry. + prev = line + } else { + fmt.Fprintln(os.Stderr, err) + } + } + + if prev == "" { + fmt.Fprint(os.Stdout, "gno> ") + } else { + fmt.Fprint(os.Stdout, "... ") + } } + return nil } -// handleInput reads the input string and parses it depending if it -// is a specific command, or source code. It returns true if the following -// input is expected to be on more than one line. -func handleInput(r *repl.Repl, input string) bool { +// handleInput executes specific "/" commands, or evaluates input as Gno source code. +func handleInput(r *repl.Repl, input string) error { switch strings.TrimSpace(input) { case "/reset": r.Reset() @@ -130,49 +160,14 @@ func handleInput(r *repl.Repl, input string) bool { fmt.Fprintln(os.Stdout, r.Src()) case "/exit": os.Exit(0) - case "/editor": - fmt.Fprintln(os.Stdout, "// Entering editor mode (^D to finish)") - return true case "": - // avoid to increase the repl execution counter if sending empty content - fmt.Fprintln(os.Stdout, "") - return false + // Avoid to increase the repl execution counter if no input. default: out, err := r.Process(input) if err != nil { - fmt.Fprintln(os.Stderr, err) + return err } fmt.Fprintln(os.Stdout, out) } - - return false -} - -const ( - inputBreaker = "^D" - nl = "\n" -) - -func getInput(ml bool) (string, error) { - s := bufio.NewScanner(os.Stdin) - var mlOut bytes.Buffer - for s.Scan() { - line := s.Text() - if !ml { - return line, nil - } - - if line == inputBreaker { - break - } - - mlOut.WriteString(line) - mlOut.WriteString(nl) - } - - if err := s.Err(); err != nil { - return "", err - } - - return mlOut.String(), nil + return nil } diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index 0f60b948f39..c7786cf08b0 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -161,7 +161,7 @@ func (r *Repl) Process(input string) (out string, err error) { return r.handleExpression(exp) } - return "", fmt.Errorf("error parsing code:\n\t- as expression (error: %q)\n\t- as declarations (error: %q)", expErr.Error(), declErr.Error()) + return "", fmt.Errorf("error parsing code:\n\t- as expression: %w\n\t- as declarations: %w", expErr, declErr) } func (r *Repl) handleExpression(e *ast.File) (string, error) { From a90a273e0f1a573d04da782d3b0c064d98f8bb24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:58:59 +0100 Subject: [PATCH 55/93] chore(deps): bump actions/checkout from 3 to 4 (#1339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
Release notes

Sourced from actions/checkout's releases.

v4.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v4.0.0

v3.6.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3.5.3...v3.6.0

v3.5.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v3...v3.5.3

v3.5.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v3.5.1...v3.5.2

v3.5.1

What's Changed

New Contributors

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.1.0

v4.0.0

v3.6.0

v3.5.3

v3.5.2

v3.5.1

v3.5.0

v3.4.0

v3.3.0

v3.2.0

v3.1.0

v3.0.2

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/examples.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index d52ed5c0ba6..f69a27d15b6 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -85,7 +85,7 @@ jobs: - uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: | GNO_CMD="$(pwd)/gnovm/cmd/gno" # Find all directories containing gno.mod file From 23c5b3bd29f4f99e90c358a92a52ca614fc5ed4a Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:00:41 +0100 Subject: [PATCH 56/93] chore(ci): update tm2 timeout on CI (#1337) It should prevent certain flaky tests from failing on GH. In any case, it increases the high-level timeout to be greater than the timeout for running 'go test'. Related to issue #1320. --- .github/workflows/tm2.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tm2.yml b/.github/workflows/tm2.yml index 7b78ccb1e0f..d5c6d9ddda6 100644 --- a/.github/workflows/tm2.yml +++ b/.github/workflows/tm2.yml @@ -55,7 +55,7 @@ jobs: - _test.pkg.bft - _test.pkg.others runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 21 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 @@ -65,7 +65,7 @@ jobs: working-directory: tm2 run: | export GOPATH=$HOME/go - export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" + export GOTEST_FLAGS="-v -p 1 -timeout=20m -coverprofile=coverage.out -covermode=atomic" make ${{ matrix.args }} touch coverage.out - uses: actions/upload-artifact@v3 From e6b4890af8462ca07658a7db815f66721a3aa15d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:11:09 +0100 Subject: [PATCH 57/93] chore(deps): bump github.com/gorilla/websocket from 1.5.0 to 1.5.1 (#1335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.0 to 1.5.1.
Release notes

Sourced from github.com/gorilla/websocket's releases.

Release v1.5.1

What's Changed

New Contributors

Full Changelog: https://github.com/gorilla/websocket/compare/v1.5.0...v1.5.1

Commits
  • ac0789b update GitHub workflows (#857)
  • 78c3487 update golang.org/x/net (#856)
  • 666c197 Update go version & add verification/testing tools (#840)
  • 8039329 Correct way to save memory using write buffer pool and freeing net.http defau...
  • 8983b96 Merge pull request #839 from gorilla/coreydaley-patch-1
  • 931041c Update README.md
  • 76ecc29 archive mode
  • af47554 check error before GotConn for trace
  • bc7ce89 Check for and report bad protocol in TLSClientConfig.NextProtos (#788)
  • 27d91a9 drop the versions of go that are no longer supported + add 1.18 to ci
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gorilla/websocket&package-manager=go_modules&previous-version=1.5.0&new-version=1.5.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5cc274f82b4..0a120db3e96 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/golang/protobuf v1.5.3 github.com/google/gofuzz v1.2.0 github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.5.0 + github.com/gorilla/websocket v1.5.1 github.com/gotuna/gotuna v0.6.0 github.com/jaekwon/testify v1.6.1 github.com/jmhodges/levigo v1.0.0 diff --git a/go.sum b/go.sum index 0be3496a422..009a75c6a46 100644 --- a/go.sum +++ b/go.sum @@ -91,8 +91,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gotuna/gotuna v0.6.0 h1:N1lQKXEi/lwRp8u3sccTYLhzOffA4QasExz/1M5Riws= github.com/gotuna/gotuna v0.6.0/go.mod h1:F/ecRt29ChB6Ycy1AFIBpBiMNK0j7Heq+gFbLWquhjc= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= From eab86e82f0bf6d01c583fb96eef5e323a54ecaa0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:19:37 +0100 Subject: [PATCH 58/93] chore(deps): bump github.com/gorilla/mux from 1.8.0 to 1.8.1 (#1334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [github.com/gorilla/mux](https://github.com/gorilla/mux) from 1.8.0 to 1.8.1.
Release notes

Sourced from github.com/gorilla/mux's releases.

Release v1.8.1

What's Changed

New Contributors

Full Changelog: https://github.com/gorilla/mux/compare/v1.8.0...v1.8.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/gorilla/mux&package-manager=go_modules&previous-version=1.8.0&new-version=1.8.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 0a120db3e96..772ada62749 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/golang/protobuf v1.5.3 github.com/google/gofuzz v1.2.0 - github.com/gorilla/mux v1.8.0 + github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.1 github.com/gotuna/gotuna v0.6.0 github.com/jaekwon/testify v1.6.1 diff --git a/go.sum b/go.sum index 009a75c6a46..94b02ca7a7c 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,9 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= From fa7b4c2b53264173d1dc19c832525925cb6c3f1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:36:31 +0530 Subject: [PATCH 59/93] chore(deps): bump golang.org/x/mod from 0.13.0 to 0.14.0 (#1336) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.13.0 to 0.14.0.
Commits
  • 6e58e47 modfile: improve directory path detection and error text consistency
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/mod&package-manager=go_modules&previous-version=0.13.0&new-version=0.14.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Hariom Verma --- gnovm/pkg/gnomod/read.go | 2 +- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gnovm/pkg/gnomod/read.go b/gnovm/pkg/gnomod/read.go index 9bbed3c4651..e279d66344d 100644 --- a/gnovm/pkg/gnomod/read.go +++ b/gnovm/pkg/gnomod/read.go @@ -774,7 +774,7 @@ func parseReplace(filename string, line *modfile.Line, verb string, args []strin if strings.Contains(ns, "@") { return nil, errorf("replacement module must match format 'path version', not 'path@version'") } - return nil, errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)") + return nil, errorf("replacement module without version must be directory path (rooted or starting with . or ..)") } if filepath.Separator == '/' && strings.Contains(ns, `\`) { return nil, errorf("replacement directory appears to be Windows path (on a non-windows system)") diff --git a/go.mod b/go.mod index 772ada62749..b627bea1ce1 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( go.etcd.io/bbolt v1.3.7 go.uber.org/multierr v1.9.0 golang.org/x/crypto v0.14.0 - golang.org/x/mod v0.13.0 + golang.org/x/mod v0.14.0 golang.org/x/net v0.17.0 golang.org/x/term v0.13.0 golang.org/x/tools v0.13.0 diff --git a/go.sum b/go.sum index 94b02ca7a7c..2bbff44d4d1 100644 --- a/go.sum +++ b/go.sum @@ -202,8 +202,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From fb06d0a2b470f7895e8acf7ceb6cdf8bc4b2953c Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:49:09 +0100 Subject: [PATCH 60/93] feat: add InMemory `gnoland` node (#1241) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- .github/workflows/gnoland.yml | 8 +- gno.land/Makefile | 4 +- gno.land/cmd/genesis/balances_add.go | 131 ++----- gno.land/cmd/genesis/balances_add_test.go | 107 +++--- gno.land/cmd/genesis/balances_export_test.go | 31 +- gno.land/cmd/genesis/balances_remove.go | 2 +- gno.land/cmd/genesis/balances_remove_test.go | 15 +- gno.land/cmd/genesis/types.go | 22 +- gno.land/cmd/genesis/verify.go | 5 +- gno.land/cmd/genesis/verify_test.go | 8 +- gno.land/cmd/gnoland/root.go | 3 +- gno.land/cmd/gnoland/start.go | 294 +++++---------- gno.land/cmd/gnoland/testdata/addpkg.txtar | 11 +- gno.land/cmd/gnoweb/main.go | 8 +- gno.land/cmd/gnoweb/main_test.go | 34 +- gno.land/pkg/gnoland/app.go | 47 ++- gno.land/pkg/gnoland/genesis.go | 126 +++++++ gno.land/pkg/gnoland/node_inmemory.go | 147 ++++++++ gno.land/pkg/gnoland/types.go | 65 +++- gno.land/pkg/gnoland/types_test.go | 98 +++++ gno.land/pkg/integration/gnoland.go | 334 ------------------ gno.land/pkg/integration/testing.go | 39 ++ .../pkg/integration/testing_integration.go | 174 ++++----- gno.land/pkg/integration/testing_node.go | 184 ++++++++++ 24 files changed, 1010 insertions(+), 887 deletions(-) create mode 100644 gno.land/pkg/gnoland/genesis.go create mode 100644 gno.land/pkg/gnoland/node_inmemory.go create mode 100644 gno.land/pkg/gnoland/types_test.go delete mode 100644 gno.land/pkg/integration/gnoland.go create mode 100644 gno.land/pkg/integration/testing.go create mode 100644 gno.land/pkg/integration/testing_node.go diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 95cb5fa8ce0..d305bed2dcd 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -60,9 +60,7 @@ jobs: - _test.gnoland - _test.gnokey - _test.pkgs - # XXX: test broken, should be rewritten to run an inmemory localnode - # Re-add to makefile when fixed. Tracked here: https://github.com/gnolang/gno/issues/1222 - #- _test.gnoweb + - _test.gnoweb runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -78,7 +76,7 @@ jobs: export LOG_DIR="${{ runner.temp }}/logs/test-${{ matrix.goversion }}-gnoland" make ${{ matrix.args }} - name: Upload Test Log - if: always() + if: always() uses: actions/upload-artifact@v3 with: name: logs-test-gnoland-go${{ matrix.goversion }} @@ -101,7 +99,7 @@ jobs: uses: codecov/codecov-action@v3 with: directory: ${{ runner.temp }}/coverage - token: ${{ secrets.CODECOV_TOKEN }} + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} docker-integration: diff --git a/gno.land/Makefile b/gno.land/Makefile index 22b9ec24650..29c192e9987 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -48,9 +48,7 @@ fmt: ######################################## # Test suite .PHONY: test -test: _test.gnoland _test.gnokey _test.pkgs -# XXX: _test.gnoweb is currently disabled. If fixed, re-enable here and in CI. -# https://github.com/gnolang/gno/issues/1222 +test: _test.gnoland _test.gnoweb _test.gnokey _test.pkgs GOTEST_FLAGS ?= -v -p 1 -timeout=30m diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go index 276e48690a8..8df193c770c 100644 --- a/gno.land/cmd/genesis/balances_add.go +++ b/gno.land/cmd/genesis/balances_add.go @@ -8,32 +8,22 @@ import ( "fmt" "io" "os" - "regexp" - "strconv" "strings" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/sdk/bank" "github.com/gnolang/gno/tm2/pkg/std" _ "github.com/gnolang/gno/gno.land/pkg/sdk/vm" ) -var ( - balanceRegex = regexp.MustCompile(`^(\w+)=(\d+)ugnot$`) - amountRegex = regexp.MustCompile(`^(\d+)ugnot$`) -) - var ( errNoBalanceSource = errors.New("at least one balance source must be set") errBalanceParsingAborted = errors.New("balance parsing aborted") - errInvalidBalanceFormat = errors.New("invalid balance format encountered") errInvalidAddress = errors.New("invalid address encountered") - errInvalidAmount = errors.New("invalid amount encountered") ) type balancesAddCfg struct { @@ -152,7 +142,7 @@ func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io *commands.IO) // Construct the initial genesis balance sheet state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := extractGenesisBalances(state) + genesisBalances, err := mapGenesisBalancesFromState(state) if err != nil { return err } @@ -190,12 +180,11 @@ func getBalancesFromEntries(entries []string) (accountBalances, error) { balances := make(accountBalances) for _, entry := range entries { - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { - return nil, fmt.Errorf("unable to extract balance data, %w", err) + var balance gnoland.Balance + if err := balance.Parse(entry); err != nil { + return nil, fmt.Errorf("unable to parse balance entry: %w", err) } - - balances[accountBalance.address] = accountBalance.amount + balances[balance.Address] = balance } return balances, nil @@ -220,12 +209,12 @@ func getBalancesFromSheet(sheet io.Reader) (accountBalances, error) { continue } - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { + var balance gnoland.Balance + if err := balance.Parse(entry); err != nil { return nil, fmt.Errorf("unable to extract balance data, %w", err) } - balances[accountBalance.address] = accountBalance.amount + balances[balance.Address] = balance } if err := scanner.Err(); err != nil { @@ -262,21 +251,19 @@ func getBalancesFromTransactions( if err := amino.UnmarshalJSON(line, &tx); err != nil { io.ErrPrintfln( - "invalid amino JSON encountered: %s", + "invalid amino JSON encountered: %q", string(line), ) continue } - feeAmount, err := getAmountFromEntry(tx.Fee.GasFee.String()) - if err != nil { + feeAmount := std.NewCoins(tx.Fee.GasFee) + if feeAmount.AmountOf("ugnot") <= 0 { io.ErrPrintfln( - "invalid gas fee amount encountered: %s", + "invalid gas fee amount encountered: %q", tx.Fee.GasFee.String(), ) - - continue } for _, msg := range tx.Msgs { @@ -286,13 +273,12 @@ func getBalancesFromTransactions( msgSend := msg.(bank.MsgSend) - sendAmount, err := getAmountFromEntry(msgSend.Amount.String()) - if err != nil { + sendAmount := msgSend.Amount + if sendAmount.AmountOf("ugnot") <= 0 { io.ErrPrintfln( "invalid send amount encountered: %s", msgSend.Amount.String(), ) - continue } @@ -304,27 +290,35 @@ func getBalancesFromTransactions( // causes an accounts balance to go < 0. In these cases, // we initialize the account (it is present in the balance sheet), but // with the balance of 0 - from := balances[msgSend.FromAddress] - to := balances[msgSend.ToAddress] - to += sendAmount + from := balances[msgSend.FromAddress].Amount + to := balances[msgSend.ToAddress].Amount + + to = to.Add(sendAmount) - if from < sendAmount || from < feeAmount { + if from.IsAllLT(sendAmount) || from.IsAllLT(feeAmount) { // Account cannot cover send amount / fee // (see message above) - from = 0 + from = std.NewCoins(std.NewCoin("ugnot", 0)) } - if from > sendAmount { - from -= sendAmount + if from.IsAllGT(sendAmount) { + from = from.Sub(sendAmount) } - if from > feeAmount { - from -= feeAmount + if from.IsAllGT(feeAmount) { + from = from.Sub(feeAmount) } - balances[msgSend.FromAddress] = from - balances[msgSend.ToAddress] = to + // Set new balance + balances[msgSend.FromAddress] = gnoland.Balance{ + Address: msgSend.FromAddress, + Amount: from, + } + balances[msgSend.ToAddress] = gnoland.Balance{ + Address: msgSend.ToAddress, + Amount: to, + } } } } @@ -340,65 +334,14 @@ func getBalancesFromTransactions( return balances, nil } -// getAmountFromEntry -func getAmountFromEntry(entry string) (int64, error) { - matches := amountRegex.FindStringSubmatch(entry) - - // Check if there is a match - if len(matches) != 2 { - return 0, fmt.Errorf( - "invalid amount, %s", - entry, - ) - } - - amount, err := strconv.ParseInt(matches[1], 10, 64) - if err != nil { - return 0, fmt.Errorf("invalid amount, %s", matches[1]) - } - - return amount, nil -} - -// getBalanceFromEntry extracts the account balance information -// from a single line in the form of:
=ugnot -func getBalanceFromEntry(entry string) (*accountBalance, error) { - matches := balanceRegex.FindStringSubmatch(entry) - if len(matches) != 3 { - return nil, fmt.Errorf("%w, %s", errInvalidBalanceFormat, entry) - } - - // Validate the address - address, err := crypto.AddressFromString(matches[1]) - if err != nil { - return nil, fmt.Errorf("%w, %w", errInvalidAddress, err) - } - - // Validate the amount - amount, err := strconv.ParseInt(matches[2], 10, 64) - if err != nil { - return nil, fmt.Errorf("%w, %w", errInvalidAmount, err) - } - - return &accountBalance{ - address: address, - amount: amount, - }, nil -} - -// extractGenesisBalances extracts the initial account balances from the +// mapGenesisBalancesFromState extracts the initial account balances from the // genesis app state -func extractGenesisBalances(state gnoland.GnoGenesisState) (accountBalances, error) { +func mapGenesisBalancesFromState(state gnoland.GnoGenesisState) (accountBalances, error) { // Construct the initial genesis balance sheet genesisBalances := make(accountBalances) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - if err != nil { - return nil, fmt.Errorf("invalid genesis balance entry, %w", err) - } - - genesisBalances[accountBalance.address] = accountBalance.amount + for _, balance := range state.Balances { + genesisBalances[balance.Address] = balance } return genesisBalances, nil diff --git a/gno.land/cmd/genesis/balances_add_test.go b/gno.land/cmd/genesis/balances_add_test.go index f986ee85274..73e2fe148a2 100644 --- a/gno.land/cmd/genesis/balances_add_test.go +++ b/gno.land/cmd/genesis/balances_add_test.go @@ -98,7 +98,7 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") @@ -107,7 +107,7 @@ func TestGenesis_Balances_Add(t *testing.T) { fmt.Sprintf( "%s=%dugnot", dummyKey.Address().String(), - amount, + amount.AmountOf("ugnot"), ), ) } @@ -127,16 +127,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -144,7 +141,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -159,7 +156,7 @@ func TestGenesis_Balances_Add(t *testing.T) { require.NoError(t, genesis.SaveAs(tempGenesis.Name())) dummyKeys := getDummyKeys(t, 10) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -170,7 +167,7 @@ func TestGenesis_Balances_Add(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -207,16 +204,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -224,7 +218,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -240,8 +234,8 @@ func TestGenesis_Balances_Add(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amount = std.NewCoins(std.NewCoin("ugnot", 10)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("ugnot", 1000000) txs = make([]std.Tx, 0) ) @@ -309,10 +303,7 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false @@ -321,11 +312,11 @@ func TestGenesis_Balances_Add(t *testing.T) { if index == 0 { // the first address should // have a balance of 0 - checkAmount = 0 + checkAmount = std.NewCoins(std.NewCoin("ugnot", 0)) } - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, checkAmount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.True(t, balance.Amount.IsEqual(checkAmount)) found = true break @@ -333,7 +324,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -349,12 +340,11 @@ func TestGenesis_Balances_Add(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ // Set an initial balance value - Balances: []string{ - fmt.Sprintf( - "%s=%dugnot", - dummyKeys[0].Address().String(), - 100, - ), + Balances: []gnoland.Balance{ + { + Address: dummyKeys[0].Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + }, }, } genesis.AppState = state @@ -369,7 +359,7 @@ func TestGenesis_Balances_Add(t *testing.T) { tempGenesis.Name(), } - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) for _, dummyKey := range dummyKeys { args = append(args, "--single") @@ -378,7 +368,7 @@ func TestGenesis_Balances_Add(t *testing.T) { fmt.Sprintf( "%s=%dugnot", dummyKey.Address().String(), - amount, + amount.AmountOf("ugnot"), ), ) } @@ -398,16 +388,13 @@ func TestGenesis_Balances_Add(t *testing.T) { require.Equal(t, len(dummyKeys), len(state.Balances)) - for _, entry := range state.Balances { - accountBalance, err := getBalanceFromEntry(entry) - require.NoError(t, err) - + for _, balance := range state.Balances { // Find the appropriate key // (the genesis is saved with randomized balance order) found := false for _, dummyKey := range dummyKeys { - if dummyKey.Address().String() == accountBalance.address.String() { - assert.Equal(t, amount, accountBalance.amount) + if dummyKey.Address().String() == balance.Address.String() { + assert.Equal(t, amount, balance.Amount) found = true break @@ -415,7 +402,7 @@ func TestGenesis_Balances_Add(t *testing.T) { } if !found { - t.Fatalf("unexpected entry with address %s found", accountBalance.address.String()) + t.Fatalf("unexpected entry with address %s found", balance.Address.String()) } } }) @@ -429,7 +416,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -437,7 +424,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -447,7 +434,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } }) @@ -461,7 +448,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidBalanceFormat.Error()) + assert.ErrorContains(t, err, "malformed entry") }) t.Run("malformed balance, invalid address", func(t *testing.T) { @@ -474,7 +461,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAddress.Error()) + assert.ErrorContains(t, err, "invalid address") }) t.Run("malformed balance, invalid amount", func(t *testing.T) { @@ -493,7 +480,7 @@ func TestBalances_GetBalancesFromEntries(t *testing.T) { balanceMap, err := getBalancesFromEntries(balances) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAmount.Error()) + assert.ErrorContains(t, err, "invalid amount") }) } @@ -505,7 +492,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Generate dummy keys dummyKeys := getDummyKeys(t, 2) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) balances := make([]string, len(dummyKeys)) @@ -513,7 +500,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balances[index] = fmt.Sprintf( "%s=%dugnot", key.Address().String(), - amount, + amount.AmountOf("ugnot"), ) } @@ -524,7 +511,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } }) @@ -546,7 +533,7 @@ func TestBalances_GetBalancesFromSheet(t *testing.T) { balanceMap, err := getBalancesFromSheet(reader) assert.Nil(t, balanceMap) - assert.ErrorContains(t, err, errInvalidAmount.Error()) + assert.ErrorContains(t, err, "invalid amount") }) } @@ -558,8 +545,8 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amount = std.NewCoins(std.NewCoin("ugnot", 10)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("ugnot", 1000000) txs = make([]std.Tx, 0) ) @@ -605,10 +592,10 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { // Validate the balance map assert.Len(t, balanceMap, len(dummyKeys)) for _, key := range dummyKeys[1:] { - assert.Equal(t, amount, balanceMap[key.Address()]) + assert.Equal(t, amount, balanceMap[key.Address()].Amount) } - assert.Equal(t, int64(0), balanceMap[sender.Address()]) + assert.Equal(t, std.Coins{}, balanceMap[sender.Address()].Amount) }) t.Run("malformed transaction, invalid fee amount", func(t *testing.T) { @@ -616,8 +603,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("ugnot", amount)) + amountCoins = std.NewCoins(std.NewCoin("ugnot", 10)) gasFee = std.NewCoin("gnos", 1) // invalid fee txs = make([]std.Tx, 0) ) @@ -669,8 +655,7 @@ func TestBalances_GetBalancesFromTransactions(t *testing.T) { var ( dummyKeys = getDummyKeys(t, 10) - amount = int64(10) - amountCoins = std.NewCoins(std.NewCoin("gnogno", amount)) // invalid send amount + amountCoins = std.NewCoins(std.NewCoin("gnogno", 10)) // invalid send amount gasFee = std.NewCoin("ugnot", 1) txs = make([]std.Tx, 0) ) diff --git a/gno.land/cmd/genesis/balances_export_test.go b/gno.land/cmd/genesis/balances_export_test.go index 33e4f7bc800..d7441fd438f 100644 --- a/gno.land/cmd/genesis/balances_export_test.go +++ b/gno.land/cmd/genesis/balances_export_test.go @@ -3,31 +3,30 @@ package main import ( "bufio" "context" - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// getDummyBalanceLines generates dummy balance lines -func getDummyBalanceLines(t *testing.T, count int) []string { +// getDummyBalances generates dummy balance lines +func getDummyBalances(t *testing.T, count int) []gnoland.Balance { t.Helper() dummyKeys := getDummyKeys(t, count) - amount := int64(10) + amount := std.NewCoins(std.NewCoin("ugnot", 10)) - balances := make([]string, len(dummyKeys)) + balances := make([]gnoland.Balance, len(dummyKeys)) for index, key := range dummyKeys { - balances[index] = fmt.Sprintf( - "%s=%dugnot", - key.Address().String(), - amount, - ) + balances[index] = gnoland.Balance{ + Address: key.Address(), + Amount: amount, + } } return balances @@ -85,7 +84,7 @@ func TestGenesis_Balances_Export(t *testing.T) { genesis := getDefaultGenesis() genesis.AppState = gnoland.GnoGenesisState{ - Balances: getDummyBalanceLines(t, 1), + Balances: getDummyBalances(t, 1), } require.NoError(t, genesis.SaveAs(tempGenesis.Name())) @@ -107,7 +106,7 @@ func TestGenesis_Balances_Export(t *testing.T) { t.Parallel() // Generate dummy balances - balances := getDummyBalanceLines(t, 10) + balances := getDummyBalances(t, 10) tempGenesis, cleanup := testutils.NewTestFile(t) t.Cleanup(cleanup) @@ -139,9 +138,13 @@ func TestGenesis_Balances_Export(t *testing.T) { // Validate the transactions were written down scanner := bufio.NewScanner(outputFile) - outputBalances := make([]string, 0) + outputBalances := make([]gnoland.Balance, 0) for scanner.Scan() { - outputBalances = append(outputBalances, scanner.Text()) + var balance gnoland.Balance + err := balance.Parse(scanner.Text()) + require.NoError(t, err) + + outputBalances = append(outputBalances, balance) } require.NoError(t, scanner.Err()) diff --git a/gno.land/cmd/genesis/balances_remove.go b/gno.land/cmd/genesis/balances_remove.go index f7e9092dc3b..f4286d95ad2 100644 --- a/gno.land/cmd/genesis/balances_remove.go +++ b/gno.land/cmd/genesis/balances_remove.go @@ -71,7 +71,7 @@ func execBalancesRemove(cfg *balancesRemoveCfg, io *commands.IO) error { // Construct the initial genesis balance sheet state := genesis.AppState.(gnoland.GnoGenesisState) - genesisBalances, err := extractGenesisBalances(state) + genesisBalances, err := mapGenesisBalancesFromState(state) if err != nil { return err } diff --git a/gno.land/cmd/genesis/balances_remove_test.go b/gno.land/cmd/genesis/balances_remove_test.go index 29179c43604..b9d10d0db08 100644 --- a/gno.land/cmd/genesis/balances_remove_test.go +++ b/gno.land/cmd/genesis/balances_remove_test.go @@ -2,12 +2,12 @@ package main import ( "context" - "fmt" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/std" "github.com/gnolang/gno/tm2/pkg/testutils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -70,12 +70,11 @@ func TestGenesis_Balances_Remove(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ // Set an initial balance value - Balances: []string{ - fmt.Sprintf( - "%s=%dugnot", - dummyKey.Address().String(), - 100, - ), + Balances: []gnoland.Balance{ + { + Address: dummyKey.Address(), + Amount: std.NewCoins(std.NewCoin("ugnot", 100)), + }, }, } genesis.AppState = state @@ -118,7 +117,7 @@ func TestGenesis_Balances_Remove(t *testing.T) { genesis := getDefaultGenesis() state := gnoland.GnoGenesisState{ - Balances: []string{}, // Empty initial balance + Balances: []gnoland.Balance{}, // Empty initial balance } genesis.AppState = state require.NoError(t, genesis.SaveAs(tempGenesis.Name())) diff --git a/gno.land/cmd/genesis/types.go b/gno.land/cmd/genesis/types.go index 208eaddb6da..dba39ea8ec1 100644 --- a/gno.land/cmd/genesis/types.go +++ b/gno.land/cmd/genesis/types.go @@ -1,8 +1,7 @@ package main import ( - "fmt" - + "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/std" ) @@ -39,23 +38,14 @@ func (i *txStore) leftMerge(b txStore) error { return nil } -type ( - accountBalances map[types.Address]int64 // address -> balance (ugnot) - accountBalance struct { - address types.Address - amount int64 - } -) +type accountBalances map[types.Address]gnoland.Balance // address -> balance (ugnot) // toList linearizes the account balances map -func (a accountBalances) toList() []string { - balances := make([]string, 0, len(a)) +func (a accountBalances) toList() []gnoland.Balance { + balances := make([]gnoland.Balance, 0, len(a)) - for address, balance := range a { - balances = append( - balances, - fmt.Sprintf("%s=%dugnot", address, balance), - ) + for _, balance := range a { + balances = append(balances, balance) } return balances diff --git a/gno.land/cmd/genesis/verify.go b/gno.land/cmd/genesis/verify.go index ba51f5801f6..6c877ca51ec 100644 --- a/gno.land/cmd/genesis/verify.go +++ b/gno.land/cmd/genesis/verify.go @@ -9,7 +9,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" - "github.com/gnolang/gno/tm2/pkg/std" ) var errInvalidGenesisState = errors.New("invalid genesis state type") @@ -68,8 +67,8 @@ func execVerify(cfg *verifyCfg, io *commands.IO) error { // Validate the initial balances for _, balance := range state.Balances { - if _, parseErr := std.ParseCoins(balance); parseErr != nil { - return fmt.Errorf("invalid balance %s, %w", balance, parseErr) + if err := balance.Verify(); err != nil { + return fmt.Errorf("invalid balance: %w", err) } } } diff --git a/gno.land/cmd/genesis/verify_test.go b/gno.land/cmd/genesis/verify_test.go index fcc5305b9d0..8388949898b 100644 --- a/gno.land/cmd/genesis/verify_test.go +++ b/gno.land/cmd/genesis/verify_test.go @@ -44,7 +44,7 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{}, + Balances: []gnoland.Balance{}, Txs: []std.Tx{ {}, }, @@ -74,8 +74,8 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{ - "dummybalance", + Balances: []gnoland.Balance{ + {}, }, Txs: []std.Tx{}, } @@ -103,7 +103,7 @@ func TestGenesis_Verify(t *testing.T) { g := getValidTestGenesis() g.AppState = gnoland.GnoGenesisState{ - Balances: []string{}, + Balances: []gnoland.Balance{}, Txs: []std.Tx{}, } diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index cf2a6252478..5b2cbe0e4fe 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -11,8 +11,7 @@ import ( ) func main() { - io := commands.NewDefaultIO() - cmd := newRootCmd(io) + cmd := newRootCmd(commands.NewDefaultIO()) if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index a42e1df1bf0..618f4f87a09 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -1,21 +1,15 @@ package main import ( - "bufio" "context" "errors" "flag" "fmt" - "os" "path/filepath" "strings" "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" - 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/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" "github.com/gnolang/gno/tm2/pkg/bft/config" "github.com/gnolang/gno/tm2/pkg/bft/node" @@ -32,13 +26,14 @@ import ( ) type startCfg struct { + gnoRootDir string skipFailingGenesisTxs bool skipStart bool genesisBalancesFile string genesisTxsFile string chainID string genesisRemote string - rootDir string + dataDir string genesisMaxVMCycles int64 config string @@ -64,6 +59,10 @@ func newStartCmd(io *commands.IO) *commands.Command { } func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { + gnoroot := gnoland.MustGuessGnoRootDir() + defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") + defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.txt") + fs.BoolVar( &c.skipFailingGenesisTxs, "skip-failing-genesis-txs", @@ -81,14 +80,14 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.genesisBalancesFile, "genesis-balances-file", - "./genesis/genesis_balances.txt", + defaultGenesisBalancesFile, "initial distribution file", ) fs.StringVar( &c.genesisTxsFile, "genesis-txs-file", - "./genesis/genesis_txs.txt", + defaultGenesisTxsFile, "initial txs to replay", ) @@ -100,8 +99,16 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { ) fs.StringVar( - &c.rootDir, - "root-dir", + &c.gnoRootDir, + "gnoroot-dir", + gnoroot, + "the root directory of the gno repository", + ) + + // XXX: Use home directory for this + fs.StringVar( + &c.dataDir, + "data-dir", "testdir", "directory for config and data", ) @@ -156,11 +163,19 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { "", fmt.Sprintf("path for the file tx event store (required if event store is '%s')", file.EventStoreType), ) + + // XXX(deprecated): use data-dir instead + fs.StringVar( + &c.dataDir, + "root-dir", + "testdir", + "deprecated: use data-dir instead - directory for config and data", + ) } func execStart(c *startCfg, io *commands.IO) error { logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) - rootDir := c.rootDir + dataDir := c.dataDir var ( cfg *config.Config @@ -174,39 +189,28 @@ func execStart(c *startCfg, io *commands.IO) error { cfg, loadCfgErr = config.LoadConfigFile(c.nodeConfigPath) } else { // Load the default node configuration - cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(rootDir, nil) + cfg, loadCfgErr = config.LoadOrMakeConfigWithOptions(dataDir, nil) } if loadCfgErr != nil { return fmt.Errorf("unable to load node configuration, %w", loadCfgErr) } - // create priv validator first. - // need it to generate genesis.json - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - - // write genesis file if missing. - genesisFilePath := filepath.Join(rootDir, cfg.Genesis) - - genesisTxs, genesisTxsErr := loadGenesisTxs(c.genesisTxsFile, c.chainID, c.genesisRemote) - if genesisTxsErr != nil { - return fmt.Errorf("unable to load genesis txs, %w", genesisTxsErr) - } + // Write genesis file if missing. + genesisFilePath := filepath.Join(dataDir, cfg.Genesis) if !osm.FileExists(genesisFilePath) { - genDoc, err := makeGenesisDoc( - priv.GetPubKey(), - c.chainID, - c.genesisBalancesFile, - genesisTxs, - ) - if err != nil { - return fmt.Errorf("unable to generate genesis.json, %w", err) + // Create priv validator first. + // Need it to generate genesis.json + newPrivValKey := cfg.PrivValidatorKeyFile() + newPrivValState := cfg.PrivValidatorStateFile() + priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) + pk := priv.GetPubKey() + + // Generate genesis.json file + if err := generateGenesisFile(genesisFilePath, pk, c); err != nil { + return fmt.Errorf("unable to generate genesis file: %w", err) } - - writeGenesisFile(genDoc, genesisFilePath) } // Initialize the indexer config @@ -214,15 +218,13 @@ func execStart(c *startCfg, io *commands.IO) error { if err != nil { return fmt.Errorf("unable to parse indexer config, %w", err) } - cfg.TxEventStore = txEventStoreCfg - // create application and node. - gnoApp, err := gnoland.NewApp(rootDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) + // Create application and node. + gnoApp, err := gnoland.NewApp(dataDir, c.skipFailingGenesisTxs, logger, c.genesisMaxVMCycles) if err != nil { return fmt.Errorf("error in creating new app: %w", err) } - cfg.LocalApp = gnoApp gnoNode, err := node.DefaultNewNode(cfg, logger) @@ -233,8 +235,7 @@ func execStart(c *startCfg, io *commands.IO) error { fmt.Fprintln(io.Err, "Node created.") if c.skipStart { - fmt.Fprintln(io.Err, "'--skip-start' is set. Exiting.") - + io.ErrPrintln("'--skip-start' is set. Exiting.") return nil } @@ -242,215 +243,96 @@ func execStart(c *startCfg, io *commands.IO) error { return fmt.Errorf("error in start node: %w", err) } - // run forever osm.TrapSignal(func() { if gnoNode.IsRunning() { _ = gnoNode.Stop() } }) - select {} // run forever + // Run forever + select {} } -// getTxEventStoreConfig constructs an event store config from provided user options -func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { - var cfg *eventstorecfg.Config - - switch c.txEventStoreType { - case file.EventStoreType: - if c.txEventStorePath == "" { - return nil, errors.New("unspecified file transaction indexer path") - } - - // Fill out the configuration - cfg = &eventstorecfg.Config{ - EventStoreType: file.EventStoreType, - Params: map[string]any{ - file.Path: c.txEventStorePath, - }, - } - default: - cfg = eventstorecfg.DefaultEventStoreConfig() - } - - return cfg, nil -} - -// Makes a local test genesis doc with local privValidator. -func makeGenesisDoc( - pvPub crypto.PubKey, - chainID string, - genesisBalancesFile string, - genesisTxs []std.Tx, -) (*bft.GenesisDoc, error) { +func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error { gen := &bft.GenesisDoc{} - gen.GenesisTime = time.Now() - gen.ChainID = chainID + gen.ChainID = c.chainID gen.ConsensusParams = abci.ConsensusParams{ Block: &abci.BlockParams{ // TODO: update limits. - MaxTxBytes: 1000000, // 1MB, - MaxDataBytes: 2000000, // 2MB, - MaxGas: 10000000, // 10M gas - TimeIotaMS: 100, // 100ms + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_00, // 10M gas + TimeIotaMS: 100, // 100ms }, } + gen.Validators = []bft.GenesisValidator{ { - Address: pvPub.Address(), - PubKey: pvPub, + Address: pk.Address(), + PubKey: pk, Power: 10, Name: "testvalidator", }, } - // Load distribution. - balances, err := loadGenesisBalances(genesisBalancesFile) + // Load balances files + balances, err := gnoland.LoadGenesisBalancesFile(c.genesisBalancesFile) if err != nil { - return nil, fmt.Errorf("unable to load genesis balances, %w", err) + return fmt.Errorf("unable to load genesis balances file %q: %w", c.genesisBalancesFile, err) } - // Load initial packages from examples. + // Load examples folder + examplesDir := filepath.Join(c.gnoRootDir, "examples") test1 := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") - txs := []std.Tx{} - - // List initial packages to load from examples. - pkgs, err := gnomod.ListPkgs(filepath.Join("..", "examples")) + defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, test1, defaultFee, nil) if err != nil { - panic(fmt.Errorf("listing gno packages: %w", err)) + return fmt.Errorf("unable to load examples folder: %w", err) } - // Sort packages by dependencies. - sortedPkgs, err := pkgs.Sort() + // Load Genesis TXs + genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote) if err != nil { - panic(fmt.Errorf("sorting packages: %w", err)) + return fmt.Errorf("unable to load genesis txs file: %w", err) } - // Filter out draft packages. - nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() - - for _, pkg := range nonDraftPkgs { - // open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - - var tx std.Tx - tx.Msgs = []std.Msg{ - vmm.MsgAddPackage{ - Creator: test1, - Package: memPkg, - Deposit: nil, - }, - } - tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) - } + genesisTxs = append(pkgsTxs, genesisTxs...) - // load genesis txs from file. - txs = append(txs, genesisTxs...) - - // construct genesis AppState. + // Construct genesis AppState. gen.AppState = gnoland.GnoGenesisState{ Balances: balances, - Txs: txs, - } - return gen, nil -} - -func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { - err := gen.SaveAs(filePath) - if err != nil { - panic(err) - } -} - -func loadGenesisTxs( - path string, - chainID string, - genesisRemote string, -) ([]std.Tx, error) { - txs := make([]std.Tx, 0) - - if !osm.FileExists(path) { - // No initial transactions - return txs, nil - } - - txsFile, openErr := os.Open(path) - if openErr != nil { - return nil, fmt.Errorf("unable to open genesis txs file, %w", openErr) - } - - scanner := bufio.NewScanner(txsFile) - - for scanner.Scan() { - txLine := scanner.Text() - - if txLine == "" { - continue // skip empty line - } - - // patch the TX - txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) - txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) - - var tx std.Tx - - if unmarshalErr := amino.UnmarshalJSON([]byte(txLine), &tx); unmarshalErr != nil { - return nil, fmt.Errorf("unable to amino unmarshal tx, %w", unmarshalErr) - } - - txs = append(txs, tx) + Txs: genesisTxs, } - if scanErr := scanner.Err(); scanErr != nil { - return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + // Write genesis state + if err := gen.SaveAs(genesisFile); err != nil { + return fmt.Errorf("unable to write genesis file %q: %w", genesisFile, err) } - return txs, nil + return nil } -func loadGenesisBalances(path string) ([]string, error) { - // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot - balances := make([]string, 0) - - if !osm.FileExists(path) { - // No initial balances - return balances, nil - } - - balancesFile, openErr := os.Open(path) - if openErr != nil { - return nil, fmt.Errorf("unable to open genesis balances file, %w", openErr) - } - - scanner := bufio.NewScanner(balancesFile) - - for scanner.Scan() { - line := scanner.Text() - - line = strings.TrimSpace(line) - - // remove comments. - line = strings.Split(line, "#")[0] - line = strings.TrimSpace(line) +// getTxEventStoreConfig constructs an event store config from provided user options +func getTxEventStoreConfig(c *startCfg) (*eventstorecfg.Config, error) { + var cfg *eventstorecfg.Config - // skip empty lines. - if line == "" { - continue + switch c.txEventStoreType { + case file.EventStoreType: + if c.txEventStorePath == "" { + return nil, errors.New("unspecified file transaction indexer path") } - if len(strings.Split(line, "=")) != 2 { - return nil, fmt.Errorf("invalid genesis_balance line: %s", line) + // Fill out the configuration + cfg = &eventstorecfg.Config{ + EventStoreType: file.EventStoreType, + Params: map[string]any{ + file.Path: c.txEventStorePath, + }, } - - balances = append(balances, line) - } - - if scanErr := scanner.Err(); scanErr != nil { - return nil, fmt.Errorf("error encountered while scanning, %w", scanErr) + default: + cfg = eventstorecfg.DefaultEventStoreConfig() } - return balances, nil + return cfg, nil } diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar index 5e871b058ac..5f1ee0caf49 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -10,7 +10,11 @@ gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 10000 gnokey maketx call -pkgpath gno.land/r/foobar/bar -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 ## compare render -cmp stdout stdout.golden +stdout '("hello from foo" string)' +stdout 'OK!' +stdout 'GAS WANTED: 2000000' +stdout 'GAS USED: [0-9]+' + -- bar.gno -- package bar @@ -19,8 +23,3 @@ func Render(path string) string { return "hello from foo" } --- stdout.golden -- -("hello from foo" string) -OK! -GAS WANTED: 2000000 -GAS USED: 69163 \ No newline at end of file diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 0d9398cb8e2..b080e0b403d 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -486,11 +486,11 @@ func writeError(w http.ResponseWriter, err error) { // XXX: writeError should return an error page template. w.WriteHeader(500) - details := errors.Unwrap(err).Error() - main := err.Error() + fmt.Println("main", err.Error()) - fmt.Println("main", main) - fmt.Println("details", details) + if details := errors.Unwrap(err); details != nil { + fmt.Println("details", details.Error()) + } w.Write([]byte(err.Error())) } diff --git a/gno.land/cmd/gnoweb/main_test.go b/gno.land/cmd/gnoweb/main_test.go index 974d3f987b7..61650563405 100644 --- a/gno.land/cmd/gnoweb/main_test.go +++ b/gno.land/cmd/gnoweb/main_test.go @@ -4,10 +4,12 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "strings" "testing" + "github.com/gnolang/gno/gno.land/pkg/gnoland" + "github.com/gnolang/gno/gno.land/pkg/integration" + "github.com/gnolang/gno/tm2/pkg/log" "github.com/gotuna/gotuna/test/assert" ) @@ -41,20 +43,17 @@ func TestRoutes(t *testing.T) { {"/blog", found, "/r/gnoland/blog"}, {"/404-not-found", notFound, "/404-not-found"}, } - if wd, err := os.Getwd(); err == nil { - if strings.HasSuffix(wd, "cmd/gnoweb") { - os.Chdir("../..") - } - } else { - panic("os.Getwd() -> err: " + err.Error()) - } - // configure default values - flags.RemoteAddr = "127.0.0.1:26657" - flags.HelpRemote = "127.0.0.1:26657" + config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) + defer node.Stop() + + // set the `remoteAddr` of the client to the listening address of the + // node, which is randomly assigned. + flags.RemoteAddr = remoteAddr flags.HelpChainID = "dev" flags.CaptchaSite = "" - flags.ViewsDir = "./cmd/gnoweb/views" + flags.ViewsDir = "../../cmd/gnoweb/views" flags.WithAnalytics = false app := makeApp() @@ -93,27 +92,34 @@ func TestAnalytics(t *testing.T) { "/404-not-found", } + config, _ := integration.TestingNodeConfig(t, gnoland.MustGuessGnoRootDir()) + node, remoteAddr := integration.TestingInMemoryNode(t, log.NewNopLogger(), config) + defer node.Stop() + + flags.ViewsDir = "../../cmd/gnoweb/views" t.Run("with", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { + flags.RemoteAddr = remoteAddr flags.WithAnalytics = true app := makeApp() request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) - assert.Contains(t, response.Body.String(), "simpleanalytics") + assert.Contains(t, response.Body.String(), "sa.gno.services") }) } }) t.Run("without", func(t *testing.T) { for _, route := range routes { t.Run(route, func(t *testing.T) { + flags.RemoteAddr = remoteAddr flags.WithAnalytics = false app := makeApp() request := httptest.NewRequest(http.MethodGet, route, nil) response := httptest.NewRecorder() app.Router.ServeHTTP(response, request) - assert.Equal(t, strings.Contains(response.Body.String(), "simpleanalytics"), false) + assert.Equal(t, strings.Contains(response.Body.String(), "sa.gno.services"), false) }) } }) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index 3585f99d7de..a8a2736c8d1 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -1,10 +1,12 @@ package gnoland import ( + "errors" "fmt" "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/gnolang/gno/gno.land/pkg/sdk/vm" @@ -36,7 +38,7 @@ func NewAppOptions() *AppOptions { return &AppOptions{ Logger: log.NewNopLogger(), DB: dbm.NewMemDB(), - GnoRootDir: GuessGnoRootDir(), + GnoRootDir: MustGuessGnoRootDir(), } } @@ -73,6 +75,8 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) { // Construct keepers. acctKpr := auth.NewAccountKeeper(mainKey, ProtoGnoAccount) bankKpr := bank.NewBankKeeper(acctKpr) + + // XXX: Embed this ? stdlibsDir := filepath.Join(cfg.GnoRootDir, "gnovm", "stdlibs") vmKpr := vm.NewVMKeeper(baseKey, mainKey, acctKpr, bankKpr, stdlibsDir, cfg.MaxCycles) @@ -142,10 +146,9 @@ func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank genState := req.AppState.(GnoGenesisState) // Parse and set genesis state balances. for _, bal := range genState.Balances { - addr, coins := parseBalance(bal) - acc := acctKpr.NewAccountWithAddress(ctx, addr) + acc := acctKpr.NewAccountWithAddress(ctx, bal.Address) acctKpr.SetAccount(ctx, acc) - err := bankKpr.SetCoins(ctx, addr, coins) + err := bankKpr.SetCoins(ctx, bal.Address, bal.Amount) if err != nil { panic(err) } @@ -195,24 +198,44 @@ func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock } } -func GuessGnoRootDir() string { - var rootdir string +// XXX: all the method bellow should be removed in favor of +// https://github.com/gnolang/gno/pull/1233 +func MustGuessGnoRootDir() string { + root, err := GuessGnoRootDir() + if err != nil { + panic(err) + } + + return root +} +func GuessGnoRootDir() (string, error) { // First try to get the root directory from the GNOROOT environment variable. - if rootdir = os.Getenv("GNOROOT"); rootdir != "" { - return filepath.Clean(rootdir) + if rootdir := os.Getenv("GNOROOT"); rootdir != "" { + return filepath.Clean(rootdir), nil } + // Try to guess GNOROOT using the nearest go.mod. if gobin, err := exec.LookPath("go"); err == nil { // If GNOROOT is not set, try to guess the root directory using the `go list` command. cmd := exec.Command(gobin, "list", "-m", "-mod=mod", "-f", "{{.Dir}}", "github.com/gnolang/gno") out, err := cmd.CombinedOutput() - if err != nil { - panic(fmt.Errorf("invalid gno directory %q: %w", rootdir, err)) + if err == nil { + return strings.TrimSpace(string(out)), nil } + } - return strings.TrimSpace(string(out)) + // Try to guess GNOROOT using caller stack. + if _, filename, _, ok := runtime.Caller(1); ok && filepath.IsAbs(filename) { + if currentDir := filepath.Dir(filename); currentDir != "" { + // Gno root directory relative from `app.go` path: + // gno/ .. /gno.land/ .. /pkg/ .. /gnoland/app.go + rootdir, err := filepath.Abs(filepath.Join(currentDir, "..", "..", "..")) + if err == nil { + return rootdir, nil + } + } } - panic("no go binary available, unable to determine gno root-dir path") + return "", errors.New("unable to guess gno's root-directory") } diff --git a/gno.land/pkg/gnoland/genesis.go b/gno.land/pkg/gnoland/genesis.go new file mode 100644 index 00000000000..e809103469d --- /dev/null +++ b/gno.land/pkg/gnoland/genesis.go @@ -0,0 +1,126 @@ +package gnoland + +import ( + "errors" + "fmt" + "strings" + + 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/tm2/pkg/amino" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/gnolang/gno/tm2/pkg/std" +) + +// LoadGenesisBalancesFile loads genesis balances from the provided file path. +func LoadGenesisBalancesFile(path string) ([]Balance, error) { + // each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot + content := osm.MustReadFile(path) + lines := strings.Split(string(content), "\n") + + balances := make([]Balance, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + + // remove comments. + line = strings.Split(line, "#")[0] + line = strings.TrimSpace(line) + + // skip empty lines. + if line == "" { + continue + } + + parts := strings.Split(line, "=") //
= + if len(parts) != 2 { + return nil, errors.New("invalid genesis_balance line: " + line) + } + + addr, err := crypto.AddressFromBech32(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid balance addr %s: %w", parts[0], err) + } + + coins, err := std.ParseCoins(parts[1]) + if err != nil { + return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err) + } + + balances = append(balances, Balance{ + Address: addr, + Amount: coins, + }) + } + + return balances, nil +} + +// LoadGenesisTxsFile loads genesis transactions from the provided file path. +// XXX: Improve the way we generate and load this file +func LoadGenesisTxsFile(path string, chainID string, genesisRemote string) ([]std.Tx, error) { + txs := []std.Tx{} + txsBz := osm.MustReadFile(path) + txsLines := strings.Split(string(txsBz), "\n") + for _, txLine := range txsLines { + if txLine == "" { + continue // Skip empty line. + } + + // Patch the TX. + txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) + txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) + + var tx std.Tx + if err := amino.UnmarshalJSON([]byte(txLine), &tx); err != nil { + return nil, fmt.Errorf("unable to Unmarshall txs file: %w", err) + } + + txs = append(txs, tx) + } + + return txs, nil +} + +// LoadPackagesFromDir loads gno packages from a directory. +// It creates and returns a list of transactions based on these packages. +func LoadPackagesFromDir(dir string, creator bft.Address, fee std.Fee, deposit std.Coins) ([]std.Tx, error) { + // list all packages from target path + pkgs, err := gnomod.ListPkgs(dir) + if err != nil { + return nil, fmt.Errorf("listing gno packages: %w", err) + } + + // Sort packages by dependencies. + sortedPkgs, err := pkgs.Sort() + if err != nil { + return nil, fmt.Errorf("sorting packages: %w", err) + } + + // Filter out draft packages. + nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() + txs := []std.Tx{} + for _, pkg := range nonDraftPkgs { + // Open files in directory as MemPackage. + memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) + + // Create transaction + tx := std.Tx{ + Fee: fee, + Msgs: []std.Msg{ + vmm.MsgAddPackage{ + Creator: creator, + Package: memPkg, + Deposit: deposit, + }, + }, + } + + tx.Signatures = make([]std.Signature, len(tx.GetSigners())) + txs = append(txs, tx) + } + + return txs, nil +} diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go new file mode 100644 index 00000000000..a0ab6a51e82 --- /dev/null +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -0,0 +1,147 @@ +package gnoland + +import ( + "fmt" + "time" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + "github.com/gnolang/gno/tm2/pkg/bft/proxy" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/db" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type InMemoryNodeConfig struct { + PrivValidator bft.PrivValidator // identity of the validator + Genesis *bft.GenesisDoc + TMConfig *tmcfg.Config + SkipFailingGenesisTxs bool + GenesisMaxVMCycles int64 +} + +// NewMockedPrivValidator generate a new key +func NewMockedPrivValidator() bft.PrivValidator { + return bft.NewMockPVWithParams(ed25519.GenPrivKey(), false, false) +} + +// NewInMemoryNodeConfig creates a default configuration for an in-memory node. +func NewDefaultGenesisConfig(pk crypto.PubKey, chainid string) *bft.GenesisDoc { + return &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: chainid, + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + AppState: &GnoGenesisState{ + Balances: []Balance{}, + Txs: []std.Tx{}, + }, + } +} + +func NewDefaultTMConfig(rootdir string) *tmcfg.Config { + return tmcfg.DefaultConfig().SetRootDir(rootdir) +} + +// NewInMemoryNodeConfig creates a default configuration for an in-memory node. +func NewDefaultInMemoryNodeConfig(rootdir string) *InMemoryNodeConfig { + tm := NewDefaultTMConfig(rootdir) + + // Create Mocked Identity + pv := NewMockedPrivValidator() + genesis := NewDefaultGenesisConfig(pv.GetPubKey(), tm.ChainID()) + + // Add self as validator + self := pv.GetPubKey() + genesis.Validators = []bft.GenesisValidator{ + { + Address: self.Address(), + PubKey: self, + Power: 10, + Name: "self", + }, + } + + return &InMemoryNodeConfig{ + PrivValidator: pv, + TMConfig: tm, + Genesis: genesis, + GenesisMaxVMCycles: 10_000_000, + } +} + +func (cfg *InMemoryNodeConfig) validate() error { + if cfg.PrivValidator == nil { + return fmt.Errorf("`PrivValidator` is required but not provided") + } + + if cfg.TMConfig == nil { + return fmt.Errorf("`TMConfig` is required but not provided") + } + + if cfg.TMConfig.RootDir == "" { + return fmt.Errorf("`TMConfig.RootDir` is required to locate `stdlibs` directory") + } + + return nil +} + +// NewInMemoryNode creates an in-memory gnoland node. In this mode, the node does not +// persist any data and uses an in-memory database. The `InMemoryNodeConfig.TMConfig.RootDir` +// should point to the correct gno repository to load the stdlibs. +func NewInMemoryNode(logger log.Logger, cfg *InMemoryNodeConfig) (*node.Node, error) { + if err := cfg.validate(); err != nil { + return nil, fmt.Errorf("validate config error: %w", err) + } + + // Initialize the application with the provided options + gnoApp, err := NewAppWithOptions(&AppOptions{ + Logger: logger, + GnoRootDir: cfg.TMConfig.RootDir, + SkipFailingGenesisTxs: cfg.SkipFailingGenesisTxs, + MaxCycles: cfg.GenesisMaxVMCycles, + DB: db.NewMemDB(), + }) + if err != nil { + return nil, fmt.Errorf("error initializing new app: %w", err) + } + + cfg.TMConfig.LocalApp = gnoApp + + // Setup app client creator + appClientCreator := proxy.DefaultClientCreator( + cfg.TMConfig.LocalApp, + cfg.TMConfig.ProxyApp, + cfg.TMConfig.ABCI, + cfg.TMConfig.DBDir(), + ) + + // Create genesis factory + genProvider := func() (*bft.GenesisDoc, error) { + return cfg.Genesis, nil + } + + // generate p2p node identity + // XXX: do we need to configur + nodekey := &p2p.NodeKey{PrivKey: ed25519.GenPrivKey()} + + // Create and return the in-memory node instance + return node.NewNode(cfg.TMConfig, + cfg.PrivValidator, nodekey, + appClientCreator, + genProvider, + node.DefaultDBProvider, + logger, + ) +} diff --git a/gno.land/pkg/gnoland/types.go b/gno.land/pkg/gnoland/types.go index 1c762366ae9..5d68064c9c5 100644 --- a/gno.land/pkg/gnoland/types.go +++ b/gno.land/pkg/gnoland/types.go @@ -1,9 +1,20 @@ package gnoland import ( + "errors" + "fmt" + "strings" + + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/std" ) +var ( + ErrBalanceEmptyAddress = errors.New("balance address is empty") + ErrBalanceEmptyAmount = errors.New("balance amount is empty") +) + type GnoAccount struct { std.BaseAccount } @@ -13,6 +24,56 @@ func ProtoGnoAccount() std.Account { } type GnoGenesisState struct { - Balances []string `json:"balances"` - Txs []std.Tx `json:"txs"` + Balances []Balance `json:"balances"` + Txs []std.Tx `json:"txs"` +} + +type Balance struct { + Address bft.Address + Amount std.Coins +} + +func (b *Balance) Verify() error { + if b.Address.IsZero() { + return ErrBalanceEmptyAddress + } + + if b.Amount.Len() == 0 { + return ErrBalanceEmptyAmount + } + + return nil +} + +func (b *Balance) Parse(entry string) error { + parts := strings.Split(strings.TrimSpace(entry), "=") //
= + if len(parts) != 2 { + return fmt.Errorf("malformed entry: %q", entry) + } + + var err error + + b.Address, err = crypto.AddressFromBech32(parts[0]) + if err != nil { + return fmt.Errorf("invalid address %q: %w", parts[0], err) + } + + b.Amount, err = std.ParseCoins(parts[1]) + if err != nil { + return fmt.Errorf("invalid amount %q: %w", parts[1], err) + } + + return nil +} + +func (b *Balance) UnmarshalAmino(rep string) error { + return b.Parse(rep) +} + +func (b Balance) MarshalAmino() (string, error) { + return b.String(), nil +} + +func (b Balance) String() string { + return fmt.Sprintf("%s=%s", b.Address.String(), b.Amount.String()) } diff --git a/gno.land/pkg/gnoland/types_test.go b/gno.land/pkg/gnoland/types_test.go new file mode 100644 index 00000000000..97222d0cdfd --- /dev/null +++ b/gno.land/pkg/gnoland/types_test.go @@ -0,0 +1,98 @@ +package gnoland + +import ( + "fmt" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/jaekwon/testify/assert" + "github.com/jaekwon/testify/require" +) + +func TestBalance_Verify(t *testing.T) { + validAddress := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + emptyAmount := std.Coins{} + nonEmptyAmount := std.NewCoins(std.NewCoin("test", 100)) + + tests := []struct { + name string + balance Balance + expectErr bool + }{ + {"empty amount", Balance{Address: validAddress, Amount: emptyAmount}, true}, + {"empty address", Balance{Address: bft.Address{}, Amount: nonEmptyAmount}, true}, + {"valid balance", Balance{Address: validAddress, Amount: nonEmptyAmount}, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.balance.Verify() + if tc.expectErr { + assert.Error(t, err, fmt.Sprintf("TestVerifyBalance: %s", tc.name)) + } else { + assert.NoError(t, err, fmt.Sprintf("TestVerifyBalance: %s", tc.name)) + } + }) + } +} + +func TestBalance_Parse(t *testing.T) { + validAddress := crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + validBalance := Balance{Address: validAddress, Amount: std.NewCoins(std.NewCoin("test", 100))} + + tests := []struct { + name string + entry string + expected Balance + expectErr bool + }{ + {"valid entry", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5=100test", validBalance, false}, + {"invalid address", "invalid=100test", Balance{}, true}, + {"incomplete entry", "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", Balance{}, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + balance := Balance{} + err := balance.Parse(tc.entry) + if tc.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expected, balance) + } + }) + } +} + +func TestBalance_AminoUnmarshalJSON(t *testing.T) { + expected := Balance{ + Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + Amount: std.MustParseCoins("100ugnot"), + } + value := fmt.Sprintf("[%q]", expected.String()) + + var balances []Balance + err := amino.UnmarshalJSON([]byte(value), &balances) + require.NoError(t, err) + require.Len(t, balances, 1, "there should be one balance after unmarshaling") + + balance := balances[0] + require.Equal(t, expected.Address, balance.Address) + require.True(t, expected.Amount.IsEqual(balance.Amount)) +} + +func TestBalance_AminoMarshalJSON(t *testing.T) { + expected := Balance{ + Address: crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), + Amount: std.MustParseCoins("100ugnot"), + } + expectedJSON := fmt.Sprintf("[%q]", expected.String()) + + balancesJSON, err := amino.MarshalJSON([]Balance{expected}) + require.NoError(t, err) + require.JSONEq(t, expectedJSON, string(balancesJSON)) +} diff --git a/gno.land/pkg/integration/gnoland.go b/gno.land/pkg/integration/gnoland.go deleted file mode 100644 index 318d76eea86..00000000000 --- a/gno.land/pkg/integration/gnoland.go +++ /dev/null @@ -1,334 +0,0 @@ -package integration - -import ( - "flag" - "fmt" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/gnolang/gno/gno.land/pkg/gnoland" - 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/tm2/pkg/amino" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/config" - "github.com/gnolang/gno/tm2/pkg/bft/node" - "github.com/gnolang/gno/tm2/pkg/bft/privval" - bft "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/crypto" - "github.com/gnolang/gno/tm2/pkg/db" - "github.com/gnolang/gno/tm2/pkg/log" - osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/std" - "github.com/rogpeppe/go-internal/testscript" -) - -type IntegrationConfig struct { - SkipFailingGenesisTxs bool - SkipStart bool - GenesisBalancesFile string - GenesisTxsFile string - ChainID string - GenesisRemote string - RootDir string - GenesisMaxVMCycles int64 - Config string -} - -// NOTE: This is a copy of gnoland actual flags. -// XXX: A lot this make no sense for integration. -func (c *IntegrationConfig) RegisterFlags(fs *flag.FlagSet) { - fs.BoolVar( - &c.SkipFailingGenesisTxs, - "skip-failing-genesis-txs", - false, - "don't panic when replaying invalid genesis txs", - ) - fs.BoolVar( - &c.SkipStart, - "skip-start", - false, - "quit after initialization, don't start the node", - ) - - fs.StringVar( - &c.GenesisBalancesFile, - "genesis-balances-file", - "./genesis/genesis_balances.txt", - "initial distribution file", - ) - - fs.StringVar( - &c.GenesisTxsFile, - "genesis-txs-file", - "./genesis/genesis_txs.txt", - "initial txs to replay", - ) - - fs.StringVar( - &c.ChainID, - "chainid", - "dev", - "the ID of the chain", - ) - - fs.StringVar( - &c.RootDir, - "root-dir", - "testdir", - "directory for config and data", - ) - - fs.StringVar( - &c.GenesisRemote, - "genesis-remote", - "localhost:26657", - "replacement for '%%REMOTE%%' in genesis", - ) - - fs.Int64Var( - &c.GenesisMaxVMCycles, - "genesis-max-vm-cycles", - 10_000_000, - "set maximum allowed vm cycles per operation. Zero means no limit.", - ) -} - -func execTestingGnoland(t *testing.T, logger log.Logger, gnoDataDir, gnoRootDir string, args []string) (*node.Node, error) { - t.Helper() - - // Setup start config. - icfg := &IntegrationConfig{} - { - fs := flag.NewFlagSet("start", flag.ExitOnError) - icfg.RegisterFlags(fs) - - // Override default value for flags. - fs.VisitAll(func(f *flag.Flag) { - switch f.Name { - case "root-dir": - f.DefValue = gnoDataDir - case "chainid": - f.DefValue = "tendermint_test" - case "genesis-balances-file": - f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_balances.txt") - case "genesis-txs-file": - f.DefValue = filepath.Join(gnoRootDir, "gno.land", "genesis", "genesis_txs.txt") - default: - return - } - - f.Value.Set(f.DefValue) - }) - - if err := fs.Parse(args); err != nil { - return nil, fmt.Errorf("unable to parse flags: %w", err) - } - } - - // Setup testing config. - cfg := config.TestConfig().SetRootDir(gnoDataDir) - { - cfg.EnsureDirs() - cfg.RPC.ListenAddress = "tcp://127.0.0.1:0" - cfg.P2P.ListenAddress = "tcp://127.0.0.1:0" - } - - // Prepare genesis. - if err := setupTestingGenesis(gnoDataDir, cfg, icfg, gnoRootDir); err != nil { - return nil, err - } - - // Create application and node. - return createAppAndNode(cfg, logger, gnoRootDir, icfg) -} - -func setupTestingGenesis(gnoDataDir string, cfg *config.Config, icfg *IntegrationConfig, gnoRootDir string) error { - newPrivValKey := cfg.PrivValidatorKeyFile() - newPrivValState := cfg.PrivValidatorStateFile() - priv := privval.LoadOrGenFilePV(newPrivValKey, newPrivValState) - - genesisFilePath := filepath.Join(gnoDataDir, cfg.Genesis) - genesisDirPath := filepath.Dir(genesisFilePath) - if err := osm.EnsureDir(genesisDirPath, 0o700); err != nil { - return fmt.Errorf("unable to ensure directory %q: %w", genesisDirPath, err) - } - - genesisTxs := loadGenesisTxs(icfg.GenesisTxsFile, icfg.ChainID, icfg.GenesisRemote) - pvPub := priv.GetPubKey() - - gen := &bft.GenesisDoc{ - GenesisTime: time.Now(), - ChainID: icfg.ChainID, - ConsensusParams: abci.ConsensusParams{ - Block: &abci.BlockParams{ - // TODO: update limits. - MaxTxBytes: 1000000, // 1MB, - MaxDataBytes: 2000000, // 2MB, - MaxGas: 10000000, // 10M gas - TimeIotaMS: 100, // 100ms - }, - }, - Validators: []bft.GenesisValidator{ - { - Address: pvPub.Address(), - PubKey: pvPub, - Power: 10, - Name: "testvalidator", - }, - }, - } - - // Load distribution. - balances := loadGenesisBalances(icfg.GenesisBalancesFile) - - // Load initial packages from examples. - // XXX: We should be able to config this. - test1 := crypto.MustAddressFromString(test1Addr) - txs := []std.Tx{} - - // List initial packages to load from examples. - // println(filepath.Join(gnoRootDir, "examples")) - pkgs, err := gnomod.ListPkgs(filepath.Join(gnoRootDir, "examples")) - if err != nil { - return fmt.Errorf("listing gno packages: %w", err) - } - - // Sort packages by dependencies. - sortedPkgs, err := pkgs.Sort() - if err != nil { - return fmt.Errorf("sorting packages: %w", err) - } - - // Filter out draft packages. - nonDraftPkgs := sortedPkgs.GetNonDraftPkgs() - - for _, pkg := range nonDraftPkgs { - // Open files in directory as MemPackage. - memPkg := gno.ReadMemPackage(pkg.Dir, pkg.Name) - - var tx std.Tx - tx.Msgs = []std.Msg{ - vmm.MsgAddPackage{ - Creator: test1, - Package: memPkg, - Deposit: nil, - }, - } - - // XXX: Add fee flag ? - // Or maybe reduce fee to the minimum ? - tx.Fee = std.NewFee(50000, std.MustParseCoin("1000000ugnot")) - tx.Signatures = make([]std.Signature, len(tx.GetSigners())) - txs = append(txs, tx) - } - - // Load genesis txs from file. - txs = append(txs, genesisTxs...) - - // Construct genesis AppState. - gen.AppState = gnoland.GnoGenesisState{ - Balances: balances, - Txs: txs, - } - - writeGenesisFile(gen, genesisFilePath) - - return nil -} - -func createAppAndNode(cfg *config.Config, logger log.Logger, gnoRootDir string, icfg *IntegrationConfig) (*node.Node, error) { - gnoApp, err := gnoland.NewAppWithOptions(&gnoland.AppOptions{ - Logger: logger, - GnoRootDir: gnoRootDir, - SkipFailingGenesisTxs: icfg.SkipFailingGenesisTxs, - MaxCycles: icfg.GenesisMaxVMCycles, - DB: db.NewMemDB(), - }) - if err != nil { - return nil, fmt.Errorf("error in creating new app: %w", err) - } - - cfg.LocalApp = gnoApp - node, err := node.DefaultNewNode(cfg, logger) - if err != nil { - return nil, fmt.Errorf("error in creating node: %w", err) - } - - return node, node.Start() -} - -func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { - if err != nil { - fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) - if !neg { - ts.Fatalf("unexpected %q command failure: %s", cmd, err) - } - } else { - if neg { - ts.Fatalf("unexpected %s command success", cmd) - } - } -} - -func loadGenesisTxs( - path string, - chainID string, - genesisRemote string, -) []std.Tx { - txs := []std.Tx{} - txsBz := osm.MustReadFile(path) - txsLines := strings.Split(string(txsBz), "\n") - for _, txLine := range txsLines { - if txLine == "" { - continue // Skip empty line. - } - - // Patch the TX. - txLine = strings.ReplaceAll(txLine, "%%CHAINID%%", chainID) - txLine = strings.ReplaceAll(txLine, "%%REMOTE%%", genesisRemote) - - var tx std.Tx - amino.MustUnmarshalJSON([]byte(txLine), &tx) - txs = append(txs, tx) - } - - return txs -} - -func loadGenesisBalances(path string) []string { - // Each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot. - balances := []string{} - content := osm.MustReadFile(path) - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - - // Remove comments. - line = strings.Split(line, "#")[0] - line = strings.TrimSpace(line) - - // Skip empty lines. - if line == "" { - continue - } - - parts := strings.Split(line, "=") - if len(parts) != 2 { - panic("invalid genesis_balance line: " + line) - } - - balances = append(balances, line) - } - return balances -} - -func writeGenesisFile(gen *bft.GenesisDoc, filePath string) { - err := gen.SaveAs(filePath) - if err != nil { - panic(err) - } -} diff --git a/gno.land/pkg/integration/testing.go b/gno.land/pkg/integration/testing.go new file mode 100644 index 00000000000..7803e213da1 --- /dev/null +++ b/gno.land/pkg/integration/testing.go @@ -0,0 +1,39 @@ +package integration + +import ( + "errors" + + "github.com/jaekwon/testify/assert" + "github.com/jaekwon/testify/require" + "github.com/rogpeppe/go-internal/testscript" +) + +// This error is from testscript.Fatalf and is needed to correctly +// handle the FailNow method. +// see: https://github.com/rogpeppe/go-internal/blob/32ae33786eccde1672d4ba373c80e1bc282bfbf6/testscript/testscript.go#L799-L812 +var errFailNow = errors.New("fail now!") //nolint:stylecheck + +var ( + _ require.TestingT = (*testingTS)(nil) + _ assert.TestingT = (*testingTS)(nil) +) + +type TestingTS = require.TestingT + +type testingTS struct { + *testscript.TestScript +} + +func TSTestingT(ts *testscript.TestScript) TestingTS { + return &testingTS{ts} +} + +func (t *testingTS) Errorf(format string, args ...interface{}) { + defer recover() // we can ignore recover result, we just want to catch it up + t.Fatalf(format, args...) +} + +func (t *testingTS) FailNow() { + // unfortunately we can't access underlying `t.t.FailNow` method + panic(errFailNow) +} diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index f0a696ddd85..b773317513f 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -10,28 +10,18 @@ import ( "strings" "sync" "testing" - "time" "github.com/gnolang/gno/gno.land/pkg/gnoland" "github.com/gnolang/gno/tm2/pkg/bft/node" - "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/crypto/keys" "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" - "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" "github.com/rogpeppe/go-internal/testscript" ) -// XXX: This should be centralize somewhere. -const ( - test1Addr = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" - test1Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" -) - type testNode struct { *node.Node - logger log.Logger nGnoKeyExec uint // Counter for execution of gnokey. } @@ -51,15 +41,11 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { // `gnoRootDir` should point to the local location of the gno repository. // It serves as the gno equivalent of GOROOT. - gnoRootDir := gnoland.GuessGnoRootDir() + gnoRootDir := gnoland.MustGuessGnoRootDir() // `gnoHomeDir` should be the local directory where gnokey stores keys. gnoHomeDir := filepath.Join(tmpdir, "gno") - // `gnoDataDir` should refer to the local location where the gnoland node - // stores its configuration and data. - gnoDataDir := filepath.Join(tmpdir, "data") - // Testscripts run concurrently by default, so we need to be prepared for that. var muNodes sync.Mutex nodes := map[string]*testNode{} @@ -76,10 +62,35 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { return err } - // XXX: Add a command to add custom account. - kb.CreateAccount("test1", test1Seed, "", "", 0, 0) - env.Setenv("USER_SEED_test1", test1Seed) - env.Setenv("USER_ADDR_test1", test1Addr) + // create sessions ID + var sid string + { + works := env.Getenv("WORK") + sum := crc32.ChecksumIEEE([]byte(works)) + sid = strconv.FormatUint(uint64(sum), 16) + env.Setenv("SID", sid) + } + + // setup logger + var logger log.Logger + { + logger = log.NewNopLogger() + if persistWorkDir || os.Getenv("LOG_DIR") != "" { + logname := fmt.Sprintf("gnoland-%s.log", sid) + logger, err = getTestingLogger(env, logname) + if err != nil { + return fmt.Errorf("unable to setup logger: %w", err) + } + } + + env.Values["_logger"] = logger + } + + // Setup "test1" default 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) env.Setenv("GNOROOT", gnoRootDir) env.Setenv("GNOHOME", gnoHomeDir) @@ -96,7 +107,8 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { return } - sid := getSessionID(ts) + logger := ts.Value("_logger").(log.Logger) // grab logger + sid := ts.Getenv("SID") // grab session id var cmd string cmd, args = args[0], args[1:] @@ -109,63 +121,20 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { break } - logger := log.NewNopLogger() - if persistWorkDir || os.Getenv("LOG_DIR") != "" { - logname := fmt.Sprintf("gnoland-%s.log", sid) - logger = getTestingLogger(ts, logname) - } + // Warp up `ts` so we can pass it to other testing method + t := TSTestingT(ts) - dataDir := filepath.Join(gnoDataDir, sid) - var node *node.Node - if node, err = execTestingGnoland(t, logger, dataDir, gnoRootDir, args); err == nil { - nodes[sid] = &testNode{ - Node: node, - logger: logger, - } - ts.Defer(func() { - muNodes.Lock() - defer muNodes.Unlock() - - if n := nodes[sid]; n != nil { - if err := n.Stop(); err != nil { - panic(fmt.Errorf("node %q was unable to stop: %w", sid, err)) - } - } - }) - - // Get listen address environment. - // It should have been updated with the right port on start. - laddr := node.Config().RPC.ListenAddress - - // Add default environements. - ts.Setenv("RPC_ADDR", laddr) - ts.Setenv("GNODATA", gnoDataDir) - - const listenerID = "testing_listener" - - // Wait for first block by waiting for `EventNewBlock` event. - nb := make(chan struct{}, 1) - node.EventSwitch().AddListener(listenerID, func(ev events.Event) { - if _, ok := ev.(types.EventNewBlock); ok { - select { - case nb <- struct{}{}: - default: - } - } - }) - - if node.BlockStore().Height() == 0 { - select { - case <-nb: // ok - case <-time.After(time.Second * 6): - ts.Fatalf("timeout while waiting for the node to start") - } - } - - node.EventSwitch().RemoveListener(listenerID) - - fmt.Fprintln(ts.Stdout(), "node started successfully") - } + // Generate config and node + cfg := TestingMinimalNodeConfig(t, gnoRootDir) + n, remoteAddr := TestingInMemoryNode(t, logger, cfg) + + // Register cleanup + nodes[sid] = &testNode{Node: n} + + // Add default environements + ts.Setenv("RPC_ADDR", remoteAddr) + + fmt.Fprintln(ts.Stdout(), "node started successfully") case "stop": n, ok := nodes[sid] if !ok { @@ -176,9 +145,8 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { if err = n.Stop(); err == nil { delete(nodes, sid) - // Unset gnoland environements. + // Unset gnoland environements ts.Setenv("RPC_ADDR", "") - ts.Setenv("GNODATA", "") fmt.Fprintln(ts.Stdout(), "node stopped successfully") } default: @@ -191,9 +159,10 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { muNodes.Lock() defer muNodes.Unlock() - sid := getSessionID(ts) + logger := ts.Value("_logger").(log.Logger) // grab logger + sid := ts.Getenv("SID") // grab session id - // Setup IO command. + // Setup IO command io := commands.NewTestIO() io.SetOut(commands.WriteNopCloser(ts.Stdout())) io.SetErr(commands.WriteNopCloser(ts.Stderr())) @@ -212,9 +181,10 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { n.nGnoKeyExec++ headerlog := fmt.Sprintf("%.02d!EXEC_GNOKEY", n.nGnoKeyExec) + // Log the command inside gnoland logger, so we can better scope errors. - n.logger.Info(headerlog, strings.Join(args, " ")) - defer n.logger.Info(headerlog, "END") + logger.Info(headerlog, strings.Join(args, " ")) + defer logger.Info(headerlog, "END") } // Inject default argument, if duplicate @@ -230,35 +200,30 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { } } -func getSessionID(ts *testscript.TestScript) string { - works := ts.Getenv("WORK") - sum := crc32.ChecksumIEEE([]byte(works)) - return strconv.FormatUint(uint64(sum), 16) -} - -func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { +func getTestingLogger(env *testscript.Env, logname string) (log.Logger, error) { var path string + if logdir := os.Getenv("LOG_DIR"); logdir != "" { if err := os.MkdirAll(logdir, 0o755); err != nil { - ts.Fatalf("unable to make log directory %q", logdir) + return nil, fmt.Errorf("unable to make log directory %q", logdir) } var err error if path, err = filepath.Abs(filepath.Join(logdir, logname)); err != nil { - ts.Fatalf("uanble to get absolute path of logdir %q", logdir) + return nil, fmt.Errorf("uanble to get absolute path of logdir %q", logdir) } - } else if workdir := ts.Getenv("WORK"); workdir != "" { + } else if workdir := env.Getenv("WORK"); workdir != "" { path = filepath.Join(workdir, logname) } else { - return log.NewNopLogger() + return log.NewNopLogger(), nil } f, err := os.Create(path) if err != nil { - ts.Fatalf("unable to create log file %q: %s", path, err.Error()) + return nil, fmt.Errorf("unable to create log file %q: %w", path, err) } - ts.Defer(func() { + env.Defer(func() { if err := f.Close(); err != nil { panic(fmt.Errorf("unable to close log file %q: %w", path, err)) } @@ -274,9 +239,22 @@ func getTestingLogger(ts *testscript.TestScript, logname string) log.Logger { logger.SetLevel(log.LevelInfo) case "": default: - ts.Fatalf("invalid log level %q", level) + return nil, fmt.Errorf("invalid log level %q", level) } - ts.Logf("starting logger: %q", path) - return logger + env.T().Log("starting logger: %q", path) + return logger, nil +} + +func tsValidateError(ts *testscript.TestScript, cmd string, neg bool, err error) { + if err != nil { + fmt.Fprintf(ts.Stderr(), "%q error: %v\n", cmd, err) + if !neg { + ts.Fatalf("unexpected %q command failure: %s", cmd, err) + } + } else { + if neg { + ts.Fatalf("unexpected %q command success", cmd) + } + } } diff --git a/gno.land/pkg/integration/testing_node.go b/gno.land/pkg/integration/testing_node.go new file mode 100644 index 00000000000..1ca7e11eb63 --- /dev/null +++ b/gno.land/pkg/integration/testing_node.go @@ -0,0 +1,184 @@ +package integration + +import ( + "path/filepath" + "sync" + "time" + + "github.com/gnolang/gno/gno.land/pkg/gnoland" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config" + "github.com/gnolang/gno/tm2/pkg/bft/node" + bft "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/crypto" + "github.com/gnolang/gno/tm2/pkg/events" + "github.com/gnolang/gno/tm2/pkg/log" + "github.com/gnolang/gno/tm2/pkg/std" + "github.com/jaekwon/testify/require" +) + +const ( + DefaultAccount_Name = "test1" + DefaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + DefaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast" +) + +// TestingInMemoryNode initializes and starts an in-memory node for testing. +// It returns the node instance and its RPC remote address. +func TestingInMemoryNode(t TestingTS, logger log.Logger, config *gnoland.InMemoryNodeConfig) (*node.Node, string) { + node, err := gnoland.NewInMemoryNode(logger, config) + require.NoError(t, err) + + err = node.Start() + require.NoError(t, err) + + select { + case <-waitForNodeReadiness(node): + case <-time.After(time.Second * 6): + require.FailNow(t, "timeout while waiting for the node to start") + } + + return node, node.Config().RPC.ListenAddress +} + +// TestingNodeConfig constructs an in-memory node configuration +// with default packages and genesis transactions already loaded. +// It will return the default creator address of the loaded packages. +func TestingNodeConfig(t TestingTS, gnoroot string) (*gnoland.InMemoryNodeConfig, bft.Address) { + cfg := TestingMinimalNodeConfig(t, gnoroot) + + creator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + + balances := LoadDefaultGenesisBalanceFile(t, gnoroot) + txs := []std.Tx{} + txs = append(txs, LoadDefaultPackages(t, creator, gnoroot)...) + txs = append(txs, LoadDefaultGenesisTXsFile(t, cfg.Genesis.ChainID, gnoroot)...) + + cfg.Genesis.AppState = gnoland.GnoGenesisState{ + Balances: balances, + Txs: txs, + } + + return cfg, creator +} + +// TestingMinimalNodeConfig constructs the default minimal in-memory node configuration for testing. +func TestingMinimalNodeConfig(t TestingTS, gnoroot string) *gnoland.InMemoryNodeConfig { + tmconfig := DefaultTestingTMConfig(gnoroot) + + // Create Mocked Identity + pv := gnoland.NewMockedPrivValidator() + + // Generate genesis config + genesis := DefaultTestingGenesisConfig(t, gnoroot, pv.GetPubKey(), tmconfig) + + return &gnoland.InMemoryNodeConfig{ + PrivValidator: pv, + Genesis: genesis, + TMConfig: tmconfig, + } +} + +func DefaultTestingGenesisConfig(t TestingTS, gnoroot string, self crypto.PubKey, tmconfig *tmcfg.Config) *bft.GenesisDoc { + return &bft.GenesisDoc{ + GenesisTime: time.Now(), + ChainID: tmconfig.ChainID(), + ConsensusParams: abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxTxBytes: 1_000_000, // 1MB, + MaxDataBytes: 2_000_000, // 2MB, + MaxGas: 10_0000_000, // 10M gas + TimeIotaMS: 100, // 100ms + }, + }, + Validators: []bft.GenesisValidator{ + { + Address: self.Address(), + PubKey: self, + Power: 10, + Name: "self", + }, + }, + AppState: gnoland.GnoGenesisState{ + Balances: []gnoland.Balance{ + { + Address: crypto.MustAddressFromString(DefaultAccount_Address), + Amount: std.MustParseCoins("10000000000000ugnot"), + }, + }, + Txs: []std.Tx{}, + }, + } +} + +// LoadDefaultPackages loads the default packages for testing using a given creator address and gnoroot directory. +func LoadDefaultPackages(t TestingTS, creator bft.Address, gnoroot string) []std.Tx { + examplesDir := filepath.Join(gnoroot, "examples") + + defaultFee := std.NewFee(50000, std.MustParseCoin("1000000ugnot")) + defaultCreator := crypto.MustAddressFromString(DefaultAccount_Address) // test1 + txs, err := gnoland.LoadPackagesFromDir(examplesDir, defaultCreator, defaultFee, nil) + require.NoError(t, err) + + return txs +} + +// LoadDefaultGenesisBalanceFile loads the default genesis balance file for testing. +func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balance { + balanceFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt") + + genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile) + require.NoError(t, err) + + return genesisBalances +} + +// LoadDefaultGenesisTXsFile loads the default genesis transactions file for testing. +func LoadDefaultGenesisTXsFile(t TestingTS, chainid string, gnoroot string) []std.Tx { + txsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.txt") + + // NOTE: We dont care about giving a correct address here, as it's only for display + // XXX: Do we care loading this TXs for testing ? + genesisTXs, err := gnoland.LoadGenesisTxsFile(txsFile, chainid, "https://127.0.0.1:26657") + require.NoError(t, err) + + return genesisTXs +} + +// DefaultTestingTMConfig constructs the default Tendermint configuration for testing. +func DefaultTestingTMConfig(gnoroot string) *tmcfg.Config { + const defaultListner = "tcp://127.0.0.1:0" + + tmconfig := tmcfg.TestConfig().SetRootDir(gnoroot) + tmconfig.Consensus.CreateEmptyBlocks = true + tmconfig.Consensus.CreateEmptyBlocksInterval = time.Duration(0) + tmconfig.RPC.ListenAddress = defaultListner + tmconfig.P2P.ListenAddress = defaultListner + return tmconfig +} + +// waitForNodeReadiness waits until the node is ready, signaling via the EventNewBlock event. +// XXX: This should be replace by https://github.com/gnolang/gno/pull/1216 +func waitForNodeReadiness(n *node.Node) <-chan struct{} { + const listenerID = "first_block_listener" + + var once sync.Once + + nb := make(chan struct{}) + ready := func() { + close(nb) + n.EventSwitch().RemoveListener(listenerID) + } + + n.EventSwitch().AddListener(listenerID, func(ev events.Event) { + if _, ok := ev.(bft.EventNewBlock); ok { + once.Do(ready) + } + }) + + if n.BlockStore().Height() > 0 { + once.Do(ready) + } + + return nb +} From a88e3e37923ab6ca562ca0d7d39606e490d9a736 Mon Sep 17 00:00:00 2001 From: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:28:50 +0100 Subject: [PATCH 61/93] feat: setup testscripts coverage (#1249) Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- .github/workflows/gnovm.yml | 43 ++++++++++-- gnovm/Makefile | 18 ++--- gnovm/cmd/gno/build_test.go | 18 ++++- gnovm/cmd/gno/main_test.go | 44 ------------ gnovm/cmd/gno/test_test.go | 18 ++++- gnovm/pkg/integration/coverage.go | 68 +++++++++++++++++++ gnovm/pkg/integration/gno.go | 108 ++++++++++++++++++++++++++++++ 7 files changed, 255 insertions(+), 62 deletions(-) create mode 100644 gnovm/pkg/integration/coverage.go create mode 100644 gnovm/pkg/integration/gno.go diff --git a/.github/workflows/gnovm.yml b/.github/workflows/gnovm.yml index 71b03b5ca05..5fa68ac305a 100644 --- a/.github/workflows/gnovm.yml +++ b/.github/workflows/gnovm.yml @@ -67,6 +67,8 @@ jobs: - _test.gnolang.other runs-on: ubuntu-latest timeout-minutes: 15 + env: + COVERAGE_DIR: "/tmp/coverage" steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v4 @@ -74,28 +76,59 @@ jobs: go-version: ${{ matrix.goversion }} - name: test working-directory: gnovm + env: + TXTARCOVERDIR: ${{ env.COVERAGE_DIR }} run: | + mkdir -p $COVERAGE_DIR + + # Setup testing environements variables export GOPATH=$HOME/go - export GOTEST_FLAGS="-v -p 1 -timeout=30m -coverprofile=coverage.out -covermode=atomic" + export GOTEST_FLAGS="-v -p 1 -timeout=30m -covermode=atomic -test.gocoverdir=$COVERAGE_DIR" + + # Run target test make ${{ matrix.args }} - uses: actions/upload-artifact@v3 if: ${{ runner.os == 'Linux' && matrix.goversion == '1.21.x' }} with: name: ${{runner.os}}-coverage-gnovm-${{ matrix.args}}-${{matrix.goversion}} - path: ./gnovm/coverage.out + path: ${{ env.COVERAGE_DIR }} upload-coverage: needs: test runs-on: ubuntu-latest + env: + COVERAGE_DATA: /tmp/coverage/coverage-raw + COVERAGE_OUTPUT: /tmp/coverage/coverage-out + COVERAGE_PROFILE: /tmp/coverage/coverage.txt steps: - - name: Download all previous coverage artifacts + - run: mkdir -p $COVERAGE_DATA $COVERAGE_OUTPUT + - name: Download all previous coverage data artifacts uses: actions/download-artifact@v3 with: - path: ${{ runner.temp }}/coverage + path: ${{ env.COVERAGE_DATA }} + - uses: actions/setup-go@v4 + with: + go-version: "1.21.x" + - name: Merge coverages + working-directory: ${{ env.COVERAGE_DATA }} + run: | + # Create coverage directory list separate by comma + export COVERAGE_DIRS="$(ls | tr '\n' ',' | sed s/,$//)" + + # Merge all coverage data directories from previous tests + go tool covdata merge -v 1 -i="$COVERAGE_DIRS" -o $COVERAGE_OUTPUT + + # Print coverage percent for debug purpose if needed + echo 'coverage results:' + go tool covdata percent -i=$COVERAGE_OUTPUT + + # Generate coverage profile + go tool covdata textfmt -v 1 -i=$COVERAGE_OUTPUT -o $COVERAGE_PROFILE + - name: Upload combined coverage to Codecov uses: codecov/codecov-action@v3 with: - directory: ${{ runner.temp }}/coverage + files: ${{ env.COVERAGE_PROFILE }} token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: ${{ github.repository == 'gnolang/gno' }} diff --git a/gnovm/Makefile b/gnovm/Makefile index 5fcacf94f62..34e94f88633 100644 --- a/gnovm/Makefile +++ b/gnovm/Makefile @@ -46,15 +46,15 @@ _test.pkg: .PHONY: _test.gnolang _test.gnolang: _test.gnolang.native _test.gnolang.stdlibs _test.gnolang.realm _test.gnolang.pkg0 _test.gnolang.pkg1 _test.gnolang.pkg2 _test.gnolang.other -_test.gnolang.other:; go test $(GOTEST_FLAGS) tests/*.go -run "(TestFileStr|TestSelectors)" -_test.gnolang.realm:; go test $(GOTEST_FLAGS) tests/*.go -run "TestFiles/^zrealm" -_test.gnolang.pkg0:; go test $(GOTEST_FLAGS) tests/*.go -run "TestPackages/(bufio|crypto|encoding|errors|internal|io|math|sort|std|stdshim|strconv|strings|testing|unicode)" -_test.gnolang.pkg1:; go test $(GOTEST_FLAGS) tests/*.go -run "TestPackages/regexp" -_test.gnolang.pkg2:; go test $(GOTEST_FLAGS) tests/*.go -run "TestPackages/bytes" -_test.gnolang.native:; go test $(GOTEST_FLAGS) tests/*.go -test.short -run "TestFilesNative/" -_test.gnolang.stdlibs:; go test $(GOTEST_FLAGS) tests/*.go -test.short -run 'TestFiles$$/' -_test.gnolang.native.sync:; go test $(GOTEST_FLAGS) tests/*.go -test.short -run "TestFilesNative/" --update-golden-tests -_test.gnolang.stdlibs.sync:; go test $(GOTEST_FLAGS) tests/*.go -test.short -run 'TestFiles$$/' --update-golden-tests +_test.gnolang.other:; go test tests/*.go -run "(TestFileStr|TestSelectors)" $(GOTEST_FLAGS) +_test.gnolang.realm:; go test tests/*.go -run "TestFiles/^zrealm" $(GOTEST_FLAGS) +_test.gnolang.pkg0:; go test tests/*.go -run "TestPackages/(bufio|crypto|encoding|errors|internal|io|math|sort|std|stdshim|strconv|strings|testing|unicode)" $(GOTEST_FLAGS) +_test.gnolang.pkg1:; go test tests/*.go -run "TestPackages/regexp" $(GOTEST_FLAGS) +_test.gnolang.pkg2:; go test tests/*.go -run "TestPackages/bytes" $(GOTEST_FLAGS) +_test.gnolang.native:; go test tests/*.go -test.short -run "TestFilesNative/" $(GOTEST_FLAGS) +_test.gnolang.stdlibs:; go test tests/*.go -test.short -run 'TestFiles$$/' $(GOTEST_FLAGS) +_test.gnolang.native.sync:; go test tests/*.go -test.short -run "TestFilesNative/" --update-golden-tests $(GOTEST_FLAGS) +_test.gnolang.stdlibs.sync:; go test tests/*.go -test.short -run 'TestFiles$$/' --update-golden-tests $(GOTEST_FLAGS) ######################################## # Code gen diff --git a/gnovm/cmd/gno/build_test.go b/gnovm/cmd/gno/build_test.go index 5bb03ef0d35..81aed7d1c79 100644 --- a/gnovm/cmd/gno/build_test.go +++ b/gnovm/cmd/gno/build_test.go @@ -3,9 +3,23 @@ package main import ( "testing" + "github.com/gnolang/gno/gnovm/pkg/integration" "github.com/rogpeppe/go-internal/testscript" + "github.com/stretchr/testify/require" ) -func TestBuild(t *testing.T) { - testscript.Run(t, setupTestScript(t, "testdata/gno_build")) +func Test_ScriptsBuild(t *testing.T) { + p := testscript.Params{ + Dir: "testdata/gno_build", + } + + if coverdir, ok := integration.ResolveCoverageDir(); ok { + err := integration.SetupTestscriptsCoverage(&p, coverdir) + require.NoError(t, err) + } + + err := integration.SetupGno(&p, t.TempDir()) + require.NoError(t, err) + + testscript.Run(t, p) } diff --git a/gnovm/cmd/gno/main_test.go b/gnovm/cmd/gno/main_test.go index 8d19a50e814..371cdf913e8 100644 --- a/gnovm/cmd/gno/main_test.go +++ b/gnovm/cmd/gno/main_test.go @@ -5,12 +5,10 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "strings" "testing" - "github.com/rogpeppe/go-internal/testscript" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/commands" @@ -144,45 +142,3 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) { }) } } - -func setupTestScript(t *testing.T, txtarDir string) testscript.Params { - t.Helper() - // Get root location of github.com/gnolang/gno - goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() - require.NoError(t, err) - rootDir := filepath.Dir(string(goModPath)) - // Build a fresh gno binary in a temp directory - gnoBin := filepath.Join(t.TempDir(), "gno") - err = exec.Command("go", "build", "-o", gnoBin, filepath.Join(rootDir, "gnovm", "cmd", "gno")).Run() - require.NoError(t, err) - // Define script params - return testscript.Params{ - Setup: func(env *testscript.Env) error { - env.Vars = append(env.Vars, - "GNOROOT="+rootDir, // thx PR 1014 :) - // by default, $HOME=/no-home, but we need an existing $HOME directory - // because some commands needs to access $HOME/.cache/go-build - "HOME="+t.TempDir(), - ) - return nil - }, - Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ - // add a custom "gno" command so txtar files can easily execute "gno" - // without knowing where is the binary or how it is executed. - "gno": func(ts *testscript.TestScript, neg bool, args []string) { - err := ts.Exec(gnoBin, args...) - if err != nil { - ts.Logf("[%v]\n", err) - if !neg { - ts.Fatalf("unexpected gno command failure") - } - } else { - if neg { - ts.Fatalf("unexpected gno command success") - } - } - }, - }, - Dir: txtarDir, - } -} diff --git a/gnovm/cmd/gno/test_test.go b/gnovm/cmd/gno/test_test.go index f5b069a5f03..b1dcfb21d29 100644 --- a/gnovm/cmd/gno/test_test.go +++ b/gnovm/cmd/gno/test_test.go @@ -3,9 +3,23 @@ package main import ( "testing" + "github.com/gnolang/gno/gnovm/pkg/integration" "github.com/rogpeppe/go-internal/testscript" + "github.com/stretchr/testify/require" ) -func TestTest(t *testing.T) { - testscript.Run(t, setupTestScript(t, "testdata/gno_test")) +func Test_ScriptsTest(t *testing.T) { + p := testscript.Params{ + Dir: "testdata/gno_test", + } + + if coverdir, ok := integration.ResolveCoverageDir(); ok { + err := integration.SetupTestscriptsCoverage(&p, coverdir) + require.NoError(t, err) + } + + err := integration.SetupGno(&p, t.TempDir()) + require.NoError(t, err) + + testscript.Run(t, p) } diff --git a/gnovm/pkg/integration/coverage.go b/gnovm/pkg/integration/coverage.go new file mode 100644 index 00000000000..017f5f9de88 --- /dev/null +++ b/gnovm/pkg/integration/coverage.go @@ -0,0 +1,68 @@ +package integration + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/rogpeppe/go-internal/testscript" +) + +var coverageEnv struct { + coverdir string +} + +func init() { + flag.StringVar(&coverageEnv.coverdir, + "txtarcoverdir", "", "write testscripts coverage intermediate files to this directory") +} + +// ResolveCoverageDir attempts to resolve the coverage directory from the 'TXTARCOVERDIR' +// environment variable first, and if not set, from the 'test.txtarcoverdir' flag. +// It returns the resolved directory and a boolean indicating if the resolution was successful. +func ResolveCoverageDir() (string, bool) { + // Attempt to resolve the cover directory from the environment variable or flag + coverdir := os.Getenv("TXTARCOVERDIR") + if coverdir == "" { + coverdir = coverageEnv.coverdir + } + + return coverdir, coverdir != "" +} + +// SetupTestscriptsCoverage sets up the given testscripts environment for coverage. +// It will mostly override `GOCOVERDIR` with the target cover directory +func SetupTestscriptsCoverage(p *testscript.Params, coverdir string) error { + // Check if the given coverage directory exist + info, err := os.Stat(coverdir) + if err != nil { + return fmt.Errorf("output directory %q inaccessible: %w", coverdir, err) + } else if !info.IsDir() { + return fmt.Errorf("output %q not a directory", coverdir) + } + + // We need to have an absolute path here, because current directory + // context will change while executing testscripts. + if !filepath.IsAbs(coverdir) { + var err error + if coverdir, err = filepath.Abs(coverdir); err != nil { + return fmt.Errorf("unable to determine absolute path of %q: %w", coverdir, err) + } + } + + // Backup the original setup function + origSetup := p.Setup + p.Setup = func(env *testscript.Env) error { + if origSetup != nil { + // Call previous setup first + origSetup(env) + } + + // Override `GOCOVEDIR` directory for sub-execution + env.Setenv("GOCOVERDIR", coverdir) + return nil + } + + return nil +} diff --git a/gnovm/pkg/integration/gno.go b/gnovm/pkg/integration/gno.go new file mode 100644 index 00000000000..56e18bbb5db --- /dev/null +++ b/gnovm/pkg/integration/gno.go @@ -0,0 +1,108 @@ +package integration + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + osm "github.com/gnolang/gno/tm2/pkg/os" + "github.com/rogpeppe/go-internal/testscript" +) + +// SetupGno prepares the given testscript environment for tests that utilize the gno command. +// If the `gno` binary doesn't exist, it's built using the `go build` command into the specified buildDir. +// The function also include the `gno` command into `p.Cmds` to and wrap environment into p.Setup +// to correctly set up the environment variables needed for the `gno` command. +func SetupGno(p *testscript.Params, buildDir string) error { + // Try to fetch `GNOROOT` from the environment variables + gnoroot := os.Getenv("GNOROOT") + if gnoroot == "" { + // If `GNOROOT` isn't set, determine the root directory of github.com/gnolang/gno + goModPath, err := exec.Command("go", "env", "GOMOD").CombinedOutput() + if err != nil { + return fmt.Errorf("unable to determine gno root directory") + } + + gnoroot = filepath.Dir(string(goModPath)) + } + + if !osm.DirExists(buildDir) { + return fmt.Errorf("%q does not exist or is not a directory", buildDir) + } + + // Determine the path to the gno binary within the build directory + gnoBin := filepath.Join(buildDir, "gno") + if _, err := os.Stat(gnoBin); err != nil { + if !errors.Is(err, os.ErrNotExist) { + // Handle other potential errors from os.Stat + return err + } + + // Build a fresh gno binary in a temp directory + gnoArgsBuilder := []string{"build", "-o", gnoBin} + + // Forward `-covermode` settings if set + if coverMode := testing.CoverMode(); coverMode != "" { + gnoArgsBuilder = append(gnoArgsBuilder, "-covermode", coverMode) + } + + // Append the path to the gno command source + gnoArgsBuilder = append(gnoArgsBuilder, filepath.Join(gnoroot, "gnovm", "cmd", "gno")) + + if err = exec.Command("go", gnoArgsBuilder...).Run(); err != nil { + return fmt.Errorf("unable to build gno binary: %w", err) + } + } + + // Store the original setup scripts for potential wrapping + origSetup := p.Setup + p.Setup = func(env *testscript.Env) error { + // If there's an original setup, execute it + if origSetup != nil { + if err := origSetup(env); err != nil { + return err + } + } + + // Set the GNOROOT environment variable + env.Setenv("GNOROOT", gnoroot) + + // Create a temporary home directory because certain commands require access to $HOME/.cache/go-build + home, err := os.MkdirTemp("", "gno") + if err != nil { + return fmt.Errorf("unable to create temporary home directory: %w", err) + } + env.Setenv("HOME", home) + + // Cleanup home folder + env.Defer(func() { os.RemoveAll(home) }) + + return nil + } + + // Initialize cmds map if needed + if p.Cmds == nil { + p.Cmds = make(map[string]func(ts *testscript.TestScript, neg bool, args []string)) + } + + // Register the gno command for testscripts + p.Cmds["gno"] = func(ts *testscript.TestScript, neg bool, args []string) { + err := ts.Exec(gnoBin, args...) + if err != nil { + ts.Logf("gno command error: %v", err) + } + + commandSucceeded := (err == nil) + successExpected := !neg + + // Compare the command's success status with the expected outcome. + if commandSucceeded != successExpected { + ts.Fatalf("unexpected gno command outcome (err=%t expected=%t)", commandSucceeded, successExpected) + } + } + + return nil +} From 17322140c6b138c1ee27187c71f4cdc8cf30fa72 Mon Sep 17 00:00:00 2001 From: deelawn Date: Tue, 7 Nov 2023 16:17:29 +0000 Subject: [PATCH 62/93] fix: don't pass value types by reference (#1263) Addresses #1096 These changes ensure that passing non-primitive value types to a function do not result in the modification of the original value, even if the value type is represented as a pointer inside the VM. --- gnovm/pkg/gnolang/op_call.go | 6 ++- gnovm/pkg/gnolang/values.go | 10 +++- gnovm/tests/files/issue-1096.gno | 87 ++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 gnovm/tests/files/issue-1096.gno diff --git a/gnovm/pkg/gnolang/op_call.go b/gnovm/pkg/gnolang/op_call.go index f43a593c50e..8d652667111 100644 --- a/gnovm/pkg/gnolang/op_call.go +++ b/gnovm/pkg/gnolang/op_call.go @@ -154,7 +154,11 @@ func (m *Machine) doOpCall() { } // TODO: some more pt <> pv.Type // reconciliations/conversions necessary. - b.Values[i] = pv + + // Make a copy so that a reference to the arguemnt isn't used + // in cases where the non-primitive value type is represented + // as a pointer, *StructValue, for example. + b.Values[i] = pv.Copy(m.Alloc) } } diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index 3de74ac0130..3bdd3332e08 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -500,7 +500,15 @@ func (sv *StructValue) Copy(alloc *Allocator) *StructValue { } */ fields := alloc.NewStructFields(len(sv.Fields)) - copy(fields, sv.Fields) + + // Each field needs to be copied individually to ensure that + // value fields are copied as such, even though they may be represented + // as pointers. A good example of this would be a struct that has + // a field that is an array. The value array is represented as a pointer. + for i, field := range sv.Fields { + fields[i] = field.Copy(alloc) + } + return alloc.NewStruct(fields) } diff --git a/gnovm/tests/files/issue-1096.gno b/gnovm/tests/files/issue-1096.gno new file mode 100644 index 00000000000..b0593913401 --- /dev/null +++ b/gnovm/tests/files/issue-1096.gno @@ -0,0 +1,87 @@ +package main + +import "fmt" + +type X struct { + Array [8]int + Test bool +} + +type Y [8]int + +func main() { + x := X{} + x.Array[1] = 888 + println(x.Array[1]) + println(x.Array[2]) + println(x.Test) + + x.manip() + println(x.Array[1]) + println(x.Array[2]) + println(x.Test) + + println("-----") + + y := Y{} + y[1] = 888 + println(y[1]) + println(y[2]) + + y.manip() + println(y[1]) + println(y[2]) + println("-----") + + x = X{} + println(x.Array[1]) + println(x.Array[2]) + println(x.Test) + + x.Array[1] = 888 + println(x.Array[1]) + println(x.Array[2]) + println(x.Test) + + manip(x) + println(x.Array[1]) + println(x.Array[2]) + println(x.Test) +} + +func (x X) manip() { + x.Array[2] = 999 + x.Test = true +} + +func manip(x X) { + x.Array[2] = 999 + x.Test = true +} + +func (y Y) manip() { + y[2] = 111 +} + +// Output: +// 888 +// 0 +// false +// 888 +// 0 +// false +// ----- +// 888 +// 0 +// 888 +// 0 +// ----- +// 0 +// 0 +// false +// 888 +// 0 +// false +// 888 +// 0 +// false From 49a5fa437c9a4fd96c4510ca91bb1f740f00050f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:37:07 +0100 Subject: [PATCH 63/93] chore(deps): bump go.etcd.io/bbolt from 1.3.7 to 1.3.8 (#1318) Bumps [go.etcd.io/bbolt](https://github.com/etcd-io/bbolt) from 1.3.7 to 1.3.8.
Release notes

Sourced from go.etcd.io/bbolt's releases.

v1.3.8

See the CHANGELOG for more details.

Commits
  • 42a914d Merge pull request #586 from ahrtr/1.3_64bit_align_20231025
  • f9d290f ensure the stats is always 64bit aligned
  • 4a17732 Merge pull request #444 from jmhbnz/backport-failpoints-injection-2
  • 95acc50 Backport add test cases to simulate mlock failure.
  • 7a13798 Backport perform unmap when failing to mlock or both meta pages corrupted.
  • ad36005 Merge pull request #439 from jmhbnz/backport-failpoints-injection
  • 8165a40 Backport change to error handling logic in db.close().
  • e4e06d2 update the usage of surgery command
  • 1108915 Avoid syscall.Syscall use on OpenBSD
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=go.etcd.io/bbolt&package-manager=go_modules&previous-version=1.3.7&new-version=1.3.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b627bea1ce1..f8b1ecf669b 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/rs/cors v1.10.1 github.com/stretchr/testify v1.8.4 github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c - go.etcd.io/bbolt v1.3.7 + go.etcd.io/bbolt v1.3.8 go.uber.org/multierr v1.9.0 golang.org/x/crypto v0.14.0 golang.org/x/mod v0.14.0 diff --git a/go.sum b/go.sum index 2bbff44d4d1..b9ab94d490c 100644 --- a/go.sum +++ b/go.sum @@ -178,8 +178,8 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= From 55e88bedf00a3584d919a68d0f4d2e8787363785 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Wed, 8 Nov 2023 16:46:48 +0900 Subject: [PATCH 64/93] chore: remove deprecated io/ioutil methods (#1277) ## Description Replace depreacated `io/ioutil` to `os` methods --------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Morgan Bazalgette --- gnovm/pkg/gnolang/nodes.go | 3 +- gnovm/pkg/gnolang/precompile.go | 3 +- gnovm/pkg/gnomod/gnomod.go | 5 +- gnovm/stdlibs/io/export_test.gno | 6 +- gnovm/stdlibs/io/io.gno | 77 +++++++++++++++++ gnovm/stdlibs/io/io_test.gno | 86 ++++++++++++++++++- gnovm/stdlibs/io/ioutil/ioutil.gno | 83 ------------------ gnovm/stdlibs/io/multi_test.gno | 4 +- gnovm/tests/backup/cli1.gno | 4 +- gnovm/tests/backup/cli2.gno | 4 +- gnovm/tests/backup/cli3.gno | 4 +- gnovm/tests/backup/cli4.gno | 4 +- gnovm/tests/backup/cli5.gno | 4 +- gnovm/tests/backup/cli6.gno | 4 +- gnovm/tests/backup/file_access.gno | 5 +- gnovm/tests/backup/ioutil.gno | 16 ---- gnovm/tests/backup/issue-558.gno | 6 +- gnovm/tests/files/{ioutil0.gno => io2.gno} | 4 +- gnovm/tests/files/issue-558b.gno | 4 +- gnovm/tests/imports.go | 8 +- tm2/pkg/amino/genproto/genproto.go | 5 +- tm2/pkg/autofile/autofile_test.go | 7 +- tm2/pkg/bft/config/toml.go | 3 +- tm2/pkg/bft/consensus/common_test.go | 7 +- tm2/pkg/bft/consensus/replay_test.go | 3 +- tm2/pkg/bft/privval/file_test.go | 25 +++--- tm2/pkg/bft/privval/socket_listeners_test.go | 3 +- tm2/pkg/bft/rpc/client/main_test.go | 3 +- tm2/pkg/bft/rpc/lib/client/http_client.go | 8 +- tm2/pkg/bft/rpc/lib/server/handlers.go | 4 +- tm2/pkg/bft/rpc/lib/server/handlers_test.go | 10 +-- .../bft/rpc/lib/server/http_server_test.go | 5 +- tm2/pkg/bft/types/genesis_test.go | 3 +- tm2/pkg/bft/types/part_set_test.go | 4 +- tm2/pkg/db/fsdb.go | 4 +- tm2/pkg/iavl/tree_dotgraph_test.go | 4 +- tm2/pkg/log/tm_logger_test.go | 6 +- tm2/pkg/os/tempfile_test.go | 3 +- tm2/pkg/p2p/upnp/upnp.go | 6 +- 39 files changed, 245 insertions(+), 202 deletions(-) delete mode 100644 gnovm/stdlibs/io/ioutil/ioutil.gno delete mode 100644 gnovm/tests/backup/ioutil.gno rename gnovm/tests/files/{ioutil0.gno => io2.gno} (88%) diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 46308fb3a02..2a9e0b51a97 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -4,7 +4,6 @@ import ( "fmt" "go/parser" "go/token" - "io/ioutil" "os" "path/filepath" "reflect" @@ -1095,7 +1094,7 @@ func PackageNameFromFileBody(name, body string) Name { // NOTE: panics if package name is invalid. func ReadMemPackage(dir string, pkgPath string) *std.MemPackage { - files, err := ioutil.ReadDir(dir) + files, err := os.ReadDir(dir) if err != nil { panic(err) } diff --git a/gnovm/pkg/gnolang/precompile.go b/gnovm/pkg/gnolang/precompile.go index f7a10d5589a..c3116f25800 100644 --- a/gnovm/pkg/gnolang/precompile.go +++ b/gnovm/pkg/gnolang/precompile.go @@ -7,7 +7,6 @@ import ( "go/format" "go/parser" "go/token" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -112,7 +111,7 @@ func GetPrecompileFilenameAndTags(gnoFilePath string) (targetFilename, tags stri func PrecompileAndCheckMempkg(mempkg *std.MemPackage) error { gofmt := "gofmt" - tmpDir, err := ioutil.TempDir("", mempkg.Name) + tmpDir, err := os.MkdirTemp("", mempkg.Name) if err != nil { return err } diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index 7bb51d6558a..3c224bafb87 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -3,7 +3,6 @@ package gnomod import ( "errors" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -172,9 +171,9 @@ func CreateGnoModFile(rootDir, modPath string) error { if modPath == "" { // Check .gno files for package name // and use it as modPath - files, err := ioutil.ReadDir(rootDir) + files, err := os.ReadDir(rootDir) if err != nil { - fmt.Errorf("read dir %q: %w", rootDir, err) + return fmt.Errorf("read dir %q: %w", rootDir, err) } var pkgName gnolang.Name diff --git a/gnovm/stdlibs/io/export_test.gno b/gnovm/stdlibs/io/export_test.gno index fa3e8e76f61..6204ffc4591 100644 --- a/gnovm/stdlibs/io/export_test.gno +++ b/gnovm/stdlibs/io/export_test.gno @@ -5,4 +5,8 @@ package io // exported for test -var ErrInvalidWrite = errInvalidWrite +var ( + ErrInvalidWrite = errInvalidWrite + ErrWhence = errWhence + ErrOffset = errOffset +) diff --git a/gnovm/stdlibs/io/io.gno b/gnovm/stdlibs/io/io.gno index 54caf32cb95..6ee52cfe293 100644 --- a/gnovm/stdlibs/io/io.gno +++ b/gnovm/stdlibs/io/io.gno @@ -16,6 +16,8 @@ import ( "errors" ) +// TODO: implement rest of io package after sync package added. + // Seek whence values. const ( SeekStart = 0 // seek relative to the origin of the file @@ -477,6 +479,15 @@ func (l *LimitedReader) Read(p []byte) (n int, err error) { // NewSectionReader returns a SectionReader that reads from r // starting at offset off and stops with EOF after n bytes. func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader { + var remaining int64 + const maxInt64 = 1<<63 - 1 + if off <= maxInt64-n { + remaining = n + off + } else { + // Overflow, with no way to return error. + // Assume we can read up to an offset of `1<<63 - 1` bytes in this case. + remaining = maxInt64 + } return &SectionReader{r, off, off, off + n} } @@ -543,6 +554,53 @@ func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) { // Size returns the size of the section in bytes. func (s *SectionReader) Size() int64 { return s.limit - s.base } +// An OffsetWriter maps writers at offset base to offset base + off in the underlying writer. +type OffsetWriter struct { + w WriterAt + base int64 // the original offset + off int64 // the current offset +} + +// NewOffsetWriter returns a new OffsetWriter that writes to w starting at offset off. +func NewOffsetWriter(w WriterAt, off int64) *OffsetWriter { + return &OffsetWriter{w: w, off: off} +} + +func (o *OffsetWriter) Write(p []byte) (n int, err error) { + // n, err = o.w.WriterAt(p, o.off) + wa := o.w + n, err = wa.WriteAt(p, o.off) + o.off += int64(n) + return +} + +func (o *OffsetWriter) WriteAt(p []byte, off int64) (n int, err error) { + if off < 0 { + return 0, errOffset + } + + off += o.base + return o.w.WriteAt(p, off) +} + +func (o *OffsetWriter) Seek(offset int64, whence int) (int64, error) { + switch whence { + default: + return 0, errWhence + case SeekStart: + offset += o.base + case SeekCurrent: + offset += o.off + } + + if offset < o.base { + return 0, errOffset + } + + o.off = offset + return offset - o.base, nil +} + // TeeReader returns a Reader that writes to w what it reads from r. // All reads from r performed through it are matched with // corresponding writes to w. There is no internal buffering - @@ -614,7 +672,12 @@ func (discard) ReadFrom(r Reader) (n int64, err error) { // NopCloser returns a ReadCloser with a no-op Close method wrapping // the provided Reader r. +// If r implements WriterTo, the returned ReadCloser will implement WriterTo +// by forwarding calls to r. func NopCloser(r Reader) ReadCloser { + if _, ok := r.(WriterTo); ok { + return nopCloserWriterTo{r} + } return nopCloser{r} } @@ -624,6 +687,16 @@ type nopCloser struct { func (nopCloser) Close() error { return nil } +type nopCloserWriterTo struct { + Reader +} + +func (nopCloserWriterTo) Close() error { return nil } + +func (c nopCloserWriterTo) WriteTo(w Writer) (n int64, err error) { + return c.Reader.(WriterTo).WriteTo(w) +} + // ReadAll reads from r until an error or EOF and returns the data it read. // A successful call returns err == nil, not err == EOF. Because ReadAll is // defined to read from src until EOF, it does not treat an EOF from Read @@ -643,5 +716,9 @@ func ReadAll(r Reader) ([]byte, error) { } return b, err } + if len(b) == cap(b) { + // Add more capacity (let append pick how much). + b = append(b, 0)[:len(b)] + } } } diff --git a/gnovm/stdlibs/io/io_test.gno b/gnovm/stdlibs/io/io_test.gno index a97f6b8c075..613b7d13e35 100644 --- a/gnovm/stdlibs/io/io_test.gno +++ b/gnovm/stdlibs/io/io_test.gno @@ -1,14 +1,15 @@ +package io_test + // 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 io_test - import ( "bytes" "errors" "fmt" "io" + "os" "strings" "testing" ) @@ -459,3 +460,84 @@ func TestCopyLargeWriter(t *testing.T) { t.Errorf("Copy error: got %v, want %v", err, want) } } + +func TestNopCloserWriterToForwarding(t *testing.T) { + for _, tc := range [...]struct { + Name string + r io.Reader + }{ + {"not a WriterTo", io.Reader(nil)}, + {"a WriterTo", struct { + io.Reader + io.WriterTo + }{}}, + } { + nc := io.NopCloser(tc.r) + + _, expected := tc.r.(io.WriterTo) + _, got := nc.(io.WriterTo) + if expected != got { + t.Errorf("NopCloser incorrectly forwards WriterTo for %s, got %t want %t", tc.Name, got, expected) + } + } +} + +// XXX os.CreateTemp is not available for now +// func TestOffsetWriter_Seek(t *testing.T) { +// tmpfilename := "TestOffsetWriter_Seek" +// tmpfile, err := os.CreateTemp(t.TempDir(), tmpfilename) +// if err != nil || tmpfile == nil { +// t.Fatalf("CreateTemp(%s) failed: %v", tmpfilename, err) +// } +// defer tmpfile.Close() +// w := NewOffsetWriter(tmpfile, 0) + +// // Should throw error errWhence if whence is not valid +// t.Run("errWhence", func(t *testing.T) { +// for _, whence := range []int{-3, -2, -1, 3, 4, 5} { +// var offset int64 = 0 +// gotOff, gotErr := w.Seek(offset, whence) +// if gotOff != 0 || gotErr != ErrWhence { +// t.Errorf("For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, %v)", +// whence, offset, gotOff, gotErr, 0, ErrWhence) +// } +// } +// }) + +// // Should throw error errOffset if offset is negative +// t.Run("errOffset", func(t *testing.T) { +// for _, whence := range []int{SeekStart, SeekCurrent} { +// for offset := int64(-3); offset < 0; offset++ { +// gotOff, gotErr := w.Seek(offset, whence) +// if gotOff != 0 || gotErr != ErrOffset { +// t.Errorf("For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, %v)", +// whence, offset, gotOff, gotErr, 0, ErrOffset) +// } +// } +// } +// }) + +// // Normal tests +// t.Run("normal", func(t *testing.T) { +// tests := []struct { +// offset int64 +// whence int +// returnOff int64 +// }{ +// // keep in order +// {whence: SeekStart, offset: 1, returnOff: 1}, +// {whence: SeekStart, offset: 2, returnOff: 2}, +// {whence: SeekStart, offset: 3, returnOff: 3}, +// {whence: SeekCurrent, offset: 1, returnOff: 4}, +// {whence: SeekCurrent, offset: 2, returnOff: 6}, +// {whence: SeekCurrent, offset: 3, returnOff: 9}, +// } +// for idx, tt := range tests { +// gotOff, gotErr := w.Seek(tt.offset, tt.whence) +// if gotOff != tt.returnOff || gotErr != nil { +// t.Errorf("%d:: For whence %d, offset %d, OffsetWriter.Seek got: (%d, %v), want: (%d, )", +// idx+1, tt.whence, tt.offset, gotOff, gotErr, tt.returnOff) +// } +// } +// }) +// } diff --git a/gnovm/stdlibs/io/ioutil/ioutil.gno b/gnovm/stdlibs/io/ioutil/ioutil.gno deleted file mode 100644 index 935031c0511..00000000000 --- a/gnovm/stdlibs/io/ioutil/ioutil.gno +++ /dev/null @@ -1,83 +0,0 @@ -// 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 ioutil implements some I/O utility functions. -// -// As of Go 1.16, the same functionality is now provided -// by package io or package os, and those implementations -// should be preferred in new code. -// See the specific function documentation for details. -package ioutil - -import ( - "io" -) - -// ReadAll reads from r until an error or EOF and returns the data it read. -// A successful call returns err == nil, not err == EOF. Because ReadAll is -// defined to read from src until EOF, it does not treat an EOF from Read -// as an error to be reported. -// -// As of Go 1.16, this function simply calls io.ReadAll. -func ReadAll(r io.Reader) ([]byte, error) { - return io.ReadAll(r) -} - -/* XXX os and os/fs removed. -// ReadFile reads the file named by filename and returns the contents. -// A successful call returns err == nil, not err == EOF. Because ReadFile -// reads the whole file, it does not treat an EOF from Read as an error -// to be reported. -// -// As of Go 1.16, this function simply calls os.ReadFile. -func ReadFile(filename string) ([]byte, error) { - return os.ReadFile(filename) -} - -// WriteFile writes data to a file named by filename. -// If the file does not exist, WriteFile creates it with permissions perm -// (before umask); otherwise WriteFile truncates it before writing, without changing permissions. -// -// As of Go 1.16, this function simply calls os.WriteFile. -func WriteFile(filename string, data []byte, perm fs.FileMode) error { - return os.WriteFile(filename, data, perm) -} - -// ReadDir reads the directory named by dirname and returns -// a list of fs.FileInfo for the directory's contents, -// sorted by filename. If an error occurs reading the directory, -// ReadDir returns no directory entries along with the error. -// -// As of Go 1.16, os.ReadDir is a more efficient and correct choice: -// it returns a list of fs.DirEntry instead of fs.FileInfo, -// and it returns partial results in the case of an error -// midway through reading a directory. -func ReadDir(dirname string) ([]fs.FileInfo, error) { - f, err := os.Open(dirname) - if err != nil { - return nil, err - } - list, err := f.Readdir(-1) - f.Close() - if err != nil { - return nil, err - } - sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() }) - return list, nil -} -*/ - -// NopCloser returns a ReadCloser with a no-op Close method wrapping -// the provided Reader r. -// -// As of Go 1.16, this function simply calls io.NopCloser. -func NopCloser(r io.Reader) io.ReadCloser { - return io.NopCloser(r) -} - -// Discard is an io.Writer on which all Write calls succeed -// without doing anything. -// -// As of Go 1.16, this value is simply io.Discard. -var Discard io.Writer = io.Discard diff --git a/gnovm/stdlibs/io/multi_test.gno b/gnovm/stdlibs/io/multi_test.gno index ee800b3ec24..31345279318 100644 --- a/gnovm/stdlibs/io/multi_test.gno +++ b/gnovm/stdlibs/io/multi_test.gno @@ -1,9 +1,9 @@ +package io_test + // Copyright 2010 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 io_test - import ( "bytes" "crypto/sha1" diff --git a/gnovm/tests/backup/cli1.gno b/gnovm/tests/backup/cli1.gno index 8eb9e8c40d7..843eb461ac2 100644 --- a/gnovm/tests/backup/cli1.gno +++ b/gnovm/tests/backup/cli1.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net" "net/http" @@ -13,7 +13,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/cli2.gno b/gnovm/tests/backup/cli2.gno index a9e8e53be3a..0be01d2f1a9 100644 --- a/gnovm/tests/backup/cli2.gno +++ b/gnovm/tests/backup/cli2.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net" "net/http" @@ -21,7 +21,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/cli3.gno b/gnovm/tests/backup/cli3.gno index 1c696d4bc4c..50b11b9e4c3 100644 --- a/gnovm/tests/backup/cli3.gno +++ b/gnovm/tests/backup/cli3.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -13,7 +13,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/cli4.gno b/gnovm/tests/backup/cli4.gno index 147b63c3e6e..aab6405917c 100644 --- a/gnovm/tests/backup/cli4.gno +++ b/gnovm/tests/backup/cli4.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -40,7 +40,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/cli5.gno b/gnovm/tests/backup/cli5.gno index a2e1787c996..6b536841a6d 100644 --- a/gnovm/tests/backup/cli5.gno +++ b/gnovm/tests/backup/cli5.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -40,7 +40,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/cli6.gno b/gnovm/tests/backup/cli6.gno index 89ae9f8b98d..e97da82736e 100644 --- a/gnovm/tests/backup/cli6.gno +++ b/gnovm/tests/backup/cli6.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -41,7 +41,7 @@ func client(uri string) { if err != nil { log.Fatal(err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/backup/file_access.gno b/gnovm/tests/backup/file_access.gno index 6d750c44a36..e81cc6a0bee 100644 --- a/gnovm/tests/backup/file_access.gno +++ b/gnovm/tests/backup/file_access.gno @@ -2,12 +2,11 @@ package main import ( "fmt" - "io/ioutil" - "os" + "io" ) func main() { - file, err := ioutil.TempFile("", "yeagibench") + file, err := io.TempFile("", "yeagibench") if err != nil { panic(err) } diff --git a/gnovm/tests/backup/ioutil.gno b/gnovm/tests/backup/ioutil.gno deleted file mode 100644 index eae22b6ea5a..00000000000 --- a/gnovm/tests/backup/ioutil.gno +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -func main() { - _, err := os.ReadFile("__NotExisting__") - if err != nil { - fmt.Println(err.Error()) - } -} - -// Output: -// open __NotExisting__: no such file or directory diff --git a/gnovm/tests/backup/issue-558.gno b/gnovm/tests/backup/issue-558.gno index d36bb4e18a7..e99566f7634 100644 --- a/gnovm/tests/backup/issue-558.gno +++ b/gnovm/tests/backup/issue-558.gno @@ -4,7 +4,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "io" "log" "strings" ) @@ -37,7 +37,7 @@ type pipe struct { func newReadAutoCloser(r io.Reader) readAutoCloser { if _, ok := r.(io.Closer); !ok { - return readAutoCloser{ioutil.NopCloser(r)} + return readAutoCloser{io.NopCloser(r)} } return readAutoCloser{r.(io.ReadCloser)} } @@ -45,7 +45,7 @@ func newReadAutoCloser(r io.Reader) readAutoCloser { func main() { p := &pipe{} p.Reader = newReadAutoCloser(strings.NewReader("test")) - b, err := ioutil.ReadAll(p.Reader) + b, err := io.ReadAll(p.Reader) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/files/ioutil0.gno b/gnovm/tests/files/io2.gno similarity index 88% rename from gnovm/tests/files/ioutil0.gno rename to gnovm/tests/files/io2.gno index 800d237be22..24655f5040c 100644 --- a/gnovm/tests/files/ioutil0.gno +++ b/gnovm/tests/files/io2.gno @@ -2,7 +2,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "log" "strings" ) @@ -10,7 +10,7 @@ import ( func main() { r := strings.NewReader("Go is a general-purpose language designed with systems programming in mind.") - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { log.Fatal(err) } diff --git a/gnovm/tests/files/issue-558b.gno b/gnovm/tests/files/issue-558b.gno index 686c73b5c88..55eba88c985 100644 --- a/gnovm/tests/files/issue-558b.gno +++ b/gnovm/tests/files/issue-558b.gno @@ -3,7 +3,7 @@ package main import ( "fmt" "io" - "io/ioutil" + "io" "log" "strings" ) @@ -36,7 +36,7 @@ type pipe struct { func newReadAutoCloser(r io.Reader) readAutoCloser { if _, ok := r.(io.Closer); !ok { - return readAutoCloser{ioutil.NopCloser(r)} + return readAutoCloser{io.NopCloser(r)} } return readAutoCloser{r.(io.ReadCloser)} } diff --git a/gnovm/tests/imports.go b/gnovm/tests/imports.go index fc2820ce00e..0741d0b466a 100644 --- a/gnovm/tests/imports.go +++ b/gnovm/tests/imports.go @@ -20,7 +20,6 @@ import ( "image" "image/color" "io" - "io/ioutil" "log" "math" "math/big" @@ -356,16 +355,13 @@ func TestStore(rootDir, filesPath string, stdin io.Reader, stdout, stderr io.Wri case "io": pkg := gno.NewPackageNode("io", pkgPath, nil) pkg.DefineGoNativeValue("EOF", io.EOF) + pkg.DefineGoNativeValue("NopCloser", io.NopCloser) pkg.DefineGoNativeValue("ReadFull", io.ReadFull) + pkg.DefineGoNativeValue("ReadAll", io.ReadAll) pkg.DefineGoNativeType(reflect.TypeOf((*io.ReadCloser)(nil)).Elem()) pkg.DefineGoNativeType(reflect.TypeOf((*io.Closer)(nil)).Elem()) pkg.DefineGoNativeType(reflect.TypeOf((*io.Reader)(nil)).Elem()) return pkg, pkg.NewPackage() - case "io/ioutil": - pkg := gno.NewPackageNode("ioutil", pkgPath, nil) - pkg.DefineGoNativeValue("NopCloser", ioutil.NopCloser) - pkg.DefineGoNativeValue("ReadAll", ioutil.ReadAll) - return pkg, pkg.NewPackage() case "log": pkg := gno.NewPackageNode("log", pkgPath, nil) pkg.DefineGoNativeValue("Fatal", log.Fatal) diff --git a/tm2/pkg/amino/genproto/genproto.go b/tm2/pkg/amino/genproto/genproto.go index 8af73b2690a..4f7154e058c 100644 --- a/tm2/pkg/amino/genproto/genproto.go +++ b/tm2/pkg/amino/genproto/genproto.go @@ -6,7 +6,6 @@ import ( "bytes" "errors" "fmt" - "io/ioutil" "os" "os/exec" "path" @@ -120,7 +119,6 @@ func (p3c *P3Context) GetP3ImportPath(p3type P3Type, implicit bool) string { func (p3c *P3Context) GenerateProto3MessagePartial(p3doc *P3Doc, rt reflect.Type) (p3msg P3Message) { if p3doc.PackageName == "" { panic(fmt.Sprintf("cannot generate message partials in the root package \"\".")) - return } if rt.Kind() == reflect.Ptr { panic("pointers not yet supported. if you meant pointer-preferred (for decoding), pass in rt.Elem()") @@ -220,7 +218,6 @@ func (p3c *P3Context) GenerateProto3MessagePartial(p3doc *P3Doc, rt reflect.Type func (p3c *P3Context) GenerateProto3ListPartial(p3doc *P3Doc, nl NList) (p3msg P3Message) { if p3doc.PackageName == "" { panic(fmt.Sprintf("cannot generate message partials in the root package \"\".")) - return } ep3 := nl.ElemP3Type() @@ -496,7 +493,7 @@ func RunProtoc(pkg *amino.Package, protosDir string) { } } // First generate output to a temp dir. - tempDir, err := ioutil.TempDir("", "amino-genproto") + tempDir, err := os.MkdirTemp("", "amino-genproto") if err != nil { return } diff --git a/tm2/pkg/autofile/autofile_test.go b/tm2/pkg/autofile/autofile_test.go index d50bdca3ce0..d631e0ed265 100644 --- a/tm2/pkg/autofile/autofile_test.go +++ b/tm2/pkg/autofile/autofile_test.go @@ -1,7 +1,6 @@ package autofile import ( - "io/ioutil" "os" "syscall" "testing" @@ -14,7 +13,7 @@ import ( func TestSIGHUP(t *testing.T) { // First, create an AutoFile writing to a tempfile dir - file, err := ioutil.TempFile("", "sighup_test") + file, err := os.CreateTemp("", "sighup_test") require.NoError(t, err) err = file.Close() require.NoError(t, err) @@ -60,7 +59,7 @@ func TestSIGHUP(t *testing.T) { // // Manually modify file permissions, close, and reopen using autofile: // // We expect the file permissions to be changed back to the intended perms. // func TestOpenAutoFilePerms(t *testing.T) { -// file, err := ioutil.TempFile("", "permission_test") +// file, err := os.CreateTemp("", "permission_test") // require.NoError(t, err) // err = file.Close() // require.NoError(t, err) @@ -86,7 +85,7 @@ func TestSIGHUP(t *testing.T) { func TestAutoFileSize(t *testing.T) { // First, create an AutoFile writing to a tempfile dir - f, err := ioutil.TempFile("", "sighup_test") + f, err := os.CreateTemp("", "sighup_test") require.NoError(t, err) err = f.Close() require.NoError(t, err) diff --git a/tm2/pkg/bft/config/toml.go b/tm2/pkg/bft/config/toml.go index fdaa1295342..1599bc78968 100644 --- a/tm2/pkg/bft/config/toml.go +++ b/tm2/pkg/bft/config/toml.go @@ -3,7 +3,6 @@ package config import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" "text/template" @@ -308,7 +307,7 @@ func ResetTestRoot(testName string) *Config { func ResetTestRootWithChainID(testName string, chainID string) *Config { // create a unique, concurrency-safe test directory under os.TempDir() - rootDir, err := ioutil.TempDir("", fmt.Sprintf("%s-%s_", chainID, testName)) + rootDir, err := os.MkdirTemp("", fmt.Sprintf("%s-%s_", chainID, testName)) if err != nil { panic(err) } diff --git a/tm2/pkg/bft/consensus/common_test.go b/tm2/pkg/bft/consensus/common_test.go index 7424305c00a..19c96756624 100644 --- a/tm2/pkg/bft/consensus/common_test.go +++ b/tm2/pkg/bft/consensus/common_test.go @@ -3,7 +3,6 @@ package consensus import ( "bytes" "fmt" - "io/ioutil" "os" "path" "path/filepath" @@ -662,11 +661,11 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF if i < nValidators { privVal = privVals[i] } else { - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") if err != nil { panic(err) } - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") if err != nil { panic(err) } @@ -795,7 +794,7 @@ func newCounter() abci.Application { } func newPersistentKVStore() abci.Application { - dir, err := ioutil.TempDir("", "persistent-kvstore") + dir, err := os.MkdirTemp("", "persistent-kvstore") if err != nil { panic(err) } diff --git a/tm2/pkg/bft/consensus/replay_test.go b/tm2/pkg/bft/consensus/replay_test.go index 3174207ef8d..556689ec3f4 100644 --- a/tm2/pkg/bft/consensus/replay_test.go +++ b/tm2/pkg/bft/consensus/replay_test.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "path/filepath" "runtime" @@ -604,7 +603,7 @@ func TestMockProxyApp(t *testing.T) { } func tempWALWithData(data []byte) string { - walFile, err := ioutil.TempFile("", "wal") + walFile, err := os.CreateTemp("", "wal") if err != nil { panic(fmt.Sprintf("failed to create temp WAL file: %v", err)) } diff --git a/tm2/pkg/bft/privval/file_test.go b/tm2/pkg/bft/privval/file_test.go index 306db4177e5..36979e19ea3 100644 --- a/tm2/pkg/bft/privval/file_test.go +++ b/tm2/pkg/bft/privval/file_test.go @@ -3,7 +3,6 @@ package privval import ( "encoding/base64" "fmt" - "io/ioutil" "os" "testing" "time" @@ -20,9 +19,9 @@ import ( func TestGenLoadValidator(t *testing.T) { assert := assert.New(t) - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) @@ -38,9 +37,9 @@ func TestGenLoadValidator(t *testing.T) { } func TestResetValidator(t *testing.T) { - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) @@ -68,9 +67,9 @@ func TestResetValidator(t *testing.T) { func TestLoadOrGenValidator(t *testing.T) { assert := assert.New(t) - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) tempKeyFilePath := tempKeyFile.Name() @@ -160,9 +159,9 @@ func TestUnmarshalValidatorKey(t *testing.T) { func TestSignVote(t *testing.T) { assert := assert.New(t) - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) @@ -206,9 +205,9 @@ func TestSignVote(t *testing.T) { func TestSignProposal(t *testing.T) { assert := assert.New(t) - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) @@ -248,9 +247,9 @@ func TestSignProposal(t *testing.T) { } func TestDifferByTimestamp(t *testing.T) { - tempKeyFile, err := ioutil.TempFile("", "priv_validator_key_") + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) - tempStateFile, err := ioutil.TempFile("", "priv_validator_state_") + tempStateFile, err := os.CreateTemp("", "priv_validator_state_") require.Nil(t, err) privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) diff --git a/tm2/pkg/bft/privval/socket_listeners_test.go b/tm2/pkg/bft/privval/socket_listeners_test.go index a3630903197..6ca7863d1fd 100644 --- a/tm2/pkg/bft/privval/socket_listeners_test.go +++ b/tm2/pkg/bft/privval/socket_listeners_test.go @@ -1,7 +1,6 @@ package privval import ( - "io/ioutil" "net" "os" "testing" @@ -29,7 +28,7 @@ type listenerTestCase struct { // testUnixAddr will attempt to obtain a platform-independent temporary file // name for a Unix socket func testUnixAddr() (string, error) { - f, err := ioutil.TempFile("", "tendermint-privval-test-*") + f, err := os.CreateTemp("", "tendermint-privval-test-*") if err != nil { return "", err } diff --git a/tm2/pkg/bft/rpc/client/main_test.go b/tm2/pkg/bft/rpc/client/main_test.go index 58dd9538412..759104a3029 100644 --- a/tm2/pkg/bft/rpc/client/main_test.go +++ b/tm2/pkg/bft/rpc/client/main_test.go @@ -1,7 +1,6 @@ package client_test import ( - "io/ioutil" "os" "testing" @@ -14,7 +13,7 @@ var node *nm.Node func TestMain(m *testing.M) { // start a tendermint node (and kvstore) in the background to test against - dir, err := ioutil.TempDir("/tmp", "rpc-client-test") + dir, err := os.MkdirTemp("/tmp", "rpc-client-test") if err != nil { panic(err) } diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go index 5a9da9ec052..c02d029f27a 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io/ioutil" + "io" "net" "net/http" "net/url" @@ -201,7 +201,7 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul return nil, errors.New("server at '%s' returned %s", c.address, httpResponse.Status) } - responseBytes, err := ioutil.ReadAll(httpResponse.Body) + responseBytes, err := io.ReadAll(httpResponse.Body) if err != nil { return nil, err } @@ -238,7 +238,7 @@ func (c *JSONRPCClient) sendBatch(requests []*jsonRPCBufferedRequest) ([]interfa return nil, errors.New("server at '%s' returned %s", c.address, httpResponse.Status) } - responseBytes, err := ioutil.ReadAll(httpResponse.Body) + responseBytes, err := io.ReadAll(httpResponse.Body) if err != nil { return nil, err } @@ -332,7 +332,7 @@ func (c *URIClient) Call(method string, params map[string]interface{}, result in return nil, errors.New("server at '%s' returned %s", c.address, resp.Status) } - responseBytes, err := ioutil.ReadAll(resp.Body) + responseBytes, err := io.ReadAll(resp.Body) if err != nil { return nil, err } diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index aa3fadd4d18..40543e5f465 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -6,7 +6,7 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "reflect" "runtime/debug" @@ -101,7 +101,7 @@ func funcReturnTypes(f interface{}) []reflect.Type { // jsonrpc calls grab the given method's function info and runs reflect.Call func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) + b, err := io.ReadAll(r.Body) if err != nil { WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(types.JSONRPCStringID(""), errors.Wrap(err, "error reading request body"))) return diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index 8bf2cc0f7f1..b7a9445a82d 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -3,7 +3,7 @@ package rpcserver_test import ( "bytes" "encoding/json" - "io/ioutil" + "io" "net/http" "net/http/httptest" "strings" @@ -70,7 +70,7 @@ func TestRPCParams(t *testing.T) { res := rec.Result() // Always expecting back a JSONRPCResponse assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) - blob, err := ioutil.ReadAll(res.Body) + blob, err := io.ReadAll(res.Body) if err != nil { t.Errorf("#%d: err reading body: %v", i, err) continue @@ -118,7 +118,7 @@ func TestJSONRPCID(t *testing.T) { res := rec.Result() // Always expecting back a JSONRPCResponse assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) - blob, err := ioutil.ReadAll(res.Body) + blob, err := io.ReadAll(res.Body) if err != nil { t.Errorf("#%d: err reading body: %v", i, err) continue @@ -147,7 +147,7 @@ func TestRPCNotification(t *testing.T) { // Always expecting back a JSONRPCResponse require.True(t, statusOK(res.StatusCode), "should always return 2XX") - blob, err := ioutil.ReadAll(res.Body) + blob, err := io.ReadAll(res.Body) require.Nil(t, err, "reading from the body should not give back an error") require.Equal(t, len(blob), 0, "a notification SHOULD NOT be responded to by the server") } @@ -182,7 +182,7 @@ func TestRPCNotificationInBatch(t *testing.T) { res := rec.Result() // Always expecting back a JSONRPCResponse assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) - blob, err := ioutil.ReadAll(res.Body) + blob, err := io.ReadAll(res.Body) if err != nil { t.Errorf("#%d: err reading body: %v", i, err) continue diff --git a/tm2/pkg/bft/rpc/lib/server/http_server_test.go b/tm2/pkg/bft/rpc/lib/server/http_server_test.go index 6753a339981..aaf817e6d85 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server_test.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server_test.go @@ -5,7 +5,6 @@ import ( "crypto/tls" "fmt" "io" - "io/ioutil" "net" "net/http" "net/http/httptest" @@ -58,7 +57,7 @@ func TestMaxOpenConnections(t *testing.T) { return } defer r.Body.Close() - io.Copy(ioutil.Discard, r.Body) + io.Copy(io.Discard, r.Body) }() } wg.Wait() @@ -91,7 +90,7 @@ func TestStartHTTPAndTLSServer(t *testing.T) { defer res.Body.Close() assert.Equal(t, http.StatusOK, res.StatusCode) - body, err := ioutil.ReadAll(res.Body) + body, err := io.ReadAll(res.Body) require.NoError(t, err) assert.Equal(t, []byte("some body"), body) } diff --git a/tm2/pkg/bft/types/genesis_test.go b/tm2/pkg/bft/types/genesis_test.go index a8816bed2e7..f8d19324eb5 100644 --- a/tm2/pkg/bft/types/genesis_test.go +++ b/tm2/pkg/bft/types/genesis_test.go @@ -1,7 +1,6 @@ package types import ( - "io/ioutil" "os" "testing" "time" @@ -88,7 +87,7 @@ func TestGenesisGood(t *testing.T) { } func TestGenesisSaveAs(t *testing.T) { - tmpfile, err := ioutil.TempFile("", "genesis") + tmpfile, err := os.CreateTemp("", "genesis") require.NoError(t, err) defer os.Remove(tmpfile.Name()) diff --git a/tm2/pkg/bft/types/part_set_test.go b/tm2/pkg/bft/types/part_set_test.go index 2e05daed849..c4e30c89372 100644 --- a/tm2/pkg/bft/types/part_set_test.go +++ b/tm2/pkg/bft/types/part_set_test.go @@ -1,7 +1,7 @@ package types import ( - "io/ioutil" + "io" "testing" "github.com/stretchr/testify/assert" @@ -54,7 +54,7 @@ func TestBasicPartSet(t *testing.T) { // Reconstruct data, assert that they are equal. data2Reader := partSet2.GetReader() - data2, err := ioutil.ReadAll(data2Reader) + data2, err := io.ReadAll(data2Reader) require.NoError(t, err) assert.Equal(t, data, data2) diff --git a/tm2/pkg/db/fsdb.go b/tm2/pkg/db/fsdb.go index aa8cea83889..e11cd6d4ce5 100644 --- a/tm2/pkg/db/fsdb.go +++ b/tm2/pkg/db/fsdb.go @@ -2,7 +2,7 @@ package db import ( "fmt" - "io/ioutil" + "io" "net/url" "os" "path/filepath" @@ -189,7 +189,7 @@ func read(path string) ([]byte, error) { } defer f.Close() - d, err := ioutil.ReadAll(f) + d, err := io.ReadAll(f) if err != nil { return nil, err } diff --git a/tm2/pkg/iavl/tree_dotgraph_test.go b/tm2/pkg/iavl/tree_dotgraph_test.go index 29be03ca241..f3c2786cda3 100644 --- a/tm2/pkg/iavl/tree_dotgraph_test.go +++ b/tm2/pkg/iavl/tree_dotgraph_test.go @@ -1,7 +1,7 @@ package iavl import ( - "io/ioutil" + "io" "testing" db "github.com/gnolang/gno/tm2/pkg/db" @@ -15,5 +15,5 @@ func TestWriteDOTGraph(t *testing.T) { key := []byte{ikey} tree.Set(key, key) } - WriteDOTGraph(ioutil.Discard, tree.ImmutableTree, []PathToLeaf{}) + WriteDOTGraph(io.Discard, tree.ImmutableTree, []PathToLeaf{}) } diff --git a/tm2/pkg/log/tm_logger_test.go b/tm2/pkg/log/tm_logger_test.go index a6d56bb8feb..81614c5ea21 100644 --- a/tm2/pkg/log/tm_logger_test.go +++ b/tm2/pkg/log/tm_logger_test.go @@ -2,7 +2,7 @@ package log_test import ( "bytes" - "io/ioutil" + "io" "strings" "testing" @@ -21,11 +21,11 @@ func TestLoggerLogsItsErrors(t *testing.T) { } func BenchmarkTMLoggerSimple(b *testing.B) { - benchmarkRunner(b, log.NewTMLogger(ioutil.Discard), baseInfoMessage) + benchmarkRunner(b, log.NewTMLogger(io.Discard), baseInfoMessage) } func BenchmarkTMLoggerContextual(b *testing.B) { - benchmarkRunner(b, log.NewTMLogger(ioutil.Discard), withInfoMessage) + benchmarkRunner(b, log.NewTMLogger(io.Discard), withInfoMessage) } func benchmarkRunner(b *testing.B, logger log.Logger, f func(log.Logger)) { diff --git a/tm2/pkg/os/tempfile_test.go b/tm2/pkg/os/tempfile_test.go index 12d6abb27cb..ec294d58e30 100644 --- a/tm2/pkg/os/tempfile_test.go +++ b/tm2/pkg/os/tempfile_test.go @@ -5,7 +5,6 @@ package os import ( "bytes" "fmt" - "io/ioutil" "os" "testing" @@ -21,7 +20,7 @@ func TestWriteFileAtomic(t *testing.T) { perm os.FileMode = 0o600 ) - f, err := ioutil.TempFile("/tmp", "write-atomic-test-") + f, err := os.CreateTemp("/tmp", "write-atomic-test-") if err != nil { t.Fatal(err) } diff --git a/tm2/pkg/p2p/upnp/upnp.go b/tm2/pkg/p2p/upnp/upnp.go index 40f2067e232..cd47ac35553 100644 --- a/tm2/pkg/p2p/upnp/upnp.go +++ b/tm2/pkg/p2p/upnp/upnp.go @@ -10,7 +10,7 @@ import ( "encoding/xml" "errors" "fmt" - "io/ioutil" + "io" "net" "net/http" "strconv" @@ -306,7 +306,7 @@ func (n *upnpNAT) getExternalIPAddress() (info statusInfo, err error) { return } var envelope Envelope - data, err := ioutil.ReadAll(response.Body) + data, err := io.ReadAll(response.Body) if err != nil { return } @@ -363,7 +363,7 @@ func (n *upnpNAT) AddPortMapping(protocol string, externalPort, internalPort int // TODO: check response to see if the port was forwarded // log.Println(message, response) // JAE: - // body, err := ioutil.ReadAll(response.Body) + // body, err := io.ReadAll(response.Body) // fmt.Println(string(body), err) mappedExternalPort = externalPort _ = response From 31d2ce95845c8e8c286e703f589447a5476d8b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Wed, 8 Nov 2023 09:15:04 +0100 Subject: [PATCH 65/93] feat: add `unused` linter (#1294) ## Description This PR adds the `unused` linter to the linter configuration, and removes unused code segments
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] 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).
--- .github/golangci.yml | 27 ++- gnovm/pkg/gnolang/helpers.go | 12 -- gnovm/pkg/gnolang/preprocess.go | 24 --- gnovm/pkg/gnolang/scanner.go | 18 -- gnovm/pkg/gnolang/store.go | 8 +- gnovm/pkg/gnolang/types.go | 1 - gnovm/stdlibs/stdlibs.go | 10 - misc/logos/buffer.go | 12 +- misc/logos/cmd/logos.go | 5 - misc/logos/debug.go | 26 --- misc/logos/types.go | 14 +- misc/logos/unicode.go | 11 +- tm2/pkg/amino/binary_decode.go | 8 - tm2/pkg/amino/codec_test.go | 3 - tm2/pkg/amino/genproto/bindings.go | 117 +---------- tm2/pkg/amino/genproto/example/main.go | 4 - tm2/pkg/amino/genproto/scanner.go | 192 ------------------ tm2/pkg/amino/reflect.go | 31 +-- tm2/pkg/amino/tests/common.go | 2 - tm2/pkg/bft/abci/client/client.go | 9 +- tm2/pkg/bft/consensus/common_test.go | 10 - tm2/pkg/bft/consensus/replay.go | 3 - tm2/pkg/bft/consensus/state_test.go | 14 +- tm2/pkg/bft/consensus/wal_generator.go | 4 +- tm2/pkg/bft/consensus/wal_test.go | 7 +- tm2/pkg/bft/node/node.go | 1 - .../privval/signer_listener_endpoint_test.go | 2 +- tm2/pkg/bft/rpc/core/pipe.go | 15 -- tm2/pkg/bft/rpc/lib/client/ws_client.go | 2 +- tm2/pkg/bft/rpc/lib/server/handlers_test.go | 6 +- tm2/pkg/bft/state/helpers_test.go | 20 +- tm2/pkg/bft/types/validator_set.go | 10 +- tm2/pkg/bft/wal/wal.go | 4 +- tm2/pkg/bft/wal/wal_test.go | 2 +- tm2/pkg/crypto/keys/client/export_test.go | 8 +- tm2/pkg/crypto/keys/keybase.go | 8 - tm2/pkg/crypto/merkle/proof_test.go | 15 +- tm2/pkg/crypto/util.go | 7 - tm2/pkg/events/store.go | 23 +-- tm2/pkg/iavl/benchmarks/bench_test.go | 30 --- tm2/pkg/iavl/node.go | 4 - tm2/pkg/iavl/nodedb.go | 2 +- tm2/pkg/iavl/proof_path.go | 57 ------ tm2/pkg/iavl/proof_range.go | 4 +- tm2/pkg/iavl/tree_test.go | 2 +- tm2/pkg/p2p/config/config.go | 4 +- tm2/pkg/p2p/switch.go | 4 - tm2/pkg/sdk/auth/test_common.go | 27 --- tm2/pkg/sdk/baseapp_test.go | 6 +- tm2/pkg/std/coin.go | 2 - tm2/pkg/store/gas/store_test.go | 6 - tm2/pkg/store/rootmulti/dbadapter.go | 33 --- 52 files changed, 89 insertions(+), 787 deletions(-) delete mode 100644 misc/logos/debug.go delete mode 100644 tm2/pkg/amino/genproto/scanner.go delete mode 100644 tm2/pkg/crypto/util.go delete mode 100644 tm2/pkg/store/rootmulti/dbadapter.go diff --git a/.github/golangci.yml b/.github/golangci.yml index 992398956b1..53141a3143d 100644 --- a/.github/golangci.yml +++ b/.github/golangci.yml @@ -1,9 +1,20 @@ run: - timeout: 5m + concurrency: 8 + timeout: 10m + issue-exit-code: 1 tests: true skip-dirs-use-default: true + modules-download-mode: readonly + allow-parallel-runners: false + go: "" + +output: + uniq-by-line: false + path-prefix: "" + sort-results: true linters: + fast: false disable-all: true enable: - whitespace # Tool for detection of leading and trailing whitespace @@ -21,12 +32,11 @@ linters: - gofmt # Whether the code was gofmt-ed - goimports # Unused imports - goconst # Repeated strings that could be replaced by a constant - #- forcetypeassert # Finds forced type assertions - dogsled # Checks assignments with too many blank identifiers (e.g. x, , , _, := f()) - #- dupl # Code clone detection - errname # Checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13 - gofumpt # Stricter gofmt + - unused # Checks Go code for unused constants, variables, functions and types linters-settings: gofmt: @@ -42,9 +52,20 @@ linters-settings: checks: [ "all", "-ST1022", "-ST1003" ] errorlint: asserts: false + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style issues: whole-files: true + max-issues-per-linter: 0 + max-same-issues: 0 + new: false + fix: false exclude-rules: - path: _test\.go linters: diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go index c16ea795ea3..4810a67304a 100644 --- a/gnovm/pkg/gnolang/helpers.go +++ b/gnovm/pkg/gnolang/helpers.go @@ -440,18 +440,6 @@ var ( precs = [][]string{prec1, prec2, prec3, prec4, prec5} ) -// 0 for prec1... -1 if no match. -func lowestMatch(op string) int { - for i, prec := range precs { - for _, op2 := range prec { - if op == op2 { - return i - } - } - } - return -1 -} - func Ss(b ...Stmt) []Stmt { return b } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index e02a158fcf1..1a1edc41222 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3338,23 +3338,6 @@ func elideCompositeExpr(vx *Expr, vt Type) { } } -// returns true of x is exactly `nil`. -func isNilExpr(x Expr) bool { - if nx, ok := x.(*NameExpr); ok { - return nx.Name == nilStr - } - return false -} - -func isNilComparableKind(k Kind) bool { - switch k { - case SliceKind, MapKind, FuncKind: - return true - default: - return false - } -} - // returns number of args, or if arg is a call result, // the number of results of the return tuple type. func countNumArgs(store Store, last BlockNode, n *CallExpr) (numArgs int) { @@ -3374,13 +3357,6 @@ func countNumArgs(store Store, last BlockNode, n *CallExpr) (numArgs int) { } } -func mergeNames(a, b []Name) []Name { - c := make([]Name, len(a)+len(b)) - copy(c, a) - copy(c[len(a):], b) - return c -} - // This is to be run *after* preprocessing is done, // to determine the order of var decl execution // (which may include functions which may refer to package vars). diff --git a/gnovm/pkg/gnolang/scanner.go b/gnovm/pkg/gnolang/scanner.go index 87695c0f346..f0b3f34c9ad 100644 --- a/gnovm/pkg/gnolang/scanner.go +++ b/gnovm/pkg/gnolang/scanner.go @@ -192,21 +192,3 @@ func (ss *scanner) advanceEscapeSequence() bool { return ss.done() } } - -// pops the next monoid term. -// The result is a string enclosed in balanced parentheses, -// brackets, or quotes; or what comes before such things. -// scanner doesn't understand operators, so a polynomial -// expression could be a single monoid as far as this scanner -// is concerned. TODO Chop functions should maybe use this. -func (ss *scanner) popMonoid() string { - startOut := ss.out() - start := ss.idx - for !ss.advance() { - if ss.out() != startOut { - end := ss.idx - return string(ss.rnz[start:end]) - } - } - panic("no monoid") -} diff --git a/gnovm/pkg/gnolang/store.go b/gnovm/pkg/gnolang/store.go index d3628edf216..24aff4936f3 100644 --- a/gnovm/pkg/gnolang/store.go +++ b/gnovm/pkg/gnolang/store.go @@ -11,8 +11,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/store" ) -const iavlCacheSize = 1024 * 1024 // TODO increase and parameterize. - // return nil if package doesn't exist. type PackageGetter func(pkgPath string) (*PackageNode, *PackageValue) @@ -626,7 +624,7 @@ func (ds *defaultStore) Flush() { // XXX } -//---------------------------------------- +// ---------------------------------------- // StoreOp type StoreOpType uint8 @@ -723,7 +721,7 @@ func (ds *defaultStore) Print() { } } -//---------------------------------------- +// ---------------------------------------- // backend keys func backendObjectKey(oid ObjectID) string { @@ -755,7 +753,7 @@ func backendPackagePathKey(path string) string { return fmt.Sprintf("pkg:" + path) } -//---------------------------------------- +// ---------------------------------------- // builtin types and packages func InitStoreCaches(store Store) { diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index c3e439e9427..6aed71fcf9b 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -879,7 +879,6 @@ type InterfaceType struct { } // General empty interface. -var gEmptyInterfaceType *InterfaceType = &InterfaceType{} func (it *InterfaceType) IsEmptyInterface() bool { return len(it.Methods) == 0 diff --git a/gnovm/stdlibs/stdlibs.go b/gnovm/stdlibs/stdlibs.go index fb230a0cf86..8931266eb9a 100644 --- a/gnovm/stdlibs/stdlibs.go +++ b/gnovm/stdlibs/stdlibs.go @@ -581,13 +581,3 @@ func typedByteArray(ln int, bz *gno.ArrayValue) gno.TypedValue { tv := gno.TypedValue{T: &gno.ArrayType{Len: ln, Elt: gno.Uint8Type}, V: bz} return tv } - -func typedByteSlice(bz *gno.SliceValue) gno.TypedValue { - tv := gno.TypedValue{T: &gno.SliceType{Elt: gno.Uint8Type}, V: bz} - return tv -} - -func typedNil(t gno.Type) gno.TypedValue { - tv := gno.TypedValue{T: t, V: nil} - return tv -} diff --git a/misc/logos/buffer.go b/misc/logos/buffer.go index 90665f79bfd..81e8d1abc75 100644 --- a/misc/logos/buffer.go +++ b/misc/logos/buffer.go @@ -7,7 +7,7 @@ import ( "github.com/gdamore/tcell/v2" ) -//---------------------------------------- +// ---------------------------------------- // Buffer // A Buffer is a buffer area in which to draw. @@ -82,7 +82,7 @@ func (bb *Buffer) DrawToScreen(s tcell.Screen) { } } -//---------------------------------------- +// ---------------------------------------- // Cell // A terminal character cell. @@ -131,10 +131,6 @@ var gDefaultSpaceTStyle = tcell.StyleDefault. Dim(true). Background(tcell.ColorGray) -var gDefaultTStyle = gDefaultStyle.GetTStyle(). - Foreground(gDefaultForeground). - Background(gDefaultBackground) - // This is where a bit of dynamic logic is performed, // namely where the attr is used to derive the final style. func (cc *Cell) GetTCellContent() (mainc rune, combc []rune, tstyle tcell.Style) { @@ -161,7 +157,7 @@ func (cc *Cell) GetTCellContent() (mainc rune, combc []rune, tstyle tcell.Style) return } -//---------------------------------------- +// ---------------------------------------- // View // analogy: "Buffer:View :: array:slice". @@ -205,7 +201,7 @@ func (bs View) GetCell(x, y int) *Cell { ) } -//---------------------------------------- +// ---------------------------------------- // BufferedView // A view onto an element. diff --git a/misc/logos/cmd/logos.go b/misc/logos/cmd/logos.go index 228895f852d..3a374fecba2 100644 --- a/misc/logos/cmd/logos.go +++ b/misc/logos/cmd/logos.go @@ -10,11 +10,6 @@ import ( "github.com/gnolang/gno/misc/logos" ) -var ( - row = 0 - style = tcell.StyleDefault -) - func main() { encoding.Register() diff --git a/misc/logos/debug.go b/misc/logos/debug.go deleted file mode 100644 index 8b2a4692bf4..00000000000 --- a/misc/logos/debug.go +++ /dev/null @@ -1,26 +0,0 @@ -package logos - -import ( - "fmt" -) - -// NOTE: the golang compiler doesn't seem to be intelligent -// enough to remove steps when const debug is True, -// so it is still faster to first check the truth value -// before calling debug.Println or debug.Printf. - -const debug debugging = false // or flip - -type debugging bool - -func (d debugging) Println(args ...interface{}) { - if d { - fmt.Println(append([]interface{}{"DEBUG:"}, args...)...) - } -} - -func (d debugging) Printf(format string, args ...interface{}) { - if d { - fmt.Printf("DEBUG: "+format, args...) - } -} diff --git a/misc/logos/types.go b/misc/logos/types.go index 944f02515d3..96e983992eb 100644 --- a/misc/logos/types.go +++ b/misc/logos/types.go @@ -7,7 +7,7 @@ import ( "github.com/gdamore/tcell/v2" ) -//---------------------------------------- +// ---------------------------------------- // Page // A Page has renderable Elem(ents). @@ -389,7 +389,7 @@ func (pg *Page) DecCursor(isVertical bool) { } } -//---------------------------------------- +// ---------------------------------------- // TextElem type TextElem struct { @@ -474,8 +474,6 @@ func (tel *TextElem) Render() (updated bool) { return true } -var ctr = 0 - func (tel *TextElem) Draw(offset Coord, view View) { minX, maxX, minY, maxY := computeIntersection(tel.Size, offset, view.Bounds) for y := minY; y < maxY; y++ { @@ -494,7 +492,7 @@ func (tel *TextElem) ProcessEventKey(ev *EventKey) bool { return false // TODO: clipboard. } -//---------------------------------------- +// ---------------------------------------- // misc. type Color = tcell.Color @@ -728,7 +726,7 @@ func (tt *Attrs) Merge(ot *Attrs) { tt.Other = ot.Other // TODO merge by key. } -//---------------------------------------- +// ---------------------------------------- // AttrFlags // NOTE: AttrFlags are merged with a simple or-assign op. @@ -752,7 +750,7 @@ type KVPair struct { Value interface{} } -//---------------------------------------- +// ---------------------------------------- // computeIntersection() // els: element size @@ -812,7 +810,7 @@ func computeIntersection(els Size, elo Coord, vws Size) (minX, maxX, minY, maxY return } -//---------------------------------------- +// ---------------------------------------- // Misc simple types type Padding struct { diff --git a/misc/logos/unicode.go b/misc/logos/unicode.go index 3bdb46cd88b..924edecc2c5 100644 --- a/misc/logos/unicode.go +++ b/misc/logos/unicode.go @@ -4,7 +4,7 @@ func isCombining(r rune) bool { return inTable(r, combining) } -//---------------------------------------- +// ---------------------------------------- // from https://github.com/mattn/go-runewidth // runewidth doesn't expose whether a character is combining or not. // TODO might as well fork both runewidth and tcell. @@ -62,15 +62,6 @@ type interval struct { type table []interval -func inTables(r rune, ts ...table) bool { - for _, t := range ts { - if inTable(r, t) { - return true - } - } - return false -} - func inTable(r rune, t table) bool { if r < t[0].first { return false diff --git a/tm2/pkg/amino/binary_decode.go b/tm2/pkg/amino/binary_decode.go index 8ac4161cede..333994d60b0 100644 --- a/tm2/pkg/amino/binary_decode.go +++ b/tm2/pkg/amino/binary_decode.go @@ -12,14 +12,6 @@ const bdOptionByte = 0x01 // ---------------------------------------- // cdc.decodeReflectBinary -var ErrOverflowInt = errors.New("encoded integer value overflows int(32)") - -const ( - // architecture dependent int limits: - maxInt = int(^uint(0) >> 1) - minInt = -maxInt - 1 -) - // This is the main entrypoint for decoding all types from binary form. This // function calls decodeReflectBinary*, and generally those functions should // only call this one, for overrides all happen here. diff --git a/tm2/pkg/amino/codec_test.go b/tm2/pkg/amino/codec_test.go index 7e025f73271..18bc8f2cd5d 100644 --- a/tm2/pkg/amino/codec_test.go +++ b/tm2/pkg/amino/codec_test.go @@ -214,9 +214,6 @@ func TestEncodeDecodeString(t *testing.T) { } func TestCodecSeal(t *testing.T) { - type Foo interface{} - type Bar interface{} - cdc := amino.NewCodec() cdc.Seal() diff --git a/tm2/pkg/amino/genproto/bindings.go b/tm2/pkg/amino/genproto/bindings.go index 458d8b14578..5d587870a7d 100644 --- a/tm2/pkg/amino/genproto/bindings.go +++ b/tm2/pkg/amino/genproto/bindings.go @@ -105,7 +105,7 @@ func generateMethodsForType(imports *ast.GenDecl, scope *ast.Scope, pkg *amino.P } dpbote_ := pbote_[1:] - ////////////////// + // ----------- // ToPBMessage() { scope2 := ast.NewScope(scope) @@ -127,7 +127,7 @@ func generateMethodsForType(imports *ast.GenDecl, scope *ast.Scope, pkg *amino.P )) } - ////////////////// + // ----------- // EmptyPBMessage() // Use to create the pbm to proto.Unmarshal to before FromPBMessage. { @@ -148,7 +148,7 @@ func generateMethodsForType(imports *ast.GenDecl, scope *ast.Scope, pkg *amino.P )) } - ////////////////// + // ----------- // FromPBMessage() { scope2 := ast.NewScope(scope) @@ -169,7 +169,7 @@ func generateMethodsForType(imports *ast.GenDecl, scope *ast.Scope, pkg *amino.P )) } - ////////////////// + // ----------- // TypeUrl() { methods = append(methods, _func("GetTypeURL", @@ -182,7 +182,7 @@ func generateMethodsForType(imports *ast.GenDecl, scope *ast.Scope, pkg *amino.P )) } - ////////////////// + // ----------- // Is*ReprEmpty() { rinfo := info.ReprType @@ -965,7 +965,7 @@ func isReprEmptyStmts(rootPkg *amino.Package, isRoot bool, imports *ast.GenDecl, return b } -//---------------------------------------- +// ---------------------------------------- // other.... // Splits a Go expression into left and right parts. @@ -1013,7 +1013,7 @@ func chopRight(expr string) (left string, tok rune, right string) { return } -//---------------------------------------- +// ---------------------------------------- // AST Construction (Expr) func _i(name string) *ast.Ident { @@ -1023,14 +1023,6 @@ func _i(name string) *ast.Ident { return &ast.Ident{Name: name} } -func _iOrNil(name string) *ast.Ident { - if name == "" { - return nil - } else { - return _i(name) - } -} - // recvTypeName is empty if there are no receivers. // recvTypeName cannot contain any dots. func _func(name string, recvRef string, recvTypeName string, params *ast.FieldList, results *ast.FieldList, b *ast.BlockStmt) *ast.FuncDecl { @@ -1344,29 +1336,6 @@ func _x(expr string, args ...interface{}) ast.Expr { // 3 == != < <= > >= // 2 && // 1 || -var sp = " " - -var ( - prec5 = strings.Split("* / % << >> & &^", sp) - prec4 = strings.Split("+ - | ^", sp) - prec3 = strings.Split("== != < <= > >=", sp) - prec2 = strings.Split("&&", sp) - prec1 = strings.Split("||", sp) - precs = [][]string{prec1, prec2, prec3, prec4, prec5} -) - -// 0 for prec1... -1 if no match. -func lowestMatch(op string) int { - for i, prec := range precs { - for _, op2 := range prec { - if op == op2 { - return i - } - } - } - return -1 -} - func _kv(k, v interface{}) *ast.KeyValueExpr { var kx, vx ast.Expr if ks, ok := k.(string); ok { @@ -1391,10 +1360,6 @@ func _block(b ...ast.Stmt) *ast.BlockStmt { } } -func _xs(exprs ...ast.Expr) []ast.Expr { - return exprs -} - // Usage: _a(lhs1, lhs2, ..., ":=", rhs1, rhs2, ...) // Token can be ":=", "=", "+=", etc. // Other strings are automatically parsed as _x(arg). @@ -1470,13 +1435,6 @@ func _call(fn ast.Expr, args ...ast.Expr) *ast.CallExpr { } } -func _ta(x ast.Expr, t ast.Expr) *ast.TypeAssertExpr { - return &ast.TypeAssertExpr{ - X: x, - Type: t, - } -} - func _sel(x ast.Expr, sel string) *ast.SelectorExpr { return &ast.SelectorExpr{ X: x, @@ -1532,7 +1490,7 @@ func _sl(x ast.Expr) *ast.ArrayType { } } -//---------------------------------------- +// ---------------------------------------- // AST Construction (Stmt) func _if(cond ast.Expr, b ...ast.Stmt) *ast.IfStmt { @@ -1564,34 +1522,6 @@ func _return(results ...ast.Expr) *ast.ReturnStmt { } } -func _continue(label string) *ast.BranchStmt { - return &ast.BranchStmt{ - Tok: token.CONTINUE, - Label: _i(label), - } -} - -func _break(label string) *ast.BranchStmt { - return &ast.BranchStmt{ - Tok: token.BREAK, - Label: _i(label), - } -} - -func _goto(label string) *ast.BranchStmt { - return &ast.BranchStmt{ - Tok: token.GOTO, - Label: _i(label), - } -} - -func _fallthrough(label string) *ast.BranchStmt { - return &ast.BranchStmt{ - Tok: token.FALLTHROUGH, - Label: _i(label), - } -} - // even/odd args are paired, // name1, path1, name2, path2, etc. func _imports(nameAndPaths ...string) *ast.GenDecl { @@ -1618,15 +1548,6 @@ func _for(init ast.Stmt, cond ast.Expr, post ast.Stmt, b ...ast.Stmt) *ast.ForSt } } -func _loop(b ...ast.Stmt) *ast.ForStmt { - return _for(nil, nil, nil, b...) -} - -func _once(b ...ast.Stmt) *ast.ForStmt { - b = append(b, _break("")) - return _for(nil, nil, nil, b...) -} - func _len(x ast.Expr) *ast.CallExpr { return _call(_i("len"), x) } @@ -1766,7 +1687,7 @@ func _aop(op string) token.Token { } } -//---------------------------------------- +// ---------------------------------------- // AST Compile-Time func _ctif(cond bool, then_, else_ ast.Stmt) ast.Stmt { @@ -1779,7 +1700,7 @@ func _ctif(cond bool, then_, else_ ast.Stmt) ast.Stmt { } } -//---------------------------------------- +// ---------------------------------------- // AST query and manipulation. func importPathForName(name string, imports *ast.GenDecl) (path string, exists bool) { @@ -1800,24 +1721,6 @@ func importPathForName(name string, imports *ast.GenDecl) (path string, exists b return "", false } -func importNameForPath(path string, imports *ast.GenDecl) (name string, exists bool) { - if imports.Tok != token.IMPORT { - panic("unexpected ast.GenDecl token " + imports.Tok.String()) - } - for _, spec := range imports.Specs { - if ispec, ok := spec.(*ast.ImportSpec); ok { - specPath, err := strconv.Unquote(ispec.Path.Value) - if err != nil { - panic("malformed path " + ispec.Path.Value) - } - if specPath == path { - return ispec.Name.Name, true - } - } - } - return "", false -} - func rootScope(scope *ast.Scope) *ast.Scope { for scope.Outer != nil { scope = scope.Outer diff --git a/tm2/pkg/amino/genproto/example/main.go b/tm2/pkg/amino/genproto/example/main.go index 24132305893..6e4023b596d 100644 --- a/tm2/pkg/amino/genproto/example/main.go +++ b/tm2/pkg/amino/genproto/example/main.go @@ -11,16 +11,12 @@ import ( // amino type StructA struct { - fieldA int - fieldB int FieldC int FieldD uint32 } // amino type StructB struct { - fieldA int - fieldB int FieldC int FieldD uint32 FieldE submodule.StructSM diff --git a/tm2/pkg/amino/genproto/scanner.go b/tm2/pkg/amino/genproto/scanner.go deleted file mode 100644 index 6c5f770ab48..00000000000 --- a/tm2/pkg/amino/genproto/scanner.go +++ /dev/null @@ -1,192 +0,0 @@ -package genproto - -import "fmt" - -type runestate int - -const ( - runestateCode runestate = 0 - runestateRune runestate = 1 - runestateStringQuote runestate = 2 - runestateStringBacktick runestate = 3 -) - -type scanner struct { - str string - rnz []rune - idx int - runestate - curly int - round int - square int -} - -// returns a new scanner. -func newScanner(str string) *scanner { - rnz := make([]rune, 0, len(str)) - for _, r := range str { - rnz = append(rnz, r) - } - return &scanner{ - str: str, - runestate: runestateCode, - rnz: rnz, - } -} - -// Peeks the next n runes and returns a string. returns a shorter string if -// there are less than n runes left. -func (ss *scanner) peek(n int) string { - if ss.idx+n > len(ss.rnz) { - return string(ss.rnz[ss.idx:len(ss.rnz)]) - } - return string(ss.rnz[ss.idx : ss.idx+n]) -} - -// Advance a single rune, e.g. by incrementing ss.curly if ss.rnz[ss.idx] is -// '{' before advancing. If ss.runestate is runestateRune or runestateQuote, -// advances escape sequences to completion so ss.idx may increment more than -// one. Returns true if done. -func (ss *scanner) advance() bool { - rn := ss.rnz[ss.idx] // just panic if out of scope, caller error. - switch ss.runestate { - case runestateCode: - switch rn { - case '}': - ss.curly-- - if ss.curly < 0 { - panic("mismatched curly: " + ss.str) - } - case ')': - ss.round-- - if ss.round < 0 { - panic("mismatched round: " + ss.str) - } - case ']': - ss.square-- - if ss.square < 0 { - panic("mismatched square: " + ss.str) - } - case '{': - ss.curly++ - case '(': - ss.round++ - case '[': - ss.square++ - case '\'': - ss.runestate = runestateRune - case '"': - ss.runestate = runestateStringQuote - case '`': - ss.runestate = runestateStringBacktick - } - case runestateRune: - switch rn { - case '\\': - return ss.advanceEscapeSequence() - case '\'': - ss.runestate = runestateCode - } - case runestateStringQuote: - switch rn { - case '\\': - return ss.advanceEscapeSequence() - case '"': - ss.runestate = runestateCode - } - case runestateStringBacktick: - switch rn { - case '`': - ss.runestate = runestateCode - } - } - ss.idx++ - return ss.done() -} - -// returns true if no runes left to advance. -func (ss *scanner) done() bool { - return ss.idx == len(ss.rnz) -} - -// returns true if outside the scope of any -// parentheses, brackets, strings, or rune literals. -func (ss *scanner) out() bool { - return ss.runestate == runestateCode && - ss.curly == int(0) && - ss.round == int(0) && - ss.square == int(0) -} - -func isOctal(r rune) bool { - switch r { - case '0', '1', '2', '3', '4', '5', '6', '7': - return true - default: - return false - } -} - -func isHex(r rune) bool { - switch r { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', - 'A', 'B', 'C', 'D', 'E', 'F': - return true - default: - return false - } -} - -// Advances runes, while checking that each passes `check`. if error, panics -// with info including `back` runes back. -func (ss *scanner) eatRunes(back int, eat int, check func(rune) bool) { - for i := 0; i < eat; i++ { - if ss.idx+i == len(ss.rnz) { - panic(fmt.Sprintf("eof while parsing: %s", - string(ss.rnz[ss.idx-back:]))) - } - if !check(ss.rnz[ss.idx+i]) { - panic(fmt.Sprintf("invalid character while parsing: %s", - string(ss.rnz[ss.idx-back:ss.idx+i+1]))) - } - ss.idx++ - } -} - -// increments ss.idx until escape sequence is complete. returns true if done. -func (ss *scanner) advanceEscapeSequence() bool { - rn1 := ss.rnz[ss.idx] - if rn1 != '\\' { - panic("should not happen") - } - if ss.idx == len(ss.rnz)-1 { - panic("eof while parsing escape sequence") - } - rn2 := ss.rnz[ss.idx+1] - switch rn2 { - case 'x': - ss.idx += 2 - ss.eatRunes(2, 2, isHex) - return ss.done() - case 'u': - ss.idx += 2 - ss.eatRunes(2, 4, isHex) - return ss.done() - case 'U': - ss.idx += 2 - ss.eatRunes(2, 8, isHex) - return ss.done() - case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '\'', '"': - ss.idx += 2 - return ss.done() - default: - ss.idx += 1 - if isOctal(rn2) { - ss.eatRunes(1, 3, isOctal) - } else { - panic("invalid escape sequence") - } - return ss.done() - } -} diff --git a/tm2/pkg/amino/reflect.go b/tm2/pkg/amino/reflect.go index bc4fa57e626..01402a320d2 100644 --- a/tm2/pkg/amino/reflect.go +++ b/tm2/pkg/amino/reflect.go @@ -1,26 +1,21 @@ package amino import ( - "encoding/json" "fmt" "reflect" "unicode" ) -//---------------------------------------- +// ---------------------------------------- // Constants -var ( - jsonMarshalerType = reflect.TypeOf(new(json.Marshaler)).Elem() - jsonUnmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem() - errorType = reflect.TypeOf(new(error)).Elem() -) +var errorType = reflect.TypeOf(new(error)).Elem() -//---------------------------------------- +// ---------------------------------------- // encode: see binary-encode.go and json-encode.go // decode: see binary-decode.go and json-decode.go -//---------------------------------------- +// ---------------------------------------- // Misc. // CONTRACT: by the time this is called, len(bz) >= _n @@ -182,18 +177,6 @@ func constructConcreteType(cinfo *TypeInfo) (crv, irvSet reflect.Value) { return } -// Like constructConcreteType(), but if pointer preferred, returns a nil one. -// We like nil pointers for efficiency. -func constructConcreteTypeNilPreferred(cinfo *TypeInfo) (crv reflect.Value) { - // Construct new concrete type. - if cinfo.PointerPreferred { - crv = reflect.Zero(cinfo.PtrToType) - } else { - crv = reflect.New(cinfo.Type).Elem() - } - return -} - func toReprObject(rv reflect.Value) (rrv reflect.Value, err error) { var mwrm reflect.Value if rv.CanAddr() { @@ -269,12 +252,6 @@ func unmarshalAminoReprType(rm reflect.Method) (rrt reflect.Type) { return } -func toPBMessage(cdc *Codec, rv reflect.Value) (pbrv reflect.Value) { - rm := rv.MethodByName("ToPBMessage") - pbrv = rm.Call([]reflect.Value{reflect.ValueOf(cdc)})[0] - return -} - // NOTE: do not change this definition. // It is also defined for genproto. func isListType(rt reflect.Type) bool { diff --git a/tm2/pkg/amino/tests/common.go b/tm2/pkg/amino/tests/common.go index b7213a56efc..1abf3aaf601 100644 --- a/tm2/pkg/amino/tests/common.go +++ b/tm2/pkg/amino/tests/common.go @@ -32,8 +32,6 @@ type PrimitivesStruct struct { Time time.Time Duration time.Duration Empty EmptyStruct - - unexposed int8 } type ShortArraysStruct struct { diff --git a/tm2/pkg/bft/abci/client/client.go b/tm2/pkg/bft/abci/client/client.go index b3d5a945839..f9f21f08fdb 100644 --- a/tm2/pkg/bft/abci/client/client.go +++ b/tm2/pkg/bft/abci/client/client.go @@ -7,11 +7,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/service" ) -const ( - dialRetryIntervalSeconds = 3 - echoRetryIntervalSeconds = 1 -) - // Client defines an interface for an ABCI client. // All `Async` methods return a `ReqRes` object. // All `Sync` methods return the appropriate protobuf ResponseXxx struct and an error. @@ -48,11 +43,11 @@ type Client interface { EndBlockSync(abci.RequestEndBlock) (abci.ResponseEndBlock, error) } -//---------------------------------------- +// ---------------------------------------- type Callback func(abci.Request, abci.Response) -//---------------------------------------- +// ---------------------------------------- type ReqRes struct { abci.Request diff --git a/tm2/pkg/bft/consensus/common_test.go b/tm2/pkg/bft/consensus/common_test.go index 19c96756624..ba19881aace 100644 --- a/tm2/pkg/bft/consensus/common_test.go +++ b/tm2/pkg/bft/consensus/common_test.go @@ -29,7 +29,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/events" "github.com/gnolang/gno/tm2/pkg/log" osm "github.com/gnolang/gno/tm2/pkg/os" - "github.com/gnolang/gno/tm2/pkg/p2p" ) const ( @@ -701,15 +700,6 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF } } -func getSwitchIndex(switches []*p2p.Switch, peer p2p.Peer) int { - for i, s := range switches { - if peer.NodeInfo().ID() == s.NodeInfo().ID() { - return i - } - } - panic("didnt find peer in switches") -} - // ------------------------------------------------------------------------------- // genesis diff --git a/tm2/pkg/bft/consensus/replay.go b/tm2/pkg/bft/consensus/replay.go index 16b4ba3fa87..b228c1b63e4 100644 --- a/tm2/pkg/bft/consensus/replay.go +++ b/tm2/pkg/bft/consensus/replay.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "hash/crc32" "io" "reflect" "time" @@ -21,8 +20,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/log" ) -var crc32c = crc32.MakeTable(crc32.Castagnoli) - // Functionality to replay blocks and messages on recovery from a crash. // There are two general failure scenarios: // diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index 1587fa03057..0e2e6b629a4 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -59,7 +59,7 @@ x * TestHalt1 - if we see +2/3 precommits after timing out into new round, we sh */ -//---------------------------------------------------------------------------------------------------- +// ---------------------------------------------------------------------------------------------------- // ProposeSuite func TestStateProposerSelection0(t *testing.T) { @@ -255,7 +255,7 @@ func TestStateBadProposal(t *testing.T) { signAddVotes(cs1, types.PrecommitType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) } -//---------------------------------------------------------------------------------------------------- +// ---------------------------------------------------------------------------------------------------- // FullRoundSuite // propose, prevote, and precommit a block @@ -354,7 +354,7 @@ func TestStateFullRound2(t *testing.T) { ensureNewBlock(newBlockCh, height) } -//------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ // LockSuite // two validators, 4 rounds. @@ -409,7 +409,7 @@ func TestStateLockNoPOL(t *testing.T) { // but with invalid args. then we enterPrecommitWait, and the timeout to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - /// + // ----------- round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -1496,7 +1496,7 @@ func TestFlappyResetTimeoutPrecommitUponNewHeight(t *testing.T) { assert.False(t, rs.TriggeredTimeoutPrecommit, "triggeredTimeoutPrecommit should be false at the beginning of each height") } -//------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ // SlashingSuite // TODO: Slashing @@ -1573,10 +1573,10 @@ func TestStateSlashingPrecommits(t *testing.T) { } */ -//------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ // CatchupSuite -//------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------ // HaltSuite // 4 vals. diff --git a/tm2/pkg/bft/consensus/wal_generator.go b/tm2/pkg/bft/consensus/wal_generator.go index dff1cca1446..7893c544c7f 100644 --- a/tm2/pkg/bft/consensus/wal_generator.go +++ b/tm2/pkg/bft/consensus/wal_generator.go @@ -40,7 +40,7 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { logger := log.TestingLogger().With("wal_generator", "wal_generator") logger.Info("generating WAL (last height msg excluded)", "numBlocks", numBlocks) - ///////////////////////////////////////////////////////////////////////////// + // ----------- // COPY PASTE FROM node.go WITH A FEW MODIFICATIONS // NOTE: we can't import node package because of circular dependency. // NOTE: we don't do handshake so need to set state.Version.Consensus.App directly. @@ -83,7 +83,7 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { consensusState.SetPrivValidator(privValidator) } // END OF COPY PASTE - ///////////////////////////////////////////////////////////////////////////// + // ----------- // set consensus wal to buffered WAL, which will write all incoming msgs to buffer numBlocksWritten := make(chan struct{}) diff --git a/tm2/pkg/bft/consensus/wal_test.go b/tm2/pkg/bft/consensus/wal_test.go index 1b271fb73c7..50f8f4c8b2a 100644 --- a/tm2/pkg/bft/consensus/wal_test.go +++ b/tm2/pkg/bft/consensus/wal_test.go @@ -13,10 +13,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/log" ) -const ( - walTestFlushInterval = time.Duration(100) * time.Millisecond -) - // ---------------------------------------- // copied over from wal/wal_test.go @@ -53,8 +49,7 @@ func makeTempWAL(t *testing.T, walChunkSize int64) (wal walm.WAL) { // ---------------------------------------- func TestWALTruncate(t *testing.T) { - const maxTestMsgSize = 1024 * 1024 // 1MB - const walChunkSize = 409610 // 4KB + const walChunkSize = 409610 // 4KB wal := makeTempWAL(t, walChunkSize) wal.SetLogger(log.TestingLogger()) diff --git a/tm2/pkg/bft/node/node.go b/tm2/pkg/bft/node/node.go index bdeb5061540..9d6143e7af3 100644 --- a/tm2/pkg/bft/node/node.go +++ b/tm2/pkg/bft/node/node.go @@ -665,7 +665,6 @@ func (n *Node) ConfigureRPC() { rpccore.SetPubKey(pubKey) rpccore.SetGenesisDoc(n.genesisDoc) rpccore.SetProxyAppQuery(n.proxyApp.Query()) - rpccore.SetTxEventStore(n.txEventStore) rpccore.SetConsensusReactor(n.consensusReactor) rpccore.SetLogger(n.Logger.With("module", "rpc")) rpccore.SetEventSwitch(n.evsw) diff --git a/tm2/pkg/bft/privval/signer_listener_endpoint_test.go b/tm2/pkg/bft/privval/signer_listener_endpoint_test.go index 8e5b1613454..c2d89660baf 100644 --- a/tm2/pkg/bft/privval/signer_listener_endpoint_test.go +++ b/tm2/pkg/bft/privval/signer_listener_endpoint_test.go @@ -130,7 +130,7 @@ func TestRetryConnToRemoteSigner(t *testing.T) { } } -// ///////////////////////////////// +// ----------- func newSignerListenerEndpoint(logger log.Logger, ln net.Listener, timeoutReadWrite time.Duration) *SignerListenerEndpoint { var listener net.Listener diff --git a/tm2/pkg/bft/rpc/core/pipe.go b/tm2/pkg/bft/rpc/core/pipe.go index a8b102d9ab7..abfdc300d31 100644 --- a/tm2/pkg/bft/rpc/core/pipe.go +++ b/tm2/pkg/bft/rpc/core/pipe.go @@ -10,7 +10,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/proxy" cfg "github.com/gnolang/gno/tm2/pkg/bft/rpc/config" sm "github.com/gnolang/gno/tm2/pkg/bft/state" - "github.com/gnolang/gno/tm2/pkg/bft/state/eventstore" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/crypto" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -68,7 +67,6 @@ var ( // objects pubKey crypto.PubKey genDoc *types.GenesisDoc // cache the genesis structure - txEventStore eventstore.TxEventStore consensusReactor *consensus.ConsensusReactor evsw events.EventSwitch gTxDispatcher *txDispatcher @@ -115,10 +113,6 @@ func SetProxyAppQuery(appConn proxy.AppConnQuery) { proxyAppQuery = appConn } -func SetTxEventStore(indexer eventstore.TxEventStore) { - txEventStore = indexer -} - func SetConsensusReactor(conR *consensus.ConsensusReactor) { consensusReactor = conR } @@ -169,12 +163,3 @@ func validatePerPage(perPage int) int { } return perPage } - -func validateSkipCount(page, perPage int) int { - skipCount := (page - 1) * perPage - if skipCount < 0 { - return 0 - } - - return skipCount -} diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client.go b/tm2/pkg/bft/rpc/lib/client/ws_client.go index 040437d11ff..4e159a3e3dc 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws_client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws_client.go @@ -226,7 +226,7 @@ func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, param return c.Send(ctx, request) } -/////////////////////////////////////////////////////////////////////////////// +// ----------- // Private methods func (c *WSClient) dial() error { diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index b7a9445a82d..05f92a3b719 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -18,11 +18,11 @@ import ( "github.com/gnolang/gno/tm2/pkg/log" ) -////////////////////////////////////////////////////////////////////////////// +// ----------- // HTTP REST API // TODO -////////////////////////////////////////////////////////////////////////////// +// ----------- // JSON-RPC over HTTP func testMux() *http.ServeMux { @@ -229,7 +229,7 @@ func TestUnknownRPCPath(t *testing.T) { require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404") } -////////////////////////////////////////////////////////////////////////////// +// ----------- // JSON-RPC over WEBSOCKETS func TestWebsocketManagerHandler(t *testing.T) { diff --git a/tm2/pkg/bft/state/helpers_test.go b/tm2/pkg/bft/state/helpers_test.go index 219baaf0f55..ca4175185a9 100644 --- a/tm2/pkg/bft/state/helpers_test.go +++ b/tm2/pkg/bft/state/helpers_test.go @@ -8,7 +8,6 @@ import ( "github.com/gnolang/gno/tm2/pkg/bft/proxy" sm "github.com/gnolang/gno/tm2/pkg/bft/state" "github.com/gnolang/gno/tm2/pkg/bft/types" - tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time" "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" dbm "github.com/gnolang/gno/tm2/pkg/db" @@ -190,24 +189,7 @@ func makeHeaderPartsResponsesParams(state sm.State, params abci.ConsensusParams) return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } -func randomGenesisDoc() *types.GenesisDoc { - pubkey := ed25519.GenPrivKey().PubKey() - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: "abc", - Validators: []types.GenesisValidator{ - { - Address: pubkey.Address(), - PubKey: pubkey, - Power: 10, - Name: "myval", - }, - }, - ConsensusParams: types.DefaultConsensusParams(), - } -} - -//---------------------------------------------------------------------------- +// ---------------------------------------------------------------------------- type testApp struct { abci.BaseApplication diff --git a/tm2/pkg/bft/types/validator_set.go b/tm2/pkg/bft/types/validator_set.go index 617ceecaae5..80ed994ca39 100644 --- a/tm2/pkg/bft/types/validator_set.go +++ b/tm2/pkg/bft/types/validator_set.go @@ -758,7 +758,7 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin return nil } -//----------------- +// ----------------- // ErrTooMuchChange func IsErrTooMuchChange(err error) bool { @@ -775,7 +775,7 @@ func (e tooMuchChangeError) Error() string { return fmt.Sprintf("Invalid commit -- insufficient old voting power: got %v, needed %v", e.got, e.needed) } -//---------------- +// ---------------- func (vals *ValidatorSet) String() string { return vals.StringIndented("") @@ -802,7 +802,7 @@ func (vals *ValidatorSet) StringIndented(indent string) string { indent) } -//------------------------------------- +// ------------------------------------- // Implements sort for sorting validators by address. // Sort validators by address. @@ -822,7 +822,7 @@ func (valz ValidatorsByAddress) Swap(i, j int) { valz[j] = it } -//---------------------------------------- +// ---------------------------------------- // for testing // RandValidatorSet returns a randomized validator set, useful for testing. @@ -841,7 +841,7 @@ func RandValidatorSet(numValidators int, votingPower int64) (*ValidatorSet, []Pr return vals, privValidators } -/////////////////////////////////////////////////////////////////////////////// +// ----------- // safe addition/subtraction func safeAdd(a, b int64) (int64, bool) { diff --git a/tm2/pkg/bft/wal/wal.go b/tm2/pkg/bft/wal/wal.go index b66a34df222..a4a14d638dd 100644 --- a/tm2/pkg/bft/wal/wal.go +++ b/tm2/pkg/bft/wal/wal.go @@ -436,7 +436,7 @@ OUTER_LOOP: return nil, false, nil } -// ///////////////////////////////////////////////////////////////////////////// +// ----------- // A WALWriter writes custom-encoded WAL messages to an output stream. // Each binary WAL entry is length encoded, then crc encoded, @@ -512,7 +512,7 @@ func (enc *WALWriter) WriteMeta(meta MetaMessage) error { return err } -// ///////////////////////////////////////////////////////////////////////////// +// ----------- // IsDataCorruptionError returns true if data has been corrupted inside WAL. func IsDataCorruptionError(err error) bool { diff --git a/tm2/pkg/bft/wal/wal_test.go b/tm2/pkg/bft/wal/wal_test.go index 4cb94766389..a1a3e831a79 100644 --- a/tm2/pkg/bft/wal/wal_test.go +++ b/tm2/pkg/bft/wal/wal_test.go @@ -29,7 +29,7 @@ type TestMessage struct { func (TestMessage) AssertWALMessage() {} -var testPackage = amino.RegisterPackage(amino.NewPackage( +var _ = amino.RegisterPackage(amino.NewPackage( "github.com/gnolang/gno/tm2/pkg/bft/wal", "wal", amino.GetCallersDirname(), diff --git a/tm2/pkg/crypto/keys/client/export_test.go b/tm2/pkg/crypto/keys/client/export_test.go index 7ddbeede993..0f4c5311dfa 100644 --- a/tm2/pkg/crypto/keys/client/export_test.go +++ b/tm2/pkg/crypto/keys/client/export_test.go @@ -64,11 +64,9 @@ func addRandomKeyToKeybase( } type testCmdKeyOptsBase struct { - kbHome string - keyName string - decryptPassword string - encryptPassword string - unsafe bool + kbHome string + keyName string + unsafe bool } type testExportKeyOpts struct { diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 16b3631d188..13f6e00979d 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -46,14 +46,6 @@ const ( infoSuffix = "info" ) -const ( - // used for deriving seed from mnemonic - DefaultBIP39Passphrase = "" - - // bits of entropy to draw when creating a mnemonic - defaultEntropySize = 256 -) - var ( // ErrUnsupportedSigningAlgo is raised when the caller tries to use a // different signing scheme than secp256k1. diff --git a/tm2/pkg/crypto/merkle/proof_test.go b/tm2/pkg/crypto/merkle/proof_test.go index 676e281ac60..3319963ce6c 100644 --- a/tm2/pkg/crypto/merkle/proof_test.go +++ b/tm2/pkg/crypto/merkle/proof_test.go @@ -26,19 +26,6 @@ func NewDominoOp(key, input, output string) DominoOp { } } -//nolint:unused -func DominoOpDecoder(pop ProofOp) (ProofOperator, error) { - if pop.Type != ProofOpDomino { - panic("unexpected proof op type") - } - var op DominoOp // a bit strange as we'll discard this, but it works. - err := amino.UnmarshalSized(pop.Data, &op) - if err != nil { - return nil, errors.Wrap(err, "decoding ProofOp.Data into SimpleValueOp") - } - return NewDominoOp(string(pop.Key), op.Input, op.Output), nil -} - func (dop DominoOp) ProofOp() ProofOp { bz := amino.MustMarshalSized(dop) return ProofOp{ @@ -63,7 +50,7 @@ func (dop DominoOp) GetKey() []byte { return []byte(dop.key) } -//---------------------------------------- +// ---------------------------------------- func TestProofOperators(t *testing.T) { var err error diff --git a/tm2/pkg/crypto/util.go b/tm2/pkg/crypto/util.go deleted file mode 100644 index f017557f09b..00000000000 --- a/tm2/pkg/crypto/util.go +++ /dev/null @@ -1,7 +0,0 @@ -package crypto - -func cp(bz []byte) (ret []byte) { - ret = make([]byte, len(bz)) - copy(ret, bz) - return ret -} diff --git a/tm2/pkg/events/store.go b/tm2/pkg/events/store.go index bea4626394e..1891fd17ac6 100644 --- a/tm2/pkg/events/store.go +++ b/tm2/pkg/events/store.go @@ -1,33 +1,12 @@ package events -import ( - "fmt" - - auto "github.com/gnolang/gno/tm2/pkg/autofile" -) - // StoreStream stores events to disk but is also listenaable. type StoreStream interface { Eventable SetHeight(height int64) // to demarcate height in WAL for replay. } -type storeStream struct { - afile *auto.AutoFile - buf []byte - height int64 -} - -func (ss *storeStream) SetHeight(height int64) { - if ss.height < height { - // write new height - ss.height = height - } else /* if height <= ss.height */ { - panic(fmt.Sprintf("invalid SetHeight height value. current %v, got %v", ss.height, height)) - } -} - -//---------------------------------------- +// ---------------------------------------- // move to own file // FilterStream is listenable and lets you filter. diff --git a/tm2/pkg/iavl/benchmarks/bench_test.go b/tm2/pkg/iavl/benchmarks/bench_test.go index de6a573020e..77ad77eed28 100644 --- a/tm2/pkg/iavl/benchmarks/bench_test.go +++ b/tm2/pkg/iavl/benchmarks/bench_test.go @@ -75,19 +75,6 @@ func runKnownQueries(b *testing.B, t *iavl.MutableTree, keys [][]byte) { } } -func runInsert(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int) *iavl.MutableTree { - b.Helper() - - for i := 1; i <= b.N; i++ { - t.Set(randBytes(keyLen), randBytes(dataLen)) - if i%blockSize == 0 { - t.Hash() - t.SaveVersion() - } - } - return t -} - func runUpdate(b *testing.B, t *iavl.MutableTree, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { b.Helper() @@ -102,23 +89,6 @@ func runUpdate(b *testing.B, t *iavl.MutableTree, dataLen, blockSize int, keys [ return t } -func runDelete(b *testing.B, t *iavl.MutableTree, blockSize int, keys [][]byte) *iavl.MutableTree { - b.Helper() - - var key []byte - l := int32(len(keys)) - for i := 1; i <= b.N; i++ { - key = keys[rand.Int31n(l)] - // key = randBytes(16) - // TODO: test if removed, use more keys (from insert) - t.Remove(key) - if i%blockSize == 0 { - commitTree(b, t) - } - } - return t -} - // runBlock measures time for an entire block, not just one tx func runBlock(b *testing.B, t *iavl.MutableTree, keyLen, dataLen, blockSize int, keys [][]byte) *iavl.MutableTree { b.Helper() diff --git a/tm2/pkg/iavl/node.go b/tm2/pkg/iavl/node.go index f2a944afc6a..ee522016f0c 100644 --- a/tm2/pkg/iavl/node.go +++ b/tm2/pkg/iavl/node.go @@ -371,10 +371,6 @@ func (node *Node) traverse(t *ImmutableTree, ascending bool, cb func(*Node) bool }) } -func (node *Node) traverseWithDepth(t *ImmutableTree, ascending bool, cb func(*Node, uint8) bool) bool { - return node.traverseInRange(t, nil, nil, ascending, false, 0, cb) -} - func (node *Node) traverseInRange(t *ImmutableTree, start, end []byte, ascending bool, inclusive bool, depth uint8, cb func(*Node, uint8) bool) bool { afterStart := start == nil || bytes.Compare(start, node.key) < 0 startOrAfter := start == nil || bytes.Compare(start, node.key) <= 0 diff --git a/tm2/pkg/iavl/nodedb.go b/tm2/pkg/iavl/nodedb.go index 3e59b3480e9..e1998d8cc1d 100644 --- a/tm2/pkg/iavl/nodedb.go +++ b/tm2/pkg/iavl/nodedb.go @@ -385,7 +385,7 @@ func (ndb *nodeDB) saveRoot(hash []byte, version int64) error { return nil } -////////////////// Utility and test functions ///////////////////////////////// +// ----------- Utility and test functions // ----------- func (ndb *nodeDB) leafNodes() []*Node { leaves := []*Node{} diff --git a/tm2/pkg/iavl/proof_path.go b/tm2/pkg/iavl/proof_path.go index f66fcf3c48c..a5ac14bc4e7 100644 --- a/tm2/pkg/iavl/proof_path.go +++ b/tm2/pkg/iavl/proof_path.go @@ -1,11 +1,8 @@ package iavl import ( - "bytes" "fmt" "strings" - - "github.com/gnolang/gno/tm2/pkg/errors" ) // pathWithLeaf is a path to a leaf node and the leaf node itself. @@ -28,14 +25,6 @@ func (pwl pathWithLeaf) StringIndented(indent string) string { indent) } -// `verify` checks that the leaf node's hash + the inner nodes merkle-izes to -// the given root. If it returns an error, it means the leafHash or the -// PathToLeaf is incorrect. -func (pwl pathWithLeaf) verify(root []byte) error { - leafHash := pwl.Leaf.Hash() - return pwl.Path.verify(leafHash, root) -} - // `computeRootHash` computes the root hash with leaf node. // Does not verify the root hash. func (pwl pathWithLeaf) computeRootHash() []byte { @@ -73,21 +62,6 @@ func (pl PathToLeaf) stringIndented(indent string) string { indent) } -// `verify` checks that the leaf node's hash + the inner nodes merkle-izes to -// the given root. If it returns an error, it means the leafHash or the -// PathToLeaf is incorrect. -func (pl PathToLeaf) verify(leafHash []byte, root []byte) error { - hash := leafHash - for i := len(pl) - 1; i >= 0; i-- { - pin := pl[i] - hash = pin.Hash(hash) - } - if !bytes.Equal(root, hash) { - return errors.Wrap(ErrInvalidProof, "") - } - return nil -} - // `computeRootHash` computes the root hash assuming some leaf hash. // Does not verify the root hash. func (pl PathToLeaf) computeRootHash(leafHash []byte) []byte { @@ -117,37 +91,6 @@ func (pl PathToLeaf) isRightmost() bool { return true } -func (pl PathToLeaf) isEmpty() bool { - return pl == nil || len(pl) == 0 -} - -func (pl PathToLeaf) dropRoot() PathToLeaf { - if pl.isEmpty() { - return pl - } - return pl[:len(pl)-1] -} - -func (pl PathToLeaf) hasCommonRoot(pl2 PathToLeaf) bool { - if pl.isEmpty() || pl2.isEmpty() { - return false - } - leftEnd := pl[len(pl)-1] - rightEnd := pl2[len(pl2)-1] - - return bytes.Equal(leftEnd.Left, rightEnd.Left) && - bytes.Equal(leftEnd.Right, rightEnd.Right) -} - -func (pl PathToLeaf) isLeftAdjacentTo(pl2 PathToLeaf) bool { - for pl.hasCommonRoot(pl2) { - pl, pl2 = pl.dropRoot(), pl2.dropRoot() - } - pl, pl2 = pl.dropRoot(), pl2.dropRoot() - - return pl.isRightmost() && pl2.isLeftmost() -} - // returns -1 if invalid. func (pl PathToLeaf) Index() (idx int64) { for i, node := range pl { diff --git a/tm2/pkg/iavl/proof_range.go b/tm2/pkg/iavl/proof_range.go index 799fb65cebb..ea6bce24fc0 100644 --- a/tm2/pkg/iavl/proof_range.go +++ b/tm2/pkg/iavl/proof_range.go @@ -299,7 +299,7 @@ func (proof *RangeProof) _computeRootHash() (rootHash []byte, treeEnd bool, err return rootHash, treeEnd, nil } -/////////////////////////////////////////////////////////////////////////////// +// ----------- // keyStart is inclusive and keyEnd is exclusive. // If keyStart or keyEnd don't exist, the leaf before keyStart @@ -442,7 +442,7 @@ func (t *ImmutableTree) getRangeProof(keyStart, keyEnd []byte, limit int) (proof }, keys, values, nil } -//---------------------------------------- +// ---------------------------------------- // GetWithProof gets the value under the key if it exists, or returns nil. // A proof of existence or absence is returned alongside the value. diff --git a/tm2/pkg/iavl/tree_test.go b/tm2/pkg/iavl/tree_test.go index 56a00ee2424..f15a3276653 100644 --- a/tm2/pkg/iavl/tree_test.go +++ b/tm2/pkg/iavl/tree_test.go @@ -1290,7 +1290,7 @@ func TestLoadVersionForOverwriting(t *testing.T) { require.NoError(err, "SaveVersion should not fail.") } -//////////////////////////// BENCHMARKS /////////////////////////////////////// +// ----------- BENCHMARKS // ----------- func BenchmarkTreeLoadAndDelete(b *testing.B) { if testing.Short() { diff --git a/tm2/pkg/p2p/config/config.go b/tm2/pkg/p2p/config/config.go index 855c0fd9844..830f84ad137 100644 --- a/tm2/pkg/p2p/config/config.go +++ b/tm2/pkg/p2p/config/config.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/errors" ) -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // P2PConfig const ( @@ -16,8 +16,6 @@ const ( FuzzModeDelay ) -var defaultConfigDir = "config" // duplicate across module configs? - // P2PConfig defines the configuration options for the Tendermint peer-to-peer networking layer type P2PConfig struct { RootDir string `toml:"home"` diff --git a/tm2/pkg/p2p/switch.go b/tm2/pkg/p2p/switch.go index 1f693544d5a..368de659fb1 100644 --- a/tm2/pkg/p2p/switch.go +++ b/tm2/pkg/p2p/switch.go @@ -387,10 +387,6 @@ func (sw *Switch) reconnectToPeer(addr *NetAddress) { // --------------------------------------------------------------------- // Dialing -type privateAddr interface { - PrivateAddr() bool -} - // DialPeersAsync dials a list of peers asynchronously in random order. // Used to dial peers from config on startup or from unsafe-RPC (trusted sources). // It ignores NetAddressLookupError. However, if there are other errors, first diff --git a/tm2/pkg/sdk/auth/test_common.go b/tm2/pkg/sdk/auth/test_common.go index a51c8576bad..ee4153184a4 100644 --- a/tm2/pkg/sdk/auth/test_common.go +++ b/tm2/pkg/sdk/auth/test_common.go @@ -19,33 +19,6 @@ type testEnv struct { bank BankKeeperI } -// moduleAccount defines an account for modules that holds coins on a pool -type moduleAccount struct { - *std.BaseAccount - name string `json:"name" yaml:"name"` // name of the module - permissions []string `json:"permissions" yaml"permissions"` // permissions of module account -} - -// HasPermission returns whether or not the module account has permission. -func (ma moduleAccount) HasPermission(permission string) bool { - for _, perm := range ma.permissions { - if perm == permission { - return true - } - } - return false -} - -// GetName returns the the name of the holder's module -func (ma moduleAccount) GetName() string { - return ma.name -} - -// GetPermissions returns permissions granted to the module account -func (ma moduleAccount) GetPermissions() []string { - return ma.permissions -} - func setupTestEnv() testEnv { db := dbm.NewMemDB() diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 2d130583885..3fb44e5f744 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -444,10 +444,6 @@ func (mch msgCounterHandler) Query(ctx Context, req abci.RequestQuery) abci.Resp panic("should not happen") } -func i2b(i int64) []byte { - return []byte{byte(i)} -} - func getIntFromStore(store store.Store, key []byte) int64 { bz := store.Get(key) if len(bz) == 0 { @@ -477,7 +473,7 @@ func incrementingCounter(t *testing.T, store store.Store, counterKey []byte, cou return } -//--------------------------------------------------------------------- +// --------------------------------------------------------------------- // Tx processing - CheckTx, DeliverTx, SimulateTx. // These tests use the serialized tx as input, while most others will use the // Check(), Deliver(), Simulate() methods directly. diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index d5eb9bb1da0..75063320ad3 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -619,11 +619,9 @@ var ( // Denominations can be 3 ~ 16 characters long. reDnmString = `[a-z][a-z0-9]{2,15}` reAmt = `[[:digit:]]+` - reDecAmt = `[[:digit:]]*\.[[:digit:]]+` reSpc = `[[:space:]]*` reDnm = regexp.MustCompile(fmt.Sprintf(`^%s$`, reDnmString)) reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnmString)) - reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, reDnmString)) ) func validateDenom(denom string) error { diff --git a/tm2/pkg/store/gas/store_test.go b/tm2/pkg/store/gas/store_test.go index 70f1fc8ea7f..ad335d73c24 100644 --- a/tm2/pkg/store/gas/store_test.go +++ b/tm2/pkg/store/gas/store_test.go @@ -13,12 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -func newGasKVStore() types.Store { - meter := types.NewGasMeter(10000) - mem := dbadapter.Store{dbm.NewMemDB()} - return gas.New(mem, meter, types.DefaultGasConfig()) -} - func bz(s string) []byte { return []byte(s) } func keyFmt(i int) []byte { return bz(fmt.Sprintf("key%0.8d", i)) } diff --git a/tm2/pkg/store/rootmulti/dbadapter.go b/tm2/pkg/store/rootmulti/dbadapter.go deleted file mode 100644 index 254d139a608..00000000000 --- a/tm2/pkg/store/rootmulti/dbadapter.go +++ /dev/null @@ -1,33 +0,0 @@ -package rootmulti - -import ( - "github.com/gnolang/gno/tm2/pkg/store/dbadapter" - "github.com/gnolang/gno/tm2/pkg/store/types" -) - -var commithash = []byte("FAKE_HASH") - -//---------------------------------------- -// commitDBStoreWrapper should only be used for simulation/debugging, -// as it doesn't compute any commit hash, and it cannot load older state. - -// Wrapper type for dbm.Db with implementation of KVStore -type commitDBStoreAdapter struct { - dbadapter.Store -} - -func (cdsa commitDBStoreAdapter) Commit() types.CommitID { - return types.CommitID{ - Version: -1, - Hash: commithash, - } -} - -func (cdsa commitDBStoreAdapter) LastCommitID() types.CommitID { - return types.CommitID{ - Version: -1, - Hash: commithash, - } -} - -func (cdsa commitDBStoreAdapter) SetPruning(_ types.PruningOptions) {} From 11f4359d2e06179132636057a09c35a74f0fc77a Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 8 Nov 2023 09:15:15 +0100 Subject: [PATCH 66/93] feat: add contribs/gnomd (#1256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a `contribs/gnomd` binary. Please review PR #1001 for the `maketx run`. Additionally, this PR introduces the `contribs/` folder. It allows the creation of `contribs/gno*` Go packages, Python/Rust/Shell/… scripts. This approach keeps the main `gno`, `gnokey`, and `gnoland` simple while adhering to the Unix philosophy and incorporating several specific gno commands. ![CleanShot 2023-10-19 at 09 17 40@2x](https://github.com/gnolang/gno/assets/94029/9b92bf18-f923-4f70-aea7-a88f185f2342) --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/workflows/contribs.yml | 35 +++++++++++++++++ .github/workflows/misc.yml | 5 ++- contribs/Makefile | 30 +++++++++++++++ contribs/README.md | 17 +++++++++ contribs/gnomd/go.mod | 25 ++++++++++++ contribs/gnomd/go.sum | 70 ++++++++++++++++++++++++++++++++++ contribs/gnomd/main.go | 38 ++++++++++++++++++ 7 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/contribs.yml create mode 100644 contribs/Makefile create mode 100644 contribs/README.md create mode 100644 contribs/gnomd/go.mod create mode 100644 contribs/gnomd/go.sum create mode 100644 contribs/gnomd/main.go diff --git a/.github/workflows/contribs.yml b/.github/workflows/contribs.yml new file mode 100644 index 00000000000..939ac670710 --- /dev/null +++ b/.github/workflows/contribs.yml @@ -0,0 +1,35 @@ +name: contribs + +on: + push: + branches: [ "master" ] + pull_request: + paths: + - "contribs/**" + - ".github/workflows/contribs.yml" + - "gnovm/**.go" + - "gno.land/**.go" + - "tm2/**.go" + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + install: + strategy: + fail-fast: false + matrix: + goversion: # two latest versions + - "1.21.x" + program: + - "gnomd" + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.goversion }} + - run: make install ${{ matrix.program }} + working-directory: contribs diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index a7c20d61ffd..f72a97c95b3 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -71,8 +71,9 @@ jobs: - name: Check go.mods run: | - sums="$(sha256sum go.mod misc/devdeps/go.mod)" - for path in . ./misc/devdeps; do + gomods=$(find . -type f -name go.mod) + sums="$(sha256sum ${gomods})" + for path in $(dirname $(gomods)); do env -C $path go mod tidy -v || exit 1 done echo "$sums" | sha256sum -c diff --git a/contribs/Makefile b/contribs/Makefile new file mode 100644 index 00000000000..b816987b733 --- /dev/null +++ b/contribs/Makefile @@ -0,0 +1,30 @@ +.PHONY: help +help: + @echo "Available make commands:" + @cat Makefile | grep '^[a-z][^:]*:' | cut -d: -f1 | sort | sed 's/^/ /' + +.PHONY: install +install: install.gnomd + +install.gnomd:; cd gnomd && go install . + +.PHONY: clean +clean: + rm -rf build + +######################################## +# Dev tools +rundep=go run -modfile ../misc/devdeps/go.mod + +.PHONY: fmt +GOFMT_FLAGS ?= -w +fmt: + $(rundep) mvdan.cc/gofumpt $(GOFMT_FLAGS) . + +######################################## +# Test suite +GOTEST_FLAGS ?= -v -p 1 -timeout=30m + +.PHONY: test +test: + @echo "nothing to do." diff --git a/contribs/README.md b/contribs/README.md new file mode 100644 index 00000000000..c481af0f4aa --- /dev/null +++ b/contribs/README.md @@ -0,0 +1,17 @@ +# Gno Contribs + +This directory houses additional commands and tools designed to enhance your Gno experience. +These tools can range from simple wrappers for `gno`, `gnoland`, and `gnokey` to complete applications. +Some may be Go binaries with their own `go.mod` files, allowing the use of specialized libraries, +while others may be shell scripts or programs written in different languages. + +## Contributing Guidelines + +If you'd like to contribute a tool to Gno Contribs, please follow these guidelines: + +1. **Naming**: Choose a clear and concise name for your tool, starting with one of the prefixes: `gno`, `gnoland`, or `gnokey`, + followed by a short descriptive word or abbreviation. + This naming convention helps users identify the purpose of the tool easily. + +2. **User-Friendly**: Ensure that your tool is user-friendly and follows a similar style to other Gno tools, + providing a consistent experience for users. diff --git a/contribs/gnomd/go.mod b/contribs/gnomd/go.mod new file mode 100644 index 00000000000..b631040ce94 --- /dev/null +++ b/contribs/gnomd/go.mod @@ -0,0 +1,25 @@ +module github.com/gnolang/gno/contribs/gnomd + +go 1.20 + +require github.com/MichaelMure/go-term-markdown v0.1.4 + +require ( + github.com/MichaelMure/go-term-text v0.3.1 // indirect + github.com/alecthomas/chroma v0.7.1 // indirect + github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/dlclark/regexp2 v1.1.6 // indirect + github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 // indirect + github.com/kyokomi/emoji/v2 v2.2.8 // indirect + github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.11 // indirect + github.com/mattn/go-runewidth v0.0.12 // indirect + github.com/rivo/uniseg v0.1.0 // indirect + golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect +) diff --git a/contribs/gnomd/go.sum b/contribs/gnomd/go.sum new file mode 100644 index 00000000000..b4ad4f5c9bf --- /dev/null +++ b/contribs/gnomd/go.sum @@ -0,0 +1,70 @@ +github.com/MichaelMure/go-term-markdown v0.1.4 h1:Ir3kBXDUtOX7dEv0EaQV8CNPpH+T7AfTh0eniMOtNcs= +github.com/MichaelMure/go-term-markdown v0.1.4/go.mod h1:EhcA3+pKYnlUsxYKBJ5Sn1cTQmmBMjeNlpV8nRb+JxA= +github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0= +github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ= +github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75 h1:vbix8DDQ/rfatfFr/8cf/sJfIL69i4BcZfjrVOxsMqk= +github.com/eliukblau/pixterm/pkg/ansimage v0.0.0-20191210081756-9fb6cf8c2f75/go.mod h1:0gZuvTO1ikSA5LtTI6E13LEOdWQNjIo5MTQOvrV0eFg= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098 h1:Qxs3bNRWe8GTcKMxYOSXm0jx6j0de8XUtb/fsP3GZ0I= +github.com/gomarkdown/markdown v0.0.0-20191123064959-2c17d62f5098/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU= +github.com/kyokomi/emoji/v2 v2.2.8 h1:jcofPxjHWEkJtkIbcLHvZhxKgCPl6C7MyjTrD4KDqUE= +github.com/kyokomi/emoji/v2 v2.2.8/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s= +golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/gnomd/main.go b/contribs/gnomd/main.go new file mode 100644 index 00000000000..2d3b7266af5 --- /dev/null +++ b/contribs/gnomd/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + + markdown "github.com/MichaelMure/go-term-markdown" +) + +func main() { + // If no arguments are provided, read from stdin + if len(os.Args) <= 1 { + fileContent, err := ioutil.ReadAll(os.Stdin) + checkErr(err) + renderMarkdown("stdin.gno", fileContent) + } + + // Iterate through command-line arguments (file paths) + for _, filePath := range os.Args[1:] { + fileContent, err := ioutil.ReadFile(filePath) + checkErr(err) + renderMarkdown(filePath, fileContent) + } +} + +func renderMarkdown(filePath string, fileContent []byte) { + fmt.Printf("-- %s --\n", filePath) + + result := markdown.Render(string(fileContent), 80, 6) + fmt.Println(string(result)) +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} From 678686a8b132bb85f76579995f4ae48ca3b9e1ff Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Wed, 8 Nov 2023 14:50:56 +0530 Subject: [PATCH 67/93] fix(ci): remove unused func `parseBalance` (#1344) Currently CI is complaining about unused func `parseBalance`. Fixed it.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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).
--- gno.land/pkg/gnoland/app.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/gno.land/pkg/gnoland/app.go b/gno.land/pkg/gnoland/app.go index a8a2736c8d1..0610cd4a93f 100644 --- a/gno.land/pkg/gnoland/app.go +++ b/gno.land/pkg/gnoland/app.go @@ -12,7 +12,6 @@ import ( "github.com/gnolang/gno/gno.land/pkg/sdk/vm" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/crypto" dbm "github.com/gnolang/gno/tm2/pkg/db" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/sdk" @@ -175,22 +174,6 @@ func InitChainer(baseApp *sdk.BaseApp, acctKpr auth.AccountKeeperI, bankKpr bank } } -func parseBalance(bal string) (crypto.Address, std.Coins) { - parts := strings.Split(bal, "=") - if len(parts) != 2 { - panic(fmt.Sprintf("invalid balance string %s", bal)) - } - addr, err := crypto.AddressFromBech32(parts[0]) - if err != nil { - panic(fmt.Sprintf("invalid balance addr %s (%v)", bal, err)) - } - coins, err := std.ParseCoins(parts[1]) - if err != nil { - panic(fmt.Sprintf("invalid balance coins %s (%v)", bal, err)) - } - return addr, coins -} - // XXX not used yet. func EndBlocker(vmk vm.VMKeeperI) func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { From 041d0a84fcca815c3fc1b809d389ed272d927698 Mon Sep 17 00:00:00 2001 From: Hariom Verma Date: Wed, 8 Nov 2023 15:48:21 +0530 Subject: [PATCH 68/93] chore(gnomod): test package does not exist (#1317) Add test for package does not exist. Not able to reproduce `Infinite loop when a requested package does not exist` (#835) --- gnovm/pkg/gnomod/file_test.go | 60 ++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/gnovm/pkg/gnomod/file_test.go b/gnovm/pkg/gnomod/file_test.go index 7acea3a6096..0012960eb4f 100644 --- a/gnovm/pkg/gnomod/file_test.go +++ b/gnovm/pkg/gnomod/file_test.go @@ -20,11 +20,30 @@ func TestFetchDeps(t *testing.T) { for _, tc := range []struct { desc string modFile File + errorShouldContain string requirements []string stdOutContains []string cachedStdOutContains []string }{ { + desc: "not_exists", + modFile: File{ + Module: &modfile.Module{ + Mod: module.Version{ + Path: "testFetchDeps", + }, + }, + Require: []*modfile.Require{ + { + Mod: module.Version{ + Path: "gno.land/p/demo/does_not_exists", + Version: "v0.0.0", + }, + }, + }, + }, + errorShouldContain: "querychain (gno.land/p/demo/does_not_exists)", + }, { desc: "fetch_gno.land/p/demo/avl", modFile: File{ Module: &modfile.Module{ @@ -89,29 +108,34 @@ func TestFetchDeps(t *testing.T) { defer cleanUpFn() // Fetching dependencies - tc.modFile.FetchDeps(dirPath, testRemote, true) + err := tc.modFile.FetchDeps(dirPath, testRemote, true) + if tc.errorShouldContain != "" { + require.ErrorContains(t, err, tc.errorShouldContain) + } else { + require.Nil(t, err) - // Read dir - entries, err := os.ReadDir(filepath.Join(dirPath, "gno.land", "p", "demo")) - require.Nil(t, err) + // Read dir + entries, err := os.ReadDir(filepath.Join(dirPath, "gno.land", "p", "demo")) + require.Nil(t, err) - // Check dir entries - assert.Equal(t, len(tc.requirements), len(entries)) - for _, e := range entries { - assert.Contains(t, tc.requirements, e.Name()) - } + // Check dir entries + assert.Equal(t, len(tc.requirements), len(entries)) + for _, e := range entries { + assert.Contains(t, tc.requirements, e.Name()) + } - // Check logs - for _, c := range tc.stdOutContains { - assert.Contains(t, buf.String(), c) - } + // Check logs + for _, c := range tc.stdOutContains { + assert.Contains(t, buf.String(), c) + } - buf.Reset() + buf.Reset() - // Try fetching again. Should be cached - tc.modFile.FetchDeps(dirPath, testRemote, true) - for _, c := range tc.cachedStdOutContains { - assert.Contains(t, buf.String(), c) + // Try fetching again. Should be cached + tc.modFile.FetchDeps(dirPath, testRemote, true) + for _, c := range tc.cachedStdOutContains { + assert.Contains(t, buf.String(), c) + } } }) } From c33c9f21b398538a9b6d30eabc33057c698b45e6 Mon Sep 17 00:00:00 2001 From: deelawn Date: Wed, 8 Nov 2023 16:29:22 +0000 Subject: [PATCH 69/93] fix: txtar tests load full config and execute serially (#1342) This fixes an issue where executing txtar tests would fail because the example packages weren't loaded into memory.
Contributors' checklist... - [x] 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).
--------- Signed-off-by: gfanton <8671905+gfanton@users.noreply.github.com> Co-authored-by: gfanton <8671905+gfanton@users.noreply.github.com> --- gno.land/cmd/gnoland/integration_test.go | 3 +- gno.land/cmd/gnoland/testdata/import.txtar | 27 ++++++++++ gno.land/pkg/gnoland/node_inmemory.go | 8 +-- gno.land/pkg/integration/integration_test.go | 4 +- .../pkg/integration/testing_integration.go | 53 ++++++++++++------- 5 files changed, 67 insertions(+), 28 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/import.txtar diff --git a/gno.land/cmd/gnoland/integration_test.go b/gno.land/cmd/gnoland/integration_test.go index 78c8e94fa09..37451df9704 100644 --- a/gno.land/cmd/gnoland/integration_test.go +++ b/gno.land/cmd/gnoland/integration_test.go @@ -4,9 +4,8 @@ import ( "testing" "github.com/gnolang/gno/gno.land/pkg/integration" - "github.com/rogpeppe/go-internal/testscript" ) func TestTestdata(t *testing.T) { - testscript.Run(t, integration.SetupGnolandTestScript(t, "testdata")) + integration.RunGnolandTestscripts(t, "testdata") } diff --git a/gno.land/cmd/gnoland/testdata/import.txtar b/gno.land/cmd/gnoland/testdata/import.txtar new file mode 100644 index 00000000000..3ad070654cf --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/import.txtar @@ -0,0 +1,27 @@ +# test that the example packages directory is loaded and usable. + +## start a new node +gnoland start + +gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/importtest -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 +stdout OK! + +## execute Render +gnokey maketx call -pkgpath gno.land/r/importtest -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1 +stdout '("92054" string)' +stdout OK! + +-- gno.mod -- +module gno.land/r/importtest + +-- import.gno -- +package importtest + +import ( + "gno.land/p/demo/ufmt" +) + +func Render(_ string) string { + return ufmt.Sprintf("%d", 92054) +} + diff --git a/gno.land/pkg/gnoland/node_inmemory.go b/gno.land/pkg/gnoland/node_inmemory.go index a0ab6a51e82..0edef304281 100644 --- a/gno.land/pkg/gnoland/node_inmemory.go +++ b/gno.land/pkg/gnoland/node_inmemory.go @@ -128,9 +128,9 @@ func NewInMemoryNode(logger log.Logger, cfg *InMemoryNodeConfig) (*node.Node, er ) // Create genesis factory - genProvider := func() (*bft.GenesisDoc, error) { - return cfg.Genesis, nil - } + genProvider := func() (*bft.GenesisDoc, error) { return cfg.Genesis, nil } + + dbProvider := func(*node.DBContext) (db.DB, error) { return db.NewMemDB(), nil } // generate p2p node identity // XXX: do we need to configur @@ -141,7 +141,7 @@ func NewInMemoryNode(logger log.Logger, cfg *InMemoryNodeConfig) (*node.Node, er cfg.PrivValidator, nodekey, appClientCreator, genProvider, - node.DefaultDBProvider, + dbProvider, logger, ) } diff --git a/gno.land/pkg/integration/integration_test.go b/gno.land/pkg/integration/integration_test.go index 3c22a190d64..a474791de2e 100644 --- a/gno.land/pkg/integration/integration_test.go +++ b/gno.land/pkg/integration/integration_test.go @@ -2,10 +2,8 @@ package integration import ( "testing" - - "github.com/rogpeppe/go-internal/testscript" ) func TestTestdata(t *testing.T) { - testscript.Run(t, SetupGnolandTestScript(t, "testdata")) + RunGnolandTestscripts(t, "testdata") } diff --git a/gno.land/pkg/integration/testing_integration.go b/gno.land/pkg/integration/testing_integration.go index b773317513f..b10686b0105 100644 --- a/gno.land/pkg/integration/testing_integration.go +++ b/gno.land/pkg/integration/testing_integration.go @@ -8,7 +8,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "testing" "github.com/gnolang/gno/gno.land/pkg/gnoland" @@ -20,21 +19,44 @@ import ( "github.com/rogpeppe/go-internal/testscript" ) +type tSeqShim struct{ *testing.T } + +// noop Parallel method allow us to run test sequentially +func (tSeqShim) Parallel() {} + +func (t tSeqShim) Run(name string, f func(testscript.T)) { + t.T.Run(name, func(t *testing.T) { + f(tSeqShim{t}) + }) +} + +func (t tSeqShim) Verbose() bool { + return testing.Verbose() +} + +// RunGnolandTestscripts sets up and runs txtar integration tests for gnoland nodes. +// It prepares an in-memory gnoland node and initializes the necessary environment and custom commands. +// The function adapts the test setup for use with the testscript package, enabling +// the execution of gnoland and gnokey commands within txtar scripts. +// +// Refer to package documentation in doc.go for more information on commands and example txtar scripts. +func RunGnolandTestscripts(t *testing.T, txtarDir string) { + t.Helper() + + p := setupGnolandTestScript(t, txtarDir) + if deadline, ok := t.Deadline(); ok && p.Deadline.IsZero() { + p.Deadline = deadline + } + + testscript.RunT(tSeqShim{t}, p) +} + type testNode struct { *node.Node nGnoKeyExec uint // Counter for execution of gnokey. } -// SetupGnolandTestScript prepares the test environment to execute txtar tests -// using a partial InMemory gnoland node. It initializes key storage, sets up the gnoland node, -// and provides custom commands like "gnoland" and "gnokey" for txtar script execution. -// -// The function returns testscript.Params which contain the test setup and command -// executions to be used with the testscript package. -// -// For a detailed explanation of the commands and their behaviors, as well as -// example txtar scripts, refer to the package documentation in doc.go. -func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { +func setupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { t.Helper() tmpdir := t.TempDir() @@ -47,7 +69,6 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { gnoHomeDir := filepath.Join(tmpdir, "gno") // Testscripts run concurrently by default, so we need to be prepared for that. - var muNodes sync.Mutex nodes := map[string]*testNode{} updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) @@ -99,9 +120,6 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { }, Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){ "gnoland": func(ts *testscript.TestScript, neg bool, args []string) { - muNodes.Lock() - defer muNodes.Unlock() - if len(args) == 0 { tsValidateError(ts, "gnoland", neg, fmt.Errorf("syntax: gnoland [start|stop]")) return @@ -125,7 +143,7 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { t := TSTestingT(ts) // Generate config and node - cfg := TestingMinimalNodeConfig(t, gnoRootDir) + cfg, _ := TestingNodeConfig(t, gnoRootDir) n, remoteAddr := TestingInMemoryNode(t, logger, cfg) // Register cleanup @@ -156,9 +174,6 @@ func SetupGnolandTestScript(t *testing.T, txtarDir string) testscript.Params { tsValidateError(ts, "gnoland "+cmd, neg, err) }, "gnokey": func(ts *testscript.TestScript, neg bool, args []string) { - muNodes.Lock() - defer muNodes.Unlock() - logger := ts.Value("_logger").(log.Logger) // grab logger sid := ts.Getenv("SID") // grab session id From e803c45154cfda03de5542b9cb1a29d22a68f031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 9 Nov 2023 15:07:58 +0100 Subject: [PATCH 70/93] test: make tests run in parallel (#1312) ## Description This PR simply introduces `t.Parallel()` for unit tests that can support it.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] 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: Guilhem Fanton <8671905+gfanton@users.noreply.github.com> --- gno.land/cmd/gnoland/start_test.go | 5 + gno.land/pkg/integration/integration_test.go | 2 + gnovm/pkg/gnolang/alloc_test.go | 2 + gnovm/pkg/gnolang/gno_test.go | 32 +++++ gnovm/pkg/gnolang/go2gno_test.go | 2 + gnovm/pkg/gnolang/gonative_test.go | 4 + gnovm/pkg/gnolang/package_test.go | 2 + gnovm/pkg/gnolang/precompile_test.go | 4 + gnovm/pkg/gnolang/values_conversions_test.go | 2 + gnovm/pkg/repl/repl_test.go | 4 + gnovm/tests/selector_test.go | 2 + misc/docker-integration/integration_test.go | 2 + tm2/pkg/amino/amino_test.go | 14 +++ tm2/pkg/amino/binary_encode_test.go | 4 + tm2/pkg/amino/binary_test.go | 22 ++++ tm2/pkg/amino/byteslice_test.go | 2 + tm2/pkg/amino/codec_test.go | 16 +++ tm2/pkg/amino/deep_copy_test.go | 22 ++++ tm2/pkg/amino/encoder_test.go | 5 + tm2/pkg/amino/gengo/gengo_test.go | 2 + tm2/pkg/amino/genproto/bindings_test.go | 2 + tm2/pkg/amino/genproto/genproto_test.go | 2 + tm2/pkg/amino/genproto/types_test.go | 2 + tm2/pkg/amino/json_test.go | 16 +++ tm2/pkg/amino/libs/press/press_test.go | 18 +++ tm2/pkg/amino/pkg/pkg_test.go | 6 + tm2/pkg/amino/reflect_test.go | 66 ++++++++-- tm2/pkg/amino/repr_test.go | 4 + .../amino/tests/proto3/proto3_compat_test.go | 26 ++++ tm2/pkg/amino/time2_test.go | 4 + tm2/pkg/amino/wellknown_test.go | 2 + tm2/pkg/async/async_test.go | 8 +- tm2/pkg/autofile/group_test.go | 14 +++ tm2/pkg/bech32/bech32_test.go | 6 + tm2/pkg/bft/blockchain/pool_test.go | 6 + tm2/pkg/bft/blockchain/reactor_test.go | 22 +++- tm2/pkg/bft/config/toml_test.go | 4 + tm2/pkg/bft/consensus/mempool_test.go | 8 ++ tm2/pkg/bft/consensus/reactor_test.go | 44 +++++++ tm2/pkg/bft/consensus/replay_test.go | 18 +++ tm2/pkg/bft/consensus/state_test.go | 56 +++++++++ .../consensus/types/height_vote_set_test.go | 2 + tm2/pkg/bft/consensus/wal_test.go | 2 + tm2/pkg/bft/mempool/cache_test.go | 4 + tm2/pkg/bft/mempool/reactor_test.go | 12 ++ tm2/pkg/bft/privval/file_test.go | 16 +++ tm2/pkg/bft/privval/signer_client_test.go | 20 ++++ .../privval/signer_listener_endpoint_test.go | 4 + tm2/pkg/bft/privval/socket_dialers_test.go | 4 + tm2/pkg/bft/privval/socket_listeners_test.go | 4 + tm2/pkg/bft/privval/utils_test.go | 2 + tm2/pkg/bft/rpc/client/helpers_test.go | 2 + tm2/pkg/bft/rpc/client/rpc_test.go | 34 ++++++ tm2/pkg/bft/rpc/core/blocks_test.go | 2 + tm2/pkg/bft/rpc/core/net_test.go | 4 + tm2/pkg/bft/rpc/core/pipe_test.go | 4 + tm2/pkg/bft/rpc/core/types/responses_test.go | 2 + tm2/pkg/bft/rpc/lib/client/args_test.go | 2 + .../bft/rpc/lib/client/http_client_test.go | 6 + .../bft/rpc/lib/client/integration_test.go | 2 + tm2/pkg/bft/rpc/lib/client/ws_client_test.go | 8 ++ tm2/pkg/bft/rpc/lib/rpc_test.go | 12 ++ tm2/pkg/bft/rpc/lib/server/handlers_test.go | 12 ++ .../bft/rpc/lib/server/http_server_test.go | 8 ++ tm2/pkg/bft/rpc/lib/server/parse_test.go | 8 ++ tm2/pkg/bft/rpc/lib/types/types_test.go | 6 + tm2/pkg/bft/state/execution_test.go | 16 +++ tm2/pkg/bft/state/state_test.go | 32 +++++ tm2/pkg/bft/state/store_test.go | 2 + tm2/pkg/bft/state/validation_test.go | 4 + tm2/pkg/bft/store/store_test.go | 12 ++ tm2/pkg/bft/types/block_test.go | 38 ++++++ tm2/pkg/bft/types/evidence_test.go | 16 +++ tm2/pkg/bft/types/genesis_test.go | 8 ++ tm2/pkg/bft/types/params_test.go | 6 + tm2/pkg/bft/types/part_set_test.go | 12 ++ tm2/pkg/bft/types/proposal_test.go | 10 ++ tm2/pkg/bft/types/results_test.go | 4 + tm2/pkg/bft/types/time/time_test.go | 2 + tm2/pkg/bft/types/tx_test.go | 8 ++ tm2/pkg/bft/types/validator_set_test.go | 52 +++++++- tm2/pkg/bft/types/vote_set_test.go | 12 ++ tm2/pkg/bft/types/vote_test.go | 24 ++++ tm2/pkg/bft/wal/wal_test.go | 8 ++ tm2/pkg/bitarray/bit_array_test.go | 20 ++++ tm2/pkg/clist/clist_test.go | 113 ++---------------- tm2/pkg/cmap/cmap_test.go | 4 + tm2/pkg/commands/commands_test.go | 25 ++-- tm2/pkg/crypto/bcrypt/bcrypt_test.go | 22 ++++ tm2/pkg/crypto/bech32_test.go | 4 + tm2/pkg/crypto/bip39/bip39_test.go | 10 ++ tm2/pkg/crypto/ed25519/ed25519_test.go | 2 + tm2/pkg/crypto/hd/fundraiser_test.go | 2 + tm2/pkg/crypto/hd/hdpath_test.go | 6 + tm2/pkg/crypto/keys/keybase_test.go | 18 +++ tm2/pkg/crypto/keys/types_test.go | 2 + tm2/pkg/crypto/merkle/proof_key_path_test.go | 2 + tm2/pkg/crypto/merkle/proof_test.go | 2 + tm2/pkg/crypto/merkle/rfc6962_test.go | 6 + tm2/pkg/crypto/merkle/simple_map_test.go | 2 + tm2/pkg/crypto/merkle/simple_proof_test.go | 4 + tm2/pkg/crypto/merkle/simple_tree_test.go | 6 + tm2/pkg/crypto/mock/mock_test.go | 2 + .../bitarray/compact_bit_array_test.go | 16 +++ .../crypto/multisig/threshold_pubkey_test.go | 10 ++ tm2/pkg/crypto/random_test.go | 2 + .../crypto/secp256k1/secp256k1_cgo_test.go | 4 + .../secp256k1/secp256k1_internal_test.go | 4 + .../crypto/secp256k1/secp256k1_nocgo_test.go | 2 + tm2/pkg/crypto/secp256k1/secp256k1_test.go | 10 ++ tm2/pkg/crypto/tmhash/hash_test.go | 4 + .../crypto/xchacha20poly1305/vector_test.go | 4 + .../xchacha20poly1305/xchachapoly_test.go | 2 + .../xsalsa20symmetric/symmetric_test.go | 4 + tm2/pkg/db/backend_test.go | 12 +- tm2/pkg/db/boltdb_test.go | 2 + tm2/pkg/db/c_level_db_test.go | 4 + tm2/pkg/db/db_test.go | 26 ++++ tm2/pkg/db/go_level_db_test.go | 2 + tm2/pkg/db/gorocks_db_test.go | 4 + tm2/pkg/db/grocks_db_test.go | 4 + tm2/pkg/db/prefix_db_test.go | 24 ++++ tm2/pkg/db/util_test.go | 20 ++++ tm2/pkg/errors/errors_test.go | 16 +++ tm2/pkg/events/events_test.go | 12 ++ tm2/pkg/flow/io_test.go | 4 + tm2/pkg/iavl/basic_test.go | 16 +++ tm2/pkg/iavl/common/random_test.go | 4 + tm2/pkg/iavl/proof_forgery_test.go | 2 + tm2/pkg/iavl/proof_test.go | 6 + tm2/pkg/iavl/tree_dotgraph_test.go | 2 + tm2/pkg/iavl/tree_fuzz_test.go | 2 + tm2/pkg/iavl/tree_test.go | 64 ++++++++++ tm2/pkg/log/tm_logger_test.go | 2 + tm2/pkg/os/net_test.go | 2 + tm2/pkg/p2p/conn/connection_test.go | 28 +++++ tm2/pkg/p2p/conn/secret_connection_test.go | 18 +++ tm2/pkg/p2p/key_test.go | 6 +- tm2/pkg/p2p/netaddress_test.go | 16 +++ tm2/pkg/p2p/node_info_test.go | 4 + tm2/pkg/p2p/peer_test.go | 4 + tm2/pkg/p2p/switch_test.go | 28 +++++ tm2/pkg/p2p/transport_test.go | 22 ++++ tm2/pkg/random/random_test.go | 8 ++ tm2/pkg/sdk/auth/ante_test.go | 41 ++++++- tm2/pkg/sdk/auth/keeper_test.go | 4 + tm2/pkg/sdk/bank/handler_test.go | 6 + tm2/pkg/sdk/bank/keeper_test.go | 6 + tm2/pkg/sdk/baseapp_test.go | 51 ++++++-- tm2/pkg/service/service_test.go | 4 + tm2/pkg/std/coin_test.go | 62 ++++++++++ tm2/pkg/std/kvpair_test.go | 4 + tm2/pkg/store/cache/store_test.go | 20 ++++ tm2/pkg/store/gas/store_test.go | 8 ++ tm2/pkg/store/iavl/store_test.go | 26 ++++ tm2/pkg/store/prefix/store_test.go | 30 +++++ tm2/pkg/store/rootmulti/proof_test.go | 8 ++ tm2/pkg/store/rootmulti/store_test.go | 14 +++ tm2/pkg/store/types/gas_test.go | 4 + tm2/pkg/strings/string_test.go | 8 ++ tm2/pkg/timer/throttle_timer_test.go | 2 + 161 files changed, 1765 insertions(+), 143 deletions(-) diff --git a/gno.land/cmd/gnoland/start_test.go b/gno.land/cmd/gnoland/start_test.go index 27ef2f572ea..6606d88ba2e 100644 --- a/gno.land/cmd/gnoland/start_test.go +++ b/gno.land/cmd/gnoland/start_test.go @@ -13,6 +13,8 @@ import ( ) func TestStartInitialize(t *testing.T) { + t.Parallel() + cases := []struct { args []string }{ @@ -23,8 +25,11 @@ func TestStartInitialize(t *testing.T) { os.Chdir(filepath.Join("..", "..")) // go to repo's root dir for _, tc := range cases { + tc := tc name := strings.Join(tc.args, " ") t.Run(name, func(t *testing.T) { + t.Parallel() + mockOut := bytes.NewBufferString("") mockErr := bytes.NewBufferString("") io := commands.NewTestIO() diff --git a/gno.land/pkg/integration/integration_test.go b/gno.land/pkg/integration/integration_test.go index a474791de2e..f88707f2b90 100644 --- a/gno.land/pkg/integration/integration_test.go +++ b/gno.land/pkg/integration/integration_test.go @@ -5,5 +5,7 @@ import ( ) func TestTestdata(t *testing.T) { + t.Parallel() + RunGnolandTestscripts(t, "testdata") } diff --git a/gnovm/pkg/gnolang/alloc_test.go b/gnovm/pkg/gnolang/alloc_test.go index e3b9f1cb069..dbb3903f862 100644 --- a/gnovm/pkg/gnolang/alloc_test.go +++ b/gnovm/pkg/gnolang/alloc_test.go @@ -6,6 +6,8 @@ import ( ) func TestAllocSizes(t *testing.T) { + t.Parallel() + // go elemental println("_allocPointer", unsafe.Sizeof(&StructValue{})) println("_allocSlice", unsafe.Sizeof([]byte("12345678901234567890123456789012345678901234567890"))) diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go index 5ed5971c836..37b590b1c4c 100644 --- a/gnovm/pkg/gnolang/gno_test.go +++ b/gnovm/pkg/gnolang/gno_test.go @@ -13,6 +13,8 @@ import ( // run empty main(). func TestRunEmptyMain(t *testing.T) { + t.Parallel() + m := NewMachine("test", nil) main := FuncD("main", nil, nil, nil) m.RunDeclaration(main) @@ -21,6 +23,8 @@ func TestRunEmptyMain(t *testing.T) { // run main() with a for loop. func TestRunLoopyMain(t *testing.T) { + t.Parallel() + m := NewMachine("test", nil) c := `package test func main() { @@ -36,6 +40,8 @@ func main() { } func TestEval(t *testing.T) { + t.Parallel() + m := NewMachine("test", nil) c := `package test func next(i int) int { @@ -64,6 +70,8 @@ func assertOutput(t *testing.T, input string, output string) { } func TestRunMakeStruct(t *testing.T) { + t.Parallel() + assertOutput(t, `package test type Outfit struct { Scarf string @@ -90,6 +98,8 @@ func main() { } func TestRunReturnStruct(t *testing.T) { + t.Parallel() + assertOutput(t, `package test type MyStruct struct { FieldA string @@ -160,6 +170,8 @@ type Struct1 struct { } func TestModifyTypeAsserted(t *testing.T) { + t.Parallel() + x := Struct1{1, 1} var v interface{} = x x2 := v.(Struct1) @@ -176,6 +188,8 @@ type Interface1 interface { } func TestTypeConversion(t *testing.T) { + t.Parallel() + x := 1 var v interface{} = x if _, ok := v.(Interface1); ok { @@ -193,6 +207,8 @@ func TestTypeConversion(t *testing.T) { } func TestSomething(t *testing.T) { + t.Parallel() + type Foo struct { X interface{} } @@ -210,6 +226,8 @@ func TestSomething(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestDeferOrder(t *testing.T) { + t.Parallel() + a := func() func(int, int) int { fmt.Println("a constructed") return func(x int, y int) int { @@ -238,6 +256,8 @@ func TestDeferOrder(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestCallOrder(t *testing.T) { + t.Parallel() + a := func() func(int, int) int { fmt.Println("a constructed") return func(x int, y int) int { @@ -264,6 +284,8 @@ func TestCallOrder(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestBinaryShortCircuit(t *testing.T) { + t.Parallel() + tr := func() bool { fmt.Println("t called") return true @@ -281,6 +303,8 @@ func TestBinaryShortCircuit(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestSwitchDefine(t *testing.T) { + t.Parallel() + var x interface{} = 1 switch y := x.(type) { case int: @@ -292,6 +316,8 @@ func TestSwitchDefine(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestBinaryCircuit(t *testing.T) { + t.Parallel() + tr := func() bool { fmt.Println("tr() called") return true @@ -316,6 +342,8 @@ func TestBinaryCircuit(t *testing.T) { } func TestMultiAssignment(t *testing.T) { + t.Parallel() + buf := make([]int, 4) ref := func(i int) *int { fmt.Printf("ref(%v) called\n", i) @@ -342,6 +370,8 @@ func TestMultiAssignment(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestCallLHS(t *testing.T) { + t.Parallel() + x := 1 xptr := func() *int { return &x @@ -352,6 +382,8 @@ func TestCallLHS(t *testing.T) { // XXX is there a way to test in Go as well as Gno? func TestCallFieldLHS(t *testing.T) { + t.Parallel() + type str struct { X int } diff --git a/gnovm/pkg/gnolang/go2gno_test.go b/gnovm/pkg/gnolang/go2gno_test.go index 6a1413a07e1..d5b94c618b0 100644 --- a/gnovm/pkg/gnolang/go2gno_test.go +++ b/gnovm/pkg/gnolang/go2gno_test.go @@ -8,6 +8,8 @@ import ( ) func TestParseForLoop(t *testing.T) { + t.Parallel() + gocode := `package main func main(){ for i:=0; i<10; i++ { diff --git a/gnovm/pkg/gnolang/gonative_test.go b/gnovm/pkg/gnolang/gonative_test.go index b9b659aae99..e348ffe9c22 100644 --- a/gnovm/pkg/gnolang/gonative_test.go +++ b/gnovm/pkg/gnolang/gonative_test.go @@ -98,6 +98,8 @@ D: } func TestGoNativeDefine3(t *testing.T) { + t.Parallel() + // Create package foo and define Foo. out := new(bytes.Buffer) pkg := NewPackageNode("foo", "test.foo", nil) @@ -135,6 +137,8 @@ D: } func TestCrypto(t *testing.T) { + t.Parallel() + addr := crypto.Address{} store := gonativeTestStore() tv := Go2GnoValue(nilAllocator, store, reflect.ValueOf(addr)) diff --git a/gnovm/pkg/gnolang/package_test.go b/gnovm/pkg/gnolang/package_test.go index 47efd736bac..f219ba9b23f 100644 --- a/gnovm/pkg/gnolang/package_test.go +++ b/gnovm/pkg/gnolang/package_test.go @@ -12,6 +12,8 @@ import ( // Tries to reproduce the bug #1036 on all registered types func TestAminoJSONRegisteredTypes(t *testing.T) { + t.Parallel() + for _, typ := range Package.Types { // Instantiate registered type x := reflect.New(typ.Type).Interface() diff --git a/gnovm/pkg/gnolang/precompile_test.go b/gnovm/pkg/gnolang/precompile_test.go index 3d50353505f..e2cb5e92d86 100644 --- a/gnovm/pkg/gnolang/precompile_test.go +++ b/gnovm/pkg/gnolang/precompile_test.go @@ -12,6 +12,8 @@ import ( ) func TestPrecompile(t *testing.T) { + t.Parallel() + cases := []struct { name string source string @@ -56,6 +58,8 @@ func TestPrecompile(t *testing.T) { for _, c := range cases { c := c // scopelint t.Run(c.name, func(t *testing.T) { + t.Parallel() + // parse gno fset := token.NewFileSet() f, err := parser.ParseFile(fset, "foo.go", c.source, parser.ParseComments) diff --git a/gnovm/pkg/gnolang/values_conversions_test.go b/gnovm/pkg/gnolang/values_conversions_test.go index b9ac9e68d68..e92496c1ac2 100644 --- a/gnovm/pkg/gnolang/values_conversions_test.go +++ b/gnovm/pkg/gnolang/values_conversions_test.go @@ -9,6 +9,8 @@ import ( ) func TestConvertUntypedBigdecToFloat(t *testing.T) { + t.Parallel() + dst := &TypedValue{} dec, err := apd.New(-math.MaxInt64, -4).SetFloat64(math.SmallestNonzeroFloat64 / 2) diff --git a/gnovm/pkg/repl/repl_test.go b/gnovm/pkg/repl/repl_test.go index 3c4d1f7c6c6..09c350dd49a 100644 --- a/gnovm/pkg/repl/repl_test.go +++ b/gnovm/pkg/repl/repl_test.go @@ -199,6 +199,8 @@ func TestRepl(t *testing.T) { } func TestReplOpts(t *testing.T) { + t.Parallel() + require := require.New(t) r := NewRepl(WithStd(nil, nil, nil), WithStore(nil)) @@ -212,6 +214,8 @@ func TestReplOpts(t *testing.T) { } func TestReplReset(t *testing.T) { + t.Parallel() + require := require.New(t) r := NewRepl() diff --git a/gnovm/tests/selector_test.go b/gnovm/tests/selector_test.go index 4d6e9c587b0..1f0b400555b 100644 --- a/gnovm/tests/selector_test.go +++ b/gnovm/tests/selector_test.go @@ -52,6 +52,8 @@ func _printValue(x interface{}) { } func TestSelectors(t *testing.T) { + t.Parallel() + x0 := struct{ F0 int }{1} _printValue(x0.F0) // *ST.F0 // F:0 diff --git a/misc/docker-integration/integration_test.go b/misc/docker-integration/integration_test.go index 6ae0ea1d36e..1142709cc16 100644 --- a/misc/docker-integration/integration_test.go +++ b/misc/docker-integration/integration_test.go @@ -29,6 +29,8 @@ const ( ) func TestDockerIntegration(t *testing.T) { + t.Parallel() + tmpdir, err := os.MkdirTemp(os.TempDir(), "*-gnoland-integration") require.NoError(t, err) diff --git a/tm2/pkg/amino/amino_test.go b/tm2/pkg/amino/amino_test.go index a06601a43da..d7dd0dc5b98 100644 --- a/tm2/pkg/amino/amino_test.go +++ b/tm2/pkg/amino/amino_test.go @@ -10,6 +10,8 @@ import ( ) func TestMarshal(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type SimpleStruct struct { @@ -35,6 +37,8 @@ func TestMarshal(t *testing.T) { } func TestUnmarshalReader(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type SimpleStruct struct { @@ -65,6 +69,8 @@ type stringWrapper struct { } func TestUnmarshalReaderSize(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() s1 := stringWrapper{"foo"} @@ -82,6 +88,8 @@ func TestUnmarshalReaderSize(t *testing.T) { } func TestUnmarshalReaderSizeLimit(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() s1 := stringWrapper{"foo"} @@ -101,6 +109,8 @@ func TestUnmarshalReaderSizeLimit(t *testing.T) { } func TestUnmarshalReaderTooLong(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type SimpleStruct struct { @@ -125,6 +135,8 @@ func TestUnmarshalReaderTooLong(t *testing.T) { } func TestUnmarshalBufferedWritesReads(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() buf := bytes.NewBuffer(nil) @@ -155,6 +167,8 @@ func TestUnmarshalBufferedWritesReads(t *testing.T) { } func TestBoolPointers(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type SimpleStruct struct { BoolPtrTrue *bool diff --git a/tm2/pkg/amino/binary_encode_test.go b/tm2/pkg/amino/binary_encode_test.go index 8208bb74c67..a775a911cf6 100644 --- a/tm2/pkg/amino/binary_encode_test.go +++ b/tm2/pkg/amino/binary_encode_test.go @@ -8,6 +8,8 @@ import ( ) func TestEncodeFieldNumberAndTyp3_1(t *testing.T) { + t.Parallel() + buf := new(bytes.Buffer) err := encodeFieldNumberAndTyp3(buf, 1, Typ3ByteLength) assert.Nil(t, err) @@ -15,6 +17,8 @@ func TestEncodeFieldNumberAndTyp3_1(t *testing.T) { } func TestEncodeFieldNumberAndTyp3_2(t *testing.T) { + t.Parallel() + buf := new(bytes.Buffer) err := encodeFieldNumberAndTyp3(buf, 2, Typ3ByteLength) assert.Nil(t, err) diff --git a/tm2/pkg/amino/binary_test.go b/tm2/pkg/amino/binary_test.go index 8516ac2e1fa..e50427afeef 100644 --- a/tm2/pkg/amino/binary_test.go +++ b/tm2/pkg/amino/binary_test.go @@ -12,6 +12,8 @@ import ( ) func TestNilSliceEmptySlice(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type TestStruct struct { @@ -59,6 +61,8 @@ func TestNilSliceEmptySlice(t *testing.T) { } func TestNewFieldBackwardsCompatibility(t *testing.T) { + t.Parallel() + type V1 struct { String string String2 string @@ -109,6 +113,8 @@ func TestNewFieldBackwardsCompatibility(t *testing.T) { } func TestWriteEmpty(t *testing.T) { + t.Parallel() + type Inner struct { Val int } @@ -136,6 +142,8 @@ func TestWriteEmpty(t *testing.T) { } func TestForceWriteEmpty(t *testing.T) { + t.Parallel() + type InnerWriteEmpty struct { // sth. that isn't zero-len if default, e.g. fixed32: ValIn int32 `amino:"write_empty" binary:"fixed32"` @@ -158,6 +166,8 @@ func TestForceWriteEmpty(t *testing.T) { } func TestStructSlice(t *testing.T) { + t.Parallel() + type Foo struct { A uint B uint @@ -182,6 +192,8 @@ func TestStructSlice(t *testing.T) { } func TestStructPointerSlice1(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type Foo struct { @@ -220,6 +232,8 @@ func TestStructPointerSlice1(t *testing.T) { // Like TestStructPointerSlice2, but without nil_elements field tag. func TestStructPointerSlice2(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() type Foo struct { @@ -251,6 +265,8 @@ func TestStructPointerSlice2(t *testing.T) { } func TestBasicTypes(t *testing.T) { + t.Parallel() + // we explicitly disallow type definitions like the following: type byteAlias []byte @@ -268,6 +284,8 @@ func TestBasicTypes(t *testing.T) { } func TestUnmarshalMapBinary(t *testing.T) { + t.Parallel() + obj := new(map[string]int) cdc := amino.NewCodec() @@ -291,6 +309,8 @@ func TestUnmarshalMapBinary(t *testing.T) { } func TestUnmarshalFuncBinary(t *testing.T) { + t.Parallel() + obj := func() {} cdc := amino.NewCodec() // Binary doesn't support decoding to a func... @@ -316,6 +336,8 @@ func TestUnmarshalFuncBinary(t *testing.T) { } func TestDuration(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() d0 := time.Duration(0) bz := cdc.MustMarshal(d0) diff --git a/tm2/pkg/amino/byteslice_test.go b/tm2/pkg/amino/byteslice_test.go index 310b294a8b7..80faca8e4e9 100644 --- a/tm2/pkg/amino/byteslice_test.go +++ b/tm2/pkg/amino/byteslice_test.go @@ -6,6 +6,8 @@ import ( ) func TestReadByteSliceEquality(t *testing.T) { + t.Parallel() + var encoded []byte var err error cdc := NewCodec() diff --git a/tm2/pkg/amino/codec_test.go b/tm2/pkg/amino/codec_test.go index 18bc8f2cd5d..9368d4ef40e 100644 --- a/tm2/pkg/amino/codec_test.go +++ b/tm2/pkg/amino/codec_test.go @@ -29,6 +29,8 @@ func newSimpleStruct() SimpleStruct { } func TestMarshalUnmarshalPointer0(t *testing.T) { + t.Parallel() + s := newSimpleStruct() cdc := amino.NewCodec() b, err := cdc.MarshalSized(s) // no indirection @@ -41,6 +43,8 @@ func TestMarshalUnmarshalPointer0(t *testing.T) { } func TestMarshalUnmarshalPointer1(t *testing.T) { + t.Parallel() + s := newSimpleStruct() cdc := amino.NewCodec() b, err := cdc.MarshalSized(&s) // extra indirection @@ -53,6 +57,8 @@ func TestMarshalUnmarshalPointer1(t *testing.T) { } func TestMarshalUnmarshalPointer2(t *testing.T) { + t.Parallel() + s := newSimpleStruct() ptr := &s cdc := amino.NewCodec() @@ -63,6 +69,8 @@ func TestMarshalUnmarshalPointer2(t *testing.T) { } func TestMarshalUnmarshalPointer3(t *testing.T) { + t.Parallel() + s := newSimpleStruct() cdc := amino.NewCodec() b, err := cdc.MarshalSized(s) // no indirection @@ -75,6 +83,8 @@ func TestMarshalUnmarshalPointer3(t *testing.T) { } func TestDecodeVarint8(t *testing.T) { + t.Parallel() + // DecodeVarint8 uses binary.Varint so we need to make // sure that all the values out of the range of [-128, 127] // return an error. @@ -122,6 +132,8 @@ func TestDecodeVarint8(t *testing.T) { } func TestDecodeVarint16(t *testing.T) { + t.Parallel() + // DecodeVarint16 uses binary.Varint so we need to make // sure that all the values out of the range of [-32768, 32767] // return an error. @@ -170,6 +182,8 @@ func TestDecodeVarint16(t *testing.T) { } func TestEncodeDecodeString(t *testing.T) { + t.Parallel() + s := "🔌🎉⛵︎♠️⎍" bs := []byte(s) di := len(bs) * 3 / 4 @@ -214,6 +228,8 @@ func TestEncodeDecodeString(t *testing.T) { } func TestCodecSeal(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.Seal() diff --git a/tm2/pkg/amino/deep_copy_test.go b/tm2/pkg/amino/deep_copy_test.go index 55aff4c6a60..f2a793bfca1 100644 --- a/tm2/pkg/amino/deep_copy_test.go +++ b/tm2/pkg/amino/deep_copy_test.go @@ -15,6 +15,8 @@ func (dcf *DCFoo1) MarshalAmino() (string, error) { return dcf.a, nil } func (dcf *DCFoo1) UnmarshalAmino(s string) error { dcf.a = s; return nil } func TestDeepCopyFoo1(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo1("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo1) assert.Equal(t, "foobar", dcf2.a) @@ -27,6 +29,8 @@ func (dcf DCFoo2) MarshalAmino() (string, error) { return dcf.a, nil } // non-p func (dcf *DCFoo2) UnmarshalAmino(s string) error { dcf.a = s; return nil } func TestDeepCopyFoo2(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo2("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo2) assert.Equal(t, "foobar", dcf2.a) @@ -39,6 +43,8 @@ func (dcf DCFoo3) MarshalAmino() (string, error) { return dcf.a, nil } func (dcf *DCFoo3) UnmarshalAmino(s []byte) error { dcf.a = string(s); return nil } // mismatch type func TestDeepCopyFoo3(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo3("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo3) assert.Equal(t, "", dcf2.a) @@ -52,6 +58,8 @@ func (dcf DCFoo4) MarshalAmino() (string, error) { return dcf.a, nil } func (dcf *DCFoo4) UnmarshalAmino(s string) error { dcf.a = s; return nil } // mismatch type func TestDeepCopyFoo4(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo4("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo4) assert.Equal(t, "good", dcf2.a) @@ -65,6 +73,8 @@ func (dcf DCFoo5) MarshalAmino() (string, error) { return dcf.a, nil } func (dcf *DCFoo5) UnmarshalAmino(s string) error { dcf.a = s; return nil } // mismatch type func TestDeepCopyFoo5(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo5("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo5) assert.Equal(t, "good", dcf2.a) @@ -76,6 +86,8 @@ func newDCFoo6(a string) *DCFoo6 { return &DCFoo6{a: a} } func (dcf *DCFoo6) DeepCopy() DCFoo6 { return DCFoo6{"good"} } func TestDeepCopyFoo6(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo6("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo6) assert.Equal(t, "good", dcf2.a) @@ -87,6 +99,8 @@ func newDCFoo7(a string) *DCFoo7 { return &DCFoo7{a: a} } func (dcf DCFoo7) DeepCopy() *DCFoo7 { return &DCFoo7{"good"} } func TestDeepCopyFoo7(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo7("foobar") dcf2 := amino.DeepCopy(dcf1).(*DCFoo7) assert.Equal(t, "good", dcf2.a) @@ -99,6 +113,8 @@ func (dcf DCFoo8) MarshalAmino() (string, error) { return "", errors.New("uh oh func (dcf *DCFoo8) UnmarshalAmino(s string) error { dcf.a = s; return nil } func TestDeepCopyFoo8(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo8("foobar") assert.Panics(t, func() { amino.DeepCopy(dcf1) }) } @@ -110,6 +126,8 @@ func (dcf DCFoo9) MarshalAmino() (string, error) { return dcf.a, nil } func (dcf *DCFoo9) UnmarshalAmino(s string) error { return errors.New("uh oh") } // error func TestDeepCopyFoo9(t *testing.T) { + t.Parallel() + dcf1 := newDCFoo9("foobar") assert.Panics(t, func() { amino.DeepCopy(dcf1) }) } @@ -119,12 +137,16 @@ type DCInterface1 struct { } func TestDeepCopyInterface1(t *testing.T) { + t.Parallel() + dci1 := DCInterface1{Foo: nil} dci2 := amino.DeepCopy(dci1).(DCInterface1) assert.Nil(t, dci2.Foo) } func TestDeepCopyInterface2(t *testing.T) { + t.Parallel() + dci1 := DCInterface1{Foo: "foo"} dci2 := amino.DeepCopy(dci1).(DCInterface1) assert.Equal(t, "foo", dci2.Foo) diff --git a/tm2/pkg/amino/encoder_test.go b/tm2/pkg/amino/encoder_test.go index 5c8afc4833d..02ece6fd667 100644 --- a/tm2/pkg/amino/encoder_test.go +++ b/tm2/pkg/amino/encoder_test.go @@ -7,6 +7,8 @@ import ( ) func TestUvarintSize(t *testing.T) { + t.Parallel() + testCases := []struct { name string u uint64 @@ -22,7 +24,10 @@ func TestUvarintSize(t *testing.T) { {"64 bits", 1 << 63, 10}, } for i, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.want, UvarintSize(tc.u), "#%d", i) //nolint:scopelint }) } diff --git a/tm2/pkg/amino/gengo/gengo_test.go b/tm2/pkg/amino/gengo/gengo_test.go index 88b25d9acdd..404b1c77c23 100644 --- a/tm2/pkg/amino/gengo/gengo_test.go +++ b/tm2/pkg/amino/gengo/gengo_test.go @@ -15,6 +15,8 @@ type SampleStruct struct { } func TestBasic(t *testing.T) { + t.Parallel() + p := press.NewPress() fmt.Println(p) ss := SampleStruct{"cat", "dog"} diff --git a/tm2/pkg/amino/genproto/bindings_test.go b/tm2/pkg/amino/genproto/bindings_test.go index 295c388def3..d1911d43542 100644 --- a/tm2/pkg/amino/genproto/bindings_test.go +++ b/tm2/pkg/amino/genproto/bindings_test.go @@ -11,6 +11,8 @@ import ( ) func TestGenerateProtoBindings(t *testing.T) { + t.Parallel() + file, err := GenerateProtoBindingsForTypes(tests.Package, tests.Package.ReflectTypes()...) assert.NoError(t, err) t.Logf("%v", file) diff --git a/tm2/pkg/amino/genproto/genproto_test.go b/tm2/pkg/amino/genproto/genproto_test.go index 20a45361519..2a52741c901 100644 --- a/tm2/pkg/amino/genproto/genproto_test.go +++ b/tm2/pkg/amino/genproto/genproto_test.go @@ -9,6 +9,8 @@ import ( ) func TestBasic(t *testing.T) { + t.Parallel() + p3c := NewP3Context() p3c.RegisterPackage(sm1.Package) p3doc := P3Doc{PackageName: "test"} diff --git a/tm2/pkg/amino/genproto/types_test.go b/tm2/pkg/amino/genproto/types_test.go index 8b7398ab184..4e57427e809 100644 --- a/tm2/pkg/amino/genproto/types_test.go +++ b/tm2/pkg/amino/genproto/types_test.go @@ -7,6 +7,8 @@ import ( ) func TestPrintP3Types(t *testing.T) { + t.Parallel() + doc := P3Doc{ Comment: "doc comment", Messages: []P3Message{ diff --git a/tm2/pkg/amino/json_test.go b/tm2/pkg/amino/json_test.go index 81118080d89..e3f7d413fcb 100644 --- a/tm2/pkg/amino/json_test.go +++ b/tm2/pkg/amino/json_test.go @@ -29,6 +29,8 @@ func registerTransports(cdc *amino.Codec) { } func TestMarshalJSON(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() registerTransports(cdc) cases := []struct { @@ -130,6 +132,8 @@ func TestMarshalJSON(t *testing.T) { } func TestMarshalJSONTime(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() registerTransports(cdc) @@ -180,6 +184,8 @@ type innerFP struct { // We don't support maps. func TestUnmarshalMap(t *testing.T) { + t.Parallel() + jsonBytes := []byte("dontcare") obj := new(map[string]int) cdc := amino.NewCodec() @@ -198,6 +204,8 @@ func TestUnmarshalMap(t *testing.T) { } func TestUnmarshalFunc(t *testing.T) { + t.Parallel() + jsonBytes := []byte(`"dontcare"`) obj := func() {} cdc := amino.NewCodec() @@ -218,6 +226,8 @@ func TestUnmarshalFunc(t *testing.T) { } func TestUnmarshalJSON(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() registerTransports(cdc) cases := []struct { @@ -310,6 +320,8 @@ func TestUnmarshalJSON(t *testing.T) { } func TestJSONCodecRoundTrip(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() registerTransports(cdc) type allInclusive struct { @@ -480,6 +492,8 @@ func interfacePtr(v interface{}) *interface{} { // Test to ensure that Amino codec's time encoding/decoding roundtrip // produces the same result as the standard library json's. func TestAminoJSONTimeEncodeDecodeRoundTrip(t *testing.T) { + t.Parallel() + loc, err := time.LoadLocation("America/Los_Angeles") require.NoError(t, err) din := time.Date(2008, 9, 15, 14, 13, 12, 11109876, loc).Round(time.Millisecond).UTC() @@ -503,6 +517,8 @@ func TestAminoJSONTimeEncodeDecodeRoundTrip(t *testing.T) { } func TestMarshalJSONIndent(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() registerTransports(cdc) obj := Transport{Vehicle: Car("Tesla")} diff --git a/tm2/pkg/amino/libs/press/press_test.go b/tm2/pkg/amino/libs/press/press_test.go index 3978e5a5f52..e05042f4556 100644 --- a/tm2/pkg/amino/libs/press/press_test.go +++ b/tm2/pkg/amino/libs/press/press_test.go @@ -7,11 +7,15 @@ import ( ) func TestEmpty(t *testing.T) { + t.Parallel() + p := NewPress() assert.Equal(t, p.Print(), "") } func TestBasic(t *testing.T) { + t.Parallel() + p := NewPress() p.P("this ") p.P("is ") @@ -20,6 +24,8 @@ func TestBasic(t *testing.T) { } func TestBasicLn(t *testing.T) { + t.Parallel() + p := NewPress() p.P("this ") p.P("is ") @@ -28,6 +34,8 @@ func TestBasicLn(t *testing.T) { } func TestNewlineStr(t *testing.T) { + t.Parallel() + p := NewPress().SetNewlineStr("\r\n") p.P("this ") p.P("is ") @@ -38,6 +46,8 @@ func TestNewlineStr(t *testing.T) { } func TestIndent(t *testing.T) { + t.Parallel() + p := NewPress() p.P("first line ") p.Pl("{").I(func(p *Press) { @@ -51,6 +61,8 @@ func TestIndent(t *testing.T) { } func TestIndent2(t *testing.T) { + t.Parallel() + p := NewPress() p.P("first line ") p.Pl("{").I(func(p *Press) { @@ -66,6 +78,8 @@ func TestIndent2(t *testing.T) { } func TestIndent3(t *testing.T) { + t.Parallel() + p := NewPress() p.P("first line ") p.Pl("{").I(func(p *Press) { @@ -78,6 +92,8 @@ func TestIndent3(t *testing.T) { } func TestIndentLn(t *testing.T) { + t.Parallel() + p := NewPress() p.P("first line ") p.Pl("{").I(func(p *Press) { @@ -92,6 +108,8 @@ func TestIndentLn(t *testing.T) { } func TestNestedIndent(t *testing.T) { + t.Parallel() + p := NewPress() p.P("first line ") p.Pl("{").I(func(p *Press) { diff --git a/tm2/pkg/amino/pkg/pkg_test.go b/tm2/pkg/amino/pkg/pkg_test.go index 2be0b0cefe3..1be87e88f4e 100644 --- a/tm2/pkg/amino/pkg/pkg_test.go +++ b/tm2/pkg/amino/pkg/pkg_test.go @@ -14,6 +14,8 @@ type Foo struct { } func TestNewPackage(t *testing.T) { + t.Parallel() + // This should panic, as slashes in p3pkg is not allowed. assert.Panics(t, func() { NewPackage("foobar.com/some/path", "some/path", "").WithTypes(Foo{}) @@ -39,6 +41,8 @@ func TestNewPackage(t *testing.T) { } func TestFullNameForType(t *testing.T) { + t.Parallel() + // The Go package depends on how this test is invoked. // Sometimes it is "github.com/gnolang/gno/tm2/pkg/amino/packagepkg_test". // Sometimes it is "command-line-arguments" @@ -55,6 +59,8 @@ func TestFullNameForType(t *testing.T) { // If the struct wasn't registered, you can't get a name or type_url for it. func TestFullNameForUnexpectedType(t *testing.T) { + t.Parallel() + gopkg := reflect.TypeOf(Foo{}).PkgPath() pkg := NewPackage(gopkg, "some.path", "") diff --git a/tm2/pkg/amino/reflect_test.go b/tm2/pkg/amino/reflect_test.go index c5c7b6b5c83..9adfa0b35d0 100644 --- a/tm2/pkg/amino/reflect_test.go +++ b/tm2/pkg/amino/reflect_test.go @@ -18,44 +18,70 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino/tests" ) -//------------------------------------- +// ------------------------------------- // Non-interface Google fuzz tests func TestCodecStruct(t *testing.T) { + t.Parallel() + for _, ptr := range tests.StructTypes { t.Logf("case %v", reflect.TypeOf(ptr)) rt := getTypeFromPointer(ptr) name := rt.Name() - t.Run(name+":binary", func(t *testing.T) { _testCodec(t, rt, "binary") }) - t.Run(name+":json", func(t *testing.T) { _testCodec(t, rt, "json") }) + t.Run(name+":binary", func(t *testing.T) { + t.Parallel() + _testCodec(t, rt, "binary") + }) + t.Run(name+":json", func(t *testing.T) { + t.Parallel() + _testCodec(t, rt, "json") + }) } } func TestCodecDef(t *testing.T) { + t.Parallel() + for _, ptr := range tests.DefTypes { t.Logf("case %v", reflect.TypeOf(ptr)) rt := getTypeFromPointer(ptr) name := rt.Name() - t.Run(name+":binary", func(t *testing.T) { _testCodec(t, rt, "binary") }) - t.Run(name+":json", func(t *testing.T) { _testCodec(t, rt, "json") }) + t.Run(name+":binary", func(t *testing.T) { + t.Parallel() + _testCodec(t, rt, "binary") + }) + t.Run(name+":json", func(t *testing.T) { + t.Parallel() + _testCodec(t, rt, "json") + }) } } func TestDeepCopyStruct(t *testing.T) { + t.Parallel() + for _, ptr := range tests.StructTypes { t.Logf("case %v", reflect.TypeOf(ptr)) rt := getTypeFromPointer(ptr) name := rt.Name() - t.Run(name+":deepcopy", func(t *testing.T) { _testDeepCopy(t, rt) }) + t.Run(name+":deepcopy", func(t *testing.T) { + t.Parallel() + _testDeepCopy(t, rt) + }) } } func TestDeepCopyDef(t *testing.T) { + t.Parallel() + for _, ptr := range tests.DefTypes { t.Logf("case %v", reflect.TypeOf(ptr)) rt := getTypeFromPointer(ptr) name := rt.Name() - t.Run(name+":deepcopy", func(t *testing.T) { _testDeepCopy(t, rt) }) + t.Run(name+":deepcopy", func(t *testing.T) { + t.Parallel() + _testDeepCopy(t, rt) + }) } } @@ -190,10 +216,12 @@ func _testDeepCopy(t *testing.T, rt reflect.Type) { } } -//---------------------------------------- +// ---------------------------------------- // Register/interface tests func TestCodecMashalFailsOnUnregisteredConcrete(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() bz, err := cdc.Marshal(struct{ tests.Interface1 }{tests.Concrete1{}}) @@ -202,6 +230,8 @@ func TestCodecMashalFailsOnUnregisteredConcrete(t *testing.T) { } func TestCodecMarshalPassesOnRegistered(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.Concrete1{}), tests.Package) @@ -218,6 +248,8 @@ func TestCodecMarshalPassesOnRegistered(t *testing.T) { } func TestCodecRegisterAndMarshalMultipleConcrete(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.Concrete1{}), tests.Package) cdc.RegisterTypeFrom(reflect.TypeOf(tests.Concrete2{}), tests.Package) @@ -251,6 +283,8 @@ func TestCodecRegisterAndMarshalMultipleConcrete(t *testing.T) { // Serialize and deserialize a registered typedef. func TestCodecRoundtripNonNilRegisteredTypeDef(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteTypeDef{}), tests.Package) @@ -322,6 +356,8 @@ func TestCodecRoundtripNonNilRegisteredTypeDef(t *testing.T) { // Exactly like TestCodecRoundtripNonNilRegisteredTypeDef but with struct // around the value instead of a type def. func TestCodecRoundtripNonNilRegisteredWrappedValue(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteWrappedBytes{}), tests.Package) @@ -351,6 +387,8 @@ func TestCodecRoundtripNonNilRegisteredWrappedValue(t *testing.T) { // MarshalAny(msg) and Marshal(&msg) are the same. func TestCodecMarshalAny(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteWrappedBytes{}), tests.Package) @@ -368,6 +406,8 @@ func TestCodecMarshalAny(t *testing.T) { // Like TestCodecRoundtripNonNilRegisteredTypeDef, but JSON. func TestCodecJSONRoundtripNonNilRegisteredTypeDef(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteTypeDef{}), tests.Package) @@ -388,6 +428,8 @@ func TestCodecJSONRoundtripNonNilRegisteredTypeDef(t *testing.T) { // Like TestCodecRoundtripNonNilRegisteredTypeDef, but serialize the concrete value directly. func TestCodecRoundtripMarshalOnConcreteNonNilRegisteredTypeDef(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteTypeDef{}), tests.Package) @@ -418,6 +460,8 @@ func TestCodecRoundtripMarshalOnConcreteNonNilRegisteredTypeDef(t *testing.T) { // Like TestCodecRoundtripNonNilRegisteredTypeDef but read into concrete var. func TestCodecRoundtripUnmarshalOnConcreteNonNilRegisteredTypeDef(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.ConcreteTypeDef{}), tests.Package) @@ -437,6 +481,8 @@ func TestCodecRoundtripUnmarshalOnConcreteNonNilRegisteredTypeDef(t *testing.T) } func TestCodecBinaryStructFieldNilInterface(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() cdc.RegisterTypeFrom(reflect.TypeOf(tests.InterfaceFieldsStruct{}), tests.Package) @@ -451,7 +497,7 @@ func TestCodecBinaryStructFieldNilInterface(t *testing.T) { require.Equal(t, i2, i1, "i1 and i2 should be the same after decoding") } -//---------------------------------------- +// ---------------------------------------- // Misc. func spw(o interface{}) string { @@ -613,7 +659,7 @@ func getTypeFromPointer(ptr interface{}) reflect.Type { return rt.Elem() } -//---------------------------------------- +// ---------------------------------------- // From https://github.com/google/gofuzz/blob/master/fuzz.go // (Apache2.0 License) diff --git a/tm2/pkg/amino/repr_test.go b/tm2/pkg/amino/repr_test.go index 5fb31515fb6..4be50b5d93d 100644 --- a/tm2/pkg/amino/repr_test.go +++ b/tm2/pkg/amino/repr_test.go @@ -55,6 +55,8 @@ var ( ) func TestMarshalAminoBinary(t *testing.T) { + t.Parallel() + cdc := NewCodec() cdc.RegisterPackage(testPackage) @@ -78,6 +80,8 @@ func TestMarshalAminoBinary(t *testing.T) { } func TestMarshalAminoJSON(t *testing.T) { + t.Parallel() + cdc := NewCodec() cdc.RegisterPackage(testPackage) diff --git a/tm2/pkg/amino/tests/proto3/proto3_compat_test.go b/tm2/pkg/amino/tests/proto3/proto3_compat_test.go index bd2a51825da..8f9e04fc35c 100644 --- a/tm2/pkg/amino/tests/proto3/proto3_compat_test.go +++ b/tm2/pkg/amino/tests/proto3/proto3_compat_test.go @@ -36,6 +36,8 @@ func init() { } func TestFixed32Roundtrip(t *testing.T) { + t.Parallel() + // amino fixed32 (int32) <-> protbuf fixed32 (uint32) type testi32 struct { Int32 int32 `binary:"fixed32"` @@ -61,6 +63,8 @@ func TestFixed32Roundtrip(t *testing.T) { } func TestVarintZigzagRoundtrip(t *testing.T) { + t.Parallel() + t.Skip("zigzag encoding isn't default anymore for (unsigned) ints") // amino varint (int) <-> protobuf zigzag32 (int32 in go sint32 in proto file) type testInt32Varint struct { @@ -85,6 +89,8 @@ func TestVarintZigzagRoundtrip(t *testing.T) { } func TestFixedU64Roundtrip(t *testing.T) { + t.Parallel() + type testFixed64Uint struct { Int64 uint64 `binary:"fixed64"` } @@ -111,6 +117,8 @@ func TestFixedU64Roundtrip(t *testing.T) { } func TestMultidimensionalSlices(t *testing.T) { + t.Parallel() + s := [][]int8{ {1, 2}, {3, 4, 5}, @@ -121,6 +129,8 @@ func TestMultidimensionalSlices(t *testing.T) { } func TestMultidimensionalArrays(t *testing.T) { + t.Parallel() + arr := [2][2]int8{ {1, 2}, {3, 4}, @@ -131,6 +141,8 @@ func TestMultidimensionalArrays(t *testing.T) { } func TestMultidimensionalByteArraysAndSlices(t *testing.T) { + t.Parallel() + arr := [2][2]byte{ {1, 2}, {3, 4}, @@ -157,6 +169,8 @@ func TestMultidimensionalByteArraysAndSlices(t *testing.T) { } func TestProto3CompatPtrsRoundtrip(t *testing.T) { + t.Parallel() + s := p3.SomeStruct{} ab, err := cdc.Marshal(s) @@ -214,6 +228,8 @@ type goAminoGotTime struct { } func TestProto3CompatEmptyTimestamp(t *testing.T) { + t.Parallel() + empty := p3.ProtoGotTime{} // protobuf also marshals to empty bytes here: pb, err := proto.Marshal(&empty) @@ -235,6 +251,8 @@ func TestProto3CompatEmptyTimestamp(t *testing.T) { } func TestProto3CompatTimestampNow(t *testing.T) { + t.Parallel() + // test with current time: now := time.Now() ptts, err := ptypes.TimestampProto(now) @@ -267,6 +285,8 @@ func TestProto3CompatTimestampNow(t *testing.T) { } func TestProto3EpochTime(t *testing.T) { + t.Parallel() + pbRes := p3.ProtoGotTime{} // amino encode epoch (1970) and decode using proto; expect the resulting time to be epoch again: ab, err := cdc.Marshal(goAminoGotTime{T: &epoch}) @@ -279,6 +299,8 @@ func TestProto3EpochTime(t *testing.T) { } func TestProtoNegativeSeconds(t *testing.T) { + t.Parallel() + pbRes := p3.ProtoGotTime{} // test with negative seconds (0001-01-01 -> seconds = -62135596800, nanos = 0): ntm, err := time.Parse("2006-01-02 15:04:05 +0000 UTC", "0001-01-01 00:00:00 +0000 UTC") @@ -296,6 +318,8 @@ func TestProtoNegativeSeconds(t *testing.T) { } func TestIntVarintCompat(t *testing.T) { + t.Parallel() + tcs := []struct { val32 int32 val64 int64 @@ -382,6 +406,8 @@ func TestIntVarintCompat(t *testing.T) { // See if encoding of type def types matches the proto3 encoding func TestTypeDefCompatibility(t *testing.T) { + t.Parallel() + pNow := ptypes.TimestampNow() now, err := ptypes.Timestamp(pNow) require.NoError(t, err) diff --git a/tm2/pkg/amino/time2_test.go b/tm2/pkg/amino/time2_test.go index c40e2fc660e..b8d73f37d55 100644 --- a/tm2/pkg/amino/time2_test.go +++ b/tm2/pkg/amino/time2_test.go @@ -18,6 +18,8 @@ type testTime struct { } func TestDecodeSkippedFieldsInTime(t *testing.T) { + t.Parallel() + tm, err := time.Parse("2006-01-02 15:04:05 +0000 UTC", "1970-01-01 00:00:00 +0000 UTC") assert.NoError(t, err) @@ -62,6 +64,8 @@ func TestDecodeSkippedFieldsInTime(t *testing.T) { } func TestMinMaxTimeEncode(t *testing.T) { + t.Parallel() + tMin, err := time.Parse("2006-01-02 15:04:05 +0000 UTC", "0001-01-01 00:00:00 +0000 UTC") assert.NoError(t, err) tm := testTime{tMin} diff --git a/tm2/pkg/amino/wellknown_test.go b/tm2/pkg/amino/wellknown_test.go index 3b91ffc77a5..345466bd285 100644 --- a/tm2/pkg/amino/wellknown_test.go +++ b/tm2/pkg/amino/wellknown_test.go @@ -9,6 +9,8 @@ import ( ) func TestAnyWellKnownNative(t *testing.T) { + t.Parallel() + cdc := amino.NewCodec() s1 := tests.InterfaceFieldsStruct{ diff --git a/tm2/pkg/async/async_test.go b/tm2/pkg/async/async_test.go index f82e32312f0..255ce6b8d51 100644 --- a/tm2/pkg/async/async_test.go +++ b/tm2/pkg/async/async_test.go @@ -12,6 +12,8 @@ import ( ) func TestParallel(t *testing.T) { + t.Parallel() + // Create tasks. counter := new(int32) tasks := make([]Task, 100*1000) @@ -52,6 +54,8 @@ func TestParallel(t *testing.T) { } func TestParallelAbort(t *testing.T) { + t.Parallel() + flow1 := make(chan struct{}, 1) flow2 := make(chan struct{}, 1) flow3 := make(chan struct{}, 1) // Cap must be > 0 to prevent blocking. @@ -103,6 +107,8 @@ func TestParallelAbort(t *testing.T) { } func TestParallelRecover(t *testing.T) { + t.Parallel() + // Create tasks. tasks := []Task{ func(i int) (res interface{}, err error, abort bool) { @@ -155,7 +161,7 @@ func waitTimeout(t *testing.T, taskResultCh TaskResultCh, taskName string) { } else { assert.Fail(t, "TaskResultCh unexpectedly returned for %v", taskName) } - case <-time.After(1 * time.Second): // TODO use deterministic time? + case <-time.After(200 * time.Millisecond): // TODO use deterministic time? // Good! } } diff --git a/tm2/pkg/autofile/group_test.go b/tm2/pkg/autofile/group_test.go index 6face4630d1..1bf79894b93 100644 --- a/tm2/pkg/autofile/group_test.go +++ b/tm2/pkg/autofile/group_test.go @@ -47,6 +47,8 @@ func assertGroupInfo(t *testing.T, gInfo GroupInfo, minIndex, maxIndex int, tota } func TestCheckHeadSizeLimit(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 1000*1000) // At first, there are no files. @@ -93,6 +95,8 @@ func TestCheckHeadSizeLimit(t *testing.T) { } func TestRotateFile(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) g.WriteLine("Line 1") g.WriteLine("Line 2") @@ -123,6 +127,8 @@ func TestRotateFile(t *testing.T) { } func TestWrite(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) written := []byte("Medusa") @@ -144,6 +150,8 @@ func TestWrite(t *testing.T) { // test that Read reads the required amount of bytes from all the files in the // group and returns no error if n == size of the given slice. func TestGroupReaderRead(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) professor := []byte("Professor Monster") @@ -173,6 +181,8 @@ func TestGroupReaderRead(t *testing.T) { // test that Read returns an error if number of bytes read < size of // the given slice. Subsequent call should return 0, io.EOF. func TestGroupReaderRead2(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) professor := []byte("Professor Monster") @@ -204,6 +214,8 @@ func TestGroupReaderRead2(t *testing.T) { } func TestMinIndex(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) assert.Zero(t, g.MinIndex(), "MinIndex should be zero at the beginning") @@ -213,6 +225,8 @@ func TestMinIndex(t *testing.T) { } func TestMaxIndex(t *testing.T) { + t.Parallel() + g := createTestGroupWithHeadSizeLimit(t, 0) assert.Zero(t, g.MaxIndex(), "MaxIndex should be zero at the beginning") diff --git a/tm2/pkg/bech32/bech32_test.go b/tm2/pkg/bech32/bech32_test.go index 557507cef57..361a18116ab 100644 --- a/tm2/pkg/bech32/bech32_test.go +++ b/tm2/pkg/bech32/bech32_test.go @@ -12,6 +12,8 @@ import ( ) func TestEncodeAndDecode(t *testing.T) { + t.Parallel() + sum := sha256.Sum256([]byte("hello world\n")) bech, err := bech32.ConvertAndEncode("shasum", sum[:]) @@ -39,6 +41,8 @@ var ( ) func TestEncode(t *testing.T) { + t.Parallel() + bz, err := hex.DecodeString(pubkeyBytes) assert.NoError(t, err) @@ -49,6 +53,8 @@ func TestEncode(t *testing.T) { } func TestDecode(t *testing.T) { + t.Parallel() + hrp, b1, err := bech32.Decode(pubkeyBech32) assert.NoError(t, err) diff --git a/tm2/pkg/bft/blockchain/pool_test.go b/tm2/pkg/bft/blockchain/pool_test.go index 79205bff5ce..39c17d8428f 100644 --- a/tm2/pkg/bft/blockchain/pool_test.go +++ b/tm2/pkg/bft/blockchain/pool_test.go @@ -72,6 +72,8 @@ func makePeers(numPeers int, minHeight, maxHeight int64) testPeers { } func TestBlockPoolBasic(t *testing.T) { + t.Parallel() + start := int64(42) peers := makePeers(10, start+1, 1000) errorsCh := make(chan peerError, 1000) @@ -128,6 +130,8 @@ func TestBlockPoolBasic(t *testing.T) { } func TestBlockPoolTimeout(t *testing.T) { + t.Parallel() + start := int64(42) peers := makePeers(10, start+1, 1000) errorsCh := make(chan peerError, 1000) @@ -187,6 +191,8 @@ func TestBlockPoolTimeout(t *testing.T) { } func TestBlockPoolRemovePeer(t *testing.T) { + t.Parallel() + peers := make(testPeers, 10) for i := 0; i < 10; i++ { peerID := p2p.ID(fmt.Sprintf("%d", i+1)) diff --git a/tm2/pkg/bft/blockchain/reactor_test.go b/tm2/pkg/bft/blockchain/reactor_test.go index f4265e0f78d..fef640b0f82 100644 --- a/tm2/pkg/bft/blockchain/reactor_test.go +++ b/tm2/pkg/bft/blockchain/reactor_test.go @@ -115,6 +115,8 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals } func TestNoBlockResponse(t *testing.T) { + t.Parallel() + config = cfg.ResetTestRoot("blockchain_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) @@ -174,6 +176,8 @@ func TestNoBlockResponse(t *testing.T) { // Alternatively we could actually dial a TCP conn but // that seems extreme. func TestFlappyBadBlockStopsPeer(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) config = cfg.ResetTestRoot("blockchain_reactor_test") @@ -245,6 +249,8 @@ func TestFlappyBadBlockStopsPeer(t *testing.T) { } func TestBcBlockRequestMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string requestHeight int64 @@ -258,6 +264,8 @@ func TestBcBlockRequestMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + request := bcBlockRequestMessage{Height: tc.requestHeight} assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) @@ -265,6 +273,8 @@ func TestBcBlockRequestMessageValidateBasic(t *testing.T) { } func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string nonResponseHeight int64 @@ -278,6 +288,8 @@ func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) @@ -285,6 +297,8 @@ func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { } func TestBcStatusRequestMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string requestHeight int64 @@ -298,6 +312,8 @@ func TestBcStatusRequestMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + request := bcStatusRequestMessage{Height: tc.requestHeight} assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) @@ -305,6 +321,8 @@ func TestBcStatusRequestMessageValidateBasic(t *testing.T) { } func TestBcStatusResponseMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string responseHeight int64 @@ -318,13 +336,15 @@ func TestBcStatusResponseMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + response := bcStatusResponseMessage{Height: tc.responseHeight} assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") }) } } -//---------------------------------------------- +// ---------------------------------------------- // utility funcs func makeTxs(height int64) (txs []types.Tx) { diff --git a/tm2/pkg/bft/config/toml_test.go b/tm2/pkg/bft/config/toml_test.go index da6a720ecd4..d1d70155cb8 100644 --- a/tm2/pkg/bft/config/toml_test.go +++ b/tm2/pkg/bft/config/toml_test.go @@ -22,6 +22,8 @@ func ensureFiles(t *testing.T, rootDir string, files ...string) { } func TestEnsureRoot(t *testing.T) { + t.Parallel() + require := require.New(t) // setup temp dir for test @@ -46,6 +48,8 @@ func TestEnsureRoot(t *testing.T) { } func TestEnsureTestRoot(t *testing.T) { + t.Parallel() + require := require.New(t) testName := "ensureTestRoot" diff --git a/tm2/pkg/bft/consensus/mempool_test.go b/tm2/pkg/bft/consensus/mempool_test.go index b9c04cabbed..85914262903 100644 --- a/tm2/pkg/bft/consensus/mempool_test.go +++ b/tm2/pkg/bft/consensus/mempool_test.go @@ -24,6 +24,8 @@ func assertMempool(txn txNotifier) mempl.Mempool { } func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { + t.Parallel() + config := ResetConfig("consensus_mempool_no_progress_until_txs_available") defer os.RemoveAll(config.RootDir) config.Consensus.CreateEmptyBlocks = false @@ -71,6 +73,8 @@ func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { } func TestMempoolProgressInHigherRound(t *testing.T) { + t.Parallel() + config := ResetConfig("consensus_mempool_progress_in_higher_round") defer os.RemoveAll(config.RootDir) config.Consensus.CreateEmptyBlocks = false @@ -138,6 +142,8 @@ func deliverTxsRange(cs *ConsensusState, start, end int) { } func TestMempoolTxConcurrentWithCommit(t *testing.T) { + t.Parallel() + state, privVals := randGenesisState(1, false, 10) blockDB := dbm.NewMemDB() app := NewCounterApplication() @@ -169,6 +175,8 @@ func TestMempoolTxConcurrentWithCommit(t *testing.T) { } func TestMempoolRmBadTx(t *testing.T) { + t.Parallel() + state, privVals := randGenesisState(1, false, 10) app := NewCounterApplication() blockDB := dbm.NewMemDB() diff --git a/tm2/pkg/bft/consensus/reactor_test.go b/tm2/pkg/bft/consensus/reactor_test.go index 2194d4d007d..db084b61395 100644 --- a/tm2/pkg/bft/consensus/reactor_test.go +++ b/tm2/pkg/bft/consensus/reactor_test.go @@ -86,6 +86,8 @@ func stopConsensusNet(logger log.Logger, reactors []*ConsensusReactor, eventSwit // Ensure a testnet makes blocks func TestReactorBasic(t *testing.T) { + t.Parallel() + N := 4 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() @@ -101,6 +103,8 @@ func TestReactorBasic(t *testing.T) { // Ensure a testnet makes blocks when there are txs func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { + t.Parallel() + N := 4 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter, func(c *cfg.Config) { @@ -122,6 +126,8 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { } func TestReactorReceiveDoesNotPanicIfAddPeerHasntBeenCalledYet(t *testing.T) { + t.Parallel() + N := 1 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() @@ -144,6 +150,8 @@ func TestReactorReceiveDoesNotPanicIfAddPeerHasntBeenCalledYet(t *testing.T) { } func TestReactorReceivePanicsIfInitPeerHasntBeenCalledYet(t *testing.T) { + t.Parallel() + N := 1 css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) defer cleanup() @@ -166,6 +174,8 @@ func TestReactorReceivePanicsIfInitPeerHasntBeenCalledYet(t *testing.T) { // Test we record stats about votes and block parts from other peers. func TestFlappyReactorRecordsVotesAndBlockParts(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) N := 4 @@ -192,6 +202,8 @@ func TestFlappyReactorRecordsVotesAndBlockParts(t *testing.T) { // ensure we can make blocks despite cycling a validator set func TestReactorVotingPowerChange(t *testing.T) { + t.Parallel() + nVals := 4 logger := log.TestingLogger() css, cleanup := randConsensusNet(nVals, "consensus_voting_power_changes_test", newMockTickerFunc(true), newPersistentKVStore) @@ -254,6 +266,8 @@ func TestReactorVotingPowerChange(t *testing.T) { } func TestReactorValidatorSetChanges(t *testing.T) { + t.Parallel() + nPeers := 7 nVals := 4 css, _, _, cleanup := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentKVStoreWithPath) @@ -350,6 +364,8 @@ func TestReactorValidatorSetChanges(t *testing.T) { // Check we can make blocks with skip_timeout_commit=false func TestReactorWithTimeoutCommit(t *testing.T) { + t.Parallel() + N := 4 css, cleanup := randConsensusNet(N, "consensus_reactor_with_timeout_commit_test", newMockTickerFunc(false), newCounter) defer cleanup() @@ -510,6 +526,8 @@ func timeoutWaitGroup(t *testing.T, n int, f func(int), css []*ConsensusState) { // Ensure basic validation of structs is functioning func TestNewRoundStepMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string messageHeight int64 @@ -529,6 +547,8 @@ func TestNewRoundStepMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + message := NewRoundStepMessage{ Height: tc.messageHeight, Round: tc.messageRound, @@ -542,6 +562,8 @@ func TestNewRoundStepMessageValidateBasic(t *testing.T) { } func TestNewValidBlockMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { malleateFn func(*NewValidBlockMessage) expErr string @@ -569,6 +591,8 @@ func TestNewValidBlockMessageValidateBasic(t *testing.T) { for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + t.Parallel() + msg := &NewValidBlockMessage{ Height: 1, Round: 0, @@ -588,6 +612,8 @@ func TestNewValidBlockMessageValidateBasic(t *testing.T) { } func TestProposalPOLMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { malleateFn func(*ProposalPOLMessage) expErr string @@ -605,6 +631,8 @@ func TestProposalPOLMessageValidateBasic(t *testing.T) { for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + t.Parallel() + msg := &ProposalPOLMessage{ Height: 1, ProposalPOLRound: 1, @@ -621,6 +649,8 @@ func TestProposalPOLMessageValidateBasic(t *testing.T) { } func TestBlockPartMessageValidateBasic(t *testing.T) { + t.Parallel() + testPart := new(types.Part) testPart.Proof.LeafHash = tmhash.Sum([]byte("leaf")) testCases := []struct { @@ -638,6 +668,8 @@ func TestBlockPartMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + message := BlockPartMessage{ Height: tc.messageHeight, Round: tc.messageRound, @@ -655,6 +687,8 @@ func TestBlockPartMessageValidateBasic(t *testing.T) { } func TestHasVoteMessageValidateBasic(t *testing.T) { + t.Parallel() + const ( validSignedMsgType types.SignedMsgType = 0x01 invalidSignedMsgType types.SignedMsgType = 0x03 @@ -678,6 +712,8 @@ func TestHasVoteMessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + message := HasVoteMessage{ Height: tc.messageHeight, Round: tc.messageRound, @@ -691,6 +727,8 @@ func TestHasVoteMessageValidateBasic(t *testing.T) { } func TestVoteSetMaj23MessageValidateBasic(t *testing.T) { + t.Parallel() + const ( validSignedMsgType types.SignedMsgType = 0x01 invalidSignedMsgType types.SignedMsgType = 0x03 @@ -723,6 +761,8 @@ func TestVoteSetMaj23MessageValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + message := VoteSetMaj23Message{ Height: tc.messageHeight, Round: tc.messageRound, @@ -736,6 +776,8 @@ func TestVoteSetMaj23MessageValidateBasic(t *testing.T) { } func TestVoteSetBitsMessageValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { //nolint: maligned malleateFn func(*VoteSetBitsMessage) expErr string @@ -762,6 +804,8 @@ func TestVoteSetBitsMessageValidateBasic(t *testing.T) { for i, tc := range testCases { tc := tc t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { + t.Parallel() + msg := &VoteSetBitsMessage{ Height: 1, Round: 0, diff --git a/tm2/pkg/bft/consensus/replay_test.go b/tm2/pkg/bft/consensus/replay_test.go index 556689ec3f4..d43a06f40c2 100644 --- a/tm2/pkg/bft/consensus/replay_test.go +++ b/tm2/pkg/bft/consensus/replay_test.go @@ -123,6 +123,8 @@ func sendTxs(ctx context.Context, cs *ConsensusState) { // TestWALCrash uses crashing WAL to test we can recover from any WAL failure. func TestWALCrash(t *testing.T) { + t.Parallel() + testCases := []struct { name string initFn func(dbm.DB, *ConsensusState, context.Context) @@ -146,6 +148,8 @@ func TestWALCrash(t *testing.T) { tc := tc consensusReplayConfig := ResetConfig(fmt.Sprintf("%s_%d", t.Name(), i)) t.Run(tc.name, func(t *testing.T) { + t.Parallel() + crashWALandCheckLiveness(t, consensusReplayConfig, tc.initFn, tc.lastBlockHeight) }) } @@ -506,6 +510,8 @@ func makeTestSim(t *testing.T, name string) (sim testSim) { // Sync from scratch func TestHandshakeReplayAll(t *testing.T) { + t.Parallel() + for _, m := range modes { testHandshakeReplay(t, config, 0, m, nil) } @@ -518,6 +524,8 @@ func TestHandshakeReplayAll(t *testing.T) { // Sync many, not from scratch func TestHandshakeReplaySome(t *testing.T) { + t.Parallel() + for _, m := range modes { testHandshakeReplay(t, config, 1, m, nil) } @@ -530,6 +538,8 @@ func TestHandshakeReplaySome(t *testing.T) { // Sync from lagging by one func TestHandshakeReplayOne(t *testing.T) { + t.Parallel() + for _, m := range modes { testHandshakeReplay(t, config, numBlocks-1, m, nil) } @@ -542,6 +552,8 @@ func TestHandshakeReplayOne(t *testing.T) { // Sync from caught up func TestFlappyHandshakeReplayNone(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) for _, m := range modes { @@ -556,6 +568,8 @@ func TestFlappyHandshakeReplayNone(t *testing.T) { // Test mockProxyApp should not panic when app return ABCIResponses with some empty ResponseDeliverTx func TestMockProxyApp(t *testing.T) { + t.Parallel() + logger := log.TestingLogger() validTxs, invalidTxs := 0, 0 txIndex := 0 @@ -810,6 +824,8 @@ func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, c } func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) { + t.Parallel() + // 1. Initialize tendermint and commit 3 blocks with the following app hashes: // - 0x01 // - 0x02 @@ -1089,6 +1105,8 @@ func (bs *mockBlockStore) LoadSeenCommit(height int64) *types.Commit { // Test handshake/init chain func TestHandshakeUpdatesValidators(t *testing.T) { + t.Parallel() + val, _ := types.RandValidator(true, 10) vals := types.NewValidatorSet([]*types.Validator{val}) app := &initChainApp{vals: vals.ABCIValidatorUpdates()} diff --git a/tm2/pkg/bft/consensus/state_test.go b/tm2/pkg/bft/consensus/state_test.go index 0e2e6b629a4..35877837ab3 100644 --- a/tm2/pkg/bft/consensus/state_test.go +++ b/tm2/pkg/bft/consensus/state_test.go @@ -63,6 +63,8 @@ x * TestHalt1 - if we see +2/3 precommits after timing out into new round, we sh // ProposeSuite func TestStateProposerSelection0(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) height, round := cs1.Height, cs1.Round @@ -103,6 +105,8 @@ func TestStateProposerSelection0(t *testing.T) { // Now let's do it all again, but starting from round 2 instead of 0 func TestStateProposerSelection2(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) // test needs more work for more than 3 validators height := cs1.Height newRoundCh := subscribe(cs1.evsw, cstypes.EventNewRound{}) @@ -138,6 +142,8 @@ func TestStateProposerSelection2(t *testing.T) { // a non-validator should timeout into the prevote round func TestStateEnterProposeNoPrivValidator(t *testing.T) { + t.Parallel() + cs, _ := randConsensusState(1) cs.SetPrivValidator(nil) height, round := cs.Height, cs.Round @@ -161,6 +167,8 @@ func TestStateEnterProposeNoPrivValidator(t *testing.T) { // a validator should not timeout of the prevote round (TODO: unless the block is really big!) func TestStateEnterProposeYesPrivValidator(t *testing.T) { + t.Parallel() + cs, _ := randConsensusState(1) height, round := cs.Height, cs.Round @@ -197,6 +205,8 @@ func TestStateEnterProposeYesPrivValidator(t *testing.T) { } func TestStateBadProposal(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(2) height, round := cs1.Height, cs1.Round vs2 := vss[1] @@ -260,6 +270,8 @@ func TestStateBadProposal(t *testing.T) { // propose, prevote, and precommit a block func TestStateFullRound1(t *testing.T) { + t.Parallel() + cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round @@ -292,6 +304,8 @@ func TestStateFullRound1(t *testing.T) { // nil is proposed, so prevote and precommit nil func TestStateFullRoundNil(t *testing.T) { + t.Parallel() + cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round cs.decideProposal = func(height int64, round int) { @@ -316,6 +330,8 @@ func TestStateFullRoundNil(t *testing.T) { // run through propose, prevote, precommit commit with two validators // where the first validator has to wait for votes from the second func TestStateFullRound2(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(2) vs2 := vss[1] height, round := cs1.Height, cs1.Round @@ -360,6 +376,8 @@ func TestStateFullRound2(t *testing.T) { // two validators, 4 rounds. // two vals take turns proposing. val1 locks on first one, precommits nil on everything else func TestStateLockNoPOL(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(2) vs2 := vss[1] height, round := cs1.Height, cs1.Round @@ -532,6 +550,8 @@ func TestStateLockNoPOL(t *testing.T) { // 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka func TestStateLockPOLRelock(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -627,6 +647,8 @@ func TestStateLockPOLRelock(t *testing.T) { // 4 vals, one precommits, other 3 polka at next round, so we unlock and precomit the polka func TestStateLockPOLUnlock(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -727,6 +749,8 @@ func TestStateLockPOLUnlock(t *testing.T) { // then a polka at round 2 that we lock on // then we see the polka from round 1 but shouldn't unlock func TestStateLockPOLSafety1(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -848,6 +872,8 @@ func TestStateLockPOLSafety1(t *testing.T) { // What we want: // dont see P0, lock on P1 at R1, dont unlock using P0 at R2 func TestStateLockPOLSafety2(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -943,6 +969,8 @@ func TestStateLockPOLSafety2(t *testing.T) { // What we want: // P0 proposes B0 at R3. func TestProposeValidBlock(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1038,6 +1066,8 @@ func TestProposeValidBlock(t *testing.T) { // What we want: // P0 miss to lock B but set valid block to B after receiving delayed prevote. func TestSetValidBlockOnDelayedPrevote(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1102,6 +1132,8 @@ func TestSetValidBlockOnDelayedPrevote(t *testing.T) { // P0 miss to lock B as Proposal Block is missing, but set valid block to B after // receiving delayed Block Proposal. func TestSetValidBlockOnDelayedProposal(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1160,6 +1192,8 @@ func TestSetValidBlockOnDelayedProposal(t *testing.T) { // What we want: // P0 waits for timeoutPrecommit before starting next round func TestWaitingTimeoutOnNilPolka(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1185,6 +1219,8 @@ func TestWaitingTimeoutOnNilPolka(t *testing.T) { // What we want: // P0 waits for timeoutPropose in the next round before entering prevote func TestWaitingTimeoutProposeOnNewRound(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1223,6 +1259,8 @@ func TestWaitingTimeoutProposeOnNewRound(t *testing.T) { // What we want: // P0 jump to higher round, precommit and start precommit wait func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, cs1.Round @@ -1261,6 +1299,8 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { // What we want: // P0 wait for timeoutPropose to expire before sending prevote. func TestWaitTimeoutProposeOnNilPolkaForTheCurrentRound(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, 1 @@ -1290,6 +1330,8 @@ func TestWaitTimeoutProposeOnNilPolkaForTheCurrentRound(t *testing.T) { // What we want: // P0 emit NewValidBlock event upon receiving 2/3+ Precommit for B but hasn't received block B yet func TestEmitNewValidBlockEventOnCommitWithoutBlock(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, 1 @@ -1327,6 +1369,8 @@ func TestEmitNewValidBlockEventOnCommitWithoutBlock(t *testing.T) { // P0 receives 2/3+ Precommit for B for round 0, while being in round 1. It emits NewValidBlock event. // After receiving block, it executes block and moves to the next height. func TestCommitFromPreviousRound(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] height, round := cs1.Height, 1 @@ -1439,6 +1483,8 @@ func TestStartNextHeightCorrectly(t *testing.T) { } func TestFlappyResetTimeoutPrecommitUponNewHeight(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) config.Consensus.SkipTimeoutCommit = false @@ -1502,6 +1548,8 @@ func TestFlappyResetTimeoutPrecommitUponNewHeight(t *testing.T) { /* func TestStateSlashingPrevotes(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(2) vs2 := vss[1] @@ -1537,6 +1585,8 @@ func TestStateSlashingPrevotes(t *testing.T) { } func TestStateSlashingPrecommits(t *testing.T) { + t.Parallel() + cs1, vss := randConsensusState(2) vs2 := vss[1] @@ -1582,6 +1632,8 @@ func TestStateSlashingPrecommits(t *testing.T) { // 4 vals. // we receive a final precommit after going into next round, but others might have gone to commit already! func TestFlappyStateHalt1(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) cs1, vss := randConsensusState(4) vs2, vs3, vs4 := vss[1], vss[2], vss[3] @@ -1652,6 +1704,8 @@ func TestFlappyStateHalt1(t *testing.T) { } func TestStateOutputsBlockPartsStats(t *testing.T) { + t.Parallel() + // create dummy peer cs, _ := randConsensusState(1) peer := p2pmock.NewPeer(nil) @@ -1694,6 +1748,8 @@ func TestStateOutputsBlockPartsStats(t *testing.T) { } func TestStateOutputVoteStats(t *testing.T) { + t.Parallel() + cs, vss := randConsensusState(2) // create dummy peer peer := p2pmock.NewPeer(nil) diff --git a/tm2/pkg/bft/consensus/types/height_vote_set_test.go b/tm2/pkg/bft/consensus/types/height_vote_set_test.go index 86dc4ee84f6..4c315585daa 100644 --- a/tm2/pkg/bft/consensus/types/height_vote_set_test.go +++ b/tm2/pkg/bft/consensus/types/height_vote_set_test.go @@ -21,6 +21,8 @@ func TestMain(m *testing.M) { } func TestPeerCatchupRounds(t *testing.T) { + t.Parallel() + valSet, privVals := types.RandValidatorSet(10, 1) hvs := NewHeightVoteSet(config.ChainID(), 1, valSet) diff --git a/tm2/pkg/bft/consensus/wal_test.go b/tm2/pkg/bft/consensus/wal_test.go index 50f8f4c8b2a..55c9be03508 100644 --- a/tm2/pkg/bft/consensus/wal_test.go +++ b/tm2/pkg/bft/consensus/wal_test.go @@ -49,6 +49,8 @@ func makeTempWAL(t *testing.T, walChunkSize int64) (wal walm.WAL) { // ---------------------------------------- func TestWALTruncate(t *testing.T) { + t.Parallel() + const walChunkSize = 409610 // 4KB wal := makeTempWAL(t, walChunkSize) diff --git a/tm2/pkg/bft/mempool/cache_test.go b/tm2/pkg/bft/mempool/cache_test.go index 5f76d6875ea..2d523db37ae 100644 --- a/tm2/pkg/bft/mempool/cache_test.go +++ b/tm2/pkg/bft/mempool/cache_test.go @@ -13,6 +13,8 @@ import ( ) func TestCacheRemove(t *testing.T) { + t.Parallel() + cache := newMapTxCache(100) numTxs := 10 txs := make([][]byte, numTxs) @@ -35,6 +37,8 @@ func TestCacheRemove(t *testing.T) { } func TestCacheAfterUpdate(t *testing.T) { + t.Parallel() + app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool, cleanup := newMempoolWithApp(cc) diff --git a/tm2/pkg/bft/mempool/reactor_test.go b/tm2/pkg/bft/mempool/reactor_test.go index c3b77fb46df..154cb1a7132 100644 --- a/tm2/pkg/bft/mempool/reactor_test.go +++ b/tm2/pkg/bft/mempool/reactor_test.go @@ -140,6 +140,8 @@ const ( ) func TestReactorBroadcastTxMessage(t *testing.T) { + t.Parallel() + mconfig := memcfg.TestMempoolConfig() pconfig := p2pcfg.TestP2PConfig() const N = 4 @@ -162,6 +164,8 @@ func TestReactorBroadcastTxMessage(t *testing.T) { } func TestReactorNoBroadcastToSender(t *testing.T) { + t.Parallel() + mconfig := memcfg.TestMempoolConfig() pconfig := p2pcfg.TestP2PConfig() const N = 2 @@ -179,6 +183,8 @@ func TestReactorNoBroadcastToSender(t *testing.T) { } func TestFlappyBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) if testing.Short() { @@ -205,6 +211,8 @@ func TestFlappyBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { } func TestFlappyBroadcastTxForPeerStopsWhenReactorStops(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) if testing.Short() { @@ -227,6 +235,8 @@ func TestFlappyBroadcastTxForPeerStopsWhenReactorStops(t *testing.T) { } func TestMempoolIDsBasic(t *testing.T) { + t.Parallel() + ids := newMempoolIDs() peer := mock.NewPeer(net.IP{127, 0, 0, 1}) @@ -241,6 +251,8 @@ func TestMempoolIDsBasic(t *testing.T) { } func TestMempoolIDsPanicsIfNodeRequestsOvermaxActiveIDs(t *testing.T) { + t.Parallel() + if testing.Short() { return } diff --git a/tm2/pkg/bft/privval/file_test.go b/tm2/pkg/bft/privval/file_test.go index 36979e19ea3..a798a7ddc64 100644 --- a/tm2/pkg/bft/privval/file_test.go +++ b/tm2/pkg/bft/privval/file_test.go @@ -17,6 +17,8 @@ import ( ) func TestGenLoadValidator(t *testing.T) { + t.Parallel() + assert := assert.New(t) tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") @@ -37,6 +39,8 @@ func TestGenLoadValidator(t *testing.T) { } func TestResetValidator(t *testing.T) { + t.Parallel() + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) tempStateFile, err := os.CreateTemp("", "priv_validator_state_") @@ -65,6 +69,8 @@ func TestResetValidator(t *testing.T) { } func TestLoadOrGenValidator(t *testing.T) { + t.Parallel() + assert := assert.New(t) tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") @@ -88,6 +94,8 @@ func TestLoadOrGenValidator(t *testing.T) { } func TestUnmarshalValidatorState(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) // create some fixed values @@ -113,6 +121,8 @@ func TestUnmarshalValidatorState(t *testing.T) { } func TestUnmarshalValidatorKey(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) // create some fixed values @@ -157,6 +167,8 @@ func TestUnmarshalValidatorKey(t *testing.T) { } func TestSignVote(t *testing.T) { + t.Parallel() + assert := assert.New(t) tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") @@ -203,6 +215,8 @@ func TestSignVote(t *testing.T) { } func TestSignProposal(t *testing.T) { + t.Parallel() + assert := assert.New(t) tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") @@ -247,6 +261,8 @@ func TestSignProposal(t *testing.T) { } func TestDifferByTimestamp(t *testing.T) { + t.Parallel() + tempKeyFile, err := os.CreateTemp("", "priv_validator_key_") require.Nil(t, err) tempStateFile, err := os.CreateTemp("", "priv_validator_state_") diff --git a/tm2/pkg/bft/privval/signer_client_test.go b/tm2/pkg/bft/privval/signer_client_test.go index 1ba6b2e08d5..a896547aec3 100644 --- a/tm2/pkg/bft/privval/signer_client_test.go +++ b/tm2/pkg/bft/privval/signer_client_test.go @@ -53,6 +53,8 @@ func getSignerTestCases(t *testing.T) []signerTestCase { } func TestSignerClose(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { err := tc.signerClient.Close() assert.NoError(t, err) @@ -63,6 +65,8 @@ func TestSignerClose(t *testing.T) { } func TestSignerPing(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { defer tc.signerServer.Stop() defer tc.signerClient.Close() @@ -73,6 +77,8 @@ func TestSignerPing(t *testing.T) { } func TestSignerGetPubKey(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { defer tc.signerServer.Stop() defer tc.signerClient.Close() @@ -90,6 +96,8 @@ func TestSignerGetPubKey(t *testing.T) { } func TestSignerProposal(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { ts := time.Now() want := &types.Proposal{Timestamp: ts} @@ -106,6 +114,8 @@ func TestSignerProposal(t *testing.T) { } func TestSignerVote(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { ts := time.Now() want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} @@ -122,6 +132,8 @@ func TestSignerVote(t *testing.T) { } func TestSignerVoteResetDeadline(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { ts := time.Now() want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} @@ -148,6 +160,8 @@ func TestSignerVoteResetDeadline(t *testing.T) { } func TestFlappySignerVoteKeepAlive(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) for _, tc := range getSignerTestCases(t) { @@ -175,6 +189,8 @@ func TestFlappySignerVoteKeepAlive(t *testing.T) { } func TestSignerSignProposalErrors(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { // Replace service with a mock that always fails tc.signerServer.privVal = types.NewErroringMockPV() @@ -197,6 +213,8 @@ func TestSignerSignProposalErrors(t *testing.T) { } func TestSignerSignVoteErrors(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { ts := time.Now() vote := &types.Vote{Timestamp: ts, Type: types.PrecommitType} @@ -243,6 +261,8 @@ func brokenHandler(privVal types.PrivValidator, request SignerMessage, chainID s } func TestSignerUnexpectedResponse(t *testing.T) { + t.Parallel() + for _, tc := range getSignerTestCases(t) { tc.signerServer.privVal = types.NewMockPV() tc.mockPV = types.NewMockPV() diff --git a/tm2/pkg/bft/privval/signer_listener_endpoint_test.go b/tm2/pkg/bft/privval/signer_listener_endpoint_test.go index c2d89660baf..16640583364 100644 --- a/tm2/pkg/bft/privval/signer_listener_endpoint_test.go +++ b/tm2/pkg/bft/privval/signer_listener_endpoint_test.go @@ -32,6 +32,8 @@ type dialerTestCase struct { // SignerDialerEndpoint.dialer() call inside SignerDialerEndpoint.acceptNewConnection() to return // successfully immediately, putting an instant stop to any retry attempts. func TestSignerRemoteRetryTCPOnly(t *testing.T) { + t.Parallel() + var ( attemptCh = make(chan int) retries = 10 @@ -83,6 +85,8 @@ func TestSignerRemoteRetryTCPOnly(t *testing.T) { } func TestRetryConnToRemoteSigner(t *testing.T) { + t.Parallel() + for _, tc := range getDialerTestCases(t) { var ( logger = log.TestingLogger() diff --git a/tm2/pkg/bft/privval/socket_dialers_test.go b/tm2/pkg/bft/privval/socket_dialers_test.go index 150b9218575..a2f2cf9743b 100644 --- a/tm2/pkg/bft/privval/socket_dialers_test.go +++ b/tm2/pkg/bft/privval/socket_dialers_test.go @@ -40,6 +40,8 @@ func getDialerTestCases(t *testing.T) []dialerTestCase { } func TestIsConnTimeoutForFundamentalTimeouts(t *testing.T) { + t.Parallel() + // Generate a networking timeout tcpAddr := "127.0.0.1:34985" dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) @@ -49,6 +51,8 @@ func TestIsConnTimeoutForFundamentalTimeouts(t *testing.T) { } func TestIsConnTimeoutForWrappedConnTimeouts(t *testing.T) { + t.Parallel() + tcpAddr := "127.0.0.1:34985" dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) _, err := dialer() diff --git a/tm2/pkg/bft/privval/socket_listeners_test.go b/tm2/pkg/bft/privval/socket_listeners_test.go index 6ca7863d1fd..f43ec6e1636 100644 --- a/tm2/pkg/bft/privval/socket_listeners_test.go +++ b/tm2/pkg/bft/privval/socket_listeners_test.go @@ -88,6 +88,8 @@ func listenerTestCases(t *testing.T, timeoutAccept, timeoutReadWrite time.Durati } func TestListenerTimeoutAccept(t *testing.T) { + t.Parallel() + for _, tc := range listenerTestCases(t, time.Millisecond, time.Second) { _, err := tc.listener.Accept() opErr, ok := err.(*net.OpError) @@ -102,6 +104,8 @@ func TestListenerTimeoutAccept(t *testing.T) { } func TestListenerTimeoutReadWrite(t *testing.T) { + t.Parallel() + const ( // This needs to be long enough s.t. the Accept will definitely succeed: timeoutAccept = time.Second diff --git a/tm2/pkg/bft/privval/utils_test.go b/tm2/pkg/bft/privval/utils_test.go index 6e5562e4cbd..63ee6a6076a 100644 --- a/tm2/pkg/bft/privval/utils_test.go +++ b/tm2/pkg/bft/privval/utils_test.go @@ -9,6 +9,8 @@ import ( ) func TestIsConnTimeoutForNonTimeoutErrors(t *testing.T) { + t.Parallel() + assert.False(t, IsConnTimeout(errors.Wrap(ErrDialRetryMax, "max retries exceeded"))) assert.False(t, IsConnTimeout(errors.New("completely irrelevant error"))) } diff --git a/tm2/pkg/bft/rpc/client/helpers_test.go b/tm2/pkg/bft/rpc/client/helpers_test.go index a58ed5adfd6..4d0b54c2358 100644 --- a/tm2/pkg/bft/rpc/client/helpers_test.go +++ b/tm2/pkg/bft/rpc/client/helpers_test.go @@ -15,6 +15,8 @@ import ( ) func TestWaitForHeight(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) // test with error result - immediate failure diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go index 7b649e1dda8..e09ae8d4466 100644 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ b/tm2/pkg/bft/rpc/client/rpc_test.go @@ -35,6 +35,8 @@ func GetClients() []client.Client { } func TestNilCustomHTTPClient(t *testing.T) { + t.Parallel() + require.Panics(t, func() { client.NewHTTPWithClient("http://example.com", "/websocket", nil) }) @@ -44,6 +46,8 @@ func TestNilCustomHTTPClient(t *testing.T) { } func TestCustomHTTPClient(t *testing.T) { + t.Parallel() + remote := rpctest.GetConfig().RPC.ListenAddress c := client.NewHTTPWithClient(remote, "/websocket", http.DefaultClient) status, err := c.Status() @@ -52,6 +56,8 @@ func TestCustomHTTPClient(t *testing.T) { } func TestCorsEnabled(t *testing.T) { + t.Parallel() + origin := rpctest.GetConfig().RPC.CORSAllowedOrigins[0] remote := strings.Replace(rpctest.GetConfig().RPC.ListenAddress, "tcp", "http", -1) @@ -68,6 +74,8 @@ func TestCorsEnabled(t *testing.T) { // Make sure status is correct (we connect properly) func TestStatus(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { moniker := rpctest.GetConfig().Moniker status, err := c.Status() @@ -78,6 +86,8 @@ func TestStatus(t *testing.T) { // Make sure info is correct (we connect properly) func TestInfo(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { // status, err := c.Status() // require.Nil(t, err, "%+v", err) @@ -90,6 +100,8 @@ func TestInfo(t *testing.T) { } func TestNetInfo(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { nc, ok := c.(client.NetworkClient) require.True(t, ok, "%d", i) @@ -101,6 +113,8 @@ func TestNetInfo(t *testing.T) { } func TestDumpConsensusState(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { // FIXME: fix server so it doesn't panic on invalid input nc, ok := c.(client.NetworkClient) @@ -113,6 +127,8 @@ func TestDumpConsensusState(t *testing.T) { } func TestConsensusState(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { // FIXME: fix server so it doesn't panic on invalid input nc, ok := c.(client.NetworkClient) @@ -124,6 +140,8 @@ func TestConsensusState(t *testing.T) { } func TestHealth(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { nc, ok := c.(client.NetworkClient) require.True(t, ok, "%d", i) @@ -133,6 +151,8 @@ func TestHealth(t *testing.T) { } func TestGenesisAndValidators(t *testing.T) { + t.Parallel() + for i, c := range GetClients() { // make sure this is the right genesis file gen, err := c.Genesis() @@ -173,6 +193,8 @@ func TestABCIQuery(t *testing.T) { // Make some app checks func TestAppCalls(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) for i, c := range GetClients() { // get an offset of height to avoid racing and guessing @@ -265,6 +287,8 @@ func TestAppCalls(t *testing.T) { } func TestBroadcastTxSync(t *testing.T) { + t.Parallel() + require := require.New(t) // TODO (melekes): use mempool which is set on RPC rather than getting it from node @@ -344,6 +368,8 @@ func TestNumUnconfirmedTxs(t *testing.T) { /* func TestTx(t *testing.T) { + t.Parallel() + // first we broadcast a tx c := getHTTPClient() _, _, tx := MakeTxKV() @@ -398,6 +424,8 @@ func TestTx(t *testing.T) { } func TestTxSearch(t *testing.T) { + t.Parallel() + // first we broadcast a tx c := getHTTPClient() _, _, tx := MakeTxKV() @@ -519,6 +547,8 @@ func testBatchedJSONRPCCalls(t *testing.T, c *client.HTTP) { } func TestBatchedJSONRPCCallsCancellation(t *testing.T) { + t.Parallel() + c := getHTTPClient() _, _, tx1 := MakeTxKV() _, _, tx2 := MakeTxKV() @@ -537,6 +567,8 @@ func TestBatchedJSONRPCCallsCancellation(t *testing.T) { } func TestSendingEmptyJSONRPCRequestBatch(t *testing.T) { + t.Parallel() + c := getHTTPClient() batch := c.NewBatch() _, err := batch.Send() @@ -544,6 +576,8 @@ func TestSendingEmptyJSONRPCRequestBatch(t *testing.T) { } func TestClearingEmptyJSONRPCRequestBatch(t *testing.T) { + t.Parallel() + c := getHTTPClient() batch := c.NewBatch() require.Zero(t, batch.Clear(), "clearing an empty batch of JSON RPC requests should result in a 0 result") diff --git a/tm2/pkg/bft/rpc/core/blocks_test.go b/tm2/pkg/bft/rpc/core/blocks_test.go index a4d689ffd23..0fcd40f6d14 100644 --- a/tm2/pkg/bft/rpc/core/blocks_test.go +++ b/tm2/pkg/bft/rpc/core/blocks_test.go @@ -8,6 +8,8 @@ import ( ) func TestBlockchainInfo(t *testing.T) { + t.Parallel() + cases := []struct { min, max int64 height int64 diff --git a/tm2/pkg/bft/rpc/core/net_test.go b/tm2/pkg/bft/rpc/core/net_test.go index 42bf79fcce1..c3a8830cb1b 100644 --- a/tm2/pkg/bft/rpc/core/net_test.go +++ b/tm2/pkg/bft/rpc/core/net_test.go @@ -13,6 +13,8 @@ import ( ) func TestUnsafeDialSeeds(t *testing.T) { + t.Parallel() + sw := p2p.MakeSwitch(p2pcfg.DefaultP2PConfig(), 1, "testing", "123.123.123", func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) err := sw.Start() @@ -43,6 +45,8 @@ func TestUnsafeDialSeeds(t *testing.T) { } func TestUnsafeDialPeers(t *testing.T) { + t.Parallel() + sw := p2p.MakeSwitch(p2pcfg.DefaultP2PConfig(), 1, "testing", "123.123.123", func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) err := sw.Start() diff --git a/tm2/pkg/bft/rpc/core/pipe_test.go b/tm2/pkg/bft/rpc/core/pipe_test.go index 3147f600734..6136f66c9d8 100644 --- a/tm2/pkg/bft/rpc/core/pipe_test.go +++ b/tm2/pkg/bft/rpc/core/pipe_test.go @@ -8,6 +8,8 @@ import ( ) func TestPaginationPage(t *testing.T) { + t.Parallel() + cases := []struct { totalCount int perPage int @@ -51,6 +53,8 @@ func TestPaginationPage(t *testing.T) { } func TestPaginationPerPage(t *testing.T) { + t.Parallel() + cases := []struct { totalCount int perPage int diff --git a/tm2/pkg/bft/rpc/core/types/responses_test.go b/tm2/pkg/bft/rpc/core/types/responses_test.go index d309a6b63a1..268a8d25c34 100644 --- a/tm2/pkg/bft/rpc/core/types/responses_test.go +++ b/tm2/pkg/bft/rpc/core/types/responses_test.go @@ -9,6 +9,8 @@ import ( ) func TestStatusIndexer(t *testing.T) { + t.Parallel() + var status *ResultStatus assert.False(t, status.TxIndexEnabled()) diff --git a/tm2/pkg/bft/rpc/lib/client/args_test.go b/tm2/pkg/bft/rpc/lib/client/args_test.go index 8f44939f0aa..2a7e749f094 100644 --- a/tm2/pkg/bft/rpc/lib/client/args_test.go +++ b/tm2/pkg/bft/rpc/lib/client/args_test.go @@ -15,6 +15,8 @@ type Foo struct { } func TestArgToJSON(t *testing.T) { + t.Parallel() + assert := assert.New(t) require := require.New(t) diff --git a/tm2/pkg/bft/rpc/lib/client/http_client_test.go b/tm2/pkg/bft/rpc/lib/client/http_client_test.go index 9546a0c1d72..460f5b9947b 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client_test.go @@ -7,6 +7,8 @@ import ( ) func Test_parseRemoteAddr(t *testing.T) { + t.Parallel() + tt := []struct { remoteAddr string network, s, errContains string @@ -32,6 +34,8 @@ func Test_parseRemoteAddr(t *testing.T) { // and other protocols are left intact from parseRemoteAddr() func Test_makeHTTPDialer(t *testing.T) { + t.Parallel() + dl := makeHTTPDialer("https://.") _, err := dl("hello", "world") if assert.NotNil(t, err) { @@ -42,6 +46,8 @@ func Test_makeHTTPDialer(t *testing.T) { } func Test_makeHTTPDialer_noConvert(t *testing.T) { + t.Parallel() + dl := makeHTTPDialer("udp://.") _, err := dl("hello", "world") if assert.NotNil(t, err) { diff --git a/tm2/pkg/bft/rpc/lib/client/integration_test.go b/tm2/pkg/bft/rpc/lib/client/integration_test.go index 486540a989f..f3c705ecf98 100644 --- a/tm2/pkg/bft/rpc/lib/client/integration_test.go +++ b/tm2/pkg/bft/rpc/lib/client/integration_test.go @@ -19,6 +19,8 @@ import ( ) func TestWSClientReconnectWithJitter(t *testing.T) { + t.Parallel() + n := 8 maxReconnectAttempts := 3 // Max wait time is ceil(1+0.999) + ceil(2+0.999) + ceil(4+0.999) + ceil(...) = 2 + 3 + 5 = 10s + ... diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go b/tm2/pkg/bft/rpc/lib/client/ws_client_test.go index b3a495f25b2..47c3a50ee63 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws_client_test.go @@ -58,6 +58,8 @@ func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func TestWSClientReconnectsAfterReadFailure(t *testing.T) { + t.Parallel() + var wg sync.WaitGroup // start server @@ -91,6 +93,8 @@ func TestWSClientReconnectsAfterReadFailure(t *testing.T) { } func TestWSClientReconnectsAfterWriteFailure(t *testing.T) { + t.Parallel() + var wg sync.WaitGroup // start server @@ -121,6 +125,8 @@ func TestWSClientReconnectsAfterWriteFailure(t *testing.T) { } func TestWSClientReconnectFailure(t *testing.T) { + t.Parallel() + // start server h := &myHandler{} s := httptest.NewServer(h) @@ -172,6 +178,8 @@ func TestWSClientReconnectFailure(t *testing.T) { } func TestNotBlockingOnStop(t *testing.T) { + t.Parallel() + timeout := 2 * time.Second s := httptest.NewServer(&myHandler{}) c := startClient(t, s.Listener.Addr()) diff --git a/tm2/pkg/bft/rpc/lib/rpc_test.go b/tm2/pkg/bft/rpc/lib/rpc_test.go index 2c1aebaae14..a299417187a 100644 --- a/tm2/pkg/bft/rpc/lib/rpc_test.go +++ b/tm2/pkg/bft/rpc/lib/rpc_test.go @@ -280,6 +280,8 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { // ------------- func TestServersAndClientsBasic(t *testing.T) { + t.Parallel() + serverAddrs := [...]string{tcpAddr, unixAddr} for _, addr := range serverAddrs { cl1 := client.NewURIClient(addr) @@ -309,6 +311,8 @@ func TestServersAndClientsBasic(t *testing.T) { } func TestHexStringArg(t *testing.T) { + t.Parallel() + cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" @@ -318,6 +322,8 @@ func TestHexStringArg(t *testing.T) { } func TestQuotedStringArg(t *testing.T) { + t.Parallel() + cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" @@ -327,6 +333,8 @@ func TestQuotedStringArg(t *testing.T) { } func TestWSNewWSRPCFunc(t *testing.T) { + t.Parallel() + cl := client.NewWSClient(tcpAddr, websocketEndpoint) cl.SetLogger(log.TestingLogger()) err := cl.Start() @@ -352,6 +360,8 @@ func TestWSNewWSRPCFunc(t *testing.T) { } func TestWSHandlesArrayParams(t *testing.T) { + t.Parallel() + cl := client.NewWSClient(tcpAddr, websocketEndpoint) cl.SetLogger(log.TestingLogger()) err := cl.Start() @@ -377,6 +387,8 @@ func TestWSHandlesArrayParams(t *testing.T) { // TestWSClientPingPong checks that a client & server exchange pings // & pongs so connection stays alive. func TestWSClientPingPong(t *testing.T) { + t.Parallel() + cl := client.NewWSClient(tcpAddr, websocketEndpoint) cl.SetLogger(log.TestingLogger()) err := cl.Start() diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index 05f92a3b719..e09a8a7cf96 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -43,6 +43,8 @@ func statusOK(code int) bool { return code >= 200 && code <= 299 } // do not crash our RPC handlers. // See Issue https://github.com/gnolang/gno/tm2/pkg/bft/issues/708. func TestRPCParams(t *testing.T) { + t.Parallel() + mux := testMux() tests := []struct { payload string @@ -91,6 +93,8 @@ func TestRPCParams(t *testing.T) { } func TestJSONRPCID(t *testing.T) { + t.Parallel() + mux := testMux() tests := []struct { payload string @@ -138,6 +142,8 @@ func TestJSONRPCID(t *testing.T) { } func TestRPCNotification(t *testing.T) { + t.Parallel() + mux := testMux() body := strings.NewReader(`{"jsonrpc": "2.0", "id": ""}`) req, _ := http.NewRequest("POST", "http://localhost/", body) @@ -153,6 +159,8 @@ func TestRPCNotification(t *testing.T) { } func TestRPCNotificationInBatch(t *testing.T) { + t.Parallel() + mux := testMux() tests := []struct { payload string @@ -219,6 +227,8 @@ func TestRPCNotificationInBatch(t *testing.T) { } func TestUnknownRPCPath(t *testing.T) { + t.Parallel() + mux := testMux() req, _ := http.NewRequest("GET", "http://localhost/unknownrpcpath", nil) rec := httptest.NewRecorder() @@ -233,6 +243,8 @@ func TestUnknownRPCPath(t *testing.T) { // JSON-RPC over WEBSOCKETS func TestWebsocketManagerHandler(t *testing.T) { + t.Parallel() + s := newWSServer() defer s.Close() diff --git a/tm2/pkg/bft/rpc/lib/server/http_server_test.go b/tm2/pkg/bft/rpc/lib/server/http_server_test.go index aaf817e6d85..5ab8d2890ae 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server_test.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server_test.go @@ -21,6 +21,8 @@ import ( ) func TestMaxOpenConnections(t *testing.T) { + t.Parallel() + const max = 5 // max simultaneous connections // Start the server. @@ -70,6 +72,8 @@ func TestMaxOpenConnections(t *testing.T) { } func TestStartHTTPAndTLSServer(t *testing.T) { + t.Parallel() + ln, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) defer ln.Close() @@ -96,6 +100,8 @@ func TestStartHTTPAndTLSServer(t *testing.T) { } func TestRecoverAndLogHandler(t *testing.T) { + t.Parallel() + tests := []struct { name string panicArg any @@ -163,6 +169,8 @@ func TestRecoverAndLogHandler(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var ( req, _ = http.NewRequest(http.MethodGet, "", nil) resp = httptest.NewRecorder() diff --git a/tm2/pkg/bft/rpc/lib/server/parse_test.go b/tm2/pkg/bft/rpc/lib/server/parse_test.go index a28dd255f23..aece3f20f3a 100644 --- a/tm2/pkg/bft/rpc/lib/server/parse_test.go +++ b/tm2/pkg/bft/rpc/lib/server/parse_test.go @@ -13,6 +13,8 @@ import ( ) func TestParseJSONMap(t *testing.T) { + t.Parallel() + input := []byte(`{"value":"1234","height":22}`) // naive is float,string @@ -101,6 +103,8 @@ func TestParseJSONMap(t *testing.T) { } func TestParseJSONArray(t *testing.T) { + t.Parallel() + input := []byte(`["1234",22]`) // naive is float,string @@ -134,6 +138,8 @@ func TestParseJSONArray(t *testing.T) { } func TestParseJSONRPC(t *testing.T) { + t.Parallel() + demo := func(ctx *types.Context, height int, name string) {} call := NewRPCFunc(demo, "height,name") @@ -170,6 +176,8 @@ func TestParseJSONRPC(t *testing.T) { } func TestParseURI(t *testing.T) { + t.Parallel() + demo := func(ctx *types.Context, height int, name string) {} call := NewRPCFunc(demo, "height,name") diff --git a/tm2/pkg/bft/rpc/lib/types/types_test.go b/tm2/pkg/bft/rpc/lib/types/types_test.go index e047c69bd77..55ee8ed3945 100644 --- a/tm2/pkg/bft/rpc/lib/types/types_test.go +++ b/tm2/pkg/bft/rpc/lib/types/types_test.go @@ -31,6 +31,8 @@ var responseTests = []responseTest{ } func TestResponses(t *testing.T) { + t.Parallel() + assert := assert.New(t) for _, tt := range responseTests { jsonid := tt.id @@ -52,6 +54,8 @@ func TestResponses(t *testing.T) { } func TestUnmarshallResponses(t *testing.T) { + t.Parallel() + assert := assert.New(t) for _, tt := range responseTests { response := &RPCResponse{} @@ -66,6 +70,8 @@ func TestUnmarshallResponses(t *testing.T) { } func TestRPCError(t *testing.T) { + t.Parallel() + assert.Equal(t, "RPC error 12 - Badness: One worse than a code 11", fmt.Sprintf("%v", &RPCError{ Code: 12, diff --git a/tm2/pkg/bft/state/execution_test.go b/tm2/pkg/bft/state/execution_test.go index 849c87d3359..25217ffccbe 100644 --- a/tm2/pkg/bft/state/execution_test.go +++ b/tm2/pkg/bft/state/execution_test.go @@ -29,6 +29,8 @@ var ( ) func TestApplyBlock(t *testing.T) { + t.Parallel() + cc := proxy.NewLocalClientCreator(kvstore.NewKVStoreApplication()) proxyApp := proxy.NewAppConns(cc) err := proxyApp.Start() @@ -53,6 +55,8 @@ func TestApplyBlock(t *testing.T) { // TestBeginBlockValidators ensures we send absent validators list. func TestBeginBlockValidators(t *testing.T) { + t.Parallel() + app := &testApp{} cc := proxy.NewLocalClientCreator(app) proxyApp := proxy.NewAppConns(cc) @@ -104,6 +108,8 @@ func TestBeginBlockValidators(t *testing.T) { } func TestValidateValidatorUpdates(t *testing.T) { + t.Parallel() + pubkey1 := ed25519.GenPrivKey().PubKey() pubkey2 := ed25519.GenPrivKey().PubKey() @@ -164,6 +170,8 @@ func TestValidateValidatorUpdates(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := sm.ValidateValidatorUpdates(tc.abciUpdates, tc.validatorParams) if tc.shouldErr { assert.Error(t, err) @@ -175,6 +183,8 @@ func TestValidateValidatorUpdates(t *testing.T) { } func TestUpdateValidators(t *testing.T) { + t.Parallel() + pubkey1 := ed25519.GenPrivKey().PubKey() val1 := types.NewValidator(pubkey1, 10) pubkey2 := ed25519.GenPrivKey().PubKey() @@ -230,6 +240,8 @@ func TestUpdateValidators(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.currentSet.UpdateWithABCIValidatorUpdates(tc.abciUpdates) if tc.shouldErr { assert.Error(t, err) @@ -250,6 +262,8 @@ func TestUpdateValidators(t *testing.T) { // TestEndBlockValidatorUpdates ensures we update validator set and send an event. func TestEndBlockValidatorUpdates(t *testing.T) { + t.Parallel() + app := &testApp{} cc := proxy.NewLocalClientCreator(app) proxyApp := proxy.NewAppConns(cc) @@ -316,6 +330,8 @@ LOOP: // TestEndBlockValidatorUpdatesResultingInEmptySet checks that processing validator updates that // would result in empty set causes no panic, an error is raised and NextValidators is not updated func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { + t.Parallel() + app := &testApp{} cc := proxy.NewLocalClientCreator(app) proxyApp := proxy.NewAppConns(cc) diff --git a/tm2/pkg/bft/state/state_test.go b/tm2/pkg/bft/state/state_test.go index a48f4f0a3e4..d19b8526d98 100644 --- a/tm2/pkg/bft/state/state_test.go +++ b/tm2/pkg/bft/state/state_test.go @@ -42,6 +42,8 @@ func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, sm.State) { // TestStateCopy tests the correct copying behaviour of State. func TestStateCopy(t *testing.T) { + t.Parallel() + t.Helper() tearDown, _, state := setupTestCase(t) @@ -62,6 +64,8 @@ func TestStateCopy(t *testing.T) { // TestMakeGenesisStateNilValidators tests state's consistency when genesis file's validators field is nil. func TestMakeGenesisStateNilValidators(t *testing.T) { + t.Parallel() + doc := types.GenesisDoc{ ChainID: "dummy", Validators: nil, @@ -75,6 +79,8 @@ func TestMakeGenesisStateNilValidators(t *testing.T) { // TestStateSaveLoad tests saving and loading State from a db. func TestStateSaveLoad(t *testing.T) { + t.Parallel() + tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) //nolint: vetshadow @@ -91,6 +97,8 @@ func TestStateSaveLoad(t *testing.T) { // TestABCIResponsesSaveLoad tests saving and loading ABCIResponses. func TestABCIResponsesSaveLoad1(t *testing.T) { + t.Parallel() + tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) //nolint: vetshadow @@ -129,6 +137,8 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // TestResultsSaveLoad tests saving and loading ABCI results. func TestABCIResponsesSaveLoad2(t *testing.T) { + t.Parallel() + tearDown, stateDB, _ := setupTestCase(t) defer tearDown(t) //nolint: vetshadow @@ -223,6 +233,8 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { // TestValidatorSimpleSaveLoad tests saving and loading validators. func TestValidatorSimpleSaveLoad(t *testing.T) { + t.Parallel() + tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) //nolint: vetshadow @@ -256,6 +268,8 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { // TestValidatorChangesSaveLoad tests saving and loading a validator set with changes. func TestOneValidatorChangesSaveLoad(t *testing.T) { + t.Parallel() + tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) @@ -309,6 +323,8 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { } func TestProposerFrequency(t *testing.T) { + t.Parallel() + // some explicit test cases testCases := []struct { powers []int64 @@ -432,6 +448,8 @@ func testProposerFreq(t *testing.T, caseNum int, valSet *types.ValidatorSet) { // TestProposerPriorityDoesNotGetResetToZero assert that we preserve accum when calling updateState // see https://github.com/tendermint/classic/issues/2718 func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { + t.Parallel() + tearDown, _, state := setupTestCase(t) defer tearDown(t) val1VotingPower := int64(10) @@ -534,6 +552,8 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { } func TestProposerPriorityProposerAlternates(t *testing.T) { + t.Parallel() + // Regression test that would fail if the inner workings of // IncrementProposerPriority change. // Additionally, make sure that same power validators alternate if both @@ -670,6 +690,8 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { } func TestLargeGenesisValidator(t *testing.T) { + t.Parallel() + tearDown, _, state := setupTestCase(t) defer tearDown(t) @@ -822,6 +844,8 @@ func TestLargeGenesisValidator(t *testing.T) { } func TestStoreLoadValidatorsIncrementsProposerPriority(t *testing.T) { + t.Parallel() + const valSetSize = 2 tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) @@ -845,6 +869,8 @@ func TestStoreLoadValidatorsIncrementsProposerPriority(t *testing.T) { // TestValidatorChangesSaveLoad tests saving and loading a validator set with // changes. func TestManyValidatorChangesSaveLoad(t *testing.T) { + t.Parallel() + const valSetSize = 7 tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) @@ -890,6 +916,8 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } func TestStateMakeBlock(t *testing.T) { + t.Parallel() + tearDown, _, state := setupTestCase(t) defer tearDown(t) @@ -905,6 +933,8 @@ func TestStateMakeBlock(t *testing.T) { // TestConsensusParamsChangesSaveLoad tests saving and loading consensus params // with changes. func TestConsensusParamsChangesSaveLoad(t *testing.T) { + t.Parallel() + tearDown, stateDB, state := setupTestCase(t) defer tearDown(t) @@ -964,6 +994,8 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { } func TestApplyUpdates(t *testing.T) { + t.Parallel() + initParams := makeConsensusParams(1, 2, 3, 3, 4) cases := [...]struct { diff --git a/tm2/pkg/bft/state/store_test.go b/tm2/pkg/bft/state/store_test.go index 79282e6a311..ed3b8e63311 100644 --- a/tm2/pkg/bft/state/store_test.go +++ b/tm2/pkg/bft/state/store_test.go @@ -15,6 +15,8 @@ import ( ) func TestStoreLoadValidators(t *testing.T) { + t.Parallel() + stateDB := dbm.NewMemDB() val, _ := types.RandValidator(true, 10) vals := types.NewValidatorSet([]*types.Validator{val}) diff --git a/tm2/pkg/bft/state/validation_test.go b/tm2/pkg/bft/state/validation_test.go index 94aafe92694..c1941381de7 100644 --- a/tm2/pkg/bft/state/validation_test.go +++ b/tm2/pkg/bft/state/validation_test.go @@ -19,6 +19,8 @@ import ( const validationTestsStopHeight int64 = 10 func TestValidateBlockHeader(t *testing.T) { + t.Parallel() + proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() @@ -80,6 +82,8 @@ func TestValidateBlockHeader(t *testing.T) { } func TestValidateBlockCommit(t *testing.T) { + t.Parallel() + proxyApp := newTestApp() require.NoError(t, proxyApp.Start()) defer proxyApp.Stop() diff --git a/tm2/pkg/bft/store/store_test.go b/tm2/pkg/bft/store/store_test.go index 1dc0bda833c..fce566903c3 100644 --- a/tm2/pkg/bft/store/store_test.go +++ b/tm2/pkg/bft/store/store_test.go @@ -58,6 +58,8 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu } func TestLoadBlockStoreStateJSON(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() bsj := &BlockStoreStateJSON{Height: 1000} @@ -69,6 +71,8 @@ func TestLoadBlockStoreStateJSON(t *testing.T) { } func TestNewBlockStore(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() db.Set(blockStoreKey, []byte(`{"height": "10000"}`)) bs := NewBlockStore(db) @@ -129,6 +133,8 @@ func TestMain(m *testing.M) { // TODO: This test should be simplified ... func TestBlockStoreSaveLoadBlock(t *testing.T) { + t.Parallel() + state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) defer cleanup() require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") @@ -325,6 +331,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) { } func TestLoadBlockPart(t *testing.T) { + t.Parallel() + bs, db := freshBlockStore() height, index := int64(10), 1 loadPart := func() (interface{}, error) { @@ -354,6 +362,8 @@ func TestLoadBlockPart(t *testing.T) { } func TestLoadBlockMeta(t *testing.T) { + t.Parallel() + bs, db := freshBlockStore() height := int64(10) loadMeta := func() (interface{}, error) { @@ -384,6 +394,8 @@ func TestLoadBlockMeta(t *testing.T) { } func TestBlockFetchAtHeight(t *testing.T) { + t.Parallel() + state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer))) defer cleanup() require.Equal(t, bs.Height(), int64(0), "initially the height should be zero") diff --git a/tm2/pkg/bft/types/block_test.go b/tm2/pkg/bft/types/block_test.go index 5b49e0c392e..3963378436b 100644 --- a/tm2/pkg/bft/types/block_test.go +++ b/tm2/pkg/bft/types/block_test.go @@ -21,6 +21,8 @@ import ( ) func TestBlockValidateBasic(t *testing.T) { + t.Parallel() + require.Error(t, (*Block)(nil).ValidateBasic()) txs := []Tx{Tx("foo"), Tx("bar")} @@ -57,6 +59,8 @@ func TestBlockValidateBasic(t *testing.T) { tc := tc i := i t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + block := MakeBlock(h, txs, commit) block.ProposerAddress = valSet.GetProposer().Address tc.malleateBlock(block) @@ -67,11 +71,15 @@ func TestBlockValidateBasic(t *testing.T) { } func TestBlockHash(t *testing.T) { + t.Parallel() + assert.Nil(t, (*Block)(nil).Hash()) assert.Nil(t, MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil).Hash()) } func TestBlockMakePartSet(t *testing.T) { + t.Parallel() + assert.Nil(t, (*Block)(nil).MakePartSet(2)) partSet := MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil).MakePartSet(1024) @@ -80,6 +88,8 @@ func TestBlockMakePartSet(t *testing.T) { } func TestBlockHashesTo(t *testing.T) { + t.Parallel() + assert.False(t, (*Block)(nil).HashesTo(nil)) lastID := makeBlockIDRandom() @@ -96,6 +106,8 @@ func TestBlockHashesTo(t *testing.T) { } func TestBlockSize(t *testing.T) { + t.Parallel() + size := MakeBlock(int64(3), []Tx{Tx("Hello World")}, nil).Size() if size <= 0 { t.Fatal("Size of the block is zero or negative") @@ -103,6 +115,8 @@ func TestBlockSize(t *testing.T) { } func TestBlockString(t *testing.T) { + t.Parallel() + assert.Equal(t, "nil-Block", (*Block)(nil).String()) assert.Equal(t, "nil-Block", (*Block)(nil).StringIndented("")) assert.Equal(t, "nil-Block", (*Block)(nil).StringShort()) @@ -135,16 +149,22 @@ func makeBlockID(hash []byte, partSetSize int, partSetHash []byte) BlockID { var nilBytes []byte func TestNilHeaderHashDoesntCrash(t *testing.T) { + t.Parallel() + assert.Equal(t, (*Header)(nil).Hash(), nilBytes) assert.Equal(t, (new(Header)).Hash(), nilBytes) } func TestNilDataHashDoesntCrash(t *testing.T) { + t.Parallel() + assert.Equal(t, (*Data)(nil).Hash(), nilBytes) assert.Equal(t, new(Data).Hash(), nilBytes) } func TestCommit(t *testing.T) { + t.Parallel() + lastID := makeBlockIDRandom() h := int64(3) voteSet, _, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) @@ -166,6 +186,8 @@ func TestCommit(t *testing.T) { } func TestCommitValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string malleateCommit func(*Commit) @@ -181,6 +203,8 @@ func TestCommitValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + com := randCommit() tc.malleateCommit(com) assert.Equal(t, tc.expectErr, com.ValidateBasic() != nil, "Validate Basic had an unexpected result") @@ -189,6 +213,8 @@ func TestCommitValidateBasic(t *testing.T) { } func TestHeaderByteSize(t *testing.T) { + t.Parallel() + // Construct a UTF-8 string of MaxChainIDLen length using the supplementary // characters. // Each supplementary character takes 4 bytes. @@ -239,6 +265,8 @@ func randCommit() *Commit { } func TestCommitToVoteSet(t *testing.T) { + t.Parallel() + lastID := makeBlockIDRandom() h := int64(3) @@ -263,6 +291,8 @@ func TestCommitToVoteSet(t *testing.T) { } func TestCommitToVoteSetWithVotesForAnotherBlockOrNilBlock(t *testing.T) { + t.Parallel() + blockID := makeBlockID([]byte("blockhash"), 1000, []byte("partshash")) blockID2 := makeBlockID([]byte("blockhash2"), 1000, []byte("partshash")) blockID3 := makeBlockID([]byte("blockhash3"), 10000, []byte("partshash")) @@ -320,6 +350,8 @@ func TestCommitToVoteSetWithVotesForAnotherBlockOrNilBlock(t *testing.T) { } func TestSignedHeaderValidateBasic(t *testing.T) { + t.Parallel() + commit := randCommit() chainID := "𠜎" timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) @@ -360,6 +392,8 @@ func TestSignedHeaderValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + sh := SignedHeader{ Header: tc.shHeader, Commit: tc.shCommit, @@ -370,6 +404,8 @@ func TestSignedHeaderValidateBasic(t *testing.T) { } func TestBlockIDValidateBasic(t *testing.T) { + t.Parallel() + validBlockID := BlockID{ Hash: []byte{}, PartsHeader: PartSetHeader{ @@ -400,6 +436,8 @@ func TestBlockIDValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + blockID := BlockID{ Hash: tc.blockIDHash, PartsHeader: tc.blockIDPartsHeader, diff --git a/tm2/pkg/bft/types/evidence_test.go b/tm2/pkg/bft/types/evidence_test.go index 4efc61f8be7..5c43c547b74 100644 --- a/tm2/pkg/bft/types/evidence_test.go +++ b/tm2/pkg/bft/types/evidence_test.go @@ -37,6 +37,8 @@ func makeVote(val PrivValidator, chainID string, valIndex int, height int64, rou } func TestEvidence(t *testing.T) { + t.Parallel() + val := NewMockPV() val2 := NewMockPV() @@ -83,6 +85,8 @@ func TestEvidence(t *testing.T) { } func TestDuplicatedVoteEvidence(t *testing.T) { + t.Parallel() + ev := randomDuplicatedVoteEvidence() assert.True(t, ev.Equal(ev)) @@ -90,6 +94,8 @@ func TestDuplicatedVoteEvidence(t *testing.T) { } func TestEvidenceList(t *testing.T) { + t.Parallel() + ev := randomDuplicatedVoteEvidence() evl := EvidenceList([]Evidence{ev}) @@ -99,6 +105,8 @@ func TestEvidenceList(t *testing.T) { } func TestEvidenceByteSize(t *testing.T) { + t.Parallel() + val := NewMockPV() blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) blockID2 := makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) @@ -127,6 +135,8 @@ func randomDuplicatedVoteEvidence() *DuplicateVoteEvidence { } func TestDuplicateVoteEvidenceValidation(t *testing.T) { + t.Parallel() + val := NewMockPV() blockID := makeBlockID(tmhash.Sum([]byte("blockhash")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) blockID2 := makeBlockID(tmhash.Sum([]byte("blockhash2")), math.MaxInt64, tmhash.Sum([]byte("partshash"))) @@ -151,6 +161,8 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + ev := &DuplicateVoteEvidence{ PubKey: secp256k1.GenPrivKey().PubKey(), VoteA: makeVote(val, chainID, math.MaxInt64, math.MaxInt64, math.MaxInt64, 0x02, blockID), @@ -163,11 +175,15 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { } func TestMockGoodEvidenceValidateBasic(t *testing.T) { + t.Parallel() + goodEvidence := NewMockGoodEvidence(int64(1), 1, crypto.AddressFromPreimage([]byte{1})) assert.Nil(t, goodEvidence.ValidateBasic()) } func TestMockBadEvidenceValidateBasic(t *testing.T) { + t.Parallel() + badEvidence := MockBadEvidence{MockGoodEvidence: NewMockGoodEvidence(int64(1), 1, crypto.AddressFromPreimage([]byte{1}))} assert.Nil(t, badEvidence.ValidateBasic()) } diff --git a/tm2/pkg/bft/types/genesis_test.go b/tm2/pkg/bft/types/genesis_test.go index f8d19324eb5..24c69c6a28e 100644 --- a/tm2/pkg/bft/types/genesis_test.go +++ b/tm2/pkg/bft/types/genesis_test.go @@ -14,6 +14,8 @@ import ( ) func TestGenesisBad(t *testing.T) { + t.Parallel() + // test some bad ones from raw json testCases := [][]byte{ {}, // empty @@ -37,6 +39,8 @@ func TestGenesisBad(t *testing.T) { } func TestGenesisGood(t *testing.T) { + t.Parallel() + // test a good one by raw json genDocBytes := []byte(`{"genesis_time":"0001-01-01T00:00:00Z","chain_id":"test-chain-QDKdJr","consensus_params":null,"validators":[{"pub_key":{"@type":"/tm.PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="},"power":"10","name":""}],"app_hash":"","app_state":{"@type":"/tm.MockAppState","account_owner":"Bob"}}`) _, err := GenesisDocFromJSON(genDocBytes) @@ -87,6 +91,8 @@ func TestGenesisGood(t *testing.T) { } func TestGenesisSaveAs(t *testing.T) { + t.Parallel() + tmpfile, err := os.CreateTemp("", "genesis") require.NoError(t, err) defer os.Remove(tmpfile.Name()) @@ -114,6 +120,8 @@ func TestGenesisSaveAs(t *testing.T) { } func TestGenesisValidatorHash(t *testing.T) { + t.Parallel() + genDoc := randomGenesisDoc() assert.NotEmpty(t, genDoc.ValidatorHash()) } diff --git a/tm2/pkg/bft/types/params_test.go b/tm2/pkg/bft/types/params_test.go index cb5f3244e8d..141a67b903c 100644 --- a/tm2/pkg/bft/types/params_test.go +++ b/tm2/pkg/bft/types/params_test.go @@ -16,6 +16,8 @@ var ( ) func TestConsensusParamsValidation(t *testing.T) { + t.Parallel() + testCases := []struct { params abci.ConsensusParams valid bool @@ -63,6 +65,8 @@ func makeParams( } func TestConsensusParamsHash(t *testing.T) { + t.Parallel() + params := []abci.ConsensusParams{ makeParams(4, 1024, 2, 10, valEd25519), makeParams(1, 1024, 4, 10, valEd25519), @@ -90,6 +94,8 @@ func TestConsensusParamsHash(t *testing.T) { } func TestConsensusParamsUpdate(t *testing.T) { + t.Parallel() + testCases := []struct { params abci.ConsensusParams updates abci.ConsensusParams diff --git a/tm2/pkg/bft/types/part_set_test.go b/tm2/pkg/bft/types/part_set_test.go index c4e30c89372..13ebb2f0bba 100644 --- a/tm2/pkg/bft/types/part_set_test.go +++ b/tm2/pkg/bft/types/part_set_test.go @@ -16,6 +16,8 @@ const ( ) func TestBasicPartSet(t *testing.T) { + t.Parallel() + // Construct random data of size partSize * 100 data := random.RandBytes(testPartSize * 100) partSet := NewPartSetFromData(data, testPartSize) @@ -61,6 +63,8 @@ func TestBasicPartSet(t *testing.T) { } func TestWrongProof(t *testing.T) { + t.Parallel() + // Construct random data of size partSize * 100 data := random.RandBytes(testPartSize * 100) partSet := NewPartSetFromData(data, testPartSize) @@ -86,6 +90,8 @@ func TestWrongProof(t *testing.T) { } func TestPartSetHeaderValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string malleatePartSetHeader func(*PartSetHeader) @@ -98,6 +104,8 @@ func TestPartSetHeaderValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + data := random.RandBytes(testPartSize * 100) ps := NewPartSetFromData(data, testPartSize) psHeader := ps.Header() @@ -108,6 +116,8 @@ func TestPartSetHeaderValidateBasic(t *testing.T) { } func TestPartValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string malleatePart func(*Part) @@ -128,6 +138,8 @@ func TestPartValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + data := random.RandBytes(testPartSize * 100) ps := NewPartSetFromData(data, testPartSize) part := ps.GetPart(0) diff --git a/tm2/pkg/bft/types/proposal_test.go b/tm2/pkg/bft/types/proposal_test.go index ebda33298d1..0692e9bdd6f 100644 --- a/tm2/pkg/bft/types/proposal_test.go +++ b/tm2/pkg/bft/types/proposal_test.go @@ -29,6 +29,8 @@ func init() { } func TestProposalSignable(t *testing.T) { + t.Parallel() + chainID := "test_chain_id" signBytes := testProposal.SignBytes(chainID) @@ -38,6 +40,8 @@ func TestProposalSignable(t *testing.T) { } func TestProposalString(t *testing.T) { + t.Parallel() + str := testProposal.String() expected := `Proposal{12345/23456 (010203:111:626C6F636B70, -1) 000000000000 @ 2018-02-11T07:09:22.765Z}` if str != expected { @@ -46,6 +50,8 @@ func TestProposalString(t *testing.T) { } func TestProposalVerifySignature(t *testing.T) { + t.Parallel() + privVal := NewMockPV() pubKey := privVal.GetPubKey() @@ -104,6 +110,8 @@ func BenchmarkProposalVerifySignature(b *testing.B) { } func TestProposalValidateBasic(t *testing.T) { + t.Parallel() + privVal := NewMockPV() testCases := []struct { testName string @@ -130,6 +138,8 @@ func TestProposalValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + prop := NewProposal( 4, 2, 2, blockID) diff --git a/tm2/pkg/bft/types/results_test.go b/tm2/pkg/bft/types/results_test.go index 6db7044f6ad..6b375413ac6 100644 --- a/tm2/pkg/bft/types/results_test.go +++ b/tm2/pkg/bft/types/results_test.go @@ -10,6 +10,8 @@ import ( ) func TestABCIResults(t *testing.T) { + t.Parallel() + a := ABCIResult{Error: nil, Data: nil} b := ABCIResult{Error: nil, Data: []byte{}} c := ABCIResult{Error: nil, Data: []byte("one")} @@ -46,6 +48,8 @@ func TestABCIResults(t *testing.T) { } func TestABCIResultsBytes(t *testing.T) { + t.Parallel() + results := NewResults([]abci.ResponseDeliverTx{ {ResponseBase: abci.ResponseBase{Error: nil, Data: []byte{}}}, {ResponseBase: abci.ResponseBase{Error: nil, Data: []byte("one")}}, diff --git a/tm2/pkg/bft/types/time/time_test.go b/tm2/pkg/bft/types/time/time_test.go index 1b1a30e5058..bf53e313fef 100644 --- a/tm2/pkg/bft/types/time/time_test.go +++ b/tm2/pkg/bft/types/time/time_test.go @@ -8,6 +8,8 @@ import ( ) func TestWeightedMedian(t *testing.T) { + t.Parallel() + m := make([]*WeightedTime, 3) t1 := Now() diff --git a/tm2/pkg/bft/types/tx_test.go b/tm2/pkg/bft/types/tx_test.go index 1470fab024d..375783f874f 100644 --- a/tm2/pkg/bft/types/tx_test.go +++ b/tm2/pkg/bft/types/tx_test.go @@ -25,6 +25,8 @@ func randInt(low, high int) int { } func TestTxIndex(t *testing.T) { + t.Parallel() + for i := 0; i < 20; i++ { txs := makeTxs(15, 60) for j := 0; j < len(txs); j++ { @@ -38,6 +40,8 @@ func TestTxIndex(t *testing.T) { } func TestTxIndexByHash(t *testing.T) { + t.Parallel() + for i := 0; i < 20; i++ { txs := makeTxs(15, 60) for j := 0; j < len(txs); j++ { @@ -51,6 +55,8 @@ func TestTxIndexByHash(t *testing.T) { } func TestValidTxProof(t *testing.T) { + t.Parallel() + cases := []struct { txs Txs }{ @@ -90,6 +96,8 @@ func TestValidTxProof(t *testing.T) { } func TestTxProofUnchangable(t *testing.T) { + t.Parallel() + // run the other test a bunch... for i := 0; i < 40; i++ { testTxProofUnchangable(t) diff --git a/tm2/pkg/bft/types/validator_set_test.go b/tm2/pkg/bft/types/validator_set_test.go index a2c38026ed8..0fafb6fca9e 100644 --- a/tm2/pkg/bft/types/validator_set_test.go +++ b/tm2/pkg/bft/types/validator_set_test.go @@ -20,6 +20,8 @@ import ( ) func TestValidatorSetBasic(t *testing.T) { + t.Parallel() + // empty or nil validator lists are allowed, // but attempting to IncrementProposerPriority on them will panic. vset := NewValidatorSet([]*Validator{}) @@ -76,6 +78,8 @@ func TestValidatorSetBasic(t *testing.T) { } func TestCopy(t *testing.T) { + t.Parallel() + vset := randValidatorSet(10) vsetHash := vset.Hash() if len(vsetHash) == 0 { @@ -92,6 +96,8 @@ func TestCopy(t *testing.T) { // Test that IncrementProposerPriority requires positive times. func TestIncrementProposerPriorityPositiveTimes(t *testing.T) { + t.Parallel() + vset := NewValidatorSet([]*Validator{ newValidator([]byte("foo"), 1000), newValidator([]byte("bar"), 300), @@ -122,9 +128,11 @@ func BenchmarkValidatorSetCopy(b *testing.B) { } } -//------------------------------------------------------------------- +// ------------------------------------------------------------------- func TestProposerSelection1(t *testing.T) { + t.Parallel() + vset := NewValidatorSet([]*Validator{ newValidator([]byte("foo"), 1000), newValidator([]byte("bar"), 300), @@ -143,6 +151,8 @@ func TestProposerSelection1(t *testing.T) { } func TestProposerSelection2(t *testing.T) { + t.Parallel() + addr0 := []byte{1} addr1 := []byte{2} addr2 := []byte{3} @@ -217,6 +227,8 @@ func TestProposerSelection2(t *testing.T) { } func TestProposerSelection3(t *testing.T) { + t.Parallel() + vset := NewValidatorSet([]*Validator{ newValidator([]byte("a"), 1), newValidator([]byte("b"), 1), @@ -321,9 +333,11 @@ func (vals *ValidatorSet) fromBytes(b []byte) { } } -//------------------------------------------------------------------- +// ------------------------------------------------------------------- func TestValidatorSetTotalVotingPowerPanicsOnOverflow(t *testing.T) { + t.Parallel() + // NewValidatorSet calls IncrementProposerPriority which calls TotalVotingPower() // which should panic on overflows: shouldPanic := func() { @@ -338,6 +352,8 @@ func TestValidatorSetTotalVotingPowerPanicsOnOverflow(t *testing.T) { } func TestAvgProposerPriority(t *testing.T) { + t.Parallel() + // Create Validator set without calling IncrementProposerPriority: tcs := []struct { vs ValidatorSet @@ -356,6 +372,8 @@ func TestAvgProposerPriority(t *testing.T) { } func TestAveragingInIncrementProposerPriority(t *testing.T) { + t.Parallel() + // Test that the averaging works as expected inside of IncrementProposerPriority. // Each validator comes with zero voting power which simplifies reasoning about // the expected ProposerPriority. @@ -408,6 +426,8 @@ func TestAveragingInIncrementProposerPriority(t *testing.T) { } func TestAveragingInIncrementProposerPriorityWithVotingPower(t *testing.T) { + t.Parallel() + // Other than TestAveragingInIncrementProposerPriority this is a more complete test showing // how each ProposerPriority changes in relation to the validator's voting power respectively. // average is zero in each round: @@ -558,6 +578,8 @@ func TestAveragingInIncrementProposerPriorityWithVotingPower(t *testing.T) { } func TestSafeAdd(t *testing.T) { + t.Parallel() + f := func(a, b int64) bool { c, overflow := safeAdd(a, b) return overflow || (!overflow && c == a+b) @@ -568,21 +590,27 @@ func TestSafeAdd(t *testing.T) { } func TestSafeAddClip(t *testing.T) { + t.Parallel() + assert.EqualValues(t, math.MaxInt64, safeAddClip(math.MaxInt64, 10)) assert.EqualValues(t, math.MaxInt64, safeAddClip(math.MaxInt64, math.MaxInt64)) assert.EqualValues(t, math.MinInt64, safeAddClip(math.MinInt64, -10)) } func TestSafeSubClip(t *testing.T) { + t.Parallel() + assert.EqualValues(t, math.MinInt64, safeSubClip(math.MinInt64, 10)) assert.EqualValues(t, 0, safeSubClip(math.MinInt64, math.MinInt64)) assert.EqualValues(t, math.MinInt64, safeSubClip(math.MinInt64, math.MaxInt64)) assert.EqualValues(t, math.MaxInt64, safeSubClip(math.MaxInt64, -10)) } -//------------------------------------------------------------------- +// ------------------------------------------------------------------- func TestValidatorSetVerifyCommit(t *testing.T) { + t.Parallel() + privKey := mock.GenPrivKey() pubKey := privKey.PubKey() v1 := NewValidator(pubKey, 1000) @@ -635,6 +663,8 @@ func TestValidatorSetVerifyCommit(t *testing.T) { } func TestEmptySet(t *testing.T) { + t.Parallel() + var valList []*Validator valSet := NewValidatorSet(valList) assert.Panics(t, func() { valSet.IncrementProposerPriority(1) }) @@ -661,6 +691,8 @@ func TestEmptySet(t *testing.T) { } func TestUpdatesForNewValidatorSet(t *testing.T) { + t.Parallel() + v1 := newValidator([]byte("v1"), 100) v2 := newValidator([]byte("v2"), 100) valList := []*Validator{v1, v2} @@ -793,6 +825,8 @@ func executeValSetErrTestCase(t *testing.T, idx int, tt valSetErrTestCase) { } func TestValSetUpdatesDuplicateEntries(t *testing.T) { + t.Parallel() + testCases := []valSetErrTestCase{ // Duplicate entries in changes { // first entry is duplicated change @@ -850,6 +884,8 @@ func TestValSetUpdatesDuplicateEntries(t *testing.T) { } func TestValSetUpdatesOverflows(t *testing.T) { + t.Parallel() + maxVP := MaxTotalVotingPower testCases := []valSetErrTestCase{ { // single update leading to overflow @@ -884,6 +920,8 @@ func TestValSetUpdatesOverflows(t *testing.T) { } func TestValSetUpdatesOtherErrors(t *testing.T) { + t.Parallel() + testCases := []valSetErrTestCase{ { // update with negative voting power testValSet(2, 10), @@ -909,6 +947,8 @@ func TestValSetUpdatesOtherErrors(t *testing.T) { } func TestValSetUpdatesBasicTestsExecute(t *testing.T) { + t.Parallel() + valSetUpdatesBasicTests := []struct { startVals []testVal updateVals []testVal @@ -970,6 +1010,8 @@ func TestValSetUpdatesBasicTestsExecute(t *testing.T) { // Test that different permutations of an update give the same result. func TestValSetUpdatesOrderIndependenceTestsExecute(t *testing.T) { + t.Parallel() + // startVals - initial validators to create the set with // updateVals - a sequence of updates to be applied to the set. // updateVals is shuffled a number of times during testing to check for same resulting validator set. @@ -1031,6 +1073,8 @@ func TestValSetUpdatesOrderIndependenceTestsExecute(t *testing.T) { // This tests the private function validator_set.go:applyUpdates() function, used only for additions and changes. // Should perform a proper merge of updatedVals and startVals func TestValSetApplyUpdatesTestsExecute(t *testing.T) { + t.Parallel() + valSetUpdatesBasicTests := []struct { startVals []testVal updateVals []testVal @@ -1174,6 +1218,8 @@ func applyChangesToValSet(t *testing.T, valSet *ValidatorSet, valsLists ...[]tes } func TestValSetUpdatePriorityOrderTests(t *testing.T) { + t.Parallel() + const nMaxElections = 5000 testCases := []testVSetCfg{ diff --git a/tm2/pkg/bft/types/vote_set_test.go b/tm2/pkg/bft/types/vote_set_test.go index e3199f83edc..ca5639f2406 100644 --- a/tm2/pkg/bft/types/vote_set_test.go +++ b/tm2/pkg/bft/types/vote_set_test.go @@ -61,6 +61,8 @@ func withBlockPartsHeader(vote *Vote, blockPartsHeader PartSetHeader) *Vote { } func TestAddVote(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrevoteType, 10, 1) val0 := privValidators[0] @@ -106,6 +108,8 @@ func TestAddVote(t *testing.T) { } func Test2_3Majority(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrevoteType, 10, 1) @@ -162,6 +166,8 @@ func Test2_3Majority(t *testing.T) { } func Test2_3MajorityRedux(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrevoteType, 100, 1) @@ -267,6 +273,8 @@ func Test2_3MajorityRedux(t *testing.T) { } func TestBadVotes(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrevoteType, 10, 1) @@ -332,6 +340,8 @@ func TestBadVotes(t *testing.T) { } func TestConflicts(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrevoteType, 4, 1) blockHash1 := random.RandBytes(32) @@ -465,6 +475,8 @@ func TestConflicts(t *testing.T) { } func TestMakeCommit(t *testing.T) { + t.Parallel() + height, round := int64(1), 0 voteSet, _, privValidators := randVoteSet(height, round, PrecommitType, 10, 1) blockHash, blockPartsHeader := crypto.CRandBytes(32), PartSetHeader{123, crypto.CRandBytes(32)} diff --git a/tm2/pkg/bft/types/vote_test.go b/tm2/pkg/bft/types/vote_test.go index 1028694ebc5..8b34e474fbd 100644 --- a/tm2/pkg/bft/types/vote_test.go +++ b/tm2/pkg/bft/types/vote_test.go @@ -50,6 +50,8 @@ func exampleVote(t byte) *Vote { // This test will fail and can be removed once CommitSig contains only sigs and // timestamps. func TestVoteEncoding(t *testing.T) { + t.Parallel() + vote := examplePrecommit() commitSig := vote.CommitSig() bz1 := amino.MustMarshal(vote) @@ -58,6 +60,8 @@ func TestVoteEncoding(t *testing.T) { } func TestVoteSignable(t *testing.T) { + t.Parallel() + vote := examplePrecommit() signBytes := vote.SignBytes("test_chain_id") @@ -68,6 +72,8 @@ func TestVoteSignable(t *testing.T) { } func TestVoteSignBytesTestVectors(t *testing.T) { + t.Parallel() + tests := []struct { chainID string vote *Vote @@ -147,6 +153,8 @@ func TestVoteSignBytesTestVectors(t *testing.T) { } func TestVoteProposalNotEq(t *testing.T) { + t.Parallel() + cv := CanonicalizeVote("", &Vote{Height: 1, Round: 1}) p := CanonicalizeProposal("", &Proposal{Height: 1, Round: 1}) vb, err := amino.MarshalSized(cv) @@ -157,6 +165,8 @@ func TestVoteProposalNotEq(t *testing.T) { } func TestVoteVerifySignature(t *testing.T) { + t.Parallel() + privVal := NewMockPV() pubkey := privVal.GetPubKey() @@ -186,6 +196,8 @@ func TestVoteVerifySignature(t *testing.T) { } func TestIsVoteTypeValid(t *testing.T) { + t.Parallel() + tc := []struct { name string in SignedMsgType @@ -199,6 +211,8 @@ func TestIsVoteTypeValid(t *testing.T) { for _, tt := range tc { tt := tt t.Run(tt.name, func(st *testing.T) { + st.Parallel() + if rs := IsVoteTypeValid(tt.in); rs != tt.out { t.Errorf("Got unexpected Vote type. Expected:\n%v\nGot:\n%v", rs, tt.out) } @@ -207,6 +221,8 @@ func TestIsVoteTypeValid(t *testing.T) { } func TestVoteVerify(t *testing.T) { + t.Parallel() + privVal := NewMockPV() pubkey := privVal.GetPubKey() @@ -225,6 +241,8 @@ func TestVoteVerify(t *testing.T) { } func TestMaxVoteBytes(t *testing.T) { + t.Parallel() + // time is varint encoded so need to pick the max. // year int, month Month, day, hour, min, sec, nsec int, loc *Location timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) @@ -256,6 +274,8 @@ func TestMaxVoteBytes(t *testing.T) { } func TestVoteString(t *testing.T) { + t.Parallel() + str := examplePrecommit().String() expected := `Vote{56789:6AF1F4111082 12345/02/2(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` if str != expected { @@ -270,6 +290,8 @@ func TestVoteString(t *testing.T) { } func TestVoteValidateBasic(t *testing.T) { + t.Parallel() + privVal := NewMockPV() testCases := []struct { @@ -289,6 +311,8 @@ func TestVoteValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + vote := examplePrecommit() err := privVal.SignVote("test_chain_id", vote) require.NoError(t, err) diff --git a/tm2/pkg/bft/wal/wal_test.go b/tm2/pkg/bft/wal/wal_test.go index a1a3e831a79..322ee82bc2b 100644 --- a/tm2/pkg/bft/wal/wal_test.go +++ b/tm2/pkg/bft/wal/wal_test.go @@ -39,6 +39,8 @@ var _ = amino.RegisterPackage(amino.NewPackage( )) func TestWALWriterReader(t *testing.T) { + t.Parallel() + now := tmtime.Now() msgs := []TimedWALMessage{ {Time: now, Msg: TestMessage{Duration: time.Second, Height: 1, Round: 1}}, @@ -96,6 +98,8 @@ func makeTempWAL(t *testing.T, maxMsgSize int64, walChunkSize int64) (wal *baseW } func TestWALWrite(t *testing.T) { + t.Parallel() + // Create WAL const walChunkSize = 100000 wal := makeTempWAL(t, maxTestMsgSize, walChunkSize) @@ -119,6 +123,8 @@ func TestWALWrite(t *testing.T) { } func TestWALSearchForHeight(t *testing.T) { + t.Parallel() + // Create WAL const numHeight, numRounds, dataSize = 100, 10000, 10 const walChunkSize = 100000 @@ -161,6 +167,8 @@ func TestWALSearchForHeight(t *testing.T) { } func TestWALPeriodicSync(t *testing.T) { + t.Parallel() + // Create WAL const numHeight, numRounds, dataSize = 100, 10000, 10 const walChunkSize = 100000 diff --git a/tm2/pkg/bitarray/bit_array_test.go b/tm2/pkg/bitarray/bit_array_test.go index a70de110790..614d56d22cc 100644 --- a/tm2/pkg/bitarray/bit_array_test.go +++ b/tm2/pkg/bitarray/bit_array_test.go @@ -28,6 +28,8 @@ func randBitArray(bits int) (*BitArray, []byte) { } func TestAnd(t *testing.T) { + t.Parallel() + bA1, _ := randBitArray(51) bA2, _ := randBitArray(31) bA3 := bA1.And(bA2) @@ -52,6 +54,8 @@ func TestAnd(t *testing.T) { } func TestOr(t *testing.T) { + t.Parallel() + bA1, _ := randBitArray(51) bA2, _ := randBitArray(31) bA3 := bA1.Or(bA2) @@ -76,6 +80,8 @@ func TestOr(t *testing.T) { } func TestSub(t *testing.T) { + t.Parallel() + testCases := []struct { initBA string subtractingBA string @@ -107,6 +113,8 @@ func TestSub(t *testing.T) { } func TestPickRandom(t *testing.T) { + t.Parallel() + empty16Bits := "________________" empty64Bits := empty16Bits + empty16Bits + empty16Bits + empty16Bits testCases := []struct { @@ -134,6 +142,8 @@ func TestPickRandom(t *testing.T) { } func TestBytes(t *testing.T) { + t.Parallel() + bA := NewBitArray(4) bA.SetIndex(0, true) check := func(bA *BitArray, bz []byte) { @@ -163,6 +173,8 @@ func TestBytes(t *testing.T) { } func TestEmptyFull(t *testing.T) { + t.Parallel() + ns := []int{47, 123} for _, n := range ns { bA := NewBitArray(n) @@ -179,6 +191,8 @@ func TestEmptyFull(t *testing.T) { } func TestUpdateNeverPanics(t *testing.T) { + t.Parallel() + newRandBitArray := func(n int) *BitArray { ba, _ := randBitArray(n) return ba @@ -201,6 +215,8 @@ func TestUpdateNeverPanics(t *testing.T) { } func TestNewBitArrayNeverCrashesOnNegatives(t *testing.T) { + t.Parallel() + bitList := []int{-127, -128, -1 << 31} for _, bits := range bitList { _ = NewBitArray(bits) @@ -208,6 +224,8 @@ func TestNewBitArrayNeverCrashesOnNegatives(t *testing.T) { } func TestJSONMarshalUnmarshal(t *testing.T) { + t.Parallel() + bA1 := NewBitArray(0) bA2 := NewBitArray(1) @@ -233,6 +251,8 @@ func TestJSONMarshalUnmarshal(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.bA.String(), func(t *testing.T) { + t.Parallel() + bz, err := json.Marshal(tc.bA) require.NoError(t, err) diff --git a/tm2/pkg/clist/clist_test.go b/tm2/pkg/clist/clist_test.go index 496966d2d61..2cef3ef847f 100644 --- a/tm2/pkg/clist/clist_test.go +++ b/tm2/pkg/clist/clist_test.go @@ -2,8 +2,6 @@ package clist import ( "fmt" - "runtime" - "sync/atomic" "testing" "time" @@ -12,6 +10,8 @@ import ( ) func TestPanicOnMaxLength(t *testing.T) { + t.Parallel() + maxLength := 1000 l := newWithMax(maxLength) @@ -24,6 +24,8 @@ func TestPanicOnMaxLength(t *testing.T) { } func TestSmall(t *testing.T) { + t.Parallel() + l := New() el1 := l.PushBack(1) el2 := l.PushBack(2) @@ -64,110 +66,9 @@ func TestSmall(t *testing.T) { } } -// This test is quite hacky because it relies on SetFinalizer -// which isn't guaranteed to run at all. -// -//nolint:unused,deadcode -func _TestGCFifo(t *testing.T) { - t.Helper() - - if runtime.GOARCH != "amd64" { - t.Skipf("Skipping on non-amd64 machine") - } - - const numElements = 1000000 - l := New() - gcCount := new(uint64) - - // SetFinalizer doesn't work well with circular structures, - // so we construct a trivial non-circular structure to - // track. - type value struct { - Int int - } - done := make(chan struct{}) - - for i := 0; i < numElements; i++ { - v := new(value) - v.Int = i - l.PushBack(v) - runtime.SetFinalizer(v, func(v *value) { - atomic.AddUint64(gcCount, 1) - }) - } - - for el := l.Front(); el != nil; { - l.Remove(el) - // oldEl := el - el = el.Next() - // oldEl.DetachPrev() - // oldEl.DetachNext() - } - - runtime.GC() - time.Sleep(time.Second * 3) - runtime.GC() - time.Sleep(time.Second * 3) - _ = done - - if *gcCount != numElements { - t.Errorf("Expected gcCount to be %v, got %v", numElements, - *gcCount) - } -} - -// This test is quite hacky because it relies on SetFinalizer -// which isn't guaranteed to run at all. -// -//nolint:unused,deadcode -func _TestGCRandom(t *testing.T) { - t.Helper() - - if runtime.GOARCH != "amd64" { - t.Skipf("Skipping on non-amd64 machine") - } - - const numElements = 1000000 - l := New() - gcCount := 0 - - // SetFinalizer doesn't work well with circular structures, - // so we construct a trivial non-circular structure to - // track. - type value struct { - Int int - } - - for i := 0; i < numElements; i++ { - v := new(value) - v.Int = i - l.PushBack(v) - runtime.SetFinalizer(v, func(v *value) { - gcCount++ - }) - } - - els := make([]*CElement, 0, numElements) - for el := l.Front(); el != nil; el = el.Next() { - els = append(els, el) - } - - for _, i := range random.RandPerm(numElements) { - el := els[i] - l.Remove(el) - _ = el.Next() - } - - runtime.GC() - time.Sleep(time.Second * 3) - - if gcCount != numElements { - t.Errorf("Expected gcCount to be %v, got %v", numElements, - gcCount) - } -} - func TestScanRightDeleteRandom(t *testing.T) { + t.Parallel() + const numElements = 1000 const numTimes = 100 const numScanners = 10 @@ -239,6 +140,8 @@ func TestScanRightDeleteRandom(t *testing.T) { } func TestWaitChan(t *testing.T) { + t.Parallel() + l := New() ch := l.WaitChan() diff --git a/tm2/pkg/cmap/cmap_test.go b/tm2/pkg/cmap/cmap_test.go index e0fc5a5dc4e..d9051ea18d6 100644 --- a/tm2/pkg/cmap/cmap_test.go +++ b/tm2/pkg/cmap/cmap_test.go @@ -9,6 +9,8 @@ import ( ) func TestIterateKeysWithValues(t *testing.T) { + t.Parallel() + cmap := NewCMap() for i := 1; i <= 10; i++ { @@ -39,6 +41,8 @@ func TestIterateKeysWithValues(t *testing.T) { } func TestContains(t *testing.T) { + t.Parallel() + cmap := NewCMap() cmap.Set("key1", "value1") diff --git a/tm2/pkg/commands/commands_test.go b/tm2/pkg/commands/commands_test.go index 96802b9d720..4879e667cf5 100644 --- a/tm2/pkg/commands/commands_test.go +++ b/tm2/pkg/commands/commands_test.go @@ -26,6 +26,8 @@ func (c *mockConfig) RegisterFlags(fs *flag.FlagSet) { } func TestCommandParseAndRun(t *testing.T) { + t.Parallel() + type flags struct { b bool s string @@ -215,7 +217,10 @@ func TestCommandParseAndRun(t *testing.T) { }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var ( invokedCmd string args []string @@ -416,9 +421,7 @@ func TestCommand_AddSubCommands(t *testing.T) { // Forked from peterbourgon/ff/ffcli func TestHelpUsage(t *testing.T) { - fs, _ := fftest.Pair() - var buf bytes.Buffer - fs.SetOutput(&buf) + t.Parallel() tests := []struct { name string @@ -432,7 +435,6 @@ func TestHelpUsage(t *testing.T) { shortUsage: "TestHelpUsage [flags] ", shortHelp: "Some short help", longHelp: "Some long help.", - flagSet: fs, }, expectedOutput: strings.TrimSpace(` USAGE @@ -455,7 +457,6 @@ FLAGS name: "TestHelpUsage", shortUsage: "TestHelpUsage [flags] ", shortHelp: "Some short help", - flagSet: fs, }, expectedOutput: strings.TrimSpace(` USAGE @@ -477,7 +478,6 @@ FLAGS command: &Command{ name: "TestHelpUsage", shortUsage: "TestHelpUsage [flags] ", - flagSet: fs, }, expectedOutput: strings.TrimSpace(` USAGE @@ -494,8 +494,15 @@ FLAGS }, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { - buf.Reset() + t.Parallel() + + fs, _ := fftest.Pair() + var buf bytes.Buffer + fs.SetOutput(&buf) + + tt.command.flagSet = fs err := tt.command.ParseAndRun(context.Background(), []string{"-h"}) @@ -507,6 +514,8 @@ FLAGS // Forked from peterbourgon/ff/ffcli func TestNestedOutput(t *testing.T) { + t.Parallel() + var ( rootHelpOutput = "USAGE\n \n\nSUBCOMMANDS\n foo\n\n" fooHelpOutput = "USAGE\n foo\n\nSUBCOMMANDS\n bar\n\n" @@ -572,6 +581,8 @@ func TestNestedOutput(t *testing.T) { }, } { t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + var ( rootfs = flag.NewFlagSet("root", flag.ContinueOnError) foofs = flag.NewFlagSet("foo", flag.ContinueOnError) diff --git a/tm2/pkg/crypto/bcrypt/bcrypt_test.go b/tm2/pkg/crypto/bcrypt/bcrypt_test.go index 96634260d65..92b296bfe52 100644 --- a/tm2/pkg/crypto/bcrypt/bcrypt_test.go +++ b/tm2/pkg/crypto/bcrypt/bcrypt_test.go @@ -12,6 +12,8 @@ import ( ) func TestBcryptingIsEasy(t *testing.T) { + t.Parallel() + pass := []byte("mypassword") salt := []byte("1234567890123456") hp, err := GenerateFromPassword(salt, pass, 0) @@ -31,6 +33,8 @@ func TestBcryptingIsEasy(t *testing.T) { } func TestBcryptingIsCorrect(t *testing.T) { + t.Parallel() + pass := []byte("allmine") salt := []byte("XajjQvNhvvRt5GSeFk1xFe") expectedHash := []byte("$2a$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga") @@ -56,6 +60,8 @@ func TestBcryptingIsCorrect(t *testing.T) { } func TestVeryShortPasswords(t *testing.T) { + t.Parallel() + key := []byte("k") salt := []byte("XajjQvNhvvRt5GSeFk1xFe") _, err := bcrypt(key, 10, salt) @@ -65,6 +71,8 @@ func TestVeryShortPasswords(t *testing.T) { } func TestTooLongPasswordsWork(t *testing.T) { + t.Parallel() + salt := []byte("XajjQvNhvvRt5GSeFk1xFe") // One byte over the usual 56 byte limit that blowfish has tooLongPass := []byte("012345678901234567890123456789012345678901234567890123456") @@ -92,6 +100,8 @@ var invalidTests = []InvalidHashTest{ } func TestInvalidHashErrors(t *testing.T) { + t.Parallel() + check := func(name string, expected, err error) { if err == nil { t.Errorf("%s: Should have returned an error", name) @@ -109,6 +119,8 @@ func TestInvalidHashErrors(t *testing.T) { } func TestUnpaddedBase64Encoding(t *testing.T) { + t.Parallel() + original := []byte{101, 201, 101, 75, 19, 227, 199, 20, 239, 236, 133, 32, 30, 109, 243, 30} encodedOriginal := []byte("XajjQvNhvvRt5GSeFk1xFe") @@ -129,6 +141,8 @@ func TestUnpaddedBase64Encoding(t *testing.T) { } func TestCost(t *testing.T) { + t.Parallel() + suffix := "XajjQvNhvvRt5GSeFk1xFe5l47dONXg781AmZtd869sO8zfsHuw7C" for _, vers := range []string{"2a", "2"} { for _, cost := range []int{4, 10} { @@ -151,6 +165,8 @@ func TestCost(t *testing.T) { } func TestCostValidationInHash(t *testing.T) { + t.Parallel() + if testing.Short() { return } @@ -185,6 +201,8 @@ func TestCostValidationInHash(t *testing.T) { } func TestCostReturnsWithLeadingZeroes(t *testing.T) { + t.Parallel() + salt := []byte("1234567890123456") hp, _ := newFromPassword(salt, []byte("abcdefgh"), 7) cost := hp.Hash()[4:7] @@ -196,6 +214,8 @@ func TestCostReturnsWithLeadingZeroes(t *testing.T) { } func TestMinorNotRequired(t *testing.T) { + t.Parallel() + noMinorHash := []byte("$2$10$XajjQvNhvvRt5GSeFk1xFeyqRrsxkhBkUiQeg0dt.wU1qD4aFDcga") h, err := newFromHash(noMinorHash) if err != nil { @@ -233,6 +253,8 @@ func BenchmarkDefaultCost(b *testing.B) { // See Issue https://github.com/golang/go/issues/20425. func TestNoSideEffectsFromCompare(t *testing.T) { + t.Parallel() + source := []byte("passw0rd123456") password := source[:len(source)-6] token := source[len(source)-6:] diff --git a/tm2/pkg/crypto/bech32_test.go b/tm2/pkg/crypto/bech32_test.go index 13bdaa8e816..f5bc3e9ed7c 100644 --- a/tm2/pkg/crypto/bech32_test.go +++ b/tm2/pkg/crypto/bech32_test.go @@ -21,6 +21,8 @@ var invalidStrs = []string{ } func TestEmptyAddresses(t *testing.T) { + t.Parallel() + require.Equal(t, (crypto.Address{}).String(), "g1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqluuxe") addr := crypto.AddressFromBytes(make([]byte, 20)) @@ -47,6 +49,8 @@ func testMarshal(t *testing.T, addr crypto.Address, marshal func(orig interface{ } func TestRandBech32AddrConsistency(t *testing.T) { + t.Parallel() + var pub ed25519.PubKeyEd25519 for i := 0; i < 1000; i++ { diff --git a/tm2/pkg/crypto/bip39/bip39_test.go b/tm2/pkg/crypto/bip39/bip39_test.go index cbe15be6c3f..7854db76f53 100644 --- a/tm2/pkg/crypto/bip39/bip39_test.go +++ b/tm2/pkg/crypto/bip39/bip39_test.go @@ -14,6 +14,8 @@ type Vector struct { } func TestBip39(t *testing.T) { + t.Parallel() + for _, vector := range testVectors() { entropy, err := hex.DecodeString(vector.entropy) assert.NoError(t, err) @@ -31,6 +33,8 @@ func TestBip39(t *testing.T) { } func TestIsMnemonicValid(t *testing.T) { + t.Parallel() + for _, vector := range badMnemonicSentences() { assert.Equal(t, IsMnemonicValid(vector.mnemonic), false) } @@ -41,6 +45,8 @@ func TestIsMnemonicValid(t *testing.T) { } func TestInvalidMnemonicFails(t *testing.T) { + t.Parallel() + for _, vector := range badMnemonicSentences() { _, err := MnemonicToByteArray(vector.mnemonic) assert.NotNil(t, err) @@ -48,6 +54,8 @@ func TestInvalidMnemonicFails(t *testing.T) { } func TestValidateEntropyWithChecksumBitSize(t *testing.T) { + t.Parallel() + // Good tests. for i := 1; i <= (12*32 + 12); i++ { err := validateEntropyWithChecksumBitSize(i) @@ -74,6 +82,8 @@ func TestValidateEntropyWithChecksumBitSize(t *testing.T) { } func TestNewEntropy(t *testing.T) { + t.Parallel() + // Good tests. for i := 128; i <= 256; i += 32 { _, err := NewEntropy(i) diff --git a/tm2/pkg/crypto/ed25519/ed25519_test.go b/tm2/pkg/crypto/ed25519/ed25519_test.go index c07b7ae2298..fdf269018c0 100644 --- a/tm2/pkg/crypto/ed25519/ed25519_test.go +++ b/tm2/pkg/crypto/ed25519/ed25519_test.go @@ -10,6 +10,8 @@ import ( ) func TestSignAndValidateEd25519(t *testing.T) { + t.Parallel() + privKey := ed25519.GenPrivKey() pubKey := privKey.PubKey() diff --git a/tm2/pkg/crypto/hd/fundraiser_test.go b/tm2/pkg/crypto/hd/fundraiser_test.go index dabfd670c37..884425c6c39 100644 --- a/tm2/pkg/crypto/hd/fundraiser_test.go +++ b/tm2/pkg/crypto/hd/fundraiser_test.go @@ -43,6 +43,8 @@ func initFundraiserTestVectors(t *testing.T) []addrData { } func TestFundraiserCompatibility(t *testing.T) { + t.Parallel() + hdToAddrTable := initFundraiserTestVectors(t) for i, d := range hdToAddrTable { diff --git a/tm2/pkg/crypto/hd/hdpath_test.go b/tm2/pkg/crypto/hd/hdpath_test.go index 1e66a9fba2c..31e806b2b1a 100644 --- a/tm2/pkg/crypto/hd/hdpath_test.go +++ b/tm2/pkg/crypto/hd/hdpath_test.go @@ -34,6 +34,8 @@ const ( ) func TestStringifyFundraiserPathParams(t *testing.T) { + t.Parallel() + path := NewFundraiserParams(4, testCoinType, 22) require.Equal(t, "44'/118'/4'/0/22", path.String()) @@ -45,6 +47,8 @@ func TestStringifyFundraiserPathParams(t *testing.T) { } func TestPathToArray(t *testing.T) { + t.Parallel() + path := NewParams(44, 118, 1, false, 4) require.Equal(t, "[44 118 1 0 4]", fmt.Sprintf("%v", path.DerivationPath())) @@ -53,6 +57,8 @@ func TestPathToArray(t *testing.T) { } func TestParamsFromPath(t *testing.T) { + t.Parallel() + goodCases := []struct { params *BIP44Params path string diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index 987a271881b..cae5f2af947 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -12,6 +12,8 @@ import ( ) func TestCreateAccountInvalidMnemonic(t *testing.T) { + t.Parallel() + kb := NewInMemory() _, err := kb.CreateAccount( "some_account", @@ -22,6 +24,8 @@ func TestCreateAccountInvalidMnemonic(t *testing.T) { } func TestCreateLedgerUnsupportedAlgo(t *testing.T) { + t.Parallel() + kb := NewInMemory() _, err := kb.CreateLedger("some_account", Ed25519, "cosmos", 0, 1) assert.Error(t, err) @@ -29,6 +33,8 @@ func TestCreateLedgerUnsupportedAlgo(t *testing.T) { } func TestCreateLedger(t *testing.T) { + t.Parallel() + kb := NewInMemory() // test_cover and test_unit will result in different answers @@ -65,6 +71,8 @@ func TestCreateLedger(t *testing.T) { // TestKeyManagement makes sure we can manipulate these keys well func TestKeyManagement(t *testing.T) { + t.Parallel() + // make the storage with reasonable defaults cstore := NewInMemory() @@ -147,6 +155,8 @@ func TestKeyManagement(t *testing.T) { // TestSignVerify does some detailed checks on how we sign and validate // signatures func TestSignVerify(t *testing.T) { + t.Parallel() + cstore := NewInMemory() n1, n2, n3 := "some dude", "a dudette", "dude-ish" @@ -233,6 +243,8 @@ func assertPassword(t *testing.T, cstore Keybase, name, pass, badpass string) { // TestExportImport tests exporting and importing func TestExportImport(t *testing.T) { + t.Parallel() + // make the storage with reasonable defaults cstore := NewInMemory() @@ -263,6 +275,8 @@ func TestExportImport(t *testing.T) { } func TestExportImportPubKey(t *testing.T) { + t.Parallel() + // make the storage with reasonable defaults cstore := NewInMemory() @@ -304,6 +318,8 @@ func TestExportImportPubKey(t *testing.T) { // TestAdvancedKeyManagement verifies update, import, export functionality func TestAdvancedKeyManagement(t *testing.T) { + t.Parallel() + // make the storage with reasonable defaults cstore := NewInMemory() @@ -352,6 +368,8 @@ func TestAdvancedKeyManagement(t *testing.T) { // TestSeedPhrase verifies restoring from a seed phrase func TestSeedPhrase(t *testing.T) { + t.Parallel() + // make the storage with reasonable defaults cstore := NewInMemory() diff --git a/tm2/pkg/crypto/keys/types_test.go b/tm2/pkg/crypto/keys/types_test.go index a0591819a88..2a2dd2c82f6 100644 --- a/tm2/pkg/crypto/keys/types_test.go +++ b/tm2/pkg/crypto/keys/types_test.go @@ -11,6 +11,8 @@ import ( ) func Test_writeReadLedgerInfo(t *testing.T) { + t.Parallel() + var tmpKey secp256k1.PubKeySecp256k1 bz, _ := hex.DecodeString("035AD6810A47F073553FF30D2FCC7E0D3B1C0B74B61A1AAA2582344037151E143A") copy(tmpKey[:], bz) diff --git a/tm2/pkg/crypto/merkle/proof_key_path_test.go b/tm2/pkg/crypto/merkle/proof_key_path_test.go index 22e3e21ca3d..669e8c5630c 100644 --- a/tm2/pkg/crypto/merkle/proof_key_path_test.go +++ b/tm2/pkg/crypto/merkle/proof_key_path_test.go @@ -10,6 +10,8 @@ import ( ) func TestKeyPath(t *testing.T) { + t.Parallel() + var path KeyPath keys := make([][]byte, 10) alphanum := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/tm2/pkg/crypto/merkle/proof_test.go b/tm2/pkg/crypto/merkle/proof_test.go index 3319963ce6c..0424ea351dd 100644 --- a/tm2/pkg/crypto/merkle/proof_test.go +++ b/tm2/pkg/crypto/merkle/proof_test.go @@ -53,6 +53,8 @@ func (dop DominoOp) GetKey() []byte { // ---------------------------------------- func TestProofOperators(t *testing.T) { + t.Parallel() + var err error // ProofRuntime setup diff --git a/tm2/pkg/crypto/merkle/rfc6962_test.go b/tm2/pkg/crypto/merkle/rfc6962_test.go index 917b09015b0..57f72cea311 100644 --- a/tm2/pkg/crypto/merkle/rfc6962_test.go +++ b/tm2/pkg/crypto/merkle/rfc6962_test.go @@ -24,6 +24,8 @@ import ( ) func TestRFC6962Hasher(t *testing.T) { + t.Parallel() + _, leafHashTrail := trailsFromByteSlices([][]byte{[]byte("L123456")}) leafHash := leafHashTrail.Hash _, leafHashTrail = trailsFromByteSlices([][]byte{{}}) @@ -58,6 +60,8 @@ func TestRFC6962Hasher(t *testing.T) { } { tc := tc t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + wantBytes, err := hex.DecodeString(tc.want) if err != nil { t.Fatalf("hex.DecodeString(%x): %v", tc.want, err) @@ -70,6 +74,8 @@ func TestRFC6962Hasher(t *testing.T) { } func TestRFC6962HasherCollisions(t *testing.T) { + t.Parallel() + // Check that different leaves have different hashes. leaf1, leaf2 := []byte("Hello"), []byte("World") _, leafHashTrail := trailsFromByteSlices([][]byte{leaf1}) diff --git a/tm2/pkg/crypto/merkle/simple_map_test.go b/tm2/pkg/crypto/merkle/simple_map_test.go index 366d9f39099..6df8e0b967e 100644 --- a/tm2/pkg/crypto/merkle/simple_map_test.go +++ b/tm2/pkg/crypto/merkle/simple_map_test.go @@ -8,6 +8,8 @@ import ( ) func TestSimpleMap(t *testing.T) { + t.Parallel() + tests := []struct { keys []string values []string // each string gets converted to []byte in test diff --git a/tm2/pkg/crypto/merkle/simple_proof_test.go b/tm2/pkg/crypto/merkle/simple_proof_test.go index 1a517905b5e..88677e3fb0e 100644 --- a/tm2/pkg/crypto/merkle/simple_proof_test.go +++ b/tm2/pkg/crypto/merkle/simple_proof_test.go @@ -7,6 +7,8 @@ import ( ) func TestSimpleProofValidateBasic(t *testing.T) { + t.Parallel() + testCases := []struct { testName string malleateProof func(*SimpleProof) @@ -23,6 +25,8 @@ func TestSimpleProofValidateBasic(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.testName, func(t *testing.T) { + t.Parallel() + _, proofs := SimpleProofsFromByteSlices([][]byte{ []byte("apple"), []byte("watermelon"), diff --git a/tm2/pkg/crypto/merkle/simple_tree_test.go b/tm2/pkg/crypto/merkle/simple_tree_test.go index 8690bae6ac3..209df75b1b5 100644 --- a/tm2/pkg/crypto/merkle/simple_tree_test.go +++ b/tm2/pkg/crypto/merkle/simple_tree_test.go @@ -17,6 +17,8 @@ func (tI testItem) Hash() []byte { } func TestSimpleProof(t *testing.T) { + t.Parallel() + total := 100 items := make([][]byte, total) @@ -69,6 +71,8 @@ func TestSimpleProof(t *testing.T) { } func TestSimpleHashAlternatives(t *testing.T) { + t.Parallel() + total := 100 items := make([][]byte, total) @@ -104,6 +108,8 @@ func BenchmarkSimpleHashAlternatives(b *testing.B) { } func Test_getSplitPoint(t *testing.T) { + t.Parallel() + tests := []struct { length int want int diff --git a/tm2/pkg/crypto/mock/mock_test.go b/tm2/pkg/crypto/mock/mock_test.go index 2e5f1d776bd..3d7ac571faa 100644 --- a/tm2/pkg/crypto/mock/mock_test.go +++ b/tm2/pkg/crypto/mock/mock_test.go @@ -10,6 +10,8 @@ import ( ) func TestSignAndValidateMock(t *testing.T) { + t.Parallel() + privKey := mock.PrivKeyMock([]byte{0x01}) pubKey := privKey.PubKey() diff --git a/tm2/pkg/crypto/multisig/bitarray/compact_bit_array_test.go b/tm2/pkg/crypto/multisig/bitarray/compact_bit_array_test.go index ecbfb64005d..c41e8e3c87b 100644 --- a/tm2/pkg/crypto/multisig/bitarray/compact_bit_array_test.go +++ b/tm2/pkg/crypto/multisig/bitarray/compact_bit_array_test.go @@ -28,6 +28,8 @@ func randCompactBitArray(bits int) (*CompactBitArray, []byte) { } func TestNewBitArrayNeverCrashesOnNegatives(t *testing.T) { + t.Parallel() + bitList := []int{-127, -128, -1 << 31} for _, bits := range bitList { bA := NewCompactBitArray(bits) @@ -36,6 +38,8 @@ func TestNewBitArrayNeverCrashesOnNegatives(t *testing.T) { } func TestJSONMarshalUnmarshal(t *testing.T) { + t.Parallel() + bA1 := NewCompactBitArray(0) bA2 := NewCompactBitArray(1) @@ -73,6 +77,8 @@ func TestJSONMarshalUnmarshal(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.bA.String(), func(t *testing.T) { + t.Parallel() + bz, err := json.Marshal(tc.bA) require.NoError(t, err) @@ -96,6 +102,8 @@ func TestJSONMarshalUnmarshal(t *testing.T) { } func TestCompactMarshalUnmarshal(t *testing.T) { + t.Parallel() + bA1 := NewCompactBitArray(0) bA2 := NewCompactBitArray(1) @@ -133,6 +141,8 @@ func TestCompactMarshalUnmarshal(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.bA.String(), func(t *testing.T) { + t.Parallel() + bz := tc.bA.CompactMarshal() assert.Equal(t, tc.marshalledBA, bz) @@ -153,6 +163,8 @@ func TestCompactMarshalUnmarshal(t *testing.T) { } func TestCompactBitArrayNumOfTrueBitsBefore(t *testing.T) { + t.Parallel() + testCases := []struct { marshalledBA string bAIndex []int @@ -169,6 +181,8 @@ func TestCompactBitArrayNumOfTrueBitsBefore(t *testing.T) { tc := tc tcIndex := tcIndex t.Run(tc.marshalledBA, func(t *testing.T) { + t.Parallel() + var bA *CompactBitArray err := json.Unmarshal([]byte(tc.marshalledBA), &bA) require.NoError(t, err) @@ -181,6 +195,8 @@ func TestCompactBitArrayNumOfTrueBitsBefore(t *testing.T) { } func TestCompactBitArrayGetSetIndex(t *testing.T) { + t.Parallel() + r := rand.New(rand.NewSource(100)) numTests := 10 numBitsPerArr := 100 diff --git a/tm2/pkg/crypto/multisig/threshold_pubkey_test.go b/tm2/pkg/crypto/multisig/threshold_pubkey_test.go index 9195344f423..548e8df0a82 100644 --- a/tm2/pkg/crypto/multisig/threshold_pubkey_test.go +++ b/tm2/pkg/crypto/multisig/threshold_pubkey_test.go @@ -15,6 +15,8 @@ import ( // This tests multisig functionality, but it expects the first k signatures to be valid // TODO: Adapt it to give more flexibility about first k signatures being valid func TestThresholdMultisigValidCases(t *testing.T) { + t.Parallel() + pkSet1, sigSet1 := generatePubKeysAndSignatures(5, []byte{1, 2, 3, 4}) cases := []struct { msg []byte @@ -105,6 +107,8 @@ func TestThresholdMultisigValidCases(t *testing.T) { // TODO: Fully replace this test with table driven tests func TestThresholdMultisigDuplicateSignatures(t *testing.T) { + t.Parallel() + msg := []byte{1, 2, 3, 4, 5} pubkeys, sigs := generatePubKeysAndSignatures(5, msg) multisigKey := NewPubKeyMultisigThreshold(2, pubkeys) @@ -118,6 +122,8 @@ func TestThresholdMultisigDuplicateSignatures(t *testing.T) { // TODO: Fully replace this test with table driven tests func TestMultiSigPubKeyEquality(t *testing.T) { + t.Parallel() + msg := []byte{1, 2, 3, 4} pubkeys, _ := generatePubKeysAndSignatures(5, msg) multisigKey := NewPubKeyMultisigThreshold(2, pubkeys) @@ -135,6 +141,8 @@ func TestMultiSigPubKeyEquality(t *testing.T) { } func TestAddress(t *testing.T) { + t.Parallel() + msg := []byte{1, 2, 3, 4} pubkeys, _ := generatePubKeysAndSignatures(5, msg) multisigKey := NewPubKeyMultisigThreshold(2, pubkeys) @@ -142,6 +150,8 @@ func TestAddress(t *testing.T) { } func TestPubKeyMultisigThresholdAminoToIface(t *testing.T) { + t.Parallel() + msg := []byte{1, 2, 3, 4} pubkeys, _ := generatePubKeysAndSignatures(5, msg) multisigKey := NewPubKeyMultisigThreshold(2, pubkeys) diff --git a/tm2/pkg/crypto/random_test.go b/tm2/pkg/crypto/random_test.go index aab505cb61c..dd405c06859 100644 --- a/tm2/pkg/crypto/random_test.go +++ b/tm2/pkg/crypto/random_test.go @@ -11,6 +11,8 @@ import ( // the purpose of this test is primarily to ensure that the randomness // generation won't error. func TestRandomConsistency(t *testing.T) { + t.Parallel() + x1 := crypto.CRandBytes(256) x2 := crypto.CRandBytes(256) x3 := crypto.CRandBytes(256) diff --git a/tm2/pkg/crypto/secp256k1/secp256k1_cgo_test.go b/tm2/pkg/crypto/secp256k1/secp256k1_cgo_test.go index e5b584707e5..d0070ad9bbe 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1_cgo_test.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1_cgo_test.go @@ -10,6 +10,8 @@ import ( ) func TestPrivKeySecp256k1SignVerify(t *testing.T) { + t.Parallel() + msg := []byte("A.1.2 ECC Key Pair Generation by Testing Candidates") priv := GenPrivKey() tests := []struct { @@ -23,6 +25,8 @@ func TestPrivKeySecp256k1SignVerify(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := tt.privKey.Sign(msg) if tt.wantSignErr { require.Error(t, err) diff --git a/tm2/pkg/crypto/secp256k1/secp256k1_internal_test.go b/tm2/pkg/crypto/secp256k1/secp256k1_internal_test.go index d680d24f6a9..d80bea83dba 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1_internal_test.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1_internal_test.go @@ -11,6 +11,8 @@ import ( ) func Test_genPrivKey(t *testing.T) { + t.Parallel() + empty := make([]byte, 32) oneB := big.NewInt(1).Bytes() onePadded := make([]byte, 32) @@ -30,6 +32,8 @@ func Test_genPrivKey(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.shouldPanic { require.Panics(t, func() { genPrivKey(bytes.NewReader(tt.notSoRand)) diff --git a/tm2/pkg/crypto/secp256k1/secp256k1_nocgo_test.go b/tm2/pkg/crypto/secp256k1/secp256k1_nocgo_test.go index e75d5377670..aeaca9df47f 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1_nocgo_test.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1_nocgo_test.go @@ -13,6 +13,8 @@ import ( // non-canonical signatures fail. // Note: run with CGO_ENABLED=0 or go test -tags !cgo. func TestSignatureVerificationAndRejectUpperS(t *testing.T) { + t.Parallel() + msg := []byte("We have lingered long enough on the shores of the cosmic ocean.") for i := 0; i < 500; i++ { priv := GenPrivKey() diff --git a/tm2/pkg/crypto/secp256k1/secp256k1_test.go b/tm2/pkg/crypto/secp256k1/secp256k1_test.go index 86aa058f95a..e04f622fdbb 100644 --- a/tm2/pkg/crypto/secp256k1/secp256k1_test.go +++ b/tm2/pkg/crypto/secp256k1/secp256k1_test.go @@ -30,6 +30,8 @@ var secpDataTable = []keyData{ } func TestPubKeySecp256k1Address(t *testing.T) { + t.Parallel() + for _, d := range secpDataTable { privB, _ := hex.DecodeString(d.priv) pubB, _ := hex.DecodeString(d.pub) @@ -50,6 +52,8 @@ func TestPubKeySecp256k1Address(t *testing.T) { } func TestSignAndValidateSecp256k1(t *testing.T) { + t.Parallel() + privKey := secp256k1.GenPrivKey() pubKey := privKey.PubKey() @@ -68,6 +72,8 @@ func TestSignAndValidateSecp256k1(t *testing.T) { // This test is intended to justify the removal of calls to the underlying library // in creating the privkey. func TestSecp256k1LoadPrivkeyAndSerializeIsIdentity(t *testing.T) { + t.Parallel() + numberOfTests := 256 for i := 0; i < numberOfTests; i++ { // Seed the test case with some random bytes @@ -87,6 +93,8 @@ func TestSecp256k1LoadPrivkeyAndSerializeIsIdentity(t *testing.T) { } func TestGenPrivKeySecp256k1(t *testing.T) { + t.Parallel() + // curve oder N N := underlyingSecp256k1.S256().N tests := []struct { @@ -102,6 +110,8 @@ func TestGenPrivKeySecp256k1(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotPrivKey := secp256k1.GenPrivKeySecp256k1(tt.secret) require.NotNil(t, gotPrivKey) // interpret as a big.Int and make sure it is a valid field element: diff --git a/tm2/pkg/crypto/tmhash/hash_test.go b/tm2/pkg/crypto/tmhash/hash_test.go index 4a129bb86ac..c69d86653b1 100644 --- a/tm2/pkg/crypto/tmhash/hash_test.go +++ b/tm2/pkg/crypto/tmhash/hash_test.go @@ -9,6 +9,8 @@ import ( ) func TestHash(t *testing.T) { + t.Parallel() + testVector := []byte("abc") hasher := tmhash.New() hasher.Write(testVector) @@ -25,6 +27,8 @@ func TestHash(t *testing.T) { } func TestHashTruncated(t *testing.T) { + t.Parallel() + testVector := []byte("abc") hasher := tmhash.NewTruncated() hasher.Write(testVector) diff --git a/tm2/pkg/crypto/xchacha20poly1305/vector_test.go b/tm2/pkg/crypto/xchacha20poly1305/vector_test.go index 3001217f41e..7ed186de19d 100644 --- a/tm2/pkg/crypto/xchacha20poly1305/vector_test.go +++ b/tm2/pkg/crypto/xchacha20poly1305/vector_test.go @@ -19,6 +19,8 @@ func fromHex(bits string) []byte { } func TestHChaCha20(t *testing.T) { + t.Parallel() + for i, v := range hChaCha20Vectors { var key [32]byte var nonce [16]byte @@ -63,6 +65,8 @@ var hChaCha20Vectors = []struct { } func TestVectors(t *testing.T) { + t.Parallel() + for i, v := range vectors { if len(v.plaintext) == 0 { v.plaintext = make([]byte, len(v.ciphertext)) diff --git a/tm2/pkg/crypto/xchacha20poly1305/xchachapoly_test.go b/tm2/pkg/crypto/xchacha20poly1305/xchachapoly_test.go index 5b92a7607e7..ea7b0bee1a4 100644 --- a/tm2/pkg/crypto/xchacha20poly1305/xchachapoly_test.go +++ b/tm2/pkg/crypto/xchacha20poly1305/xchachapoly_test.go @@ -14,6 +14,8 @@ import ( // Use of this source code is governed by a BSD-style // license that can be found at the bottom of this file. func TestRandom(t *testing.T) { + t.Parallel() + // Some random tests to verify Open(Seal) == Plaintext for i := 0; i < 256; i++ { var nonce [24]byte diff --git a/tm2/pkg/crypto/xsalsa20symmetric/symmetric_test.go b/tm2/pkg/crypto/xsalsa20symmetric/symmetric_test.go index dea965a4356..7460add5185 100644 --- a/tm2/pkg/crypto/xsalsa20symmetric/symmetric_test.go +++ b/tm2/pkg/crypto/xsalsa20symmetric/symmetric_test.go @@ -11,6 +11,8 @@ import ( ) func TestSimple(t *testing.T) { + t.Parallel() + plaintext := []byte("sometext") secret := []byte("somesecretoflengththirtytwo===32") ciphertext := EncryptSymmetric(plaintext, secret) @@ -21,6 +23,8 @@ func TestSimple(t *testing.T) { } func TestSimpleWithKDF(t *testing.T) { + t.Parallel() + salt := []byte("1234567890123456") plaintext := []byte("sometext") secretPass := []byte("somesecret") diff --git a/tm2/pkg/db/backend_test.go b/tm2/pkg/db/backend_test.go index b6cd3f026aa..fd03629fd58 100644 --- a/tm2/pkg/db/backend_test.go +++ b/tm2/pkg/db/backend_test.go @@ -41,8 +41,12 @@ func testBackendGetSetDelete(t *testing.T, backend BackendType) { } func TestBackendsGetSetDelete(t *testing.T) { + t.Parallel() + for dbType := range backends { t.Run(string(dbType), func(t *testing.T) { + t.Parallel() + testBackendGetSetDelete(t, dbType) }) } @@ -59,6 +63,8 @@ func withDB(t *testing.T, creator dbCreator, fn func(DB)) { } func TestBackendsNilKeys(t *testing.T) { + t.Parallel() + // Test all backends. for dbType, creator := range backends { withDB(t, creator, func(db DB) { @@ -137,6 +143,8 @@ func TestBackendsNilKeys(t *testing.T) { } func TestGoLevelDBBackend(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db, err := NewDB(name, GoLevelDBBackend, t.TempDir()) require.NoError(t, err) @@ -146,9 +154,11 @@ func TestGoLevelDBBackend(t *testing.T) { } func TestDBIterator(t *testing.T) { + t.Parallel() + for dbType := range backends { t.Run(fmt.Sprintf("%v", dbType), func(t *testing.T) { - t.Helper() + t.Parallel() testDBIterator(t, dbType) }) diff --git a/tm2/pkg/db/boltdb_test.go b/tm2/pkg/db/boltdb_test.go index 031023f275a..57091fb53a2 100644 --- a/tm2/pkg/db/boltdb_test.go +++ b/tm2/pkg/db/boltdb_test.go @@ -10,6 +10,8 @@ import ( ) func TestBoltDBNewBoltDB(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db, err := NewBoltDB(name, t.TempDir()) diff --git a/tm2/pkg/db/c_level_db_test.go b/tm2/pkg/db/c_level_db_test.go index 2bdffd14a83..7044ea0f4de 100644 --- a/tm2/pkg/db/c_level_db_test.go +++ b/tm2/pkg/db/c_level_db_test.go @@ -88,6 +88,8 @@ func bytes2Int64(buf []byte) int64 { */ func TestCLevelDBBackend(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) // Can't use "" (current directory) or "./" here because levigo.Open returns: // "Error initializing DB: IO error: test_XXX.db: Invalid argument" @@ -99,6 +101,8 @@ func TestCLevelDBBackend(t *testing.T) { } func TestCLevelDBStats(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db, err := NewDB(name, CLevelDBBackend, t.TempDir()) require.NoError(t, err) diff --git a/tm2/pkg/db/db_test.go b/tm2/pkg/db/db_test.go index 3634fb4bf03..62231645613 100644 --- a/tm2/pkg/db/db_test.go +++ b/tm2/pkg/db/db_test.go @@ -8,8 +8,12 @@ import ( ) func TestDBIteratorSingleKey(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) db.SetSync(bz("1"), bz("value_1")) @@ -27,8 +31,12 @@ func TestDBIteratorSingleKey(t *testing.T) { } func TestDBIteratorTwoKeys(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) db.SetSync(bz("1"), bz("value_1")) @@ -54,8 +62,12 @@ func TestDBIteratorTwoKeys(t *testing.T) { } func TestDBIteratorMany(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) keys := make([][]byte, 100) @@ -78,8 +90,12 @@ func TestDBIteratorMany(t *testing.T) { } func TestDBIteratorEmpty(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) itr := db.Iterator(nil, nil) @@ -90,8 +106,12 @@ func TestDBIteratorEmpty(t *testing.T) { } func TestDBIteratorEmptyBeginAfter(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) itr := db.Iterator(bz("1"), nil) @@ -102,8 +122,12 @@ func TestDBIteratorEmptyBeginAfter(t *testing.T) { } func TestDBIteratorNonemptyBeginAfter(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) db.SetSync(bz("1"), bz("value_1")) @@ -115,6 +139,8 @@ func TestDBIteratorNonemptyBeginAfter(t *testing.T) { } func TestDBBatchWrite(t *testing.T) { + t.Parallel() + testCases := []struct { modify func(batch Batch) calls map[string]int diff --git a/tm2/pkg/db/go_level_db_test.go b/tm2/pkg/db/go_level_db_test.go index a85bb6e8713..677b8d86c08 100644 --- a/tm2/pkg/db/go_level_db_test.go +++ b/tm2/pkg/db/go_level_db_test.go @@ -9,6 +9,8 @@ import ( ) func TestGoLevelDBNewGoLevelDB(t *testing.T) { + t.Parallel() + dir := t.TempDir() name := fmt.Sprintf("test_%x", randStr(12)) diff --git a/tm2/pkg/db/gorocks_db_test.go b/tm2/pkg/db/gorocks_db_test.go index 9d1b10cef34..d4792424f39 100644 --- a/tm2/pkg/db/gorocks_db_test.go +++ b/tm2/pkg/db/gorocks_db_test.go @@ -10,6 +10,8 @@ import ( ) func TestGoRocksDBBackend(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db := NewDB(name, GoRocksDBBackend, t.TempDir()) @@ -18,6 +20,8 @@ func TestGoRocksDBBackend(t *testing.T) { } func TestGoRocksDBStats(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db := NewDB(name, GoRocksDBBackend, t.TempDir()) diff --git a/tm2/pkg/db/grocks_db_test.go b/tm2/pkg/db/grocks_db_test.go index 60592bc8d82..10b33026128 100644 --- a/tm2/pkg/db/grocks_db_test.go +++ b/tm2/pkg/db/grocks_db_test.go @@ -10,6 +10,8 @@ import ( ) func TestGRocksDBBackend(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db := NewDB(name, GRocksDBBackend, t.TempDir()) @@ -18,6 +20,8 @@ func TestGRocksDBBackend(t *testing.T) { } func TestGRocksDBStats(t *testing.T) { + t.Parallel() + name := fmt.Sprintf("test_%x", randStr(12)) db := NewDB(name, GRocksDBBackend, t.TempDir()) diff --git a/tm2/pkg/db/prefix_db_test.go b/tm2/pkg/db/prefix_db_test.go index e3e37c7d12c..0948cce98a5 100644 --- a/tm2/pkg/db/prefix_db_test.go +++ b/tm2/pkg/db/prefix_db_test.go @@ -18,6 +18,8 @@ func mockDBWithStuff() DB { } func TestPrefixDBSimple(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -36,6 +38,8 @@ func TestPrefixDBSimple(t *testing.T) { } func TestPrefixDBIterator1(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -54,6 +58,8 @@ func TestPrefixDBIterator1(t *testing.T) { } func TestPrefixDBIterator2(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -64,6 +70,8 @@ func TestPrefixDBIterator2(t *testing.T) { } func TestPrefixDBIterator3(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -82,6 +90,8 @@ func TestPrefixDBIterator3(t *testing.T) { } func TestPrefixDBIterator4(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -92,6 +102,8 @@ func TestPrefixDBIterator4(t *testing.T) { } func TestPrefixDBReverseIterator1(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -110,6 +122,8 @@ func TestPrefixDBReverseIterator1(t *testing.T) { } func TestPrefixDBReverseIterator2(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -128,6 +142,8 @@ func TestPrefixDBReverseIterator2(t *testing.T) { } func TestPrefixDBReverseIterator3(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -138,6 +154,8 @@ func TestPrefixDBReverseIterator3(t *testing.T) { } func TestPrefixDBReverseIterator4(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -148,6 +166,8 @@ func TestPrefixDBReverseIterator4(t *testing.T) { } func TestPrefixDBReverseIterator5(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -164,6 +184,8 @@ func TestPrefixDBReverseIterator5(t *testing.T) { } func TestPrefixDBReverseIterator6(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) @@ -178,6 +200,8 @@ func TestPrefixDBReverseIterator6(t *testing.T) { } func TestPrefixDBReverseIterator7(t *testing.T) { + t.Parallel() + db := mockDBWithStuff() pdb := NewPrefixDB(db, bz("key")) diff --git a/tm2/pkg/db/util_test.go b/tm2/pkg/db/util_test.go index adf52123290..f66fddde64e 100644 --- a/tm2/pkg/db/util_test.go +++ b/tm2/pkg/db/util_test.go @@ -7,8 +7,12 @@ import ( // Empty iterator for empty db. func TestPrefixIteratorNoMatchNil(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) itr := IteratePrefix(db, []byte("2")) @@ -19,6 +23,8 @@ func TestPrefixIteratorNoMatchNil(t *testing.T) { // Empty iterator for db populated after iterator created. func TestPrefixIteratorNoMatch1(t *testing.T) { + t.Parallel() + for backend := range backends { if backend == BoltDBBackend { t.Log("bolt does not support concurrent writes while iterating") @@ -26,6 +32,8 @@ func TestPrefixIteratorNoMatch1(t *testing.T) { } t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) itr := IteratePrefix(db, []byte("2")) db.SetSync(bz("1"), bz("value_1")) @@ -37,8 +45,12 @@ func TestPrefixIteratorNoMatch1(t *testing.T) { // Empty iterator for prefix starting after db entry. func TestPrefixIteratorNoMatch2(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) db.SetSync(bz("3"), bz("value_3")) itr := IteratePrefix(db, []byte("4")) @@ -50,8 +62,12 @@ func TestPrefixIteratorNoMatch2(t *testing.T) { // Iterator with single val for db with single val, starting from that val. func TestPrefixIteratorMatch1(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) db.SetSync(bz("2"), bz("value_2")) itr := IteratePrefix(db, bz("2")) @@ -68,8 +84,12 @@ func TestPrefixIteratorMatch1(t *testing.T) { // Iterator with prefix iterates over everything with same prefix. func TestPrefixIteratorMatches1N(t *testing.T) { + t.Parallel() + for backend := range backends { t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { + t.Parallel() + db := newTempDB(t, backend) // prefixed diff --git a/tm2/pkg/errors/errors_test.go b/tm2/pkg/errors/errors_test.go index ddd2539738f..21115c21862 100644 --- a/tm2/pkg/errors/errors_test.go +++ b/tm2/pkg/errors/errors_test.go @@ -9,6 +9,8 @@ import ( ) func TestErrorPanic(t *testing.T) { + t.Parallel() + type pnk struct { msg string } @@ -31,6 +33,8 @@ func TestErrorPanic(t *testing.T) { } func TestWrapSomething(t *testing.T) { + t.Parallel() + err := Wrap("something", "formatter%v%v", 0, 1) assert.Equal(t, "something", err.Data()) @@ -40,6 +44,8 @@ func TestWrapSomething(t *testing.T) { } func TestWrapNothing(t *testing.T) { + t.Parallel() + err := Wrap(nil, "formatter%v%v", 0, 1) assert.Equal(t, @@ -51,6 +57,8 @@ func TestWrapNothing(t *testing.T) { } func TestErrorNew(t *testing.T) { + t.Parallel() + err := New("formatter%v%v", 0, 1) assert.Equal(t, @@ -62,6 +70,8 @@ func TestErrorNew(t *testing.T) { } func TestErrorNewWithDetails(t *testing.T) { + t.Parallel() + err := New("formatter%v%v", 0, 1) err.Trace(0, "trace %v", 1) err.Trace(0, "trace %v", 2) @@ -71,6 +81,8 @@ func TestErrorNewWithDetails(t *testing.T) { } func TestErrorNewWithStacktrace(t *testing.T) { + t.Parallel() + err := New("formatter%v%v", 0, 1).Stacktrace() assert.Equal(t, @@ -82,6 +94,8 @@ func TestErrorNewWithStacktrace(t *testing.T) { } func TestErrorNewWithTrace(t *testing.T) { + t.Parallel() + err := New("formatter%v%v", 0, 1) err.Trace(0, "trace %v", 1) err.Trace(0, "trace %v", 2) @@ -100,6 +114,8 @@ func TestErrorNewWithTrace(t *testing.T) { } func TestWrapError(t *testing.T) { + t.Parallel() + var err1 error = New("my message") var err2 error = Wrap(err1, "another message") assert.Equal(t, err1, err2) diff --git a/tm2/pkg/events/events_test.go b/tm2/pkg/events/events_test.go index f3b4aa2fd82..5232364b1c8 100644 --- a/tm2/pkg/events/events_test.go +++ b/tm2/pkg/events/events_test.go @@ -35,6 +35,8 @@ func TestAddListenerFireOnce(t *testing.T) { // TestAddListenerFireMany sets up an EventSwitch, subscribes a single // listener, and sends a thousand integers. func TestAddListenerFireMany(t *testing.T) { + t.Parallel() + evsw := NewEventSwitch() err := evsw.Start() require.NoError(t, err) @@ -62,6 +64,8 @@ func TestAddListenerFireMany(t *testing.T) { // TestAddListeners sets up an EventSwitch, subscribes three // listeners, and sends a thousand integers for each. func TestAddListeners(t *testing.T) { + t.Parallel() + evsw := NewEventSwitch() err := evsw.Start() require.NoError(t, err) @@ -100,6 +104,8 @@ func TestAddListeners(t *testing.T) { } func TestAddAndRemoveListenerConcurrency(t *testing.T) { + t.Parallel() + var ( stopInputEvent = false roundCount = 2000 @@ -145,6 +151,8 @@ func TestAddAndRemoveListenerConcurrency(t *testing.T) { } func TestAddAndRemoveListener(t *testing.T) { + t.Parallel() + evsw := NewEventSwitch() err := evsw.Start() require.NoError(t, err) @@ -192,6 +200,8 @@ func TestAddAndRemoveListener(t *testing.T) { // TestRemoveListener does basic tests on adding and removing func TestRemoveListener(t *testing.T) { + t.Parallel() + evsw := NewEventSwitch() err := evsw.Start() require.NoError(t, err) @@ -241,6 +251,8 @@ func TestRemoveListener(t *testing.T) { // NOTE: it is important to run this test with race conditions tracking on, // `go test -race`, to examine for possible race conditions. func TestRemoveListenersAsync(t *testing.T) { + t.Parallel() + evsw := NewEventSwitch() err := evsw.Start() require.NoError(t, err) diff --git a/tm2/pkg/flow/io_test.go b/tm2/pkg/flow/io_test.go index 196c7c65d9a..d023d32c347 100644 --- a/tm2/pkg/flow/io_test.go +++ b/tm2/pkg/flow/io_test.go @@ -32,6 +32,8 @@ func nextStatus(m *Monitor) Status { } func TestReader(t *testing.T) { + t.Parallel() + in := make([]byte, 100) for i := range in { in[i] = byte(i) @@ -101,6 +103,8 @@ func TestReader(t *testing.T) { } func TestWriter(t *testing.T) { + t.Parallel() + b := make([]byte, 100) for i := range b { b[i] = byte(i) diff --git a/tm2/pkg/iavl/basic_test.go b/tm2/pkg/iavl/basic_test.go index ab46792aee4..f684209a682 100644 --- a/tm2/pkg/iavl/basic_test.go +++ b/tm2/pkg/iavl/basic_test.go @@ -13,6 +13,8 @@ import ( ) func TestBasic(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) up := tree.Set([]byte("1"), []byte("one")) if up { @@ -103,6 +105,8 @@ func TestBasic(t *testing.T) { } func TestUnit(t *testing.T) { + t.Parallel() + expectHash := func(tree *ImmutableTree, hashCount int64) { // ensure number of new hash calculations is as expected. hash, count := tree.hashWithCount() @@ -184,6 +188,8 @@ func TestUnit(t *testing.T) { } func TestRemove(t *testing.T) { + t.Parallel() + size := 10000 keyLen, dataLen := 16, 40 @@ -214,6 +220,8 @@ func TestRemove(t *testing.T) { } func TestIntegration(t *testing.T) { + t.Parallel() + type record struct { key string value string @@ -279,6 +287,8 @@ func TestIntegration(t *testing.T) { } func TestIterateRange(t *testing.T) { + t.Parallel() + type record struct { key string value string @@ -363,6 +373,8 @@ func TestIterateRange(t *testing.T) { } func TestPersistence(t *testing.T) { + t.Parallel() + db := db.NewMemDB() // Create some random key value pairs @@ -390,6 +402,8 @@ func TestPersistence(t *testing.T) { } func TestProof(t *testing.T) { + t.Parallel() + // Construct some random tree db := db.NewMemDB() tree := NewMutableTree(db, 100) @@ -420,6 +434,8 @@ func TestProof(t *testing.T) { } func TestTreeProof(t *testing.T) { + t.Parallel() + db := db.NewMemDB() tree := NewMutableTree(db, 100) assert.Equal(t, tree.Hash(), []byte(nil)) diff --git a/tm2/pkg/iavl/common/random_test.go b/tm2/pkg/iavl/common/random_test.go index 6ed73ab898c..4c1ee8024e0 100644 --- a/tm2/pkg/iavl/common/random_test.go +++ b/tm2/pkg/iavl/common/random_test.go @@ -11,12 +11,16 @@ import ( ) func TestRandStr(t *testing.T) { + t.Parallel() + l := 243 s := RandStr(l) assert.Equal(t, l, len(s)) } func TestRandBytes(t *testing.T) { + t.Parallel() + l := 243 b := RandBytes(l) assert.Equal(t, l, len(b)) diff --git a/tm2/pkg/iavl/proof_forgery_test.go b/tm2/pkg/iavl/proof_forgery_test.go index a742ad40fcc..028675f7c48 100644 --- a/tm2/pkg/iavl/proof_forgery_test.go +++ b/tm2/pkg/iavl/proof_forgery_test.go @@ -14,6 +14,8 @@ import ( ) func TestProofForgery(t *testing.T) { + t.Parallel() + source := rand.NewSource(0) r := rand.New(source) cacheSize := 0 diff --git a/tm2/pkg/iavl/proof_test.go b/tm2/pkg/iavl/proof_test.go index f67ea45b00a..ad4d85d1cd5 100644 --- a/tm2/pkg/iavl/proof_test.go +++ b/tm2/pkg/iavl/proof_test.go @@ -15,6 +15,8 @@ import ( ) func TestTreeGetWithProof(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) require := require.New(t) for _, ikey := range []byte{0x11, 0x32, 0x50, 0x72, 0x99} { @@ -49,6 +51,8 @@ func TestTreeGetWithProof(t *testing.T) { } func TestTreeKeyExistsProof(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) root := tree.WorkingHash() @@ -115,6 +119,8 @@ func TestTreeKeyExistsProof(t *testing.T) { } func TestTreeKeyInRangeProofs(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) require := require.New(t) keys := []byte{0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7} // 10 total. diff --git a/tm2/pkg/iavl/tree_dotgraph_test.go b/tm2/pkg/iavl/tree_dotgraph_test.go index f3c2786cda3..3c0233f51fa 100644 --- a/tm2/pkg/iavl/tree_dotgraph_test.go +++ b/tm2/pkg/iavl/tree_dotgraph_test.go @@ -8,6 +8,8 @@ import ( ) func TestWriteDOTGraph(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) for _, ikey := range []byte{ 0x0a, 0x11, 0x2e, 0x32, 0x50, 0x72, 0x99, 0xa1, 0xe4, 0xf7, diff --git a/tm2/pkg/iavl/tree_fuzz_test.go b/tm2/pkg/iavl/tree_fuzz_test.go index 7ea9bb50389..64e20d944a2 100644 --- a/tm2/pkg/iavl/tree_fuzz_test.go +++ b/tm2/pkg/iavl/tree_fuzz_test.go @@ -105,6 +105,8 @@ func genRandomProgram(size int) *program { // Generate many programs and run them. func TestMutableTreeFuzz(t *testing.T) { + t.Parallel() + maxIterations := testFuzzIterations progsPerIteration := 100000 iterations := 0 diff --git a/tm2/pkg/iavl/tree_test.go b/tm2/pkg/iavl/tree_test.go index f15a3276653..ba5bbe3e5ef 100644 --- a/tm2/pkg/iavl/tree_test.go +++ b/tm2/pkg/iavl/tree_test.go @@ -51,6 +51,8 @@ func getTestDB() (db.DB, func()) { } func TestVersionedRandomTree(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() @@ -105,6 +107,8 @@ func TestVersionedRandomTree(t *testing.T) { } func TestVersionedRandomTreeSmallKeys(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() defer closeDB() @@ -146,6 +150,8 @@ func TestVersionedRandomTreeSmallKeys(t *testing.T) { } func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() defer closeDB() @@ -187,6 +193,8 @@ func TestVersionedRandomTreeSmallKeysRandomDeletes(t *testing.T) { } func TestVersionedTreeSpecial1(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 100) tree.Set([]byte("C"), []byte("so43QQFN")) @@ -209,6 +217,8 @@ func TestVersionedTreeSpecial1(t *testing.T) { } func TestVersionedRandomTreeSpecial2(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 100) @@ -225,6 +235,8 @@ func TestVersionedRandomTreeSpecial2(t *testing.T) { } func TestVersionedEmptyTree(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() defer closeDB() @@ -281,6 +293,8 @@ func TestVersionedEmptyTree(t *testing.T) { } func TestVersionedTree(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() defer closeDB() @@ -468,6 +482,8 @@ func TestVersionedTree(t *testing.T) { } func TestVersionedTreeVersionDeletingEfficiency(t *testing.T) { + t.Parallel() + d, closeDB := getTestDB() defer closeDB() @@ -512,6 +528,8 @@ func TestVersionedTreeVersionDeletingEfficiency(t *testing.T) { } func TestVersionedTreeOrphanDeleting(t *testing.T) { + t.Parallel() + mdb := db.NewMemDB() tree := NewMutableTree(mdb, 0) @@ -550,6 +568,8 @@ func TestVersionedTreeOrphanDeleting(t *testing.T) { } func TestVersionedTreeSpecialCase(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 100) @@ -571,6 +591,8 @@ func TestVersionedTreeSpecialCase(t *testing.T) { } func TestVersionedTreeSpecialCase2(t *testing.T) { + t.Parallel() + require := require.New(t) d := db.NewMemDB() @@ -598,6 +620,8 @@ func TestVersionedTreeSpecialCase2(t *testing.T) { } func TestVersionedTreeSpecialCase3(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -626,6 +650,8 @@ func TestVersionedTreeSpecialCase3(t *testing.T) { } func TestVersionedTreeSaveAndLoad(t *testing.T) { + t.Parallel() + require := require.New(t) d := db.NewMemDB() tree := NewMutableTree(d, 0) @@ -677,6 +703,8 @@ func TestVersionedTreeSaveAndLoad(t *testing.T) { } func TestVersionedTreeErrors(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 100) @@ -706,6 +734,8 @@ func TestVersionedTreeErrors(t *testing.T) { } func TestVersionedCheckpoints(t *testing.T) { + t.Parallel() + require := require.New(t) d, closeDB := getTestDB() defer closeDB() @@ -762,6 +792,8 @@ func TestVersionedCheckpoints(t *testing.T) { } func TestVersionedCheckpointsSpecialCase(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) key := []byte("k") @@ -788,6 +820,8 @@ func TestVersionedCheckpointsSpecialCase(t *testing.T) { } func TestVersionedCheckpointsSpecialCase2(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) tree.Set([]byte("U"), []byte("XamDUtiJ")) @@ -808,6 +842,8 @@ func TestVersionedCheckpointsSpecialCase2(t *testing.T) { } func TestVersionedCheckpointsSpecialCase3(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) tree.Set([]byte("n"), []byte("2wUCUs8q")) @@ -828,6 +864,8 @@ func TestVersionedCheckpointsSpecialCase3(t *testing.T) { } func TestVersionedCheckpointsSpecialCase4(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) tree.Set([]byte("U"), []byte("XamDUtiJ")) @@ -860,6 +898,8 @@ func TestVersionedCheckpointsSpecialCase4(t *testing.T) { } func TestVersionedCheckpointsSpecialCase5(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) tree.Set([]byte("R"), []byte("ygZlIzeW")) @@ -877,6 +917,8 @@ func TestVersionedCheckpointsSpecialCase5(t *testing.T) { } func TestVersionedCheckpointsSpecialCase6(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 0) tree.Set([]byte("Y"), []byte("MW79JQeV")) @@ -909,6 +951,8 @@ func TestVersionedCheckpointsSpecialCase6(t *testing.T) { } func TestVersionedCheckpointsSpecialCase7(t *testing.T) { + t.Parallel() + tree := NewMutableTree(db.NewMemDB(), 100) tree.Set([]byte("n"), []byte("OtqD3nyn")) @@ -942,6 +986,8 @@ func TestVersionedCheckpointsSpecialCase7(t *testing.T) { } func TestVersionedTreeEfficiency(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) versions := 20 @@ -977,6 +1023,8 @@ func TestVersionedTreeEfficiency(t *testing.T) { } func TestVersionedTreeProofs(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -1047,6 +1095,8 @@ func TestVersionedTreeProofs(t *testing.T) { } func TestOrphans(t *testing.T) { + t.Parallel() + // If you create a sequence of saved versions // Then randomly delete versions other than the first and last until only those two remain // Any remaining orphan nodes should be constrained to just the first version @@ -1079,6 +1129,8 @@ func TestOrphans(t *testing.T) { } func TestVersionedTreeHash(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -1101,6 +1153,8 @@ func TestVersionedTreeHash(t *testing.T) { } func TestNilValueSemantics(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -1110,6 +1164,8 @@ func TestNilValueSemantics(t *testing.T) { } func TestCopyValueSemantics(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -1127,6 +1183,8 @@ func TestCopyValueSemantics(t *testing.T) { } func TestRollback(t *testing.T) { + t.Parallel() + require := require.New(t) tree := NewMutableTree(db.NewMemDB(), 0) @@ -1156,6 +1214,8 @@ func TestRollback(t *testing.T) { } func TestLazyLoadVersion(t *testing.T) { + t.Parallel() + mdb := db.NewMemDB() tree := NewMutableTree(mdb, 0) maxVersions := 10 @@ -1194,6 +1254,8 @@ func TestLazyLoadVersion(t *testing.T) { } func TestOverwrite(t *testing.T) { + t.Parallel() + require := require.New(t) mdb := db.NewMemDB() @@ -1226,6 +1288,8 @@ func TestOverwrite(t *testing.T) { } func TestLoadVersionForOverwriting(t *testing.T) { + t.Parallel() + require := require.New(t) mdb := db.NewMemDB() diff --git a/tm2/pkg/log/tm_logger_test.go b/tm2/pkg/log/tm_logger_test.go index 81614c5ea21..43ca98eb0f9 100644 --- a/tm2/pkg/log/tm_logger_test.go +++ b/tm2/pkg/log/tm_logger_test.go @@ -10,6 +10,8 @@ import ( ) func TestLoggerLogsItsErrors(t *testing.T) { + t.Parallel() + var buf bytes.Buffer logger := log.NewTMLogger(&buf) diff --git a/tm2/pkg/os/net_test.go b/tm2/pkg/os/net_test.go index 258ee28bb28..8cbba60aef2 100644 --- a/tm2/pkg/os/net_test.go +++ b/tm2/pkg/os/net_test.go @@ -7,6 +7,8 @@ import ( ) func TestProtocolAndAddress(t *testing.T) { + t.Parallel() + cases := []struct { fullAddr string proto string diff --git a/tm2/pkg/p2p/conn/connection_test.go b/tm2/pkg/p2p/conn/connection_test.go index 6974dd97e56..68f7fbc0841 100644 --- a/tm2/pkg/p2p/conn/connection_test.go +++ b/tm2/pkg/p2p/conn/connection_test.go @@ -37,6 +37,8 @@ func createMConnectionWithCallbacks(conn net.Conn, onReceive func(chID byte, msg } func TestMConnectionSendFlushStop(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() //nolint: errcheck defer client.Close() //nolint: errcheck @@ -82,6 +84,8 @@ func TestMConnectionSendFlushStop(t *testing.T) { } func TestMConnectionSend(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() //nolint: errcheck defer client.Close() //nolint: errcheck @@ -113,6 +117,8 @@ func TestMConnectionSend(t *testing.T) { } func TestMConnectionReceive(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() //nolint: errcheck defer client.Close() //nolint: errcheck @@ -149,6 +155,8 @@ func TestMConnectionReceive(t *testing.T) { } func TestMConnectionStatus(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() //nolint: errcheck defer client.Close() //nolint: errcheck @@ -164,6 +172,8 @@ func TestMConnectionStatus(t *testing.T) { } func TestMConnectionPongTimeoutResultsInError(t *testing.T) { + t.Parallel() + server, client := net.Pipe() defer server.Close() defer client.Close() @@ -203,6 +213,8 @@ func TestMConnectionPongTimeoutResultsInError(t *testing.T) { } func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) { + t.Parallel() + server, client := net.Pipe() defer server.Close() defer client.Close() @@ -256,6 +268,8 @@ func TestMConnectionMultiplePongsInTheBeginning(t *testing.T) { } func TestMConnectionMultiplePings(t *testing.T) { + t.Parallel() + server, client := net.Pipe() defer server.Close() defer client.Close() @@ -293,6 +307,8 @@ func TestMConnectionMultiplePings(t *testing.T) { } func TestMConnectionPingPongs(t *testing.T) { + t.Parallel() + // check that we are not leaking any go-routines defer leaktest.CheckTimeout(t, 10*time.Second)() @@ -348,6 +364,8 @@ func TestMConnectionPingPongs(t *testing.T) { } func TestMConnectionStopsAndReturnsError(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() //nolint: errcheck defer client.Close() //nolint: errcheck @@ -422,6 +440,8 @@ func expectSend(ch chan struct{}) bool { } func TestMConnectionReadErrorBadEncoding(t *testing.T) { + t.Parallel() + chOnErr := make(chan struct{}) mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr) defer mconnClient.Stop() @@ -440,6 +460,8 @@ func TestMConnectionReadErrorBadEncoding(t *testing.T) { } func TestMConnectionReadErrorUnknownChannel(t *testing.T) { + t.Parallel() + chOnErr := make(chan struct{}) mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr) defer mconnClient.Stop() @@ -457,6 +479,8 @@ func TestMConnectionReadErrorUnknownChannel(t *testing.T) { } func TestMConnectionReadErrorLongMessage(t *testing.T) { + t.Parallel() + chOnErr := make(chan struct{}) chOnRcv := make(chan struct{}) @@ -499,6 +523,8 @@ func TestMConnectionReadErrorLongMessage(t *testing.T) { } func TestMConnectionReadErrorUnknownMsgType(t *testing.T) { + t.Parallel() + chOnErr := make(chan struct{}) mconnClient, mconnServer := newClientAndServerConnsForReadErrors(t, chOnErr) defer mconnClient.Stop() @@ -513,6 +539,8 @@ func TestMConnectionReadErrorUnknownMsgType(t *testing.T) { } func TestMConnectionTrySend(t *testing.T) { + t.Parallel() + server, client := NetPipe() defer server.Close() defer client.Close() diff --git a/tm2/pkg/p2p/conn/secret_connection_test.go b/tm2/pkg/p2p/conn/secret_connection_test.go index a2560af34eb..b238297ae5e 100644 --- a/tm2/pkg/p2p/conn/secret_connection_test.go +++ b/tm2/pkg/p2p/conn/secret_connection_test.go @@ -98,6 +98,8 @@ func makeSecretConnPair(tb testing.TB) (fooSecConn, barSecConn *SecretConnection } func TestSecretConnectionHandshake(t *testing.T) { + t.Parallel() + fooSecConn, barSecConn := makeSecretConnPair(t) if err := fooSecConn.Close(); err != nil { t.Error(err) @@ -110,6 +112,8 @@ func TestSecretConnectionHandshake(t *testing.T) { // Test that shareEphPubKey rejects lower order public keys based on an // (incomplete) blacklist. func TestShareLowOrderPubkey(t *testing.T) { + t.Parallel() + fooConn, barConn := makeKVStoreConnPair() defer fooConn.Close() defer barConn.Close() @@ -141,6 +145,8 @@ func TestShareLowOrderPubkey(t *testing.T) { // Test that additionally that the Diffie-Hellman shared secret is non-zero. // The shared secret would be zero for lower order pub-keys (but tested against the blacklist only). func TestComputeDHFailsOnLowOrder(t *testing.T) { + t.Parallel() + _, locPrivKey := genEphKeys() for _, remLowOrderPubKey := range blacklist { remLowOrderPubKey := remLowOrderPubKey @@ -153,6 +159,8 @@ func TestComputeDHFailsOnLowOrder(t *testing.T) { } func TestConcurrentWrite(t *testing.T) { + t.Parallel() + fooSecConn, barSecConn := makeSecretConnPair(t) fooWriteText := random.RandStr(dataMaxSize) @@ -175,6 +183,8 @@ func TestConcurrentWrite(t *testing.T) { } func TestConcurrentRead(t *testing.T) { + t.Parallel() + fooSecConn, barSecConn := makeSecretConnPair(t) fooWriteText := random.RandStr(dataMaxSize) n := 100 @@ -221,6 +231,8 @@ func readLots(t *testing.T, wg *sync.WaitGroup, conn net.Conn, n int) { } func TestSecretConnectionReadWrite(t *testing.T) { + t.Parallel() + fooConn, barConn := makeKVStoreConnPair() fooWrites, barWrites := []string{}, []string{} fooReads, barReads := []string{}, []string{} @@ -341,6 +353,8 @@ func TestSecretConnectionReadWrite(t *testing.T) { var update = flag.Bool("update", false, "update .golden files") func TestDeriveSecretsAndChallengeGolden(t *testing.T) { + t.Parallel() + goldenFilepath := filepath.Join("testdata", t.Name()+".golden") if *update { t.Logf("Updating golden test vector file %s", goldenFilepath) @@ -386,6 +400,8 @@ func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil } func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) } func TestNilPubkey(t *testing.T) { + t.Parallel() + fooConn, barConn := makeKVStoreConnPair() fooPrvKey := ed25519.GenPrivKey() barPrvKey := privKeyWithNilPubKey{ed25519.GenPrivKey()} @@ -404,6 +420,8 @@ func TestNilPubkey(t *testing.T) { } func TestNonEd25519Pubkey(t *testing.T) { + t.Parallel() + fooConn, barConn := makeKVStoreConnPair() fooPrvKey := ed25519.GenPrivKey() barPrvKey := secp256k1.GenPrivKey() diff --git a/tm2/pkg/p2p/key_test.go b/tm2/pkg/p2p/key_test.go index 8046f58eb25..4f67cc0a5da 100644 --- a/tm2/pkg/p2p/key_test.go +++ b/tm2/pkg/p2p/key_test.go @@ -11,6 +11,8 @@ import ( ) func TestLoadOrGenNodeKey(t *testing.T) { + t.Parallel() + filePath := filepath.Join(os.TempDir(), random.RandStr(12)+"_peer_id.json") nodeKey, err := LoadOrGenNodeKey(filePath) @@ -22,13 +24,15 @@ func TestLoadOrGenNodeKey(t *testing.T) { assert.Equal(t, nodeKey, nodeKey2) } -//---------------------------------------------------------- +// ---------------------------------------------------------- func padBytes(bz []byte, targetBytes int) []byte { return append(bz, bytes.Repeat([]byte{0xFF}, targetBytes-len(bz))...) } func TestPoWTarget(t *testing.T) { + t.Parallel() + targetBytes := 20 cases := []struct { difficulty uint diff --git a/tm2/pkg/p2p/netaddress_test.go b/tm2/pkg/p2p/netaddress_test.go index a5dc3fca375..413d020c153 100644 --- a/tm2/pkg/p2p/netaddress_test.go +++ b/tm2/pkg/p2p/netaddress_test.go @@ -11,6 +11,8 @@ import ( ) func TestAddress2ID(t *testing.T) { + t.Parallel() + idbz, _ := hex.DecodeString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef") id := crypto.AddressFromBytes(idbz).ID() assert.Equal(t, crypto.ID("g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6"), id) @@ -21,6 +23,8 @@ func TestAddress2ID(t *testing.T) { } func TestNewNetAddress(t *testing.T) { + t.Parallel() + tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") require.Nil(t, err) @@ -41,6 +45,8 @@ func TestNewNetAddress(t *testing.T) { } func TestNewNetAddressFromString(t *testing.T) { + t.Parallel() + testCases := []struct { name string addr string @@ -85,6 +91,8 @@ func TestNewNetAddressFromString(t *testing.T) { for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() + addr, err := NewNetAddressFromString(tc.addr) if tc.correct { if assert.Nil(t, err, tc.addr) { @@ -98,6 +106,8 @@ func TestNewNetAddressFromString(t *testing.T) { } func TestNewNetAddressFromStrings(t *testing.T) { + t.Parallel() + addrs, errs := NewNetAddressFromStrings([]string{ "127.0.0.1:8080", "g1m6kmam774klwlh4dhmhaatd7al02m0h0jwnyc6@127.0.0.1:8080", @@ -108,11 +118,15 @@ func TestNewNetAddressFromStrings(t *testing.T) { } func TestNewNetAddressFromIPPort(t *testing.T) { + t.Parallel() + addr := NewNetAddressFromIPPort("", net.ParseIP("127.0.0.1"), 8080) assert.Equal(t, "127.0.0.1:8080", addr.String()) } func TestNetAddressProperties(t *testing.T) { + t.Parallel() + // TODO add more test cases testCases := []struct { addr string @@ -140,6 +154,8 @@ func TestNetAddressProperties(t *testing.T) { } func TestNetAddressReachabilityTo(t *testing.T) { + t.Parallel() + // TODO add more test cases testCases := []struct { addr string diff --git a/tm2/pkg/p2p/node_info_test.go b/tm2/pkg/p2p/node_info_test.go index ea97239643f..58f1dab8854 100644 --- a/tm2/pkg/p2p/node_info_test.go +++ b/tm2/pkg/p2p/node_info_test.go @@ -11,6 +11,8 @@ import ( ) func TestNodeInfoValidate(t *testing.T) { + t.Parallel() + // empty fails ni := NodeInfo{} assert.Error(t, ni.Validate()) @@ -86,6 +88,8 @@ func TestNodeInfoValidate(t *testing.T) { } func TestNodeInfoCompatible(t *testing.T) { + t.Parallel() + nodeKey1 := NodeKey{PrivKey: ed25519.GenPrivKey()} nodeKey2 := NodeKey{PrivKey: ed25519.GenPrivKey()} name := "testing" diff --git a/tm2/pkg/p2p/peer_test.go b/tm2/pkg/p2p/peer_test.go index 070d783d2af..8841e34cc83 100644 --- a/tm2/pkg/p2p/peer_test.go +++ b/tm2/pkg/p2p/peer_test.go @@ -19,6 +19,8 @@ import ( ) func TestPeerBasic(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) // simulate remote peer @@ -43,6 +45,8 @@ func TestPeerBasic(t *testing.T) { } func TestPeerSend(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) config := cfg diff --git a/tm2/pkg/p2p/switch_test.go b/tm2/pkg/p2p/switch_test.go index a0d4853493b..9e82e20bca1 100644 --- a/tm2/pkg/p2p/switch_test.go +++ b/tm2/pkg/p2p/switch_test.go @@ -105,6 +105,8 @@ func initSwitchFunc(i int, sw *Switch) *Switch { } func TestSwitches(t *testing.T) { + t.Parallel() + s1, s2 := MakeSwitchPair(t, initSwitchFunc) defer s1.Stop() defer s2.Stop() @@ -152,6 +154,8 @@ func assertMsgReceivedWithTimeout(t *testing.T, msgBytes []byte, channel byte, r } func TestSwitchFiltersOutItself(t *testing.T) { + t.Parallel() + s1 := MakeSwitch(cfg, 1, "127.0.0.1", "123.123.123", initSwitchFunc) // simulate s1 having a public IP by creating a remote peer with the same ID @@ -176,6 +180,8 @@ func TestSwitchFiltersOutItself(t *testing.T) { } func TestSwitchPeerFilter(t *testing.T) { + t.Parallel() + var ( filters = []PeerFilterFunc{ func(_ IPeerSet, _ Peer) error { return nil }, @@ -219,6 +225,8 @@ func TestSwitchPeerFilter(t *testing.T) { } func TestSwitchPeerFilterTimeout(t *testing.T) { + t.Parallel() + var ( filters = []PeerFilterFunc{ func(_ IPeerSet, _ Peer) error { @@ -260,6 +268,8 @@ func TestSwitchPeerFilterTimeout(t *testing.T) { } func TestSwitchPeerFilterDuplicate(t *testing.T) { + t.Parallel() + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) sw.Start() defer sw.Stop() @@ -303,6 +313,8 @@ func assertNoPeersAfterTimeout(t *testing.T, sw *Switch, timeout time.Duration) } func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) @@ -338,6 +350,8 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { } func TestSwitchStopPeerForError(t *testing.T) { + t.Parallel() + // make two connected switches sw1, sw2 := MakeSwitchPair(t, func(i int, sw *Switch) *Switch { return initSwitchFunc(i, sw) @@ -360,6 +374,8 @@ func TestSwitchStopPeerForError(t *testing.T) { } func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { + t.Parallel() + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) err := sw.Start() require.NoError(t, err) @@ -405,6 +421,8 @@ func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { } func TestSwitchReconnectsToInboundPersistentPeer(t *testing.T) { + t.Parallel() + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) err := sw.Start() require.NoError(t, err) @@ -430,6 +448,8 @@ func TestSwitchReconnectsToInboundPersistentPeer(t *testing.T) { } func TestSwitchDialPeersAsync(t *testing.T) { + t.Parallel() + if testing.Short() { return } @@ -460,6 +480,8 @@ func waitUntilSwitchHasAtLeastNPeers(sw *Switch, n int) { } func TestSwitchFullConnectivity(t *testing.T) { + t.Parallel() + switches := MakeConnectedSwitches(cfg, 3, initSwitchFunc, Connect2Switches) defer func() { for _, sw := range switches { @@ -475,6 +497,8 @@ func TestSwitchFullConnectivity(t *testing.T) { } func TestSwitchAcceptRoutine(t *testing.T) { + t.Parallel() + cfg.MaxNumInboundPeers = 5 // make switch @@ -547,6 +571,8 @@ func (errorTransport) Cleanup(Peer) { } func TestSwitchAcceptRoutineErrorCases(t *testing.T) { + t.Parallel() + sw := NewSwitch(cfg, errorTransport{FilterTimeoutError{}}) assert.NotPanics(t, func() { err := sw.Start() @@ -599,6 +625,8 @@ func (r *mockReactor) InitCalledBeforeRemoveFinished() bool { // see stopAndRemovePeer func TestFlappySwitchInitPeerIsNotCalledBeforeRemovePeer(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) // make reactor diff --git a/tm2/pkg/p2p/transport_test.go b/tm2/pkg/p2p/transport_test.go index 49ab9ac52e3..63b1c26e666 100644 --- a/tm2/pkg/p2p/transport_test.go +++ b/tm2/pkg/p2p/transport_test.go @@ -33,6 +33,8 @@ func newMultiplexTransport( } func TestTransportMultiplexConnFilter(t *testing.T) { + t.Parallel() + mt := newMultiplexTransport( emptyNodeInfo(), NodeKey{ @@ -87,6 +89,8 @@ func TestTransportMultiplexConnFilter(t *testing.T) { } func TestTransportMultiplexConnFilterTimeout(t *testing.T) { + t.Parallel() + mt := newMultiplexTransport( emptyNodeInfo(), NodeKey{ @@ -137,6 +141,8 @@ func TestTransportMultiplexConnFilterTimeout(t *testing.T) { } func TestTransportMultiplexAcceptMultiple(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) laddr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) @@ -212,6 +218,8 @@ func testDialer(dialAddr NetAddress, errc chan error) { } func TestFlappyTransportMultiplexAcceptNonBlocking(t *testing.T) { + t.Parallel() + testutils.FilterStability(t, testutils.Flappy) mt := testSetupMultiplexTransport(t) @@ -298,6 +306,8 @@ func TestFlappyTransportMultiplexAcceptNonBlocking(t *testing.T) { } func TestTransportMultiplexValidateNodeInfo(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) errc := make(chan error) @@ -339,6 +349,8 @@ func TestTransportMultiplexValidateNodeInfo(t *testing.T) { } func TestTransportMultiplexRejectMismatchID(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) errc := make(chan error) @@ -378,6 +390,8 @@ func TestTransportMultiplexRejectMismatchID(t *testing.T) { } func TestTransportMultiplexDialRejectWrongID(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) var ( @@ -407,6 +421,8 @@ func TestTransportMultiplexDialRejectWrongID(t *testing.T) { } func TestTransportMultiplexRejectIncompatible(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) errc := make(chan error) @@ -443,6 +459,8 @@ func TestTransportMultiplexRejectIncompatible(t *testing.T) { } func TestTransportMultiplexRejectSelf(t *testing.T) { + t.Parallel() + mt := testSetupMultiplexTransport(t) errc := make(chan error) @@ -482,6 +500,8 @@ func TestTransportMultiplexRejectSelf(t *testing.T) { } func TestTransportConnDuplicateIPFilter(t *testing.T) { + t.Parallel() + filter := ConnDuplicateIPFilter() if err := filter(nil, &testTransportConn{}, nil); err != nil { @@ -507,6 +527,8 @@ func TestTransportConnDuplicateIPFilter(t *testing.T) { } func TestTransportHandshake(t *testing.T) { + t.Parallel() + ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) diff --git a/tm2/pkg/random/random_test.go b/tm2/pkg/random/random_test.go index e2d45533626..71e05e95e16 100644 --- a/tm2/pkg/random/random_test.go +++ b/tm2/pkg/random/random_test.go @@ -15,18 +15,24 @@ import ( ) func TestRandStr(t *testing.T) { + t.Parallel() + l := 243 s := RandStr(l) assert.Equal(t, l, len(s)) } func TestRandBytes(t *testing.T) { + t.Parallel() + l := 243 b := RandBytes(l) assert.Equal(t, l, len(b)) } func TestRandIntn(t *testing.T) { + t.Parallel() + n := 243 for i := 0; i < 100; i++ { x := RandIntn(n) @@ -76,6 +82,8 @@ func testThemAll() string { } func TestRngConcurrencySafety(t *testing.T) { + t.Parallel() + var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) diff --git a/tm2/pkg/sdk/auth/ante_test.go b/tm2/pkg/sdk/auth/ante_test.go index 2dc3ec08176..9c15cca18a2 100644 --- a/tm2/pkg/sdk/auth/ante_test.go +++ b/tm2/pkg/sdk/auth/ante_test.go @@ -59,6 +59,8 @@ func defaultAnteOptions() AnteOptions { // Test various error cases in the AnteHandler control flow. func TestAnteHandlerSigErrors(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() ctx := env.ctx @@ -107,6 +109,8 @@ func TestAnteHandlerSigErrors(t *testing.T) { // Test logic around account number checking with one signer and many signers. func TestAnteHandlerAccountNumbers(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -164,6 +168,8 @@ func TestAnteHandlerAccountNumbers(t *testing.T) { // Test logic around account number checking with many signers when BlockHeight is 0. func TestAnteHandlerAccountNumbersAtBlockHeightZero(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -223,6 +229,8 @@ func TestAnteHandlerAccountNumbersAtBlockHeightZero(t *testing.T) { // Test logic around sequence checking with one signer and many signers. func TestAnteHandlerSequences(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -300,6 +308,8 @@ func TestAnteHandlerSequences(t *testing.T) { // Test logic around fee deduction. func TestAnteHandlerFees(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() ctx := env.ctx @@ -341,6 +351,8 @@ func TestAnteHandlerFees(t *testing.T) { // Test logic around memo gas consumption. func TestAnteHandlerMemoGas(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -381,6 +393,8 @@ func TestAnteHandlerMemoGas(t *testing.T) { } func TestAnteHandlerMultiSigner(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -431,6 +445,8 @@ func TestAnteHandlerMultiSigner(t *testing.T) { } func TestAnteHandlerBadSignBytes(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -508,6 +524,8 @@ func TestAnteHandlerBadSignBytes(t *testing.T) { } func TestAnteHandlerSetPubKey(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -560,6 +578,8 @@ func TestAnteHandlerSetPubKey(t *testing.T) { } func TestProcessPubKey(t *testing.T) { + t.Parallel() + env := setupTestEnv() ctx := env.ctx @@ -588,7 +608,10 @@ func TestProcessPubKey(t *testing.T) { {"pubkey doesn't match addr, simulate on", args{acc1, std.Signature{PubKey: priv2.PubKey()}, true}, false}, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + _, err := ProcessPubKey(tt.args.acc, tt.args.sig, tt.args.simulate) require.Equal(t, tt.wantErr, !err.IsOK()) }) @@ -596,6 +619,8 @@ func TestProcessPubKey(t *testing.T) { } func TestConsumeSignatureVerificationGas(t *testing.T) { + t.Parallel() + params := DefaultParams() msg := []byte{1, 2, 3, 4} @@ -625,7 +650,10 @@ func TestConsumeSignatureVerificationGas(t *testing.T) { {"unknown key", args{store.NewInfiniteGasMeter(), nil, nil, params}, 0, true}, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + res := DefaultSigVerificationGasConsumer(tt.args.meter, tt.args.sig, tt.args.pubkey, tt.args.params) if tt.shouldErr { @@ -671,6 +699,8 @@ func expectedGasCostByKeys(pubkeys []crypto.PubKey) int64 { } func TestCountSubkeys(t *testing.T) { + t.Parallel() + genPubKeys := func(n int) []crypto.PubKey { var ret []crypto.PubKey for i := 0; i < n; i++ { @@ -698,13 +728,18 @@ func TestCountSubkeys(t *testing.T) { {"multi level multikey", args{multiLevelMultiKey}, 11}, } for _, tt := range tests { - t.Run(tt.name, func(T *testing.T) { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, std.CountSubKeys(tt.args.pub)) }) } } func TestAnteHandlerSigLimitExceeded(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() anteHandler := NewAnteHandler(env.acck, env.bank, DefaultSigVerificationGasConsumer, defaultAnteOptions()) @@ -742,6 +777,8 @@ func TestAnteHandlerSigLimitExceeded(t *testing.T) { } func TestEnsureSufficientMempoolFees(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() ctx := env.ctx.WithMinGasPrices( @@ -775,6 +812,8 @@ func TestEnsureSufficientMempoolFees(t *testing.T) { // Test custom SignatureVerificationGasConsumer func TestCustomSignatureVerificationGasConsumer(t *testing.T) { + t.Parallel() + // setup env := setupTestEnv() // setup an ante handler that only accepts PubKeyEd25519 diff --git a/tm2/pkg/sdk/auth/keeper_test.go b/tm2/pkg/sdk/auth/keeper_test.go index 3af74cf4a4c..d40d96cdb4b 100644 --- a/tm2/pkg/sdk/auth/keeper_test.go +++ b/tm2/pkg/sdk/auth/keeper_test.go @@ -9,6 +9,8 @@ import ( ) func TestAccountMapperGetSet(t *testing.T) { + t.Parallel() + env := setupTestEnv() addr := crypto.AddressFromPreimage([]byte("some-address")) @@ -38,6 +40,8 @@ func TestAccountMapperGetSet(t *testing.T) { } func TestAccountMapperRemoveAccount(t *testing.T) { + t.Parallel() + env := setupTestEnv() addr1 := crypto.AddressFromPreimage([]byte("addr1")) addr2 := crypto.AddressFromPreimage([]byte("addr2")) diff --git a/tm2/pkg/sdk/bank/handler_test.go b/tm2/pkg/sdk/bank/handler_test.go index fef4810c027..85fc68fc304 100644 --- a/tm2/pkg/sdk/bank/handler_test.go +++ b/tm2/pkg/sdk/bank/handler_test.go @@ -16,6 +16,8 @@ import ( ) func TestInvalidMsg(t *testing.T) { + t.Parallel() + h := NewHandler(BankKeeper{}) res := h.Process(sdk.NewContext(sdk.RunTxModeDeliver, nil, &bft.Header{ChainID: "test-chain"}, nil), tu.NewTestMsg()) require.False(t, res.IsOK()) @@ -23,6 +25,8 @@ func TestInvalidMsg(t *testing.T) { } func TestBalances(t *testing.T) { + t.Parallel() + env := setupTestEnv() h := NewHandler(env.bank) _, _, addr := tu.KeyTestPubAddr() @@ -51,6 +55,8 @@ func TestBalances(t *testing.T) { } func TestQuerierRouteNotFound(t *testing.T) { + t.Parallel() + env := setupTestEnv() h := NewHandler(env.bank) req := abci.RequestQuery{ diff --git a/tm2/pkg/sdk/bank/keeper_test.go b/tm2/pkg/sdk/bank/keeper_test.go index d4e230330d5..59b4c12689c 100644 --- a/tm2/pkg/sdk/bank/keeper_test.go +++ b/tm2/pkg/sdk/bank/keeper_test.go @@ -11,6 +11,8 @@ import ( ) func TestKeeper(t *testing.T) { + t.Parallel() + env := setupTestEnv() ctx := env.ctx @@ -88,6 +90,8 @@ func TestKeeper(t *testing.T) { } func TestBankKeeper(t *testing.T) { + t.Parallel() + env := setupTestEnv() ctx := env.ctx @@ -134,6 +138,8 @@ func TestBankKeeper(t *testing.T) { } func TestViewKeeper(t *testing.T) { + t.Parallel() + env := setupTestEnv() ctx := env.ctx view := NewViewKeeper(env.acck) diff --git a/tm2/pkg/sdk/baseapp_test.go b/tm2/pkg/sdk/baseapp_test.go index 3fb44e5f744..f6c2038569a 100644 --- a/tm2/pkg/sdk/baseapp_test.go +++ b/tm2/pkg/sdk/baseapp_test.go @@ -80,6 +80,8 @@ func setupBaseApp(t *testing.T, options ...func(*BaseApp)) *BaseApp { } func TestMountStores(t *testing.T) { + t.Parallel() + app := setupBaseApp(t) // check both stores @@ -92,6 +94,8 @@ func TestMountStores(t *testing.T) { // Test that we can make commits and then reload old versions. // Test that LoadLatestVersion actually does. func TestLoadVersion(t *testing.T) { + t.Parallel() + pruningOpt := SetPruningOptions(store.PruneSyncable) name := t.Name() db := dbm.NewMemDB() @@ -139,6 +143,8 @@ func TestLoadVersion(t *testing.T) { } func TestAppVersionSetterGetter(t *testing.T) { + t.Parallel() + pruningOpt := SetPruningOptions(store.PruneSyncable) name := t.Name() db := dbm.NewMemDB() @@ -158,6 +164,8 @@ func TestAppVersionSetterGetter(t *testing.T) { } func TestLoadVersionInvalid(t *testing.T) { + t.Parallel() + pruningOpt := SetPruningOptions(store.PruneSyncable) name := t.Name() db := dbm.NewMemDB() @@ -198,6 +206,8 @@ func testLoadVersionHelper(t *testing.T, app *BaseApp, expectedHeight int64, exp } func TestOptionFunction(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() bap := newBaseApp("starting name", db, testChangeNameHelper("new name")) require.Equal(t, bap.name, "new name", "BaseApp should have had name changed via option function") @@ -211,6 +221,8 @@ func testChangeNameHelper(name string) func(*BaseApp) { // Test that Info returns the latest committed state. func TestInfo(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() app := newBaseApp(t.Name(), db) @@ -229,6 +241,8 @@ func TestInfo(t *testing.T) { } func TestBaseAppOptionSeal(t *testing.T) { + t.Parallel() + app := setupBaseApp(t) require.Panics(t, func() { @@ -258,6 +272,8 @@ func TestBaseAppOptionSeal(t *testing.T) { } func TestSetMinGasPrices(t *testing.T) { + t.Parallel() + minGasPrices, err := ParseGasPrices("5000stake/10gas") require.Nil(t, err) db := dbm.NewMemDB() @@ -266,6 +282,8 @@ func TestSetMinGasPrices(t *testing.T) { } func TestInitChainer(t *testing.T) { + t.Parallel() + name := t.Name() // keep the db and logger ourselves so // we can reload the same app later @@ -483,6 +501,8 @@ func incrementingCounter(t *testing.T, store store.Store, counterKey []byte, cou // on the store within a block, and that the CheckTx state // gets reset to the latest committed state during Commit func TestCheckTx(t *testing.T) { + t.Parallel() + // This ante handler reads the key and checks that the value matches the current counter. // This ensures changes to the kvstore persist across successive CheckTx. counterKey := []byte("counter-key") @@ -526,6 +546,8 @@ func TestCheckTx(t *testing.T) { // Test that successive DeliverTx can see each others' effects // on the store, both within and across blocks. func TestDeliverTx(t *testing.T) { + t.Parallel() + // test increments in the ante anteKey := []byte("ante-key") anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, mainKey, anteKey)) } @@ -562,14 +584,10 @@ func TestDeliverTx(t *testing.T) { } } -// Number of messages doesn't matter to CheckTx. -func TestMultiMsgCheckTx(t *testing.T) { - // TODO: ensure we get the same results - // with one message or many -} - // One call to DeliverTx should process all the messages, in order. func TestMultiMsgDeliverTx(t *testing.T) { + t.Parallel() + // increment the tx counter anteKey := []byte("ante-key") anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, mainKey, anteKey)) } @@ -629,17 +647,12 @@ func TestMultiMsgDeliverTx(t *testing.T) { require.Equal(t, int64(2), msgCounter2) } -// Interleave calls to Check and Deliver and ensure -// that there is no cross-talk. Check sees results of the previous Check calls -// and Deliver sees that of the previous Deliver calls, but they don't see eachother. -func TestConcurrentCheckDeliver(t *testing.T) { - // TODO -} - // Simulate a transaction that uses gas to compute the gas. // Simulate() and Query(".app/simulate", txBytes) should give // the same results. func TestSimulateTx(t *testing.T) { + t.Parallel() + gasConsumed := int64(5) anteOpt := func(bapp *BaseApp) { @@ -700,6 +713,8 @@ func TestSimulateTx(t *testing.T) { } func TestRunInvalidTransaction(t *testing.T) { + t.Parallel() + anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx Context, tx Tx, simulate bool) (newCtx Context, res Result, abort bool) { newCtx = ctx @@ -775,6 +790,8 @@ func TestRunInvalidTransaction(t *testing.T) { // Test that transactions exceeding gas limits fail func TestTxGasLimits(t *testing.T) { + t.Parallel() + gasGranted := int64(10) anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx Context, tx Tx, simulate bool) (newCtx Context, res Result, abort bool) { @@ -849,6 +866,8 @@ func TestTxGasLimits(t *testing.T) { // Test that transactions exceeding gas limits fail func TestMaxBlockGasLimits(t *testing.T) { + t.Parallel() + gasGranted := int64(10) anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx Context, tx Tx, simulate bool) (newCtx Context, res Result, abort bool) { @@ -938,6 +957,8 @@ func TestMaxBlockGasLimits(t *testing.T) { } func TestBaseAppAnteHandler(t *testing.T) { + t.Parallel() + anteKey := []byte("ante-key") anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(anteHandlerTxTest(t, mainKey, anteKey)) @@ -1007,6 +1028,8 @@ func TestBaseAppAnteHandler(t *testing.T) { } func TestGasConsumptionBadTx(t *testing.T) { + t.Parallel() + gasWanted := int64(5) anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx Context, tx Tx, simulate bool) (newCtx Context, res Result, abort bool) { @@ -1070,6 +1093,8 @@ func TestGasConsumptionBadTx(t *testing.T) { // Test that we can only query from the latest committed state. func TestQuery(t *testing.T) { + t.Parallel() + key, value := []byte("hello"), []byte("goodbye") anteOpt := func(bapp *BaseApp) { bapp.SetAnteHandler(func(ctx Context, tx Tx, simulate bool) (newCtx Context, res Result, abort bool) { diff --git a/tm2/pkg/service/service_test.go b/tm2/pkg/service/service_test.go index b3817d6f60a..adf14e0c539 100644 --- a/tm2/pkg/service/service_test.go +++ b/tm2/pkg/service/service_test.go @@ -16,6 +16,8 @@ func (testService) OnReset() error { } func TestBaseServiceWait(t *testing.T) { + t.Parallel() + ts := &testService{} ts.BaseService = *NewBaseService(nil, "TestService", ts) ts.Start() @@ -37,6 +39,8 @@ func TestBaseServiceWait(t *testing.T) { } func TestBaseServiceReset(t *testing.T) { + t.Parallel() + ts := &testService{} ts.BaseService = *NewBaseService(nil, "TestService", ts) ts.Start() diff --git a/tm2/pkg/std/coin_test.go b/tm2/pkg/std/coin_test.go index 923781ed412..8d9a9359b61 100644 --- a/tm2/pkg/std/coin_test.go +++ b/tm2/pkg/std/coin_test.go @@ -18,12 +18,16 @@ var ( // Coin tests func TestCoin(t *testing.T) { + t.Parallel() + require.Panics(t, func() { NewCoin(testDenom1, -1) }) require.Panics(t, func() { NewCoin(strings.ToUpper(testDenom1), 10) }) require.Equal(t, int64(5), NewCoin(testDenom1, 5).Amount) } func TestIsEqualCoin(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coin inputTwo Coin @@ -46,6 +50,8 @@ func TestIsEqualCoin(t *testing.T) { } func TestCoinIsValid(t *testing.T) { + t.Parallel() + cases := []struct { coin Coin expectPass bool @@ -66,6 +72,8 @@ func TestCoinIsValid(t *testing.T) { } func TestAddCoin(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coin inputTwo Coin @@ -88,6 +96,8 @@ func TestAddCoin(t *testing.T) { } func TestSubCoin(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coin inputTwo Coin @@ -120,6 +130,8 @@ func TestSubCoin(t *testing.T) { } func TestIsGTECoin(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coin inputTwo Coin @@ -142,6 +154,8 @@ func TestIsGTECoin(t *testing.T) { } func TestIsLTCoin(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coin inputTwo Coin @@ -167,6 +181,8 @@ func TestIsLTCoin(t *testing.T) { } func TestCoinIsZero(t *testing.T) { + t.Parallel() + coin := NewCoin(testDenom1, 0) res := coin.IsZero() require.True(t, res) @@ -180,6 +196,8 @@ func TestCoinIsZero(t *testing.T) { // Coins tests func TestIsZeroCoins(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coins expected bool @@ -198,6 +216,8 @@ func TestIsZeroCoins(t *testing.T) { } func TestEqualCoins(t *testing.T) { + t.Parallel() + cases := []struct { inputOne Coins inputTwo Coins @@ -224,6 +244,8 @@ func TestEqualCoins(t *testing.T) { } func TestAddCoins(t *testing.T) { + t.Parallel() + zero := int64(0) one := int64(1) two := int64(2) @@ -248,6 +270,8 @@ func TestAddCoins(t *testing.T) { } func TestSubCoins(t *testing.T) { + t.Parallel() + zero := int64(0) one := int64(1) two := int64(2) @@ -277,6 +301,8 @@ func TestSubCoins(t *testing.T) { } func TestCoins(t *testing.T) { + t.Parallel() + good := Coins{ {"gas", int64(1)}, {"mineral", int64(1)}, @@ -339,6 +365,8 @@ func TestCoins(t *testing.T) { } func TestCoinsGT(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -351,6 +379,8 @@ func TestCoinsGT(t *testing.T) { } func TestCoinsLT(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -366,6 +396,8 @@ func TestCoinsLT(t *testing.T) { } func TestCoinsLTE(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -381,6 +413,8 @@ func TestCoinsLTE(t *testing.T) { } func TestParse(t *testing.T) { + t.Parallel() + one := int64(1) cases := []struct { @@ -413,6 +447,8 @@ func TestParse(t *testing.T) { } func TestSortCoins(t *testing.T) { + t.Parallel() + good := Coins{ NewCoin("gas", 1), NewCoin("mineral", 1), @@ -462,6 +498,8 @@ func TestSortCoins(t *testing.T) { } func TestAmountOf(t *testing.T) { + t.Parallel() + case0 := Coins{} case1 := Coins{ NewCoin("gold", 0), @@ -504,6 +542,8 @@ func TestAmountOf(t *testing.T) { } func TestCoinsIsAnyGTE(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -524,6 +564,8 @@ func TestCoinsIsAnyGTE(t *testing.T) { } func TestCoinsIsAllGT(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -544,6 +586,8 @@ func TestCoinsIsAllGT(t *testing.T) { } func TestCoinsIsAllGTE(t *testing.T) { + t.Parallel() + one := int64(1) two := int64(2) @@ -566,6 +610,8 @@ func TestCoinsIsAllGTE(t *testing.T) { } func TestNewCoins(t *testing.T) { + t.Parallel() + tenatom := NewCoin("atom", 10) tenbtc := NewCoin("btc", 10) zeroeth := NewCoin("eth", 0) @@ -582,7 +628,10 @@ func TestNewCoins(t *testing.T) { {"panic on dups", []Coin{tenatom, tenatom}, Coins{}, true}, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if tt.wantPanic { require.Panics(t, func() { NewCoins(tt.coins...) }) return @@ -594,6 +643,8 @@ func TestNewCoins(t *testing.T) { } func TestCoinsIsAnyGT(t *testing.T) { + t.Parallel() + twoAtom := NewCoin("atom", 2) fiveAtom := NewCoin("atom", 5) threeEth := NewCoin("eth", 3) @@ -613,6 +664,8 @@ func TestCoinsIsAnyGT(t *testing.T) { } func TestFindDup(t *testing.T) { + t.Parallel() + abc := NewCoin("abc", 10) def := NewCoin("def", 10) ghi := NewCoin("ghi", 10) @@ -632,7 +685,10 @@ func TestFindDup(t *testing.T) { {"dup after first position", args{Coins{abc, def, def}}, 2}, } for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := findDup(tt.args.coins); got != tt.want { t.Errorf("findDup() = %v, want %v", got, tt.want) } @@ -641,6 +697,8 @@ func TestFindDup(t *testing.T) { } func TestMarshalJSONCoins(t *testing.T) { + t.Parallel() + testCases := []struct { name string input Coins @@ -652,7 +710,11 @@ func TestMarshalJSONCoins(t *testing.T) { } for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + bz, err := amino.MarshalJSON(tc.input) require.NoError(t, err) require.Equal(t, tc.strOutput, string(bz)) diff --git a/tm2/pkg/std/kvpair_test.go b/tm2/pkg/std/kvpair_test.go index d030da48053..5edf750e6dd 100644 --- a/tm2/pkg/std/kvpair_test.go +++ b/tm2/pkg/std/kvpair_test.go @@ -7,6 +7,8 @@ import ( ) func TestKVPairs(t *testing.T) { + t.Parallel() + kvs := KVPairs{ {Key: []byte("k2"), Value: []byte("")}, {Key: []byte("k1"), Value: []byte("2")}, @@ -32,6 +34,8 @@ func TestKVPairs(t *testing.T) { } func TestKI64Pairs(t *testing.T) { + t.Parallel() + kvs := KI64Pairs{ {Key: []byte("k2"), Value: 0}, {Key: []byte("k1"), Value: 2}, diff --git a/tm2/pkg/store/cache/store_test.go b/tm2/pkg/store/cache/store_test.go index 5daf83fcb61..adf122fc637 100644 --- a/tm2/pkg/store/cache/store_test.go +++ b/tm2/pkg/store/cache/store_test.go @@ -22,6 +22,8 @@ func keyFmt(i int) []byte { return bz(fmt.Sprintf("key%0.8d", i)) } func valFmt(i int) []byte { return bz(fmt.Sprintf("value%0.8d", i)) } func TestCacheStore(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} st := cache.New(mem) @@ -65,12 +67,16 @@ func TestCacheStore(t *testing.T) { } func TestCacheStoreNoNilSet(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} st := cache.New(mem) require.Panics(t, func() { st.Set([]byte("key"), nil) }, "setting a nil value should panic") } func TestCacheStoreNested(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} st := cache.New(mem) @@ -100,6 +106,8 @@ func TestCacheStoreNested(t *testing.T) { } func TestCacheKVIteratorBounds(t *testing.T) { + t.Parallel() + st := newCacheStore() // set some items @@ -151,6 +159,8 @@ func TestCacheKVIteratorBounds(t *testing.T) { } func TestCacheKVReverseIteratorBounds(t *testing.T) { + t.Parallel() + st := newCacheStore() // set some items @@ -195,6 +205,8 @@ func TestCacheKVReverseIteratorBounds(t *testing.T) { } func TestCacheKVMergeIteratorBasics(t *testing.T) { + t.Parallel() + st := newCacheStore() // set and delete an item in the cache, iterator should be empty @@ -243,6 +255,8 @@ func TestCacheKVMergeIteratorBasics(t *testing.T) { } func TestCacheKVMergeIteratorDeleteLast(t *testing.T) { + t.Parallel() + st := newCacheStore() // set some items and write them @@ -269,6 +283,8 @@ func TestCacheKVMergeIteratorDeleteLast(t *testing.T) { } func TestCacheKVMergeIteratorDeletes(t *testing.T) { + t.Parallel() + st := newCacheStore() truth := dbm.NewMemDB() @@ -303,6 +319,8 @@ func TestCacheKVMergeIteratorDeletes(t *testing.T) { } func TestCacheKVMergeIteratorChunks(t *testing.T) { + t.Parallel() + st := newCacheStore() // Use the truth to check values on the merge iterator @@ -334,6 +352,8 @@ func TestCacheKVMergeIteratorChunks(t *testing.T) { } func TestCacheKVMergeIteratorRandom(t *testing.T) { + t.Parallel() + st := newCacheStore() truth := dbm.NewMemDB() diff --git a/tm2/pkg/store/gas/store_test.go b/tm2/pkg/store/gas/store_test.go index ad335d73c24..54729abf2ed 100644 --- a/tm2/pkg/store/gas/store_test.go +++ b/tm2/pkg/store/gas/store_test.go @@ -19,6 +19,8 @@ func keyFmt(i int) []byte { return bz(fmt.Sprintf("key%0.8d", i)) } func valFmt(i int) []byte { return bz(fmt.Sprintf("value%0.8d", i)) } func TestGasKVStoreBasic(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} meter := types.NewGasMeter(10000) st := gas.New(mem, meter, types.DefaultGasConfig()) @@ -31,6 +33,8 @@ func TestGasKVStoreBasic(t *testing.T) { } func TestGasKVStoreIterator(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} meter := types.NewGasMeter(10000) st := gas.New(mem, meter, types.DefaultGasConfig()) @@ -55,6 +59,8 @@ func TestGasKVStoreIterator(t *testing.T) { } func TestGasKVStoreOutOfGasSet(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} meter := types.NewGasMeter(0) st := gas.New(mem, meter, types.DefaultGasConfig()) @@ -62,6 +68,8 @@ func TestGasKVStoreOutOfGasSet(t *testing.T) { } func TestGasKVStoreOutOfGasIterator(t *testing.T) { + t.Parallel() + mem := dbadapter.Store{dbm.NewMemDB()} meter := types.NewGasMeter(20000) st := gas.New(mem, meter, types.DefaultGasConfig()) diff --git a/tm2/pkg/store/iavl/store_test.go b/tm2/pkg/store/iavl/store_test.go index 83afae91860..a29dbab096a 100644 --- a/tm2/pkg/store/iavl/store_test.go +++ b/tm2/pkg/store/iavl/store_test.go @@ -49,6 +49,8 @@ func newAlohaTree(t *testing.T, db dbm.DB) (*iavl.MutableTree, types.CommitID) { } func TestGetImmutable(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree, cID := newAlohaTree(t, db) store := UnsafeNewStore(tree, storeOptions(10, 10)) @@ -79,6 +81,8 @@ func TestGetImmutable(t *testing.T) { } func TestTestGetImmutableIterator(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree, cID := newAlohaTree(t, db) store := UnsafeNewStore(tree, storeOptions(10, 10)) @@ -102,6 +106,8 @@ func TestTestGetImmutableIterator(t *testing.T) { } func TestIAVLStoreGetSetHasDelete(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree, _ := newAlohaTree(t, db) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -127,6 +133,8 @@ func TestIAVLStoreGetSetHasDelete(t *testing.T) { } func TestIAVLStoreNoNilSet(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree, _ := newAlohaTree(t, db) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -134,6 +142,8 @@ func TestIAVLStoreNoNilSet(t *testing.T) { } func TestIAVLIterator(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree, _ := newAlohaTree(t, db) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -207,6 +217,8 @@ func TestIAVLIterator(t *testing.T) { } func TestIAVLReverseIterator(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -240,6 +252,8 @@ func TestIAVLReverseIterator(t *testing.T) { } func TestIAVLPrefixIterator(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -302,6 +316,8 @@ func TestIAVLPrefixIterator(t *testing.T) { } func TestIAVLReversePrefixIterator(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) @@ -368,6 +384,8 @@ func nextVersion(iavl *Store) { } func TestIAVLDefaultPruning(t *testing.T) { + t.Parallel() + // Expected stored / deleted version numbers for: // numRecent = 5, storeEvery = 3 states := []pruneState{ @@ -392,6 +410,8 @@ func TestIAVLDefaultPruning(t *testing.T) { } func TestIAVLAlternativePruning(t *testing.T) { + t.Parallel() + // Expected stored / deleted version numbers for: // numRecent = 3, storeEvery = 5 states := []pruneState{ @@ -442,6 +462,8 @@ func testPruning(t *testing.T, numRecent int64, storeEvery int64, states []prune } func TestIAVLNoPrune(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, int64(1))) @@ -457,6 +479,8 @@ func TestIAVLNoPrune(t *testing.T) { } func TestIAVLPruneEverything(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(int64(0), int64(0))) @@ -475,6 +499,8 @@ func TestIAVLPruneEverything(t *testing.T) { } func TestIAVLStoreQuery(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := iavl.NewMutableTree(db, cacheSize) iavlStore := UnsafeNewStore(tree, storeOptions(numRecent, storeEvery)) diff --git a/tm2/pkg/store/prefix/store_test.go b/tm2/pkg/store/prefix/store_test.go index 70f0eae05c8..6f70c5bd16f 100644 --- a/tm2/pkg/store/prefix/store_test.go +++ b/tm2/pkg/store/prefix/store_test.go @@ -88,6 +88,8 @@ func testPrefixStore(t *testing.T, baseStore types.Store, prefix []byte) { } func TestIAVLStorePrefix(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() tree := tiavl.NewMutableTree(db, cacheSize) iavlStore := iavl.UnsafeNewStore(tree, types.StoreOptions{ @@ -101,6 +103,8 @@ func TestIAVLStorePrefix(t *testing.T) { } func TestPrefixStoreNoNilSet(t *testing.T) { + t.Parallel() + meter := types.NewGasMeter(100000000) mem := dbadapter.Store{dbm.NewMemDB()} gasStore := gas.New(mem, meter, types.DefaultGasConfig()) @@ -108,6 +112,8 @@ func TestPrefixStoreNoNilSet(t *testing.T) { } func TestPrefixStoreIterate(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() baseStore := dbadapter.Store{db} prefix := []byte("test") @@ -135,6 +141,8 @@ func incFirstByte(bz []byte) { } func TestCloneAppend(t *testing.T) { + t.Parallel() + kvps := genRandomKVPairs() for _, kvp := range kvps { bz := cloneAppend(kvp.key, kvp.value) @@ -154,6 +162,8 @@ func TestCloneAppend(t *testing.T) { } func TestPrefixStoreIteratorEdgeCase(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() baseStore := dbadapter.Store{db} @@ -184,6 +194,8 @@ func TestPrefixStoreIteratorEdgeCase(t *testing.T) { } func TestPrefixStoreReverseIteratorEdgeCase(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() baseStore := dbadapter.Store{db} @@ -323,6 +335,8 @@ func checkNextPanics(t *testing.T, itr types.Iterator) { } func TestPrefixDBSimple(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -341,6 +355,8 @@ func TestPrefixDBSimple(t *testing.T) { } func TestPrefixDBIterator1(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -359,6 +375,8 @@ func TestPrefixDBIterator1(t *testing.T) { } func TestPrefixDBIterator2(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -369,6 +387,8 @@ func TestPrefixDBIterator2(t *testing.T) { } func TestPrefixDBIterator3(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -387,6 +407,8 @@ func TestPrefixDBIterator3(t *testing.T) { } func TestPrefixDBIterator4(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -397,6 +419,8 @@ func TestPrefixDBIterator4(t *testing.T) { } func TestPrefixDBReverseIterator1(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -415,6 +439,8 @@ func TestPrefixDBReverseIterator1(t *testing.T) { } func TestPrefixDBReverseIterator2(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -433,6 +459,8 @@ func TestPrefixDBReverseIterator2(t *testing.T) { } func TestPrefixDBReverseIterator3(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) @@ -443,6 +471,8 @@ func TestPrefixDBReverseIterator3(t *testing.T) { } func TestPrefixDBReverseIterator4(t *testing.T) { + t.Parallel() + store := mockStoreWithStuff() pstore := New(store, bz("key")) diff --git a/tm2/pkg/store/rootmulti/proof_test.go b/tm2/pkg/store/rootmulti/proof_test.go index 565ea59face..c79cbb07bd5 100644 --- a/tm2/pkg/store/rootmulti/proof_test.go +++ b/tm2/pkg/store/rootmulti/proof_test.go @@ -13,6 +13,8 @@ import ( ) func TestVerifyIAVLStoreQueryProof(t *testing.T) { + t.Parallel() + // Create main tree for testing. db := dbm.NewMemDB() opts := types.StoreOptions{ @@ -58,6 +60,8 @@ func TestVerifyIAVLStoreQueryProof(t *testing.T) { } func TestVerifyMultiStoreQueryProof(t *testing.T) { + t.Parallel() + // Create main tree for testing. db := dbm.NewMemDB() store := NewMultiStore(db) @@ -113,6 +117,8 @@ func TestVerifyMultiStoreQueryProof(t *testing.T) { } func TestVerifyMultiStoreQueryProofEmptyStore(t *testing.T) { + t.Parallel() + // Create main tree for testing. db := dbm.NewMemDB() store := NewMultiStore(db) @@ -142,6 +148,8 @@ func TestVerifyMultiStoreQueryProofEmptyStore(t *testing.T) { } func TestVerifyMultiStoreQueryProofAbsence(t *testing.T) { + t.Parallel() + // Create main tree for testing. db := dbm.NewMemDB() store := NewMultiStore(db) diff --git a/tm2/pkg/store/rootmulti/store_test.go b/tm2/pkg/store/rootmulti/store_test.go index e660f80c6bb..e6a04ee5ded 100644 --- a/tm2/pkg/store/rootmulti/store_test.go +++ b/tm2/pkg/store/rootmulti/store_test.go @@ -15,6 +15,8 @@ import ( ) func TestStoreType(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() store := NewMultiStore(db) store.MountStoreWithDB( @@ -22,6 +24,8 @@ func TestStoreType(t *testing.T) { } func TestStoreMount(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() store := NewMultiStore(db) @@ -37,6 +41,8 @@ func TestStoreMount(t *testing.T) { } func TestCacheMultiStoreWithVersion(t *testing.T) { + t.Parallel() + var db dbm.DB = dbm.NewMemDB() ms := newMultiStoreWithMounts(db) err := ms.LoadLatestVersion() @@ -74,6 +80,8 @@ func TestCacheMultiStoreWithVersion(t *testing.T) { } func TestHashStableWithEmptyCommit(t *testing.T) { + t.Parallel() + var db dbm.DB = dbm.NewMemDB() ms := newMultiStoreWithMounts(db) err := ms.LoadLatestVersion() @@ -98,6 +106,8 @@ func TestHashStableWithEmptyCommit(t *testing.T) { } func TestMultistoreCommitLoad(t *testing.T) { + t.Parallel() + var db dbm.DB = dbm.NewMemDB() store := newMultiStoreWithMounts(db) err := store.LoadLatestVersion() @@ -158,6 +168,8 @@ func TestMultistoreCommitLoad(t *testing.T) { } func TestParsePath(t *testing.T) { + t.Parallel() + _, _, err := parsePath("foo") require.Error(t, err) @@ -178,6 +190,8 @@ func TestParsePath(t *testing.T) { } func TestMultiStoreQuery(t *testing.T) { + t.Parallel() + db := dbm.NewMemDB() multi := newMultiStoreWithMounts(db) err := multi.LoadLatestVersion() diff --git a/tm2/pkg/store/types/gas_test.go b/tm2/pkg/store/types/gas_test.go index 7fef09227c5..410ba0b7e92 100644 --- a/tm2/pkg/store/types/gas_test.go +++ b/tm2/pkg/store/types/gas_test.go @@ -9,6 +9,8 @@ import ( ) func TestGasMeter(t *testing.T) { + t.Parallel() + cases := []struct { limit Gas usage []Gas @@ -46,6 +48,8 @@ func TestGasMeter(t *testing.T) { } func TestAddUint64Overflow(t *testing.T) { + t.Parallel() + testCases := []struct { a, b int64 result int64 diff --git a/tm2/pkg/strings/string_test.go b/tm2/pkg/strings/string_test.go index 1ec7b0d56be..55deac00b1f 100644 --- a/tm2/pkg/strings/string_test.go +++ b/tm2/pkg/strings/string_test.go @@ -9,6 +9,8 @@ import ( ) func TestStringInSlice(t *testing.T) { + t.Parallel() + assert.True(t, StringInSlice("a", []string{"a", "b", "c"})) assert.False(t, StringInSlice("d", []string{"a", "b", "c"})) assert.True(t, StringInSlice("", []string{""})) @@ -16,6 +18,8 @@ func TestStringInSlice(t *testing.T) { } func TestIsASCIIText(t *testing.T) { + t.Parallel() + notASCIIText := []string{ "", "\xC2", "\xC2\xA2", "\xFF", "\x80", "\xF0", "\n", "\t", } @@ -31,6 +35,8 @@ func TestIsASCIIText(t *testing.T) { } func TestASCIITrim(t *testing.T) { + t.Parallel() + assert.Equal(t, ASCIITrim(" "), "") assert.Equal(t, ASCIITrim(" a"), "a") assert.Equal(t, ASCIITrim("a "), "a") @@ -39,6 +45,8 @@ func TestASCIITrim(t *testing.T) { } func TestStringSliceEqual(t *testing.T) { + t.Parallel() + tests := []struct { a []string b []string diff --git a/tm2/pkg/timer/throttle_timer_test.go b/tm2/pkg/timer/throttle_timer_test.go index 2bee1dc0125..fb82fe39ff6 100644 --- a/tm2/pkg/timer/throttle_timer_test.go +++ b/tm2/pkg/timer/throttle_timer_test.go @@ -36,6 +36,8 @@ func (c *thCounter) Read() { } func TestThrottle(test *testing.T) { + test.Parallel() + assert := asrt.New(test) ms := 100 From da05213ffef88ad013b6a0ebcff965c1f2156826 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 9 Nov 2023 17:18:53 +0100 Subject: [PATCH 71/93] fix: In Keybase GetByAddress, change generic error to NewErrKeyNotFound (#1316) The Keybase method `GetByName` [returns the specific error](https://github.com/gnolang/gno/blob/199cd29584d44812a0aec3606bbff37a320c609a/tm2/pkg/crypto/keys/keybase.go#L184) `ErrKeyNotFound` if the key is not found. This is very nice because the GnoMobile API wraps this with a gRPC function and we use `keyerror.IsErrKeyNotFound` to check the error type and convert it to the gRPC equivalent. But the Keybase method `GetByAddress` just [returns a generic error](https://github.com/gnolang/gno/blob/199cd29584d44812a0aec3606bbff37a320c609a/tm2/pkg/crypto/keys/keybase.go#L192) if the key isn't found. (I assume that it is not intentional to restrict this to a generic error.) This means that the gRPC interface must search the error string for "not found", which is unreliable. We would prefer to use `keyerror.IsErrKeyNotFound`. Furthermore, `GetByNameOrAddress` [can return an error from either](https://github.com/gnolang/gno/blob/199cd29584d44812a0aec3606bbff37a320c609a/tm2/pkg/crypto/keys/keybase.go#L175-L177) `GetByName` or `GetByAddress`. It is preferable to return just one error type for key not found. This pull request updates `GetByAddress` to use `keyerror.NewErrKeyNotFound` with the same error message.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] 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).
Signed-off-by: Jeff Thompson --- tm2/pkg/crypto/keys/keybase.go | 2 +- tm2/pkg/crypto/keys/keybase_test.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 13f6e00979d..06eaf54f503 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -181,7 +181,7 @@ func (kb dbKeybase) GetByName(name string) (Info, error) { func (kb dbKeybase) GetByAddress(address crypto.Address) (Info, error) { ik := kb.db.Get(addrKey(address)) if len(ik) == 0 { - return nil, fmt.Errorf("key with address %s not found", address) + return nil, keyerror.NewErrKeyNotFound(fmt.Sprintf("key with address %s not found", address)) } bs := kb.db.Get(ik) return readInfo(bs) diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index cae5f2af947..e80c6c3cd03 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/crypto/ed25519" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/keyerror" ) func TestCreateAccountInvalidMnemonic(t *testing.T) { @@ -107,6 +108,7 @@ func TestKeyManagement(t *testing.T) { require.NoError(t, err) _, err = cstore.GetByAddress(addr) require.NotNil(t, err) + require.True(t, keyerror.IsErrKeyNotFound(err)) // list shows them in order keyS, err := cstore.List() From 39ea6620d327a506340182fc7043d623ba7c9507 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:38:56 +0100 Subject: [PATCH 72/93] feat: add contribs/gnokeykc (#1270) ## Done - add a `contribs/gnokeykc` wrapper (adding keychain support). - make `tm2/commands.IO` an interface instead of a struct. ## Usage ```console $ gnokeykc kc set Enter password. Successfully added password for key. $ gnokeykc maketx send --send 1ugnot --to g1fsu3z335h5qngf7t3lmakvpmpwg9ae76tqwh7c --chainid test3 --remote test3.gno.land:36657 --gas-fee "1000000ugnot" --gas-wanted "2000000" --broadcast moul OK! GAS WANTED: 2000000 GAS USED: 47072 ``` ## Links - [x] Depends on #1256 (for contribs/' Makefile & CI) --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- contribs/gnokeykc/README.md | 16 ++ contribs/gnokeykc/go.mod | 54 ++++++ contribs/gnokeykc/go.sum | 237 +++++++++++++++++++++++ contribs/gnokeykc/kc.go | 94 +++++++++ contribs/gnokeykc/main.go | 32 +++ gno.land/cmd/genesis/balances.go | 2 +- gno.land/cmd/genesis/balances_add.go | 6 +- gno.land/cmd/genesis/balances_export.go | 4 +- gno.land/cmd/genesis/balances_remove.go | 4 +- gno.land/cmd/genesis/generate.go | 4 +- gno.land/cmd/genesis/main.go | 2 +- gno.land/cmd/genesis/txs.go | 2 +- gno.land/cmd/genesis/txs_add.go | 4 +- gno.land/cmd/genesis/txs_export.go | 4 +- gno.land/cmd/genesis/txs_remove.go | 4 +- gno.land/cmd/genesis/validator.go | 2 +- gno.land/cmd/genesis/validator_add.go | 4 +- gno.land/cmd/genesis/validator_remove.go | 4 +- gno.land/cmd/genesis/verify.go | 4 +- gno.land/cmd/gnofaucet/serve.go | 4 +- gno.land/cmd/gnoland/root.go | 2 +- gno.land/cmd/gnoland/start.go | 8 +- gnovm/cmd/gno/build.go | 4 +- gnovm/cmd/gno/clean.go | 6 +- gnovm/cmd/gno/doc.go | 6 +- gnovm/cmd/gno/lint.go | 8 +- gnovm/cmd/gno/main.go | 2 +- gnovm/cmd/gno/mod.go | 10 +- gnovm/cmd/gno/precompile.go | 4 +- gnovm/cmd/gno/run.go | 10 +- gnovm/cmd/gno/test.go | 14 +- tm2/pkg/commands/io.go | 69 ++++--- tm2/pkg/commands/utils.go | 10 +- tm2/pkg/crypto/keys/client/add.go | 8 +- tm2/pkg/crypto/keys/client/addpkg.go | 6 +- tm2/pkg/crypto/keys/client/broadcast.go | 4 +- tm2/pkg/crypto/keys/client/call.go | 4 +- tm2/pkg/crypto/keys/client/delete.go | 6 +- tm2/pkg/crypto/keys/client/export.go | 4 +- tm2/pkg/crypto/keys/client/generate.go | 4 +- tm2/pkg/crypto/keys/client/import.go | 4 +- tm2/pkg/crypto/keys/client/list.go | 6 +- tm2/pkg/crypto/keys/client/maketx.go | 2 +- tm2/pkg/crypto/keys/client/query.go | 4 +- tm2/pkg/crypto/keys/client/root.go | 2 +- tm2/pkg/crypto/keys/client/send.go | 4 +- tm2/pkg/crypto/keys/client/sign.go | 4 +- tm2/pkg/crypto/keys/client/verify.go | 4 +- 48 files changed, 582 insertions(+), 124 deletions(-) create mode 100644 contribs/gnokeykc/README.md create mode 100644 contribs/gnokeykc/go.mod create mode 100644 contribs/gnokeykc/go.sum create mode 100644 contribs/gnokeykc/kc.go create mode 100644 contribs/gnokeykc/main.go diff --git a/contribs/gnokeykc/README.md b/contribs/gnokeykc/README.md new file mode 100644 index 00000000000..5d508b4b221 --- /dev/null +++ b/contribs/gnokeykc/README.md @@ -0,0 +1,16 @@ +# `gnokeykc` + +`gnokeykc` is a Go-based CLI tool that enhances [`gnokey`](../../gno.land/cmd/gnokey) by integrating with your system's keychain. It adds `gnokey kc ...` subcommands to set and unset passwords in the keychain, allowing Gnokey to fetch passwords directly from the keychain instead of prompting for terminal input. + +## Usage + + gnokey kc -h + +## Terminal Alias + +For ease of use, set up a terminal alias to replace `gnokey` with `gnokeykc`: + + echo "alias gnokey='gnokeykc'" >> ~/.bashrc && source ~/.bashrc + +Now, `gnokey` commands will use `gnokeykc`, fetching passwords from the keychain. + diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod new file mode 100644 index 00000000000..fca27e7455a --- /dev/null +++ b/contribs/gnokeykc/go.mod @@ -0,0 +1,54 @@ +module github.com/gnolang/gno/contribs/gnokeykc + +go 1.20 + +replace github.com/gnolang/gno => ../.. + +require ( + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 + github.com/zalando/go-keyring v0.2.3 +) + +require ( + github.com/alessio/shellescape v1.4.1 // indirect + github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c // indirect + github.com/btcsuite/btcd/btcutil v1.0.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cockroachdb/apd v1.1.0 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/badger/v3 v3.2103.4 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gnolang/goleveldb v0.0.9 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.12.3 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/linxGnu/grocksdb v1.8.4 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/rs/cors v1.10.1 // indirect + github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + go.etcd.io/bbolt v1.3.7 // indirect + go.opencensus.io v0.22.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/tools v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum new file mode 100644 index 00000000000..9ab29663ae6 --- /dev/null +++ b/contribs/gnokeykc/go.sum @@ -0,0 +1,237 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c h1:lnAMg3ra/Gw4AkRMxrxYs8nrprWsHowg8H9zaYsJOo4= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd/btcutil v1.0.0 h1:dB36qRTOucIh6NUe40UCieOS+axPhP6VNyRtYkTUKKk= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger/v3 v3.2103.4 h1:WE1B07YNTTJTtG9xjBcSW2wn0RJLyiV99h959RKZqM4= +github.com/dgraph-io/badger/v3 v3.2103.4/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE= +github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jaekwon/testify v1.6.1 h1:4AtAJcR9GzXN5W4DdY7ie74iCPiJV1JJUJL90t2ZUyw= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/linxGnu/grocksdb v1.8.4 h1:ZMsBpPpJNtRLHiKKp0mI7gW+NT4s7UgfD5xHxx1jVRo= +github.com/linxGnu/grocksdb v1.8.4/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= +go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/contribs/gnokeykc/kc.go b/contribs/gnokeykc/kc.go new file mode 100644 index 00000000000..f91dc8ba4dd --- /dev/null +++ b/contribs/gnokeykc/kc.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/zalando/go-keyring" +) + +const ( + kcService = "gnokey" + kcName = "encryption" +) + +func newKcCmd(io commands.IO) *commands.Command { + cmd := commands.NewCommand( + commands.Metadata{ + Name: "kc", + ShortUsage: "kc ", + ShortHelp: "Manage OS keychain", + }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + cmd.AddSubCommands( + newKcSetCmd(io), + newKcUnsetCmd(io), + ) + return cmd +} + +func newKcSetCmd(io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "set", + ShortUsage: "set", + ShortHelp: "set encryption password in OS keychain", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execKcSet(args, io) + }, + ) +} + +func execKcSet(args []string, io commands.IO) error { + if len(args) != 0 { + return flag.ErrHelp + } + + insecurePasswordStdin := false // XXX: cfg.rootCfg.InsecurePasswordStdin + password, err := io.GetPassword("Enter password.", insecurePasswordStdin) + if err != nil { + return fmt.Errorf("cannot read password: %w", err) + } + + err = keyring.Set(kcService, kcName, password) + if err != nil { + return fmt.Errorf("cannot set password is OS keychain") + } + + io.Printfln("Successfully added password for key.") + return nil +} + +func newKcUnsetCmd(io commands.IO) *commands.Command { + return commands.NewCommand( + commands.Metadata{ + Name: "unset", + ShortUsage: "unset", + ShortHelp: "unset password in OS keychain", + }, + commands.NewEmptyConfig(), + func(_ context.Context, args []string) error { + return execKcUnset(args, io) + }, + ) +} + +func execKcUnset(args []string, io commands.IO) error { + if len(args) != 0 { + return flag.ErrHelp + } + + err := keyring.Delete(kcService, kcName) + if err != nil { + return fmt.Errorf("cannot unset password from OS keychain") + } + + io.Printfln("Successfully unset password") + return nil +} diff --git a/contribs/gnokeykc/main.go b/contribs/gnokeykc/main.go new file mode 100644 index 00000000000..8060f8cb1e3 --- /dev/null +++ b/contribs/gnokeykc/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys/client" + "github.com/zalando/go-keyring" +) + +func main() { + stdio := commands.NewDefaultIO() + wrappedio := &wrappedIO{IO: stdio} + cmd := client.NewRootCmd(wrappedio) + cmd.AddSubCommands(newKcCmd(stdio)) + + if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v\n", err) + + os.Exit(1) + } +} + +type wrappedIO struct { + commands.IO +} + +func (io *wrappedIO) GetPassword(prompt string, insecure bool) (string, error) { + return keyring.Get(kcService, kcName) +} diff --git a/gno.land/cmd/genesis/balances.go b/gno.land/cmd/genesis/balances.go index bb6cd8b532c..0e81f280f33 100644 --- a/gno.land/cmd/genesis/balances.go +++ b/gno.land/cmd/genesis/balances.go @@ -11,7 +11,7 @@ type balancesCfg struct { } // newBalancesCmd creates the genesis balances subcommand -func newBalancesCmd(io *commands.IO) *commands.Command { +func newBalancesCmd(io commands.IO) *commands.Command { cfg := &balancesCfg{} cmd := commands.NewCommand( diff --git a/gno.land/cmd/genesis/balances_add.go b/gno.land/cmd/genesis/balances_add.go index 8df193c770c..d1e88efcc6b 100644 --- a/gno.land/cmd/genesis/balances_add.go +++ b/gno.land/cmd/genesis/balances_add.go @@ -35,7 +35,7 @@ type balancesAddCfg struct { } // newBalancesAddCmd creates the genesis balances add subcommand -func newBalancesAddCmd(rootCfg *balancesCfg, io *commands.IO) *commands.Command { +func newBalancesAddCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { cfg := &balancesAddCfg{ rootCfg: rootCfg, } @@ -75,7 +75,7 @@ func (c *balancesAddCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io *commands.IO) error { +func execBalancesAdd(ctx context.Context, cfg *balancesAddCfg, io commands.IO) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) if loadErr != nil { @@ -232,7 +232,7 @@ func getBalancesFromSheet(sheet io.Reader) (accountBalances, error) { // and construct a balance sheet based off of this information func getBalancesFromTransactions( ctx context.Context, - io *commands.IO, + io commands.IO, reader io.Reader, ) (accountBalances, error) { balances := make(accountBalances) diff --git a/gno.land/cmd/genesis/balances_export.go b/gno.land/cmd/genesis/balances_export.go index fd5ade26663..c07f250afeb 100644 --- a/gno.land/cmd/genesis/balances_export.go +++ b/gno.land/cmd/genesis/balances_export.go @@ -11,7 +11,7 @@ import ( ) // newBalancesExportCmd creates the genesis balances export subcommand -func newBalancesExportCmd(balancesCfg *balancesCfg, io *commands.IO) *commands.Command { +func newBalancesExportCmd(balancesCfg *balancesCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "export", @@ -26,7 +26,7 @@ func newBalancesExportCmd(balancesCfg *balancesCfg, io *commands.IO) *commands.C ) } -func execBalancesExport(cfg *balancesCfg, io *commands.IO, args []string) error { +func execBalancesExport(cfg *balancesCfg, io commands.IO, args []string) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/balances_remove.go b/gno.land/cmd/genesis/balances_remove.go index f4286d95ad2..a752bbda4fd 100644 --- a/gno.land/cmd/genesis/balances_remove.go +++ b/gno.land/cmd/genesis/balances_remove.go @@ -24,7 +24,7 @@ type balancesRemoveCfg struct { } // newBalancesRemoveCmd creates the genesis balances remove subcommand -func newBalancesRemoveCmd(rootCfg *balancesCfg, io *commands.IO) *commands.Command { +func newBalancesRemoveCmd(rootCfg *balancesCfg, io commands.IO) *commands.Command { cfg := &balancesRemoveCfg{ rootCfg: rootCfg, } @@ -51,7 +51,7 @@ func (c *balancesRemoveCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execBalancesRemove(cfg *balancesRemoveCfg, io *commands.IO) error { +func execBalancesRemove(cfg *balancesRemoveCfg, io commands.IO) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/generate.go b/gno.land/cmd/genesis/generate.go index 93f8553f9e7..684f8441874 100644 --- a/gno.land/cmd/genesis/generate.go +++ b/gno.land/cmd/genesis/generate.go @@ -23,7 +23,7 @@ type generateCfg struct { } // newGenerateCmd creates the genesis generate subcommand -func newGenerateCmd(io *commands.IO) *commands.Command { +func newGenerateCmd(io commands.IO) *commands.Command { cfg := &generateCfg{} return commands.NewCommand( @@ -91,7 +91,7 @@ func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execGenerate(cfg *generateCfg, io *commands.IO) error { +func execGenerate(cfg *generateCfg, io commands.IO) error { // Start with the default configuration genesis := getDefaultGenesis() diff --git a/gno.land/cmd/genesis/main.go b/gno.land/cmd/genesis/main.go index c0b043b456a..507d004e8ed 100644 --- a/gno.land/cmd/genesis/main.go +++ b/gno.land/cmd/genesis/main.go @@ -20,7 +20,7 @@ func main() { } } -func newRootCmd(io *commands.IO) *commands.Command { +func newRootCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", diff --git a/gno.land/cmd/genesis/txs.go b/gno.land/cmd/genesis/txs.go index a7be307c4be..a292a9c01de 100644 --- a/gno.land/cmd/genesis/txs.go +++ b/gno.land/cmd/genesis/txs.go @@ -11,7 +11,7 @@ type txsCfg struct { } // newTxsCmd creates the genesis txs subcommand -func newTxsCmd(io *commands.IO) *commands.Command { +func newTxsCmd(io commands.IO) *commands.Command { cfg := &txsCfg{} cmd := commands.NewCommand( diff --git a/gno.land/cmd/genesis/txs_add.go b/gno.land/cmd/genesis/txs_add.go index 027cedae0bd..e356badc4aa 100644 --- a/gno.land/cmd/genesis/txs_add.go +++ b/gno.land/cmd/genesis/txs_add.go @@ -22,7 +22,7 @@ var ( ) // newTxsAddCmd creates the genesis txs add subcommand -func newTxsAddCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { +func newTxsAddCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "add", @@ -40,7 +40,7 @@ func newTxsAddCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { func execTxsAdd( ctx context.Context, cfg *txsCfg, - io *commands.IO, + io commands.IO, args []string, ) error { // Load the genesis diff --git a/gno.land/cmd/genesis/txs_export.go b/gno.land/cmd/genesis/txs_export.go index 170166e8a37..b06660d87d4 100644 --- a/gno.land/cmd/genesis/txs_export.go +++ b/gno.land/cmd/genesis/txs_export.go @@ -15,7 +15,7 @@ import ( var errNoOutputFile = errors.New("no output file path specified") // newTxsExportCmd creates the genesis txs export subcommand -func newTxsExportCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { +func newTxsExportCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "export", @@ -30,7 +30,7 @@ func newTxsExportCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { ) } -func execTxsExport(cfg *txsCfg, io *commands.IO, args []string) error { +func execTxsExport(cfg *txsCfg, io commands.IO, args []string) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/txs_remove.go b/gno.land/cmd/genesis/txs_remove.go index 2aef44fe1e5..7893ad50cac 100644 --- a/gno.land/cmd/genesis/txs_remove.go +++ b/gno.land/cmd/genesis/txs_remove.go @@ -20,7 +20,7 @@ var ( ) // newTxsRemoveCmd creates the genesis txs remove subcommand -func newTxsRemoveCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { +func newTxsRemoveCmd(txsCfg *txsCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "remove", @@ -35,7 +35,7 @@ func newTxsRemoveCmd(txsCfg *txsCfg, io *commands.IO) *commands.Command { ) } -func execTxsRemove(cfg *txsCfg, io *commands.IO, args []string) error { +func execTxsRemove(cfg *txsCfg, io commands.IO, args []string) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/validator.go b/gno.land/cmd/genesis/validator.go index a1fee07d070..bf858a7732e 100644 --- a/gno.land/cmd/genesis/validator.go +++ b/gno.land/cmd/genesis/validator.go @@ -13,7 +13,7 @@ type validatorCfg struct { } // newValidatorCmd creates the genesis validator subcommand -func newValidatorCmd(io *commands.IO) *commands.Command { +func newValidatorCmd(io commands.IO) *commands.Command { cfg := &validatorCfg{ commonCfg: commonCfg{}, } diff --git a/gno.land/cmd/genesis/validator_add.go b/gno.land/cmd/genesis/validator_add.go index 603bfa90caa..502b572effb 100644 --- a/gno.land/cmd/genesis/validator_add.go +++ b/gno.land/cmd/genesis/validator_add.go @@ -28,7 +28,7 @@ type validatorAddCfg struct { } // newValidatorAddCmd creates the genesis validator add subcommand -func newValidatorAddCmd(validatorCfg *validatorCfg, io *commands.IO) *commands.Command { +func newValidatorAddCmd(validatorCfg *validatorCfg, io commands.IO) *commands.Command { cfg := &validatorAddCfg{ rootCfg: validatorCfg, } @@ -69,7 +69,7 @@ func (c *validatorAddCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execValidatorAdd(cfg *validatorAddCfg, io *commands.IO) error { +func execValidatorAdd(cfg *validatorAddCfg, io commands.IO) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.rootCfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/validator_remove.go b/gno.land/cmd/genesis/validator_remove.go index f769b53b0e1..960d891239b 100644 --- a/gno.land/cmd/genesis/validator_remove.go +++ b/gno.land/cmd/genesis/validator_remove.go @@ -13,7 +13,7 @@ import ( var errValidatorNotPresent = errors.New("validator not present in genesis.json") // newValidatorRemoveCmd creates the genesis validator remove subcommand -func newValidatorRemoveCmd(rootCfg *validatorCfg, io *commands.IO) *commands.Command { +func newValidatorRemoveCmd(rootCfg *validatorCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "remove", @@ -27,7 +27,7 @@ func newValidatorRemoveCmd(rootCfg *validatorCfg, io *commands.IO) *commands.Com ) } -func execValidatorRemove(cfg *validatorCfg, io *commands.IO) error { +func execValidatorRemove(cfg *validatorCfg, io commands.IO) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/genesis/verify.go b/gno.land/cmd/genesis/verify.go index 6c877ca51ec..260a9eb734d 100644 --- a/gno.land/cmd/genesis/verify.go +++ b/gno.land/cmd/genesis/verify.go @@ -18,7 +18,7 @@ type verifyCfg struct { } // newVerifyCmd creates the genesis verify subcommand -func newVerifyCmd(io *commands.IO) *commands.Command { +func newVerifyCmd(io commands.IO) *commands.Command { cfg := &verifyCfg{} return commands.NewCommand( @@ -39,7 +39,7 @@ func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { c.commonCfg.RegisterFlags(fs) } -func execVerify(cfg *verifyCfg, io *commands.IO) error { +func execVerify(cfg *verifyCfg, io commands.IO) error { // Load the genesis genesis, loadErr := types.GenesisDocFromFile(cfg.genesisPath) if loadErr != nil { diff --git a/gno.land/cmd/gnofaucet/serve.go b/gno.land/cmd/gnofaucet/serve.go index 3fe0966592c..e00406a6e80 100644 --- a/gno.land/cmd/gnofaucet/serve.go +++ b/gno.land/cmd/gnofaucet/serve.go @@ -152,7 +152,7 @@ func (c *config) RegisterFlags(fs *flag.FlagSet) { ) } -func execServe(cfg *config, args []string, io *commands.IO) error { +func execServe(cfg *config, args []string, io commands.IO) error { if len(args) != 1 { return flag.ErrHelp } @@ -345,7 +345,7 @@ func execServe(cfg *config, args []string, io *commands.IO) error { func sendAmountTo( cfg *config, cli rpcclient.Client, - io *commands.IO, + io commands.IO, name, pass string, toAddr crypto.Address, diff --git a/gno.land/cmd/gnoland/root.go b/gno.land/cmd/gnoland/root.go index 5b2cbe0e4fe..dfb595347c0 100644 --- a/gno.land/cmd/gnoland/root.go +++ b/gno.land/cmd/gnoland/root.go @@ -19,7 +19,7 @@ func main() { } } -func newRootCmd(io *commands.IO) *commands.Command { +func newRootCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", diff --git a/gno.land/cmd/gnoland/start.go b/gno.land/cmd/gnoland/start.go index 618f4f87a09..77d6eb4ed51 100644 --- a/gno.land/cmd/gnoland/start.go +++ b/gno.land/cmd/gnoland/start.go @@ -42,7 +42,7 @@ type startCfg struct { nodeConfigPath string } -func newStartCmd(io *commands.IO) *commands.Command { +func newStartCmd(io commands.IO) *commands.Command { cfg := &startCfg{} return commands.NewCommand( @@ -173,8 +173,8 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execStart(c *startCfg, io *commands.IO) error { - logger := log.NewTMLogger(log.NewSyncWriter(io.Out)) +func execStart(c *startCfg, io commands.IO) error { + logger := log.NewTMLogger(log.NewSyncWriter(io.Out())) dataDir := c.dataDir var ( @@ -232,7 +232,7 @@ func execStart(c *startCfg, io *commands.IO) error { return fmt.Errorf("error in creating node: %w", err) } - fmt.Fprintln(io.Err, "Node created.") + fmt.Fprintln(io.Err(), "Node created.") if c.skipStart { io.ErrPrintln("'--skip-start' is set. Exiting.") diff --git a/gnovm/cmd/gno/build.go b/gnovm/cmd/gno/build.go index b80711586e4..7396d17fd8f 100644 --- a/gnovm/cmd/gno/build.go +++ b/gnovm/cmd/gno/build.go @@ -20,7 +20,7 @@ var defaultBuildOptions = &buildCfg{ goBinary: "go", } -func newBuildCmd(io *commands.IO) *commands.Command { +func newBuildCmd(io commands.IO) *commands.Command { cfg := &buildCfg{} return commands.NewCommand( @@ -52,7 +52,7 @@ func (c *buildCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execBuild(cfg *buildCfg, args []string, io *commands.IO) error { +func execBuild(cfg *buildCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp } diff --git a/gnovm/cmd/gno/clean.go b/gnovm/cmd/gno/clean.go index 999cc5f1f53..dad5332c4fa 100644 --- a/gnovm/cmd/gno/clean.go +++ b/gnovm/cmd/gno/clean.go @@ -20,7 +20,7 @@ type cleanCfg struct { modCache bool // clean -modcache flag } -func newCleanCmd(io *commands.IO) *commands.Command { +func newCleanCmd(io commands.IO) *commands.Command { cfg := &cleanCfg{} return commands.NewCommand( @@ -59,7 +59,7 @@ func (c *cleanCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execClean(cfg *cleanCfg, args []string, io *commands.IO) error { +func execClean(cfg *cleanCfg, args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp } @@ -96,7 +96,7 @@ func execClean(cfg *cleanCfg, args []string, io *commands.IO) error { } // clean removes generated files from a directory. -func clean(dir string, cfg *cleanCfg, io *commands.IO) error { +func clean(dir string, cfg *cleanCfg, io commands.IO) error { return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err diff --git a/gnovm/cmd/gno/doc.go b/gnovm/cmd/gno/doc.go index 0de49470c2a..4617c31741e 100644 --- a/gnovm/cmd/gno/doc.go +++ b/gnovm/cmd/gno/doc.go @@ -22,7 +22,7 @@ type docCfg struct { rootDir string } -func newDocCmd(io *commands.IO) *commands.Command { +func newDocCmd(io commands.IO) *commands.Command { c := &docCfg{} return commands.NewCommand( commands.Metadata{ @@ -74,7 +74,7 @@ func (c *docCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execDoc(cfg *docCfg, args []string, io *commands.IO) error { +func execDoc(cfg *docCfg, args []string, io commands.IO) error { // guess opts.RootDir if cfg.rootDir == "" { cfg.rootDir = guessRootDir() @@ -112,7 +112,7 @@ func execDoc(cfg *docCfg, args []string, io *commands.IO) error { io.Printfln("warning: error parsing some candidate packages:\n%v", err) } return res.WriteDocumentation( - io.Out, + io.Out(), &doc.WriteDocumentationOptions{ ShowAll: cfg.all, Source: cfg.src, diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 7acd9877770..ad223afa3b0 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -19,7 +19,7 @@ type lintCfg struct { // auto-fix: apply suggested fixes automatically. } -func newLintCmd(io *commands.IO) *commands.Command { +func newLintCmd(io commands.IO) *commands.Command { cfg := &lintCfg{} return commands.NewCommand( @@ -41,7 +41,7 @@ func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) { fs.IntVar(&c.setExitStatus, "set-exit-status", 1, "set exit status to 1 if any issues are found") } -func execLint(cfg *lintCfg, args []string, io *commands.IO) error { +func execLint(cfg *lintCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp } @@ -62,12 +62,12 @@ func execLint(cfg *lintCfg, args []string, io *commands.IO) error { hasError := false addIssue := func(issue lintIssue) { hasError = true - fmt.Fprint(io.Err, issue.String()+"\n") + fmt.Fprint(io.Err(), issue.String()+"\n") } for _, pkgPath := range pkgPaths { if verbose { - fmt.Fprintf(io.Err, "Linting %q...\n", pkgPath) + fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath) } // 'gno.mod' exists? diff --git a/gnovm/cmd/gno/main.go b/gnovm/cmd/gno/main.go index 433db703b90..c84aa52fc86 100644 --- a/gnovm/cmd/gno/main.go +++ b/gnovm/cmd/gno/main.go @@ -18,7 +18,7 @@ func main() { } } -func newGnocliCmd(io *commands.IO) *commands.Command { +func newGnocliCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ ShortUsage: " [flags] [...]", diff --git a/gnovm/cmd/gno/mod.go b/gnovm/cmd/gno/mod.go index 9fb5dd704ce..da8db3ed68a 100644 --- a/gnovm/cmd/gno/mod.go +++ b/gnovm/cmd/gno/mod.go @@ -21,7 +21,7 @@ type modDownloadCfg struct { verbose bool } -func newModCmd(io *commands.IO) *commands.Command { +func newModCmd(io commands.IO) *commands.Command { cmd := commands.NewCommand( commands.Metadata{ Name: "mod", @@ -41,7 +41,7 @@ func newModCmd(io *commands.IO) *commands.Command { return cmd } -func newModDownloadCmd(io *commands.IO) *commands.Command { +func newModDownloadCmd(io commands.IO) *commands.Command { cfg := &modDownloadCfg{} return commands.NewCommand( @@ -71,7 +71,7 @@ func newModInitCmd() *commands.Command { ) } -func newModTidy(io *commands.IO) *commands.Command { +func newModTidy(io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "tidy", @@ -101,7 +101,7 @@ func (c *modDownloadCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execModDownload(cfg *modDownloadCfg, args []string, io *commands.IO) error { +func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp } @@ -172,7 +172,7 @@ func execModInit(args []string) error { return nil } -func execModTidy(args []string, io *commands.IO) error { +func execModTidy(args []string, io commands.IO) error { if len(args) > 0 { return flag.ErrHelp } diff --git a/gnovm/cmd/gno/precompile.go b/gnovm/cmd/gno/precompile.go index 7e895109706..4a4528001a2 100644 --- a/gnovm/cmd/gno/precompile.go +++ b/gnovm/cmd/gno/precompile.go @@ -47,7 +47,7 @@ func (p *precompileOptions) markAsPrecompiled(pkg importPath) { p.precompiled[pkg] = struct{}{} } -func newPrecompileCmd(io *commands.IO) *commands.Command { +func newPrecompileCmd(io commands.IO) *commands.Command { cfg := &precompileCfg{} return commands.NewCommand( @@ -107,7 +107,7 @@ func (c *precompileCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execPrecompile(cfg *precompileCfg, args []string, io *commands.IO) error { +func execPrecompile(cfg *precompileCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp } diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 2c8d8c77350..d37c86df502 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -20,7 +20,7 @@ type runCfg struct { expr string } -func newRunCmd(io *commands.IO) *commands.Command { +func newRunCmd(io commands.IO) *commands.Command { cfg := &runCfg{} return commands.NewCommand( @@ -59,7 +59,7 @@ func (c *runCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execRun(cfg *runCfg, args []string, io *commands.IO) error { +func execRun(cfg *runCfg, args []string, io commands.IO) error { if len(args) == 0 { return flag.ErrHelp } @@ -68,9 +68,9 @@ func execRun(cfg *runCfg, args []string, io *commands.IO) error { cfg.rootDir = guessRootDir() } - stdin := io.In - stdout := io.Out - stderr := io.Err + stdin := io.In() + stdout := io.Out() + stderr := io.Err() // init store and machine testStore := tests.TestStore(cfg.rootDir, diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index 85fe3f7ee7d..718235c1390 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -37,7 +37,7 @@ type testCfg struct { withNativeFallback bool } -func newTestCmd(io *commands.IO) *commands.Command { +func newTestCmd(io commands.IO) *commands.Command { cfg := &testCfg{} return commands.NewCommand( @@ -157,7 +157,7 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execTest(cfg *testCfg, args []string, io *commands.IO) error { +func execTest(cfg *testCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp } @@ -279,7 +279,7 @@ func gnoTestPkg( unittestFiles, filetestFiles []string, cfg *testCfg, - io *commands.IO, + io commands.IO, ) error { var ( verbose = cfg.verbose @@ -287,9 +287,9 @@ func gnoTestPkg( runFlag = cfg.run printRuntimeMetrics = cfg.printRuntimeMetrics - stdin = io.In - stdout = io.Out - stderr = io.Err + stdin = io.In() + stdout = io.Out() + stderr = io.Err() errs error ) @@ -415,7 +415,7 @@ func runTestFiles( verbose bool, printRuntimeMetrics bool, runFlag string, - io *commands.IO, + io commands.IO, ) error { var errs error diff --git a/tm2/pkg/commands/io.go b/tm2/pkg/commands/io.go index b23455e4602..fc0d3378a88 100644 --- a/tm2/pkg/commands/io.go +++ b/tm2/pkg/commands/io.go @@ -9,21 +9,42 @@ import ( // IO holds settable command // input, output and error buffers -type IO struct { - In io.Reader +type IO interface { + // getters + In() io.Reader + Out() io.WriteCloser + Err() io.WriteCloser + + // setters and helpers + SetIn(in io.Reader) + SetOut(out io.WriteCloser) + SetErr(err io.WriteCloser) + Println(args ...interface{}) + Printf(format string, args ...interface{}) + Printfln(format string, args ...interface{}) + ErrPrintln(args ...interface{}) + ErrPrintfln(format string, args ...interface{}) + GetCheckPassword(prompts [2]string, insecure bool) (string, error) + GetConfirmation(prompt string) (bool, error) + GetPassword(prompt string, insecure bool) (string, error) + GetString(prompt string) (string, error) +} + +type IOImpl struct { + in io.Reader inBuf *bufio.Reader - Out io.WriteCloser + out io.WriteCloser outBuf *bufio.Writer - Err io.WriteCloser + err io.WriteCloser errBuf *bufio.Writer } // NewDefaultIO returns a default command io // that utilizes standard input / output / error -func NewDefaultIO() *IO { - c := &IO{} +func NewDefaultIO() IO { + c := &IOImpl{} c.SetIn(os.Stdin) c.SetOut(os.Stdout) @@ -34,17 +55,21 @@ func NewDefaultIO() *IO { // NewTestIO returns a test command io // that only sets standard input (to avoid panics) -func NewTestIO() *IO { - c := &IO{} +func NewTestIO() IO { + c := &IOImpl{} c.SetIn(os.Stdin) return c } +func (io *IOImpl) In() io.Reader { return io.in } +func (io *IOImpl) Out() io.WriteCloser { return io.out } +func (io *IOImpl) Err() io.WriteCloser { return io.err } + // SetIn sets the input reader for the command io -func (io *IO) SetIn(in io.Reader) { - io.In = in - if inbuf, ok := io.In.(*bufio.Reader); ok { +func (io *IOImpl) SetIn(in io.Reader) { + io.in = in + if inbuf, ok := io.in.(*bufio.Reader); ok { io.inBuf = inbuf return @@ -54,19 +79,19 @@ func (io *IO) SetIn(in io.Reader) { } // SetOut sets the output writer for the command io -func (io *IO) SetOut(out io.WriteCloser) { - io.Out = out - io.outBuf = bufio.NewWriter(io.Out) +func (io *IOImpl) SetOut(out io.WriteCloser) { + io.out = out + io.outBuf = bufio.NewWriter(io.out) } // SetErr sets the error writer for the command io -func (io *IO) SetErr(err io.WriteCloser) { - io.Err = err - io.errBuf = bufio.NewWriter(io.Err) +func (io *IOImpl) SetErr(err io.WriteCloser) { + io.err = err + io.errBuf = bufio.NewWriter(io.err) } // Println prints a line terminated by a newline -func (io *IO) Println(args ...interface{}) { +func (io *IOImpl) Println(args ...interface{}) { if io.outBuf == nil { return } @@ -76,7 +101,7 @@ func (io *IO) Println(args ...interface{}) { } // Printf prints a formatted string without trailing newline -func (io *IO) Printf(format string, args ...interface{}) { +func (io *IOImpl) Printf(format string, args ...interface{}) { if io.outBuf == nil { return } @@ -86,7 +111,7 @@ func (io *IO) Printf(format string, args ...interface{}) { } // Printfln prints a formatted string terminated by a newline -func (io *IO) Printfln(format string, args ...interface{}) { +func (io *IOImpl) Printfln(format string, args ...interface{}) { if io.outBuf == nil { return } @@ -97,7 +122,7 @@ func (io *IO) Printfln(format string, args ...interface{}) { // ErrPrintln prints a line terminated by a newline to // cmd.Err(Buf) -func (io *IO) ErrPrintln(args ...interface{}) { +func (io *IOImpl) ErrPrintln(args ...interface{}) { if io.errBuf == nil { return } @@ -107,7 +132,7 @@ func (io *IO) ErrPrintln(args ...interface{}) { } // ErrPrintfln prints a formatted string terminated by a newline to cmd.Err(Buf) -func (io *IO) ErrPrintfln(format string, args ...interface{}) { +func (io *IOImpl) ErrPrintfln(format string, args ...interface{}) { if io.errBuf == nil { return } diff --git a/tm2/pkg/commands/utils.go b/tm2/pkg/commands/utils.go index c7ce19e8138..90b6fa12b64 100644 --- a/tm2/pkg/commands/utils.go +++ b/tm2/pkg/commands/utils.go @@ -9,7 +9,7 @@ import ( ) // GetPassword fetches the password using the provided prompt, if any -func (io *IO) GetPassword( +func (io *IOImpl) GetPassword( prompt string, insecure bool, ) (string, error) { @@ -27,7 +27,7 @@ func (io *IO) GetPassword( } // readLine reads a new line from standard input -func (io *IO) readLine() (string, error) { +func (io *IOImpl) readLine() (string, error) { input, err := io.inBuf.ReadString('\n') if err != nil { return "", err @@ -52,7 +52,7 @@ func readPassword() (string, error) { // GetConfirmation will request user give the confirmation from stdin. // "y", "Y", "yes", "YES", and "Yes" all count as confirmations. // If the input is not recognized, it returns false and a nil error. -func (io *IO) GetConfirmation(prompt string) (bool, error) { +func (io *IOImpl) GetConfirmation(prompt string) (bool, error) { // On stderr so it isn't part of bash output. io.ErrPrintfln("%s [y/n]:", prompt) @@ -78,7 +78,7 @@ func (io *IO) GetConfirmation(prompt string) (bool, error) { // match (for creating a new password). // It enforces the password length. Only parses password once if // input is piped in. -func (io *IO) GetCheckPassword( +func (io *IOImpl) GetCheckPassword( prompts [2]string, insecure bool, ) (string, error) { @@ -100,7 +100,7 @@ func (io *IO) GetCheckPassword( } // GetString simply returns the trimmed string output of a given reader. -func (io *IO) GetString(prompt string) (string, error) { +func (io *IOImpl) GetString(prompt string) (string, error) { if prompt != "" { // On stderr so it isn't part of bash output. io.ErrPrintln(prompt) diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index c90dfc9f803..a4089316e92 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -29,7 +29,7 @@ type addCfg struct { index uint64 } -func newAddCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newAddCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &addCfg{ rootCfg: rootCfg, } @@ -131,7 +131,7 @@ input output - armor encrypted private key (saved to file) */ -func execAdd(cfg *addCfg, args []string, io *commands.IO) error { +func execAdd(cfg *addCfg, args []string, io commands.IO) error { var ( kb keys.Keybase err error @@ -280,7 +280,7 @@ func execAdd(cfg *addCfg, args []string, io *commands.IO) error { return printCreate(info, showMnemonic, mnemonic, io) } -func printCreate(info keys.Info, showMnemonic bool, mnemonic string, io *commands.IO) error { +func printCreate(info keys.Info, showMnemonic bool, mnemonic string, io commands.IO) error { io.Println("") printNewInfo(info, io) @@ -296,7 +296,7 @@ It is the only way to recover your account if you ever forget your password. return nil } -func printNewInfo(info keys.Info, io *commands.IO) { +func printNewInfo(info keys.Info, io commands.IO) { keyname := info.GetName() keytype := info.GetType() keypub := info.GetPubKey() diff --git a/tm2/pkg/crypto/keys/client/addpkg.go b/tm2/pkg/crypto/keys/client/addpkg.go index 3de9a6de546..5bbd3f08ad0 100644 --- a/tm2/pkg/crypto/keys/client/addpkg.go +++ b/tm2/pkg/crypto/keys/client/addpkg.go @@ -24,7 +24,7 @@ type addPkgCfg struct { deposit string } -func newAddPkgCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { +func newAddPkgCmd(rootCfg *makeTxCfg, io commands.IO) *commands.Command { cfg := &addPkgCfg{ rootCfg: rootCfg, } @@ -65,7 +65,7 @@ func (c *addPkgCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execAddPkg(cfg *addPkgCfg, args []string, io *commands.IO) error { +func execAddPkg(cfg *addPkgCfg, args []string, io commands.IO) error { if cfg.pkgPath == "" { return errors.New("pkgpath not specified") } @@ -142,7 +142,7 @@ func signAndBroadcast( cfg *makeTxCfg, args []string, tx std.Tx, - io *commands.IO, + io commands.IO, ) error { baseopts := cfg.rootCfg txopts := cfg diff --git a/tm2/pkg/crypto/keys/client/broadcast.go b/tm2/pkg/crypto/keys/client/broadcast.go index f1d448495a6..9c05f2c43b3 100644 --- a/tm2/pkg/crypto/keys/client/broadcast.go +++ b/tm2/pkg/crypto/keys/client/broadcast.go @@ -23,7 +23,7 @@ type broadcastCfg struct { tx *std.Tx } -func newBroadcastCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newBroadcastCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &broadcastCfg{ rootCfg: rootCfg, } @@ -50,7 +50,7 @@ func (c *broadcastCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execBroadcast(cfg *broadcastCfg, args []string, io *commands.IO) error { +func execBroadcast(cfg *broadcastCfg, args []string, io commands.IO) error { if len(args) != 1 { return flag.ErrHelp } diff --git a/tm2/pkg/crypto/keys/client/call.go b/tm2/pkg/crypto/keys/client/call.go index 29fe9739a36..6f9c9d52f5f 100644 --- a/tm2/pkg/crypto/keys/client/call.go +++ b/tm2/pkg/crypto/keys/client/call.go @@ -22,7 +22,7 @@ type callCfg struct { args commands.StringArr } -func newCallCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { +func newCallCmd(rootCfg *makeTxCfg, io commands.IO) *commands.Command { cfg := &callCfg{ rootCfg: rootCfg, } @@ -69,7 +69,7 @@ func (c *callCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execCall(cfg *callCfg, args []string, io *commands.IO) error { +func execCall(cfg *callCfg, args []string, io commands.IO) error { if cfg.pkgPath == "" { return errors.New("pkgpath not specified") } diff --git a/tm2/pkg/crypto/keys/client/delete.go b/tm2/pkg/crypto/keys/client/delete.go index e22ac30988c..cf65b8fc60a 100644 --- a/tm2/pkg/crypto/keys/client/delete.go +++ b/tm2/pkg/crypto/keys/client/delete.go @@ -16,7 +16,7 @@ type deleteCfg struct { force bool } -func newDeleteCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newDeleteCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &deleteCfg{ rootCfg: rootCfg, } @@ -50,7 +50,7 @@ func (c *deleteCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execDelete(cfg *deleteCfg, args []string, io *commands.IO) error { +func execDelete(cfg *deleteCfg, args []string, io commands.IO) error { if len(args) != 1 { return flag.ErrHelp } @@ -101,7 +101,7 @@ func execDelete(cfg *deleteCfg, args []string, io *commands.IO) error { return nil } -func confirmDeletion(io *commands.IO) error { +func confirmDeletion(io commands.IO) error { answer, err := io.GetConfirmation("Key reference will be deleted. Continue?") if err != nil { return err diff --git a/tm2/pkg/crypto/keys/client/export.go b/tm2/pkg/crypto/keys/client/export.go index 6eff8aa97b3..c3921c31fd7 100644 --- a/tm2/pkg/crypto/keys/client/export.go +++ b/tm2/pkg/crypto/keys/client/export.go @@ -18,7 +18,7 @@ type exportCfg struct { unsafe bool } -func newExportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newExportCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &exportCfg{ rootCfg: rootCfg, } @@ -59,7 +59,7 @@ func (c *exportCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execExport(cfg *exportCfg, io *commands.IO) error { +func execExport(cfg *exportCfg, io commands.IO) error { // Create a new instance of the key-base kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { diff --git a/tm2/pkg/crypto/keys/client/generate.go b/tm2/pkg/crypto/keys/client/generate.go index d209bd70bd3..04a0ea8947f 100644 --- a/tm2/pkg/crypto/keys/client/generate.go +++ b/tm2/pkg/crypto/keys/client/generate.go @@ -16,7 +16,7 @@ type generateCfg struct { customEntropy bool } -func newGenerateCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newGenerateCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &generateCfg{ rootCfg: rootCfg, } @@ -43,7 +43,7 @@ func (c *generateCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execGenerate(cfg *generateCfg, args []string, io *commands.IO) error { +func execGenerate(cfg *generateCfg, args []string, io commands.IO) error { customEntropy := cfg.customEntropy if len(args) != 0 { diff --git a/tm2/pkg/crypto/keys/client/import.go b/tm2/pkg/crypto/keys/client/import.go index e1d8af55861..e4f20ff6402 100644 --- a/tm2/pkg/crypto/keys/client/import.go +++ b/tm2/pkg/crypto/keys/client/import.go @@ -18,7 +18,7 @@ type importCfg struct { unsafe bool } -func newImportCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newImportCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &importCfg{ rootCfg: rootCfg, } @@ -59,7 +59,7 @@ func (c *importCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execImport(cfg *importCfg, io *commands.IO) error { +func execImport(cfg *importCfg, io commands.IO) error { // Create a new instance of the key-base kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.Home) if err != nil { diff --git a/tm2/pkg/crypto/keys/client/list.go b/tm2/pkg/crypto/keys/client/list.go index cb86feb2395..bdee6b3bbe9 100644 --- a/tm2/pkg/crypto/keys/client/list.go +++ b/tm2/pkg/crypto/keys/client/list.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto/keys" ) -func newListCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newListCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { return commands.NewCommand( commands.Metadata{ Name: "list", @@ -22,7 +22,7 @@ func newListCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { ) } -func execList(cfg *baseCfg, args []string, io *commands.IO) error { +func execList(cfg *baseCfg, args []string, io commands.IO) error { if len(args) != 0 { return flag.ErrHelp } @@ -40,7 +40,7 @@ func execList(cfg *baseCfg, args []string, io *commands.IO) error { return err } -func printInfos(infos []keys.Info, io *commands.IO) { +func printInfos(infos []keys.Info, io commands.IO) { for i, info := range infos { keyname := info.GetName() keytype := info.GetType() diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index 36214a5a983..e3fe89b930d 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -17,7 +17,7 @@ type makeTxCfg struct { chainID string } -func newMakeTxCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newMakeTxCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &makeTxCfg{ rootCfg: rootCfg, } diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 8cc5757aba7..746048e772c 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -21,7 +21,7 @@ type queryCfg struct { path string } -func newQueryCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newQueryCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &queryCfg{ rootCfg: rootCfg, } @@ -62,7 +62,7 @@ func (c *queryCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execQuery(cfg *queryCfg, args []string, io *commands.IO) error { +func execQuery(cfg *queryCfg, args []string, io commands.IO) error { if len(args) != 1 { return flag.ErrHelp } diff --git a/tm2/pkg/crypto/keys/client/root.go b/tm2/pkg/crypto/keys/client/root.go index 550dd408b77..61fd7d077b5 100644 --- a/tm2/pkg/crypto/keys/client/root.go +++ b/tm2/pkg/crypto/keys/client/root.go @@ -18,7 +18,7 @@ type baseCfg struct { BaseOptions } -func NewRootCmd(io *commands.IO) *commands.Command { +func NewRootCmd(io commands.IO) *commands.Command { cfg := &baseCfg{} cmd := commands.NewCommand( diff --git a/tm2/pkg/crypto/keys/client/send.go b/tm2/pkg/crypto/keys/client/send.go index 6d19ffcb393..a5098aea08c 100644 --- a/tm2/pkg/crypto/keys/client/send.go +++ b/tm2/pkg/crypto/keys/client/send.go @@ -21,7 +21,7 @@ type sendCfg struct { to string } -func newSendCmd(rootCfg *makeTxCfg, io *commands.IO) *commands.Command { +func newSendCmd(rootCfg *makeTxCfg, io commands.IO) *commands.Command { cfg := &sendCfg{ rootCfg: rootCfg, } @@ -55,7 +55,7 @@ func (c *sendCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execSend(cfg *sendCfg, args []string, io *commands.IO) error { +func execSend(cfg *sendCfg, args []string, io commands.IO) error { if len(args) != 1 { return flag.ErrHelp } diff --git a/tm2/pkg/crypto/keys/client/sign.go b/tm2/pkg/crypto/keys/client/sign.go index 761e0d7a563..f8fcc02fdde 100644 --- a/tm2/pkg/crypto/keys/client/sign.go +++ b/tm2/pkg/crypto/keys/client/sign.go @@ -28,7 +28,7 @@ type signCfg struct { pass string } -func newSignCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newSignCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &signCfg{ rootCfg: rootCfg, } @@ -83,7 +83,7 @@ func (c *signCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execSign(cfg *signCfg, args []string, io *commands.IO) error { +func execSign(cfg *signCfg, args []string, io commands.IO) error { var err error if len(args) != 1 { diff --git a/tm2/pkg/crypto/keys/client/verify.go b/tm2/pkg/crypto/keys/client/verify.go index bb486c1a8fa..fff2fcd852f 100644 --- a/tm2/pkg/crypto/keys/client/verify.go +++ b/tm2/pkg/crypto/keys/client/verify.go @@ -16,7 +16,7 @@ type verifyCfg struct { docPath string } -func newVerifyCmd(rootCfg *baseCfg, io *commands.IO) *commands.Command { +func newVerifyCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { cfg := &verifyCfg{ rootCfg: rootCfg, } @@ -43,7 +43,7 @@ func (c *verifyCfg) RegisterFlags(fs *flag.FlagSet) { ) } -func execVerify(cfg *verifyCfg, args []string, io *commands.IO) error { +func execVerify(cfg *verifyCfg, args []string, io commands.IO) error { var ( kb keys.Keybase err error From 09d7c5291ca847990d7cac641562562b85f0338e Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Thu, 9 Nov 2023 18:43:34 +0100 Subject: [PATCH 73/93] chore: Add Keybase HasByNameOrAddress, HasByName and HasByAddress (#1313) The Keybase [interface has](https://github.com/gnolang/gno/blob/7105d00e10209003ea41fbae7a4d7c463c4167c3/tm2/pkg/crypto/keys/types.go#L16C2-L18) `GetByNameOrAddress`, `GetByName` and `GetByAddress`. If an application simply wants to check if the key is in the key base, it must use one of these and check for an error, as is done [in the gnokey command line](https://github.com/gnolang/gno/blob/7105d00e10209003ea41fbae7a4d7c463c4167c3/tm2/pkg/crypto/keys/client/add.go#L159-L160). ``` _, err = kb.GetByName(name) if err == nil { // account exists, ask for user confirmation ... } ``` A GnoMobile app also needs to check for the key. But it's expensive to return the entire key info. The byte buffer must be copies multiple times, in the Keybase and in the gRPC interface, and the key info structure must be reformatted to Protobuf and then to the application's language. And not preferable to generate errors to pass information in normal program flow. In the underlying `DB` interface, there is already a [Has function](https://github.com/gnolang/gno/blob/7105d00e10209003ea41fbae7a4d7c463c4167c3/tm2/pkg/db/types.go#L13). This PR updates the `Keybase` with the related functions `HasByNameOrAddress`, `HasByName` and `HasByAddress` which use `Has` to inexpensively return a simple bool. It also updates the gnokey command line to use `HasByName` to be more clear: ``` if has, err := kb.HasByName(name); err == nil && has { // account exists, ask for user confirmation ... } ``` --------- Signed-off-by: Jeff Thompson Co-authored-by: Morgan Bazalgette --- tm2/pkg/crypto/keys/client/add.go | 3 +-- tm2/pkg/crypto/keys/keybase.go | 22 +++++++++++++++++++-- tm2/pkg/crypto/keys/keybase_test.go | 27 +++++++++++++++++--------- tm2/pkg/crypto/keys/lazy_keybase.go | 30 +++++++++++++++++++++++++++++ tm2/pkg/crypto/keys/types.go | 3 +++ 5 files changed, 72 insertions(+), 13 deletions(-) diff --git a/tm2/pkg/crypto/keys/client/add.go b/tm2/pkg/crypto/keys/client/add.go index a4089316e92..71dc6f03090 100644 --- a/tm2/pkg/crypto/keys/client/add.go +++ b/tm2/pkg/crypto/keys/client/add.go @@ -156,8 +156,7 @@ func execAdd(cfg *addCfg, args []string, io commands.IO) error { return err } - _, err = kb.GetByName(name) - if err == nil { + if has, err := kb.HasByName(name); err == nil && has { // account exists, ask for user confirmation response, err2 := io.GetConfirmation(fmt.Sprintf("Override the existing name %s", name)) if err2 != nil { diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 06eaf54f503..31b0012c433 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -160,14 +160,32 @@ func (kb dbKeybase) List() ([]Info, error) { return res, nil } +// HasByNameOrAddress checks if a key with the name or bech32 string address is in the keybase. +func (kb dbKeybase) HasByNameOrAddress(nameOrBech32 string) (bool, error) { + address, err := crypto.AddressFromBech32(nameOrBech32) + if err != nil { + return kb.HasByName(nameOrBech32) + } + return kb.HasByAddress(address) +} + +// HasByName checks if a key with the name is in the keybase. +func (kb dbKeybase) HasByName(name string) (bool, error) { + return kb.db.Has(infoKey(name)), nil +} + +// HasByAddress checks if a key with the address is in the keybase. +func (kb dbKeybase) HasByAddress(address crypto.Address) (bool, error) { + return kb.db.Has(addrKey(address)), nil +} + // Get returns the public information about one key. func (kb dbKeybase) GetByNameOrAddress(nameOrBech32 string) (Info, error) { addr, err := crypto.AddressFromBech32(nameOrBech32) if err != nil { return kb.GetByName(nameOrBech32) - } else { - return kb.GetByAddress(addr) } + return kb.GetByAddress(addr) } func (kb dbKeybase) GetByName(name string) (Info, error) { diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index e80c6c3cd03..d7660ac38f1 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -89,8 +89,9 @@ func TestKeyManagement(t *testing.T) { assert.Empty(t, l) // create some keys - _, err = cstore.GetByName(n1) - require.Error(t, err) + has, err := cstore.HasByName(n1) + require.NoError(t, err) + require.False(t, has) i, err := cstore.CreateAccount(n1, mn1, bip39Passphrase, p1, 0, 0) require.NoError(t, err) require.Equal(t, n1, i.GetName()) @@ -100,10 +101,16 @@ func TestKeyManagement(t *testing.T) { // we can get these keys i2, err := cstore.GetByName(n2) require.NoError(t, err) - _, err = cstore.GetByName(n3) - require.NotNil(t, err) - _, err = cstore.GetByAddress(toAddr(i2)) + has, err = cstore.HasByName(n3) + require.NoError(t, err) + require.False(t, has) + has, err = cstore.HasByAddress(toAddr(i2)) + require.NoError(t, err) + require.True(t, has) + // Also check with HasByNameOrAddress + has, err = cstore.HasByNameOrAddress(crypto.AddressToBech32(toAddr(i2))) require.NoError(t, err) + require.True(t, has) addr, err := crypto.AddressFromBech32("g1frtkxv37nq7arvyz5p0mtjqq7hwuvd4dnt892p") require.NoError(t, err) _, err = cstore.GetByAddress(addr) @@ -127,8 +134,9 @@ func TestKeyManagement(t *testing.T) { keyS, err = cstore.List() require.NoError(t, err) require.Equal(t, 1, len(keyS)) - _, err = cstore.GetByName(n1) - require.Error(t, err) + has, err = cstore.HasByName(n1) + require.NoError(t, err) + require.False(t, has) // create an offline key o1 := "offline" @@ -388,8 +396,9 @@ func TestSeedPhrase(t *testing.T) { // now, let us delete this key err = cstore.Delete(n1, p1, false) require.Nil(t, err, "%+v", err) - _, err = cstore.GetByName(n1) - require.NotNil(t, err) + has, err := cstore.HasByName(n1) + require.NoError(t, err) + require.False(t, has) } func ExampleNew() { diff --git a/tm2/pkg/crypto/keys/lazy_keybase.go b/tm2/pkg/crypto/keys/lazy_keybase.go index f7f9e229980..62e88d9a8e2 100644 --- a/tm2/pkg/crypto/keys/lazy_keybase.go +++ b/tm2/pkg/crypto/keys/lazy_keybase.go @@ -37,6 +37,36 @@ func (lkb lazyKeybase) List() ([]Info, error) { return NewDBKeybase(db).List() } +func (lkb lazyKeybase) HasByNameOrAddress(nameOrBech32 string) (bool, error) { + db, err := db.NewDB(lkb.name, dbBackend, lkb.dir) + if err != nil { + return false, err + } + defer db.Close() + + return NewDBKeybase(db).HasByNameOrAddress(nameOrBech32) +} + +func (lkb lazyKeybase) HasByName(name string) (bool, error) { + db, err := db.NewDB(lkb.name, dbBackend, lkb.dir) + if err != nil { + return false, err + } + defer db.Close() + + return NewDBKeybase(db).HasByName(name) +} + +func (lkb lazyKeybase) HasByAddress(address crypto.Address) (bool, error) { + db, err := db.NewDB(lkb.name, dbBackend, lkb.dir) + if err != nil { + return false, err + } + defer db.Close() + + return NewDBKeybase(db).HasByAddress(address) +} + func (lkb lazyKeybase) GetByNameOrAddress(nameOrBech32 string) (Info, error) { db, err := db.NewDB(lkb.name, dbBackend, lkb.dir) if err != nil { diff --git a/tm2/pkg/crypto/keys/types.go b/tm2/pkg/crypto/keys/types.go index bba3a917b69..c5d33023a0a 100644 --- a/tm2/pkg/crypto/keys/types.go +++ b/tm2/pkg/crypto/keys/types.go @@ -13,6 +13,9 @@ import ( type Keybase interface { // CRUD on the keystore List() ([]Info, error) + HasByNameOrAddress(nameOrBech32 string) (bool, error) + HasByName(name string) (bool, error) + HasByAddress(address crypto.Address) (bool, error) GetByNameOrAddress(nameOrBech32 string) (Info, error) GetByName(name string) (Info, error) GetByAddress(address crypto.Address) (Info, error) From 117bcd1fac5c3db5e828046de25306baa4bf6eac Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Thu, 9 Nov 2023 18:48:07 +0100 Subject: [PATCH 74/93] feat: add p/demo/pausable (#1328) ## Description This PR adds the `Pausable` package to p/demo, allowing realms to have pausability. It relies on the already existing `Ownable` package for access control, #1314. Inspired by OpenZeppelin's [Pausable](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.3/contracts/security/Pausable.sol).
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] 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 Bazalgette --- examples/gno.land/p/demo/pausable/gno.mod | 3 + .../gno.land/p/demo/pausable/pausable.gno | 49 ++++++++++++ .../p/demo/pausable/pausable_test.gno | 77 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 examples/gno.land/p/demo/pausable/gno.mod create mode 100644 examples/gno.land/p/demo/pausable/pausable.gno create mode 100644 examples/gno.land/p/demo/pausable/pausable_test.gno diff --git a/examples/gno.land/p/demo/pausable/gno.mod b/examples/gno.land/p/demo/pausable/gno.mod new file mode 100644 index 00000000000..08c7a4f7e5f --- /dev/null +++ b/examples/gno.land/p/demo/pausable/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/pausable + +require gno.land/p/demo/ownable v0.0.0-latest diff --git a/examples/gno.land/p/demo/pausable/pausable.gno b/examples/gno.land/p/demo/pausable/pausable.gno new file mode 100644 index 00000000000..eae3456ba61 --- /dev/null +++ b/examples/gno.land/p/demo/pausable/pausable.gno @@ -0,0 +1,49 @@ +package pausable + +import "gno.land/p/demo/ownable" + +type Pausable struct { + *ownable.Ownable + paused bool +} + +// New returns a new Pausable struct with non-paused state as default +func New() *Pausable { + return &Pausable{ + Ownable: ownable.New(), + paused: false, + } +} + +// NewFromOwnable is the same as New, but with a pre-existing top-level ownable +func NewFromOwnable(ownable *ownable.Ownable) *Pausable { + return &Pausable{ + Ownable: ownable, + paused: false, + } +} + +// IsPaused checks if Pausable is paused +func (p Pausable) IsPaused() bool { + return p.paused +} + +// Pause sets the state of Pausable to true, meaning all pausable functions are paused +func (p *Pausable) Pause() error { + if err := p.CallerIsOwner(); err != nil { + return err + } + + p.paused = true + return nil +} + +// Unpause sets the state of Pausable to false, meaning all pausable functions are resumed +func (p *Pausable) Unpause() error { + if err := p.CallerIsOwner(); err != nil { + return err + } + + p.paused = false + return nil +} diff --git a/examples/gno.land/p/demo/pausable/pausable_test.gno b/examples/gno.land/p/demo/pausable/pausable_test.gno new file mode 100644 index 00000000000..cc95c457573 --- /dev/null +++ b/examples/gno.land/p/demo/pausable/pausable_test.gno @@ -0,0 +1,77 @@ +package pausable + +import ( + "std" + "testing" + + "gno.land/p/demo/ownable" +) + +var ( + firstCaller = std.Address("g1l9aypkr8xfvs82zeux486ddzec88ty69lue9de") + secondCaller = std.Address("g127jydsh6cms3lrtdenydxsckh23a8d6emqcvfa") +) + +func TestNew(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + + if result.paused != false { + t.Fatalf("Expected result to be unpaused, got %t\n", result.paused) + } + + if result.Owner() != firstCaller { + t.Fatalf("Expected %s, got %s\n", firstCaller, result.Owner()) + } +} + +func TestNewFromOwnable(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + o := ownable.New() + + std.TestSetOrigCaller(secondCaller) + result := NewFromOwnable(o) + + if result.Owner() != firstCaller { + t.Fatalf("Expected %s, got %s\n", firstCaller, result.Owner()) + } +} + +func TestSetUnpaused(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + result.Unpause() + + if result.IsPaused() { + t.Fatalf("Expected result to be unpaused, got %t\n", result.IsPaused()) + } +} + +func TestSetPaused(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + result.Pause() + + if !result.IsPaused() { + t.Fatalf("Expected result to be paused, got %t\n", result.IsPaused()) + } +} + +func TestIsPaused(t *testing.T) { + std.TestSetOrigCaller(firstCaller) + + result := New() + + if result.IsPaused() { + t.Fatalf("Expected result to be unpaused, got %t\n", result.IsPaused()) + } + + result.Pause() + + if !result.IsPaused() { + t.Fatalf("Expected result to be paused, got %t\n", result.IsPaused()) + } +} From 9c8f2483a863bdb15f7668bcf6fdc08e4c41c06a Mon Sep 17 00:00:00 2001 From: Morgan Date: Thu, 9 Nov 2023 18:52:26 +0100 Subject: [PATCH 75/93] docs: update Go<>Gno compatibility document with up-to-date info (#1311) --- gnovm/docs/go-gno-compatibility.md | 515 +++++++++++++---------------- 1 file changed, 226 insertions(+), 289 deletions(-) diff --git a/gnovm/docs/go-gno-compatibility.md b/gnovm/docs/go-gno-compatibility.md index a39cec533f4..6ff433c056f 100644 --- a/gnovm/docs/go-gno-compatibility.md +++ b/gnovm/docs/go-gno-compatibility.md @@ -1,11 +1,7 @@ # Go<>Gno compatibility -**WIP: does not reflect the current state yet.** - ## Native keywords -Legend: full, partial, missing, TBD. - | keyword | support | |-------------|------------------------| | break | full | @@ -32,22 +28,27 @@ Legend: full, partial, missing, TBD. | type | full | | var | full | +Generics are currently not implemented. + ## Native types | type | usage | persistency | |-----------------------------------------------|------------------------|------------------------------------------------------------| | `bool` | full | full | | `byte` | full | full | -| `float32`, `float64` | full | full | | `int`, `int8`, `int16`, `int32`, `int64` | full | full | | `uint`, `uint8`, `uint16`, `uint32`, `uint64` | full | full | +| `float32`, `float64` | full | full | +| `complex64`, `complex128` | missing (TBD) | missing | +| `uintptr`, `unsafe.Pointer` | missing | missing | | `string` | full | full | | `rune` | full | full | | `interface{}` | full | full | -| `[]T` (slices) | full | full* | -| `map[T1]T2` | full | full* | +| `[]T` (slices) | full | full\* | +| `[N]T` (arrays) | full | full\* | +| `map[T1]T2` | full | full\* | | `func (T1...) T2...` | full | full (needs more tests) | -| `*T` (pointers) | full | full* | +| `*T` (pointers) | full | full\* | | `chan T` (channels) | missing (after launch) | missing (after launch) | **\*:** depends on `T`/`T1`/`T2` @@ -62,286 +63,221 @@ Additional native types: ## Stdlibs - +Legend: + +* `nondet`: the standard library in question would require non-deterministic + behaviour to implement as in Go, such as cryptographical randomness or + os/network access. The library may still be implemented at one point, with a + different API. +* `gospec`: the standard library is very Go-specific -- for instance, it is used + for debugging information or for parsing/build Go source code. A Gno version + may exist at one point, likely with a different package name or semantics. +* `gnics`: the standard library requires generics. +* `test`: the standard library is currently available for use exclusively in + test contexts, and may have limited functionality. +* `cmd`: the Go standard library is a command -- a direct equivalent in Gno + would not be useful. +* `tbd`: whether to include the standard library or not is still up for + discussion. +* `todo`: the standard libary is to be added, and + [contributions are welcome.](https://github.com/gnolang/gno/issues/1267) +* `part`: the standard library is partially implemented in Gno, and contributions are + welcome to add the missing functionality. +* `full`: the standard library is fully implemented in Gno. + + | package | status | |---------------------------------------------|----------| -| archive/tar | TBD | -| archive/zip | TBD | -| arena | TBD | -| bufio | TBD | -| builtin | TBD | -| bytes | TBD | -| cmd/addr2line | TBD | -| cmd/api | TBD | -| cmd/api/testdata/src/issue21181/dep | TBD | -| cmd/api/testdata/src/issue21181/indirect | TBD | -| cmd/api/testdata/src/issue21181/p | TBD | -| cmd/api/testdata/src/pkg/p1 | TBD | -| cmd/api/testdata/src/pkg/p2 | TBD | -| cmd/api/testdata/src/pkg/p3 | TBD | -| cmd/api/testdata/src/pkg/p4 | TBD | -| cmd/asm | TBD | -| cmd/buildid | TBD | -| cmd/cgo | TBD | -| cmd/compile | TBD | -| cmd/covdata | TBD | -| cmd/covdata/testdata | TBD | -| cmd/cover | TBD | -| cmd/cover/testdata | TBD | -| cmd/cover/testdata/html | TBD | -| cmd/cover/testdata/pkgcfg/a | TBD | -| cmd/cover/testdata/pkgcfg/b | TBD | -| cmd/cover/testdata/pkgcfg/main | TBD | -| cmd/dist | TBD | -| cmd/distpack | TBD | -| cmd/doc | TBD | -| cmd/doc/testdata | TBD | -| cmd/doc/testdata/merge | TBD | -| cmd/doc/testdata/nested | TBD | -| cmd/doc/testdata/nested/empty | TBD | -| cmd/doc/testdata/nested/nested | TBD | -| cmd/fix | TBD | -| cmd/go | TBD | -| cmd/gofmt | TBD | -| cmd/go/testdata | TBD | -| cmd/link | TBD | -| cmd/link/testdata/pe-binutils | TBD | -| cmd/link/testdata/pe-llvm | TBD | -| cmd/link/testdata/testBuildFortvOS | TBD | -| cmd/link/testdata/testHashedSyms | TBD | -| cmd/link/testdata/testIndexMismatch | TBD | -| cmd/link/testdata/testRO | TBD | -| cmd/nm | TBD | -| cmd/objdump | TBD | -| cmd/objdump/testdata | TBD | -| cmd/objdump/testdata/testfilenum | TBD | -| cmd/pack | TBD | -| cmd/pprof | TBD | -| cmd/pprof/testdata | TBD | -| cmd/test2json | TBD | -| cmd/trace | TBD | -| cmd/vet | TBD | -| cmd/vet/testdata/asm | TBD | -| cmd/vet/testdata/assign | TBD | -| cmd/vet/testdata/atomic | TBD | -| cmd/vet/testdata/bool | TBD | -| cmd/vet/testdata/buildtag | TBD | -| cmd/vet/testdata/cgo | TBD | -| cmd/vet/testdata/composite | TBD | -| cmd/vet/testdata/copylock | TBD | -| cmd/vet/testdata/deadcode | TBD | -| cmd/vet/testdata/directive | TBD | -| cmd/vet/testdata/httpresponse | TBD | -| cmd/vet/testdata/lostcancel | TBD | -| cmd/vet/testdata/method | TBD | -| cmd/vet/testdata/nilfunc | TBD | -| cmd/vet/testdata/print | TBD | -| cmd/vet/testdata/rangeloop | TBD | -| cmd/vet/testdata/shift | TBD | -| cmd/vet/testdata/structtag | TBD | -| cmd/vet/testdata/tagtest | TBD | -| cmd/vet/testdata/testingpkg | TBD | -| cmd/vet/testdata/unmarshal | TBD | -| cmd/vet/testdata/unsafeptr | TBD | -| cmd/vet/testdata/unused | TBD | -| compress/bzip2 | TBD | -| compress/flate | TBD | -| compress/gzip | TBD | -| compress/lzw | TBD | -| compress/zlib | TBD | -| container/heap | TBD | -| container/list | TBD | -| container/ring | TBD | -| context | TBD | -| crypto | TBD | -| crypto/aes | TBD | -| crypto/boring | TBD | -| crypto/cipher | TBD | -| crypto/des | TBD | -| crypto/dsa | TBD | -| crypto/ecdh | TBD | -| crypto/ecdsa | TBD | -| crypto/ed25519 | TBD | -| crypto/elliptic | TBD | -| crypto/hmac | TBD | -| crypto/md5 | TBD | -| crypto/rand | TBD | -| crypto/rc4 | TBD | -| crypto/rsa | TBD | -| crypto/sha1 | TBD | -| crypto/sha256 | TBD | -| crypto/sha512 | TBD | -| crypto/subtle | TBD | -| crypto/tls | TBD | -| crypto/tls/fipsonly | TBD | -| crypto/x509 | TBD | -| crypto/x509/pkix | TBD | -| database/sql | TBD | -| database/sql/driver | TBD | -| debug/buildinfo | TBD | -| debug/dwarf | TBD | -| debug/elf | TBD | -| debug/gosym | TBD | -| debug/gosym/testdata | TBD | -| debug/macho | TBD | -| debug/pe | TBD | -| debug/plan9obj | TBD | -| embed | TBD | -| encoding | TBD | -| encoding/ascii85 | TBD | -| encoding/asn1 | TBD | -| encoding/base32 | TBD | -| encoding/base64 | TBD | -| encoding/binary | partial | -| encoding/csv | TBD | -| encoding/gob | TBD | -| encoding/hex | TBD | -| encoding/json | TBD | -| encoding/pem | TBD | -| encoding/xml | TBD | -| errors | TBD | -| expvar | TBD | -| flag | TBD | -| fmt | TBD | -| go/ast | TBD | -| go/build | TBD | -| go/build/constraint | TBD | -| go/build/testdata/alltags | TBD | -| go/build/testdata/cgo_disabled | TBD | -| go/build/testdata/directives | TBD | -| go/build/testdata/doc | TBD | -| go/build/testdata/multi | TBD | -| go/build/testdata/non_source_tags | TBD | -| go/build/testdata/other | TBD | -| go/build/testdata/other/file | TBD | -| go/constant | TBD | -| go/doc | TBD | -| go/doc/comment | TBD | -| go/doc/testdata | TBD | -| go/doc/testdata/examples | TBD | -| go/doc/testdata/pkgdoc | TBD | -| go/format | TBD | -| go/importer | TBD | -| go/parser | TBD | -| go/parser/testdata/goversion | TBD | -| go/parser/testdata/issue42951 | TBD | -| go/parser/testdata/issue42951/not_a_file.go | TBD | -| go/printer | TBD | -| go/printer/testdata | TBD | -| go/scanner | TBD | -| go/token | TBD | -| go/types | TBD | -| go/types/testdata | TBD | -| go/types/testdata/local | TBD | -| hash | partial | -| hash/adler32 | full | -| hash/crc32 | TBD | -| hash/crc64 | TBD | -| hash/fnv | TBD | -| hash/maphash | TBD | -| html | TBD | -| html/template | TBD | -| image | TBD | -| image/color | TBD | -| image/color/palette | TBD | -| image/draw | TBD | -| image/gif | TBD | -| image/jpeg | TBD | -| image/png | TBD | -| index/suffixarray | TBD | -| io | TBD | -| io/fs | TBD | -| io/ioutil | TBD | -| log | TBD | -| log/internal | TBD | -| log/slog | TBD | -| log/slog/internal | TBD | -| log/syslog | TBD | -| maps | TBD | -| math | partial | -| math/big | TBD | -| math/bits | TBD | -| math/cmplx | TBD | -| math/rand | TBD | -| mime | TBD | -| mime/multipart | TBD | -| mime/quotedprintable | TBD | -| net | TBD | -| net/http | TBD | -| net/http/cgi | TBD | -| net/http/cookiejar | TBD | -| net/http/fcgi | TBD | -| net/http/httptest | TBD | -| net/http/httptrace | TBD | -| net/http/httputil | TBD | -| net/http/internal | TBD | -| net/http/pprof | TBD | -| net/mail | TBD | -| net/netip | TBD | -| net/rpc | TBD | -| net/rpc/jsonrpc | TBD | -| net/smtp | TBD | -| net/textproto | TBD | -| net/url | full | -| os | TBD | -| os/exec | TBD | -| os/signal | TBD | -| os/user | TBD | -| path | full | -| path/filepath | TBD | -| plugin | TBD | -| reflect | TBD | -| regexp | TBD | -| regexp/syntax | TBD | -| runtime | TBD | -| runtime/asan | TBD | -| runtime/cgo | TBD | -| runtime/coverage | TBD | -| runtime/coverage/testdata | TBD | -| runtime/coverage/testdata/issue56006 | TBD | -| runtime/debug | TBD | -| runtime/metrics | TBD | -| runtime/msan | TBD | -| runtime/pprof | TBD | -| runtime/pprof/testdata/mappingtest | TBD | -| runtime/race | TBD | -| runtime/race/testdata | TBD | -| runtime/testdata/testexithooks | TBD | -| runtime/testdata/testfaketime | TBD | -| runtime/testdata/testprog | TBD | -| runtime/testdata/testprogcgo | TBD | -| runtime/testdata/testprogcgo/windows | TBD | -| runtime/testdata/testprognet | TBD | -| runtime/testdata/testwinlib | TBD | -| runtime/testdata/testwinlibsignal | TBD | -| runtime/testdata/testwinlibthrow | TBD | -| runtime/testdata/testwinsignal | TBD | -| runtime/trace | TBD | -| slices | TBD | -| sort | TBD | -| strconv | TBD | -| strings | TBD | -| sync | TBD | -| sync/atomic | TBD | -| syscall | TBD | -| syscall/js | TBD | -| testing | TBD | -| testing/fstest | TBD | -| testing/iotest | TBD | -| testing/quick | TBD | -| text/scanner | TBD | -| text/tabwriter | TBD | -| text/template | TBD | -| text/template/parse | TBD | -| time | TBD | -| time/tzdata | TBD | -| unicode | TBD | -| unicode/utf16 | TBD | -| unicode/utf8 | TBD | -| unsafe | TBD | - +| archive/tar | `tbd` | +| archive/zip | `tbd` | +| arena | `improb` | +| bufio | `full` | +| builtin | `full`[^1] | +| bytes | `full` | +| cmd/\* | `cmd` | +| compress/bzip2 | `tbd` | +| compress/flate | `tbd` | +| compress/gzip | `tbd` | +| compress/lzw | `tbd` | +| compress/zlib | `tbd` | +| container/heap | `tbd` | +| container/list | `tbd` | +| container/ring | `tbd` | +| context | `tbd` | +| crypto | `todo` | +| crypto/aes | `todo` | +| crypto/boring | `tbd` | +| crypto/cipher | `part` | +| crypto/des | `tbd` | +| crypto/dsa | `tbd` | +| crypto/ecdh | `tbd` | +| crypto/ecdsa | `tbd` | +| crypto/ed25519 | `tbd` | +| crypto/elliptic | `tbd` | +| crypto/hmac | `todo` | +| crypto/md5 | `test`[^2] | +| crypto/rand | `nondet` | +| crypto/rc4 | `tbd` | +| crypto/rsa | `tbd` | +| crypto/sha1 | `test`[^2] | +| crypto/sha256 | `part`[^3] | +| crypto/sha512 | `tbd` | +| crypto/subtle | `tbd` | +| crypto/tls | `nondet` | +| crypto/tls/fipsonly | `nondet` | +| crypto/x509 | `tbd` | +| crypto/x509/pkix | `tbd` | +| database/sql | `nondet` | +| database/sql/driver | `nondet` | +| debug/buildinfo | `gospec` | +| debug/dwarf | `gospec` | +| debug/elf | `gospec` | +| debug/gosym | `gospec` | +| debug/macho | `gospec` | +| debug/pe | `gospec` | +| debug/plan9obj | `gospec` | +| embed | `tbd` | +| encoding | `full` | +| encoding/ascii85 | `todo` | +| encoding/asn1 | `todo` | +| encoding/base32 | `todo` | +| encoding/base64 | `full` | +| encoding/binary | `part` | +| encoding/csv | `todo` | +| encoding/gob | `tbd` | +| encoding/hex | `full` | +| encoding/json | `todo` | +| encoding/pem | `todo` | +| encoding/xml | `todo` | +| errors | `part` | +| expvar | `tbd` | +| flag | `nondet` | +| fmt | `test`[^4] | +| go/ast | `gospec` | +| go/build | `gospec` | +| go/build/constraint | `gospec` | +| go/constant | `gospec` | +| go/doc | `gospec` | +| go/doc/comment | `gospec` | +| go/format | `gospec` | +| go/importer | `gospec` | +| go/parser | `gospec` | +| go/printer | `gospec` | +| go/scanner | `gospec` | +| go/token | `gospec` | +| go/types | `gospec` | +| hash | `full` | +| hash/adler32 | `full` | +| hash/crc32 | `todo` | +| hash/crc64 | `todo` | +| hash/fnv | `todo` | +| hash/maphash | `todo` | +| html | `todo` | +| html/template | `todo` | +| image | `tbd` | +| image/color | `tbd` | +| image/color/palette | `tbd` | +| image/draw | `tbd` | +| image/gif | `tbd` | +| image/jpeg | `tbd` | +| image/png | `tbd` | +| index/suffixarray | `tbd` | +| io | `full` | +| io/fs | `tbd` | +| io/ioutil | removed[^5] | +| log | `tbd` | +| log/slog | `tbd` | +| log/syslog | `nondet` | +| maps | `gnics` | +| math | `part` | +| math/big | `tbd` | +| math/bits | `todo` | +| math/cmplx | `tbd` | +| math/rand | `todo` | +| mime | `tbd` | +| mime/multipart | `tbd` | +| mime/quotedprintable | `tbd` | +| net | `nondet` | +| net/http | `nondet` | +| net/http/cgi | `nondet` | +| net/http/cookiejar | `nondet` | +| net/http/fcgi | `nondet` | +| net/http/httptest | `nondet` | +| net/http/httptrace | `nondet` | +| net/http/httputil | `nondet` | +| net/http/internal | `nondet` | +| net/http/pprof | `nondet` | +| net/mail | `nondet` | +| net/netip | `nondet` | +| net/rpc | `nondet` | +| net/rpc/jsonrpc | `nondet` | +| net/smtp | `nondet` | +| net/textproto | `nondet` | +| net/url | `full` | +| os | `nondet` | +| os/exec | `nondet` | +| os/signal | `nondet` | +| os/user | `nondet` | +| path | `full` | +| path/filepath | `nondet` | +| plugin | `nondet` | +| reflect | `todo` | +| regexp | `full` | +| regexp/syntax | `full` | +| runtime | `gospec` | +| runtime/asan | `gospec` | +| runtime/cgo | `gospec` | +| runtime/coverage | `gospec` | +| runtime/debug | `gospec` | +| runtime/metrics | `gospec` | +| runtime/msan | `gospec` | +| runtime/pprof | `gospec` | +| runtime/race | `gospec` | +| runtime/trace | `gospec` | +| slices | `gnics` | +| sort | `part`[^6] | +| strconv | `part` | +| strings | `full` | +| sync | `tbd` | +| sync/atomic | `tbd` | +| syscall | `nondet` | +| syscall/js | `nondet` | +| testing | `part` | +| testing/fstest | `tbd` | +| testing/iotest | `tbd` | +| testing/quick | `tbd` | +| text/scanner | `todo` | +| text/tabwriter | `todo` | +| text/template | `todo` | +| text/template/parse | `todo` | +| time | `full`[^7] | +| time/tzdata | `tbd` | +| unicode | `full` | +| unicode/utf16 | `tbd` | +| unicode/utf8 | `full` | +| unsafe | `nondet` | +[^1]: `builtin` is a "fake" package that exists to document the behaviour of + some builtin functions. The "fake" package does not currently exist in Gno, + but [all functions up to Go 1.17 exist](https://pkg.go.dev/builtin@go1.17), + except for those relating to complex or channel types. +[^2]: `crypto/sha1` and `crypto/md5` implement "deprecated" hashing + algorithms, widely considered unsafe for cryptographic hashing. Decision on + whether to include these as part of the official standard libraries is still + pending. +[^3]: `crypto/sha256` is currently only implemented for `Sum256`, which should + still cover a majority of use cases. A full implementation is welcome. +[^4]: like many other encoding packages, `fmt` depends on `reflect` to be added. + For now, package `gno.land/p/demo/ufmt` may do what you need. In test + functions, `fmt` works. +[^5]: `io/ioutil` [is deprecated in Go.](https://pkg.go.dev/io/ioutil) + Its functionality has been moved to packages `os` and `io`. The functions + which have been moved in `io` are implemented in that package. +[^6]: `sort` has the notable omission of `sort.Slice`. You'll need to write a + bit of boilerplate, but you can use `sort.Interface` + `sort.Sort`! +[^7]: `time.Now` returns the block time rather than the system time, for + determinism. Concurrent functionality (such as `time.Ticker`) is not implemented. ## Tooling (`gno` binary) @@ -353,15 +289,16 @@ Additional native types: | go doc | gno doc | limited compatibility; see https://github.com/gnolang/gno/issues/522 | | go env | | | | go fix | | | -| go fmt | | | +| go fmt | | gofmt (& similar tools, like gofumpt) works on gno code. | | go generate | | | -| go get | | | -| go help | | | +| go get | | see `gno mod download`. | +| go help | gno $cmd --help | ie. `gno doc --help` | | go install | | | | go list | | | -| go mod | | | -| + go mod download | gno mod download | same behavior | +| go mod | gno mod | | | + go mod init | gno mod init | same behavior | +| + go mod download | gno mod download | same behavior | +| + go mod tidy | gno mod tidy | same behavior | | | gno precompile | | | go work | | | | | gno repl | | From afcdc665b0e456031c86ee02bab45a768cc1ccb1 Mon Sep 17 00:00:00 2001 From: Abhinesh <142514166+AbhineshJha@users.noreply.github.com> Date: Thu, 9 Nov 2023 23:57:46 +0530 Subject: [PATCH 76/93] docs(grammatical): correct typos and improve grammar (#1321) This pull request addresses and corrects several grammatical errors found throughout the codebase. The changes include fixing typos, punctuation, and ensuring proper sentence structure for improved readability and clarity. No functional changes have been made; this is purely a maintenance update.
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 Bazalgette --- CONTRIBUTING.md | 2 +- PHILOSOPHY.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1739d50f034..5c2bb8f9996 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ Likewise, if you have an idea on how to improve this guide, go for it as well. ### Environment -The gno repository is primarily based on Golang (Go), and Gnolang (Gno). +The gno repository is primarily based on Golang (Go) and Gnolang (Gno). The primary tech stack for working on the repository: diff --git a/PHILOSOPHY.md b/PHILOSOPHY.md index c55d479f741..bde27a48ef7 100644 --- a/PHILOSOPHY.md +++ b/PHILOSOPHY.md @@ -5,7 +5,7 @@ * Readability is paramount - beautiful is better than fast. * Minimal code - keep total footprint small. * Minimal dependencies - all dependencies must get audited, and become part of the repo. - * Modular dependencies - whereever reasonable, make components modular. + * Modular dependencies - wherever reasonable, make components modular. * Finished - software projects that don't become finished are projects that are forever vulnerable. One of the primary goals of the Gno language and related works is to become finished within a reasonable timeframe. From bc5af65338a3cbca8f63e940d1dd228ba9389da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Thu, 9 Nov 2023 20:14:11 +0100 Subject: [PATCH 77/93] feat: official documentation (#1046) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description This PR introduces the official Gno documentation, which will be hosted at `docs.gno.land`, using the Docusaurus framework. As discussed with @moul, the docs are supposed to be framework agnostic, with Docusaurus living in the `misc/docusaurus` directory. This documentation also includes work done by @alexiscolin, @leohhhn, @waymobetta, @harry-hov, and the Onbloc team (@dongwon8247 and @r3v4s, migrated from their Developer Portal). Thank you to everyone who contributed and got us to this point, for the v1 of the docs 🙏 @leohhhn @waymobetta (and DevRels reading this in the future), full ownership of the documentation will be relinquished to you after this PR is merged out. To run the docs locally: - `cd misc/docusaurus` - `yarn install` - `yarn start`
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] 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: jon roethke Co-authored-by: Blake <104744707+r3v4s@users.noreply.github.com> Co-authored-by: Dongwon <74406335+dongwon8247@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Co-authored-by: Hariom Verma Co-authored-by: Alexis Colin Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Morgan --- .github/CODEOWNERS | 3 + docs/assets/explanation/packages/pkg-1.gno | 6 + docs/assets/explanation/packages/pkg-2.gno | 11 + docs/assets/explanation/packages/pkg-3.gno | 12 + docs/assets/explanation/packages/pkg-4.gno | 8 + .../browsing-gno-source-code/gnoweb-avl.png | Bin 0 -> 170719 bytes .../gnoweb-boards-source.png | Bin 0 -> 222415 bytes .../gnoweb-boards.png | Bin 0 -> 156387 bytes .../browsing-gno-source-code/gnoweb.png | Bin 0 -> 222800 bytes .../gnokey-add-mnemonic.gif | Bin 0 -> 48407 bytes .../creating-a-key-pair/gnokey-add-random.gif | Bin 0 -> 53739 bytes .../creating-a-key-pair/gnokey-export.gif | Bin 0 -> 66376 bytes .../creating-a-key-pair/gnokey-generate.gif | Bin 0 -> 26843 bytes .../creating-a-key-pair/gnokey-import.gif | Bin 0 -> 43816 bytes .../creating-a-key-pair/gnokey-list.gif | Bin 0 -> 27057 bytes .../getting-started/local-setup/gno-help.gif | Bin 0 -> 44108 bytes .../local-setup/gnokey-help.gif | Bin 0 -> 147394 bytes .../local-setup/make-build-gnoland.gif | Bin 0 -> 29922 bytes .../local-setup/make-build-gnovm.gif | Bin 0 -> 38706 bytes .../gnoland-start.gif | Bin 0 -> 2543753 bytes .../setting-up-funds/faucet-page.png | Bin 0 -> 111085 bytes .../setting-up-funds/gnofaucet-serve.gif | Bin 0 -> 24578 bytes .../setting-up-funds/gnokey-query.gif | Bin 0 -> 38198 bytes .../setting-up-funds/gnoland-start.gif | Bin 0 -> 2676536 bytes .../setting-up-funds/gnoweb.gif | Bin 0 -> 14329 bytes .../creating-grc20/mytoken-1.gno | 21 + .../creating-grc20/mytoken-2.gno | 83 + .../creating-grc721/mynonfungibletoken-1.gno | 17 + .../creating-grc721/mynonfungibletoken-2.gno | 102 + .../porting-solidity-to-gno/porting-1.gno | 39 + .../porting-solidity-to-gno/porting-10.sol | 29 + .../porting-solidity-to-gno/porting-11.gno | 17 + .../porting-solidity-to-gno/porting-12.gno | 18 + .../porting-solidity-to-gno/porting-13.gno | 74 + .../porting-solidity-to-gno/porting-2.sol | 47 + .../porting-solidity-to-gno/porting-3.gno | 8 + .../porting-solidity-to-gno/porting-4.sol | 32 + .../porting-solidity-to-gno/porting-5.gno | 30 + .../porting-solidity-to-gno/porting-6.gno | 41 + .../porting-solidity-to-gno/porting-7.sol | 20 + .../porting-solidity-to-gno/porting-8.gno | 15 + .../porting-solidity-to-gno/porting-9.gno | 17 + .../how-to-guides/simple-contract/counter.gno | 17 + .../how-to-guides/simple-contract/init.gno | 11 + .../how-to-guides/simple-library/tapas.gno | 40 + .../how-to-guides/testing-gno/counter-1.gno | 19 + .../how-to-guides/testing-gno/counter-2.gno | 51 + .../write-simple-dapp/poll-1.gno | 69 + .../write-simple-dapp/poll-2.gno | 78 + .../write-simple-dapp/poll-3.gno | 74 + .../reference/standard-library/std-1.gno | 14 + .../reference/standard-library/std-2.gno | 4 + docs/{ => explanation}/from-go-to-gno.md | 6 +- docs/explanation/gno-language.md | 68 + docs/explanation/gno-modules.md | 42 + docs/explanation/gno-test.md | 82 + docs/explanation/gno-tooling/cli/gno.md | 66 + docs/explanation/gno-tooling/cli/gnofaucet.md | 61 + docs/explanation/gno-tooling/cli/gnokey.md | 290 + docs/explanation/gno-tooling/cli/gnoland.md | 31 + docs/explanation/gno-tooling/cli/tm2txsync.md | 45 + docs/explanation/gnovm.md | 27 + docs/explanation/packages.md | 113 + .../proof-of-contribution.md | 4 + docs/explanation/realms.md | 49 + docs/explanation/tendermint2.md | 122 + .../browsing-gno-source-code.md | 96 + docs/getting-started/local-setup.md | 111 + .../setting-up-a-local-chain.md | 142 + .../setting-up-funds/premining-balances.md | 106 + .../setting-up-funds/running-a-faucet.md | 100 + .../getting-started/working-with-key-pairs.md | 191 + docs/go-gno-compatibility.md | 1 - docs/how-to-guides/connect-wallet-dapp.md | 115 + docs/how-to-guides/creating-grc20.md | 164 + docs/how-to-guides/creating-grc721.md | 199 + docs/how-to-guides/deploy.md | 74 + docs/how-to-guides/interact-with-gnoland.md | 94 + docs/how-to-guides/porting-solidity-to-gno.md | 680 ++ docs/how-to-guides/simple-contract.md | 149 + docs/how-to-guides/simple-library.md | 133 + docs/how-to-guides/testing-gno.md | 185 + docs/how-to-guides/write-simple-dapp.md | 301 + docs/overview.md | 45 + docs/peace.md | 4 + .../gno-js-client/getting-started.md | 25 + docs/reference/gno-js-client/gno-provider.md | 124 + docs/reference/gno-js-client/gno-wallet.md | 78 + .../reference}/go-gno-compatibility.md | 6 +- docs/reference/rpc-endpoints.md | 502 ++ docs/reference/standard-library.md | 68 + .../Provider/json-rpc-provider.md | 22 + .../tm2-js-client/Provider/provider.md | 443 + .../tm2-js-client/Provider/utility.md | 116 + .../tm2-js-client/Provider/ws-provider.md | 95 + docs/reference/tm2-js-client/Signer/key.md | 30 + docs/reference/tm2-js-client/Signer/ledger.md | 27 + docs/reference/tm2-js-client/Signer/signer.md | 96 + .../tm2-js-client/getting-started.md | 65 + docs/reference/tm2-js-client/wallet.md | 275 + misc/docusaurus/.gitignore | 20 + misc/docusaurus/babel.config.js | 3 + misc/docusaurus/docusaurus.config.js | 158 + misc/docusaurus/package.json | 47 + misc/docusaurus/sidebars.js | 113 + misc/docusaurus/src/css/custom.css | 280 + misc/docusaurus/static/.nojekyll | 0 misc/docusaurus/static/img/favicon.ico | Bin 0 -> 15086 bytes misc/docusaurus/static/img/logo.svg | 12 + misc/docusaurus/static/img/logo_light.svg | 12 + misc/docusaurus/tsconfig.json | 7 + misc/docusaurus/yarn.lock | 7726 +++++++++++++++++ 112 files changed, 15270 insertions(+), 3 deletions(-) create mode 100644 docs/assets/explanation/packages/pkg-1.gno create mode 100644 docs/assets/explanation/packages/pkg-2.gno create mode 100644 docs/assets/explanation/packages/pkg-3.gno create mode 100644 docs/assets/explanation/packages/pkg-4.gno create mode 100644 docs/assets/getting-started/browsing-gno-source-code/gnoweb-avl.png create mode 100644 docs/assets/getting-started/browsing-gno-source-code/gnoweb-boards-source.png create mode 100644 docs/assets/getting-started/browsing-gno-source-code/gnoweb-boards.png create mode 100644 docs/assets/getting-started/browsing-gno-source-code/gnoweb.png create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-add-mnemonic.gif create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-add-random.gif create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-export.gif create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-generate.gif create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-import.gif create mode 100644 docs/assets/getting-started/creating-a-key-pair/gnokey-list.gif create mode 100644 docs/assets/getting-started/local-setup/gno-help.gif create mode 100644 docs/assets/getting-started/local-setup/gnokey-help.gif create mode 100644 docs/assets/getting-started/local-setup/make-build-gnoland.gif create mode 100644 docs/assets/getting-started/local-setup/make-build-gnovm.gif create mode 100644 docs/assets/getting-started/setting-up-a-local-chain/gnoland-start.gif create mode 100644 docs/assets/getting-started/setting-up-funds/faucet-page.png create mode 100644 docs/assets/getting-started/setting-up-funds/gnofaucet-serve.gif create mode 100644 docs/assets/getting-started/setting-up-funds/gnokey-query.gif create mode 100644 docs/assets/getting-started/setting-up-funds/gnoland-start.gif create mode 100644 docs/assets/getting-started/setting-up-funds/gnoweb.gif create mode 100644 docs/assets/how-to-guides/creating-grc20/mytoken-1.gno create mode 100644 docs/assets/how-to-guides/creating-grc20/mytoken-2.gno create mode 100644 docs/assets/how-to-guides/creating-grc721/mynonfungibletoken-1.gno create mode 100644 docs/assets/how-to-guides/creating-grc721/mynonfungibletoken-2.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-1.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-10.sol create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-11.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-12.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-13.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-2.sol create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-3.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-4.sol create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-5.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-6.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-7.sol create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-8.gno create mode 100644 docs/assets/how-to-guides/porting-solidity-to-gno/porting-9.gno create mode 100644 docs/assets/how-to-guides/simple-contract/counter.gno create mode 100644 docs/assets/how-to-guides/simple-contract/init.gno create mode 100644 docs/assets/how-to-guides/simple-library/tapas.gno create mode 100644 docs/assets/how-to-guides/testing-gno/counter-1.gno create mode 100644 docs/assets/how-to-guides/testing-gno/counter-2.gno create mode 100644 docs/assets/how-to-guides/write-simple-dapp/poll-1.gno create mode 100644 docs/assets/how-to-guides/write-simple-dapp/poll-2.gno create mode 100644 docs/assets/how-to-guides/write-simple-dapp/poll-3.gno create mode 100644 docs/assets/reference/standard-library/std-1.gno create mode 100644 docs/assets/reference/standard-library/std-2.gno rename docs/{ => explanation}/from-go-to-gno.md (97%) create mode 100644 docs/explanation/gno-language.md create mode 100644 docs/explanation/gno-modules.md create mode 100644 docs/explanation/gno-test.md create mode 100644 docs/explanation/gno-tooling/cli/gno.md create mode 100644 docs/explanation/gno-tooling/cli/gnofaucet.md create mode 100644 docs/explanation/gno-tooling/cli/gnokey.md create mode 100644 docs/explanation/gno-tooling/cli/gnoland.md create mode 100644 docs/explanation/gno-tooling/cli/tm2txsync.md create mode 100644 docs/explanation/gnovm.md create mode 100644 docs/explanation/packages.md rename docs/{ => explanation}/proof-of-contribution.md (99%) create mode 100644 docs/explanation/realms.md create mode 100644 docs/explanation/tendermint2.md create mode 100644 docs/getting-started/browsing-gno-source-code.md create mode 100644 docs/getting-started/local-setup.md create mode 100644 docs/getting-started/setting-up-a-local-chain.md create mode 100644 docs/getting-started/setting-up-funds/premining-balances.md create mode 100644 docs/getting-started/setting-up-funds/running-a-faucet.md create mode 100644 docs/getting-started/working-with-key-pairs.md delete mode 120000 docs/go-gno-compatibility.md create mode 100644 docs/how-to-guides/connect-wallet-dapp.md create mode 100644 docs/how-to-guides/creating-grc20.md create mode 100644 docs/how-to-guides/creating-grc721.md create mode 100644 docs/how-to-guides/deploy.md create mode 100644 docs/how-to-guides/interact-with-gnoland.md create mode 100644 docs/how-to-guides/porting-solidity-to-gno.md create mode 100644 docs/how-to-guides/simple-contract.md create mode 100644 docs/how-to-guides/simple-library.md create mode 100644 docs/how-to-guides/testing-gno.md create mode 100644 docs/how-to-guides/write-simple-dapp.md create mode 100644 docs/overview.md create mode 100644 docs/reference/gno-js-client/getting-started.md create mode 100644 docs/reference/gno-js-client/gno-provider.md create mode 100644 docs/reference/gno-js-client/gno-wallet.md rename {gnovm/docs => docs/reference}/go-gno-compatibility.md (99%) create mode 100644 docs/reference/rpc-endpoints.md create mode 100644 docs/reference/standard-library.md create mode 100644 docs/reference/tm2-js-client/Provider/json-rpc-provider.md create mode 100644 docs/reference/tm2-js-client/Provider/provider.md create mode 100644 docs/reference/tm2-js-client/Provider/utility.md create mode 100644 docs/reference/tm2-js-client/Provider/ws-provider.md create mode 100644 docs/reference/tm2-js-client/Signer/key.md create mode 100644 docs/reference/tm2-js-client/Signer/ledger.md create mode 100644 docs/reference/tm2-js-client/Signer/signer.md create mode 100644 docs/reference/tm2-js-client/getting-started.md create mode 100644 docs/reference/tm2-js-client/wallet.md create mode 100644 misc/docusaurus/.gitignore create mode 100644 misc/docusaurus/babel.config.js create mode 100644 misc/docusaurus/docusaurus.config.js create mode 100644 misc/docusaurus/package.json create mode 100644 misc/docusaurus/sidebars.js create mode 100644 misc/docusaurus/src/css/custom.css create mode 100644 misc/docusaurus/static/.nojekyll create mode 100644 misc/docusaurus/static/img/favicon.ico create mode 100644 misc/docusaurus/static/img/logo.svg create mode 100644 misc/docusaurus/static/img/logo_light.svg create mode 100644 misc/docusaurus/tsconfig.json create mode 100644 misc/docusaurus/yarn.lock diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 98ff9ec12fa..0906f3914da 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,3 +37,6 @@ PHILOSOPHY.md @jaekwon @moul CONTRIBUTING.md @jaekwon @moul LICENSE.md @jaekwon @moul .github/CODEOWNERS @jaekwon @moul + +# Documentation. +docs/* @gnolang/devrels \ No newline at end of file diff --git a/docs/assets/explanation/packages/pkg-1.gno b/docs/assets/explanation/packages/pkg-1.gno new file mode 100644 index 00000000000..e68d506612a --- /dev/null +++ b/docs/assets/explanation/packages/pkg-1.gno @@ -0,0 +1,6 @@ +func TotalSupply() uint64 +func BalanceOf(account std.Address) uint64 +func Transfer(to std.Address, amount uint64) +func Approve(spender std.Address, amount uint64) +func TransferFrom(from, to std.Address, amount uint64) +func Allowance(owner, spender std.Address) uint64 diff --git a/docs/assets/explanation/packages/pkg-2.gno b/docs/assets/explanation/packages/pkg-2.gno new file mode 100644 index 00000000000..0054cc95e3d --- /dev/null +++ b/docs/assets/explanation/packages/pkg-2.gno @@ -0,0 +1,11 @@ +// functions that work similarly to those of grc20 +func BalanceOf(owner std.Address) (uint64, error) +func Approve(approved std.Address, tid TokenID) error +func TransferFrom(from, to std.Address, tid TokenID) error + +// functions unique to grc721 +func OwnerOf(tid TokenID) (std.Address, error) +func SafeTransferFrom(from, to std.Address, tid TokenID) error +func SetApprovalForAll(operator std.Address, approved bool) error +func GetApproved(tid TokenID) (std.Address, error) +func IsApprovedForAll(owner, operator std.Address) bool diff --git a/docs/assets/explanation/packages/pkg-3.gno b/docs/assets/explanation/packages/pkg-3.gno new file mode 100644 index 00000000000..f1ba5609d6b --- /dev/null +++ b/docs/assets/explanation/packages/pkg-3.gno @@ -0,0 +1,12 @@ +func TestAddress(name string) std.Address { + if len(name) > std.RawAddressSize { + panic("address name cannot be greater than std.AddressSize bytes") + } + addr := std.RawAddress{} + // TODO: use strings.RepeatString or similar. + // NOTE: I miss python's "".Join(). + blanks := "____________________" + copy(addr[:], []byte(blanks)) + copy(addr[:], []byte(name)) + return std.Address(std.EncodeBech32("g", addr)) +} diff --git a/docs/assets/explanation/packages/pkg-4.gno b/docs/assets/explanation/packages/pkg-4.gno new file mode 100644 index 00000000000..edd34b5cc5d --- /dev/null +++ b/docs/assets/explanation/packages/pkg-4.gno @@ -0,0 +1,8 @@ +admin := users.AddressOrName("g1tntwtvzrkt2gex69f0pttan0fp05zmeg5yykv8") +test2 := users.AddressOrName(testutils.TestAddress("test2")) +recv := users.AddressOrName(testutils.TestAddress("recv")) +normal := users.AddressOrName(testutils.TestAddress("normal")) +owner := users.AddressOrName(testutils.TestAddress("owner")) +spender := users.AddressOrName(testutils.TestAddress("spender")) +recv2 := users.AddressOrName(testutils.TestAddress("recv2")) +mibu := users.AddressOrName(testutils.TestAddress("mint_burn")) diff --git a/docs/assets/getting-started/browsing-gno-source-code/gnoweb-avl.png b/docs/assets/getting-started/browsing-gno-source-code/gnoweb-avl.png new file mode 100644 index 0000000000000000000000000000000000000000..3754ccfe88e7118ef0ed20a723159d32d603e9c7 GIT binary patch literal 170719 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 >9|ImBYK^%nFEx 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
  • 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∾RY;PSVWYmPhg@sCbD~Y<9T=6y-?jG@6U%&VN_DV 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

    k!ioPGsr8Zbrr-pz--8$Z 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

    a2dtL+^-!(u;tUP?RFQhE4!ckX}O| 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∋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

    m*UdP}I2k0XODN#)n)~ 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¨D1gFN;Q5}~Z3S`Q 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 literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/creating-a-key-pair/gnokey-add-random.gif b/docs/assets/getting-started/creating-a-key-pair/gnokey-add-random.gif new file mode 100644 index 0000000000000000000000000000000000000000..dba61a6b287e95ce364a4917eb9ff249dc2b33ea GIT binary patch literal 53739 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 bL$vR81B!>GvxXw>tYia6FMX{50G0m*7#oJL literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/creating-a-key-pair/gnokey-list.gif b/docs/assets/getting-started/creating-a-key-pair/gnokey-list.gif new file mode 100644 index 0000000000000000000000000000000000000000..ad050da585c7f27954dc4ee7505448a4c9855708 GIT binary patch literal 27057 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 b#Mg;a6}KdPSdGJm0;hFv$w?0lP5Qq8?;ymy literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/local-setup/gno-help.gif b/docs/assets/getting-started/local-setup/gno-help.gif new file mode 100644 index 0000000000000000000000000000000000000000..ebd7d4cfcb0aeaa856a40ad0180e4e92169f50df GIT binary patch literal 44108 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 x4Ups{SnwXKA#C38>_5UUtM!!@^$(o5(dv6$#} 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

    ZAXoAux-8oeU0!O#@WtzyiNK+6%Hn5AW{mMg8B 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 literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/local-setup/gnokey-help.gif b/docs/assets/getting-started/local-setup/gnokey-help.gif new file mode 100644 index 0000000000000000000000000000000000000000..d96d24246794eb0f052798e89033c48a882bfa10 GIT binary patch literal 147394 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 O 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

    D9tg}HIz^P#dVC+jSyVN`DFV>f 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 I3TK!!DZ`F*%0Q#E4*q3i>L9SFLjp6 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 c;85!0z^sH;`gK?tY(2uR6zczLB|r0M7x$jB*# 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 Tb3~rmloIyxaLGy?01)~=9?Xn5 literal 0 HcmV?d00001 diff --git a/docs/assets/getting-started/setting-up-a-local-chain/gnoland-start.gif b/docs/assets/getting-started/setting-up-a-local-chain/gnoland-start.gif new file mode 100644 index 0000000000000000000000000000000000000000..4da21bc863b3aa60f2420489800be2dc0798efa6 GIT binary patch literal 2543753 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