-
Notifications
You must be signed in to change notification settings - Fork 3
/
utils_real_test.go
224 lines (185 loc) · 8.05 KB
/
utils_real_test.go
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
//go:build !gowslmock
// This file contains the implementation of testutils geared towards the real back-end.
package gowsl_test
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/ubuntu/decorate"
wsl "github.com/ubuntu/gowsl"
wslmock "github.com/ubuntu/gowsl/mock"
)
// installDistro installs using powershell to decouple the tests from Distro.Register
// CommandContext often fails to stop it, so a more aggressive approach is taken by rebooting WSL.
//
//nolint:revive // No, I wont' put the context before the *testing.T.//nolint:revive
func installDistro(t *testing.T, ctx context.Context, distroName, location, rootfs string) {
t.Helper()
defer wslExeGuard(2 * time.Minute)()
cmd := fmt.Sprintf("$env:WSL_UTF8=1 ; wsl --import %q %q %q", distroName, location, rootfs)
//nolint:gosec // Code injection is not a concern in tests.
out, err := exec.Command("powershell.exe", "-Command", cmd).CombinedOutput()
require.NoErrorf(t, err, "Setup: failed to register %q: %s", distroName, out)
}
// uninstallDistro checks if a distro exists and if it does, it unregisters it.
func uninstallDistro(distro wsl.Distro, allowShutdown bool) (err error) {
defer decorate.OnError(&err, "could not uninstall %q", distro.Name())
if r, err := distro.IsRegistered(); err == nil && !r {
return nil
}
unregisterCmd := fmt.Sprintf("$env:WSL_UTF8=1 ; wsl.exe --unregister %q", distro.Name())
defer wslExeGuard(2 * time.Minute)()
// 1. Attempt unregistering
//nolint:gosec // Code injection is not a concern in tests.
e := exec.Command("powershell.exe", "-Command", unregisterCmd).Run()
if e == nil {
return nil
}
// Failed unregistration
err = errors.Join(err, fmt.Errorf("could not unregister: %v", e))
// 2. Attempt terminate, then unregister
cmd := fmt.Sprintf("$env:WSL_UTF8=1 ; wsl.exe --terminate %q", distro.Name())
if out, e := exec.Command("powershell.exe", "-Command", cmd).CombinedOutput(); e != nil { //nolint:gosec // Code injection is not a concern in tests.
// Failed to terminate
err = errors.Join(err, fmt.Errorf("could not terminate after failing to unregister: %v. Output: %s", e, string(out)))
} else {
// Terminated, retry unregistration
out, e := exec.Command("powershell.exe", "-Command", unregisterCmd).CombinedOutput() //nolint:gosec // Code injection is not a concern in tests.
if e != nil {
return nil
}
// Failed unregistration
err = errors.Join(err, fmt.Errorf("could not unregister after terminating: %v. Output: %s", e, string(out)))
}
if !allowShutdown {
return err
}
// 3. Attempt shutdown, then unregister
fmt.Fprintf(os.Stderr, "Could not unregister %q, shutting down WSL and retrying.", distro.Name())
if out, e := exec.Command("powershell.exe", "-Command", "$env:WSL_UTF8=1 ; wsl.exe --shutdown").CombinedOutput(); e != nil {
// Failed to shut down WSL
return errors.Join(err, fmt.Errorf("could not shut down WSL after failing to unregister: %v. Output: %s", e, string(out)))
}
// WSL has been shut down, retry unregistration
out, e := exec.Command("powershell.exe", "-Command", unregisterCmd).Output() //nolint:gosec // Code injection is not a concern in tests.
if e != nil {
// Failed unregistration
return errors.Join(err, fmt.Errorf("could not unregister after shutdown: %v\nOutput: %v", e, string(out)))
}
// Success
return nil
}
// testDistros finds all distros with a mangled name.
func registeredDistros(ctx context.Context) (distros []wsl.Distro, err error) {
defer wslExeGuard(5 * time.Second)()
outp, err := exec.Command("powershell.exe", "-Command", "$env:WSL_UTF8=1 ; wsl.exe --list --quiet --all").Output()
if err != nil {
return distros, err
}
for _, line := range strings.Fields(string(outp)) {
distros = append(distros, wsl.NewDistro(ctx, line))
}
return distros, err
}
// defaultDistro gets the default distro's name via wsl.exe to bypass wsl.DefaultDistro in order to
// better decouple tests.
func defaultDistro(ctx context.Context) (string, bool, error) {
defer wslExeGuard(5 * time.Second)()
out, err := exec.Command("powershell.exe", "-Command", "$env:WSL_UTF8=1; wsl.exe --list --verbose").CombinedOutput()
if err != nil {
if target := (&exec.ExitError{}); !errors.As(err, &target) {
return "", false, fmt.Errorf("failed to find current default distro: %v", err)
}
// cannot read from target.StdErr because message is printed to Stdout
if !strings.Contains(string(out), "Wsl/WSL_E_DEFAULT_DISTRO_NOT_FOUND") {
return "", false, fmt.Errorf("failed to find current default distro: %v. Output: %s", err, out)
}
return "", false, nil // No distros installed: no default
}
s := bufio.NewScanner(bytes.NewReader(out))
s.Scan() // Ignore first line (table header)
for s.Scan() {
line := s.Text()
if !strings.HasPrefix(line, "*") {
continue
}
data := strings.Fields(line)
if len(data) < 2 {
return "", false, fmt.Errorf("failed to parse 'wsl.exe --list --verbose' output, line %q", line)
}
return data[1], true, nil
}
if err := s.Err(); err != nil {
return "", false, err
}
// No distro is default (but some exist, likely in the process of being installed/unistalled)
return "", false, nil
}
// setDefaultDistro sets a distro as default using Powershell.
func setDefaultDistro(ctx context.Context, distroName string) error {
defer wslExeGuard(5 * time.Second)()
// No threat of code injection, wsl.exe will only interpret this text as a distro name
// and throw ErrNotExist if it does not exist.
out, err := exec.Command("wsl.exe", "--set-default", distroName).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to set distro %q back as default: %v. Output: %s", distroName, err, out)
}
return nil
}
// wslExeGuard guards against common problems with wsl.exe, and should be called every time
// wsl.exe is used. It solves:
// - Trying to use it from WSL can have unexpected results, so it panics when not on Windows.
// - wsl.exe occasionally freezing: sometimes, for no apparent reason, wsl.exe stops responding,
// and cancelling the context of the command is not enough to unfreeze it. The only known
// workaround is to call `wsl --shutdown` from elsewhere.
//
// This function does just that when the timeout is exceeded.
func wslExeGuard(timeout time.Duration) (cancel func()) {
if runtime.GOOS != "windows" {
panic("You must use the mock back-end when not running on Windows")
}
gentleTimeout := time.AfterFunc(timeout, func() {
fmt.Fprintf(os.Stderr, "wslExec guard triggered, shutting WSL down")
_ = exec.Command("powershell.exe", "-Command", "$env:WSL_UTF8=1 ; wsl.exe --shutdown").Run()
})
panicTimeout := time.AfterFunc(timeout+30*time.Second, func() {
panic("WSL froze and couldn't be stopped. Tests aborted.")
})
return func() {
gentleTimeout.Stop()
panicTimeout.Stop()
}
}
// setupBackend is a convenience function that allows tests to build both with the production
// and mock back-ends, and take appropriate measures to make it work at runtime. Thus, its
// behaviour is different depending on the back-end.
//
// # Production back-end
//
// Any test that manipulates the mock needs the mock back-end to be accessible. setupBackend therefore does nothing,
// except return the same context that was passed, plus the modifyMock function. Attempting to call this function
// means that we need the mock back-end, so tests that call this function are skipped.
//
// # Mock back-end
//
// This module's only statefulness comes from the state of the registry. We're initializing a new back-end,
// therefore the state is not shared with any other tests. Hence, the current test can be marked parallel.
// The returned context contains the mock, and the returned function passes the mock to the supplied closure.
//
//nolint:revive // I'll put t before ctx, thank you.
func setupBackend(t *testing.T, ctx context.Context) (outCtx context.Context, modifyMock func(t *testing.T, f func(m *wslmock.Backend))) {
t.Helper()
return ctx, func(t *testing.T, f func(*wslmock.Backend)) {
t.Helper()
t.Skip("This test is only available with the mock enabled")
}
}