From b00a175ec3d8003e99bc3ce37370f2d9ffd2f138 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 18 Aug 2026 13:51:36 +0800 Subject: [PATCH 1/3] ci: test LLGo across Go 1.20 through 1.26 Build the compiler and repository tooling only with the pinned Go 1.26 toolchain, while testing source compatibility with exact Go 1.20 through Go 1.26 toolchains and matching alternate module files. Add reusable local runners, release-tagged standard-library tests, runtime and wasm endpoints, and version-aware symbol coverage. --- .github/actions/setup-go/action.yml | 19 +- .github/actions/test-helloworld/action.yml | 60 +---- .github/test-go.mod | 10 + .github/test-go.sum | 6 + .github/workflows/go.yml | 2 +- .github/workflows/goroot.yml | 58 ++-- .github/workflows/llgo.yml | 254 +++++++++--------- .github/workflows/release-build.yml | 9 +- .go-version | 1 + README.md | 4 +- chore/check_std_symbols/main.go | 20 +- cmd/internal/flags/flags_test.go | 4 +- cmd/internal/flags/gobuild.go | 2 +- dev/README.md | 51 +++- dev/build_ci_tools.sh | 31 +++ dev/go_toolchain.sh | 48 ++++ dev/local_ci.sh | 56 +--- dev/test_go_version.sh | 169 ++++++++++++ dev/test_go_versions.sh | 45 ++++ dev/test_goroot.sh | 23 +- dev/test_helloworld.sh | 90 +++++++ dev/test_runtime_go_version.sh | 15 ++ dev/test_runtime_go_versions.sh | 17 ++ dev/test_std_buildmodes.sh | 13 +- dev/test_wasm_runtime_go_version.sh | 59 ++++ dev/test_wasm_runtime_go_versions.sh | 26 ++ dev/with_go_version.sh | 26 ++ doc/_readme/scripts/check_std_cover.sh | 79 ++++-- go.mod | 2 +- .../lib/runtime/pprof_goroutine_go123_llgo.go | 10 + .../runtime/pprof_goroutine_pre_go123_llgo.go | 10 + .../lib/runtime/pprof_linkname_llgo.go | 5 - test/README.md | 48 ++++ test/go/caller_acceptance_test.go | 6 +- test/go/cgo_malloc_test.go | 9 +- test/go/defer_ir_regression_test.go | 5 + test/go/fault_unwind_test.go | 3 + test/go/generic_local_types_test.go | 6 +- test/go/newexpr_go126_test.go | 2 +- test/go/package_init_order_test.go | 3 + test/go/print_builtin_legacy_test.go | 4 +- test/go/print_builtin_test.go | 6 +- test/go/runtime_lineinfo_stack_test.go | 7 +- test/go/runtime_statement_line_test.go | 7 +- test/go/string_conversion_test.go | 8 +- test/go/test_command_test.go | 75 ++++++ test/go/tool_compile_compat_test.go | 1 + test/std/README.md | 5 +- test/std/bytes/bytes_go121_test.go | 34 +++ test/std/bytes/bytes_go124_test.go | 49 ++++ test/std/bytes/bytes_test.go | 74 +---- ..._symbols_test.go => go125_symbols_test.go} | 2 +- test/std/crypto/go125_symbols_test.go | 41 +++ test/std/crypto/go126_symbols_test.go | 32 +-- ..._symbols_test.go => go125_symbols_test.go} | 2 +- test/std/debug/dwarf/dwarf_test.go | 1 + test/std/debug/elf/elf_test.go | 1 + test/std/debug/macho/macho_test.go | 1 + test/std/debug/pe/pe_test.go | 1 + test/std/go/ast/go125_symbols_test.go | 30 +++ test/std/go/ast/go126_symbols_test.go | 22 -- test/std/go/token/go125_symbols_test.go | 27 ++ test/std/go/token/go126_symbols_test.go | 16 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- test/std/log/slog/go125_symbols_test.go | 21 ++ test/std/log/slog/go126_symbols_test.go | 11 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- test/std/net/http/go125_symbols_test.go | 53 ++++ test/std/net/http/go126_symbols_test.go | 44 --- test/std/os/go125_symbols_test.go | 63 +++++ test/std/os/go126_symbols_test.go | 54 ---- test/std/reflect/go125_symbols_test.go | 18 ++ test/std/reflect/go126_symbols_test.go | 10 - ..._symbols_test.go => go125_symbols_test.go} | 4 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- test/std/testing/go125_symbols_test.go | 38 +++ test/std/testing/synctest/synctest_test.go | 2 +- ..._symbols_test.go => go125_symbols_test.go} | 2 +- 82 files changed, 1490 insertions(+), 596 deletions(-) create mode 100644 .github/test-go.mod create mode 100644 .github/test-go.sum create mode 100644 .go-version create mode 100755 dev/build_ci_tools.sh create mode 100755 dev/go_toolchain.sh create mode 100755 dev/test_go_version.sh create mode 100755 dev/test_go_versions.sh create mode 100755 dev/test_helloworld.sh create mode 100755 dev/test_runtime_go_version.sh create mode 100755 dev/test_runtime_go_versions.sh create mode 100755 dev/test_wasm_runtime_go_version.sh create mode 100755 dev/test_wasm_runtime_go_versions.sh create mode 100755 dev/with_go_version.sh create mode 100644 runtime/internal/lib/runtime/pprof_goroutine_go123_llgo.go create mode 100644 runtime/internal/lib/runtime/pprof_goroutine_pre_go123_llgo.go create mode 100644 test/README.md create mode 100644 test/go/test_command_test.go create mode 100644 test/std/bytes/bytes_go121_test.go create mode 100644 test/std/bytes/bytes_go124_test.go rename test/std/crypto/ecdsa/{go126_symbols_test.go => go125_symbols_test.go} (98%) create mode 100644 test/std/crypto/go125_symbols_test.go rename test/std/crypto/sha3/{go126_symbols_test.go => go125_symbols_test.go} (96%) create mode 100644 test/std/go/ast/go125_symbols_test.go create mode 100644 test/std/go/token/go125_symbols_test.go rename test/std/go/types/{go126_symbols_test.go => go125_symbols_test.go} (98%) rename test/std/hash/{go126_symbols_test.go => go125_symbols_test.go} (98%) rename test/std/hash/maphash/{go126_symbols_test.go => go125_symbols_test.go} (96%) rename test/std/io/fs/{go126_symbols_test.go => go125_symbols_test.go} (98%) create mode 100644 test/std/log/slog/go125_symbols_test.go rename test/std/mime/multipart/{go126_symbols_test.go => go125_symbols_test.go} (96%) create mode 100644 test/std/net/http/go125_symbols_test.go create mode 100644 test/std/os/go125_symbols_test.go create mode 100644 test/std/reflect/go125_symbols_test.go rename test/std/runtime/trace/{go126_symbols_test.go => go125_symbols_test.go} (92%) rename test/std/sync/{go126_symbols_test.go => go125_symbols_test.go} (95%) rename test/std/testing/fstest/{go126_symbols_test.go => go125_symbols_test.go} (97%) create mode 100644 test/std/testing/go125_symbols_test.go rename test/std/unicode/{go126_symbols_test.go => go125_symbols_test.go} (97%) diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml index 1ce97f4bd5..97e607fc94 100644 --- a/.github/actions/setup-go/action.yml +++ b/.github/actions/setup-go/action.yml @@ -2,15 +2,21 @@ name: "Setup Go" description: "Set up and verify the requested Go toolchain" inputs: go-version: - description: "The exact Go version to install" + description: "The exact Go version to install; omit it for the LLGo build toolchain" required: false - # Keep this pin synchronized with the primary CI and release smoke-test - # matrices. Upgrade it in a dedicated toolchain-update PR. - default: "1.26.5" + default: "" runs: using: "composite" steps: - - name: Set up Go + - name: Set up the LLGo build toolchain + if: inputs.go-version == '' + uses: actions/setup-go@v7 + with: + go-version-file: .go-version + cache: false + + - name: Set up an explicit Go toolchain + if: inputs.go-version != '' uses: actions/setup-go@v7 with: go-version: ${{ inputs.go-version }} @@ -22,6 +28,9 @@ runs: set -euo pipefail requested="${{ inputs.go-version }}" + if [[ -z "${requested}" ]]; then + requested="$(tr -d '[:space:]' < .go-version)" + fi requested="${requested#go}" actual="$(go env GOVERSION)" actual="${actual#go}" diff --git a/.github/actions/test-helloworld/action.yml b/.github/actions/test-helloworld/action.yml index 9020e7887b..7c4991ee65 100644 --- a/.github/actions/test-helloworld/action.yml +++ b/.github/actions/test-helloworld/action.yml @@ -2,8 +2,13 @@ name: "Test Hello World" description: "Test Hello World with specific Go and module versions" inputs: go-version: - description: "Go version being tested" - required: true + description: "Exact Go series used to run the smoke test" + required: false + default: "1.26" + embedded: + description: "Also run the ESP32 build smoke test" + required: false + default: "true" mod-version: description: "Go module version to use" required: true @@ -13,51 +18,6 @@ runs: - name: Test Hello World shell: bash run: | - echo "Testing with Go ${{ inputs.go-version }} and go.mod ${{ inputs.mod-version }}" - mkdir -p _test/helloworld && cd _test/helloworld - cat > go.mod << 'EOL' - module hello - go ${{ inputs.mod-version }} - EOL - cat > main.go << 'EOL' - package main - import ( - "fmt" - "github.com/goplus/lib/c" - "github.com/goplus/lib/cpp/std" - ) - func main() { - fmt.Println("Hello, LLGo!") - println("Hello, LLGo!") - c.Printf(c.Str("Hello, LLGo!\n")) - c.Printf(std.Str("Hello LLGo by cpp/std.Str\n").CStr()) - } - EOL - go mod tidy - EXPECTED="Hello, LLGo! - Hello, LLGo! - Hello, LLGo! - Hello LLGo by cpp/std.Str" - OUTPUT=$(llgo run . 2>&1 | tee /dev/stderr) - if echo "$OUTPUT" | grep -qF "$EXPECTED"; then - echo "Basic test passed" - else - echo "Basic test failed" - echo "Expected to contain:" - echo "$EXPECTED" - echo "Got:" - echo "$OUTPUT" - exit 1 - fi - - cd ../.. - mkdir -p _test/emb && cd _test/emb - cat > main.go << 'EOL' - package main - - func main() { - } - EOL - llgo build -v -target esp32-coreboard-v2 -o demo.out . - test -f demo.out.elf && echo "ESP32 cross-compilation test passed: demo.out.elf generated" - exit $? + LLGO_HELLO_EMBED="${{ inputs.embedded }}" \ + dev/with_go_version.sh "${{ inputs.go-version }}" \ + dev/test_helloworld.sh "${{ inputs.mod-version }}" diff --git a/.github/test-go.mod b/.github/test-go.mod new file mode 100644 index 0000000000..5465771899 --- /dev/null +++ b/.github/test-go.mod @@ -0,0 +1,10 @@ +module github.com/xgo-dev/llgo + +go 1.20 + +require ( + github.com/goplus/lib v0.3.1 + github.com/xgo-dev/llgo/runtime v0.0.0-00010101000000-000000000000 + go.yaml.in/yaml/v3 v3.0.5 + golang.org/x/sys v0.30.0 +) diff --git a/.github/test-go.sum b/.github/test-go.sum new file mode 100644 index 0000000000..d9ac21accd --- /dev/null +++ b/.github/test-go.sum @@ -0,0 +1,6 @@ +github.com/goplus/lib v0.3.1 h1:Xws4DBVvgOMu58awqB972wtvTacDbk3nqcbHjdx9KSg= +github.com/goplus/lib v0.3.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c2bc55dec2..c13d798128 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -74,7 +74,7 @@ jobs: set -euo pipefail # test/go intentionally contains compiler edge cases that make the - # Go 1.26.5 printf analyzer panic. Keep the normal go test vet gate + # The current Go 1.26 printf analyzer panics here. Keep the normal go test vet gate # for every other package, and disable vet only for that package. go list ./... \ | grep -v '^github.com/xgo-dev/llgo/test/go$' \ diff --git a/.github/workflows/goroot.yml b/.github/workflows/goroot.yml index ec9af260da..c63dab720e 100644 --- a/.github/workflows/goroot.yml +++ b/.github/workflows/goroot.yml @@ -15,7 +15,7 @@ concurrency: jobs: goroot: - name: GOROOT (${{ matrix.lane }}, ${{ matrix.os }}, Go ${{ matrix.go-version }}, shard ${{ matrix.shard-index }}/4) + name: GOROOT (${{ matrix.lane }}, ${{ matrix.os }}, Go ${{ matrix.go-label }}, shard ${{ matrix.shard-index }}/4) timeout-minutes: 180 env: GOPROXY: https://proxy.golang.org,direct @@ -24,18 +24,21 @@ jobs: strategy: fail-fast: false matrix: - # These are reproducibility pins, not floating series selectors. - # Upgrade them together in a dedicated toolchain-update PR. + # Go 1.25 is an explicit compatibility target. The primary toolchain + # is read from .go-version after checkout. os: [macos-latest, ubuntu-latest] - go-version: ["1.25.0", "1.26.5"] + toolchain: [go1.25, current] shard-index: ["0", "1", "2", "3"] include: - - go-version: "1.25.0" + - toolchain: go1.25 + go-version: "1.25.11" + go-label: "1.25.11" lane: compatibility - - go-version: "1.26.5" + - toolchain: current + go-version: "" + go-label: current lane: primary - # Keep both supported runtime generations on Linux and macOS. Go 1.25 - # is intentionally omitted because it is not a compatibility target. + # Keep the compatibility and primary toolchains on Linux and macOS. runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v7 @@ -45,7 +48,14 @@ jobs: with: llvm-version: 19 - - name: Set up Go + - name: Set up Go for building llgo and the runner + uses: ./.github/actions/setup-go + + - name: Build llgo and the GOROOT runner + run: | + LLGO_BUILD_GOROOT_RUNNER=1 dev/build_ci_tools.sh "$RUNNER_TEMP/llgo-bin" + + - name: Set up target Go uses: ./.github/actions/setup-go with: go-version: ${{ matrix.go-version }} @@ -56,11 +66,11 @@ jobs: go env GOPROXY GOMODCACHE clang --version - - name: Download Go modules - timeout-minutes: 10 - run: go mod download - - name: Run GOROOT runner + env: + GOTOOLCHAIN: local + LLGO_GOROOT_RUNNER: ${{ runner.temp }}/llgo-bin/goroot-runner + LLGO_TEST_LLGO: ${{ runner.temp }}/llgo-bin/llgo run: | min_swap_free_mib=512 if [[ "$RUNNER_OS" == "macOS" ]]; then @@ -68,8 +78,9 @@ jobs: # does not mean the runner cannot make forward progress. min_swap_free_mib=0 fi + target_goroot="$(go env GOROOT)" set +e - bash dev/test_goroot.sh -- \ + bash dev/test_goroot.sh "$target_goroot" -- \ -directive-mode ci \ -min-swap-free-mib "$min_swap_free_mib" \ -progress 60s \ @@ -83,9 +94,10 @@ jobs: if: always() env: MATRIX_OS: ${{ matrix.os }} - GO_VERSION: ${{ matrix.go-version }} SHARD_INDEX: ${{ matrix.shard-index }} run: | + GO_VERSION="$(GOTOOLCHAIN=local go env GOVERSION)" + GO_VERSION="${GO_VERSION#go}" log="$RUNNER_TEMP/goroot.log" report_dir="$RUNNER_TEMP/goroot-report" mkdir -p "$report_dir" @@ -139,7 +151,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: goroot-stat-${{ matrix.os }}-${{ matrix.go-version }}-${{ matrix.shard-index }} + name: goroot-stat-${{ matrix.os }}-${{ matrix.toolchain }}-${{ matrix.shard-index }} path: ${{ runner.temp }}/goroot-report if-no-files-found: error retention-days: 7 @@ -167,6 +179,12 @@ jobs: find reports -type f -name 'failures-*.tsv' -exec cat {} + >"$failure_tsv" report_count=$(wc -l <"$summary_tsv" | tr -d ' ') + compatibility_version=$(awk -F '\t' '$2 ~ /^1\.25\./ { print $2; exit }' "$summary_tsv") + primary_version=$(awk -F '\t' '$2 ~ /^1\.26\./ { print $2; exit }' "$summary_tsv") + if [[ -z "$compatibility_version" || -z "$primary_version" ]]; then + echo "error: could not determine compatibility and primary Go versions" >&2 + exit 1 + fi write_row() { local label=$1 local platform=$2 @@ -204,11 +222,11 @@ jobs: echo echo '| Platform / toolchain | Shards | Selected | Observed | Passed | Failed | Skipped |' echo '|---|---:|---:|---:|---:|---:|---:|' - write_row 'Darwin · Go 1.25.0' darwin/arm64 1.25.0 4 - write_row 'Darwin · Go 1.26.5' darwin/arm64 1.26.5 4 + write_row "Darwin · Go $compatibility_version" darwin/arm64 "$compatibility_version" 4 + write_row "Darwin · Go $primary_version" darwin/arm64 "$primary_version" 4 write_row '**Darwin total**' darwin/arm64 '' 8 - write_row 'Linux · Go 1.25.0' linux/amd64 1.25.0 4 - write_row 'Linux · Go 1.26.5' linux/amd64 1.26.5 4 + write_row "Linux · Go $compatibility_version" linux/amd64 "$compatibility_version" 4 + write_row "Linux · Go $primary_version" linux/amd64 "$primary_version" 4 write_row '**Linux total**' linux/amd64 '' 8 echo echo '_Passed means the runner classification succeeded; expected xfail/not-applicable failures and classified flakes are counted as Passed._' diff --git a/.github/workflows/llgo.yml b/.github/workflows/llgo.yml index d37781368e..b8b560d27a 100644 --- a/.github/workflows/llgo.yml +++ b/.github/workflows/llgo.yml @@ -16,33 +16,17 @@ concurrency: jobs: llgo: - name: llgo (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) - continue-on-error: ${{ matrix.lane == 'compatibility' }} + name: llgo (${{ matrix.os }}, LLVM ${{ matrix.llvm }}) timeout-minutes: 60 strategy: matrix: - # Compatibility results are tied to these exact patch releases. - # Keep the supported Go 1.25 and 1.26 endpoints on macOS so user - # projects exercise both runtime generations there. macOS Intel is - # covered by the release artifact smoke test, avoiding a duplicate - # 35-minute demo job. + # The compiler and all repository tooling are built only with Go 1.26. + # The runtime and demos keep their own module language versions. include: - os: ubuntu-latest llvm: 19 - go: "1.25.0" - lane: compatibility - - os: ubuntu-latest - llvm: 19 - go: "1.26.5" - lane: primary - - os: macos-latest - llvm: 19 - go: "1.25.0" - lane: compatibility - os: macos-latest llvm: 19 - go: "1.26.5" - lane: primary runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -63,10 +47,9 @@ jobs: go install ./... echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - - name: Set up Go for testing - uses: ./.github/actions/setup-go - with: - go-version: ${{matrix.go}} + - name: Test runtime module compatibility + if: matrix.os == 'ubuntu-latest' + run: dev/test_runtime_go_versions.sh - name: Test demo without RPATH (expect failure) run: | @@ -124,11 +107,10 @@ jobs: run: cat result.md - name: Install LLDB for integration tests - if: ${{ matrix.os == 'ubuntu-latest' && matrix.lane == 'primary' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: sudo apt-get install -y lldb-${{matrix.llvm}} - name: LLDB integration tests - if: ${{ matrix.lane == 'primary' }} run: | echo "Test lldb with llgo plugin on ${{matrix.os}} with LLVM ${{matrix.llvm}}" bash cmd/llgo/lldbtest/runtest.sh -v @@ -138,34 +120,92 @@ jobs: run: go test -timeout 15m ./internal/build -run '^TestStandardDWARF$' -count=1 -v test: - name: test (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}, shard ${{ matrix.shard }}) - continue-on-error: ${{ matrix.lane == 'compatibility' }} - timeout-minutes: ${{ startsWith(matrix.os, 'macos') && 45 || 30 }} + name: test (${{ matrix.lane }}, ${{ matrix.os }}, Go ${{ matrix.go_version }}, shard ${{ matrix.shard_index }}/${{ matrix.shard_total }}) + timeout-minutes: ${{ matrix.lane == 'full' && (startsWith(matrix.os, 'macos') && 45 || 40) || 30 }} strategy: + fail-fast: false matrix: - # Keep compatibility and primary toolchains pinned to exact patches. - os: - - macos-latest - - ubuntu-latest - llvm: [19] - go: ["1.25.0", "1.26.5"] - # In-command package parallelism lets Ubuntu use two shards while - # retaining headroom for the serial std build-mode checks. - shard: ["0", "1"] + # LLGo and the checker are always built with .go-version. Tests then + # switch to the exact target toolchain and a matching alternate + # modfile, so release tags and standard-library APIs are genuine. include: - - go: "1.25.0" + - os: ubuntu-latest + llvm: 19 + go_version: "1.20" lane: compatibility - - go: "1.26.5" - lane: primary - exclude: - # The full demo lane above exercises Go 1.25 user-project/runtime - # compatibility on macOS. Keep the much larger per-package - # compatibility matrix on Ubuntu and use one parallel primary shard - # on macOS. + shard_index: "0" + shard_total: "1" + check_symbols: true + - os: macos-latest + llvm: 19 + go_version: "1.21" + lane: compatibility + shard_index: "0" + shard_total: "1" + check_symbols: true + - os: ubuntu-latest + llvm: 19 + go_version: "1.22" + lane: compatibility + shard_index: "0" + shard_total: "1" + check_symbols: true + - os: macos-latest + llvm: 19 + go_version: "1.23" + lane: compatibility + shard_index: "0" + shard_total: "1" + check_symbols: true + - os: ubuntu-latest + llvm: 19 + go_version: "1.24" + lane: compatibility + shard_index: "0" + shard_total: "1" + check_symbols: true + - os: ubuntu-latest + llvm: 19 + go_version: "1.25" + lane: full + shard_index: "0" + shard_total: "2" + check_symbols: true + - os: ubuntu-latest + llvm: 19 + go_version: "1.25" + lane: full + shard_index: "1" + shard_total: "2" + check_symbols: true - os: macos-latest - go: "1.25.0" + llvm: 19 + go_version: "1.25" + lane: full + shard_index: "0" + shard_total: "1" + - os: ubuntu-latest + llvm: 19 + go_version: "1.26" + lane: full + shard_index: "0" + shard_total: "2" + check_symbols: true + std_buildmodes: true + - os: ubuntu-latest + llvm: 19 + go_version: "1.26" + lane: full + shard_index: "1" + shard_total: "2" + check_symbols: true + std_buildmodes: true - os: macos-latest - shard: "1" + llvm: 19 + go_version: "1.26" + lane: full + shard_index: "0" + shard_total: "1" runs-on: ${{matrix.os}} steps: - uses: actions/checkout@v7 @@ -174,6 +214,7 @@ jobs: with: llvm-version: ${{matrix.llvm}} - name: Install further optional dependencies for demos + if: matrix.lane == 'full' run: | py_deps=( numpy # for github.com/goplus/lib/py/numpy @@ -184,78 +225,44 @@ jobs: - name: Set up Go for build uses: ./.github/actions/setup-go - - name: Install + - name: Build and install llgo run: | - go install ./... + dev/build_ci_tools.sh "$RUNNER_TEMP/llgo-bin" echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - - name: Set up Go for testing - uses: ./.github/actions/setup-go - with: - go-version: ${{matrix.go}} - - name: run llgo test + - name: Run versioned llgo tests env: - SHARD_INDEX: ${{ matrix.shard }} - SHARD_TOTAL: ${{ startsWith(matrix.os, 'macos') && '1' || '2' }} - TEST_JOBS: ${{ startsWith(matrix.os, 'macos') && '3' || '4' }} + LLGO: ${{ runner.temp }}/llgo-bin/llgo + CHECK_STD_SYMBOLS: ${{ runner.temp }}/llgo-bin/check_std_symbols + LLGO_BUILD_CACHE: "1" + LLGO_TEST_BENCH_GO126: "1" + LLGO_TEST_CHECK_SYMBOLS: ${{ matrix.check_symbols && '1' || '0' }} + LLGO_TEST_JOBS: ${{ startsWith(matrix.os, 'macos') && '3' || '4' }} + LLGO_TEST_STD_BUILDMODES: ${{ matrix.std_buildmodes && '1' || '0' }} + SHARD_INDEX: ${{ matrix.shard_index }} + SHARD_TOTAL: ${{ matrix.shard_total }} run: | - set -euo pipefail - - pkgs=() - while IFS= read -r pkg; do - pkgs+=("${pkg}") - done < <(go list -tags=llgo ./test/... | sort) - - selected=() - for i in "${!pkgs[@]}"; do - if (( i % SHARD_TOTAL == SHARD_INDEX )); then - selected+=("${pkgs[$i]}") - fi - done - - echo "Shard: ${SHARD_INDEX}/${SHARD_TOTAL}, selected: ${#selected[@]} package(s)" - if [ "${#selected[@]}" -eq 0 ]; then - echo "No packages in this shard." - exit 0 - fi - printf ' %s\n' "${selected[@]}" - - std_pkgs=() - for pkg in "${selected[@]}"; do - if [[ "${{ matrix.os }}" == ubuntu-latest && "${{ matrix.go }}" == 1.26.5 && "${pkg}" == */test/std/* ]]; then - std_pkgs+=("${pkg}") - fi - done - - echo "==> llgo test -p=${TEST_JOBS} (${#selected[@]} packages)" - SECONDS=0 - llgo test -p="${TEST_JOBS}" -timeout=20m -bench='^BenchmarkGo126' -benchtime=1x "${selected[@]}" - echo "==> llgo test done (${SECONDS}s)" - - if [[ "${#std_pkgs[@]}" -ne 0 ]]; then - dev/test_std_buildmodes.sh "${std_pkgs[@]}" - fi + dev/test_go_version.sh "${{ matrix.go_version }}" hello: - name: hello (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go }}) - continue-on-error: ${{ matrix.lane == 'compatibility' }} + name: hello (${{ matrix.lane }}, ${{ matrix.os }}, LLVM ${{ matrix.llvm }}, Go ${{ matrix.go_version }}) timeout-minutes: 30 strategy: matrix: include: - os: ubuntu-latest llvm: 19 - go: "1.25.0" + go_version: "1.20" lane: compatibility - os: ubuntu-latest llvm: 19 - go: "1.26.5" + go_version: "1.26" lane: primary # Keep the Go 1.26 user-module compatibility matrix on both host # platforms; release artifact smoke tests alone only cover go 1.26. - os: macos-latest llvm: 19 - go: "1.26.5" + go_version: "1.26" lane: primary runs-on: ${{matrix.os}} steps: @@ -270,40 +277,47 @@ jobs: - name: Install llgo run: | - go install ./... + dev/build_ci_tools.sh "$RUNNER_TEMP/llgo-bin" + echo "$RUNNER_TEMP/llgo-bin" >> $GITHUB_PATH echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - - name: Set up Go for testing - uses: ./.github/actions/setup-go + + - name: Test Hello World with go.mod 1.20 + uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go}} + go-version: ${{ matrix.go_version }} + embedded: "false" + mod-version: "1.20" - name: Test Hello World with go.mod 1.21 - if: startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') + if: matrix.go_version == '1.26' uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go}} + go-version: ${{ matrix.go_version }} + embedded: "false" mod-version: "1.21" - name: Test Hello World with go.mod 1.22 - if: startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') + if: matrix.go_version == '1.26' uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go}} + go-version: ${{ matrix.go_version }} + embedded: "false" mod-version: "1.22" - name: Test Hello World with go.mod 1.24 - if: startsWith(matrix.go, '1.25') || startsWith(matrix.go, '1.26') + if: matrix.go_version == '1.26' uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go}} + go-version: ${{ matrix.go_version }} + embedded: "false" mod-version: "1.24" - name: Test Hello World with go.mod 1.26 - if: startsWith(matrix.go, '1.26') + if: matrix.go_version == '1.26' uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go}} + go-version: ${{ matrix.go_version }} mod-version: "1.26" cross-compile: @@ -356,11 +370,14 @@ jobs: iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm wasm-runtime: + name: wasm-runtime (Go ${{ matrix.go_version }}) timeout-minutes: 30 strategy: fail-fast: false matrix: - go: ["1.25.0", "1.26.5"] + include: + - go_version: "1.24" + - go_version: "1.26" runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -379,17 +396,12 @@ jobs: - name: Install llgo run: | - go install ./... + dev/build_ci_tools.sh "$RUNNER_TEMP/llgo-bin" echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV - - name: Set up Go for testing - uses: ./.github/actions/setup-go - with: - go-version: ${{matrix.go}} - - name: Build standard runtime for wasm shell: bash - run: | - GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-js.wasm" ./internal/build/testdata/wasm-runtime - GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-wasip1.wasm" ./internal/build/testdata/wasm-runtime - file "$RUNNER_TEMP/runtime-js.wasm" "$RUNNER_TEMP/runtime-wasip1.wasm" + env: + LLGO: ${{ runner.temp }}/llgo-bin/llgo + LLGO_BUILD_CACHE: "1" + run: dev/test_wasm_runtime_go_version.sh "${{ matrix.go_version }}" diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 7f367be4d4..428de0ac11 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -15,7 +15,7 @@ concurrency: env: # Container tags cannot use Go patch selectors. Upgrade this exact pin in - # the same periodic toolchain-update PR as the setup-go default. + # the same periodic toolchain-update PR as .go-version. GORELEASER_CROSS_IMAGE: ghcr.io/goreleaser/goreleaser-cross:v1.26.4 jobs: @@ -135,22 +135,18 @@ jobs: - os: macos-15-intel goos: darwin goarch: amd64 - go-version: "1.26.5" go-mod-version: "1.26" - os: macos-latest goos: darwin goarch: arm64 - go-version: "1.26.5" go-mod-version: "1.26" - os: ubuntu-latest goos: linux goarch: amd64 - go-version: "1.26.5" go-mod-version: "1.26" - os: ubuntu-24.04-arm goos: linux goarch: arm64 - go-version: "1.26.5" go-mod-version: "1.26" runs-on: ${{ matrix.os }} steps: @@ -161,8 +157,6 @@ jobs: install-llvm: false - name: Set up Go uses: ./.github/actions/setup-go - with: - go-version: ${{ matrix.go-version }} - name: Download Platform Artifact uses: actions/download-artifact@v8 with: @@ -181,7 +175,6 @@ jobs: - name: Test Hello World uses: ./.github/actions/test-helloworld with: - go-version: ${{matrix.go-version}} mod-version: ${{ matrix.go-mod-version }} release: diff --git a/.go-version b/.go-version new file mode 100644 index 0000000000..8fe00a57fe --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.26.5 diff --git a/README.md b/README.md index e55253a125..3b875f42cb 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,9 @@ Here are the Go packages that can be imported correctly: ## Dependencies -- [Go 1.25+](https://go.dev) +- [Go 1.26](https://go.dev) for building llgo. The runtime and demo modules + retain their declared Go 1.20 compatibility floor unless they use newer + language or standard-library features. - [LLVM 18](https://llvm.org) - [Clang 18](https://clang.llvm.org) - [LLD 18](https://lld.llvm.org) diff --git a/chore/check_std_symbols/main.go b/chore/check_std_symbols/main.go index c564104843..11dc48c2c4 100644 --- a/chore/check_std_symbols/main.go +++ b/chore/check_std_symbols/main.go @@ -3,7 +3,7 @@ // // Usage: // -// go run ./chore/check_std_symbols -pkg math [-pkg strings ...] +// go run ./chore/check_std_symbols [-modfile test.mod] -pkg math [-pkg strings ...] // // The tool compares exported symbols from a package (via go doc) with // symbols used in test files, reporting any uncovered exported identifiers. @@ -94,15 +94,17 @@ var testingCommonEmbeddedIn = []string{"T", "B", "F"} func main() { var specs pkgSpecs + var modfile string var verbose bool flag.Var(&specs, "pkg", "package coverage check in the form =") + flag.StringVar(&modfile, "modfile", "", "alternate go.mod used to load version-tagged tests") flag.BoolVar(&verbose, "v", false, "display coverage status for each exported symbol") flag.Parse() debugUsed := os.Getenv("DEBUG_USED_SYMBOLS") != "" if len(specs) == 0 { - fmt.Fprintln(os.Stderr, "usage: go run ./chore/check_std_symbols -pkg math [-pkg strings ...]") + fmt.Fprintln(os.Stderr, "usage: go run ./chore/check_std_symbols [-modfile test.mod] -pkg math [-pkg strings ...]") os.Exit(2) } @@ -111,6 +113,13 @@ func main() { fmt.Fprintf(os.Stderr, "failed to determine working directory: %v\n", err) os.Exit(2) } + if modfile != "" { + modfile, err = filepath.Abs(modfile) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to resolve modfile: %v\n", err) + os.Exit(2) + } + } var failures int @@ -134,7 +143,7 @@ func main() { failures++ continue } - used, err := usedSymbols(testDir, spec.pkgPath) + used, err := usedSymbols(testDir, spec.pkgPath, modfile) if err != nil { fmt.Fprintf(os.Stderr, "failed to inspect tests in %s: %v\n", spec.testDir, err) failures++ @@ -276,12 +285,15 @@ func receiverTypeName(recv string) string { return parseIdentifier(typ) } -func usedSymbols(testDir, targetPkg string) (map[string]bool, error) { +func usedSymbols(testDir, targetPkg, modfile string) (map[string]bool, error) { cfg := &packages.Config{ Mode: packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports, Dir: testDir, Tests: true, } + if modfile != "" { + cfg.BuildFlags = []string{"-modfile=" + modfile} + } pkgs, err := packages.Load(cfg, ".") if err != nil { return nil, fmt.Errorf("packages.Load: %w", err) diff --git a/cmd/internal/flags/flags_test.go b/cmd/internal/flags/flags_test.go index c5ec81dae7..ea0a2d4cd3 100644 --- a/cmd/internal/flags/flags_test.go +++ b/cmd/internal/flags/flags_test.go @@ -17,7 +17,7 @@ import ( func TestApplyGoBuildFlags(t *testing.T) { cmd := new(base.Command) captured := CaptureGoBuildFlags(cmd) - if err := cmd.Flag.Parse([]string{"-ldflags=-s -w", "-gcflags=all=-N", "."}); err != nil { + if err := cmd.Flag.Parse([]string{"-ldflags=-s -w", "-gcflags=all=-N", "-modfile=test.mod", "."}); err != nil { t.Fatal(err) } @@ -25,7 +25,7 @@ func TestApplyGoBuildFlags(t *testing.T) { if err := ApplyGoBuildFlags(conf, captured.Args); err != nil { t.Fatal(err) } - want := []string{"-tags=existing", "-ldflags=-s -w", "-gcflags=all=-N"} + want := []string{"-tags=existing", "-ldflags=-s -w", "-gcflags=all=-N", "-modfile=test.mod"} if !reflect.DeepEqual(conf.GoBuildFlags, want) { t.Fatalf("GoBuildFlags = %v, want %v", conf.GoBuildFlags, want) } diff --git a/cmd/internal/flags/gobuild.go b/cmd/internal/flags/gobuild.go index f5b868362f..0c561450ad 100644 --- a/cmd/internal/flags/gobuild.go +++ b/cmd/internal/flags/gobuild.go @@ -29,7 +29,7 @@ func CaptureGoBuildFlags(cmd *base.Command) *base.PassArgs { p := base.NewPassArgs(&cmd.Flag) p.Bool("n") p.Bool("linkshared", "race", "msan", "asan", "trimpath", "work") - p.Var("p", "asmflags", "compiler", "gcflags", "gccgoflags", "installsuffix", "ldflags", "pkgdir", "toolexec", "buildvcs") + p.Var("p", "asmflags", "compiler", "gcflags", "gccgoflags", "installsuffix", "ldflags", "modfile", "pkgdir", "toolexec", "buildvcs") return p } diff --git a/dev/README.md b/dev/README.md index 518bb05a93..6feb66e212 100644 --- a/dev/README.md +++ b/dev/README.md @@ -50,16 +50,59 @@ From anywhere inside the repo: ./dev/llgo_wasm.sh build ./... ``` -## 4) Run local CI (covers most checks) +## 4) Run versioned tests + +Run one exact Go toolchain against its default representative/full package set: + +```bash +./dev/test_go_version.sh 1.20 +./dev/test_go_version.sh 1.24 ./test/std/bytes +``` + +Run the complete Go 1.20 through Go 1.26 integration matrix: + +```bash +./dev/test_go_versions.sh +``` + +This integration command is intentionally sequential and may take tens of +minutes locally. CI invokes `test_go_version.sh` in separate versioned jobs +instead of running the full integration script in one job. + +The corresponding wasm runtime commands are: + +```bash +./dev/test_wasm_runtime_go_version.sh 1.24 +./dev/test_wasm_runtime_go_versions.sh +``` + +The native runtime module has matching single-version and integration entries: + +```bash +./dev/test_runtime_go_version.sh 1.20 +./dev/test_runtime_go_versions.sh +``` + +Any standalone task can be run under an exact target toolchain with: + +```bash +./dev/with_go_version.sh 1.20 ./dev/test_helloworld.sh 1.20 +``` + +All scripts select exact toolchains, set `GOTOOLCHAIN=local` while testing, and +build llgo and repository tools with the version pinned in `.go-version`. + +## 5) Run local CI (covers most checks) ```bash ./dev/local_ci.sh ``` -This script creates a temporary workspace, runs formatting/build/tests, runs `llgo test`, and then runs demo checks. +This script creates a temporary workspace, runs formatting/build/tests, runs the +complete versioned llgo test integration script, and then runs demo checks. You can control demo parallelism via `LLGO_DEMO_JOBS` (defaults to up to 4 jobs). -## 5) `dev/docker.sh` (composition-friendly) +## 6) `dev/docker.sh` (composition-friendly) `dev/docker.sh` is a thin wrapper around `docker compose`: @@ -72,7 +115,7 @@ You can control demo parallelism via `LLGO_DEMO_JOBS` (defaults to up to 4 jobs) - If `[command...]` is provided, it runs that command and exits. - You must run it from within the repo (within `LLGO_ROOT`), and it will start in the matching repo subdirectory inside the container. -## 6) Refresh test goldens +## 7) Refresh test goldens LLGo has separate refresh flows for runtime data and LLVM IR checks: diff --git a/dev/build_ci_tools.sh b/dev/build_ci_tools.sh new file mode 100755 index 0000000000..c2a44784cd --- /dev/null +++ b/dev/build_ci_tools.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${repo_root}/dev/go_toolchain.sh" + +if [[ $# -ne 1 ]]; then + echo "usage: $0 " >&2 + exit 2 +fi + +output_dir=$1 +mkdir -p "${output_dir}" +output_dir="$(cd "${output_dir}" && pwd)" + +build_version="$(llgo_resolve_go_version "${repo_root}" 1.26)" +build_root="$(llgo_go_root "${build_version}")" +build_go="${build_root}/bin/go" + +echo "Building LLGo tools with go${build_version} into ${output_dir}" +( + cd "${repo_root}" + GOBIN="${output_dir}" GOTOOLCHAIN=local "${build_go}" install ./... + if [[ "${LLGO_BUILD_GOROOT_RUNNER:-}" == 1 ]]; then + GOTOOLCHAIN=local "${build_go}" build -tags=dev \ + -o "${output_dir}/llgo" ./cmd/llgo + GOTOOLCHAIN=local "${build_go}" test -c \ + -o "${output_dir}/goroot-runner" ./test/goroot + fi +) diff --git a/dev/go_toolchain.sh b/dev/go_toolchain.sh new file mode 100755 index 0000000000..fe6830b29d --- /dev/null +++ b/dev/go_toolchain.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Shared helpers for selecting exact Go toolchains in local and CI scripts. +# This file is intended to be sourced. + +llgo_resolve_go_version() { + local repo_root=$1 + local requested=$2 + case "${requested}" in + 1.20) printf '%s\n' 1.20.14 ;; + 1.21) printf '%s\n' 1.21.13 ;; + 1.22) printf '%s\n' 1.22.12 ;; + 1.23) printf '%s\n' 1.23.12 ;; + 1.24) printf '%s\n' 1.24.13 ;; + 1.25) printf '%s\n' 1.25.11 ;; + 1.26) tr -d '[:space:]' <"${repo_root}/.go-version" ;; + *) + if [[ "${requested}" =~ ^1\.2[0-6]\.[0-9]+$ ]]; then + printf '%s\n' "${requested}" + else + return 2 + fi + ;; + esac +} + +llgo_go_root() { + local version=$1 + local current_version + local toolchain_root + + current_version="$(GOTOOLCHAIN=local go env GOVERSION 2>/dev/null || true)" + if [[ "${current_version}" == "go${version}" ]]; then + toolchain_root="$(GOTOOLCHAIN=local go env GOROOT)" + else + toolchain_root="$(GOTOOLCHAIN="go${version}" go env GOROOT)" + fi + + if [[ ! -x "${toolchain_root}/bin/go" ]]; then + echo "missing go binary for go${version}: ${toolchain_root}/bin/go" >&2 + return 1 + fi + if [[ "$(GOTOOLCHAIN=local "${toolchain_root}/bin/go" env GOVERSION)" != "go${version}" ]]; then + echo "failed to select exact Go toolchain go${version}" >&2 + return 1 + fi + printf '%s\n' "${toolchain_root}" +} diff --git a/dev/local_ci.sh b/dev/local_ci.sh index 0e984a5cda..ba9e510bfe 100755 --- a/dev/local_ci.sh +++ b/dev/local_ci.sh @@ -95,48 +95,13 @@ ensure_llama2_model run_hello() { local mod_version="$1" - local hello_dir="$tmp_root/helloworld-$mod_version" - rm -rf "$hello_dir" || true - mkdir -p "$hello_dir" - cat >"$hello_dir/go.mod" <"$hello_dir/main.go" <<'EOF' -package main - -import ( - "fmt" - "github.com/goplus/lib/c" - "github.com/goplus/lib/cpp/std" -) - -func main() { - fmt.Println("Hello from fmt.Println") - println("Hello from builtin println") - c.Printf(c.Str("Hello from c printf\n")) - c.Printf(std.Str("Hello from cpp/std.Str\n").CStr()) -} -EOF - (cd "$hello_dir" && go mod tidy) - local output - if ! output=$(cd "$hello_dir" && llgo run . 2>&1); then - printf "%s\n" "$output" - exit 1 + local go_version=1.26 + if [[ "$mod_version" == 1.20 ]]; then + go_version=1.20 fi - local expected=( - "Hello from fmt.Println" - "Hello from builtin println" - "Hello from c printf" - "Hello from cpp/std.Str" - ) - for want in "${expected[@]}"; do - if ! grep -Fqx "$want" <<<"$output"; then - printf "%s\n" "$output" - echo "missing output: $want" >&2 - exit 1 - fi - done + LLGO="$(command -v llgo)" LLGO_HELLO_EMBED=0 \ + "$repo_root/dev/with_go_version.sh" "$go_version" \ + "$repo_root/dev/test_helloworld.sh" "$mod_version" } log_section "Format" @@ -161,6 +126,9 @@ log_section "Go Build" log_section "Go Test" (cd "$workdir" && go test -timeout 30m ./...) +log_section "Runtime module with Go 1.20 and Go 1.26" +(cd "$workdir" && dev/test_runtime_go_versions.sh) + log_section "Install llgo" (cd "$workdir" && go install -tags=dev ./cmd/llgo) gobin="$(cd "$workdir" && go env GOBIN)" @@ -170,8 +138,8 @@ if [ -z "$gobin" ]; then fi export PATH="$gobin:$PATH" -log_section "llgo test" -(cd "$workdir" && llgo test -timeout 30m ./...) +log_section "llgo test with Go 1.20 through Go 1.26" +(cd "$workdir" && LLGO="$(command -v llgo)" dev/test_go_versions.sh) log_section "Demo Tests" demo_jobs="${LLGO_DEMO_JOBS:-}" @@ -194,7 +162,7 @@ log_section "Build targets" log_section "Hello World" hello_logs=() -for mod in 1.21 1.22 1.23 1.24; do +for mod in 1.20 1.21 1.22 1.23 1.24 1.26; do log="$tmp_root/hello-${mod}.log" (run_hello "$mod") >"$log" 2>&1 & hello_logs+=("$log:$!") diff --git a/dev/test_go_version.sh b/dev/test_go_version.sh new file mode 100755 index 0000000000..872d846009 --- /dev/null +++ b/dev/test_go_version.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +set -euo pipefail + +root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${root_dir}/dev/go_toolchain.sh" +cd "${root_dir}" + +usage() { + echo "usage: $0 <1.20|...|1.26|exact-version> [package ...]" >&2 + exit 2 +} + +if [[ $# -eq 0 ]]; then + usage +fi + +requested="$1" +shift +if ! target_version="$(llgo_resolve_go_version "${root_dir}" "${requested}")"; then + usage +fi +target_minor="${target_version%.*}" + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/llgo-test-go.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT + +tools_built=0 +build_ci_tools() { + if [[ "${tools_built}" == 1 ]]; then + return + fi + dev/build_ci_tools.sh "${work_dir}/bin" + tools_built=1 +} + +llgo_cmd="${LLGO:-}" +if [[ -z "${llgo_cmd}" ]]; then + build_ci_tools + llgo_cmd="${work_dir}/bin/llgo" +elif [[ "${llgo_cmd}" != */* ]]; then + llgo_cmd="$(command -v "${llgo_cmd}")" +elif [[ "${llgo_cmd}" != /* ]]; then + llgo_cmd="$(cd "$(dirname "${llgo_cmd}")" && pwd)/$(basename "${llgo_cmd}")" +fi + +llgen_cmd="${LLGO_TEST_LLGEN:-}" +if [[ -z "${llgen_cmd}" && -x "$(dirname "${llgo_cmd}")/llgen" ]]; then + llgen_cmd="$(dirname "${llgo_cmd}")/llgen" +fi +if [[ -z "${llgen_cmd}" ]]; then + build_ci_tools + llgen_cmd="${work_dir}/bin/llgen" +fi + +check_std_symbols="${CHECK_STD_SYMBOLS:-}" +if [[ "${LLGO_TEST_CHECK_SYMBOLS:-}" == 1 && -z "${check_std_symbols}" ]]; then + build_ci_tools + check_std_symbols="${work_dir}/bin/check_std_symbols" +fi + +target_root="$(llgo_go_root "${target_version}")" +target_go="${target_root}/bin/go" +actual_version="$(cd "${work_dir}" && GOTOOLCHAIN=local "${target_go}" env GOVERSION)" +if [[ "${actual_version}" != "go${target_version}" ]]; then + echo "expected go${target_version}, got ${actual_version}" >&2 + exit 1 +fi + +modfile="${work_dir}/test.mod" +cp .github/test-go.mod "${modfile}" +cp .github/test-go.sum "${work_dir}/test.sum" +GOTOOLCHAIN=local "${target_go}" mod edit \ + -modfile="${modfile}" \ + -go="${target_minor}" \ + -replace="github.com/xgo-dev/llgo/runtime=${root_dir}/runtime" + +export PATH="${target_root}/bin:${PATH}" +export GOTOOLCHAIN=local +export GOWORK=off +export GOENV=off +export GOFLAGS= +export LLGO_ROOT="${root_dir}" +export LLGO_TEST_LLGO="${llgo_cmd}" +export LLGO_TEST_LLGEN="${llgen_cmd}" +export LLGO_TEST_MODFILE="${modfile}" +# LLGo's shared cache may contain standard-library objects from another Go +# release. CI jobs are isolated and may opt back in explicitly. +export LLGO_BUILD_CACHE="${LLGO_BUILD_CACHE:-off}" + +requested_packages=("$@") +if [[ "${#requested_packages[@]}" -eq 0 ]]; then + case "${target_minor}" in + 1.20|1.21) + requested_packages=(./test/std/bufio ./test/std/bytes ./test/std/encoding/json ./test/std/math/bits ./test/goroot) + ;; + 1.22) + requested_packages=(./test/std/bufio ./test/std/bytes ./test/std/encoding/json ./test/std/go/version ./test/goroot) + ;; + 1.23) + requested_packages=(./test/std/iter ./test/std/maps ./test/std/slices ./test/std/structs ./test/std/unique ./test/goroot) + ;; + 1.24) + requested_packages=(./test/std/bytes ./test/std/crypto/hkdf ./test/std/crypto/pbkdf2 ./test/std/weak ./test/goroot) + ;; + *) requested_packages=(./test/...) ;; + esac +fi + +packages_file="${work_dir}/packages.txt" +go list -modfile="${modfile}" -tags=llgo "${requested_packages[@]}" | sort -u >"${packages_file}" +packages=() +while IFS= read -r package; do + packages+=("${package}") +done <"${packages_file}" + +shard_index="${SHARD_INDEX:-0}" +shard_total="${SHARD_TOTAL:-1}" +if (( shard_total < 1 || shard_index < 0 || shard_index >= shard_total )); then + echo "invalid shard ${shard_index}/${shard_total}" >&2 + exit 2 +fi +selected=() +for i in "${!packages[@]}"; do + if (( i % shard_total == shard_index )); then + selected+=("${packages[$i]}") + fi +done +if [[ "${#selected[@]}" -eq 0 ]]; then + echo "no packages selected for shard ${shard_index}/${shard_total}" >&2 + exit 1 +fi + +echo "Go toolchain: ${actual_version} (${target_root})" +echo "LLGo: ${llgo_cmd}" +echo "Shard: ${shard_index}/${shard_total}; packages: ${#selected[@]}" +printf ' %s\n' "${selected[@]}" + +test_flags=(-p="${LLGO_TEST_JOBS:-4}" -timeout="${LLGO_TEST_TIMEOUT:-20m}" -modfile="${modfile}") +if [[ "${LLGO_TEST_COMPILE_ONLY:-}" == 1 ]]; then + test_flags+=(-run='^$') +fi +if [[ "${LLGO_TEST_BENCH_GO126:-}" == 1 && "${target_minor}" == 1.26 ]]; then + test_flags+=(-bench='^BenchmarkGo126' -benchtime=1x) +fi +SECONDS=0 +"${llgo_cmd}" test "${test_flags[@]}" "${selected[@]}" +echo "llgo test completed in ${SECONDS}s" + +std_packages=() +for package in "${selected[@]}"; do + case "${package}" in + github.com/xgo-dev/llgo/test/std/*) std_packages+=("${package}") ;; + esac +done + +if [[ "${LLGO_TEST_CHECK_SYMBOLS:-}" == 1 && "${#std_packages[@]}" -ne 0 ]]; then + SECONDS=0 + LLGO_TEST_MODFILE="${modfile}" \ + CHECK_STD_SYMBOLS="${check_std_symbols}" \ + doc/_readme/scripts/check_std_cover.sh "${std_packages[@]}" + echo "standard-library symbol check completed in ${SECONDS}s" +fi +if [[ "${LLGO_TEST_STD_BUILDMODES:-}" == 1 && "${#std_packages[@]}" -ne 0 ]]; then + SECONDS=0 + LLGO="${llgo_cmd}" LLGO_TEST_MODFILE="${modfile}" \ + dev/test_std_buildmodes.sh "${std_packages[@]}" + echo "standard-library build-mode checks completed in ${SECONDS}s" +fi diff --git a/dev/test_go_versions.sh b/dev/test_go_versions.sh new file mode 100755 index 0000000000..01c67ad2b1 --- /dev/null +++ b/dev/test_go_versions.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +versions=("$@") +if [[ ${#versions[@]} -eq 0 ]]; then + versions=(1.20 1.21 1.22 1.23 1.24 1.25 1.26) +fi + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/llgo-test-go-versions.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT + +llgo_cmd="${LLGO:-}" +check_std_symbols="${CHECK_STD_SYMBOLS:-}" +check_symbols="${LLGO_TEST_CHECK_SYMBOLS:-1}" +if [[ -z "${llgo_cmd}" || ( "${check_symbols}" == 1 && -z "${check_std_symbols}" ) ]]; then + dev/build_ci_tools.sh "${work_dir}/bin" + if [[ -z "${llgo_cmd}" ]]; then + llgo_cmd="${work_dir}/bin/llgo" + fi + if [[ -z "${check_std_symbols}" ]]; then + check_std_symbols="${work_dir}/bin/check_std_symbols" + fi +fi + +for version in "${versions[@]}"; do + echo + echo "==== test/ with Go ${version} ====" + std_buildmodes="${LLGO_TEST_STD_BUILDMODES:-}" + if [[ -z "${std_buildmodes}" ]]; then + case "${version}" in + 1.26|1.26.*) std_buildmodes=1 ;; + *) std_buildmodes=0 ;; + esac + fi + LLGO="${llgo_cmd}" \ + CHECK_STD_SYMBOLS="${check_std_symbols}" \ + LLGO_TEST_BENCH_GO126="${LLGO_TEST_BENCH_GO126:-1}" \ + LLGO_TEST_CHECK_SYMBOLS="${check_symbols}" \ + LLGO_TEST_STD_BUILDMODES="${std_buildmodes}" \ + dev/test_go_version.sh "${version}" +done diff --git a/dev/test_goroot.sh b/dev/test_goroot.sh index f611751f86..68da5aa704 100755 --- a/dev/test_goroot.sh +++ b/dev/test_goroot.sh @@ -68,11 +68,24 @@ for goroot in "${goroots[@]}"; do echo "==== $version ($goroot) ====" ( cd "$repo_root" - go_test_args=() - if [[ "${LLGO_GOROOT_VERBOSE:-0}" != "0" ]]; then - go_test_args+=("-v") - fi goroot_gomaxprocs="${LLGO_GOROOT_GOMAXPROCS:-${GOMAXPROCS:-2}}" - run_with_heartbeat env GOMAXPROCS="$goroot_gomaxprocs" go test -p=1 ./test/goroot "${go_test_args[@]}" -count=1 -timeout 180m -args -goroot "$goroot" "${runner_args[@]}" + if [[ -n "${LLGO_GOROOT_RUNNER:-}" ]]; then + cd "$repo_root/test/goroot" + test_args=(-test.count=1 -test.timeout=180m) + if [[ "${LLGO_GOROOT_VERBOSE:-0}" != "0" ]]; then + test_args+=("-test.v") + fi + run_with_heartbeat env GOMAXPROCS="$goroot_gomaxprocs" \ + "${LLGO_GOROOT_RUNNER}" "${test_args[@]}" \ + -goroot "$goroot" "${runner_args[@]}" + else + go_test_args=() + if [[ "${LLGO_GOROOT_VERBOSE:-0}" != "0" ]]; then + go_test_args+=("-v") + fi + run_with_heartbeat env GOMAXPROCS="$goroot_gomaxprocs" \ + go test -p=1 ./test/goroot "${go_test_args[@]}" \ + -count=1 -timeout 180m -args -goroot "$goroot" "${runner_args[@]}" + fi ) done diff --git a/dev/test_helloworld.sh b/dev/test_helloworld.sh new file mode 100755 index 0000000000..6c38e6c726 --- /dev/null +++ b/dev/test_helloworld.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 1 || ! "$1" =~ ^1\.[0-9]+$ ]]; then + echo "usage: $0 " >&2 + exit 2 +fi +mod_version=$1 + +llgo_cmd="${LLGO:-llgo}" +if [[ "${llgo_cmd}" != */* ]]; then + llgo_cmd="$(command -v "${llgo_cmd}")" +elif [[ "${llgo_cmd}" != /* ]]; then + llgo_cmd="$(cd "$(dirname "${llgo_cmd}")" && pwd)/$(basename "${llgo_cmd}")" +fi + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/llgo-hello.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT +export GOTOOLCHAIN=local + +hello_dir="${work_dir}/helloworld" +mkdir -p "${hello_dir}" +cat >"${hello_dir}/go.mod" <"${hello_dir}/main.go" <<'EOF' +package main + +import ( + "fmt" + + "github.com/goplus/lib/c" + "github.com/goplus/lib/cpp/std" +) + +func main() { + fmt.Println("Hello, LLGo!") + println("Hello, LLGo!") + c.Printf(c.Str("Hello, LLGo!\n")) + c.Printf(std.Str("Hello LLGo by cpp/std.Str\n").CStr()) +} +EOF +( + cd "${hello_dir}" + go mod tidy + if output="$("${llgo_cmd}" run . 2>&1)"; then + : + else + status=$? + printf '%s\n' "${output}" >&2 + exit "${status}" + fi + printf '%s\n' "${output}" + hello_count="$(grep -Fxc "Hello, LLGo!" <<<"${output}" || true)" + if [[ "${hello_count}" -ne 3 ]]; then + echo "expected three Hello, LLGo! lines, got ${hello_count}" >&2 + exit 1 + fi + if ! grep -Fqx "Hello LLGo by cpp/std.Str" <<<"${output}"; then + echo "missing cpp/std.Str output" >&2 + exit 1 + fi +) + +if [[ "${LLGO_HELLO_EMBED:-1}" != 0 && "${LLGO_HELLO_EMBED:-1}" != false ]]; then + embed_dir="${work_dir}/embed" + mkdir -p "${embed_dir}" + cat >"${embed_dir}/go.mod" <"${embed_dir}/main.go" <<'EOF' +package main + +func main() {} +EOF + ( + cd "${embed_dir}" + "${llgo_cmd}" build -v -target esp32-coreboard-v2 -o demo.out . + test -f demo.out.elf + ) +fi + +echo "Hello World smoke test passed with $(go env GOVERSION), go.mod ${mod_version}" diff --git a/dev/test_runtime_go_version.sh b/dev/test_runtime_go_version.sh new file mode 100755 index 0000000000..e0a40c4ee5 --- /dev/null +++ b/dev/test_runtime_go_version.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +if [[ $# -ne 1 ]]; then + echo "usage: $0 <1.20|...|1.26|exact-version>" >&2 + exit 2 +fi + +( + cd "${repo_root}/runtime" + "${repo_root}/dev/with_go_version.sh" "$1" go test ./... +) diff --git a/dev/test_runtime_go_versions.sh b/dev/test_runtime_go_versions.sh new file mode 100755 index 0000000000..e3b3a9eb1e --- /dev/null +++ b/dev/test_runtime_go_versions.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +versions=("$@") +if [[ ${#versions[@]} -eq 0 ]]; then + versions=(1.20 1.26) +fi + +for version in "${versions[@]}"; do + echo + echo "==== runtime module with Go ${version} ====" + dev/test_runtime_go_version.sh "${version}" +done diff --git a/dev/test_std_buildmodes.sh b/dev/test_std_buildmodes.sh index b3efb2a8c3..407ff72f37 100755 --- a/dev/test_std_buildmodes.sh +++ b/dev/test_std_buildmodes.sh @@ -9,12 +9,19 @@ fi root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" test_pkgs=("$@") +go_list_flags=() +llgo_modfile_flags=() +if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + go_list_flags+=("-modfile=${LLGO_TEST_MODFILE}") + llgo_modfile_flags+=("-modfile=${LLGO_TEST_MODFILE}") +fi import_paths=() stems=() groups=() max_group=0 for test_pkg in "${test_pkgs[@]}"; do - read -r import_path package_dir < <(go list -tags=llgo -f '{{.ImportPath}} {{.Dir}}' "${test_pkg}") + package_info="$(go list "${go_list_flags[@]}" -tags=llgo -f '{{.ImportPath}} {{.Dir}}' "${test_pkg}")" + read -r import_path package_dir <<<"${package_info}" case "${import_path}" in github.com/xgo-dev/llgo/test/std/*) ;; *) @@ -43,7 +50,7 @@ done for i in "${!import_paths[@]}"; do if [[ "${import_paths[$i]}" == "github.com/xgo-dev/llgo/test/std/unique" ]]; then max_group=$((max_group + 1)) - groups[$i]="${max_group}" + groups[i]="${max_group}" fi done @@ -87,7 +94,7 @@ for mode in c-shared c-archive; do echo "==> ${mode}: compile ${#group_imports[@]} test package(s)" ( cd "${work_dir}" - "${llgo_cmd}" test -c -buildmode="${mode}" "${group_imports[@]}" + "${llgo_cmd}" test -c -buildmode="${mode}" "${llgo_modfile_flags[@]}" "${group_imports[@]}" ) for i in "${!import_paths[@]}"; do diff --git a/dev/test_wasm_runtime_go_version.sh b/dev/test_wasm_runtime_go_version.sh new file mode 100755 index 0000000000..e36d68b299 --- /dev/null +++ b/dev/test_wasm_runtime_go_version.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${repo_root}/dev/go_toolchain.sh" +cd "${repo_root}" + +if [[ $# -ne 1 ]]; then + echo "usage: $0 <1.20|...|1.26|exact-version>" >&2 + exit 2 +fi +if ! target_version="$(llgo_resolve_go_version "${repo_root}" "$1")"; then + echo "unsupported Go version: $1" >&2 + exit 2 +fi +target_minor="${target_version%.*}" +case "${target_minor}" in + 1.20|1.21|1.22|1.23) + echo "wasm runtime compatibility starts at Go 1.24 (requires structs.HostLayout)" >&2 + exit 2 + ;; +esac +target_root="$(llgo_go_root "${target_version}")" +target_go="${target_root}/bin/go" + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/llgo-wasm-runtime.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT + +llgo_cmd="${LLGO:-}" +if [[ -z "${llgo_cmd}" ]]; then + dev/build_ci_tools.sh "${work_dir}/bin" + llgo_cmd="${work_dir}/bin/llgo" +elif [[ "${llgo_cmd}" != */* ]]; then + llgo_cmd="$(command -v "${llgo_cmd}")" +elif [[ "${llgo_cmd}" != /* ]]; then + llgo_cmd="$(cd "$(dirname "${llgo_cmd}")" && pwd)/$(basename "${llgo_cmd}")" +fi + +modfile="${work_dir}/wasm-runtime.mod" +cp .github/test-go.mod "${modfile}" +cp .github/test-go.sum "${work_dir}/wasm-runtime.sum" +GOTOOLCHAIN=local "${target_go}" mod edit -modfile="${modfile}" \ + -go="${target_minor}" \ + -replace="github.com/xgo-dev/llgo/runtime=${repo_root}/runtime" + +export PATH="${target_root}/bin:${PATH}" +export GOTOOLCHAIN=local +export GOENV=off +export GOFLAGS= +export LLGO_ROOT="${repo_root}" +export LLGO_BUILD_CACHE="${LLGO_BUILD_CACHE:-off}" + +echo "Building wasm runtime with go${target_version}" +GOOS=js GOARCH=wasm "${llgo_cmd}" build -modfile="${modfile}" \ + -o "${work_dir}/runtime-js.wasm" ./internal/build/testdata/wasm-runtime +GOOS=wasip1 GOARCH=wasm "${llgo_cmd}" build -modfile="${modfile}" \ + -o "${work_dir}/runtime-wasip1.wasm" ./internal/build/testdata/wasm-runtime +file "${work_dir}/runtime-js.wasm" "${work_dir}/runtime-wasip1.wasm" diff --git a/dev/test_wasm_runtime_go_versions.sh b/dev/test_wasm_runtime_go_versions.sh new file mode 100755 index 0000000000..9daf39736f --- /dev/null +++ b/dev/test_wasm_runtime_go_versions.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${repo_root}" + +versions=("$@") +if [[ ${#versions[@]} -eq 0 ]]; then + versions=(1.24 1.26) +fi + +work_dir="$(mktemp -d "${TMPDIR:-/tmp}/llgo-wasm-runtime-versions.XXXXXX")" +trap 'rm -rf "${work_dir}"' EXIT + +llgo_cmd="${LLGO:-}" +if [[ -z "${llgo_cmd}" ]]; then + dev/build_ci_tools.sh "${work_dir}/bin" + llgo_cmd="${work_dir}/bin/llgo" +fi + +for version in "${versions[@]}"; do + echo + echo "==== wasm runtime with Go ${version} ====" + LLGO="${llgo_cmd}" dev/test_wasm_runtime_go_version.sh "${version}" +done diff --git a/dev/with_go_version.sh b/dev/with_go_version.sh new file mode 100755 index 0000000000..3ce9c0e4e7 --- /dev/null +++ b/dev/with_go_version.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +source "${repo_root}/dev/go_toolchain.sh" + +if [[ $# -lt 2 ]]; then + echo "usage: $0 <1.20|...|1.26|exact-version> [argument ...]" >&2 + exit 2 +fi +requested=$1 +shift +if ! target_version="$(llgo_resolve_go_version "${repo_root}" "${requested}")"; then + echo "unsupported Go version: ${requested}" >&2 + exit 2 +fi +target_root="$(llgo_go_root "${target_version}")" + +export PATH="${target_root}/bin:${PATH}" +export GOTOOLCHAIN=local +export GOENV=off +export GOFLAGS= + +echo "Running with go${target_version}: $*" +"$@" diff --git a/doc/_readme/scripts/check_std_cover.sh b/doc/_readme/scripts/check_std_cover.sh index 15448b9712..f4e3b14363 100755 --- a/doc/_readme/scripts/check_std_cover.sh +++ b/doc/_readme/scripts/check_std_cover.sh @@ -5,12 +5,25 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" cd "${ROOT_DIR}" -module_path="$(go list -m)" +go_list_flags=() +if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + go_list_flags+=("-modfile=${LLGO_TEST_MODFILE}") +fi + +module_path="$(go list "${go_list_flags[@]}" -m)" packages=() -while IFS= read -r pkg; do - packages+=("${pkg}") -done < <(go list ./test/std/... | sort) +if [[ $# -eq 0 ]]; then + package_output="$(go list "${go_list_flags[@]}" ./test/std/... | sort)" + while IFS= read -r pkg; do + [[ -n "${pkg}" ]] && packages+=("${pkg}") + done <<< "${package_output}" +else + package_output="$(go list "${go_list_flags[@]}" "$@" | sort -u)" + while IFS= read -r pkg; do + [[ -n "${pkg}" ]] && packages+=("${pkg}") + done <<< "${package_output}" +fi if [ "${#packages[@]}" -eq 0 ]; then echo "No stdlib test packages discovered under test/std" >&2 @@ -20,7 +33,7 @@ fi args=() covered_packages=() for pkg in "${packages[@]}"; do - rel_path="${pkg#${module_path}/}" + rel_path="${pkg#"${module_path}"/}" if [[ "${rel_path}" != test/std/* ]]; then continue fi @@ -32,32 +45,50 @@ for pkg in "${packages[@]}"; do args+=("-pkg" "${stdlib_pkg}") done -expected_file="$(mktemp)" -covered_file="$(mktemp)" -trap 'rm -f "${expected_file}" "${covered_file}"' EXIT +if [[ $# -eq 0 ]]; then + expected_file="$(mktemp)" + covered_file="$(mktemp)" + trap 'rm -f "${expected_file}" "${covered_file}"' EXIT -go list std \ - | awk '!/(^|\/)internal(\/|$)/ && !/(^|\/)vendor(\/|$)/' \ - | sort -u > "${expected_file}" -printf '%s\n' "${covered_packages[@]}" | sort -u > "${covered_file}" + go list "${go_list_flags[@]}" std \ + | awk '!/(^|\/)internal(\/|$)/ && !/(^|\/)vendor(\/|$)/' \ + | sort -u > "${expected_file}" + printf '%s\n' "${covered_packages[@]}" | sort -u > "${covered_file}" -missing_packages="$(comm -23 "${expected_file}" "${covered_file}")" -if [[ -n "${missing_packages}" ]]; then - echo "Public standard-library packages missing test/std coverage:" >&2 - while IFS= read -r pkg; do - echo " - ${pkg}" >&2 - done <<< "${missing_packages}" - exit 1 + missing_packages="$(comm -23 "${expected_file}" "${covered_file}")" + if [[ -n "${missing_packages}" ]]; then + echo "Public standard-library packages missing test/std coverage:" >&2 + while IFS= read -r pkg; do + echo " - ${pkg}" >&2 + done <<< "${missing_packages}" + exit 1 + fi + + expected_count="$(wc -l < "${expected_file}" | tr -d ' ')" + covered_count="$(wc -l < "${covered_file}" | tr -d ' ')" + echo "Public standard-library package coverage: ${covered_count}/${expected_count}" +fi + +if [[ "${#args[@]}" -eq 0 ]]; then + echo "No standard-library symbols selected for coverage checking" + exit 0 fi -expected_count="$(wc -l < "${expected_file}" | tr -d ' ')" -covered_count="$(wc -l < "${covered_file}" | tr -d ' ')" -echo "Public standard-library package coverage: ${covered_count}/${expected_count}" +check_command=() +if [[ -n "${CHECK_STD_SYMBOLS:-}" ]]; then + check_command+=("${CHECK_STD_SYMBOLS}") +else + check_command+=(go run ./chore/check_std_symbols) +fi +if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + args+=("-modfile" "${LLGO_TEST_MODFILE}") +fi -printf '+ go run ./chore/check_std_symbols' +printf '+' +printf ' %q' "${check_command[@]}" for arg in "${args[@]}"; do printf ' %q' "${arg}" done printf '\n' -go run ./chore/check_std_symbols "${args[@]}" +"${check_command[@]}" "${args[@]}" diff --git a/go.mod b/go.mod index 375979d3d3..e4cace1660 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/xgo-dev/llgo -go 1.25.0 +go 1.26.0 require ( github.com/goplus/cobra v1.9.12 //xgo:class diff --git a/runtime/internal/lib/runtime/pprof_goroutine_go123_llgo.go b/runtime/internal/lib/runtime/pprof_goroutine_go123_llgo.go new file mode 100644 index 0000000000..cc5a691ae7 --- /dev/null +++ b/runtime/internal/lib/runtime/pprof_goroutine_go123_llgo.go @@ -0,0 +1,10 @@ +//go:build (darwin || linux) && go1.23 + +package runtime + +import "unsafe" + +//go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels +func pprof_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + return 0, true +} diff --git a/runtime/internal/lib/runtime/pprof_goroutine_pre_go123_llgo.go b/runtime/internal/lib/runtime/pprof_goroutine_pre_go123_llgo.go new file mode 100644 index 0000000000..d7616c51a9 --- /dev/null +++ b/runtime/internal/lib/runtime/pprof_goroutine_pre_go123_llgo.go @@ -0,0 +1,10 @@ +//go:build (darwin || linux) && !go1.23 + +package runtime + +import "unsafe" + +//go:linkname runtime_goroutineProfileWithLabels runtime/pprof.runtime_goroutineProfileWithLabels +func runtime_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) { + return 0, true +} diff --git a/runtime/internal/lib/runtime/pprof_linkname_llgo.go b/runtime/internal/lib/runtime/pprof_linkname_llgo.go index 3dff79d9a4..a1ff95f8a2 100644 --- a/runtime/internal/lib/runtime/pprof_linkname_llgo.go +++ b/runtime/internal/lib/runtime/pprof_linkname_llgo.go @@ -56,11 +56,6 @@ func runtime_pprof_readProfile() (data []uint64, tags []unsafe.Pointer, eof bool return cpuProfilePeriodRecord[:], cpuProfilePeriodTags[:], true } -//go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels -func pprof_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) { - return 0, true -} - //go:linkname runtime_goroutineLeakGC runtime/pprof.runtime_goroutineLeakGC func runtime_goroutineLeakGC() {} diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000000..1618b8df1f --- /dev/null +++ b/test/README.md @@ -0,0 +1,48 @@ +# LLGo test version coverage + +CI builds the llgo compiler and repository tooling only with the exact Go 1.26 +release pinned in `.go-version`. Packages below `test/` are then loaded and +tested with real Go toolchains from Go 1.20 through Go 1.26. The version runner +uses a temporary alternate module file whose `go` directive matches the target +release and sets `GOTOOLCHAIN=local`, so a test cannot silently upgrade or +downgrade to another toolchain. + +Go 1.25 and Go 1.26 run all packages on both Linux and macOS. To limit runner +usage, Go 1.20 through Go 1.24 each run a representative package set, alternating +between Linux and macOS. Tests for APIs introduced by a newer Go release belong +in files with standard release tags such as `//go:build go1.24`; the selected Go +toolchain then includes those files automatically. Symbol-coverage checks use +the same toolchain and tags. + +The ordinary current-version command remains: + +```sh +llgo test ./test/... +``` + +Use the version runner for an older release or a smaller local package set: + +```sh +dev/test_go_version.sh 1.20 +dev/test_go_version.sh 1.24 ./test/std/bytes ./test/goroot + +# Run the complete local Go 1.20 through Go 1.26 matrix +dev/test_go_versions.sh +``` + +The complete local matrix is sequential and may take tens of minutes. CI runs +the versions in separate jobs, with the full Go 1.25 and Go 1.26 package sets +sharded on Linux. + +The runner downloads an exact toolchain when needed, builds llgo itself with +the `.go-version` toolchain, and leaves the working tree unchanged. Set `LLGO` +to reuse an existing compiler binary. + +The wasm runtime lanes use the same model and are locally reproducible with +`dev/test_wasm_runtime_go_version.sh 1.24`; run both CI endpoints with +`dev/test_wasm_runtime_go_versions.sh`. The wasm clite syscall implementation +uses `structs.HostLayout`, so this subrange explicitly requires Go 1.24. + +The `runtime` and `_demo` modules retain a Go 1.20 compatibility floor unless a +submodule explicitly needs a newer language or standard-library feature. The +native runtime floor is reproducible with `dev/test_runtime_go_version.sh 1.20`. diff --git a/test/go/caller_acceptance_test.go b/test/go/caller_acceptance_test.go index 3e5112d716..67dee87921 100644 --- a/test/go/caller_acceptance_test.go +++ b/test/go/caller_acceptance_test.go @@ -387,8 +387,7 @@ func runLLGoProbeWithFlags(t *testing.T, dir string, flags ...string) (string, e repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) args := append([]string{"run", "./cmd/llgo", "run", "-a"}, flags...) - cmd := exec.Command("go", append(args, filepath.Join(dir, "main.go"))...) - cmd.Dir = repoRoot + cmd := commandForTest(t, repoRoot, "go", append(args, filepath.Join(dir, "main.go"))...) out, err := cmd.CombinedOutput() return string(out), err } @@ -667,6 +666,9 @@ func acceptanceLLGoBinary(t *testing.T) string { t.Helper() repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) + if llgo := configuredLLGo(t); llgo != "" { + return llgo + } acceptanceLLGoOnce.Do(func() { tmp, err := os.MkdirTemp("", "llgo-acceptance-bin") if err != nil { diff --git a/test/go/cgo_malloc_test.go b/test/go/cgo_malloc_test.go index 41c01caa8c..fc3cbadce0 100644 --- a/test/go/cgo_malloc_test.go +++ b/test/go/cgo_malloc_test.go @@ -63,14 +63,13 @@ func main() { func runGoCmd(t *testing.T, dir string, args ...string) string { t.Helper() - cmd := exec.Command("go", args...) - if dir != "" { - cmd.Dir = dir - } + cmd := commandForTest(t, dir, "go", args...) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr - cmd.Env = os.Environ() + if cmd.Env == nil { + cmd.Env = os.Environ() + } if err := cmd.Run(); err != nil { t.Fatalf("go %s failed: %v\nstdout:\n%s\nstderr:\n%s", strings.Join(args, " "), err, stdout.String(), stderr.String()) } diff --git a/test/go/defer_ir_regression_test.go b/test/go/defer_ir_regression_test.go index 46c17e891a..9f8944a302 100644 --- a/test/go/defer_ir_regression_test.go +++ b/test/go/defer_ir_regression_test.go @@ -101,6 +101,11 @@ func llgoIRFromProbe(t *testing.T, name, src string) string { if err := os.WriteFile(mainFile, []byte(src), 0644); err != nil { t.Fatal(err) } + if os.Getenv("LLGO_TEST_LLGEN") != "" { + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module llgo-ir-probe\n\ngo 1.20\n"), 0644); err != nil { + t.Fatal(err) + } + } runGoCmd(t, root, "run", "./chore/llgen", filepath.ToSlash(dir)) data, err := os.ReadFile(filepath.Join(dir, "llgo_autogen.ll")) diff --git a/test/go/fault_unwind_test.go b/test/go/fault_unwind_test.go index f6dae85141..5f9797d6a9 100644 --- a/test/go/fault_unwind_test.go +++ b/test/go/fault_unwind_test.go @@ -110,6 +110,9 @@ func faultLLGo(t *testing.T) string { t.Helper() repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) + if llgo := configuredLLGo(t); llgo != "" { + return llgo + } faultLLGoOnce.Do(func() { tmp, err := os.MkdirTemp("", "llgo-fault-bin") if err != nil { diff --git a/test/go/generic_local_types_test.go b/test/go/generic_local_types_test.go index 61cf24228c..867164355e 100644 --- a/test/go/generic_local_types_test.go +++ b/test/go/generic_local_types_test.go @@ -3,7 +3,6 @@ package gotest import ( "bytes" "os" - "os/exec" "path/filepath" "reflect" "strings" @@ -101,7 +100,7 @@ func TestGenericNestedLocalRuntimeTypeNamesForCommandLineMain(t *testing.T) { t.Fatal(err) } repoRoot := genericLocalRepoRoot(t) - goOut := runGenericLocalProbe(t, repoRoot, "go", "run", file) + goOut := runGenericLocalProbe(t, dir, "go", "run", file) const want = "main.T[int;int]\nmain.T[int;main.U[int;int]·3]\n" if goOut != want { t.Fatalf("go probe output = %q, want %q", goOut, want) @@ -115,8 +114,7 @@ func TestGenericNestedLocalRuntimeTypeNamesForCommandLineMain(t *testing.T) { func runGenericLocalProbe(t *testing.T, dir, name string, args ...string) string { t.Helper() - cmd := exec.Command(name, args...) - cmd.Dir = dir + cmd := commandForTest(t, dir, name, args...) cmd.Env = os.Environ() var stderr bytes.Buffer cmd.Stderr = &stderr diff --git a/test/go/newexpr_go126_test.go b/test/go/newexpr_go126_test.go index b03f1873cc..313b72cfdd 100644 --- a/test/go/newexpr_go126_test.go +++ b/test/go/newexpr_go126_test.go @@ -70,7 +70,7 @@ func TestNewExpressionInitializesAllocatedValue(t *testing.T) { } repoRoot := findRepoRoot(t) - runStringConversionProbe(t, repoRoot, "go", "run", file) + runStringConversionProbe(t, dir, "go", "run", file) t.Setenv("LLGO_ROOT", repoRoot) runStringConversionProbe(t, repoRoot, "go", "run", "./cmd/llgo", "run", file) } diff --git a/test/go/package_init_order_test.go b/test/go/package_init_order_test.go index 9e3113327e..a0bffb0b87 100644 --- a/test/go/package_init_order_test.go +++ b/test/go/package_init_order_test.go @@ -34,6 +34,9 @@ func packageInitLLGo(t *testing.T) string { t.Helper() repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) + if llgo := configuredLLGo(t); llgo != "" { + return llgo + } packageInitLLGoOnce.Do(func() { dir, err := os.MkdirTemp("", "llgo-package-init-bin") if err != nil { diff --git a/test/go/print_builtin_legacy_test.go b/test/go/print_builtin_legacy_test.go index dddd212cf9..78a673fc91 100644 --- a/test/go/print_builtin_legacy_test.go +++ b/test/go/print_builtin_legacy_test.go @@ -22,7 +22,6 @@ package gotest import ( "bytes" "os" - "os/exec" "path/filepath" "strings" "testing" @@ -56,8 +55,7 @@ func TestBuiltinPrintLegacyExponentWidth(t *testing.T) { } root := findLLGoRoot(t) - cmd := exec.Command("go", "run", "./cmd/llgo", "run", mainFile) - cmd.Dir = root + cmd := commandForTest(t, root, "go", "run", "./cmd/llgo", "run", mainFile) cmd.Env = append(os.Environ(), "LLGO_ROOT="+root) var stdout, stderr bytes.Buffer diff --git a/test/go/print_builtin_test.go b/test/go/print_builtin_test.go index 5dadb3a329..0fa9b42bbe 100644 --- a/test/go/print_builtin_test.go +++ b/test/go/print_builtin_test.go @@ -18,7 +18,6 @@ package gotest import ( "os" - "os/exec" "path/filepath" "testing" ) @@ -100,7 +99,7 @@ func TestBuiltinPrintOutputMatchesGo(t *testing.T) { repoRoot := findBuiltinPrintRepoRoot(t) goBin := filepath.Join(dir, "go-probe") llgoBin := filepath.Join(dir, "llgo-probe") - runBuiltinPrintCommand(t, repoRoot, "go", "build", "-o", goBin, file) + runBuiltinPrintCommand(t, dir, "go", "build", "-o", goBin, file) t.Setenv("LLGO_ROOT", repoRoot) runBuiltinPrintCommand(t, repoRoot, "go", "run", "./cmd/llgo", "build", "-o", llgoBin, file) @@ -113,8 +112,7 @@ func TestBuiltinPrintOutputMatchesGo(t *testing.T) { func runBuiltinPrintCommand(t *testing.T, dir, name string, args ...string) []byte { t.Helper() - cmd := exec.Command(name, args...) - cmd.Dir = dir + cmd := commandForTest(t, dir, name, args...) cmd.Env = os.Environ() out, err := cmd.CombinedOutput() if err != nil { diff --git a/test/go/runtime_lineinfo_stack_test.go b/test/go/runtime_lineinfo_stack_test.go index 46beea2360..aa333aa40f 100644 --- a/test/go/runtime_lineinfo_stack_test.go +++ b/test/go/runtime_lineinfo_stack_test.go @@ -18,7 +18,6 @@ package gotest import ( "os" - "os/exec" "path/filepath" "strconv" "strings" @@ -214,8 +213,7 @@ func TestRuntimeLineInfoAndStack(t *testing.T) { repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) - cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) - cmd.Dir = repoRoot + cmd := commandForTest(t, repoRoot, "go", "run", "./cmd/llgo", "run", "-a", file) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("llgo lineinfo probe failed: %v\n%s", err, out) } @@ -343,8 +341,7 @@ func TestRuntimeFuncInfoConcurrentFirstUse(t *testing.T) { repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) - cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) - cmd.Dir = repoRoot + cmd := commandForTest(t, repoRoot, "go", "run", "./cmd/llgo", "run", "-a", file) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("llgo concurrent funcinfo probe failed: %v\n%s", err, out) } diff --git a/test/go/runtime_statement_line_test.go b/test/go/runtime_statement_line_test.go index 63c8b3cc9a..a371129983 100644 --- a/test/go/runtime_statement_line_test.go +++ b/test/go/runtime_statement_line_test.go @@ -18,7 +18,6 @@ package gotest import ( "os" - "os/exec" "path/filepath" "strconv" "strings" @@ -245,8 +244,7 @@ func TestRuntimeStatementLineInfo(t *testing.T) { repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) - cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) - cmd.Dir = repoRoot + cmd := commandForTest(t, repoRoot, "go", "run", "./cmd/llgo", "run", "-a", file) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("llgo statement line probe failed: %v\n%s", err, out) } @@ -280,8 +278,7 @@ func TestRuntimeDeferredPanicLine(t *testing.T) { repoRoot := findRepoRoot(t) t.Setenv("LLGO_ROOT", repoRoot) - cmd := exec.Command("go", "run", "./cmd/llgo", "run", "-a", file) - cmd.Dir = repoRoot + cmd := commandForTest(t, repoRoot, "go", "run", "./cmd/llgo", "run", "-a", file) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("llgo deferred panic line probe failed: %v\n%s", err, out) diff --git a/test/go/string_conversion_test.go b/test/go/string_conversion_test.go index 0bbd0fb638..8661c5d0b2 100644 --- a/test/go/string_conversion_test.go +++ b/test/go/string_conversion_test.go @@ -18,7 +18,6 @@ package gotest import ( "os" - "os/exec" "path/filepath" "testing" ) @@ -99,7 +98,7 @@ func TestStringConversionFromWideIntegers(t *testing.T) { t.Fatal(err) } repoRoot := findRepoRoot(t) - runStringConversionProbe(t, repoRoot, "go", "run", file) + runStringConversionProbe(t, dir, "go", "run", file) t.Setenv("LLGO_ROOT", repoRoot) runStringConversionProbe(t, repoRoot, "go", "run", "./cmd/llgo", "run", file) } @@ -111,15 +110,14 @@ func TestEmptyStringToByteRuneSlicesNonNil(t *testing.T) { t.Fatal(err) } repoRoot := findRepoRoot(t) - runStringConversionProbe(t, repoRoot, "go", "run", file) + runStringConversionProbe(t, dir, "go", "run", file) t.Setenv("LLGO_ROOT", repoRoot) runStringConversionProbe(t, repoRoot, "go", "run", "./cmd/llgo", "run", file) } func runStringConversionProbe(t *testing.T, dir, name string, args ...string) { t.Helper() - cmd := exec.Command(name, args...) - cmd.Dir = dir + cmd := commandForTest(t, dir, name, args...) cmd.Env = os.Environ() out, err := cmd.CombinedOutput() if err != nil { diff --git a/test/go/test_command_test.go b/test/go/test_command_test.go new file mode 100644 index 0000000000..de3c60c923 --- /dev/null +++ b/test/go/test_command_test.go @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package gotest + +import ( + "os" + "os/exec" + "testing" +) + +func configuredLLGo(t *testing.T) string { + t.Helper() + name := os.Getenv("LLGO_TEST_LLGO") + if name == "" { + name = os.Getenv("LLGO") + } + return configuredTestTool(t, name) +} + +func configuredTestTool(t *testing.T, name string) string { + t.Helper() + if name == "" { + return "" + } + path, err := exec.LookPath(name) + if err != nil { + t.Fatalf("resolve configured test tool %q: %v", name, err) + } + return path +} + +// commandForTest reuses repository tools built by the build toolchain when a +// versioned test would otherwise rebuild them with its target Go toolchain. +// Ordinary non-versioned tests retain the original go-run path. +func commandForTest(t *testing.T, dir, name string, args ...string) *exec.Cmd { + t.Helper() + modfile := os.Getenv("LLGO_TEST_MODFILE") + if name == "go" && len(args) >= 2 && args[0] == "run" && args[1] == "./cmd/llgo" { + if llgo := configuredLLGo(t); llgo != "" { + name = llgo + args = args[2:] + if modfile != "" && len(args) != 0 { + args = append([]string{args[0], "-modfile=" + modfile}, args[1:]...) + } + } + } else if name == "go" && len(args) >= 2 && args[0] == "run" && args[1] == "./chore/llgen" { + if llgen := configuredTestTool(t, os.Getenv("LLGO_TEST_LLGEN")); llgen != "" { + name = llgen + args = args[2:] + if len(args) != 0 { + dir = args[len(args)-1] + args[len(args)-1] = "." + } + } + } + cmd := exec.Command(name, args...) + if dir != "" { + cmd.Dir = dir + } + return cmd +} diff --git a/test/go/tool_compile_compat_test.go b/test/go/tool_compile_compat_test.go index c18697ac52..42e4577f98 100644 --- a/test/go/tool_compile_compat_test.go +++ b/test/go/tool_compile_compat_test.go @@ -185,6 +185,7 @@ func writeToolCompileSource(t *testing.T, dir, name, content string) { func writeToolCompileStdlibImportCfg(t *testing.T, dir string) { t.Helper() cmd := exec.Command("go", "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}", "std") + cmd.Dir = dir cmd.Env = append(os.Environ(), "GOENV=off", "GOFLAGS=") output, err := cmd.CombinedOutput() if err != nil { diff --git a/test/std/README.md b/test/std/README.md index 9c59917373..3bea39ce0d 100644 --- a/test/std/README.md +++ b/test/std/README.md @@ -1,6 +1,6 @@ # Standard Library Compatibility Tests -This directory contains compatibility tests for the Go standard library on llgo. These tests ensure that llgo's libc-backed implementations conform to the Go standard library specification across the pinned supported CI toolchains (Go 1.25 and Go 1.26). +This directory contains compatibility tests for the Go standard library on llgo. The tests run with real Go 1.20 through Go 1.26 toolchains and matching module versions, so release tags and available standard-library APIs reflect the version being checked. Go 1.25 and Go 1.26 receive full coverage on Linux and macOS; each earlier version runs a representative, resource-bounded package set on one platform. ## Directory Structure @@ -144,6 +144,9 @@ llgo test ./test/... # Run specific package llgo test ./test/std/math/ + +# Run with an exact older Go toolchain and matching release tags +dev/test_go_version.sh 1.20 ./test/std/math ``` ## Contributing New Package Tests diff --git a/test/std/bytes/bytes_go121_test.go b/test/std/bytes/bytes_go121_test.go new file mode 100644 index 0000000000..64ddb44c8f --- /dev/null +++ b/test/std/bytes/bytes_go121_test.go @@ -0,0 +1,34 @@ +//go:build go1.21 + +package bytes_test + +import ( + "bytes" + "testing" + "unicode" +) + +func TestBytesGo121Functions(t *testing.T) { + if !bytes.ContainsFunc([]byte("abc123"), unicode.IsDigit) { + t.Fatal("ContainsFunc should locate digit rune") + } + + var buf bytes.Buffer + buf.Grow(16) + initialAvail := buf.Available() + if initialAvail <= 0 { + t.Fatalf("Available should report spare capacity, got %d", initialAvail) + } + + space := buf.AvailableBuffer() + if len(space) != 0 || cap(space) != initialAvail { + t.Fatalf("AvailableBuffer mismatch: len=%d cap=%d want cap=%d", len(space), cap(space), initialAvail) + } + space = append(space, 'G', 'o') + if n, err := buf.Write(space); err != nil || n != len(space) { + t.Fatalf("Write via AvailableBuffer mismatch: n=%d err=%v", n, err) + } + if buf.Available() != initialAvail-len(space) { + t.Fatalf("Available after write mismatch: %d", buf.Available()) + } +} diff --git a/test/std/bytes/bytes_go124_test.go b/test/std/bytes/bytes_go124_test.go new file mode 100644 index 0000000000..4b213afa34 --- /dev/null +++ b/test/std/bytes/bytes_go124_test.go @@ -0,0 +1,49 @@ +//go:build go1.24 + +package bytes_test + +import ( + "bytes" + "iter" + "testing" +) + +func collectByteSeq(seq iter.Seq[[]byte]) [][]byte { + var out [][]byte + for chunk := range seq { + out = append(out, append([]byte(nil), chunk...)) + } + return out +} + +func TestBytesSequenceIterators(t *testing.T) { + fields := collectByteSeq(bytes.FieldsSeq([]byte(" a b c\t"))) + if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(fields, want) { + t.Fatalf("FieldsSeq mismatch: %v", fields) + } + + fieldsFunc := collectByteSeq(bytes.FieldsFuncSeq([]byte("a|b||c"), func(r rune) bool { return r == '|' })) + if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(fieldsFunc, want) { + t.Fatalf("FieldsFuncSeq mismatch: %v", fieldsFunc) + } + + lines := collectByteSeq(bytes.Lines([]byte("a\nb\n"))) + if want := [][]byte{[]byte("a\n"), []byte("b\n")}; !equalByteSlices(lines, want) { + t.Fatalf("Lines mismatch: %v", lines) + } + + linesSingle := collectByteSeq(bytes.Lines([]byte("single"))) + if want := [][]byte{[]byte("single")}; !equalByteSlices(linesSingle, want) { + t.Fatalf("Lines single mismatch: %v", linesSingle) + } + + splitSeq := collectByteSeq(bytes.SplitSeq([]byte("a,b,c"), []byte(","))) + if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(splitSeq, want) { + t.Fatalf("SplitSeq mismatch: %v", splitSeq) + } + + splitAfterSeq := collectByteSeq(bytes.SplitAfterSeq([]byte("a,b,c"), []byte(","))) + if want := [][]byte{[]byte("a,"), []byte("b,"), []byte("c")}; !equalByteSlices(splitAfterSeq, want) { + t.Fatalf("SplitAfterSeq mismatch: %v", splitAfterSeq) + } +} diff --git a/test/std/bytes/bytes_test.go b/test/std/bytes/bytes_test.go index dc2930eb0a..35907d88c0 100644 --- a/test/std/bytes/bytes_test.go +++ b/test/std/bytes/bytes_test.go @@ -3,8 +3,6 @@ package bytes_test import ( "bytes" "io" - "iter" - "slices" "strings" "testing" "unicode" @@ -12,17 +10,15 @@ import ( ) func equalByteSlices(a, b [][]byte) bool { - return slices.EqualFunc(a, b, func(x, y []byte) bool { - return bytes.Equal(x, y) - }) -} - -func collectByteSeq(seq iter.Seq[[]byte]) [][]byte { - var out [][]byte - for chunk := range seq { - out = append(out, append([]byte(nil), chunk...)) + if len(a) != len(b) { + return false + } + for i := range a { + if !bytes.Equal(a[i], b[i]) { + return false + } } - return out + return true } func TestBytesBasicFunctions(t *testing.T) { @@ -51,9 +47,6 @@ func TestBytesBasicFunctions(t *testing.T) { if !bytes.ContainsAny([]byte("hello"), "xyzlo") { t.Fatal("ContainsAny should find shared character") } - if !bytes.ContainsFunc([]byte("abc123"), unicode.IsDigit) { - t.Fatal("ContainsFunc should locate digit rune") - } if !bytes.ContainsRune([]byte("caf\u00e9"), 'é') { t.Fatal("ContainsRune should find rune") } @@ -214,7 +207,7 @@ func TestBytesBasicFunctions(t *testing.T) { t.Fatal("TrimSuffix mismatch") } - if runes := bytes.Runes([]byte("πλα")); !slices.Equal(runes, []rune{'π', 'λ', 'α'}) { + if runes := bytes.Runes([]byte("πλα")); string(runes) != "πλα" { t.Fatalf("Runes mismatch: %v", runes) } @@ -254,38 +247,6 @@ func TestBytesSplitFunctions(t *testing.T) { } } -func TestBytesSequenceIterators(t *testing.T) { - fields := collectByteSeq(bytes.FieldsSeq([]byte(" a b c\t"))) - if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(fields, want) { - t.Fatalf("FieldsSeq mismatch: %v", fields) - } - - fieldsFunc := collectByteSeq(bytes.FieldsFuncSeq([]byte("a|b||c"), func(r rune) bool { return r == '|' })) - if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(fieldsFunc, want) { - t.Fatalf("FieldsFuncSeq mismatch: %v", fieldsFunc) - } - - lines := collectByteSeq(bytes.Lines([]byte("a\nb\n"))) - if want := [][]byte{[]byte("a\n"), []byte("b\n")}; !equalByteSlices(lines, want) { - t.Fatalf("Lines mismatch: %v", lines) - } - - linesSingle := collectByteSeq(bytes.Lines([]byte("single"))) - if want := [][]byte{[]byte("single")}; !equalByteSlices(linesSingle, want) { - t.Fatalf("Lines single mismatch: %v", linesSingle) - } - - splitSeq := collectByteSeq(bytes.SplitSeq([]byte("a,b,c"), []byte(","))) - if want := [][]byte{[]byte("a"), []byte("b"), []byte("c")}; !equalByteSlices(splitSeq, want) { - t.Fatalf("SplitSeq mismatch: %v", splitSeq) - } - - splitAfterSeq := collectByteSeq(bytes.SplitAfterSeq([]byte("a,b,c"), []byte(","))) - if want := [][]byte{[]byte("a,"), []byte("b,"), []byte("c")}; !equalByteSlices(splitAfterSeq, want) { - t.Fatalf("SplitAfterSeq mismatch: %v", splitAfterSeq) - } -} - func TestBytesReader(t *testing.T) { data := []byte("hello") r := bytes.NewReader(data) @@ -358,21 +319,8 @@ func TestBytesBuffer(t *testing.T) { t.Fatalf("Grow should ensure capacity, got %d", buf.Cap()) } - initialAvail := buf.Available() - if initialAvail <= 0 { - t.Fatalf("Available should report spare capacity, got %d", initialAvail) - } - - space := buf.AvailableBuffer() - if len(space) != 0 || cap(space) != initialAvail { - t.Fatalf("AvailableBuffer mismatch: len=%d cap=%d want cap=%d", len(space), cap(space), initialAvail) - } - space = append(space, 'G', 'o') - if n, err := buf.Write(space); err != nil || n != len(space) { - t.Fatalf("Write via AvailableBuffer mismatch: n=%d err=%v", n, err) - } - if buf.Available() != initialAvail-len(space) { - t.Fatalf("Available after write mismatch: %d", buf.Available()) + if n, err := buf.Write([]byte("Go")); err != nil || n != 2 { + t.Fatalf("Write mismatch: n=%d err=%v", n, err) } if err := buf.WriteByte('+'); err != nil { t.Fatalf("WriteByte error: %v", err) diff --git a/test/std/crypto/ecdsa/go126_symbols_test.go b/test/std/crypto/ecdsa/go125_symbols_test.go similarity index 98% rename from test/std/crypto/ecdsa/go126_symbols_test.go rename to test/std/crypto/ecdsa/go125_symbols_test.go index 8b3ed39463..46ea941e3c 100644 --- a/test/std/crypto/ecdsa/go126_symbols_test.go +++ b/test/std/crypto/ecdsa/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package ecdsa_test diff --git a/test/std/crypto/go125_symbols_test.go b/test/std/crypto/go125_symbols_test.go new file mode 100644 index 0000000000..ce40c213d0 --- /dev/null +++ b/test/std/crypto/go125_symbols_test.go @@ -0,0 +1,41 @@ +//go:build go1.25 + +package crypto_test + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "io" + "testing" +) + +type messageSigner struct { + key ed25519.PrivateKey +} + +func (s messageSigner) Public() crypto.PublicKey { + return s.key.Public() +} + +func (s messageSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return s.key.Sign(rand, digest, opts) +} + +func (s messageSigner) SignMessage(_ io.Reader, message []byte, _ crypto.SignerOpts) ([]byte, error) { + return ed25519.Sign(s.key, message), nil +} + +func TestMessageSigner(t *testing.T) { + seed := bytes.Repeat([]byte{0x42}, ed25519.SeedSize) + privateKey := ed25519.NewKeyFromSeed(seed) + var signer crypto.MessageSigner = messageSigner{key: privateKey} + message := []byte("message signer") + signature, err := crypto.SignMessage(signer, nil, message, crypto.Hash(0)) + if err != nil { + t.Fatal(err) + } + if !ed25519.Verify(privateKey.Public().(ed25519.PublicKey), message, signature) { + t.Fatal("SignMessage produced an invalid signature") + } +} diff --git a/test/std/crypto/go126_symbols_test.go b/test/std/crypto/go126_symbols_test.go index aca05ee009..bb555df7fd 100644 --- a/test/std/crypto/go126_symbols_test.go +++ b/test/std/crypto/go126_symbols_test.go @@ -5,41 +5,11 @@ package crypto_test import ( "bytes" "crypto" - "crypto/ed25519" "crypto/mlkem" - "io" "testing" ) -type messageSigner struct { - key ed25519.PrivateKey -} - -func (s messageSigner) Public() crypto.PublicKey { - return s.key.Public() -} - -func (s messageSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - return s.key.Sign(rand, digest, opts) -} - -func (s messageSigner) SignMessage(_ io.Reader, message []byte, _ crypto.SignerOpts) ([]byte, error) { - return ed25519.Sign(s.key, message), nil -} - -func TestGo126Interfaces(t *testing.T) { - seed := bytes.Repeat([]byte{0x42}, ed25519.SeedSize) - privateKey := ed25519.NewKeyFromSeed(seed) - var signer crypto.MessageSigner = messageSigner{key: privateKey} - message := []byte("go1.26 message signer") - signature, err := crypto.SignMessage(signer, nil, message, crypto.Hash(0)) - if err != nil { - t.Fatal(err) - } - if !ed25519.Verify(privateKey.Public().(ed25519.PublicKey), message, signature) { - t.Fatal("SignMessage produced an invalid signature") - } - +func TestGo126EncapsulationInterfaces(t *testing.T) { key, err := mlkem.GenerateKey768() if err != nil { t.Fatal(err) diff --git a/test/std/crypto/sha3/go126_symbols_test.go b/test/std/crypto/sha3/go125_symbols_test.go similarity index 96% rename from test/std/crypto/sha3/go126_symbols_test.go rename to test/std/crypto/sha3/go125_symbols_test.go index d286cdb2de..c4e00eebba 100644 --- a/test/std/crypto/sha3/go126_symbols_test.go +++ b/test/std/crypto/sha3/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package sha3_test diff --git a/test/std/debug/dwarf/dwarf_test.go b/test/std/debug/dwarf/dwarf_test.go index 8c510574ea..4321d14a25 100644 --- a/test/std/debug/dwarf/dwarf_test.go +++ b/test/std/debug/dwarf/dwarf_test.go @@ -19,6 +19,7 @@ func buildLinuxFixture(t *testing.T) string { } out := filepath.Join(dir, "fixture") cmd := exec.Command("go", "build", "-o", out, src) + cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0") if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build linux fixture: %v\n%s", err, b) diff --git a/test/std/debug/elf/elf_test.go b/test/std/debug/elf/elf_test.go index bc8fa77f4e..d885e38a7f 100644 --- a/test/std/debug/elf/elf_test.go +++ b/test/std/debug/elf/elf_test.go @@ -20,6 +20,7 @@ func buildLinuxFixture(t *testing.T) string { } out := filepath.Join(dir, "fixture") cmd := exec.Command("go", "build", "-o", out, src) + cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0") if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build linux fixture: %v\n%s", err, b) diff --git a/test/std/debug/macho/macho_test.go b/test/std/debug/macho/macho_test.go index 9a046069a6..8340fea5a8 100644 --- a/test/std/debug/macho/macho_test.go +++ b/test/std/debug/macho/macho_test.go @@ -18,6 +18,7 @@ func buildDarwinFixture(t *testing.T) string { } out := filepath.Join(dir, "fixture") cmd := exec.Command("go", "build", "-o", out, src) + cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=darwin", "GOARCH=amd64", "CGO_ENABLED=0") if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build darwin fixture: %v\n%s", err, b) diff --git a/test/std/debug/pe/pe_test.go b/test/std/debug/pe/pe_test.go index 1df52743de..af20e8b708 100644 --- a/test/std/debug/pe/pe_test.go +++ b/test/std/debug/pe/pe_test.go @@ -20,6 +20,7 @@ func buildWindowsFixture(t *testing.T) string { } out := filepath.Join(dir, "fixture.exe") cmd := exec.Command("go", "build", "-o", out, src) + cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=windows", "GOARCH=amd64", "CGO_ENABLED=0") if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("go build windows fixture: %v\n%s", err, b) diff --git a/test/std/go/ast/go125_symbols_test.go b/test/std/go/ast/go125_symbols_test.go new file mode 100644 index 0000000000..22d7195d4a --- /dev/null +++ b/test/std/go/ast/go125_symbols_test.go @@ -0,0 +1,30 @@ +//go:build go1.25 + +package ast_test + +import ( + "go/ast" + "go/parser" + "testing" +) + +func TestPreorderStack(t *testing.T) { + expr, err := parser.ParseExpr("left + right") + if err != nil { + t.Fatal(err) + } + found := false + ast.PreorderStack(expr, nil, func(node ast.Node, stack []ast.Node) bool { + ident, ok := node.(*ast.Ident) + if ok && ident.Name == "right" { + found = true + if len(stack) != 1 || stack[0] != expr { + t.Fatalf("stack for right identifier = %#v, want expression root", stack) + } + } + return true + }) + if !found { + t.Fatal("PreorderStack did not visit the right identifier") + } +} diff --git a/test/std/go/ast/go126_symbols_test.go b/test/std/go/ast/go126_symbols_test.go index b51f2c1d65..8444c0f75f 100644 --- a/test/std/go/ast/go126_symbols_test.go +++ b/test/std/go/ast/go126_symbols_test.go @@ -4,7 +4,6 @@ package ast_test import ( "go/ast" - "go/parser" "go/token" "reflect" "testing" @@ -36,24 +35,3 @@ func TestDirective(t *testing.T) { t.Fatalf("ParseArgs = %q, want %q", got, want) } } - -func TestPreorderStack(t *testing.T) { - expr, err := parser.ParseExpr("left + right") - if err != nil { - t.Fatal(err) - } - found := false - ast.PreorderStack(expr, nil, func(node ast.Node, stack []ast.Node) bool { - ident, ok := node.(*ast.Ident) - if ok && ident.Name == "right" { - found = true - if len(stack) != 1 || stack[0] != expr { - t.Fatalf("stack for right identifier = %#v, want expression root", stack) - } - } - return true - }) - if !found { - t.Fatal("PreorderStack did not visit the right identifier") - } -} diff --git a/test/std/go/token/go125_symbols_test.go b/test/std/go/token/go125_symbols_test.go new file mode 100644 index 0000000000..76a2f369de --- /dev/null +++ b/test/std/go/token/go125_symbols_test.go @@ -0,0 +1,27 @@ +//go:build go1.25 + +package token_test + +import ( + "go/token" + "testing" +) + +func TestAddExistingFiles(t *testing.T) { + var source token.FileSet + file := source.AddFile("source.go", -1, 10) + + var destination token.FileSet + destination.AddExistingFiles(file) + if got := destination.File(file.Pos(5)); got != file { + t.Fatalf("FileSet.File returned %p, want %p", got, file) + } + count := 0 + destination.Iterate(func(got *token.File) bool { + count++ + return got == file + }) + if count != 1 { + t.Fatalf("FileSet contains %d files, want 1", count) + } +} diff --git a/test/std/go/token/go126_symbols_test.go b/test/std/go/token/go126_symbols_test.go index 2e703a5303..b41ab6a2ed 100644 --- a/test/std/go/token/go126_symbols_test.go +++ b/test/std/go/token/go126_symbols_test.go @@ -7,24 +7,10 @@ import ( "testing" ) -func TestAddExistingFiles(t *testing.T) { +func TestFileEnd(t *testing.T) { var source token.FileSet file := source.AddFile("source.go", -1, 10) if got, want := file.End(), token.Pos(file.Base()+file.Size()); got != want { t.Fatalf("File.End = %d, want %d", got, want) } - - var destination token.FileSet - destination.AddExistingFiles(file) - if got := destination.File(file.Pos(5)); got != file { - t.Fatalf("FileSet.File returned %p, want %p", got, file) - } - count := 0 - destination.Iterate(func(got *token.File) bool { - count++ - return got == file - }) - if count != 1 { - t.Fatalf("FileSet contains %d files, want 1", count) - } } diff --git a/test/std/go/types/go126_symbols_test.go b/test/std/go/types/go125_symbols_test.go similarity index 98% rename from test/std/go/types/go126_symbols_test.go rename to test/std/go/types/go125_symbols_test.go index 79e775db3b..15f7bafa91 100644 --- a/test/std/go/types/go126_symbols_test.go +++ b/test/std/go/types/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package types_test diff --git a/test/std/hash/go126_symbols_test.go b/test/std/hash/go125_symbols_test.go similarity index 98% rename from test/std/hash/go126_symbols_test.go rename to test/std/hash/go125_symbols_test.go index 1d458c8cb3..13849bf69b 100644 --- a/test/std/hash/go126_symbols_test.go +++ b/test/std/hash/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package hash_test diff --git a/test/std/hash/maphash/go126_symbols_test.go b/test/std/hash/maphash/go125_symbols_test.go similarity index 96% rename from test/std/hash/maphash/go126_symbols_test.go rename to test/std/hash/maphash/go125_symbols_test.go index 84a89159dc..495abaea81 100644 --- a/test/std/hash/maphash/go126_symbols_test.go +++ b/test/std/hash/maphash/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package maphash_test diff --git a/test/std/io/fs/go126_symbols_test.go b/test/std/io/fs/go125_symbols_test.go similarity index 98% rename from test/std/io/fs/go126_symbols_test.go rename to test/std/io/fs/go125_symbols_test.go index abaaacbfa3..d32d875b8b 100644 --- a/test/std/io/fs/go126_symbols_test.go +++ b/test/std/io/fs/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package fs_test diff --git a/test/std/log/slog/go125_symbols_test.go b/test/std/log/slog/go125_symbols_test.go new file mode 100644 index 0000000000..af93c67b0c --- /dev/null +++ b/test/std/log/slog/go125_symbols_test.go @@ -0,0 +1,21 @@ +//go:build go1.25 + +package slog_test + +import ( + "log/slog" + "runtime" + "strings" + "testing" + "time" +) + +func TestRecordSource(t *testing.T) { + pcs := make([]uintptr, 1) + runtime.Callers(1, pcs) + record := slog.NewRecord(time.Time{}, slog.LevelInfo, "source", pcs[0]) + source := record.Source() + if source == nil || !strings.Contains(source.Function, "TestRecordSource") || source.Line == 0 { + t.Fatalf("Record.Source = %#v", source) + } +} diff --git a/test/std/log/slog/go126_symbols_test.go b/test/std/log/slog/go126_symbols_test.go index abfdbdadbd..fe9c6fe20b 100644 --- a/test/std/log/slog/go126_symbols_test.go +++ b/test/std/log/slog/go126_symbols_test.go @@ -7,13 +7,12 @@ import ( "context" "encoding/json" "log/slog" - "runtime" "strings" "testing" "time" ) -func TestMultiHandlerAndRecordSource(t *testing.T) { +func TestMultiHandler(t *testing.T) { var first, second bytes.Buffer multi := slog.NewMultiHandler( slog.NewTextHandler(&first, nil), @@ -68,12 +67,4 @@ func TestMultiHandlerAndRecordSource(t *testing.T) { if !ok || build["files"] != float64(2) { t.Fatalf("third JSON record has wrong build group: %#v", records[2]) } - - pcs := make([]uintptr, 1) - runtime.Callers(1, pcs) - sourceRecord := slog.NewRecord(time.Time{}, slog.LevelInfo, "source", pcs[0]) - source := sourceRecord.Source() - if source == nil || !strings.Contains(source.Function, "TestMultiHandlerAndRecordSource") || source.Line == 0 { - t.Fatalf("Record.Source = %#v", source) - } } diff --git a/test/std/mime/multipart/go126_symbols_test.go b/test/std/mime/multipart/go125_symbols_test.go similarity index 96% rename from test/std/mime/multipart/go126_symbols_test.go rename to test/std/mime/multipart/go125_symbols_test.go index 58c8382106..d30b3952ab 100644 --- a/test/std/mime/multipart/go126_symbols_test.go +++ b/test/std/mime/multipart/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package multipart_test diff --git a/test/std/net/http/go125_symbols_test.go b/test/std/net/http/go125_symbols_test.go new file mode 100644 index 0000000000..4b4196c7d7 --- /dev/null +++ b/test/std/net/http/go125_symbols_test.go @@ -0,0 +1,53 @@ +//go:build go1.25 + +package http_test + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestCrossOriginProtection(t *testing.T) { + protection := http.NewCrossOriginProtection() + if err := protection.AddTrustedOrigin("https://trusted.example"); err != nil { + t.Fatal(err) + } + protection.AddInsecureBypassPattern("POST /health") + + request := httptest.NewRequest(http.MethodPost, "http://service.example/write", nil) + request.Header.Set("Sec-Fetch-Site", "cross-site") + request.Header.Set("Origin", "https://evil.example") + if err := protection.Check(request); err == nil { + t.Fatal("Check accepted a cross-origin POST") + } + request.Header.Set("Origin", "https://trusted.example") + if err := protection.Check(request); err != nil { + t.Fatalf("Check rejected a trusted origin: %v", err) + } + + bypass := httptest.NewRequest(http.MethodPost, "http://service.example/health", nil) + bypass.Header.Set("Sec-Fetch-Site", "cross-site") + if err := protection.Check(bypass); err != nil { + t.Fatalf("Check rejected a bypass pattern: %v", err) + } + + protection.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusTeapot) + })) + recorder := httptest.NewRecorder() + protection.Handler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })).ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "http://service.example/write", nil)) + if recorder.Code != http.StatusNoContent { + t.Fatalf("same-origin handler status = %d, want %d", recorder.Code, http.StatusNoContent) + } + + recorder = httptest.NewRecorder() + rejected := httptest.NewRequest(http.MethodPost, "http://service.example/write", nil) + rejected.Header.Set("Sec-Fetch-Site", "cross-site") + protection.Handler(http.NotFoundHandler()).ServeHTTP(recorder, rejected) + if recorder.Code != http.StatusTeapot { + t.Fatalf("deny handler status = %d, want %d", recorder.Code, http.StatusTeapot) + } +} diff --git a/test/std/net/http/go126_symbols_test.go b/test/std/net/http/go126_symbols_test.go index 7e9b3297d3..5cf33287bb 100644 --- a/test/std/net/http/go126_symbols_test.go +++ b/test/std/net/http/go126_symbols_test.go @@ -70,47 +70,3 @@ func TestClientConn(t *testing.T) { t.Fatalf("closed state: available=%d in-flight=%d err=%v", connection.Available(), connection.InFlight(), connection.Err()) } } - -func TestCrossOriginProtection(t *testing.T) { - protection := http.NewCrossOriginProtection() - if err := protection.AddTrustedOrigin("https://trusted.example"); err != nil { - t.Fatal(err) - } - protection.AddInsecureBypassPattern("POST /health") - - request := httptest.NewRequest(http.MethodPost, "http://service.example/write", nil) - request.Header.Set("Sec-Fetch-Site", "cross-site") - request.Header.Set("Origin", "https://evil.example") - if err := protection.Check(request); err == nil { - t.Fatal("Check accepted a cross-origin POST") - } - request.Header.Set("Origin", "https://trusted.example") - if err := protection.Check(request); err != nil { - t.Fatalf("Check rejected a trusted origin: %v", err) - } - - bypass := httptest.NewRequest(http.MethodPost, "http://service.example/health", nil) - bypass.Header.Set("Sec-Fetch-Site", "cross-site") - if err := protection.Check(bypass); err != nil { - t.Fatalf("Check rejected a bypass pattern: %v", err) - } - - protection.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusTeapot) - })) - recorder := httptest.NewRecorder() - protection.Handler(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNoContent) - })).ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "http://service.example/write", nil)) - if recorder.Code != http.StatusNoContent { - t.Fatalf("same-origin handler status = %d, want %d", recorder.Code, http.StatusNoContent) - } - - recorder = httptest.NewRecorder() - rejected := httptest.NewRequest(http.MethodPost, "http://service.example/write", nil) - rejected.Header.Set("Sec-Fetch-Site", "cross-site") - protection.Handler(http.NotFoundHandler()).ServeHTTP(recorder, rejected) - if recorder.Code != http.StatusTeapot { - t.Fatalf("deny handler status = %d, want %d", recorder.Code, http.StatusTeapot) - } -} diff --git a/test/std/os/go125_symbols_test.go b/test/std/os/go125_symbols_test.go new file mode 100644 index 0000000000..8e3efe12ad --- /dev/null +++ b/test/std/os/go125_symbols_test.go @@ -0,0 +1,63 @@ +//go:build go1.25 + +package os_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + "time" +) + +func TestRootFileOperations(t *testing.T) { + directory := t.TempDir() + root, err := os.OpenRoot(directory) + if err != nil { + t.Fatal(err) + } + defer root.Close() + + if err := root.MkdirAll("nested/dir", 0755); err != nil { + t.Fatal(err) + } + if err := root.WriteFile("nested/dir/source.txt", []byte("contents"), 0644); err != nil { + t.Fatal(err) + } + data, err := root.ReadFile("nested/dir/source.txt") + if err != nil || string(data) != "contents" { + t.Fatalf("ReadFile = %q, %v; want contents, nil", data, err) + } + if err := root.Chmod("nested/dir/source.txt", 0600); err != nil { + t.Fatal(err) + } + when := time.Unix(123456789, 0) + if err := root.Chtimes("nested/dir/source.txt", when, when); err != nil { + t.Fatal(err) + } + if err := root.Chown("nested/dir/source.txt", -1, -1); err != nil { + t.Fatal(err) + } + if err := root.Link("nested/dir/source.txt", "nested/hardlink.txt"); err != nil { + t.Fatal(err) + } + if err := root.Rename("nested/hardlink.txt", "nested/renamed.txt"); err != nil { + t.Fatal(err) + } + if err := root.Symlink("dir/source.txt", "nested/symlink.txt"); err != nil { + t.Fatal(err) + } + if err := root.Lchown("nested/symlink.txt", -1, -1); err != nil { + t.Fatal(err) + } + target, err := root.Readlink("nested/symlink.txt") + if err != nil || target != "dir/source.txt" { + t.Fatalf("Readlink = %q, %v; want dir/source.txt, nil", target, err) + } + if err := root.RemoveAll("nested"); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(directory, "nested")); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("RemoveAll left nested directory: %v", err) + } +} diff --git a/test/std/os/go126_symbols_test.go b/test/std/os/go126_symbols_test.go index c1962f6cd1..0457e74eba 100644 --- a/test/std/os/go126_symbols_test.go +++ b/test/std/os/go126_symbols_test.go @@ -5,63 +5,9 @@ package os_test import ( "errors" "os" - "path/filepath" "testing" - "time" ) -func TestRootFileOperations(t *testing.T) { - directory := t.TempDir() - root, err := os.OpenRoot(directory) - if err != nil { - t.Fatal(err) - } - defer root.Close() - - if err := root.MkdirAll("nested/dir", 0755); err != nil { - t.Fatal(err) - } - if err := root.WriteFile("nested/dir/source.txt", []byte("contents"), 0644); err != nil { - t.Fatal(err) - } - data, err := root.ReadFile("nested/dir/source.txt") - if err != nil || string(data) != "contents" { - t.Fatalf("ReadFile = %q, %v; want contents, nil", data, err) - } - if err := root.Chmod("nested/dir/source.txt", 0600); err != nil { - t.Fatal(err) - } - when := time.Unix(123456789, 0) - if err := root.Chtimes("nested/dir/source.txt", when, when); err != nil { - t.Fatal(err) - } - if err := root.Chown("nested/dir/source.txt", -1, -1); err != nil { - t.Fatal(err) - } - if err := root.Link("nested/dir/source.txt", "nested/hardlink.txt"); err != nil { - t.Fatal(err) - } - if err := root.Rename("nested/hardlink.txt", "nested/renamed.txt"); err != nil { - t.Fatal(err) - } - if err := root.Symlink("dir/source.txt", "nested/symlink.txt"); err != nil { - t.Fatal(err) - } - if err := root.Lchown("nested/symlink.txt", -1, -1); err != nil { - t.Fatal(err) - } - target, err := root.Readlink("nested/symlink.txt") - if err != nil || target != "dir/source.txt" { - t.Fatalf("Readlink = %q, %v; want dir/source.txt, nil", target, err) - } - if err := root.RemoveAll("nested"); err != nil { - t.Fatal(err) - } - if _, err := os.Stat(filepath.Join(directory, "nested")); !errors.Is(err, os.ErrNotExist) { - t.Fatalf("RemoveAll left nested directory: %v", err) - } -} - func TestProcessWithHandle(t *testing.T) { process, err := os.FindProcess(os.Getpid()) if err != nil { diff --git a/test/std/reflect/go125_symbols_test.go b/test/std/reflect/go125_symbols_test.go new file mode 100644 index 0000000000..de4c26c7a4 --- /dev/null +++ b/test/std/reflect/go125_symbols_test.go @@ -0,0 +1,18 @@ +//go:build go1.25 + +package reflect_test + +import ( + "reflect" + "testing" +) + +func TestTypeAssert(t *testing.T) { + value := reflect.ValueOf(42) + if got, ok := reflect.TypeAssert[int](value); !ok || got != 42 { + t.Fatalf("TypeAssert[int] = %d, %v; want 42, true", got, ok) + } + if got, ok := reflect.TypeAssert[string](value); ok || got != "" { + t.Fatalf("TypeAssert[string] = %q, %v; want empty, false", got, ok) + } +} diff --git a/test/std/reflect/go126_symbols_test.go b/test/std/reflect/go126_symbols_test.go index b558df9558..b7507a604c 100644 --- a/test/std/reflect/go126_symbols_test.go +++ b/test/std/reflect/go126_symbols_test.go @@ -16,16 +16,6 @@ func (v go126Value) Summary(prefix string) string { return prefix + v.Name } -func TestTypeAssert(t *testing.T) { - value := reflect.ValueOf(42) - if got, ok := reflect.TypeAssert[int](value); !ok || got != 42 { - t.Fatalf("TypeAssert[int] = %d, %v; want 42, true", got, ok) - } - if got, ok := reflect.TypeAssert[string](value); ok || got != "" { - t.Fatalf("TypeAssert[string] = %q, %v; want empty, false", got, ok) - } -} - func TestValueFieldsAndMethods(t *testing.T) { value := reflect.ValueOf(go126Value{Name: "llgo", Count: 2}) fields := make(map[string]any) diff --git a/test/std/runtime/trace/go126_symbols_test.go b/test/std/runtime/trace/go125_symbols_test.go similarity index 92% rename from test/std/runtime/trace/go126_symbols_test.go rename to test/std/runtime/trace/go125_symbols_test.go index 7f77800f47..93203b04ee 100644 --- a/test/std/runtime/trace/go126_symbols_test.go +++ b/test/std/runtime/trace/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package trace_test @@ -27,7 +27,7 @@ func TestFlightRecorder(t *testing.T) { if !recorder.Enabled() { t.Fatal("flight recorder is disabled after Start") } - trace.WithRegion(context.Background(), "go1.26", func() { + trace.WithRegion(context.Background(), "go1.25", func() { trace.Log(context.Background(), "stdlib", "flight recorder") }) var output bytes.Buffer diff --git a/test/std/sync/go126_symbols_test.go b/test/std/sync/go125_symbols_test.go similarity index 95% rename from test/std/sync/go126_symbols_test.go rename to test/std/sync/go125_symbols_test.go index 7ee45d7aad..9ed8f8d141 100644 --- a/test/std/sync/go126_symbols_test.go +++ b/test/std/sync/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package sync_test diff --git a/test/std/testing/fstest/go126_symbols_test.go b/test/std/testing/fstest/go125_symbols_test.go similarity index 97% rename from test/std/testing/fstest/go126_symbols_test.go rename to test/std/testing/fstest/go125_symbols_test.go index 82340bf373..011ddee8de 100644 --- a/test/std/testing/fstest/go126_symbols_test.go +++ b/test/std/testing/fstest/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package fstest_test diff --git a/test/std/testing/go125_symbols_test.go b/test/std/testing/go125_symbols_test.go new file mode 100644 index 0000000000..5bdc0639a3 --- /dev/null +++ b/test/std/testing/go125_symbols_test.go @@ -0,0 +1,38 @@ +//go:build go1.25 + +package testing_test + +import ( + "fmt" + "testing" +) + +func TestOutputAndAttr(t *testing.T) { + t.Attr("go-version", "1.25+") + if _, err := fmt.Fprintln(t.Output(), "test output"); err != nil { + t.Fatal(err) + } +} + +func TestBenchmarkOutputAndAttr(t *testing.T) { + result := testing.Benchmark(func(b *testing.B) { + b.Attr("go-version", "1.25+") + fmt.Fprintln(b.Output(), "benchmark output") + for range b.N { + } + }) + if result.N <= 0 { + t.Fatalf("Benchmark ran %d iterations", result.N) + } +} + +func FuzzOutputAndAttr(f *testing.F) { + f.Attr("go-version", "1.25+") + fmt.Fprintln(f.Output(), "fuzz output") + f.Add("seed") + f.Fuzz(func(t *testing.T, input string) { + if input == "" { + t.Skip() + } + }) +} diff --git a/test/std/testing/synctest/synctest_test.go b/test/std/testing/synctest/synctest_test.go index f5de1ee28b..6bfe9f909b 100644 --- a/test/std/testing/synctest/synctest_test.go +++ b/test/std/testing/synctest/synctest_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package synctest_test diff --git a/test/std/unicode/go126_symbols_test.go b/test/std/unicode/go125_symbols_test.go similarity index 97% rename from test/std/unicode/go126_symbols_test.go rename to test/std/unicode/go125_symbols_test.go index 8b0bfb3480..2af5a8387c 100644 --- a/test/std/unicode/go126_symbols_test.go +++ b/test/std/unicode/go125_symbols_test.go @@ -1,4 +1,4 @@ -//go:build go1.26 +//go:build go1.25 package unicode_test From 9618cfc3c4ea90481442e2b88544fd1c09fefcf8 Mon Sep 17 00:00:00 2001 From: visualfc Date: Tue, 18 Aug 2026 15:08:36 +0800 Subject: [PATCH 2/3] runtime: fix trace stop deadlock on Go 1.25 --- runtime/internal/lib/runtime/trace_stub_llgo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/internal/lib/runtime/trace_stub_llgo.go b/runtime/internal/lib/runtime/trace_stub_llgo.go index 35f5512b3a..28aa8fed4d 100644 --- a/runtime/internal/lib/runtime/trace_stub_llgo.go +++ b/runtime/internal/lib/runtime/trace_stub_llgo.go @@ -11,7 +11,7 @@ func traceAdvance(stopTrace bool) {} func traceClockNow() uint64 { return 0 } //go:linkname runtime_readTrace runtime/trace.runtime_readTrace -func runtime_readTrace() []byte { return nil } +func runtime_readTrace() []byte { return ReadTrace() } //go:linkname trace_userTaskCreate runtime/trace.userTaskCreate func trace_userTaskCreate(id, parentID uint64, taskType string) {} From e7e6b163a1b97b0c47b8299174686c373043fd19 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 18 Aug 2026 16:32:11 +0800 Subject: [PATCH 3/3] ci: support macOS Bash 3.2 in test scripts --- dev/test_std_buildmodes.sh | 22 ++++++++++++++-------- doc/_readme/scripts/check_std_cover.sh | 19 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/dev/test_std_buildmodes.sh b/dev/test_std_buildmodes.sh index 407ff72f37..4dc1431005 100755 --- a/dev/test_std_buildmodes.sh +++ b/dev/test_std_buildmodes.sh @@ -9,18 +9,19 @@ fi root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" test_pkgs=("$@") -go_list_flags=() -llgo_modfile_flags=() -if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then - go_list_flags+=("-modfile=${LLGO_TEST_MODFILE}") - llgo_modfile_flags+=("-modfile=${LLGO_TEST_MODFILE}") -fi +go_list() { + if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + command go list "-modfile=${LLGO_TEST_MODFILE}" "$@" + else + command go list "$@" + fi +} import_paths=() stems=() groups=() max_group=0 for test_pkg in "${test_pkgs[@]}"; do - package_info="$(go list "${go_list_flags[@]}" -tags=llgo -f '{{.ImportPath}} {{.Dir}}' "${test_pkg}")" + package_info="$(go_list -tags=llgo -f '{{.ImportPath}} {{.Dir}}' "${test_pkg}")" read -r import_path package_dir <<<"${package_info}" case "${import_path}" in github.com/xgo-dev/llgo/test/std/*) ;; @@ -94,7 +95,12 @@ for mode in c-shared c-archive; do echo "==> ${mode}: compile ${#group_imports[@]} test package(s)" ( cd "${work_dir}" - "${llgo_cmd}" test -c -buildmode="${mode}" "${llgo_modfile_flags[@]}" "${group_imports[@]}" + if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + "${llgo_cmd}" test -c -buildmode="${mode}" \ + "-modfile=${LLGO_TEST_MODFILE}" "${group_imports[@]}" + else + "${llgo_cmd}" test -c -buildmode="${mode}" "${group_imports[@]}" + fi ) for i in "${!import_paths[@]}"; do diff --git a/doc/_readme/scripts/check_std_cover.sh b/doc/_readme/scripts/check_std_cover.sh index f4e3b14363..b128a69526 100755 --- a/doc/_readme/scripts/check_std_cover.sh +++ b/doc/_readme/scripts/check_std_cover.sh @@ -5,21 +5,24 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" cd "${ROOT_DIR}" -go_list_flags=() -if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then - go_list_flags+=("-modfile=${LLGO_TEST_MODFILE}") -fi +go_list() { + if [[ -n "${LLGO_TEST_MODFILE:-}" ]]; then + command go list "-modfile=${LLGO_TEST_MODFILE}" "$@" + else + command go list "$@" + fi +} -module_path="$(go list "${go_list_flags[@]}" -m)" +module_path="$(go_list -m)" packages=() if [[ $# -eq 0 ]]; then - package_output="$(go list "${go_list_flags[@]}" ./test/std/... | sort)" + package_output="$(go_list ./test/std/... | sort)" while IFS= read -r pkg; do [[ -n "${pkg}" ]] && packages+=("${pkg}") done <<< "${package_output}" else - package_output="$(go list "${go_list_flags[@]}" "$@" | sort -u)" + package_output="$(go_list "$@" | sort -u)" while IFS= read -r pkg; do [[ -n "${pkg}" ]] && packages+=("${pkg}") done <<< "${package_output}" @@ -50,7 +53,7 @@ if [[ $# -eq 0 ]]; then covered_file="$(mktemp)" trap 'rm -f "${expected_file}" "${covered_file}"' EXIT - go list "${go_list_flags[@]}" std \ + go_list std \ | awk '!/(^|\/)internal(\/|$)/ && !/(^|\/)vendor(\/|$)/' \ | sort -u > "${expected_file}" printf '%s\n' "${covered_packages[@]}" | sort -u > "${covered_file}"