From 25a3e19384e888e9d50c39b629662f914467672a Mon Sep 17 00:00:00 2001 From: Li Jie Date: Mon, 13 Jan 2025 10:53:43 +0800 Subject: [PATCH 1/9] ci: update codecov ignore config --- .github/ci-config/codecov.yml | 8 -------- .github/codecov.yml | 4 ++++ .github/workflows/go.yml | 2 -- 3 files changed, 4 insertions(+), 10 deletions(-) delete mode 100644 .github/ci-config/codecov.yml create mode 100644 .github/codecov.yml 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..d7686ea0 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,4 @@ +coverage: + ignore: + - "compiler/chore" + - "compiler/cmd" 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 From 8749923f1ab450bf36507726fd19e6271c4d2554 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jan 2025 10:50:43 +0800 Subject: [PATCH 2/9] test: make cmd testable --- .github/codecov.yml | 1 - compiler/cmd/internal/build/build.go | 3 +- compiler/cmd/internal/help/help.go | 5 +- compiler/cmd/internal/install/install.go | 3 +- compiler/cmd/internal/run/run.go | 3 +- compiler/cmd/llgo/llgo.go | 5 +- compiler/cmd/llgo/llgo_test.go | 367 +++++++++-------------- compiler/internal/build/build.go | 19 +- compiler/internal/mockable/mockable.go | 29 ++ 9 files changed, 186 insertions(+), 249 deletions(-) create mode 100644 compiler/internal/mockable/mockable.go diff --git a/.github/codecov.yml b/.github/codecov.yml index d7686ea0..5da4fe08 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,4 +1,3 @@ coverage: ignore: - "compiler/chore" - - "compiler/cmd" 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/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..4863f88b 100644 --- a/compiler/cmd/llgo/llgo_test.go +++ b/compiler/cmd/llgo/llgo_test.go @@ -1,22 +1,62 @@ package main import ( - "bytes" - "flag" - "io" "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" ) +var origWd string + +func init() { + var err error + origWd, err = os.Getwd() + if err != nil { + panic(err) + } +} + +type testContext struct { + origLLGORoot string + tmpDir string +} + +func setupTest(t *testing.T) *testContext { + ctx := &testContext{} + + // Save original state + ctx.origLLGORoot = os.Getenv("LLGO_ROOT") + + // Create temporary LLGO_ROOT + tmpDir, err := os.MkdirTemp("", "llgo-root-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + ctx.tmpDir = tmpDir + + // Set LLGO_ROOT + llgoRoot := filepath.Join(origWd, "../../..") + if err := os.Setenv("LLGO_ROOT", llgoRoot); err != nil { + os.RemoveAll(tmpDir) + t.Fatalf("Failed to set LLGO_ROOT: %v", err) + } + t.Logf("LLGO_ROOT set to: %s", llgoRoot) + mockable.EnableMock() + + return ctx +} + +func teardownTest(ctx *testContext) { + os.Chdir(origWd) + os.Setenv("LLGO_ROOT", ctx.origLLGORoot) + if ctx.tmpDir != "" { + os.RemoveAll(ctx.tmpDir) + } +} + func setupTestProject(t *testing.T) string { // Create a temporary directory for the test project tmpDir, err := os.MkdirTemp("", "llgo-test-*") @@ -58,12 +98,8 @@ func TestProjectCommands(t *testing.T) { 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) + ctx := setupTest(t) + defer teardownTest(ctx) // Change to test project directory if err := os.Chdir(tmpDir); err != nil { @@ -72,150 +108,53 @@ func TestProjectCommands(t *testing.T) { tests := []struct { name string - cmd *base.Command args []string wantErr bool }{ { 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", + args: []string{"llgo", "run", "main.go"}, wantErr: false, }, } - // 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) - - // 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) - } - - 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{}{} + defer func() { + if r := recover(); r != nil { + 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) + } + } }() - 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() - }() + os.Args = tt.args + main() - flag.Parse() - base.CmdName = tt.cmd.Name() - - if !tt.cmd.Runnable() { - t.Fatalf("%s: Command is not runnable", tt.name) - } - - // 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()) - } - } - } - - // Run the command - tt.cmd.Run(tt.cmd, tt.args) - }() - - <-done - <-done - - // 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 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) - } + // For build/install commands, check if binary was created + if strings.HasPrefix(tt.name, "build") || strings.HasPrefix(tt.name, "install") { + binName := "testproject" + if _, err := os.Stat(binName); os.IsNotExist(err) { + t.Errorf("Binary %s was not created", binName) } } }) @@ -223,82 +162,8 @@ 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 - }() + ctx := setupTest(t) + defer teardownTest(ctx) tests := []struct { name string @@ -306,36 +171,84 @@ func TestHelpCommand(t *testing.T) { 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) { + ctx := setupTest(t) + defer teardownTest(ctx) + + 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"}, + }, + } + + 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/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 +} From e7e2ba479055714d7d6c9eed94b3647fc2506d7b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jan 2025 12:08:12 +0800 Subject: [PATCH 3/9] test: test llgo run -v --- compiler/cmd/llgo/llgo_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compiler/cmd/llgo/llgo_test.go b/compiler/cmd/llgo/llgo_test.go index 4863f88b..88b4bd7b 100644 --- a/compiler/cmd/llgo/llgo_test.go +++ b/compiler/cmd/llgo/llgo_test.go @@ -131,6 +131,11 @@ func TestProjectCommands(t *testing.T) { args: []string{"llgo", "run", "main.go"}, wantErr: false, }, + { + name: "run command", + args: []string{"llgo", "run", "-v", "."}, + wantErr: false, + }, } for _, tt := range tests { From b6c5503c5bab17a2127cedf4806592b605408bac Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jan 2025 12:56:04 +0800 Subject: [PATCH 4/9] test: chore/llgen --- compiler/chore/llgen/llgen_test.go | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 compiler/chore/llgen/llgen_test.go 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") + } +} From 521b70c7156e657c365e8a809c90cbd12ff93be5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jan 2025 12:20:40 +0800 Subject: [PATCH 5/9] test: llgo clean, llgo cmptest, llgo cmptest -gen --- compiler/cmd/llgo/llgo_test.go | 267 +++++++++++++++++++++++---------- 1 file changed, 188 insertions(+), 79 deletions(-) diff --git a/compiler/cmd/llgo/llgo_test.go b/compiler/cmd/llgo/llgo_test.go index 88b4bd7b..ed4e9585 100644 --- a/compiler/cmd/llgo/llgo_test.go +++ b/compiler/cmd/llgo/llgo_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "path/filepath" "strings" @@ -9,84 +10,67 @@ import ( "github.com/goplus/llgo/compiler/internal/mockable" ) -var origWd string - func init() { - var err error - origWd, err = os.Getwd() + origWd, err := os.Getwd() if err != nil { panic(err) } -} -type testContext struct { - origLLGORoot string - tmpDir string -} - -func setupTest(t *testing.T) *testContext { - ctx := &testContext{} - - // Save original state - ctx.origLLGORoot = os.Getenv("LLGO_ROOT") - - // Create temporary LLGO_ROOT - tmpDir, err := os.MkdirTemp("", "llgo-root-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - ctx.tmpDir = tmpDir - - // Set LLGO_ROOT + // Set LLGO_ROOT to project root llgoRoot := filepath.Join(origWd, "../../..") if err := os.Setenv("LLGO_ROOT", llgoRoot); err != nil { - os.RemoveAll(tmpDir) - t.Fatalf("Failed to set LLGO_ROOT: %v", err) - } - t.Logf("LLGO_ROOT set to: %s", llgoRoot) - mockable.EnableMock() - - return ctx -} - -func teardownTest(ctx *testContext) { - os.Chdir(origWd) - os.Setenv("LLGO_ROOT", ctx.origLLGORoot) - if ctx.tmpDir != "" { - os.RemoveAll(ctx.tmpDir) + panic(fmt.Sprintf("Failed to set LLGO_ROOT: %v", err)) } } func setupTestProject(t *testing.T) string { - // Create a temporary directory for the test project - tmpDir, err := os.MkdirTemp("", "llgo-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } + tmpDir := t.TempDir() - // Create a simple Go program - mainFile := filepath.Join(tmpDir, "main.go") - err = os.WriteFile(mainFile, []byte(`package main + // 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) } @@ -94,22 +78,11 @@ go 1.20 } func TestProjectCommands(t *testing.T) { - // Setup test project - tmpDir := setupTestProject(t) - defer os.RemoveAll(tmpDir) - - ctx := setupTest(t) - defer teardownTest(ctx) - - // 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 args []string wantErr bool + setup func(dir string) error }{ { name: "build command", @@ -127,27 +100,145 @@ func TestProjectCommands(t *testing.T) { wantErr: false, }, { - name: "run command", + name: "run command with file", args: []string{"llgo", "run", "main.go"}, wantErr: false, }, { - name: "run command", + 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) + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + // Create a new test directory for each test case + tmpDir := setupTestProject(t) + defer os.RemoveAll(tmpDir) + + // Change to test project directory + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("Failed to change directory: %v", err) + } + + if tt.setup != nil { + if err := tt.setup(tmpDir); err != nil { + t.Fatalf("Failed to setup test: %v", err) + } + } + + mockable.EnableMock() defer func() { if r := recover(); r != nil { 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) + 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) + } } } }() @@ -158,8 +249,24 @@ func TestProjectCommands(t *testing.T) { // For build/install commands, check if binary was created if strings.HasPrefix(tt.name, "build") || strings.HasPrefix(tt.name, "install") { binName := "testproject" - if _, err := os.Stat(binName); os.IsNotExist(err) { - t.Errorf("Binary %s was not created", binName) + 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) + } + if _, err := os.Stat(binPath); os.IsNotExist(err) { + t.Errorf("Binary %s was not created at %s", binName, binPath) } } }) @@ -167,9 +274,6 @@ func TestProjectCommands(t *testing.T) { } func TestCommandHandling(t *testing.T) { - ctx := setupTest(t) - defer teardownTest(ctx) - tests := []struct { name string args []string @@ -213,9 +317,6 @@ func TestCommandHandling(t *testing.T) { } func TestHelpCommand(t *testing.T) { - ctx := setupTest(t) - defer teardownTest(ctx) - tests := []struct { name string args []string @@ -236,6 +337,14 @@ func TestHelpCommand(t *testing.T) { 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 { From cabc7ffca8fd712ebb125ba62b80d584675c191c Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 18 Jan 2025 12:33:54 +0800 Subject: [PATCH 6/9] env: find llgo root for development --- compiler/cmd/internal/version/version.go | 2 +- {x => compiler/internal}/env/build.go | 0 compiler/internal/env/env.go | 43 ++++++++++++++++++++---- compiler/internal/env/env_test.go | 6 ++-- {x => compiler/internal}/env/version.go | 6 ++++ 5 files changed, 48 insertions(+), 9 deletions(-) rename {x => compiler/internal}/env/build.go (100%) rename {x => compiler/internal}/env/version.go (92%) 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/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..1088de59 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 { @@ -53,13 +56,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 +95,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..0bb19cdb 100644 --- a/compiler/internal/env/env_test.go +++ b/compiler/internal/env/env_test.go @@ -58,8 +58,10 @@ 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, _ := os.Getwd() + expected, _ := filepath.Abs(filepath.Join(wd, "../../../runtime")) + if got := LLGoRuntimeDir(); got != expected { + t.Errorf("LLGoRuntimeDir() = %v, want %v", got, expected) } }) } diff --git a/x/env/version.go b/compiler/internal/env/version.go similarity index 92% rename from x/env/version.go rename to compiler/internal/env/version.go index 975d4fef..ea80e2bf 100644 --- a/x/env/version.go +++ b/compiler/internal/env/version.go @@ -23,6 +23,8 @@ import "runtime/debug" var buildVersion string // Version returns the version of the running LLGo binary. +// +//export LLGoVersion func Version() string { if buildVersion != "" { return buildVersion @@ -33,3 +35,7 @@ func Version() string { } return "(devel)" } + +func Devel() bool { + return Version() == "(devel)" +} From 93d36d40d8be304210fb05e70e578cd15c9caab2 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 18 Jan 2025 15:39:19 +0800 Subject: [PATCH 7/9] env: fix version --- compiler/internal/env/version.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/compiler/internal/env/version.go b/compiler/internal/env/version.go index ea80e2bf..362e3740 100644 --- a/compiler/internal/env/version.go +++ b/compiler/internal/env/version.go @@ -16,7 +16,13 @@ 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. @@ -30,12 +36,12 @@ func Version() string { 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)" + return Version() == devel } From 977db867006b342d3cee633bd4408212cd3774cc Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 18 Jan 2025 15:38:51 +0800 Subject: [PATCH 8/9] env: warning and fallback if LLGO_ROOT is invalid --- compiler/internal/env/env.go | 8 +++++-- compiler/internal/env/env_test.go | 35 +++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/compiler/internal/env/env.go b/compiler/internal/env/env.go index 1088de59..09246f60 100644 --- a/compiler/internal/env/env.go +++ b/compiler/internal/env/env.go @@ -41,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() diff --git a/compiler/internal/env/env_test.go b/compiler/internal/env/env_test.go index 0bb19cdb..823edeeb 100644 --- a/compiler/internal/env/env_test.go +++ b/compiler/internal/env/env_test.go @@ -58,10 +58,28 @@ func TestLLGoRuntimeDir(t *testing.T) { defer os.Setenv("LLGO_ROOT", origLLGoRoot) os.Setenv("LLGO_ROOT", "/nonexistent/path") - wd, _ := os.Getwd() - expected, _ := filepath.Abs(filepath.Join(wd, "../../../runtime")) - if got := LLGoRuntimeDir(); got != expected { - t.Errorf("LLGoRuntimeDir() = %v, want %v", got, expected) + 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) } }) } @@ -92,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) } }) From a9ee32376081af19ebb6e6c54b969cce2d2c0ebe Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 18 Jan 2025 16:42:11 +0800 Subject: [PATCH 9/9] test: disable log and output to speed up testing --- compiler/cl/compile_test.go | 10 ++++++++++ compiler/ssa/cl_test.go | 7 +++++++ 2 files changed, 17 insertions(+) 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/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) }