From 8749923f1ab450bf36507726fd19e6271c4d2554 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Tue, 14 Jan 2025 10:50:43 +0800 Subject: [PATCH] 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 +}