6
6
"fmt"
7
7
"io"
8
8
"os"
9
+ "os/exec"
9
10
"os/signal"
10
11
"time"
11
12
@@ -21,36 +22,43 @@ const (
21
22
cleanerRespOK = "OK"
22
23
envCleanerParentUUID = "DB_CLEANER_PARENT_UUID"
23
24
envCleanerDSN = "DB_CLEANER_DSN"
24
- )
25
-
26
- var (
27
- originalWorkingDir string
28
- errGettingWorkingDir error
25
+ envCleanerMagic = "DB_CLEANER_MAGIC"
26
+ envCleanerMagicValue = "XEHdJqWehWek8AaWwopy" // 20 random characters to make this collision resistant
29
27
)
30
28
31
29
func init () {
32
- // We expect our tests to run from somewhere in the project tree where `go run` below in `startCleaner` will
33
- // be able to resolve the command package. However, some of the tests modify the working directory during the run.
34
- // So, we grab the working directory during package init, before tests are run, and then set that work dir on the
35
- // subcommand process before it starts.
36
- originalWorkingDir ,errGettingWorkingDir = os .Getwd ()
30
+ // We are hijacking the init() function here to do something very non-standard.
31
+ //
32
+ // We want to be able to run the cleaner as a subprocess of the test process so that it can outlive the test binary
33
+ // and still clean up, even if the test process times out or is killed. So, what we do is in startCleaner() below,
34
+ // which is called in the parent process, we exec our own binary and set a collision-resistant environment variable.
35
+ // Then here in the init(), which will run before main() and therefore before executing tests, we check for the
36
+ // environment variable, and if present we know this is the child process and we exec the cleaner. Instead of
37
+ // returning normally from init() we call os.Exit(). This prevents tests from being re-run in the child process (and
38
+ // recursion).
39
+ //
40
+ // If the magic value is not present, we know we are the parent process and init() returns normally.
41
+ magicValue := os .Getenv (envCleanerMagic )
42
+ if magicValue == envCleanerMagicValue {
43
+ RunCleaner ()
44
+ os .Exit (0 )
45
+ }
37
46
}
38
47
39
48
// startCleaner starts the cleaner in a subprocess. holdThis is an opaque reference that needs to be kept from being
40
49
// garbage collected until we are done with all test databases (e.g. the end of the process).
41
- func startCleaner (ctx context.Context ,t TBSubset ,parentUUID uuid.UUID ,dsn string ) (holdThis any ,err error ) {
42
- cmd := cleanerCmd (t )
50
+ func startCleaner (ctx context.Context ,_ TBSubset ,parentUUID uuid.UUID ,dsn string ) (holdThis any ,err error ) {
51
+ bin ,err := os .Executable ()
52
+ if err != nil {
53
+ return nil ,xerrors .Errorf ("could not get executable path: %w" ,err )
54
+ }
55
+ cmd := exec .Command (bin )
43
56
cmd .Env = append (os .Environ (),
44
57
fmt .Sprintf ("%s=%s" ,envCleanerParentUUID ,parentUUID .String ()),
45
58
fmt .Sprintf ("%s=%s" ,envCleanerDSN ,dsn ),
59
+ fmt .Sprintf ("%s=%s" ,envCleanerMagic ,envCleanerMagicValue ),
46
60
)
47
61
48
- // c.f. comment on `func init()` in this file.
49
- if errGettingWorkingDir != nil {
50
- return nil ,xerrors .Errorf ("failed to get working directory during init: %w" ,errGettingWorkingDir )
51
- }
52
- cmd .Dir = originalWorkingDir
53
-
54
62
// Here we don't actually use the reference to the stdin pipe, because we never write anything to it. When this
55
63
// process exits, the pipe is closed by the OS and this triggers the cleaner to do its cleaning work. But, we do
56
64
// need to hang on to a reference to it so that it doesn't get garbage collected and trigger cleanup early.
@@ -177,8 +185,7 @@ func (c *cleaner) waitAndClean() {
177
185
}
178
186
179
187
// RunCleaner runs the test database cleaning process. It takes no arguments but uses stdio and environment variables
180
- // for its operation. It is designed to be launched as the only task of a `main()` process, but is included in this
181
- // package to share constants with the parent code that launches it above.
188
+ // for its operation.
182
189
//
183
190
// The cleaner is designed to run in a separate process from the main test suite, connected over stdio. If the main test
184
191
// process ends (panics, times out, or is killed) without explicitly discarding the databases it clones, the cleaner