Repository: goplus/gop
Pull request #1662 (2024-01-23 03:44:18)

add _test.gox (classfile for testing)


Patch:
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index 2e61bcb0..91acd95c 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -50,13 +50,3 @@ jobs:
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         WINGET_PKGS_PRIVATE_KEY: ${{ secrets.WINGET_PKGS_PRIVATE_KEY }}
-
-    - name: Upload deb/rpm to Fury.io
-      run: |
-        for file in dist/*.{deb,rpm}
-        do
-          echo "Uploading $file to Fury.io"
-          curl -sS -F package=@$file https://$FURY_TOKEN@push.fury.io/goplus/
-        done
-      env:
-        FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 36519f89..908dddb4 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -68,25 +68,12 @@ changelog:
       - "^test:"
 
 winget:
-  - name: goplus
+  - name: gop
     homepage: "https://goplus.org/"
     publisher: goplus
     publisher_url: https://github.com/goplus/gop
     publisher_support_url: "https://github.com/goplus/gop/issues/new"
     package_identifier: goplus.gop
-    path: "manifests/g/goplus/gop/{{.Version}}"
-    tags:
-      - golang
-      - go
-      - gop
-      - goplus
-      - programming
-      - language
-      - compiler
-      - interpreter
-      - data science
-      - engineering
-      - education
     short_description: The Go+ Programming Language
     description: |
       The Go+ programming language is designed for engineering, STEM education, and data science.
@@ -116,7 +103,7 @@ winget:
           branch: master
 
 nfpms:
-  - package_name: gop
+  - package_name: goplus-gop
     vendor: goplus
     homepage: https://goplus.org/
     maintainer: Li Jie <cpunion@gmail.com>
@@ -192,7 +179,7 @@ nfpms:
 
 snapcrafts:
   - id: gop
-    name: gop
+    name: goplus
     title: The Go+ Programming Language
     summary: The Go+ Programming Language
     description: |
diff --git a/README.md b/README.md
index f0f460e8..397bc498 100644
--- a/README.md
+++ b/README.md
@@ -47,41 +47,6 @@ For more details, see [Quick Start](doc/docs.md).
 
 ## How to install
 
-### on Windows
-
-```sh
-winget install goplus
-```
-
-Or
-
-```sh
-winget install goplus.gop
-```
-
-### on Debian/Ubuntu
-
-```sh
-sudo bash -c ' echo "deb [trusted=yes] https://pkgs.goplus.org/apt/ /" > /etc/apt/sources.list.d/goplus.list'
-sudo apt update
-sudo apt install gop
-```
-
-### on RedHat/CentOS/Fedora
-
-```sh
-sudo bash -c 'echo -e "[goplus]\nname=Go+ Repo\nbaseurl=https://pkgs.goplus.org/yum/\nenabled=1\ngpgcheck=0" > /etc/yum.repos.d/goplus.repo'
-sudo yum install gop
-```
-
-### on macOS/Linux(Homebrew)
-
-Install via [brew](https://brew.sh/)
-
-```sh
-$ brew install goplus
-```
-
 ### from source code
 
 For now, we suggest you install Go+ from source code.
@@ -98,6 +63,14 @@ cd gop
 all.bat
 ```
 
+### on macOS/Linux
+
+Install via [brew](https://brew.sh/)
+```sh
+$ brew install goplus
+```
+
+
 ## Go+ Applications
 
 ### 2D Games powered by Go+
diff --git a/cl/builtin.go b/cl/builtin.go
index 661635ad..dda8c6d3 100644
--- a/cl/builtin.go
+++ b/cl/builtin.go
@@ -25,23 +25,22 @@ import (
 
 // -----------------------------------------------------------------------------
 
-func initMathBig(pkg *gox.Package, conf *gox.Config, big *gox.PkgRef) {
-	big.EnsureImported()
+func initMathBig(pkg *gox.Package, conf *gox.Config, big gox.PkgRef) {
 	conf.UntypedBigInt = big.Ref("UntypedBigint").Type().(*types.Named)
 	conf.UntypedBigRat = big.Ref("UntypedBigrat").Type().(*types.Named)
 	conf.UntypedBigFloat = big.Ref("UntypedBigfloat").Type().(*types.Named)
 }
 
-func initBuiltinFns(builtin *types.Package, scope *types.Scope, pkg *gox.PkgRef, fns []string) {
+func initBuiltinFns(builtin *types.Package, scope *types.Scope, pkg gox.PkgRef, fns []string) {
 	for _, fn := range fns {
 		fnTitle := string(fn[0]-'a'+'A') + fn[1:]
 		scope.Insert(gox.NewOverloadFunc(token.NoPos, builtin, fn, pkg.Ref(fnTitle)))
 	}
 }
 
-func initBuiltin(pkg *gox.Package, builtin *types.Package, os, fmt, ng, iox, buil *gox.PkgRef) {
+func initBuiltin(pkg *gox.Package, builtin *types.Package, os, fmt, ng, iox, buil gox.PkgRef) {
 	scope := builtin.Scope()
-	if ng != nil {
+	if ng.Types != nil {
 		typs := []string{"bigint", "bigrat", "bigfloat"}
 		for _, typ := range typs {
 			name := string(typ[0]-('a'-'A')) + typ[1:]
@@ -50,7 +49,7 @@ func initBuiltin(pkg *gox.Package, builtin *types.Package, os, fmt, ng, iox, bui
 		scope.Insert(types.NewTypeName(token.NoPos, builtin, "uint128", ng.Ref("Uint128").Type()))
 		scope.Insert(types.NewTypeName(token.NoPos, builtin, "int128", ng.Ref("Int128").Type()))
 	}
-	if fmt != nil {
+	if fmt.Types != nil {
 		scope.Insert(gox.NewOverloadFunc(token.NoPos, builtin, "echo", fmt.Ref("Println")))
 		initBuiltinFns(builtin, scope, fmt, []string{
 			"print", "println", "printf", "errorf",
@@ -58,18 +57,18 @@ func initBuiltin(pkg *gox.Package, builtin *types.Package, os, fmt, ng, iox, bui
 			"sprint", "sprintln", "sprintf",
 		})
 	}
-	if os != nil {
+	if os.Types != nil {
 		initBuiltinFns(builtin, scope, os, []string{
 			"open", "create",
 		})
 	}
-	if iox != nil {
+	if iox.Types != nil {
 		initBuiltinFns(builtin, scope, iox, []string{
 			"lines",
 		})
 		scope.Insert(gox.NewOverloadFunc(token.NoPos, builtin, "blines", iox.Ref("BLines")))
 	}
-	if buil != nil {
+	if buil.Types != nil {
 		scope.Insert(gox.NewOverloadFunc(token.NoPos, builtin, "newRange", buil.Ref("NewRange__0")))
 	}
 	scope.Insert(types.NewTypeName(token.NoPos, builtin, "any", gox.TyEmptyInterface))
@@ -84,7 +83,7 @@ func newBuiltinDefault(pkg *gox.Package, conf *gox.Config) *types.Package {
 	iox := pkg.TryImport("github.com/goplus/gop/builtin/iox")
 	pkg.TryImport("strconv")
 	pkg.TryImport("strings")
-	if ng != nil {
+	if ng.Types != nil {
 		initMathBig(pkg, conf, ng)
 	}
 	initBuiltin(pkg, builtin, os, fmt, ng, iox, buil)
diff --git a/cl/c.go b/cl/c.go
index b203bbb2..bf0d15ae 100644
--- a/cl/c.go
+++ b/cl/c.go
@@ -57,12 +57,12 @@ func c2goBase(base string) string {
 
 // -----------------------------------------------------------------------------
 
-func loadC2goPkg(ctx *blockCtx, realPath string, src *ast.BasicLit) *gox.PkgRef {
+func loadC2goPkg(ctx *blockCtx, realPath string, src *ast.BasicLit) (ret gox.PkgRef) {
 	cpkg, err := ctx.cpkgs.Import(realPath)
 	if err != nil {
 		ctx.handleErrorf(src.Pos(),
 			"%v not found or not a valid C package (c2go.a.pub file not found).\n", realPath)
-		return nil
+		return
 	}
 	ctx.clookups = append(ctx.clookups, cpkg)
 	return cpkg.Pkg()
diff --git a/cl/classfile.go b/cl/classfile.go
index 1b4e8e5d..d019783e 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -45,10 +45,11 @@ type gmxProject struct {
 	sptypes    []string           // <sptype>.spx
 	scheds     []string
 	schedStmts []goast.Stmt // nil or len(scheds) == 2 (delayload)
-	pkgImps    []*gox.PkgRef
+	pkgImps    []gox.PkgRef
 	pkgPaths   []string
 	hasScheds  bool
 	gameIsPtr  bool
+	isTest     bool
 }
 
 func (p *gmxProject) getScheds(cb *gox.CodeBuilder) []goast.Stmt {
@@ -77,6 +78,10 @@ func ClassNameAndExt(file string) (name, ext string) {
 	return
 }
 
+func isGoxTestFile(ext string) bool {
+	return strings.HasSuffix(ext, "test.gox")
+}
+
 func loadClass(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Config) *gmxProject {
 	tname, ext := ClassNameAndExt(file)
 	gt, ok := conf.LookupClass(ext)
@@ -89,10 +94,11 @@ func loadClass(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Co
 		p = &gmxProject{pkgPaths: pkgPaths}
 		ctx.projs[gt.Ext] = p
 
-		p.pkgImps = make([]*gox.PkgRef, len(pkgPaths))
+		p.pkgImps = make([]gox.PkgRef, len(pkgPaths))
 		for i, pkgPath := range pkgPaths {
 			p.pkgImps[i] = pkg.Import(pkgPath)
 		}
+
 		spx := p.pkgImps[0]
 		if gt.Class != "" {
 			p.game, p.gameIsPtr = spxRef(spx, gt.Class)
@@ -107,12 +113,12 @@ func loadClass(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Co
 		}
 	}
 	if f.IsProj {
-		if p.gameClass != "" {
-			panic("TODO: multiple project files found")
-		}
 		if tname == "main" {
 			tname = gt.Class
 		}
+		if p.gameClass != "" {
+			log.Panicln("multiple project files found:", tname, p.gameClass)
+		}
 		p.gameClass = tname
 	} else {
 		p.sptypes = append(p.sptypes, tname)
@@ -124,7 +130,7 @@ func loadClass(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Co
 	return p
 }
 
-func spxLookup(pkgImps []*gox.PkgRef, name string) gox.Ref {
+func spxLookup(pkgImps []gox.PkgRef, name string) gox.Ref {
 	for _, pkg := range pkgImps {
 		if o := pkg.TryRef(name); o != nil {
 			return o
@@ -133,7 +139,7 @@ func spxLookup(pkgImps []*gox.PkgRef, name string) gox.Ref {
 	panic("spxLookup: symbol not found - " + name)
 }
 
-func spxTryRef(spx *gox.PkgRef, typ string) (obj types.Object, isPtr bool) {
+func spxTryRef(spx gox.PkgRef, typ string) (obj types.Object, isPtr bool) {
 	if strings.HasPrefix(typ, "*") {
 		typ, isPtr = typ[1:], true
 	}
@@ -141,7 +147,7 @@ func spxTryRef(spx *gox.PkgRef, typ string) (obj types.Object, isPtr bool) {
 	return
 }
 
-func spxRef(spx *gox.PkgRef, typ string) (obj gox.Ref, isPtr bool) {
+func spxRef(spx gox.PkgRef, typ string) (obj gox.Ref, isPtr bool) {
 	obj, isPtr = spxTryRef(spx, typ)
 	if obj == nil {
 		panic(spx.Path() + "." + typ + " not found")
@@ -149,7 +155,7 @@ func spxRef(spx *gox.PkgRef, typ string) (obj gox.Ref, isPtr bool) {
 	return
 }
 
-func getStringConst(spx *gox.PkgRef, name string) string {
+func getStringConst(spx gox.PkgRef, name string) string {
 	if o := spx.TryRef(name); o != nil {
 		if c, ok := o.(*types.Const); ok {
 			return constant.StringVal(c.Val())
@@ -185,14 +191,50 @@ func setBodyHandler(ctx *blockCtx) {
 	}
 }
 
-func gmxMainFunc(p *gox.Package, ctx *pkgCtx, noAutoGenMain bool) func() {
-	if len(ctx.projs) == 1 { // only one project file
-		var proj *gmxProject
-		for _, v := range ctx.projs {
-			proj = v
-			break
+const (
+	casePrefix = "case"
+)
+
+func testNameSuffix(testType string) string {
+	if c := testType[0]; c >= 'A' && c <= 'Z' {
+		return testType
+	}
+	return "_" + testType
+}
+
+func gmxTestFunc(pkg *gox.Package, testType string, isProj bool) {
+	if isProj {
+		genTestFunc(pkg, "TestMain", testType, "m", "M")
+	} else {
+		name := testNameSuffix(testType)
+		genTestFunc(pkg, "Test"+name, casePrefix+name, "t", "T")
+	}
+}
+
+func genTestFunc(pkg *gox.Package, name, testType, param, paramType string) {
+	testing := pkg.Import("testing")
+	objT := testing.Ref(paramType)
+	paramT := types.NewParam(token.NoPos, pkg.Types, param, types.NewPointer(objT.Type()))
+	params := types.NewTuple(paramT)
+
+	pkg.NewFunc(nil, name, params, nil, false).BodyStart(pkg).
+		Val(pkg.Builtin().Ref("new")).Val(pkg.Ref(testType)).Call(1).
+		MemberVal("TestMain").Val(paramT).Call(1).EndStmt().
+		End()
+}
+
+func gmxMainFunc(pkg *gox.Package, ctx *pkgCtx, noAutoGenMain bool) func() {
+	var proj *gmxProject
+	for _, v := range ctx.projs {
+		if v.isTest {
+			continue
+		} else if proj != nil {
+			return nil
 		}
-		scope := p.Types.Scope()
+		proj = v
+	}
+	if proj != nil { // only one project file
+		scope := pkg.Types.Scope()
 		var o types.Object
 		if proj.gameClass != "" {
 			o = scope.Lookup(proj.gameClass)
@@ -205,10 +247,10 @@ func gmxMainFunc(p *gox.Package, ctx *pkgCtx, noAutoGenMain bool) func() {
 		if !noAutoGenMain && o != nil {
 			// new(Game).Main()
 			// new(Game).Main(workers...)
-			fn := p.NewFunc(nil, "main", nil, nil, false)
+			fn := pkg.NewFunc(nil, "main", nil, nil, false)
 			return func() {
-				new := p.Builtin().Ref("new")
-				cb := fn.BodyStart(p).Val(new).Val(o).Call(1).MemberVal("Main")
+				new := pkg.Builtin().Ref("new")
+				cb := fn.BodyStart(pkg).Val(new).Val(o).Call(1).MemberVal("Main")
 
 				sig := cb.Get(-1).Type.(*types.Signature)
 				narg := gmxMainNarg(sig)
diff --git a/cl/compile.go b/cl/compile.go
index 3b9367c5..cd1a5acb 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -354,7 +354,7 @@ type pkgCtx struct {
 }
 
 type pkgImp struct {
-	*gox.PkgRef
+	gox.PkgRef
 	pkgName *types.PkgName
 }
 
@@ -364,8 +364,8 @@ type blockCtx struct {
 	pkg        *gox.Package
 	cb         *gox.CodeBuilder
 	imports    map[string]pkgImp
-	lookups    []*gox.PkgRef
-	clookups   []*cpackages.PkgRef
+	lookups    []gox.PkgRef
+	clookups   []cpackages.PkgRef
 	tlookup    *typeParamLookup
 	c2goBase   string // default is `github.com/goplus/`
 	relBaseDir string
@@ -529,9 +529,22 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 		}
 	}
 
+	// sort files
+	type File struct {
+		*ast.File
+		path string
+	}
+	sfiles := make([]*File, 0, len(files))
 	for fpath, f := range files {
+		sfiles = append(sfiles, &File{f, fpath})
+	}
+	sort.Slice(sfiles, func(i, j int) bool {
+		return sfiles[i].path < sfiles[j].path
+	})
+
+	for _, f := range sfiles {
 		fileLine := !conf.NoFileLine
-		fileScope := types.NewScope(p.Types.Scope(), f.Pos(), f.End(), fpath)
+		fileScope := types.NewScope(p.Types.Scope(), f.Pos(), f.End(), f.path)
 		ctx := &blockCtx{
 			pkg: p, pkgCtx: ctx, cb: p.CB(), relBaseDir: relBaseDir, fileScope: fileScope,
 			fileLine: fileLine, isClass: f.IsClass, rec: rec,
@@ -540,7 +553,7 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 		if rec := ctx.rec; rec != nil {
 			rec.Scope(f, fileScope)
 		}
-		preloadGopFile(p, ctx, fpath, f, conf)
+		preloadGopFile(p, ctx, f.path, f.File, conf)
 	}
 
 	gopSyms := make(map[string]bool) // TODO: remove this map
@@ -556,24 +569,11 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 			pkg: p, pkgCtx: ctx, cb: p.CB(), relBaseDir: relBaseDir,
 			imports: make(map[string]pkgImp),
 		}
-		preloadFile(p, ctx, fpath, f, false, false)
+		preloadFile(p, ctx, fpath, f, skippingGoFile, false)
 	}
 
 	initGopPkg(ctx, p, gopSyms)
 
-	// sort files
-	type File struct {
-		*ast.File
-		path string
-	}
-	sfiles := make([]*File, 0, len(files))
-	for fpath, f := range files {
-		sfiles = append(sfiles, &File{f, fpath})
-	}
-	sort.Slice(sfiles, func(i, j int) bool {
-		return sfiles[i].path < sfiles[j].path
-	})
-
 	// genMain = true if it is main package and no main func
 	var genMain bool
 	var gen func()
@@ -692,30 +692,43 @@ func loadFile(ctx *pkgCtx, f *ast.File) {
 	}
 }
 
-func genGoFile(file string, gopFile bool) string {
-	if gopFile {
-		if strings.HasSuffix(file, "_test.gop") {
-			return testingGoFile
-		}
-		return defaultGoFile
+// gen testingGoFile for:
+//
+//	*_test.gop
+//	*test.gox
+func genGoFile(file string, goxTestFile bool) string {
+	if goxTestFile || strings.HasSuffix(file, "_test.gop") {
+		return testingGoFile
 	}
-	return skippingGoFile
+	return defaultGoFile
 }
 
 func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, conf *Config) {
 	var proj *gmxProject
 	var classType string
+	var testType string
 	var baseTypeName string
 	var baseType types.Type
 	var spxClass bool
+	var goxTestFile bool
 	var parent = ctx.pkgCtx
 	if f.IsClass {
 		if f.IsNormalGox {
 			classType, _ = ClassNameAndExt(file)
+			if classType == "main" {
+				classType = "_main"
+			}
 		} else {
 			c := parent.classes[f]
-			classType, proj = c.tname, c.proj
-			ctx.proj = proj
+			proj, ctx.proj = c.proj, c.proj
+			classType = c.tname
+			if isGoxTestFile(c.ext) { // test classfile
+				testType = c.tname
+				goxTestFile, proj.isTest = true, true
+				if !f.IsProj {
+					classType = casePrefix + testNameSuffix(testType)
+				}
+			}
 			if f.IsProj {
 				o := proj.game
 				baseTypeName, baseType = o.Name(), o.Type()
@@ -728,12 +741,13 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 			}
 		}
 	}
+	goFile := genGoFile(file, goxTestFile)
 	if classType != "" {
 		if debugLoad {
 			log.Println("==> Preload type", classType)
 		}
 		if proj != nil {
-			ctx.lookups = make([]*gox.PkgRef, len(proj.pkgPaths))
+			ctx.lookups = make([]gox.PkgRef, len(proj.pkgPaths))
 			for i, pkgPath := range proj.pkgPaths {
 				ctx.lookups[i] = p.Import(pkgPath)
 			}
@@ -746,7 +760,9 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 			if debugLoad {
 				log.Println("==> Load > NewType", classType)
 			}
-			decl := p.NewType(classType)
+			old, _ := p.SetCurFile(goFile, true)
+			defer p.RestoreCurFile(old)
+			decl := p.NewTypeDefs().NewType(classType)
 			ld.typInit = func() { // decycle
 				if debugLoad {
 					log.Println("==> Load > InitType", classType)
@@ -833,11 +849,19 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 				Type: &ast.FuncType{
 					Params: &ast.FieldList{},
 				},
-				Body: &ast.BlockStmt{},
+				Body:   &ast.BlockStmt{},
+				Shadow: true,
 			})
 		}
 	}
-	preloadFile(p, ctx, file, f, true, !conf.Outline)
+	preloadFile(p, ctx, file, f, goFile, !conf.Outline)
+	if goxTestFile {
+		parent.inits = append(parent.inits, func() {
+			old, _ := p.SetCurFile(testingGoFile, true)
+			gmxTestFunc(p, testType, f.IsProj)
+			p.RestoreCurFile(old)
+		})
+	}
 }
 
 func parseTypeEmbedName(typ ast.Expr) *ast.Ident {
@@ -854,10 +878,9 @@ retry:
 	panic("TODO: parseTypeEmbedName unexpected")
 }
 
-func preloadFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, gopFile, genFnBody bool) {
+func preloadFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, goFile string, genFnBody bool) {
 	parent := ctx.pkgCtx
 	syms := parent.syms
-	goFile := genGoFile(file, gopFile)
 	old, _ := p.SetCurFile(goFile, true)
 	defer p.RestoreCurFile(old)
 	var skipClassFields bool
@@ -926,7 +949,7 @@ func preloadFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, gopFil
 					pos := tName.Pos()
 					ld := getTypeLoader(parent, syms, pos, name)
 					defs := ctx.pkg.NewTypeDefs()
-					if gopFile {
+					if goFile != skippingGoFile { // is Go+ file
 						ld.typ = func() {
 							old, _ := p.SetCurFile(goFile, true)
 							defer p.RestoreCurFile(old)
@@ -1086,8 +1109,11 @@ func loadFunc(ctx *blockCtx, recv *types.Var, d *ast.FuncDecl, genBody bool) {
 	if genBody {
 		if body := d.Body; body != nil {
 			if recv != nil {
+				file := pkg.CurFile()
 				ctx.inits = append(ctx.inits, func() { // interface issue: #795
+					old := pkg.RestoreCurFile(file)
 					loadFuncBody(ctx, fn, body, d)
+					pkg.RestoreCurFile(old)
 				})
 			} else {
 				loadFuncBody(ctx, fn, body, d)
@@ -1167,13 +1193,13 @@ func loadImport(ctx *blockCtx, spec *ast.ImportSpec) {
 			}
 		}()
 	}
-	var pkg *gox.PkgRef
+	var pkg gox.PkgRef
 	var pkgPath = toString(spec.Path)
 	if realPath, kind := checkC2go(pkgPath); kind != c2goInvalid {
 		if kind == c2goStandard {
 			realPath = ctx.c2goBase + realPath
 		}
-		if pkg = loadC2goPkg(ctx, realPath, spec.Path); pkg == nil {
+		if pkg = loadC2goPkg(ctx, realPath, spec.Path); pkg.Types == nil {
 			return
 		}
 	} else {
@@ -1203,7 +1229,7 @@ func loadImport(ctx *blockCtx, spec *ast.ImportSpec) {
 			return
 		}
 		if name == "_" {
-			pkg.MarkForceUsed()
+			pkg.MarkForceUsed(ctx.pkg)
 			return
 		}
 	}
diff --git a/cl/expr.go b/cl/expr.go
index e014d1ce..ee2cddb3 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -74,7 +74,7 @@ const (
 
 const errorPkgPath = "github.com/qiniu/x/errors"
 
-func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg *gox.PkgRef, kind int) {
+func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg gox.PkgRef, kind int) {
 	fvalue := (flags&clIdentSelectorExpr) != 0 || (flags&clIdentLHS) == 0
 	name := ident.Name
 	if name == "_" {
@@ -122,7 +122,8 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg *gox.PkgRef,
 	// pkgRef object
 	if (flags & clIdentSelectorExpr) != 0 {
 		if name == "C" && len(ctx.clookups) > 0 {
-			return nil, objCPkgRef
+			kind = objCPkgRef
+			return
 		}
 		if pi, ok := ctx.findImport(name); ok {
 			if rec := ctx.recorder(); rec != nil {
@@ -133,7 +134,7 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg *gox.PkgRef,
 	}
 
 	// object from import . "xxx"
-	if compilePkgRef(ctx, nil, ident, flags, objPkgRef) {
+	if compilePkgRef(ctx, gox.PkgRef{}, ident, flags, objPkgRef) {
 		return
 	}
 
@@ -389,7 +390,7 @@ func compileSelectorExpr(ctx *blockCtx, v *ast.SelectorExpr, flags int) {
 	}
 }
 
-func pkgRef(at *gox.PkgRef, name string) (o types.Object, alias bool) {
+func pkgRef(at gox.PkgRef, name string) (o types.Object, alias bool) {
 	if c := name[0]; c >= 'a' && c <= 'z' {
 		name = string(rune(c)+('A'-'a')) + name[1:]
 		if v := at.TryRef(name); v != nil && gox.IsFunc(v.Type()) {
@@ -400,8 +401,9 @@ func pkgRef(at *gox.PkgRef, name string) (o types.Object, alias bool) {
 	return at.TryRef(name), false
 }
 
-func lookupPkgRef(ctx *blockCtx, pkg *gox.PkgRef, x *ast.Ident, pkgKind int) (o types.Object, alias bool) {
-	if pkg != nil {
+// allow pkg.Types to be nil
+func lookupPkgRef(ctx *blockCtx, pkg gox.PkgRef, x *ast.Ident, pkgKind int) (o types.Object, alias bool) {
+	if pkg.Types != nil {
 		return pkgRef(pkg, x.Name)
 	}
 	if pkgKind == objPkgRef {
@@ -416,7 +418,7 @@ func lookupPkgRef(ctx *blockCtx, pkg *gox.PkgRef, x *ast.Ident, pkgKind int) (o
 			}
 		}
 	} else {
-		var cpkg *cpackages.PkgRef
+		var cpkg cpackages.PkgRef
 		for _, at := range ctx.clookups {
 			if o2 := at.Lookup(x.Name); o2 != nil {
 				if o != nil {
@@ -431,7 +433,8 @@ func lookupPkgRef(ctx *blockCtx, pkg *gox.PkgRef, x *ast.Ident, pkgKind int) (o
 	return
 }
 
-func compilePkgRef(ctx *blockCtx, at *gox.PkgRef, x *ast.Ident, flags, pkgKind int) bool {
+// allow at.Types to be nil
+func compilePkgRef(ctx *blockCtx, at gox.PkgRef, x *ast.Ident, flags, pkgKind int) bool {
 	if v, alias := lookupPkgRef(ctx, at, x, pkgKind); v != nil {
 		cb := ctx.cb
 		if (flags & clIdentLHS) != 0 {
diff --git a/cl/func_type_and_var.go b/cl/func_type_and_var.go
index 98d5e42a..8141f011 100644
--- a/cl/func_type_and_var.go
+++ b/cl/func_type_and_var.go
@@ -251,7 +251,7 @@ func toIdentType(ctx *blockCtx, ident *ast.Ident) (ret types.Type) {
 		obj = t
 		return t.Type()
 	}
-	if v, _ := lookupPkgRef(ctx, nil, ident, objPkgRef); v != nil {
+	if v, _ := lookupPkgRef(ctx, gox.PkgRef{}, ident, objPkgRef); v != nil {
 		if t, ok := v.(*types.TypeName); ok {
 			obj = t
 			return t.Type()
diff --git a/go.mod b/go.mod
index 18e69c35..287469eb 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.18
 require (
 	github.com/fsnotify/fsnotify v1.7.0
 	github.com/goplus/c2go v0.7.20-0.20240115161159-762f74054b17
-	github.com/goplus/gox v1.13.1-0.20240122055409-d0400bc230e8
-	github.com/goplus/mod v0.12.2-0.20240122042636-7db361b76b12
+	github.com/goplus/gox v1.13.1-0.20240124090446-576b08a67368
+	github.com/goplus/mod v0.12.2-0.20240123033711-abba4c2e8b32
 	github.com/qiniu/x v1.13.2
 	golang.org/x/tools v0.17.0
 )
diff --git a/go.sum b/go.sum
index 804d365b..3d95753d 100644
--- a/go.sum
+++ b/go.sum
@@ -7,11 +7,11 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
 github.com/goplus/c2go v0.7.20-0.20240115161159-762f74054b17 h1:2+AWW9KjzTwaM8pfvRZqhI3PCXs7cMxzsn98AqDpgH0=
 github.com/goplus/c2go v0.7.20-0.20240115161159-762f74054b17/go.mod h1:WFbERV86IVV4wJ2/mLa8756Fy6iSXtlwGQbEqkYoIR4=
 github.com/goplus/gox v1.13.1-0.20240115155941-e657d899cb2e/go.mod h1:G7Hz+cAOUyJyN9pPHrpqhfQPDUtiJNmoRVTwoaQ9nw0=
-github.com/goplus/gox v1.13.1-0.20240122055409-d0400bc230e8 h1:k3ZTzEEU1ct6ljE1gqCg5LH2iyOs9W69lCS5COYS9As=
-github.com/goplus/gox v1.13.1-0.20240122055409-d0400bc230e8/go.mod h1:G7Hz+cAOUyJyN9pPHrpqhfQPDUtiJNmoRVTwoaQ9nw0=
+github.com/goplus/gox v1.13.1-0.20240124090446-576b08a67368 h1:16zrjGXKFES1T5EwfSAsIjSANprK2SHIYxKxwb5MlFY=
+github.com/goplus/gox v1.13.1-0.20240124090446-576b08a67368/go.mod h1:G7Hz+cAOUyJyN9pPHrpqhfQPDUtiJNmoRVTwoaQ9nw0=
 github.com/goplus/mod v0.12.2-0.20240107203906-5044606d0c51/go.mod h1:ZtlS9wHOcAVxZ/zq7WLdKVes1HG/8Yn3KNuWZGcpeTs=
-github.com/goplus/mod v0.12.2-0.20240122042636-7db361b76b12 h1:oQX3E8MWcXznTsQ6mzXYXGbniwyfKW4GtPl5vSdOr9k=
-github.com/goplus/mod v0.12.2-0.20240122042636-7db361b76b12/go.mod h1:ZtlS9wHOcAVxZ/zq7WLdKVes1HG/8Yn3KNuWZGcpeTs=
+github.com/goplus/mod v0.12.2-0.20240123033711-abba4c2e8b32 h1:9eOpqJKwb/V3uxfM/CZ0V9Wn/a9NtSmfx2VyXics2+I=
+github.com/goplus/mod v0.12.2-0.20240123033711-abba4c2e8b32/go.mod h1:ZtlS9wHOcAVxZ/zq7WLdKVes1HG/8Yn3KNuWZGcpeTs=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=


Test patch:
diff --git a/cl/builtin_test.go b/cl/builtin_test.go
index b1b3298e..9fa72894 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -53,6 +53,14 @@ func TestErrStringLit(t *testing.T) {
 	})
 }
 
+func TestGmxMainFunc(t *testing.T) {
+	gmxMainFunc(nil, &pkgCtx{
+		projs: map[string]*gmxProject{
+			".a": {}, ".b": {},
+		},
+	}, false)
+}
+
 func TestNodeInterp(t *testing.T) {
 	ni := &nodeInterp{}
 	if v := ni.Caller(&ast.Ident{}); v != "the function call" {
@@ -206,7 +214,7 @@ func TestClRangeStmt(t *testing.T) {
 // -----------------------------------------------------------------------------
 
 func TestGetStringConst(t *testing.T) {
-	spx := &gox.PkgRef{Types: types.NewPackage("", "foo")}
+	spx := gox.PkgRef{Types: types.NewPackage("", "foo")}
 	if v := getStringConst(spx, "unknown"); v != "" {
 		t.Fatal("getStringConst:", v)
 	}
@@ -218,7 +226,7 @@ func TestSpxRef(t *testing.T) {
 			t.Fatal("TestSpxRef:", e)
 		}
 	}()
-	pkg := &gox.PkgRef{
+	pkg := gox.PkgRef{
 		Types: types.NewPackage("foo", "foo"),
 	}
 	spxRef(pkg, "bar")
@@ -277,7 +285,7 @@ func TestGmxProject(t *testing.T) {
 	}()
 	func() {
 		defer func() {
-			if e := recover(); e != "TODO: multiple project files found" {
+			if e := recover(); e != "multiple project files found: Game Game\n" {
 				t.Fatal("TestGmxProject failed:", e)
 			}
 		}()
@@ -319,10 +327,13 @@ func lookupClassErr(ext string) (c *modfile.Project, ok bool) {
 }
 
 func TestGetGoFile(t *testing.T) {
-	if f := genGoFile("a_test.gop", true); f != testingGoFile {
+	if f := genGoFile("a_test.gop", false); f != testingGoFile {
+		t.Fatal("TestGetGoFile:", f)
+	}
+	if f := genGoFile("a_test.gox", true); f != testingGoFile {
 		t.Fatal("TestGetGoFile:", f)
 	}
-	if f := genGoFile("a_test.gop", false); f != skippingGoFile {
+	if f := genGoFile("a.gop", false); f != defaultGoFile {
 		t.Fatal("TestGetGoFile:", f)
 	}
 }
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index 8768d39e..03f289e4 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -51,6 +51,11 @@ func lookupClass(ext string) (c *modfile.Project, ok bool) {
 			Ext: "_spx.gox", Class: "Game",
 			Works:    []*modfile.Class{{Ext: "_spx.gox", Class: "Sprite"}},
 			PkgPaths: []string{"github.com/goplus/gop/cl/internal/spx3", "math"}}, true
+	case "_xtest.gox":
+		return &modfile.Project{
+			Ext: "_xtest.gox", Class: "App",
+			Works:    []*modfile.Class{{Ext: "_xtest.gox", Class: "Case"}},
+			PkgPaths: []string{"github.com/goplus/gop/test", "testing"}}, true
 	}
 	return
 }
@@ -59,12 +64,9 @@ func spxParserConf() parser.Config {
 	return parser.Config{
 		ClassKind: func(fname string) (isProj bool, ok bool) {
 			ext := modfile.ClassExt(fname)
-			if ext == "_spx.gox" {
-				return fname == "main_spx.gox", true
-			}
 			c, ok := lookupClass(ext)
 			if ok {
-				isProj = (c.Ext == ext)
+				isProj = c.IsProj(ext, fname)
 			}
 			return
 		},
@@ -76,10 +78,14 @@ func gopSpxTest(t *testing.T, gmx, spxcode, expected string) {
 }
 
 func gopSpxTestEx(t *testing.T, gmx, spxcode, expected, gmxfile, spxfile string) {
-	gopSpxTestExConf(t, "gopSpxTest", gblConf, gmx, spxcode, expected, gmxfile, spxfile)
+	gopSpxTestExConf(t, "gopSpxTest", gblConf, gmx, spxcode, expected, gmxfile, spxfile, "")
+}
+
+func gopSpxTestEx2(t *testing.T, gmx, spxcode, expected, gmxfile, spxfile, resultFile string) {
+	gopSpxTestExConf(t, "gopSpxTest", gblConf, gmx, spxcode, expected, gmxfile, spxfile, resultFile)
 }
 
-func gopSpxTestExConf(t *testing.T, name string, conf *cl.Config, gmx, spxcode, expected, gmxfile, spxfile string) {
+func gopSpxTestExConf(t *testing.T, name string, conf *cl.Config, gmx, spxcode, expected, gmxfile, spxfile, resultFile string) {
 	t.Run(name, func(t *testing.T) {
 		cl.SetDisableRecover(true)
 		defer cl.SetDisableRecover(false)
@@ -96,7 +102,7 @@ func gopSpxTestExConf(t *testing.T, name string, conf *cl.Config, gmx, spxcode,
 			t.Fatal("NewPackage:", err)
 		}
 		var b bytes.Buffer
-		err = pkg.WriteTo(&b)
+		err = pkg.WriteTo(&b, resultFile)
 		if err != nil {
 			t.Fatal("gox.WriteTo failed:", err)
 		}
@@ -531,7 +537,7 @@ func (this *Game) MainEntry() {
 func main() {
 	new(Game).Main()
 }
-`, "Game.t2gmx", "Kai.t2spx")
+`, "Game.t2gmx", "Kai.t2spx", "")
 	gopSpxTestExConf(t, "OnlyGmx", &conf, `
 var (
 	Kai Kai
@@ -555,7 +561,7 @@ func (this *Game) MainEntry() {
 func main() {
 	new(Game).Main()
 }
-`, "Game.t2gmx", "Kai.t2spx")
+`, "Game.t2gmx", "Kai.t2spx", "")
 
 	gopSpxTestExConf(t, "KaiAndGmx", &conf, `
 var (
@@ -597,7 +603,7 @@ func (this *Kai) Main() {
 }
 func (this *Kai) onMsg(msg string) {
 }
-`, "Game.t2gmx", "Kai.t2spx")
+`, "Game.t2gmx", "Kai.t2spx", "")
 }
 
 func TestSpxGoxBasic(t *testing.T) {
@@ -940,3 +946,72 @@ func (this *Kai) Main() {
 }
 `, "Game.tgmx", "Kai.tspx")
 }
+
+func TestTestClassFile(t *testing.T) {
+	gopSpxTestEx2(t, `
+println "Hi"
+`, `
+t.log "Hi"
+t.run "a test", t => {
+	t.fatal "failed"
+}
+`, `package main
+
+import (
+	"fmt"
+	"github.com/goplus/gop/test"
+	"testing"
+)
+
+type App struct {
+	test.App
+}
+
+func (this *App) MainEntry() {
+	fmt.Println("Hi")
+}
+
+type caseFoo struct {
+	test.Case
+	*App
+}
+
+func (this *caseFoo) Main() {
+	this.T().Log("Hi")
+	this.T().Run("a test", func(t *testing.T) {
+		t.Fatal("failed")
+	})
+}
+func TestFoo(t *testing.T) {
+	test.Gopt_Case_TestMain(new(caseFoo), t)
+}
+func TestMain(m *testing.M) {
+	test.Gopt_App_TestMain(new(App), m)
+}
+`, "main_xtest.gox", "Foo_xtest.gox", "_test")
+}
+
+func TestTestClassFile2(t *testing.T) {
+	gopSpxTestEx2(t, `
+println "Hi"
+`, `
+t.log "Hi"
+`, `package main
+
+import (
+	"github.com/goplus/gop/test"
+	"testing"
+)
+
+type case_foo struct {
+	test.Case
+}
+
+func (this *case_foo) Main() {
+	this.T().Log("Hi")
+}
+func Test_foo(t *testing.T) {
+	test.Gopt_Case_TestMain(new(case_foo), t)
+}
+`, "main.gox", "foo_xtest.gox", "_test")
+}
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 3e157d12..031ee2e0 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -193,9 +193,9 @@ for line <- open("foo.txt")! {
 
 import (
 	"fmt"
-	"os"
 	"github.com/goplus/gop/builtin/iox"
 	"github.com/qiniu/x/errors"
+	"os"
 )
 
 func main() {
@@ -230,8 +230,8 @@ for line <- os.Stdin {
 
 import (
 	"fmt"
-	"os"
 	"github.com/goplus/gop/builtin/iox"
+	"os"
 )
 
 func main() {
@@ -1502,9 +1502,9 @@ func foo(script string) {
 
 import (
 	"fmt"
+	"github.com/goplus/gop/ast/gopq"
 	"github.com/goplus/gop/ast/goptest"
 	"github.com/qiniu/x/errors"
-	"github.com/goplus/gop/ast/gopq"
 )
 
 func foo(script string) {
@@ -1536,9 +1536,9 @@ func foo(script string) {
 
 import (
 	"fmt"
+	"github.com/goplus/gop/ast/gopq"
 	"github.com/goplus/gop/ast/goptest"
 	"github.com/qiniu/x/errors"
-	"github.com/goplus/gop/ast/gopq"
 )
 
 func foo(script string) {
@@ -1567,8 +1567,8 @@ func add(x, y string) (int, error) {
 `, `package main
 
 import (
-	"strconv"
 	"github.com/qiniu/x/errors"
+	"strconv"
 )
 
 func add(x string, y string) (int, error) {
diff --git a/test/classfile.go b/test/classfile.go
new file mode 100644
index 00000000..8ca2593a
--- /dev/null
+++ b/test/classfile.go
@@ -0,0 +1,61 @@
+package test
+
+import (
+	"os"
+	"testing"
+)
+
+const (
+	GopPackage = true
+)
+
+// -----------------------------------------------------------------------------
+
+type testingT = testing.T
+
+// Case represents a Go+ testcase.
+type Case struct {
+	t *testingT
+}
+
+func (p *Case) initCase(t *testing.T) {
+	p.t = t
+}
+
+// T returns a *testing.T object.
+func (p Case) T() *testing.T { return p.t }
+
+// Run runs f as a subtest of t called name. It runs f in a separate goroutine
+// and blocks until f returns or calls t.Parallel to become a parallel test.
+// Run reports whether f succeeded (or at least did not fail before calling t.Parallel).
+func (p Case) Run(name string, f func(t *testing.T)) bool {
+	return p.t.Run(name, f)
+}
+
+// Gopt_Case_TestMain is required by Go+ compiler as the test case entry.
+func Gopt_Case_TestMain(c interface{ initCase(t *testing.T) }, t *testing.T) {
+	c.initCase(t)
+	c.(interface{ Main() }).Main()
+}
+
+// -----------------------------------------------------------------------------
+
+// App represents a Go+ testing main application.
+type App struct {
+	m *testing.M
+}
+
+func (p *App) initApp(m *testing.M) {
+	p.m = m
+}
+
+// Gopt_App_TestMain is required by Go+ compiler as the entry of a Go+ testing project.
+func Gopt_App_TestMain(app interface{ initApp(m *testing.M) }, m *testing.M) {
+	app.initApp(m)
+	if me, ok := app.(interface{ MainEntry() }); ok {
+		me.MainEntry()
+	}
+	os.Exit(m.Run())
+}
+
+// -----------------------------------------------------------------------------
diff --git a/testdata/unit-test/foo.gop b/testdata/unit-test/foo.gop
new file mode 100644
index 00000000..f954044e
--- /dev/null
+++ b/testdata/unit-test/foo.gop
@@ -0,0 +1,3 @@
+func foo() int {
+	return 100
+}
diff --git a/testdata/unit-test/foo_test.gox b/testdata/unit-test/foo_test.gox
new file mode 100644
index 00000000..6d3533f9
--- /dev/null
+++ b/testdata/unit-test/foo_test.gox
@@ -0,0 +1,5 @@
+if v := foo(); v == 100 {
+	t.log "test bar ok"
+} else {
+	t.error "bar() ret: ${v}"
+}
diff --git a/x/build/build_test.go b/x/build/build_test.go
index 82305e72..116776eb 100644
--- a/x/build/build_test.go
+++ b/x/build/build_test.go
@@ -183,8 +183,8 @@ println "Go+"
 `, `package main
 
 import (
-	"fmt"
 	"bytes"
+	"fmt"
 )
 
 type Rect struct {
@@ -208,8 +208,8 @@ println "Go+"
 `, `package main
 
 import (
-	"fmt"
 	"bytes"
+	"fmt"
 )
 
 type Rect struct {
@@ -300,8 +300,8 @@ println addSafe("10", "abc")
 
 import (
 	"fmt"
-	"strconv"
 	"github.com/qiniu/x/errors"
+	"strconv"
 )
 
 func add(x string, y string) (int, error) {