diff --git a/.github/ci-config/codecov.yml b/.github/ci-config/codecov.yml deleted file mode 100644 index 347eee08..00000000 --- a/.github/ci-config/codecov.yml +++ /dev/null @@ -1,8 +0,0 @@ -coverage: - ignore: - - "compiler/chore" - - "chore" - - "py" - - "x" - - "cpp" - - "runtime" diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 00000000..5da4fe08 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,3 @@ +coverage: + ignore: + - "compiler/chore" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 2d458e1c..a26d87b7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -54,5 +54,3 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{secrets.CODECOV_TOKEN}} - slug: goplus/llgo - codecov_yml_path: .github/ci-config/codecov.yml diff --git a/compiler/chore/llgen/llgen_test.go b/compiler/chore/llgen/llgen_test.go new file mode 100644 index 00000000..c24bdb70 --- /dev/null +++ b/compiler/chore/llgen/llgen_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestMain(t *testing.T) { + // Create test package in current module + testPkg := filepath.Join(".testdata_dont_commit", "hello") + err := os.MkdirAll(testPkg, 0755) + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(filepath.Join(".testdata_dont_commit")) + + helloFile := filepath.Join(testPkg, "hello.go") + err = os.WriteFile(helloFile, []byte(`package hello + +func Hello() string { + return "Hello, World!" +} +`), 0644) + if err != nil { + t.Fatal(err) + } + + // Save original args and restore them after test + oldArgs := os.Args + defer func() { os.Args = oldArgs }() + + // Get absolute path to test package + absTestPkg, err := filepath.Abs(testPkg) + if err != nil { + t.Fatal(err) + } + + // Set test arguments + os.Args = []string{"llgen", absTestPkg} + + // Run main + main() + + // Check if the output file exists + outputFile := filepath.Join(testPkg, "llgo_autogen.ll") + log.Printf("Generated file: %s", filepath.Join(absTestPkg, "llgo_autogen.ll")) + if _, err = os.Stat(outputFile); err != nil { + t.Fatalf("Generated file should exist: %v", err) + } + + // Read and verify file content + content, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("Should be able to read generated file: %v", err) + } + if !strings.Contains(string(content), "define") { + t.Error("Generated file should contain LLVM IR code") + } +} diff --git a/compiler/cl/compile_test.go b/compiler/cl/compile_test.go index 5922f66f..7132ad05 100644 --- a/compiler/cl/compile_test.go +++ b/compiler/cl/compile_test.go @@ -17,6 +17,9 @@ package cl_test import ( + "io" + "log" + "os" "testing" "github.com/goplus/llgo/compiler/cl" @@ -24,6 +27,13 @@ import ( "github.com/goplus/llgo/compiler/internal/build" ) +func init() { + devNull, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + os.Stderr = devNull + os.Stdout = devNull + log.SetOutput(io.Discard) +} + func testCompile(t *testing.T, src, expected string) { t.Helper() cltest.TestCompileEx(t, src, "foo.go", expected, false) diff --git a/compiler/cmd/internal/build/build.go b/compiler/cmd/internal/build/build.go index fb704c51..9ff594b7 100644 --- a/compiler/cmd/internal/build/build.go +++ b/compiler/cmd/internal/build/build.go @@ -23,6 +23,7 @@ import ( "github.com/goplus/llgo/compiler/cmd/internal/base" "github.com/goplus/llgo/compiler/internal/build" + "github.com/goplus/llgo/compiler/internal/mockable" ) // llgo build @@ -47,6 +48,6 @@ func runCmd(cmd *base.Command, args []string) { _, err := build.Do(args, conf) if err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + mockable.Exit(1) } } diff --git a/compiler/cmd/internal/help/help.go b/compiler/cmd/internal/help/help.go index 16c72298..f78de6ed 100644 --- a/compiler/cmd/internal/help/help.go +++ b/compiler/cmd/internal/help/help.go @@ -29,6 +29,7 @@ import ( "unicode/utf8" "github.com/goplus/llgo/compiler/cmd/internal/base" + "github.com/goplus/llgo/compiler/internal/mockable" ) // Help implements the 'help' command. @@ -49,7 +50,7 @@ Args: helpSuccess += " " + strings.Join(args[:i], " ") } fmt.Fprintf(os.Stderr, "llgo help %s: unknown help topic. Run '%s'.\n", strings.Join(args, " "), helpSuccess) - os.Exit(2) + mockable.Exit(2) } if len(cmd.Commands) > 0 { @@ -98,7 +99,7 @@ func tmpl(w io.Writer, text string, data interface{}) { if ew.err != nil { // I/O error writing. Ignore write on closed pipe. if strings.Contains(ew.err.Error(), "pipe") { - os.Exit(1) + mockable.Exit(1) } log.Fatalf("writing output: %v", ew.err) } diff --git a/compiler/cmd/internal/install/install.go b/compiler/cmd/internal/install/install.go index f917ed42..621919b9 100644 --- a/compiler/cmd/internal/install/install.go +++ b/compiler/cmd/internal/install/install.go @@ -23,6 +23,7 @@ import ( "github.com/goplus/llgo/compiler/cmd/internal/base" "github.com/goplus/llgo/compiler/internal/build" + "github.com/goplus/llgo/compiler/internal/mockable" ) // llgo install @@ -40,6 +41,6 @@ func runCmd(cmd *base.Command, args []string) { _, err := build.Do(args, conf) if err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + mockable.Exit(1) } } diff --git a/compiler/cmd/internal/run/run.go b/compiler/cmd/internal/run/run.go index cb37f73e..101dcf26 100644 --- a/compiler/cmd/internal/run/run.go +++ b/compiler/cmd/internal/run/run.go @@ -25,6 +25,7 @@ import ( "github.com/goplus/llgo/compiler/cmd/internal/base" "github.com/goplus/llgo/compiler/internal/build" + "github.com/goplus/llgo/compiler/internal/mockable" ) var ( @@ -68,7 +69,7 @@ func runCmdEx(_ *base.Command, args []string, mode build.Mode) { _, err = build.Do(args, conf) if err != nil { fmt.Fprintln(os.Stderr, err) - os.Exit(1) + mockable.Exit(1) } } diff --git a/compiler/cmd/internal/version/version.go b/compiler/cmd/internal/version/version.go index 9caf7cb2..75bba7a1 100644 --- a/compiler/cmd/internal/version/version.go +++ b/compiler/cmd/internal/version/version.go @@ -21,7 +21,7 @@ import ( "runtime" "github.com/goplus/llgo/compiler/cmd/internal/base" - "github.com/goplus/llgo/x/env" + "github.com/goplus/llgo/compiler/internal/env" ) // llgo version diff --git a/compiler/cmd/llgo/llgo.go b/compiler/cmd/llgo/llgo.go index 5ac3b026..77cf35a8 100644 --- a/compiler/cmd/llgo/llgo.go +++ b/compiler/cmd/llgo/llgo.go @@ -32,6 +32,7 @@ import ( "github.com/goplus/llgo/compiler/cmd/internal/install" "github.com/goplus/llgo/compiler/cmd/internal/run" "github.com/goplus/llgo/compiler/cmd/internal/version" + "github.com/goplus/llgo/compiler/internal/mockable" ) func mainUsage() { @@ -77,7 +78,7 @@ BigCmdLoop: bigCmd = cmd if len(args) == 0 { help.PrintUsage(os.Stderr, bigCmd) - os.Exit(2) + mockable.Exit(2) } if args[0] == "help" { help.Help(os.Stderr, append(strings.Split(base.CmdName, " "), args[1:]...)) @@ -97,6 +98,6 @@ BigCmdLoop: helpArg = " " + base.CmdName[:i] } fmt.Fprintf(os.Stderr, "llgo %s: unknown command\nRun 'llgo help%s' for usage.\n", base.CmdName, helpArg) - os.Exit(2) + mockable.Exit(2) } } diff --git a/compiler/cmd/llgo/llgo_test.go b/compiler/cmd/llgo/llgo_test.go index fbdf21de..ed4e9585 100644 --- a/compiler/cmd/llgo/llgo_test.go +++ b/compiler/cmd/llgo/llgo_test.go @@ -1,52 +1,76 @@ package main import ( - "bytes" - "flag" - "io" + "fmt" "os" "path/filepath" "strings" "testing" - "github.com/goplus/llgo/compiler/cmd/internal/base" - "github.com/goplus/llgo/compiler/cmd/internal/build" - "github.com/goplus/llgo/compiler/cmd/internal/help" - "github.com/goplus/llgo/compiler/cmd/internal/install" - "github.com/goplus/llgo/compiler/cmd/internal/run" - "github.com/goplus/llgo/compiler/cmd/internal/version" + "github.com/goplus/llgo/compiler/internal/mockable" ) -func setupTestProject(t *testing.T) string { - // Create a temporary directory for the test project - tmpDir, err := os.MkdirTemp("", "llgo-test-*") +func init() { + origWd, err := os.Getwd() if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) + panic(err) } - // Create a simple Go program - mainFile := filepath.Join(tmpDir, "main.go") - err = os.WriteFile(mainFile, []byte(`package main + // Set LLGO_ROOT to project root + llgoRoot := filepath.Join(origWd, "../../..") + if err := os.Setenv("LLGO_ROOT", llgoRoot); err != nil { + panic(fmt.Sprintf("Failed to set LLGO_ROOT: %v", err)) + } +} + +func setupTestProject(t *testing.T) string { + tmpDir := t.TempDir() + + // Create main.go + mainGo := filepath.Join(tmpDir, "main.go") + err := os.WriteFile(mainGo, []byte(`package main import "fmt" +import "os" func main() { - fmt.Println("Hello, LLGO!") + var arg string = "LLGO" + if len(os.Args) > 1 { + arg = os.Args[1] + } + switch arg { + case "stderr": + fmt.Fprintln(os.Stderr, "Hello, World!") + case "exit": + os.Exit(1) + default: + fmt.Println("Hello, " + arg + "!") + } } `), 0644) if err != nil { - os.RemoveAll(tmpDir) - t.Fatalf("Failed to write main.go: %v", err) + t.Fatalf("Failed to create main.go: %v", err) + } + + // Create llgo.expect for cmptest + expectFile := filepath.Join(tmpDir, "llgo.expect") + err = os.WriteFile(expectFile, []byte(`#stdout +Hello, LLGO! + +#stderr + +#exit 0 +`), 0644) + if err != nil { + t.Fatalf("Failed to create llgo.expect: %v", err) } // Create a go.mod file - goMod := filepath.Join(tmpDir, "go.mod") - err = os.WriteFile(goMod, []byte(`module testproject + err = os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(`module testproject go 1.20 `), 0644) if err != nil { - os.RemoveAll(tmpDir) t.Fatalf("Failed to write go.mod: %v", err) } @@ -54,168 +78,195 @@ go 1.20 } func TestProjectCommands(t *testing.T) { - // Setup test project - tmpDir := setupTestProject(t) - defer os.RemoveAll(tmpDir) - - // Save original working directory and environment - origWd, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get current directory: %v", err) - } - defer os.Chdir(origWd) - - // Change to test project directory - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change to test directory: %v", err) - } - tests := []struct { name string - cmd *base.Command args []string wantErr bool + setup func(dir string) error }{ { name: "build command", - cmd: build.Cmd, - args: []string{"."}, + args: []string{"llgo", "build", "."}, wantErr: false, }, { name: "install command", - cmd: install.Cmd, - args: []string{"."}, + args: []string{"llgo", "install", "."}, wantErr: false, }, { name: "run command", - cmd: run.Cmd, - args: []string{"main.go"}, + args: []string{"llgo", "run", "."}, wantErr: false, }, + { + name: "run command with file", + args: []string{"llgo", "run", "main.go"}, + wantErr: false, + }, + { + name: "run command verbose", + args: []string{"llgo", "run", "-v", "."}, + wantErr: false, + }, + { + name: "clean command", + args: []string{"llgo", "clean"}, + wantErr: false, + }, + { + name: "cmptest command", + args: []string{"llgo", "cmptest", "."}, + wantErr: false, + }, + { + name: "cmptest command with gen", + args: []string{"llgo", "cmptest", "-gen", "."}, + wantErr: false, + setup: func(dir string) error { + return os.Remove(filepath.Join(dir, "llgo.expect")) + }, + }, + { + name: "cmptest command with args", + args: []string{"llgo", "cmptest", ".", "World"}, + wantErr: true, + setup: func(dir string) error { + return os.WriteFile(filepath.Join(dir, "llgo.expect"), []byte(`#stdout +Hello, World! + +#stderr + +#exit 0 +`), 0644) + }, + }, + { + name: "cmptest command with different stderr", + args: []string{"llgo", "cmptest", ".", "stderr"}, + wantErr: true, + }, + { + name: "cmptest command with different exit code", + args: []string{"llgo", "cmptest", ".", "exit"}, + wantErr: true, + setup: func(dir string) error { + // Create llgo.expect with different exit code + return os.WriteFile(filepath.Join(dir, "llgo.expect"), []byte(`#stdout +Hello, LLGO! + +#stderr + +#exit 1 +`), 0644) + }, + }, + { + name: "cmptest command without llgo.expect to compare with go run", + args: []string{"llgo", "cmptest", "."}, + wantErr: false, + setup: func(dir string) error { + return os.Remove(filepath.Join(dir, "llgo.expect")) + }, + }, + { + name: "cmptest command with different go run output", + args: []string{"llgo", "cmptest", "."}, + wantErr: true, + setup: func(dir string) error { + // Remove llgo.expect file + if err := os.Remove(filepath.Join(dir, "llgo.expect")); err != nil && !os.IsNotExist(err) { + return err + } + + // Create main_llgo.go for llgo + if err := os.WriteFile(filepath.Join(dir, "main_llgo.go"), []byte(`//go:build llgo +// +build llgo + +package main + +import "fmt" + +func main() { + fmt.Println("Hello, LLGO!") +} +`), 0644); err != nil { + return err + } + + // Create main_go.go for go + return os.WriteFile(filepath.Join(dir, "main.go"), []byte(`//go:build !llgo +// +build !llgo + +package main + +import "fmt" + +func main() { + fmt.Println("Hello, Go!") +} +`), 0644) + }, + }, } - // Save original args and flags - oldArgs := os.Args - oldFlagCommandLine := flag.CommandLine - oldStdout := os.Stdout - oldStderr := os.Stderr - defer func() { - os.Args = oldArgs - flag.CommandLine = oldFlagCommandLine - os.Stdout = oldStdout - os.Stderr = oldStderr - }() - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Reset flag.CommandLine for each test - flag.CommandLine = flag.NewFlagSet("llgo", flag.ContinueOnError) + // Create a new test directory for each test case + tmpDir := setupTestProject(t) + defer os.RemoveAll(tmpDir) - // Setup command arguments - args := append([]string{"llgo", tt.cmd.Name()}, tt.args...) - os.Args = args - - // Capture output - outR, outW, err := os.Pipe() - if err != nil { - t.Fatalf("Failed to create stdout pipe: %v", err) - } - errR, errW, err := os.Pipe() - if err != nil { - t.Fatalf("Failed to create stderr pipe: %v", err) + // Change to test project directory + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change directory: %v", err) } - os.Stdout = outW - os.Stderr = errW - - // Run command - done := make(chan struct{}) - var outBuf, errBuf bytes.Buffer - go func() { - _, _ = io.Copy(&outBuf, outR) - done <- struct{}{} - }() - go func() { - _, _ = io.Copy(&errBuf, errR) - done <- struct{}{} - }() - - panicked := false - func() { - defer func() { - if r := recover(); r != nil { - panicked = true - t.Logf("%s: Command panicked: %v", tt.name, r) - } - outW.Close() - errW.Close() - }() - - flag.Parse() - base.CmdName = tt.cmd.Name() - - if !tt.cmd.Runnable() { - t.Fatalf("%s: Command is not runnable", tt.name) + if tt.setup != nil { + if err := tt.setup(tmpDir); err != nil { + t.Fatalf("Failed to setup test: %v", err) } + } - // Print current working directory and files for debugging - if cwd, err := os.Getwd(); err == nil { - t.Logf("%s: Current working directory: %s", tt.name, cwd) - if files, err := os.ReadDir("."); err == nil { - t.Log("Files in current directory:") - for _, f := range files { - t.Logf(" %s", f.Name()) + mockable.EnableMock() + defer func() { + if r := recover(); r != nil { + if r != "exit" { + if !tt.wantErr { + t.Errorf("unexpected panic: %v", r) + } + } else { + exitCode := mockable.ExitCode() + if (exitCode != 0) != tt.wantErr { + t.Errorf("got exit code %d, wantErr %v", exitCode, tt.wantErr) } } } - - // Run the command - tt.cmd.Run(tt.cmd, tt.args) }() - <-done - <-done + os.Args = tt.args + main() - // Check output - outStr := outBuf.String() - errStr := errBuf.String() - - if outStr == "" && errStr == "" && !panicked { - t.Logf("%s: Command completed with no output", tt.name) - } else { - if outStr != "" { - t.Logf("%s stdout:\n%s", tt.name, outStr) - } - if errStr != "" { - t.Logf("%s stderr:\n%s", tt.name, errStr) - } - } - - // Check if the command succeeded - if !tt.wantErr { - // For build/install commands, check if binary was created - if tt.cmd == build.Cmd || tt.cmd == install.Cmd { - binName := "testproject" - if _, err := os.Stat(binName); os.IsNotExist(err) { - t.Logf("%s: Binary %s was not created", tt.name, binName) + // For build/install commands, check if binary was created + if strings.HasPrefix(tt.name, "build") || strings.HasPrefix(tt.name, "install") { + binName := "testproject" + var binPath string + if strings.HasPrefix(tt.name, "install") { + // For install command, binary should be in GOBIN or GOPATH/bin + gobin := os.Getenv("GOBIN") + if gobin == "" { + gopath := os.Getenv("GOPATH") + if gopath == "" { + gopath = filepath.Join(os.Getenv("HOME"), "go") + } + gobin = filepath.Join(gopath, "bin") } + binPath = filepath.Join(gobin, binName) + } else { + // For build command, binary should be in current directory + binPath = filepath.Join(tmpDir, binName) } - - // For run command, check if output contains expected string - if tt.cmd == run.Cmd { - if !strings.Contains(outStr, "Hello, LLGO!") { - t.Logf("%s: Expected output to contain 'Hello, LLGO!', got:\n%s", tt.name, outStr) - } - } - - // Check for common error indicators, but don't fail the test - if strings.Contains(errStr, "error:") || strings.Contains(errStr, "failed") { - // Ignore LLVM reexported library warning - if !strings.Contains(errStr, "ld: warning: reexported library") { - t.Logf("%s: Command produced error output:\n%s", tt.name, errStr) - } + if _, err := os.Stat(binPath); os.IsNotExist(err) { + t.Errorf("Binary %s was not created at %s", binName, binPath) } } }) @@ -223,119 +274,95 @@ func TestProjectCommands(t *testing.T) { } func TestCommandHandling(t *testing.T) { - // Save original args and flags - oldArgs := os.Args - oldFlagCommandLine := flag.CommandLine - defer func() { - os.Args = oldArgs - flag.CommandLine = oldFlagCommandLine - }() - - tests := []struct { - name string - args []string - wantErr bool - commands []*base.Command - }{ - { - name: "version command", - args: []string{"llgo", "version"}, - wantErr: false, - commands: []*base.Command{ - version.Cmd, - }, - }, - { - name: "build command", - args: []string{"llgo", "build"}, - wantErr: false, - commands: []*base.Command{ - build.Cmd, - }, - }, - { - name: "unknown command", - args: []string{"llgo", "unknowncommand"}, - wantErr: true, - }, - { - name: "help command", - args: []string{"llgo", "help"}, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Reset flag.CommandLine for each test - flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError) - os.Args = tt.args - - if tt.commands != nil { - base.Llgo.Commands = tt.commands - } - - // Capture panic that would normally exit - defer func() { - if r := recover(); r != nil { - if !tt.wantErr { - t.Errorf("unexpected panic: %v", r) - } - } - }() - - flag.Parse() - if len(tt.args) > 1 { - base.CmdName = tt.args[1] - } - }) - } -} - -func TestHelpCommand(t *testing.T) { - oldArgs := os.Args - oldFlagCommandLine := flag.CommandLine - defer func() { - os.Args = oldArgs - flag.CommandLine = oldFlagCommandLine - }() - tests := []struct { name string args []string wantErr bool }{ { - name: "help without subcommand", + name: "version command", + args: []string{"llgo", "version"}, + wantErr: false, + }, + { + name: "help command", args: []string{"llgo", "help"}, wantErr: false, }, { - name: "help with subcommand", - args: []string{"llgo", "help", "build"}, - wantErr: false, + name: "invalid command", + args: []string{"llgo", "invalid"}, + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError) - os.Args = tt.args - - var buf bytes.Buffer defer func() { if r := recover(); r != nil { - if !tt.wantErr { + if r != "exit" { t.Errorf("unexpected panic: %v", r) } + exitCode := mockable.ExitCode() + if (exitCode != 0) != tt.wantErr { + t.Errorf("got exit code %d, wantErr %v", exitCode, tt.wantErr) + } } }() - flag.Parse() - args := flag.Args() - if len(args) > 0 && args[0] == "help" { - help.Help(&buf, args[1:]) - } + os.Args = tt.args + main() + }) + } +} + +func TestHelpCommand(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "help build", + args: []string{"llgo", "help", "build"}, + }, + { + name: "help install", + args: []string{"llgo", "help", "install"}, + }, + { + name: "help run", + args: []string{"llgo", "help", "run"}, + }, + { + name: "help version", + args: []string{"llgo", "help", "version"}, + }, + { + name: "help clean", + args: []string{"llgo", "help", "clean"}, + }, + { + name: "help cmptest", + args: []string{"llgo", "help", "cmptest"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if r != "exit" { + t.Errorf("unexpected panic: %v", r) + } + exitCode := mockable.ExitCode() + if exitCode != 0 { + t.Errorf("got exit code %d, want 0", exitCode) + } + } + }() + + os.Args = tt.args + main() }) } } diff --git a/compiler/internal/build/build.go b/compiler/internal/build/build.go index 6daaacf6..f37455a8 100644 --- a/compiler/internal/build/build.go +++ b/compiler/internal/build/build.go @@ -38,6 +38,7 @@ import ( "github.com/goplus/llgo/compiler/cl" "github.com/goplus/llgo/compiler/internal/env" + "github.com/goplus/llgo/compiler/internal/mockable" "github.com/goplus/llgo/compiler/internal/packages" "github.com/goplus/llgo/compiler/internal/typepatch" "github.com/goplus/llgo/compiler/ssa/abi" @@ -221,15 +222,11 @@ func Do(args []string, conf *Config) ([]Package, error) { } if mode != ModeBuild { - nErr := 0 for _, pkg := range initial { if pkg.Name == "main" { - nErr += linkMainPkg(ctx, pkg, pkgs, linkArgs, conf, mode, verbose) + linkMainPkg(ctx, pkg, pkgs, linkArgs, conf, mode, verbose) } } - if nErr > 0 { - os.Exit(nErr) - } } return dpkg, nil } @@ -290,7 +287,7 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs fmt.Fprintln(os.Stderr, "cannot build SSA for package", errPkg) } if len(errPkgs) > 0 { - os.Exit(1) + mockable.Exit(1) } built := ctx.built for _, aPkg := range pkgs { @@ -372,7 +369,7 @@ func buildAllPkgs(ctx *context, initial []*packages.Package, verbose bool) (pkgs return } -func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs []string, conf *Config, mode Mode, verbose bool) (nErr int) { +func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs []string, conf *Config, mode Mode, verbose bool) { pkgPath := pkg.PkgPath name := path.Base(pkgPath) app := conf.OutFile @@ -458,11 +455,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs if verbose || mode != ModeRun { fmt.Fprintln(os.Stderr, "#", pkgPath) } - defer func() { - if e := recover(); e != nil { - nErr = 1 - } - }() // add rpath and find libs exargs := make([]string, 0, ctx.nLibdir<<1) @@ -506,12 +498,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, linkArgs cmd.Stderr = os.Stderr cmd.Run() if s := cmd.ProcessState; s != nil { - os.Exit(s.ExitCode()) + mockable.Exit(s.ExitCode()) } case ModeCmpTest: cmpTest(filepath.Dir(pkg.GoFiles[0]), pkgPath, app, conf.GenExpect, conf.RunArgs) } - return } func buildPkg(ctx *context, aPkg *aPackage, verbose bool) (cgoLdflags []string, err error) { diff --git a/x/env/build.go b/compiler/internal/env/build.go similarity index 100% rename from x/env/build.go rename to compiler/internal/env/build.go diff --git a/compiler/internal/env/env.go b/compiler/internal/env/env.go index 6eef99be..09246f60 100644 --- a/compiler/internal/env/env.go +++ b/compiler/internal/env/env.go @@ -2,9 +2,11 @@ package env import ( "bytes" + "fmt" "os" "os/exec" "path/filepath" + "runtime" "strings" ) @@ -12,6 +14,7 @@ const ( LLGoCompilerPkg = "github.com/goplus/llgo" LLGoRuntimePkgName = "runtime" LLGoRuntimePkg = LLGoCompilerPkg + "/" + LLGoRuntimePkgName + envFileName = "/compiler/internal/env/env.go" ) func GOROOT() string { @@ -38,8 +41,12 @@ func LLGoRuntimeDir() string { } func LLGoROOT() string { - if root, ok := isLLGoRoot(os.Getenv("LLGO_ROOT")); ok { - return root + llgoRootEnv := os.Getenv("LLGO_ROOT") + if llgoRootEnv != "" { + if root, ok := isLLGoRoot(llgoRootEnv); ok { + return root + } + fmt.Fprintf(os.Stderr, "WARNING: LLGO_ROOT is not a valid LLGO root: %s\n", llgoRootEnv) } // Get executable path exe, err := os.Executable() @@ -53,13 +60,22 @@ func LLGoROOT() string { } // Check if parent directory is bin dir := filepath.Dir(exe) - if filepath.Base(dir) != "bin" { - return "" + if filepath.Base(dir) == "bin" { + // Get parent directory of bin + root := filepath.Dir(dir) + if root, ok := isLLGoRoot(root); ok { + return root + } } - // Get parent directory of bin - root := filepath.Dir(dir) - if root, ok := isLLGoRoot(root); ok { - return root + if Devel() { + root, err := getRuntimePkgDirByCaller() + if err != nil { + return "" + } + if root, ok := isLLGoRoot(root); ok { + fmt.Fprintln(os.Stderr, "WARNING: Using LLGO root for devel: "+root) + return root + } } return "" } @@ -83,3 +99,22 @@ func isLLGoRoot(root string) (string, bool) { } return root, true } + +func getRuntimePkgDirByCaller() (string, error) { + _, file, _, ok := runtime.Caller(0) + if !ok { + return "", fmt.Errorf("cannot get caller") + } + if !strings.HasSuffix(file, envFileName) { + return "", fmt.Errorf("wrong caller") + } + // check file exists + if _, err := os.Stat(file); os.IsNotExist(err) { + return "", fmt.Errorf("file %s not exists", file) + } + modPath := strings.TrimSuffix(file, envFileName) + if st, err := os.Stat(modPath); os.IsNotExist(err) || !st.IsDir() { + return "", fmt.Errorf("not llgo compiler root: %s", modPath) + } + return modPath, nil +} diff --git a/compiler/internal/env/env_test.go b/compiler/internal/env/env_test.go index 14a49808..823edeeb 100644 --- a/compiler/internal/env/env_test.go +++ b/compiler/internal/env/env_test.go @@ -58,8 +58,28 @@ func TestLLGoRuntimeDir(t *testing.T) { defer os.Setenv("LLGO_ROOT", origLLGoRoot) os.Setenv("LLGO_ROOT", "/nonexistent/path") - if got := LLGoRuntimeDir(); got != "" { - t.Errorf("LLGoRuntimeDir() = %v, want empty string", got) + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + runtimeDir := filepath.Join(wd, "../../../runtime") + if got := LLGoRuntimeDir(); got != runtimeDir { + t.Errorf("LLGoRuntimeDir() = %v, want %v", got, runtimeDir) + } + }) + + t.Run("devel runtime dir", func(t *testing.T) { + origLLGoRoot := os.Getenv("LLGO_ROOT") + defer os.Setenv("LLGO_ROOT", origLLGoRoot) + + os.Setenv("LLGO_ROOT", "") + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + runtimeDir := filepath.Join(wd, "../../../runtime") + if got := LLGoRuntimeDir(); got != runtimeDir { + t.Errorf("LLGoRuntimeDir() = %v, want %v", got, runtimeDir) } }) } @@ -90,8 +110,13 @@ func TestLLGoROOT(t *testing.T) { defer os.Setenv("LLGO_ROOT", origLLGoRoot) os.Setenv("LLGO_ROOT", "/nonexistent/path") - if got := LLGoROOT(); got != "" { - t.Errorf("LLGoROOT() = %v, want empty string", got) + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + rootDir := filepath.Join(wd, "../../..") + if got := LLGoROOT(); got != rootDir { + t.Errorf("LLGoROOT() = %v, want %v", got, rootDir) } }) diff --git a/x/env/version.go b/compiler/internal/env/version.go similarity index 84% rename from x/env/version.go rename to compiler/internal/env/version.go index 975d4fef..362e3740 100644 --- a/x/env/version.go +++ b/compiler/internal/env/version.go @@ -16,20 +16,32 @@ package env -import "runtime/debug" +import ( + "runtime/debug" +) + +const ( + devel = "(devel)" +) // buildVersion is the LLGo tree's version string at build time. It should be // set by the linker. var buildVersion string // Version returns the version of the running LLGo binary. +// +//export LLGoVersion func Version() string { if buildVersion != "" { return buildVersion } info, ok := debug.ReadBuildInfo() - if ok { + if ok && info.Main.Version != "" { return info.Main.Version } - return "(devel)" + return devel +} + +func Devel() bool { + return Version() == devel } diff --git a/compiler/internal/mockable/mockable.go b/compiler/internal/mockable/mockable.go new file mode 100644 index 00000000..e9ca7fe3 --- /dev/null +++ b/compiler/internal/mockable/mockable.go @@ -0,0 +1,29 @@ +package mockable + +import ( + "os" +) + +var ( + exitFunc = os.Exit + exitCode int +) + +// EnableMock enables mocking of os.Exit +func EnableMock() { + exitCode = 0 + exitFunc = func(code int) { + exitCode = code + panic("exit") + } +} + +// Exit calls the current exit function +func Exit(code int) { + exitFunc(code) +} + +// ExitCode returns the last exit code from a mocked Exit call +func ExitCode() int { + return exitCode +} diff --git a/compiler/ssa/cl_test.go b/compiler/ssa/cl_test.go index 91d229f6..0fe49ce3 100644 --- a/compiler/ssa/cl_test.go +++ b/compiler/ssa/cl_test.go @@ -18,6 +18,9 @@ package ssa_test import ( "go/types" + "io" + "log" + "os" "testing" "github.com/goplus/llgo/compiler/cl/cltest" @@ -26,6 +29,10 @@ import ( ) func init() { + devNull, _ := os.OpenFile(os.DevNull, os.O_WRONLY, 0) + os.Stderr = devNull + os.Stdout = devNull + log.SetOutput(io.Discard) ssa.SetDebug(ssa.DbgFlagAll) }