test: make cmd testable
This commit is contained in:
1
.github/codecov.yml
vendored
1
.github/codecov.yml
vendored
@@ -1,4 +1,3 @@
|
||||
coverage:
|
||||
ignore:
|
||||
- "compiler/chore"
|
||||
- "compiler/cmd"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{}{}
|
||||
}()
|
||||
|
||||
panicked := false
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
panicked = true
|
||||
t.Logf("%s: Command panicked: %v", tt.name, r)
|
||||
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)
|
||||
}
|
||||
}
|
||||
outW.Close()
|
||||
errW.Close()
|
||||
}()
|
||||
|
||||
flag.Parse()
|
||||
base.CmdName = tt.cmd.Name()
|
||||
os.Args = tt.args
|
||||
main()
|
||||
|
||||
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 {
|
||||
if strings.HasPrefix(tt.name, "build") || strings.HasPrefix(tt.name, "install") {
|
||||
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)
|
||||
}
|
||||
t.Errorf("Binary %s was not created", binName)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -223,119 +162,93 @@ 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
|
||||
}()
|
||||
ctx := setupTest(t)
|
||||
defer teardownTest(ctx)
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
name: "invalid command",
|
||||
args: []string{"llgo", "invalid"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
if len(tt.args) > 1 {
|
||||
base.CmdName = tt.args[1]
|
||||
}
|
||||
os.Args = tt.args
|
||||
main()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
args []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "help without subcommand",
|
||||
args: []string{"llgo", "help"},
|
||||
wantErr: false,
|
||||
name: "help build",
|
||||
args: []string{"llgo", "help", "build"},
|
||||
},
|
||||
{
|
||||
name: "help with subcommand",
|
||||
args: []string{"llgo", "help", "build"},
|
||||
wantErr: false,
|
||||
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) {
|
||||
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 {
|
||||
t.Errorf("got exit code %d, want 0", exitCode)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if len(args) > 0 && args[0] == "help" {
|
||||
help.Help(&buf, args[1:])
|
||||
}
|
||||
os.Args = tt.args
|
||||
main()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
29
compiler/internal/mockable/mockable.go
Normal file
29
compiler/internal/mockable/mockable.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user