Welcome to mirror list, hosted at ThFree Co, Russian Federation.

configure.go « testhelper « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: b93b5b804ca597f8d81741ed2f45461ab314198d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package testhelper

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"testing"

	log "github.com/sirupsen/logrus"
	gitalylog "gitlab.com/gitlab-org/gitaly/internal/log"
)

var testDirectory string

// RunOption is an option that can be passed to Run.
type RunOption func(*runConfig)

type runConfig struct {
	setup                  func() error
	disableGoroutineChecks bool
}

// WithSetup allows the caller of Run to pass a setup function that will be called after global
// test state has been configured.
func WithSetup(setup func() error) RunOption {
	return func(cfg *runConfig) {
		cfg.setup = setup
	}
}

// WithDisabledGoroutineChecker disables checking for leaked Goroutines after tests have run. This
// should ideally only be used as a temporary measure until all Goroutine leaks have been fixed.
//
// Deprecated: This should not be used, but instead you should try to fix all Goroutine leakages.
func WithDisabledGoroutineChecker() RunOption {
	return func(cfg *runConfig) {
		cfg.disableGoroutineChecks = true
	}
}

// Run sets up required testing state and executes the given test suite. It can optionally receive a
// variable number of RunOptions.
func Run(m *testing.M, opts ...RunOption) {
	// Run tests in a separate function such that we can use deferred statements and still
	// (indirectly) call `os.Exit()` in case the test setup failed.
	if err := func() error {
		var cfg runConfig
		for _, opt := range opts {
			opt(&cfg)
		}

		defer mustHaveNoChildProcess()
		if !cfg.disableGoroutineChecks {
			defer mustHaveNoGoroutines()
		}

		cleanup, err := configure()
		if err != nil {
			return fmt.Errorf("test configuration: %w", err)
		}
		defer cleanup()

		if cfg.setup != nil {
			if err := cfg.setup(); err != nil {
				return fmt.Errorf("error calling setup function: %w", err)
			}
		}

		m.Run()

		return nil
	}(); err != nil {
		fmt.Printf("%s", err)
		os.Exit(1)
	}
}

// configure sets up the global test configuration. On failure,
// terminates the program.
func configure() (_ func(), returnedErr error) {
	gitalylog.Configure(gitalylog.Loggers, "json", "panic")

	for key, value := range map[string]string{
		// We inject the following two variables, which instruct Git to search its
		// configuration in non-default locations in the global and system scope. This is
		// done as a sanity check to verify that we don't ever pick up this configuration
		// but instead filter it out whenever we execute Git. The values are set to the
		// root directory: this directory is guaranteed to exist, and Git is guaranteed to
		// fail parsing those directories as configuration.
		"GIT_CONFIG_GLOBAL": "/",
		"GIT_CONFIG_SYSTEM": "/",
		// Same as above, this value is injected such that we can detect whether any site
		// which executes Git fails to inject the expected Git directory. This is also
		// required such that we don't ever inadvertently execute Git commands in the wrong
		// Git repository.
		"GIT_DIR": "/dev/null",
	} {
		if err := os.Setenv(key, value); err != nil {
			return nil, err
		}
	}

	cleanup, err := configureTestDirectory()
	if err != nil {
		return nil, fmt.Errorf("configuring test directory: %w", err)
	}
	defer func() {
		if returnedErr != nil {
			cleanup()
		}
	}()

	return cleanup, nil
}

func configureTestDirectory() (_ func(), returnedErr error) {
	if testDirectory != "" {
		return nil, errors.New("test directory has already been configured")
	}

	// Ideally, we'd just pass "" to `os.MkdirTemp()`, which would then use either the value of
	// `$TMPDIR` or alternatively "/tmp". But given that macOS sets `$TMPDIR` to a user specific
	// temporary directory, resulting paths would be too long and thus cause issues galore. We
	// thus support our own specific variable instead which allows users to override it, with
	// our default being "/tmp".
	tempDirLocation := os.Getenv("TEST_TMP_DIR")
	if tempDirLocation == "" {
		tempDirLocation = "/tmp"
	}

	var err error
	testDirectory, err = os.MkdirTemp(tempDirLocation, "gitaly-")
	if err != nil {
		return nil, fmt.Errorf("creating test directory: %w", err)
	}

	cleanup := func() {
		if err := os.RemoveAll(testDirectory); err != nil {
			log.Errorf("cleaning up test directory: %v", err)
		}
	}
	defer func() {
		if returnedErr != nil {
			cleanup()
		}
	}()

	// macOS symlinks /tmp/ to /private/tmp/ which can cause some check to fail. We thus resolve
	// the symlinks to their actual location.
	testDirectory, err = filepath.EvalSymlinks(testDirectory)
	if err != nil {
		return nil, err
	}

	// In many locations throughout Gitaly, we create temporary files and directories. By
	// default, these would clutter the "real" temporary directory with useless cruft that stays
	// around after our tests. To avoid this, we thus set the TMPDIR environment variable to
	// point into a directory inside of out test directory.
	globalTempDir := filepath.Join(testDirectory, "tmp")
	if err := os.Mkdir(globalTempDir, 0o755); err != nil {
		return nil, fmt.Errorf("creating global temporary directory: %w", err)
	}
	if err := os.Setenv("TMPDIR", globalTempDir); err != nil {
		return nil, fmt.Errorf("setting global temporary directory: %w", err)
	}

	return cleanup, nil
}