Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pam: Fix races handling in VHS tests and fix bubbletea models implementations #846

Merged
merged 23 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
10f0a14
pam/integration-tests/vhs-helpers: Fix handing of data races
3v1n0 Mar 18, 2025
e7ab3a3
examplebroker: Do not access to exampleUsers map without a lock
3v1n0 Mar 19, 2025
81161b6
testutils/daemon: Add option to save the output log to a file path
3v1n0 Mar 4, 2025
d3d4cd2
pam/integration-tests/helpers: Save the authd log file as artifact
3v1n0 Mar 4, 2025
acfda1c
testutils/daemon: Do not use the test parameter when sharing the daemon
3v1n0 Mar 18, 2025
6fbfebd
pam/newpassword: Create the entries using the helper function
3v1n0 Mar 18, 2025
3142383
pam/gdmmodel-test: Increase tests timeout
3v1n0 Mar 19, 2025
f41597d
pam/gdmmodel-test: Clarify in logs if timeout happened
3v1n0 Feb 27, 2025
b9fe60e
pam/gdmmodel-test: Use tests sleeps multiplier for timeout
3v1n0 Feb 27, 2025
aade61c
pam/pam-client-dummy: Use SleepMultiplier to compute the wait time
3v1n0 Mar 7, 2025
5628bbf
testutils/args: Ignore build type if AUTHD_TESTS_SLEEP_MULTIPLIER is set
3v1n0 Feb 27, 2025
aaa6f60
ci: Set the tests sleeps multiplier as env variable
3v1n0 Feb 27, 2025
bd7a90e
pam/gdmmodel: Do not use unneeded pointer receivers
3v1n0 Mar 11, 2025
28afdc3
pam/authentication: Don't use pointer-receivers for Update functions
3v1n0 Mar 18, 2025
8619c2c
pam/model: Do not use a pointer receiver for the UIModel Update
3v1n0 Mar 19, 2025
eb5e20a
pam/model: Move the exit status ownership to the appState caller
3v1n0 Mar 19, 2025
d69ecfd
pam/models: Do not use pointer receivers in focus-related methods
3v1n0 Mar 19, 2025
b9e2a8c
pam/integration-tests: Do not skip tests with bubbletea races
3v1n0 Mar 19, 2025
54ea0cf
pam/nativemodel: Split the model creation and initialization
3v1n0 Mar 19, 2025
3ff4a22
pam/model: Implement the UIModel as proper bubbletea tea.Model
3v1n0 Mar 19, 2025
ea51798
pam/model: Make the uiModel private and only expose it as a tea.Model
3v1n0 Mar 19, 2025
4c86ccb
pam/adapter: Avoid using pointer receivers for Init functions
3v1n0 Mar 19, 2025
dd3f797
pam/newpasswordmodel: Do not ignore the potential Focus command on clear
3v1n0 Mar 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ jobs:
if: matrix.test == 'race'
env:
GO_TESTS_TIMEOUT: 35m
AUTHD_TESTS_SLEEP_MULTIPLIER: 3
run: |
go test -json -timeout ${GO_TESTS_TIMEOUT} -race ./... | \
gotestfmt --logfile "${AUTHD_TEST_ARTIFACTS_PATH}/gotestfmt.race.log"
Expand All @@ -266,6 +267,7 @@ jobs:
CGO_CFLAGS: "-O0 -g3 -fno-omit-frame-pointer"
G_DEBUG: "fatal-criticals"
GO_TESTS_TIMEOUT: 30m
AUTHD_TESTS_SLEEP_MULTIPLIER: 1.5
# Use these flags to give ASAN a better time to unwind the stack trace
GO_GC_FLAGS: -N -l
run: |
Expand Down
3 changes: 3 additions & 0 deletions examplebroker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@ func (b *Broker) UserPreCheck(ctx context.Context, username string) (string, err
strings.Contains(username, fmt.Sprintf("-%s-", UserIntegrationPreCheckValue)) {
return userInfoFromName(username), nil
}

exampleUsersMu.Lock()
defer exampleUsersMu.Unlock()
if _, exists := exampleUsers[username]; !exists {
return "", fmt.Errorf("user %q does not exist", username)
}
Expand Down
1 change: 1 addition & 0 deletions internal/testutils/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func SleepMultiplier() float64 {
if sleepMultiplier <= 0 {
panic("Negative or 0 sleep multiplier is not supported")
}
return
}

if IsAsan() {
Expand Down
49 changes: 46 additions & 3 deletions internal/testutils/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package testutils
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
Expand All @@ -24,6 +25,8 @@ type daemonOptions struct {
existentDB string
socketPath string
pidFile string
outputFile string
shared bool
env []string
}

Expand Down Expand Up @@ -66,6 +69,20 @@ func WithPidFile(pidFile string) DaemonOption {
}
}

// WithOutputFile sets the path where the process log will be saved.
func WithOutputFile(outputFile string) DaemonOption {
return func(o *daemonOptions) {
o.outputFile = outputFile
}
}

// WithSharedDaemon sets whether the daemon is shared between tests.
func WithSharedDaemon(shared bool) DaemonOption {
return func(o *daemonOptions) {
o.shared = shared
}
}

// RunDaemon runs the daemon in a separate process and returns the socket path and a channel that will be closed when
// the daemon stops.
func RunDaemon(ctx context.Context, t *testing.T, execPath string, args ...DaemonOption) (socketPath string, stopped chan struct{}) {
Expand Down Expand Up @@ -134,16 +151,42 @@ paths:
if opts.pidFile != "" {
processPid <- cmd.Process.Pid
}

// When using a shared daemon we should not use the test parameter from now on
// since the test is referring to may not be the one actually running.
t := t
logger := t.Logf
errorIs := func(err, target error, format string, args ...any) {
require.ErrorIs(t, err, target, fmt.Sprintf(format, args...))
}
if opts.shared {
// Unset the testing value, since it's wrong to use it from!
t = nil
logger = func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }
errorIs = func(err, target error, format string, args ...any) {
if errors.Is(err, target) {
return
}
panic(fmt.Sprintf("Error %v is not matching %v: %s", err, target, fmt.Sprintf(format, args...)))
}
}

err = cmd.Wait()
out := b.Bytes()
require.ErrorIs(t, err, context.Canceled, "Setup: daemon stopped unexpectedly: %s", out)
if opts.outputFile != "" {
logger("writing authd log files to %v", opts.outputFile)
if err := os.WriteFile(opts.outputFile, out, 0600); err != nil {
logger("TearDown: failed to save output file %q: %v", opts.outputFile, err)
}
}
errorIs(err, context.Canceled, "Setup: daemon stopped unexpectedly: %s", out)
if opts.pidFile != "" {
defer cancel(nil)
if err := os.Remove(opts.pidFile); err != nil {
t.Logf("TearDown: failed to remove pid file %q: %v", opts.pidFile, err)
logger("TearDown: failed to remove pid file %q: %v", opts.pidFile, err)
}
}
t.Logf("Daemon stopped (%v)\n ##### Output #####\n %s \n ##### END #####", err, out)
logger("Daemon stopped (%v)\n ##### Output #####\n %s \n ##### END #####", err, out)
}()

conn, err := grpc.NewClient("unix://"+opts.socketPath, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithUnaryInterceptor(errmessages.FormatErrorMessage))
Expand Down
9 changes: 8 additions & 1 deletion pam/integration-tests/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,18 @@ func runAuthdForTesting(t *testing.T, gpasswdOutput, groupsFile string, currentU
t.Helper()

ctx, cancel := context.WithCancel(context.Background())

env := localgroupstestutils.AuthdIntegrationTestsEnvWithGpasswdMock(t, gpasswdOutput, groupsFile)
if currentUserAsRoot {
env = append(env, authdCurrentUserRootEnvVariableContent)
}
args = append(args, testutils.WithEnvironment(env...))

outputFile := filepath.Join(t.TempDir(), "authd.log")
args = append(args, testutils.WithOutputFile(outputFile))

socketPath, stopped := testutils.RunDaemon(ctx, t, daemonPath, args...)
saveArtifactsForDebugOnCleanup(t, []string{outputFile})
return socketPath, func() {
cancel()
<-stopped
Expand Down Expand Up @@ -121,7 +127,8 @@ func sharedAuthd(t *testing.T) (socketPath string, gpasswdFile string) {

sa.gPasswdOutputPath = filepath.Join(t.TempDir(), "gpasswd.output")
sa.groupsFile = filepath.Join(testutils.TestFamilyPath(t), "gpasswd.group")
sa.socketPath, sa.cleanup = runAuthdForTesting(t, sa.gPasswdOutputPath, sa.groupsFile, true)
sa.socketPath, sa.cleanup = runAuthdForTesting(t, sa.gPasswdOutputPath,
sa.groupsFile, true, testutils.WithSharedDaemon(true))
return sa.socketPath, sa.gPasswdOutputPath
}

Expand Down
50 changes: 29 additions & 21 deletions pam/integration-tests/vhs-helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package main_test

import (
"errors"
"fmt"
"maps"
"math"
Expand Down Expand Up @@ -260,14 +259,13 @@ func (td tapeData) RunVhs(t *testing.T, testType vhsTestType, outDir string, cli
var raceLog string
if testutils.IsRace() {
raceLog = filepath.Join(t.TempDir(), "gorace.log")
cmd.Env = append(cmd.Env, fmt.Sprintf("GORACE=log_path=%s", raceLog))
saveArtifactsForDebugOnCleanup(t, []string{raceLog})
cmd.Env = append(cmd.Env, fmt.Sprintf("GORACE=log_path=%s exitcode=0", raceLog))
}

cmd.Args = append(cmd.Args, td.PrepareTape(t, testType, outDir))
out, err := cmd.CombinedOutput()
if err != nil {
checkDataRace(t, raceLog)
if raceLog != "" {
checkDataRaces(t, raceLog)
}

isSSHError := func(processOut []byte) bool {
Expand Down Expand Up @@ -324,35 +322,45 @@ func (td tapeData) Output() string {
return txt
}

func checkDataRace(t *testing.T, raceLog string) {
func checkDataRaces(t *testing.T, raceLog string) {
t.Helper()

if !testutils.IsRace() || raceLog == "" {
if !testutils.IsRace() {
return
}

content, err := os.ReadFile(raceLog)
if err == nil || errors.Is(err, os.ErrNotExist) {
return
var raceLogs []string
err := filepath.Walk(filepath.Dir(raceLog),
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !strings.HasPrefix(info.Name(), filepath.Base(raceLog)) {
return nil
}
t.Logf("Found data race %s", info.Name())
raceLogs = append(raceLogs, filepath.Join(filepath.Dir(raceLog), info.Name()))
return nil
})

require.NoError(t, err, "TearDown: Check for races")
saveArtifactsForDebugOnCleanup(t, raceLogs)
for _, raceLog := range raceLogs {
checkDataRace(t, raceLog)
}
}

func checkDataRace(t *testing.T, raceLog string) {
t.Helper()

content, err := os.ReadFile(raceLog)
require.NoError(t, err, "TearDown: Error reading race log %q", raceLog)

out := string(content)
if strings.TrimSpace(out) == "" {
return
}

if strings.Contains(out, "bubbles/cursor.(*Model).BlinkCmd.func1") {
// FIXME: This is a well known race of bubble tea:
// https://github.com/charmbracelet/bubbletea/issues/909
// We can't do much here, as the workaround will likely affect the
// GUI behavior, but we ignore this since it's definitely not our bug.

// TODO: In case other races are detected, we should still fail here.
t.Skipf("This is a very well known bubble tea bug (#909), ignoring it:\n%s", out)
return
}

t.Fatalf("Got a GO Race on vhs child:\n%s", out)
}

Expand Down
Loading
Loading