Ctrl K
ComunidadGuías…

main_exitcode_wiring_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package main

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"testing"

	"go.keploy.io/server/v3/utils"
	"go.uber.org/zap"
)

// subprocessEnv makes TestMain run the real CLI instead of the test suite, so
// the tests below observe genuine process exit codes. Testing the helper in
// isolation is not enough: the bug this guards against was the helper's result
// never being assigned to utils.ErrCode, which a pure unit test cannot see.
const subprocessEnv = "KEPLOY_EXITCODE_TEST_SUBPROCESS"

// childWouldSudo is the exit status of a child refused because start() would
// re-execute it through sudo. No code in the exit-code contract is 99.
const childWouldSudo = 99

func TestMain(m *testing.M) {
	if os.Getenv(subprocessEnv) == "1" {
		// start() re-executes keploy through sudo before cobra parses
		// anything when the -c is a docker command and the user is not root
		// (utils.ShouldReexecWithSudo). Here that would run this test binary
		// as root, with the caller's environment, where sudo needs no
		// password; ask for a password where it does, and block the test;
		// and fail where there is no sudo. The result would depend on the
		// machine's sudo, not on keploy, so a case that would do it is
		// refused, the same way everywhere: give it a native -c.
		if utils.ShouldReexecWithSudo() && os.Getenv(reexecWithNoSudoEnv) != "1" {
			fmt.Fprintf(os.Stderr, "refused: keploy %s would re-execute this test binary through sudo; give the case a native -c\n", strings.Join(os.Args[1:], " "))
			os.Exit(childWouldSudo)
		}
		if hold := os.Getenv(holdAfterLoggerEnv); hold != "" {
			holdAfterLogger(hold)
		}
		setVersion()
		start(utils.NewCtx())
		os.Exit(utils.ErrCode)
	}
	os.Exit(m.Run())
}

// reexecWithNoSudoEnv lets a re-executed CLI that would re-execute itself
// through sudo go on to that re-exec. Only a row whose PATH has no sudo on it
// sets it, so the re-exec fails where it looks for sudo, and nothing runs as
// root.
const reexecWithNoSudoEnv = "KEPLOY_TEST_REEXEC_WITH_NO_SUDO"

// holdAfterLoggerEnv names a named pipe at which a re-executed CLI is held
// right after start() built its logger (newLogger, the real one), before
// cobra parses anything: holdAfterLogger.
const holdAfterLoggerEnv = "KEPLOY_TEST_HOLD_AFTER_LOGGER"

// holdAfterLogger holds the CLI right after its logger is built: it opens the
// named pipe fifo to read, which waits for the test to open it to write, and
// reads it until the test closes it. When the test's open returns, the run
// has its logger, and its log file if it keeps one, and does nothing more
// until the test lets it go.
func holdAfterLogger(fifo string) {
	inner := newLogger
	newLogger = func() (*zap.Logger, *os.File, error) {
		logger, f, err := inner()
		if r, openErr := os.Open(fifo); openErr == nil {
			_, _ = io.Copy(io.Discard, r)
			_ = r.Close()
		}
		return logger, f, err
	}
}

// runCLI re-executes this test binary as the keploy CLI with the given
// arguments, in a scratch working directory, and returns the exit code.
func runCLI(t *testing.T, args ...string) int {
	t.Helper()
	code, _ := runCLIOutput(t, args...)
	return code
}

// runCLIOutput is runCLI, with what the CLI wrote to stdout and stderr.
func runCLIOutput(t *testing.T, args ...string) (int, string) {
	t.Helper()

	cmd := exec.Command(os.Args[0])
	cmd.Args = append([]string{"keploy"}, args...)
	cmd.Env = append(os.Environ(), subprocessEnv+"=1")
	cmd.Dir = t.TempDir()
	var out bytes.Buffer
	cmd.Stdout, cmd.Stderr = &out, &out

	if err := cmd.Run(); err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			return exitErr.ExitCode(), out.String()
		}
		t.Fatalf("running %v: %v", args, err)
	}
	return 0, out.String()
}

func TestCLIExitCodes(t *testing.T) {
	// Startup does more than parse flags - it reads/creates the installation
	// id, for one - and none of that is under test here. If the happy path
	// cannot even reach exit 0 the environment is broken, and asserting the
	// failure cases would only produce a misleading red.
	if code := runCLI(t, "--help"); code != 0 {
		t.Skipf("keploy --help exited %d; CLI startup is unusable in this environment", code)
	}

	tests := []struct {
		name string
		args []string
		want int
	}{
		{name: "root help", args: []string{"--help"}, want: 0},
		{name: "subcommand help", args: []string{"test", "--help"}, want: 0},
		{name: "version flag", args: []string{"--version"}, want: 0},
		{name: "unknown command", args: []string{"bogus-cmd"}, want: utils.ExitUsageError},
		// A flag pflag cannot parse is a usage error too: the command never ran.
		{name: "unknown flag", args: []string{"test", "--nope"}, want: utils.ExitUsageError},
		{name: "unknown shorthand", args: []string{"test", "-Z"}, want: utils.ExitUsageError},
		{name: "invalid flag value", args: []string{"test", "--delay", "abc"}, want: utils.ExitUsageError},
		{name: "flag missing its value", args: []string{"test", "--delay"}, want: utils.ExitUsageError},
		{name: "unknown mock flag", args: []string{"mock", "record", "--nope"}, want: utils.ExitUsageError},
		{name: "too many arguments", args: []string{"diff", "a", "b", "c"}, want: utils.ExitUsageError},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := runCLI(t, tt.args...); got != tt.want {
				t.Errorf("keploy %v exit code = %d, want %d", tt.args, got, tt.want)
			}
		})
	}
}

// A usage error is said once, then where the help of the command that failed
// is: the flag `keploy record --delay 10` got wrong is one of record's, which
// `keploy record --help` lists, and the root's help lists only commands. No
// usage text is printed under it (some 80 lines, which scrolled the error out
// of view). start takes the failing command from ExecuteC and hands its path
// to exitCodeForCmdErr, which prints whatever path it is given: only a run of
// start tells the command that failed from the root.
func TestCLIPointsAUsageErrorAtTheHelpOfTheCommandThatFailed(t *testing.T) {
	// A CLI that cannot even print its help is broken, and that is a failure
	// of this test, not a reason to skip it: skipped, it would go green.
	if code, out := runCLIOutput(t, "--help"); code != 0 {
		t.Fatalf("keploy --help exited %d, want 0; the CLI does not start:\n%s", code, out)
	}

	for _, tc := range []struct {
		args []string
		said string // the error, said once
		help string // the command whose --help it points at
	}{
		// The walkthrough's first command, with a native -c: its docker one,
		// `-c "docker compose up"`, would re-execute the child through sudo
		// (TestMain refuses that). The docker line itself is run in-process,
		// where nothing re-executes, by cli's
		// TestRecordGivenAnUnknownFlagSaysSoWithoutItsUsage.
		{args: []string{"record", "-c", "./app", "--container-name", "api", "--delay", "10"},
			said: "unknown flag: --delay", help: "keploy record"},
		{args: []string{"test", "--nope"}, said: "unknown flag: --nope", help: "keploy test"},
		{args: []string{"mock", "record", "--nope"}, said: "unknown flag: --nope", help: "keploy mock record"},
		{args: []string{"diff", "a", "b", "c"}, said: "accepts at most 2 arg(s), received 3", help: "keploy diff"},
		{args: []string{"bogus-cmd"}, said: `unknown command "bogus-cmd" for "keploy"`, help: "keploy"},
	} {
		t.Run(strings.Join(tc.args, " "), func(t *testing.T) {
			code, out := runCLIOutput(t, tc.args...)
			if code != utils.ExitUsageError {
				t.Errorf("exit code = %d, want %d", code, utils.ExitUsageError)
			}
			if n := strings.Count(out, tc.said); n != 1 {
				t.Errorf("%q said %d times, want once:\n%s", tc.said, n, out)
			}
			if hint := "Run '" + tc.help + " --help' for usage."; strings.Count(out, "--help' for usage.") != 1 || !strings.Contains(out, hint) {
				t.Errorf("want one help pointer, %q:\n%s", hint, out)
			}
			if strings.Contains(out, "Usage:") {
				t.Errorf("the usage text was printed under the error:\n%s", out)
			}
		})
	}
}

// TestMain's refusal is what keeps every case above off sudo, the same way on
// every machine: a child given a docker -c, run by a user who is not root, is
// refused before start() runs, and so before anything re-executes. The line is
// the walkthrough's. Were the refusal gone, start() would re-execute the child
// through sudo first, since its sudo check runs before cobra parses the line;
// only the re-executed child would stop at --delay, which record has no flag
// for. It would never start the application, but it would go through sudo.
func TestCLIChildThatWouldReexecThroughSudoIsRefused(t *testing.T) {
	if runtime.GOOS != "linux" || os.Geteuid() == 0 {
		t.Skip("start() re-executes through sudo only on Linux, for a user who is not root")
	}
	code, out := runCLIOutput(t, "record", "-c", "docker compose up", "--container-name", "api", "--delay", "10")
	if code != childWouldSudo || !strings.Contains(out, "would re-execute this test binary through sudo") {
		t.Errorf("exit code = %d, want %d (refused), and the refusal said:\n%s", code, childWouldSudo, out)
	}
}

// TestCLICleansUpLogFileOnUnknownCommand pins the other half of routing errors
// through utils.ErrCode rather than os.Exit(1): the deferred cleanup in start()
// now actually runs, so a failed invocation leaves no log behind -- not in the
// temp directory, where the log is, and nothing in the working directory,
// where keploy-logs.txt used to be.
func TestCLICleansUpLogFileOnUnknownCommand(t *testing.T) {
	if code := runCLI(t, "--help"); code != 0 {
		t.Skipf("keploy --help exited %d; CLI startup is unusable in this environment", code)
	}

	dir, tmp := t.TempDir(), t.TempDir()
	cmd := exec.Command(os.Args[0])
	cmd.Args = []string{"keploy", "bogus-cmd"}
	cmd.Env = append(os.Environ(), subprocessEnv+"=1", "TMPDIR="+tmp)
	cmd.Dir = dir
	_ = cmd.Run()

	for _, d := range []string{dir, tmp} {
		entries, err := os.ReadDir(d)
		if err != nil {
			t.Fatal(err)
		}
		for _, e := range entries {
			t.Errorf("a failed command left %s behind in %s", e.Name(), d)
		}
	}
}

// `keploy test` with no keploy folder, a new user's first mistake, fails with
// exit code 1 and names its log, and the log is gone at the end, from the
// temp directory as from the working directory. ValidateFlags used to
// os.Exit(1) there, past start()'s deferred calls: each such run left one
// more keploy-logs-*.txt in the temp directory. It now returns
// provider.ErrNoTestSets, which main exits 1 for, and which nothing says a
// second time as "failed to validate flags".
func TestCLITestWithNoKeployFolderExitsOneAndLeavesNoLog(t *testing.T) {
	if code := runCLI(t, "--help"); code != 0 {
		t.Skipf("keploy --help exited %d; CLI startup is unusable in this environment", code)
	}

	dir, tmp := t.TempDir(), t.TempDir()
	cmd := exec.Command(os.Args[0])
	cmd.Args = []string{"keploy", "test", "-c", "true"}
	cmd.Env = append(os.Environ(), subprocessEnv+"=1", "TMPDIR="+tmp)
	cmd.Dir = dir
	var out bytes.Buffer
	cmd.Stdout, cmd.Stderr = &out, &out
	code := 0
	if err := cmd.Run(); err != nil {
		var exitErr *exec.ExitError
		if !errors.As(err, &exitErr) {
			t.Fatalf("running keploy test: %v", err)
		}
		code = exitErr.ExitCode()
	}

	if code != utils.ExitKeployError {
		t.Errorf("exit code = %d, want %d", code, utils.ExitKeployError)
	}
	said := out.String()
	if !strings.Contains(said, "No test-sets found") {
		t.Errorf("the run did not say it found no test-sets:\n%s", said)
	}
	if !strings.Contains(said, "logs: "+filepath.Join(tmp, "keploy-logs-")) {
		t.Errorf("the run did not name its log in %s:\n%s", tmp, said)
	}
	if strings.Contains(said, "failed to validate flags") {
		t.Errorf("the refusal was said a second time, as a validation failure:\n%s", said)
	}
	for _, d := range []string{dir, tmp} {
		entries, err := os.ReadDir(d)
		if err != nil {
			t.Fatal(err)
		}
		for _, e := range entries {
			t.Errorf("keploy test with no keploy folder left %s behind in %s", e.Name(), d)
		}
	}
}

// A docker run that keploy cannot re-execute through sudo, for there is no
// sudo, fails with exit code 1 and leaves no log: ReexecWithSudo returns its
// error to start(), whose deferred calls remove the log. It used to os.Exit(1)
// inside ReexecWithSudo, before the log's name went at the exec.
func TestCLIReexecWithNoSudoExitsOneAndLeavesNoLog(t *testing.T) {
	if runtime.GOOS != "linux" || os.Geteuid() == 0 {
		t.Skip("start() re-executes through sudo only on Linux, for a user who is not root")
	}
	if code := runCLI(t, "--help"); code != 0 {
		t.Skipf("keploy --help exited %d; CLI startup is unusable in this environment", code)
	}

	dir, tmp, bin := t.TempDir(), t.TempDir(), t.TempDir()
	cmd := exec.Command(os.Args[0])
	cmd.Args = []string{"keploy", "record", "-c", "docker run --name app img"}
	env := []string{subprocessEnv + "=1", reexecWithNoSudoEnv + "=1", "TMPDIR=" + tmp, "PATH=" + bin}
	for _, kv := range os.Environ() {
		if !strings.HasPrefix(kv, "PATH=") && !strings.HasPrefix(kv, "TMPDIR=") {
			env = append(env, kv)
		}
	}
	cmd.Env = env
	cmd.Dir = dir
	var out bytes.Buffer
	cmd.Stdout, cmd.Stderr = &out, &out
	code := 0
	if err := cmd.Run(); err != nil {
		var exitErr *exec.ExitError
		if !errors.As(err, &exitErr) {
			t.Fatalf("running keploy record: %v", err)
		}
		code = exitErr.ExitCode()
	}

	if code != utils.ExitKeployError {
		t.Errorf("exit code = %d, want %d:\n%s", code, utils.ExitKeployError, out.String())
	}
	if !strings.Contains(out.String(), "sudo is required") {
		t.Errorf("the run did not say it needs sudo:\n%s", out.String())
	}
	for _, d := range []string{dir, tmp} {
		entries, err := os.ReadDir(d)
		if err != nil {
			t.Fatal(err)
		}
		for _, e := range entries {
			t.Errorf("a re-exec with no sudo left %s behind in %s", e.Name(), d)
		}
	}
}