main_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"context"
"errors"
"go/ast"
"go/parser"
"go/token"
"os"
"github.com/spf13/cobra"
"testing"
"go.keploy.io/server/v3/utils"
"go.uber.org/zap"
)
// A logger that cannot start stops the CLI with exit code 1 (#4441). log.New
// does not fail where it cannot create its log file -- it logs to the console
// only -- so the failure is injected.
func TestStart_LoggerInitFailureSetsErrCode(t *testing.T) {
orig := newLogger
t.Cleanup(func() { newLogger = orig })
newLogger = func() (*zap.Logger, *os.File, error) {
return nil, nil, errors.New("injected logger failure")
}
utils.ErrCode = 0
t.Cleanup(func() { utils.ErrCode = 0 })
start(context.Background())
if utils.ErrCode != 1 {
t.Fatalf("expected utils.ErrCode = 1 on logger failure, got %d", utils.ErrCode)
}
}
// A chown -R of the working directory is a destructive thing to do on the
// strength of a guess.
//
// It used to be gated on conf.Path being non-empty, which is true only once
// ValidateFlags has run -- an incidental signal, and one that turned `sudo
// keploy --version` into a recursive chown of the whole tree the moment the
// config default for path stopped being the empty string. Only the commands
// that can CREATE files under that path have anything to restore.
func TestWritesKeployFolder(t *testing.T) {
// A real command tree, because argv cannot be read by eye: a root flag
// that takes a separate value puts a non-flag word before the command.
root := func() *cobra.Command {
r := &cobra.Command{Use: "keploy"}
r.PersistentFlags().String("storage-format", "yaml", "")
r.PersistentFlags().Bool("debug", false, "")
for _, n := range []string{"record", "test", "normalize", "templatize", "contract", "import", "export", "sanitize", "diff", "config", "login", "update"} {
c := &cobra.Command{Use: n, Run: func(*cobra.Command, []string) {}}
r.AddCommand(c)
}
mock := &cobra.Command{Use: "mock", Run: func(*cobra.Command, []string) {}}
mock.AddCommand(&cobra.Command{Use: "record", Run: func(*cobra.Command, []string) {}})
r.AddCommand(mock)
return r
}
for _, c := range []struct {
args []string
want bool
}{
// A root flag with a separate value used to be read as the command.
{[]string{"keploy", "--storage-format", "json", "record", "-c", "npm test"}, true},
{[]string{"keploy", "--debug", "--storage-format", "json", "test"}, true},
{[]string{"keploy", "--storage-format", "json", "config", "--generate"}, false},
{[]string{"keploy", "mock", "record", "-c", "npm test"}, true},
{[]string{"keploy", "record", "-c", "npm test"}, true},
{[]string{"keploy", "test"}, true},
{[]string{"keploy", "mock", "record", "-c", "npm test"}, true},
{[]string{"keploy", "--debug", "record"}, true},
{[]string{"keploy", "--version"}, false},
{[]string{"keploy", "--help"}, false},
{[]string{"keploy"}, false},
{[]string{"keploy", "config", "--generate"}, false},
{[]string{"keploy", "config", "defaults"}, false},
{[]string{"keploy", "login"}, false},
{[]string{"keploy", "update"}, false},
} {
if got := writesKeployFolder(root(), c.args[1:]); got != c.want {
t.Errorf("writesKeployFolder(%v) = %v, want %v", c.args[1:], got, c.want)
}
}
}
// main says its agent records the steps of its whole teardown before the
// command runs, and that the detach is done once the command has returned,
// right before the process exits (utils.DeclareTeardownSteps,
// utils.MarkDetachDone): the CLI that stops an agent kills it for want of
// progress only when it says the first. The second is the agent's last
// report, so nothing after it is watched: a statement main ran between it and
// os.Exit, or a call in os.Exit's argument, that hung -- a log sync on a
// blocked pipe, a flush -- would never be forced, only waited for until it
// ends or Ctrl+C is pressed. So nothing may come between them, os.Exit's
// argument calls nothing and receives from no channel, and only the Go
// runtime's own exit goes unwatched.
func TestMainReportsItsTeardownSteps(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var body []ast.Stmt
for _, d := range f.Decls {
if fn, ok := d.(*ast.FuncDecl); ok && fn.Name.Name == "main" {
body = fn.Body.List
}
}
at := func(pkg, name string) int {
for i, st := range body {
es, ok := st.(*ast.ExprStmt)
if !ok {
continue
}
call, ok := es.X.(*ast.CallExpr)
if !ok {
continue
}
switch fun := call.Fun.(type) {
case *ast.SelectorExpr:
if x, ok := fun.X.(*ast.Ident); ok && x.Name == pkg && fun.Sel.Name == name {
return i
}
case *ast.Ident:
if pkg == "" && fun.Name == name {
return i
}
}
}
return -1
}
declare, start, done, exit := at("utils", "DeclareTeardownSteps"), at("", "start"), at("utils", "MarkDetachDone"), at("os", "Exit")
if start < 0 || exit < 0 {
t.Fatalf("main no longer calls start and os.Exit as statements (start=%d exit=%d)", start, exit)
}
if declare < 0 || declare > start {
t.Errorf("main does not call utils.DeclareTeardownSteps before start (at %d, start at %d)", declare, start)
}
if done < 0 || done < start || done != exit-1 {
t.Errorf("main does not call utils.MarkDetachDone after start and as the statement right before os.Exit (at %d, start %d, exit %d)", done, start, exit)
}
for _, arg := range body[exit].(*ast.ExprStmt).X.(*ast.CallExpr).Args {
ast.Inspect(arg, func(n ast.Node) bool {
switch e := n.(type) {
case *ast.CallExpr:
t.Errorf("%s: os.Exit's argument in main makes a call, which runs after utils.MarkDetachDone's last report, unwatched", fset.Position(e.Pos()))
return false
case *ast.UnaryExpr:
if e.Op == token.ARROW {
t.Errorf("%s: os.Exit's argument in main receives from a channel, which can wait after utils.MarkDetachDone's last report, unwatched", fset.Position(e.Pos()))
return false
}
}
return true
})
}
}