diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c839c1..5e4103d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: integration: name: Sentry integration / ${{ matrix.arch }} runs-on: ${{ matrix.runner }} - timeout-minutes: 30 + timeout-minutes: 60 strategy: fail-fast: false matrix: @@ -112,6 +112,8 @@ jobs: bash sentry/build-linux.sh "$TEST_BIN" cd testdata go test -mod=readonly -ldflags='-checklinkname=0 -s=false -w=false' -c -o "$TEST_BIN/ixgo.test" ./ixgo + go test -mod=readonly -ldflags='-checklinkname=0 -s=false -w=false' -c -o "$TEST_BIN/ixgo-upstream.test" ./ixgo/upstream + go test -mod=readonly -ldflags='-checklinkname=0 -s=false -w=false' -c -o "$TEST_BIN/reflect.test" ./reflect go build -mod=readonly -ldflags='-checklinkname=0 -s=false -w=false' -o "$TEST_BIN/smoke" . cd llar go test -mod=readonly -ldflags='-checklinkname=0 -s=false -w=false' -c -o "$TEST_BIN/llar.test" . @@ -119,6 +121,22 @@ jobs: if: ${{ !cancelled() && steps.build.outcome == 'success' }} working-directory: testdata/ixgo run: sudo env GLIBC_TUNABLES="$GLIBC_TUNABLES" SANDBOX_TEST_LIBRARY="$SANDBOX_TEST_LIBRARY" "$TEST_BIN/ixgo.test" -test.v -test.timeout=10m + - name: Test Go reflect in Sentry + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + working-directory: testdata + run: | + sudo env GLIBC_TUNABLES="$GLIBC_TUNABLES" SANDBOX_TEST_LIBRARY="$SANDBOX_TEST_LIBRARY" \ + PATH="$PATH" GOTOOLCHAIN="$GOTOOLCHAIN" GOROOT="$(go env GOROOT)" \ + GOPATH="$(go env GOPATH)" GOMODCACHE="$(go env GOMODCACHE)" GOCACHE="$(go env GOCACHE)" \ + TMPDIR=/tmp "$TEST_BIN/reflect.test" -test.v -test.timeout=20m + - name: Test upstream ixgo interpreter state round trips + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + working-directory: testdata + run: | + sudo env GLIBC_TUNABLES="$GLIBC_TUNABLES" SANDBOX_TEST_LIBRARY="$SANDBOX_TEST_LIBRARY" \ + PATH="$PATH" GOTOOLCHAIN="$GOTOOLCHAIN" GOROOT="$(go env GOROOT)" \ + GOPATH="$(go env GOPATH)" GOMODCACHE="$(go env GOMODCACHE)" GOCACHE="$(go env GOCACHE)" \ + TMPDIR=/tmp "$TEST_BIN/ixgo-upstream.test" -test.v -test.timeout=40m - name: Test LLAR formulas if: ${{ !cancelled() && steps.build.outcome == 'success' }} working-directory: testdata/llar diff --git a/testdata/go.mod b/testdata/go.mod index b8996f7..b4306ad 100644 --- a/testdata/go.mod +++ b/testdata/go.mod @@ -5,7 +5,9 @@ go 1.26.6 require ( github.com/goplus/ixgo v1.1.6 github.com/xgo-dev/sandbox v0.0.0 + golang.org/x/mod v0.39.0 golang.org/x/sys v0.47.0 + golang.org/x/tools v0.49.0 ) require ( @@ -15,8 +17,7 @@ require ( github.com/visualfc/goembed v0.3.4 // indirect github.com/visualfc/xtype v0.3.3 // indirect golang.org/x/arch v0.28.0 // indirect - golang.org/x/mod v0.39.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/sync v0.22.0 // indirect ) replace github.com/xgo-dev/sandbox => .. diff --git a/testdata/goroot/upstream_test.go b/testdata/goroot/upstream_test.go index 2caf456..fdf6d44 100644 --- a/testdata/goroot/upstream_test.go +++ b/testdata/goroot/upstream_test.go @@ -52,6 +52,12 @@ func init() { gorootTestSkips["fixedbugs/issue30116u.go"] = "BUG, slice bound check" gorootTestSkips["chan/select5.go"] = "bug, select case expr call order" + // Sandbox-specific: ixgo Goexit compares the current goroutine ID with the + // interpreter's host mainid. Exclude these cases until IDs are rebound. + gorootTestSkips["fixedbugs/issue5963.go"] = "ixgo Goexit depends on a process-local goroutine ID" + gorootTestSkips["fixedbugs/issue8158.go"] = "ixgo Goexit depends on a process-local goroutine ID" + gorootTestSkips["fixedbugs/issue11256.go"] = "ixgo Goexit depends on a process-local goroutine ID" + // fixedbugs/issue7740.go // const ulp = (1.0 + (2.0 / 3.0)) - (5.0 / 3.0) // Go 1.14 1.15 1.16 ulp = 1.4916681462400413e-154 diff --git a/testdata/ixgo/reflect_public_test.go b/testdata/ixgo/reflect_public_test.go new file mode 100644 index 0000000..623a613 --- /dev/null +++ b/testdata/ixgo/reflect_public_test.go @@ -0,0 +1,210 @@ +//go:build linux && (amd64 || arm64) && cgo + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in testdata/LICENSE. + +package ixgo_test + +import ( + "reflect" + "testing" +) + +// These cases adapt public API checks from Go 1.26.6 reflect/all_test.go, +// type_test.go and set_test.go. Objects are created before Run; assertions +// also check aliases and mutations after the guest returns. +type reflectCounter struct{ N int } + +func (c *reflectCounter) Add(n int) int { c.N += n; return c.N } + +type reflectAdder interface{ Add(int) int } +type reflectIntPointer *int +type reflectOtherIntPointer *int + +func publicReflectTypes() []reflect.Type { + number := reflect.TypeFor[int]() + return []reflect.Type{ + number, reflect.TypeFor[any](), reflect.TypeFor[reflectAdder](), + reflect.PointerTo(number), reflect.ArrayOf(3, number), reflect.SliceOf(number), + reflect.MapOf(reflect.TypeFor[string](), number), + reflect.ChanOf(reflect.BothDir, number), + reflect.ChanOf(reflect.RecvDir, number), + reflect.ChanOf(reflect.SendDir, number), + reflect.StructOf([]reflect.StructField{{Name: "Count", Type: number, Tag: `json:"count"`}}), + reflect.FuncOf([]reflect.Type{number}, []reflect.Type{number}, false), + reflect.FuncOf([]reflect.Type{number, reflect.SliceOf(number)}, []reflect.Type{number}, true), + } +} + +func TestReflectPublicTypes(t *testing.T) { + types := publicReflectTypes() + var returned []reflect.Type + if err := runSandbox(t, func() { + local := publicReflectTypes() + for i, typ := range types { + if typ != local[i] { + panic("restored type differs from the guest constructor") + } + } + field := types[10].Field(0) + if field.Name != "Count" || field.Tag.Get("json") != "count" || field.Type != types[0] { + panic("StructOf field metadata changed") + } + if !types[12].IsVariadic() || types[12].In(1) != types[5] || types[12].Out(0) != types[0] { + panic("FuncOf signature changed") + } + returned = local + }); err != nil { + t.Fatal(err) + } + for i, typ := range types { + if i >= len(returned) || returned[i] != typ { + t.Fatalf("returned type %d lost host identity", i) + } + } +} + +func TestReflectPublicStructValue(t *testing.T) { + typ := reflect.StructOf([]reflect.StructField{ + {Name: "Count", Type: reflect.TypeFor[int](), Tag: `json:"count"`}, + {Name: "Name", Type: reflect.TypeFor[string]()}, + }) + value := reflect.New(typ).Elem() + value.Field(0).SetInt(41) + value.Field(1).SetString("host") + count := value.Field(0).Addr().Interface().(*int) + if err := runSandbox(t, func() { + if value.Type() != typ || !value.CanSet() || value.Field(0).Addr().Interface().(*int) != count { + panic("reflected struct lost its type, access or field alias") + } + value.FieldByName("Count").SetInt(42) + value.FieldByName("Name").SetString("guest") + if *count != 42 { + panic("reflected field mutation lost alias") + } + }); err != nil { + t.Fatal(err) + } + if *count != 42 || value.Field(0).Int() != 42 || value.Field(1).String() != "guest" || value.Type() != typ { + t.Fatal("reflected struct mutation was not written back") + } +} + +func TestReflectPublicContainers(t *testing.T) { + array := [3]int{1, 2, 3} + slice := array[:] + arrayValue, sliceValue := reflect.ValueOf(&array).Elem(), reflect.ValueOf(slice) + mapping := reflect.MakeMap(reflect.TypeFor[map[string][]int]()) + mapping.SetMapIndex(reflect.ValueOf("shared"), sliceValue) + if err := runSandbox(t, func() { + arrayValue.Index(1).SetInt(20) + fromMap := mapping.MapIndex(reflect.ValueOf("shared")) + if sliceValue.Index(1).Int() != 20 || fromMap.Index(1).Int() != 20 || slice[1] != 20 { + panic("array, slice and map lost their shared backing storage") + } + fromMap.Index(2).SetInt(30) + mapping.SetMapIndex(reflect.ValueOf("grown"), reflect.Append(sliceValue, reflect.ValueOf(4))) + }); err != nil { + t.Fatal(err) + } + if array != [3]int{1, 20, 30} || &slice[0] != &array[0] { + t.Fatal("array or slice identity was not written back") + } + shared := mapping.MapIndex(reflect.ValueOf("shared")).Interface().([]int) + grown := mapping.MapIndex(reflect.ValueOf("grown")).Interface().([]int) + if &shared[0] != &array[0] || !reflect.DeepEqual(grown, []int{1, 20, 30, 4}) { + t.Fatal("reflected map or appended slice was not written back") + } +} + +func TestReflectPublicMethods(t *testing.T) { + counter := &reflectCounter{N: 10} + var boxed any = counter + value := reflect.ValueOf(&boxed).Elem() + method := reflect.ValueOf(counter).MethodByName("Add") + var result int + if err := runSandbox(t, func() { + if value.Elem().Interface().(*reflectCounter) != counter { + panic("interface lost receiver identity") + } + if method.Call([]reflect.Value{reflect.ValueOf(2)})[0].Int() != 12 { + panic("bound reflected method") + } + m, ok := reflect.TypeOf(counter).MethodByName("Add") + if !ok || m.Func.Call([]reflect.Value{reflect.ValueOf(counter), reflect.ValueOf(3)})[0].Int() != 15 { + panic("reflected method expression") + } + result = value.Elem().Interface().(reflectAdder).Add(4) + }); err != nil { + t.Fatal(err) + } + if result != 19 || counter.N != 19 || boxed.(*reflectCounter) != counter { + t.Fatal("method receiver mutation was not written back") + } + if got := method.Call([]reflect.Value{reflect.ValueOf(1)})[0].Int(); got != 20 || counter.N != 20 { + t.Fatal("original reflected method lost its receiver") + } +} + +func TestReflectPublicMakeFunc(t *testing.T) { + n := 10 + typ := reflect.FuncOf([]reflect.Type{reflect.TypeFor[int](), reflect.TypeFor[[]int]()}, []reflect.Type{reflect.TypeFor[int]()}, true) + value := reflect.MakeFunc(typ, func(in []reflect.Value) []reflect.Value { + n += int(in[0].Int()) + for i := 0; i < in[1].Len(); i++ { + n += int(in[1].Index(i).Int()) + } + return []reflect.Value{reflect.ValueOf(n)} + }) + fn := value.Interface().(func(int, ...int) int) + var results [3]int + if err := runSandbox(t, func() { + if value.Type() != typ { + panic("MakeFunc signature changed") + } + results[0] = fn(1, 2, 3) + results[1] = int(value.Call([]reflect.Value{reflect.ValueOf(4), reflect.ValueOf(5)})[0].Int()) + results[2] = int(value.CallSlice([]reflect.Value{reflect.ValueOf(6), reflect.ValueOf([]int{7, 8})})[0].Int()) + }); err != nil { + t.Fatal(err) + } + if results != [3]int{16, 25, 46} || n != 46 { + t.Fatalf("MakeFunc results=%v capture=%d", results, n) + } + if got := fn(1); got != 47 || n != 47 { + t.Fatal("original MakeFunc lost its captured variable") + } +} + +func TestReflectPublicAssignableTo(t *testing.T) { + cases := []struct { + from, to reflect.Type + want bool + }{ + {reflect.TypeFor[chan int](), reflect.TypeFor[<-chan int](), true}, + {reflect.TypeFor[<-chan int](), reflect.TypeFor[chan int](), false}, + {reflect.TypeFor[*int](), reflect.TypeFor[reflectIntPointer](), true}, + {reflect.TypeFor[reflectIntPointer](), reflect.TypeFor[*int](), true}, + {reflect.TypeFor[reflectIntPointer](), reflect.TypeFor[reflectOtherIntPointer](), false}, + {reflect.TypeFor[*reflectCounter](), reflect.TypeFor[reflectAdder](), true}, + {reflect.TypeFor[reflectCounter](), reflect.TypeFor[reflectAdder](), false}, + } + var checked int + if err := runSandbox(t, func() { + for _, test := range cases { + if test.from.AssignableTo(test.to) != test.want { + panic("AssignableTo changed after transfer") + } + if test.to.Kind() == reflect.Interface && test.from.Implements(test.to) != test.want { + panic("Implements changed after transfer") + } + checked++ + } + }); err != nil { + t.Fatal(err) + } + if checked != len(cases) { + t.Fatalf("checked %d assignability cases, want %d", checked, len(cases)) + } +} diff --git a/testdata/ixgo/testdata/types/generic.go b/testdata/ixgo/testdata/types/generic.go new file mode 100644 index 0000000..4ebdf08 --- /dev/null +++ b/testdata/ixgo/testdata/types/generic.go @@ -0,0 +1,122 @@ +// Copyright 2022 The GoPlus Authors (goplus.org). 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. + +// Adapted from ixgo v1.1.6 typeparam_test.go: TestTypeParamNamed, +// TestNestedTypeParams, TestTypeParamsRecursive and TestAtomicPointer. +// Prepare the captured values on the host and check them on every invocation. +package main + +import ( + "path/filepath" + "reflect" + "sync/atomic" +) + +type N int +type _Nil[A any, B any] struct{} + +func PrepareTypeParamNamed() func() int { + var v1 _Nil[int, N] + var v2 _Nil[filepath.WalkFunc, _Nil[int, N]] + values := []any{v1, v2} + types := []reflect.Type{reflect.TypeOf(v1), reflect.TypeOf(v2)} + calls := 0 + return func() int { + if s := types[0].String(); s != "main._Nil[int,main.N]" { + panic(s) + } + if s := types[1].String(); s != "main._Nil[path/filepath.WalkFunc,main._Nil[int,main.N]]" { + panic(s) + } + for i, value := range values { + if reflect.TypeOf(value) != types[i] { + panic("generic value and reflect.Type lost identity") + } + } + calls++ + return calls + } +} + +func nested[N ~int](initial N) func() int { + type T []N + values := T{initial} + typ := reflect.TypeOf(values) + return func() int { + if reflect.TypeOf(values) != typ { + panic("local generic type changed") + } + values[0]++ + return int(values[0] - initial) + } +} + +func PrepareNestedTypeParams() func() int { return nested(N(100)) } + +type Integer interface { + ~int | ~int32 | ~int64 +} + +func recur1[T Integer](n T) T { + if n == 0 || n == 1 { + return T(1) + } + return n * recur2(n-1) +} + +func recur2[T Integer](n T) T { + list := make([]T, n) + for i := range list { + list[i] = T(i + 1) + } + var sum T + for _, value := range list { + sum += value + } + return sum + recur1(n-1) +} + +func PrepareTypeParamsRecursive() func() int { + type T int + n := T(5) + calls := 0 + return func() int { + if recur1(n) != T(110) { + panic("recursive generic call") + } + calls++ + return calls + } +} + +func PrepareAtomicPointer() func() int { + var i atomic.Int64 + i.Store(200) + n, text := 200, "hello" + var number atomic.Pointer[int] + var word atomic.Pointer[string] + number.Store(&n) + word.Store(&text) + calls := 0 + return func() int { + if number.Load() != &n || word.Load() != &text || *word.Load() != "hello" { + panic("atomic pointer lost target or alias") + } + calls++ + *number.Load()++ + if i.Add(1) != int64(200+calls) || n != 200+calls { + panic("atomic value was not written back") + } + return calls + } +} diff --git a/testdata/ixgo/testdata/types/methods.go b/testdata/ixgo/testdata/types/methods.go new file mode 100644 index 0000000..7731b57 --- /dev/null +++ b/testdata/ixgo/testdata/types/methods.go @@ -0,0 +1,64 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in ../LICENSE. + +// Adapted from ixgo v1.1.6 interp_test.go: TestReflectArray and +// TestRuntimeRefelct. Keep receivers, reflected values and interfaces alive +// across the sandbox boundary and check mutations through their aliases. +package main + +import ( + "fmt" + "reflect" +) + +type IntArray [2]int + +func (a IntArray) String() string { return fmt.Sprintf("(%v,%v)", a[0], a[1]) } +func (a *IntArray) Set(x, y int) { a[0], a[1] = x, y } +func (a IntArray) Get() (int, int) { return a[0], a[1] } +func (a IntArray) Scale(n int) IntArray { return IntArray{a[0] * n, a[1] * n} } + +func PrepareReflectArray() func() int { + a := &IntArray{100, 200} + typ := reflect.TypeOf(a).Elem() + v := reflect.ValueOf(a).Elem() + calls := 0 + return func() int { + if v.Type() != typ || v.Addr().Interface().(*IntArray) != a { + panic("reflected array lost type or receiver") + } + calls++ + v.Addr().MethodByName("Set").Call([]reflect.Value{ + reflect.ValueOf(100 + calls), reflect.ValueOf(200 + calls), + }) + b := a.Scale(5) + result := v.MethodByName("Scale").Call([]reflect.Value{reflect.ValueOf(5)})[0] + if b[0] != 5*(100+calls) || b[1] != 5*(200+calls) || + result.Index(0).Int() != int64(b[0]) || result.Index(1).Int() != int64(b[1]) { + panic("direct and reflected array methods disagree") + } + return calls + } +} + +type Game struct{ Runs int } + +func (g *Game) MainEntry() { g.Runs++ } + +type Alias = Game +type Entry interface{ MainEntry() } + +func PrepareAliasInterface() func() int { + g := &Alias{} + var entry Entry = g + var boxed any = entry + typ := reflect.TypeOf(g) + return func() int { + if reflect.TypeOf(boxed) != typ || typ != reflect.TypeOf((*Game)(nil)) || boxed.(*Game) != g { + panic("alias, interface or receiver identity changed") + } + entry.MainEntry() + return g.Runs + } +} diff --git a/testdata/ixgo/types_test.go b/testdata/ixgo/types_test.go new file mode 100644 index 0000000..273eb7a --- /dev/null +++ b/testdata/ixgo/types_test.go @@ -0,0 +1,64 @@ +//go:build linux && (amd64 || arm64) && cgo + +package ixgo_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/goplus/ixgo" + _ "github.com/goplus/ixgo/pkg/path/filepath" + _ "github.com/goplus/ixgo/pkg/sync/atomic" +) + +// Adapt the ixgo v1.1.6 type cases into closures whose state exists before Run. +// The fixture comments identify the upstream cases and the added writeback checks. +func TestIxgoTypeRoundTrips(t *testing.T) { + for _, test := range []struct { + file, name string + }{ + {"generic.go", "TypeParamNamed"}, + {"generic.go", "NestedTypeParams"}, + {"generic.go", "TypeParamsRecursive"}, + {"generic.go", "AtomicPointer"}, + {"methods.go", "ReflectArray"}, + {"methods.go", "AliasInterface"}, + } { + t.Run(test.name, func(t *testing.T) { + path := filepath.Join("testdata", "types", test.file) + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + interp, err := ixgo.NewContext(ixgo.SupportMultipleInterp).LoadInterp(path, data) + if err != nil { + t.Fatal(err) + } + defer interp.UnsafeRelease() + if err := interp.RunInit(); err != nil { + t.Fatal(err) + } + value, err := interp.RunFunc("Prepare" + test.name) + if err != nil { + t.Fatal(err) + } + fn := value.(func() int) + if got := fn(); got != 1 { + t.Fatalf("host before Run: got %d, want 1", got) + } + var first, second int + if err := runSandbox(t, func() { + first, second = fn(), fn() + }); err != nil { + t.Fatal(err) + } + if first != 2 || second != 3 { + t.Fatalf("guest: got %d, %d, want 2, 3", first, second) + } + if got := fn(); got != 4 { + t.Fatalf("original host closure after Run: got %d, want 4", got) + } + }) + } +} diff --git a/testdata/ixgo/upstream/interpreter_test.go b/testdata/ixgo/upstream/interpreter_test.go new file mode 100644 index 0000000..8af4a7f --- /dev/null +++ b/testdata/ixgo/upstream/interpreter_test.go @@ -0,0 +1,270 @@ +//go:build linux && (amd64 || arm64) && cgo + +package upstream_test + +import ( + "bytes" + "context" + _ "embed" + "go/ast" + "go/format" + "go/types" + "os" + "os/exec" + "path/filepath" + "regexp" + "slices" + "sort" + "strings" + "testing" + "time" + + "golang.org/x/mod/modfile" + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" +) + +//go:embed testdata/bridge.go.txt +var bridgeSource string + +//go:embed testdata/main.go.txt +var mainSource string + +// TestInterpreterUpstream preserves the pinned ixgo interpreter tests and their +// assertions. Only test call sites change: loading stays on the host, execution +// goes through Sandbox.Run, and results are restored before assertions run. +// REPL, optimizer, and direct-call tests are outside this suite. +func TestInterpreterUpstream(t *testing.T) { + if os.Getenv("SANDBOX_TEST_LIBRARY") == "" { + t.Fatal("SANDBOX_TEST_LIBRARY must point to the matching Sentry shared library") + } + const path = "github.com/goplus/ixgo" + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | + packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | + packages.NeedImports | packages.NeedDeps | packages.NeedModule, + Tests: true, + BuildFlags: []string{"-mod=readonly"}, + }, path) + if err != nil { + t.Fatal(err) + } + if packages.PrintErrors(pkgs) != 0 { + t.Fatal("load ixgo tests") + } + dir, err := os.MkdirTemp("", "sandbox-ixgo-upstream-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(dir); err != nil { + t.Error(err) + } + }) + if err := os.Chmod(dir, 0755); err != nil { + t.Fatal(err) + } + replacements := make(map[string]string) + var sourceDir string + var names []string + for _, pkg := range pkgs { + if pkg.PkgPath != path && pkg.PkgPath != path+"_test" { + continue + } + for _, file := range pkg.Syntax { + filename := pkg.Fset.Position(file.Pos()).Filename + if !strings.HasSuffix(filename, "_test.go") { + continue + } + if _, exists := replacements[filename]; exists { + continue + } + sourceDir = filepath.Dir(filename) + base := filepath.Base(filename) + if base == "repl_test.go" || base == "transform_test.go" || base == "direct_call_test.go" { + file.Decls, file.Comments = nil, nil + } else { + if base == "interp_test.go" { + file.Decls = slices.DeleteFunc(file.Decls, func(decl ast.Decl) bool { + fn, ok := decl.(*ast.FuncDecl) + return ok && fn.Name.Name == "TestGeneratedDirectCalls" + }) + astutil.DeleteImport(pkg.Fset, file, "github.com/goplus/ixgo/testdata/direct_call/github.com/goplus/ixgo/testdata/direct_call/pkg") + } + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if ok && fn.Recv == nil && strings.HasPrefix(fn.Name.Name, "Test") { + names = append(names, fn.Name.Name) + } + } + ast.Inspect(file, func(node ast.Node) bool { + call, ok := node.(*ast.CallExpr) + if !ok { + return true + } + selector, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + fn, ok := pkg.TypesInfo.Uses[selector.Sel].(*types.Func) + if !ok || fn.Pkg() == nil || fn.Pkg().Path() != path { + return true + } + sig := fn.Type().(*types.Signature) + name := fn.Name() + var args []ast.Expr + if sig.Recv() == nil { + if name != "Run" && name != "RunFile" { + return true + } + args = append(args, &ast.CallExpr{ + Fun: &ast.SelectorExpr{X: selector.X, Sel: ast.NewIdent("NewContext")}, + Args: []ast.Expr{call.Args[len(call.Args)-1]}, + }) + args = append(args, call.Args[:len(call.Args)-1]...) + } else { + switch name { + case "Run", "RunFile", "RunPkg", "RunInterp", "RunTest", "RunMain", "RunInit", "RunFunc": + default: + return true + } + args = append([]ast.Expr{selector.X}, call.Args...) + if name == "RunFunc" { + recv := sig.Recv().Type().(*types.Pointer).Elem().(*types.Named) + name = recv.Obj().Name() + name + } + } + helper := ast.NewIdent("SandboxTest" + name) + if pkg.PkgPath == path { + call.Fun = helper + } else { + call.Fun = &ast.SelectorExpr{X: ast.NewIdent("ixgo"), Sel: helper} + } + call.Args = args + return true + }) + } + var data bytes.Buffer + if err := format.Node(&data, pkg.Fset, file); err != nil { + t.Fatal(err) + } + filenameCopy := filepath.Join(dir, base) + if err := os.WriteFile(filenameCopy, data.Bytes(), 0600); err != nil { + t.Fatal(err) + } + replacements[filename] = filenameCopy + } + } + if sourceDir == "" || len(names) == 0 { + t.Fatal("no ixgo interpreter tests found") + } + sort.Strings(names) + for name, source := range map[string]string{ + "sandbox_bridge_test.go": bridgeSource, + "sandbox_main_test.go": mainSource, + } { + filename := filepath.Join(dir, name) + if err := os.WriteFile(filename, []byte(source), 0600); err != nil { + t.Fatal(err) + } + replacements[filepath.Join(sourceDir, name)] = filename + } + // Go forbids overlays beneath GOMODCACHE. Build and run a writable copy; + // only its test files are adapted, and production sources remain unchanged. + work := filepath.Join(dir, "work") + if err := os.CopyFS(work, os.DirFS(sourceDir)); err != nil { + t.Fatal(err) + } + for original, replacement := range replacements { + data, err := os.ReadFile(replacement) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(work, filepath.Base(original)), data, 0600); err != nil { + t.Fatal(err) + } + } + modulePath, err := exec.Command("go", "env", "GOMOD").Output() + if err != nil { + t.Fatal(err) + } + moduleDir := filepath.Dir(strings.TrimSpace(string(modulePath))) + data, err := os.ReadFile(filepath.Join(moduleDir, "go.mod")) + if err != nil { + t.Fatal(err) + } + mod, err := modfile.Parse("go.mod", data, nil) + if err != nil { + t.Fatal(err) + } + for _, replacement := range mod.Replace { + if replacement.New.Version == "" && !filepath.IsAbs(replacement.New.Path) { + if err := mod.AddReplace(replacement.Old.Path, replacement.Old.Version, + filepath.Join(moduleDir, replacement.New.Path), ""); err != nil { + t.Fatal(err) + } + } + } + if err := mod.AddReplace(path, "", work, ""); err != nil { + t.Fatal(err) + } + data, err = mod.Format() + if err != nil { + t.Fatal(err) + } + modPath := filepath.Join(dir, "go.mod") + if err := os.WriteFile(modPath, data, 0600); err != nil { + t.Fatal(err) + } + sums, err := os.ReadFile(filepath.Join(moduleDir, "go.sum")) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "go.sum"), sums, 0600); err != nil { + t.Fatal(err) + } + binary := filepath.Join(dir, "ixgo.test") + cmd := exec.Command("go", "test", "-mod=readonly", "-modfile="+modPath, + "-ldflags=-checklinkname=0 -s=false -w=false", "-c", "-o", binary, path) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("build ixgo interpreter tests: %v\n%s", err, output) + } + t.Logf("%d upstream interpreter tests", len(names)) + for _, name := range names { + t.Run(name, func(t *testing.T) { + switch name { + case "TestGoexitDeadlock": + t.Skip("ixgo retains the host main goroutine ID; Goexit is not supported across migration") + case "TestRunContext": + t.Skip("host context.timerCtx/cancelCtx contains process-local cancellation and timer state") + case "TestTestdataFiles", "TestTestdataFilesRace1", "TestTestdataFilesRace2", "TestTestdataFilesRace3", "TestTestdataFilesRace4", "TestTestdataFilesRace5": + t.Run("issue5963.go", func(t *testing.T) { + t.Skip("Goexit fixture excluded from the upstream corpus: ixgo retains the host main goroutine ID") + }) + case "TestEmbedImethod", "TestStructEmbed": + t.Skip("test-time ixgo package registration is not transferred to the guest") + case "TestShadowedMethod": + // reflectx v1.7.8 drops private field PkgPath when cloning method sets. + // https://github.com/goplus/reflectx/pull/115 + t.Skip("reflectx loses private field PkgPath, preventing type reconstruction") + case "TestInterpreter_ConcurrentRun1": + t.Skip("reflectxtype does not preserve shared methods, exhausting icall slots during concurrent round trips") + } + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Minute) + defer cancel() + cmd := exec.CommandContext(ctx, binary, "-test.v", "-test.timeout=4m", "-test.run=^"+regexp.QuoteMeta(name)+"$") + cmd.Dir = work + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("ixgo state round trip: %v\n%s", err, output) + } + if bytes.Contains(output, []byte("--- SKIP: "+name+" ")) { + t.Skipf("upstream skipped:\n%s", output) + } + if !bytes.Contains(output, []byte("--- PASS: "+name+" ")) { + t.Fatalf("upstream test did not report completion:\n%s", output) + } + t.Logf("%s", output) + }) + } +} diff --git a/testdata/ixgo/upstream/testdata/bridge.go.txt b/testdata/ixgo/upstream/testdata/bridge.go.txt new file mode 100644 index 0000000..2692a74 --- /dev/null +++ b/testdata/ixgo/upstream/testdata/bridge.go.txt @@ -0,0 +1,97 @@ +package ixgo + +import ( + "os" + "path/filepath" + "strings" + + "golang.org/x/tools/go/ssa" +) + +// Installed by the external test package to avoid an ixgo -> sandbox import +// cycle. A migration failure panics so expected interpreter errors cannot hide it. +var SandboxTestRoundTrip func(func()) + +func SandboxTestRunFile(ctx *Context, filename string, src interface{}, args []string) (int, error) { + pkg, err := ctx.LoadFile(filename, src) + if err != nil { + return 2, err + } + return SandboxTestRunPkg(ctx, pkg, filename, args) +} + +func SandboxTestRun(ctx *Context, path string, args []string) (int, error) { + if strings.HasSuffix(path, ".go") { + return SandboxTestRunFile(ctx, path, nil, args) + } + pkg, err := ctx.LoadDir(path, false) + if err != nil { + return 2, err + } + if !isMainPkg(pkg) { + return 2, ErrNotFoundMain + } + return SandboxTestRunPkg(ctx, pkg, path, args) +} + +func SandboxTestRunPkg(ctx *Context, pkg *ssa.Package, input string, args []string) (int, error) { + interp, err := ctx.NewInterp(pkg) + if err != nil { + return 2, err + } + defer interp.UnsafeRelease() + return SandboxTestRunInterp(ctx, interp, input, args) +} + +func SandboxTestRunInterp(ctx *Context, interp *Interp, input string, args []string) (code int, err error) { + SandboxTestRoundTrip(func() { code, err = ctx.RunInterp(interp, input, args) }) + return +} + +func SandboxTestRunMain(interp *Interp) (code int, err error) { + SandboxTestRoundTrip(func() { code, err = interp.RunMain() }) + return +} + +func SandboxTestRunInit(interp *Interp) (err error) { + SandboxTestRoundTrip(func() { err = interp.RunInit() }) + return +} + +func SandboxTestInterpRunFunc(interp *Interp, name string, args ...Value) (value Value, err error) { + SandboxTestRoundTrip(func() { value, err = interp.RunFunc(name, args...) }) + return +} + +func SandboxTestContextRunFunc(ctx *Context, pkg *ssa.Package, name string, args ...Value) (Value, error) { + interp, err := ctx.NewInterp(pkg) + if err != nil { + return nil, err + } + return SandboxTestInterpRunFunc(interp, name, args...) +} + +func SandboxTestRunTest(ctx *Context, dir string, args []string) error { + pkg, err := ctx.LoadDir(dir, true) + if err != nil { + if err == ErrNoTestFiles { + return nil + } + return err + } + if filepath.IsAbs(dir) { + wd, err := os.Getwd() + if err != nil { + return err + } + if err := os.Chdir(dir); err != nil { + return err + } + defer os.Chdir(wd) + } + code, err := SandboxTestRunPkg(ctx, pkg, dir, args) + if err != nil || code != 0 { + return ErrTestFailed + } + return nil +} diff --git a/testdata/ixgo/upstream/testdata/main.go.txt b/testdata/ixgo/upstream/testdata/main.go.txt new file mode 100644 index 0000000..085646c --- /dev/null +++ b/testdata/ixgo/upstream/testdata/main.go.txt @@ -0,0 +1,53 @@ +package ixgo_test + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "sync/atomic" + "testing" + + "github.com/goplus/ixgo" + "github.com/xgo-dev/sandbox" +) + +func TestMain(m *testing.M) { + // All corpus runners share this list. Keep the remaining programs and their + // concurrency unchanged; issue5963 executes Goexit during package init. + testdataTests = slices.DeleteFunc(testdataTests, func(name string) bool { + return name == "issue5963.go" + }) + executable, err := os.Executable() + if err != nil { + panic(err) + } + binaryDir := filepath.Dir(executable) + s := sandbox.Sandbox{ + Library: os.Getenv("SANDBOX_TEST_LIBRARY"), + Mounts: []sandbox.Mount{ + {Type: "bind", Source: "/", Target: "/", Options: []string{"ro"}}, + {Type: "tmpfs", Target: "/tmp"}, + {Type: "bind", Source: binaryDir, Target: binaryDir, Options: []string{"ro"}}, + {Type: "proc", Target: "/proc"}, + }, + } + var roundTrips atomic.Int64 + ixgo.SandboxTestRoundTrip = func(fn func()) { + completed := false + if err := s.Run(func() { fn(); completed = true }); err != nil { + panic(fmt.Errorf("state round trip: %w", err)) + } + if !completed { + panic("guest completion was not written back") + } + roundTrips.Add(1) + } + code := m.Run() + if err := s.Close(); err != nil { + fmt.Fprintln(os.Stderr, err) + code = 1 + } + fmt.Fprintf(os.Stderr, "state round trips: %d\n", roundTrips.Load()) + os.Exit(code) +} diff --git a/testdata/ixgo/upstream_test.go b/testdata/ixgo/upstream_test.go index 9e1ec8c..0d47f58 100644 --- a/testdata/ixgo/upstream_test.go +++ b/testdata/ixgo/upstream_test.go @@ -37,6 +37,9 @@ func TestUpstreamPrograms(t *testing.T) { "issue23536.go", "tinyfin.go", "issue5963.go", } { t.Run(file, func(t *testing.T) { + if file == "issue5963.go" { + t.Skip("Goexit during package init is excluded from migration coverage") + } path := filepath.Join("testdata", file) data, err := os.ReadFile(path) if err != nil { diff --git a/testdata/reflect/testdata/driver.go.txt b/testdata/reflect/testdata/driver.go.txt new file mode 100644 index 0000000..c01e4ac --- /dev/null +++ b/testdata/reflect/testdata/driver.go.txt @@ -0,0 +1,23 @@ +package sandboxdriver + +import ( + "flag" + "os" + "regexp" + "testing" + "testing/internal/testdeps" +) + +// Run constructs testing's state in the guest. The host's testing.T and M +// contain process resources and are not part of the transferred closure. +func Run(test testing.InternalTest) int { + if err := os.Setenv("TMPDIR", "/tmp"); err != nil { + panic(err) + } + testdeps.ImportPath = "reflect" + m := testing.MainStart(testdeps.TestDeps{}, []testing.InternalTest{test}, nil, nil, nil) + if err := flag.CommandLine.Parse([]string{"-test.v", "-test.run=^" + regexp.QuoteMeta(test.Name) + "$", "-test.timeout=2m"}); err != nil { + panic(err) + } + return m.Run() +} diff --git a/testdata/reflect/testdata/main.go.txt b/testdata/reflect/testdata/main.go.txt new file mode 100644 index 0000000..71ad84a --- /dev/null +++ b/testdata/reflect/testdata/main.go.txt @@ -0,0 +1,60 @@ +package reflect_test + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "testing" + "testing/sandboxdriver" + + "github.com/xgo-dev/sandbox" +) + +var sandboxReflectCase = flag.String("sandbox-reflect", "", "run one upstream reflect test in Sentry") + +var sandboxReflectTests = []testing.InternalTest{ + // TESTS +} + +func TestMain(m *testing.M) { + flag.Parse() + if *sandboxReflectCase == "" { + os.Exit(m.Run()) + } + for _, test := range sandboxReflectTests { + if test.Name != *sandboxReflectCase { + continue + } + code, completed := 1, false + executable, err := os.Executable() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + binaryDir := filepath.Dir(executable) + s := sandbox.Sandbox{ + Library: os.Getenv("SANDBOX_TEST_LIBRARY"), + Mounts: []sandbox.Mount{ + {Type: "bind", Source: "/", Target: "/", Options: []string{"ro"}}, + {Type: "tmpfs", Target: "/tmp"}, + {Type: "bind", Source: binaryDir, Target: binaryDir, Options: []string{"ro"}}, + {Type: "proc", Target: "/proc"}, + }, + } + if err := s.Run(func() { + code = sandboxdriver.Run(test) + completed = true + }); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if !completed { + fmt.Fprintln(os.Stderr, "guest completion was not written back") + os.Exit(1) + } + os.Exit(code) + } + fmt.Fprintln(os.Stderr, "unknown upstream reflect test:", *sandboxReflectCase) + os.Exit(1) +} diff --git a/testdata/reflect/upstream_test.go b/testdata/reflect/upstream_test.go new file mode 100644 index 0000000..6698163 --- /dev/null +++ b/testdata/reflect/upstream_test.go @@ -0,0 +1,134 @@ +//go:build linux && (amd64 || arm64) && cgo + +package reflect_test + +import ( + "bytes" + "context" + _ "embed" + "encoding/json" + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "testing" + "time" +) + +//go:embed testdata/main.go.txt +var sandboxMain string + +//go:embed testdata/driver.go.txt +var sandboxDriver string + +// TestStandardReflect runs the current toolchain's unmodified reflect tests +// inside Sentry. Objects created by those tests live in the guest; the separate +// ixgo/reflect_public_test.go cases exercise host-created object round trips. +func TestStandardReflect(t *testing.T) { + if os.Getenv("SANDBOX_TEST_LIBRARY") == "" { + t.Fatal("SANDBOX_TEST_LIBRARY must point to the matching Sentry shared library") + } + cmd := exec.Command("go", "list", "-json", "reflect") + data, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + var pkg struct { + Dir string + XTestGoFiles []string + } + if err := json.Unmarshal(data, &pkg); err != nil { + t.Fatal(err) + } + var names []string + fset := token.NewFileSet() + for _, name := range pkg.XTestGoFiles { + file, err := parser.ParseFile(fset, filepath.Join(pkg.Dir, name), nil, 0) + if err != nil { + t.Fatal(err) + } + for _, decl := range file.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Recv != nil || !strings.HasPrefix(fn.Name.Name, "Test") { + continue + } + if fn.Name.Name == "TestMain" { + t.Fatal("upstream reflect now has a TestMain; review the sandbox entry") + } + names = append(names, fn.Name.Name) + } + } + if len(names) == 0 { + t.Fatal("no upstream reflect tests found") + } + sort.Strings(names) + var entries strings.Builder + for _, name := range names { + fmt.Fprintf(&entries, "\t{%q, %s},\n", name, name) + } + dir, err := os.MkdirTemp("", "sandbox-reflect-") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll(dir); err != nil { + t.Error(err) + } + }) + if err := os.Chmod(dir, 0755); err != nil { + t.Fatal(err) + } + mainPath, driverPath := filepath.Join(dir, "main.go"), filepath.Join(dir, "driver.go") + if err := os.WriteFile(mainPath, []byte(strings.Replace(sandboxMain, "// TESTS", entries.String(), 1)), 0600); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(driverPath, []byte(sandboxDriver), 0600); err != nil { + t.Fatal(err) + } + // Keep the upstream internal imports and export_test.go in their original + // package context. Overlay files exist only for this test's compilation. + overlay := struct{ Replace map[string]string }{map[string]string{ + filepath.Join(pkg.Dir, "sandbox_entry_test.go"): mainPath, + filepath.Join(runtime.GOROOT(), "src", "testing", "sandboxdriver", "driver.go"): driverPath, + }} + data, err = json.Marshal(overlay) + if err != nil { + t.Fatal(err) + } + overlayPath := filepath.Join(dir, "overlay.json") + if err := os.WriteFile(overlayPath, data, 0600); err != nil { + t.Fatal(err) + } + binary := filepath.Join(dir, "reflect.test") + cmd = exec.Command("go", "test", "-vet=off", "-mod=readonly", "-overlay="+overlayPath, + "-ldflags=-checklinkname=0 -s=false -w=false", "-c", "-o", binary, "reflect") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("build upstream reflect: %v\n%s", err, output) + } + t.Logf("%s: %d upstream reflect tests", runtime.Version(), len(names)) + for _, name := range names { + t.Run(name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 3*time.Minute) + defer cancel() + cmd := exec.CommandContext(ctx, binary, "-sandbox-reflect="+name) + cmd.Dir = pkg.Dir + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("reflect/Sentry: %v (context: %v)\n%s", err, ctx.Err(), output) + } + if bytes.Contains(output, []byte("--- SKIP: "+name+" ")) { + t.Skipf("upstream skipped:\n%s", output) + } + if !bytes.Contains(output, []byte("--- PASS: "+name+" ")) { + t.Fatalf("upstream test did not report completion:\n%s", output) + } + t.Logf("%s", output) + }) + } +}