Skip to content

Commit 64510ee

Browse files
authored
pam: Fix races handling in VHS tests and fix bubbletea models implementations (#846)
See commits for details, but basically lots of cleanups to ensure that the races are properly monitored and that the artifacts saved in CI, then I decided to tackle the root cause of charmbracelet/bubbletea#909 that was mostly due to the fact that we were, in various cases, updating the models with pointer receivers, instead of relying on the returned and modified value. Coming with various cleanups. UDENG-6474
2 parents 5846129 + dd3f797 commit 64510ee

22 files changed

+304
-195
lines changed

.github/workflows/qa.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ jobs:
254254
if: matrix.test == 'race'
255255
env:
256256
GO_TESTS_TIMEOUT: 35m
257+
AUTHD_TESTS_SLEEP_MULTIPLIER: 3
257258
run: |
258259
go test -json -timeout ${GO_TESTS_TIMEOUT} -race ./... | \
259260
gotestfmt --logfile "${AUTHD_TEST_ARTIFACTS_PATH}/gotestfmt.race.log"
@@ -266,6 +267,7 @@ jobs:
266267
CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer"
267268
G_DEBUG: "fatal-criticals"
268269
GO_TESTS_TIMEOUT: 30m
270+
AUTHD_TESTS_SLEEP_MULTIPLIER: 1.5
269271
# Use these flags to give ASAN a better time to unwind the stack trace
270272
GO_GC_FLAGS: -N -l
271273
run: |

examplebroker/broker.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,9 @@ func (b *Broker) UserPreCheck(ctx context.Context, username string) (string, err
843843
strings.Contains(username, fmt.Sprintf("-%s-", UserIntegrationPreCheckValue)) {
844844
return userInfoFromName(username), nil
845845
}
846+
847+
exampleUsersMu.Lock()
848+
defer exampleUsersMu.Unlock()
846849
if _, exists := exampleUsers[username]; !exists {
847850
return "", fmt.Errorf("user %q does not exist", username)
848851
}

internal/testutils/args.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func SleepMultiplier() float64 {
7979
if sleepMultiplier <= 0 {
8080
panic("Negative or 0 sleep multiplier is not supported")
8181
}
82+
return
8283
}
8384

8485
if IsAsan() {

internal/testutils/daemon.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package testutils
33
import (
44
"bytes"
55
"context"
6+
"errors"
67
"fmt"
78
"os"
89
"os/exec"
@@ -24,6 +25,8 @@ type daemonOptions struct {
2425
existentDB string
2526
socketPath string
2627
pidFile string
28+
outputFile string
29+
shared bool
2730
env []string
2831
}
2932

@@ -66,6 +69,20 @@ func WithPidFile(pidFile string) DaemonOption {
6669
}
6770
}
6871

72+
// WithOutputFile sets the path where the process log will be saved.
73+
func WithOutputFile(outputFile string) DaemonOption {
74+
return func(o *daemonOptions) {
75+
o.outputFile = outputFile
76+
}
77+
}
78+
79+
// WithSharedDaemon sets whether the daemon is shared between tests.
80+
func WithSharedDaemon(shared bool) DaemonOption {
81+
return func(o *daemonOptions) {
82+
o.shared = shared
83+
}
84+
}
85+
6986
// RunDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when
7087
// the daemon stops.
7188
func RunDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) {
@@ -134,16 +151,42 @@ paths:
134151
if opts.pidFile != "" {
135152
processPid <- cmd.Process.Pid
136153
}
154+
155+
// When using a shared daemon we should not use the test parameter from now on
156+
// since the test is referring to may not be the one actually running.
157+
t := t
158+
logger := t.Logf
159+
errorIs := func(err, target error, format string, args ...any) {
160+
require.ErrorIs(t, err, target, fmt.Sprintf(format, args...))
161+
}
162+
if opts.shared {
163+
// Unset the testing value, since it's wrong to use it from!
164+
t = nil
165+
logger = func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }
166+
errorIs = func(err, target error, format string, args ...any) {
167+
if errors.Is(err, target) {
168+
return
169+
}
170+
panic(fmt.Sprintf("Error %v is not matching %v: %s", err, target, fmt.Sprintf(format, args...)))
171+
}
172+
}
173+
137174
err = cmd.Wait()
138175
out := b.Bytes()
139-
require.ErrorIs(t, err, context.Canceled, "Setup: daemon stopped unexpectedly: %s", out)
176+
if opts.outputFile != "" {
177+
logger("writing authd log files to %v", opts.outputFile)
178+
if err := os.WriteFile(opts.outputFile, out, 0600); err != nil {
179+
logger("TearDown: failed to save output file %q: %v", opts.outputFile, err)
180+
}
181+
}
182+
errorIs(err, context.Canceled, "Setup: daemon stopped unexpectedly: %s", out)
140183
if opts.pidFile != "" {
141184
defer cancel(nil)
142185
if err := os.Remove(opts.pidFile); err != nil {
143-
t.Logf("TearDown: failed to remove pid file %q: %v", opts.pidFile, err)
186+
logger("TearDown: failed to remove pid file %q: %v", opts.pidFile, err)
144187
}
145188
}
146-
t.Logf("Daemon stopped (%v)\n ##### Output #####\n %s \n ##### END #####", err, out)
189+
logger("Daemon stopped (%v)\n ##### Output #####\n %s \n ##### END #####", err, out)
147190
}()
148191

149192
conn, err := grpc.NewClient("unix://"+opts.socketPath, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(errmessages.FormatErrorMessage))

pam/integration-tests/helpers_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,18 @@ func runAuthdForTesting(t *testing.T, gpasswdOutput, groupsFile string, currentU
5252
t.Helper()
5353

5454
ctx, cancel := context.WithCancel(context.Background())
55+
5556
env := localgroupstestutils.AuthdIntegrationTestsEnvWithGpasswdMock(t, gpasswdOutput, groupsFile)
5657
if currentUserAsRoot {
5758
env = append(env, authdCurrentUserRootEnvVariableContent)
5859
}
5960
args = append(args, testutils.WithEnvironment(env...))
61+
62+
outputFile := filepath.Join(t.TempDir(), "authd.log")
63+
args = append(args, testutils.WithOutputFile(outputFile))
64+
6065
socketPath, stopped := testutils.RunDaemon(ctx, t, daemonPath, args...)
66+
saveArtifactsForDebugOnCleanup(t, []string{outputFile})
6167
return socketPath, func() {
6268
cancel()
6369
<-stopped
@@ -121,7 +127,8 @@ func sharedAuthd(t *testing.T) (socketPath string, gpasswdFile string) {
121127

122128
sa.gPasswdOutputPath = filepath.Join(t.TempDir(), "gpasswd.output")
123129
sa.groupsFile = filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group")
124-
sa.socketPath, sa.cleanup = runAuthdForTesting(t, sa.gPasswdOutputPath, sa.groupsFile, true)
130+
sa.socketPath, sa.cleanup = runAuthdForTesting(t, sa.gPasswdOutputPath,
131+
sa.groupsFile, true, testutils.WithSharedDaemon(true))
125132
return sa.socketPath, sa.gPasswdOutputPath
126133
}
127134

pam/integration-tests/vhs-helpers_test.go

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main_test
22

33
import (
4-
"errors"
54
"fmt"
65
"maps"
76
"math"
@@ -260,14 +259,13 @@ func (td tapeData) RunVhs(t *testing.T, testType vhsTestType, outDir string, cli
260259
var raceLog string
261260
if testutils.IsRace() {
262261
raceLog = filepath.Join(t.TempDir(), "gorace.log")
263-
cmd.Env = append(cmd.Env, fmt.Sprintf("GORACE=log_path=%s", raceLog))
264-
saveArtifactsForDebugOnCleanup(t, []string{raceLog})
262+
cmd.Env = append(cmd.Env, fmt.Sprintf("GORACE=log_path=%s exitcode=0", raceLog))
265263
}
266264

267265
cmd.Args = append(cmd.Args, td.PrepareTape(t, testType, outDir))
268266
out, err := cmd.CombinedOutput()
269-
if err != nil {
270-
checkDataRace(t, raceLog)
267+
if raceLog != "" {
268+
checkDataRaces(t, raceLog)
271269
}
272270

273271
isSSHError := func(processOut []byte) bool {
@@ -324,35 +322,45 @@ func (td tapeData) Output() string {
324322
return txt
325323
}
326324

327-
func checkDataRace(t *testing.T, raceLog string) {
325+
func checkDataRaces(t *testing.T, raceLog string) {
328326
t.Helper()
329327

330-
if !testutils.IsRace() || raceLog == "" {
328+
if !testutils.IsRace() {
331329
return
332330
}
333331

334-
content, err := os.ReadFile(raceLog)
335-
if err == nil || errors.Is(err, os.ErrNotExist) {
336-
return
332+
var raceLogs []string
333+
err := filepath.Walk(filepath.Dir(raceLog),
334+
func(path string, info os.FileInfo, err error) error {
335+
if err != nil {
336+
return err
337+
}
338+
if !strings.HasPrefix(info.Name(), filepath.Base(raceLog)) {
339+
return nil
340+
}
341+
t.Logf("Found data race %s", info.Name())
342+
raceLogs = append(raceLogs, filepath.Join(filepath.Dir(raceLog), info.Name()))
343+
return nil
344+
})
345+
346+
require.NoError(t, err, "TearDown: Check for races")
347+
saveArtifactsForDebugOnCleanup(t, raceLogs)
348+
for _, raceLog := range raceLogs {
349+
checkDataRace(t, raceLog)
337350
}
351+
}
352+
353+
func checkDataRace(t *testing.T, raceLog string) {
354+
t.Helper()
355+
356+
content, err := os.ReadFile(raceLog)
338357
require.NoError(t, err, "TearDown: Error reading race log %q", raceLog)
339358

340359
out := string(content)
341360
if strings.TrimSpace(out) == "" {
342361
return
343362
}
344363

345-
if strings.Contains(out, "bubbles/cursor.(*Model).BlinkCmd.func1") {
346-
// FIXME: This is a well known race of bubble tea:
347-
// https://github.com/charmbracelet/bubbletea/issues/909
348-
// We can't do much here, as the workaround will likely affect the
349-
// GUI behavior, but we ignore this since it's definitely not our bug.
350-
351-
// TODO: In case other races are detected, we should still fail here.
352-
t.Skipf("This is a very well known bubble tea bug (#909), ignoring it:\n%s", out)
353-
return
354-
}
355-
356364
t.Fatalf("Got a GO Race on vhs child:\n%s", out)
357365
}
358366

0 commit comments

Comments
 (0)