1package testhelper
2
3import (
4	"errors"
5	"fmt"
6	"io/ioutil"
7	"os"
8	"os/exec"
9	"path/filepath"
10	"runtime"
11	"strings"
12	"sync"
13	"testing"
14	"time"
15
16	log "github.com/sirupsen/logrus"
17	"github.com/stretchr/testify/require"
18	"gitlab.com/gitlab-org/gitaly/v14/internal/gitaly/config"
19	gitalylog "gitlab.com/gitlab-org/gitaly/v14/internal/log"
20)
21
22var (
23	configureOnce sync.Once
24	testDirectory string
25)
26
27// Configure sets up the global test configuration. On failure,
28// terminates the program.
29func Configure() func() {
30	configureOnce.Do(func() {
31		gitalylog.Configure(gitalylog.Loggers, "json", "panic")
32
33		var err error
34		testDirectory, err = ioutil.TempDir("", "gitaly-")
35		if err != nil {
36			log.Fatal(err)
37		}
38
39		for _, f := range []func() error{
40			ConfigureGit,
41		} {
42			if err := f(); err != nil {
43				os.RemoveAll(testDirectory)
44				log.Fatalf("error configuring tests: %v", err)
45			}
46		}
47	})
48
49	return func() {
50		if err := os.RemoveAll(testDirectory); err != nil {
51			log.Fatalf("error removing test directory: %v", err)
52		}
53	}
54}
55
56// ConfigureGit configures git for test purpose
57func ConfigureGit() error {
58	// We cannot use gittest here given that we ain't got no config yet. We thus need to
59	// manually resolve the git executable, which is either stored in below envvar if
60	// executed via our Makefile, or else just git as resolved via PATH.
61	gitPath := "git"
62	if path, ok := os.LookupEnv("GITALY_TESTING_GIT_BINARY"); ok {
63		gitPath = path
64	}
65
66	// Unset environment variables which have an effect on Git itself.
67	cmd := exec.Command(gitPath, "rev-parse", "--local-env-vars")
68	envvars, err := cmd.CombinedOutput()
69	if err != nil {
70		return fmt.Errorf("error computing local envvars: %w", err)
71	}
72	for _, envvar := range strings.Split(string(envvars), "\n") {
73		if err := os.Unsetenv(envvar); err != nil {
74			return fmt.Errorf("error unsetting envvar: %w", err)
75		}
76	}
77
78	_, currentFile, _, ok := runtime.Caller(0)
79	if !ok {
80		return fmt.Errorf("could not get caller info")
81	}
82
83	// Set both GOCACHE and GOPATH to the currently active settings to not
84	// have them be overridden by changing our home directory. default it
85	for _, envvar := range []string{"GOCACHE", "GOPATH"} {
86		cmd := exec.Command("go", "env", envvar)
87
88		output, err := cmd.Output()
89		if err != nil {
90			return err
91		}
92
93		err = os.Setenv(envvar, strings.TrimSpace(string(output)))
94		if err != nil {
95			return err
96		}
97	}
98
99	testHome := filepath.Join(filepath.Dir(currentFile), "testdata/home")
100	// overwrite HOME env variable so user global .gitconfig doesn't influence tests
101	return os.Setenv("HOME", testHome)
102}
103
104// ConfigureRuby configures Ruby settings for test purposes at run time.
105func ConfigureRuby(cfg *config.Cfg) error {
106	if dir := os.Getenv("GITALY_TEST_RUBY_DIR"); len(dir) > 0 {
107		// Sometimes runtime.Caller is unreliable. This environment variable provides a bypass.
108		cfg.Ruby.Dir = dir
109	} else {
110		_, currentFile, _, ok := runtime.Caller(0)
111		if !ok {
112			return fmt.Errorf("could not get caller info")
113		}
114		cfg.Ruby.Dir = filepath.Join(filepath.Dir(currentFile), "../../ruby")
115	}
116
117	if err := cfg.ConfigureRuby(); err != nil {
118		log.Fatalf("validate ruby config: %v", err)
119	}
120
121	return nil
122}
123
124// ConfigureGitalyGit2GoBin configures the gitaly-git2go command for tests
125func ConfigureGitalyGit2GoBin(t testing.TB, cfg config.Cfg) {
126	buildBinary(t, cfg.BinDir, "gitaly-git2go")
127}
128
129// ConfigureGitalyLfsSmudge configures the gitaly-lfs-smudge command for tests
130func ConfigureGitalyLfsSmudge(t *testing.T, outputDir string) {
131	buildCommand(t, outputDir, "gitaly-lfs-smudge")
132}
133
134// ConfigureGitalyHooksBin builds gitaly-hooks command for tests for the cfg.
135func ConfigureGitalyHooksBin(t testing.TB, cfg config.Cfg) {
136	buildBinary(t, cfg.BinDir, "gitaly-hooks")
137}
138
139// ConfigureGitalySSHBin builds gitaly-ssh command for tests for the cfg.
140func ConfigureGitalySSHBin(t testing.TB, cfg config.Cfg) {
141	buildBinary(t, cfg.BinDir, "gitaly-ssh")
142}
143
144func buildBinary(t testing.TB, dstDir, name string) {
145	// binsPath is a shared between all tests location where all compiled binaries should be placed
146	binsPath := filepath.Join(testDirectory, "bins")
147	// binPath is a path to a specific binary file
148	binPath := filepath.Join(binsPath, name)
149	// lockPath is a path to the special lock file used to prevent parallel build runs
150	lockPath := binPath + ".lock"
151
152	defer func() {
153		if !t.Failed() {
154			// copy compiled binary to the destination folder
155			require.NoError(t, os.MkdirAll(dstDir, os.ModePerm))
156			MustRunCommand(t, nil, "cp", binPath, dstDir)
157		}
158	}()
159
160	require.NoError(t, os.MkdirAll(binsPath, os.ModePerm))
161
162	lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL, 0600)
163	if err != nil {
164		if !errors.Is(err, os.ErrExist) {
165			require.FailNow(t, err.Error())
166		}
167		// another process is creating the binary at the moment, wait for it to complete (5s)
168		for i := 0; i < 50; i++ {
169			if _, err := os.Stat(binPath); err != nil {
170				if !errors.Is(err, os.ErrExist) {
171					require.NoError(t, err)
172				}
173				time.Sleep(100 * time.Millisecond)
174				continue
175			}
176			// binary was created
177			return
178		}
179		require.FailNow(t, "another process is creating binary for too long")
180	}
181	defer func() { require.NoError(t, os.Remove(lockPath)) }()
182	require.NoError(t, lockFile.Close())
183
184	if _, err := os.Stat(binPath); err != nil {
185		if !errors.Is(err, os.ErrNotExist) {
186			// something went wrong and for some reason the binary already exists
187			require.FailNow(t, err.Error())
188		}
189		buildCommand(t, binsPath, name)
190	}
191}
192
193func buildCommand(t testing.TB, outputDir, cmd string) {
194	if outputDir == "" {
195		log.Fatal("BinDir must be set")
196	}
197
198	goBuildArgs := []string{
199		"build",
200		"-tags", "static,system_libgit2",
201		"-o", filepath.Join(outputDir, cmd),
202		fmt.Sprintf("gitlab.com/gitlab-org/gitaly/v14/cmd/%s", cmd),
203	}
204	MustRunCommand(t, nil, "go", goBuildArgs...)
205}
206