diff options
author | Patrick Steinhardt <psteinhardt@gitlab.com> | 2022-01-11 16:53:43 +0300 |
---|---|---|
committer | Patrick Steinhardt <psteinhardt@gitlab.com> | 2022-01-14 17:25:03 +0300 |
commit | 1f74e9150cb8238cb45715a65a5b66e7b821bd2e (patch) | |
tree | 4d3922febc64877b2b836567283cb736be7e729c | |
parent | 8b8c314ddc9ec769db31a0b4603cc69c4f5b24ae (diff) |
cmd/gitaly-wrapper: Add tests verifying the executable works as expected
While we now have a bunch of tests which verify that low-level
components of gitaly-wrapper work as expected, we have no high-level
tests which verify that it comes together nicely.
Add a bunch of tests which verify that running the gitaly-wrapper
produces desired results.
-rw-r--r-- | cmd/gitaly-wrapper/main_test.go | 174 | ||||
-rw-r--r-- | internal/testhelper/testcfg/build.go | 5 |
2 files changed, 179 insertions, 0 deletions
diff --git a/cmd/gitaly-wrapper/main_test.go b/cmd/gitaly-wrapper/main_test.go index 19a20f4a1..cad8fbceb 100644 --- a/cmd/gitaly-wrapper/main_test.go +++ b/cmd/gitaly-wrapper/main_test.go @@ -1,7 +1,10 @@ package main import ( + "bufio" "errors" + "fmt" + "io" "os" "os/exec" "path/filepath" @@ -9,7 +12,9 @@ import ( "testing" "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/v14/internal/bootstrap" "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper" + "gitlab.com/gitlab-org/gitaly/v14/internal/testhelper/testcfg" ) // TestStolenPid tests for regressions in https://gitlab.com/gitlab-org/gitaly/issues/1661 @@ -253,3 +258,172 @@ func TestIsProcessAlive(t *testing.T) { }) }) } + +func TestRun(t *testing.T) { + binary := testcfg.BuildGitalyWrapper(t, testcfg.Build(t)) + + t.Run("missing arguments", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + output, err := exec.CommandContext(ctx, binary).CombinedOutput() + require.Error(t, err) + require.Contains(t, string(output), "usage: ") + }) + + t.Run("missing PID file envvar", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + output, err := exec.CommandContext(ctx, binary, "binary").CombinedOutput() + require.Error(t, err) + require.Contains(t, string(output), "missing pid file ENV variable") + }) + + t.Run("invalid executable", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + pidPath := filepath.Join(testhelper.TempDir(t), "pid") + + cmd := exec.CommandContext(ctx, binary, "does-not-exist") + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", bootstrap.EnvPidFile, pidPath)) + + output, err := cmd.CombinedOutput() + require.Error(t, err) + require.Contains(t, string(output), "executable file not found in $PATH") + }) + + t.Run("adopting executable", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + script := testhelper.WriteExecutable(t, filepath.Join(testhelper.TempDir(t), "script"), []byte( + `#!/usr/bin/env bash + echo ready + read wait_until_closed + `)) + + scriptCmd := exec.CommandContext(ctx, script) + scriptStdout, err := scriptCmd.StdoutPipe() + require.NoError(t, err) + scriptStdin, err := scriptCmd.StdinPipe() + require.NoError(t, err) + + require.NoError(t, scriptCmd.Start()) + + // Read the first byte such that we know the process has been spawned. + _, err = scriptStdout.Read(make([]byte, 10)) + require.NoError(t, err) + + // Write the PID of the running process into the PID file. As a result, it should + // get adopted by gitaly-wrapper, which means it wouldn't try to execute it anew. + pidPath := filepath.Join(testhelper.TempDir(t), "pid") + require.NoError(t, os.WriteFile(pidPath, []byte(strconv.FormatInt(int64(scriptCmd.Process.Pid), 10)), 0o644)) + + // Run gitaly-script with a binary path whose basename matches, but which ultimately + // doesn't exist. This proves that it doesn't try to execute the script again. + wrapperCmd := exec.CommandContext(ctx, binary, "/does/not/exist/bash") + wrapperCmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", bootstrap.EnvPidFile, pidPath)) + wrapperStdout, err := wrapperCmd.StdoutPipe() + require.NoError(t, err) + require.NoError(t, wrapperCmd.Start()) + + // We're now waiting for gitaly-wrapper to adopt the running script. + reader := bufio.NewReader(wrapperStdout) + for _, expectedLine := range []string{ + "Wrapper started", + "finding process", + "adopting a process", + } { + line, err := reader.ReadBytes('\n') + require.NoError(t, err, "reading expected line %q", + expectedLine) + require.Contains(t, string(line), expectedLine) + } + + // The script has been adopted, so we can now close its stdin and thus cause it to + // exit. + testhelper.MustClose(t, scriptStdin) + require.Error(t, scriptCmd.Wait()) + + // As a result, the wrapper should also exit successfully. + require.NoError(t, wrapperCmd.Wait()) + }) + + t.Run("spawning executable", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + script := testhelper.WriteExecutable(t, filepath.Join(testhelper.TempDir(t), "script"), []byte( + `#!/usr/bin/env bash + echo "I have been executed" + `)) + + pidPath := filepath.Join(testhelper.TempDir(t), "pid") + + cmd := exec.CommandContext(ctx, binary, script) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", bootstrap.EnvPidFile, pidPath)) + output, err := cmd.CombinedOutput() + require.NoError(t, err) + + require.Contains(t, string(output), "spawning a process") + require.Contains(t, string(output), "I have been executed") + + require.NoFileExists(t, pidPath) + }) + + t.Run("spawning executable with missing process", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + script := testhelper.WriteExecutable(t, filepath.Join(testhelper.TempDir(t), "script"), []byte( + `#!/usr/bin/env bash + echo "I have been executed" + `)) + + pidPath := filepath.Join(testhelper.TempDir(t), "pid") + require.NoError(t, os.WriteFile(pidPath, []byte("12345"), 0o644)) + + cmd := exec.CommandContext(ctx, binary, script) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", bootstrap.EnvPidFile, pidPath)) + + output, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Contains(t, string(output), "spawning a process") + require.Contains(t, string(output), "I have been executed") + }) + + t.Run("spawning executable with zombie process", func(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + script := testhelper.WriteExecutable(t, filepath.Join(testhelper.TempDir(t), "script"), []byte( + `#!/usr/bin/env bash + echo "I have been executed" + `)) + + scriptCmd := exec.CommandContext(ctx, script) + scriptStdout, err := scriptCmd.StdoutPipe() + require.NoError(t, err) + require.NoError(t, scriptCmd.Start()) + + // Read until we get an EOF, which means that the script has terminated. It's now in + // a zombie state because we don't call `Wait()`. + _, err = io.ReadAll(scriptStdout) + require.NoError(t, err) + + pidPath := filepath.Join(testhelper.TempDir(t), "pid") + require.NoError(t, os.WriteFile(pidPath, []byte(strconv.FormatInt(int64(scriptCmd.Process.Pid), 10)), 0o644)) + + cmd := exec.CommandContext(ctx, binary, script) + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", bootstrap.EnvPidFile, pidPath)) + + output, err := cmd.CombinedOutput() + require.NoError(t, err) + require.Contains(t, string(output), "spawning a process") + require.Contains(t, string(output), "I have been executed") + + require.NoError(t, scriptCmd.Wait()) + }) +} diff --git a/internal/testhelper/testcfg/build.go b/internal/testhelper/testcfg/build.go index c18a9fae5..ce958358c 100644 --- a/internal/testhelper/testcfg/build.go +++ b/internal/testhelper/testcfg/build.go @@ -33,6 +33,11 @@ func BuildGitalyGit2Go(t testing.TB, cfg config.Cfg) string { return symlinkPath } +// BuildGitalyWrapper builds the gitaly-wrapper command and installs it into the binary directory. +func BuildGitalyWrapper(t *testing.T, cfg config.Cfg) string { + return BuildBinary(t, cfg.BinDir, gitalyCommandPath("gitaly-wrapper")) +} + // BuildGitalyLFSSmudge builds the gitaly-lfs-smudge command and installs it into the binary // directory. func BuildGitalyLFSSmudge(t *testing.T, cfg config.Cfg) string { |