28d5043eb0
CI / Lint (pull_request) Successful in 31s
CI / Build (linux/amd64) (pull_request) Successful in 20s
CI / Build (linux/arm64) (pull_request) Successful in 19s
CI / Test (linux/amd64) (pull_request) Successful in 1m27s
CI / Build (windows/amd64) (pull_request) Successful in 1m34s
The CI runs go test with -race; the agent runner has two pump goroutines (pumpStdout + pumpStderr) writing through the sender concurrently, and the unprotected fakeSender slice append raced. The cancel_test had a local 'safeSender' workaround for the same issue; promote that mutex onto fakeSender itself so every test in the package is race-clean without per-test variants. - fakeSender grows mu sync.Mutex; Send takes/releases. New snapshot() helper for tests that want a stable copy. - cancel_test drops its local safeSender + sync import; uses fakeSender. Verified: go test -race ./... passes across all packages.
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
|
|
)
|
|
|
|
// (fakeSender is defined in runner_test.go; it's already lock-protected
|
|
// because the runner's stdout + stderr pump goroutines call Send
|
|
// concurrently. The original local 'safeSender' here was a workaround
|
|
// from before fakeSender itself grew the mutex.)
|
|
|
|
// TestRunBackupCanceledMidRunReportsCanceled spawns a backup against
|
|
// a fake restic that sleeps for 30 seconds, cancels the context after
|
|
// a short delay, and confirms the resulting job.finished envelope
|
|
// reports status=canceled (not failed).
|
|
func TestRunBackupCanceledMidRunReportsCanceled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Fake restic: replace the shell with a long sleep via `exec` so the
|
|
// process tree is one process — SIGTERM goes directly to sleep and
|
|
// it exits. Without `exec`, the shell stays in the foreground while
|
|
// sleep is its child; SIGTERM-to-shell may or may not propagate to
|
|
// sleep depending on the shell, leading to the WaitDelay-then-
|
|
// SIGKILL fallback path firing — slower and noisier.
|
|
bin := setupScript(t, `exec sleep 30`)
|
|
|
|
tx := &fakeSender{}
|
|
r := New(Config{ResticBin: bin}, tx, 0)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- r.RunBackup(ctx, "job-cancel", []string{"/tmp/x"}, nil, nil, BackupHooks{})
|
|
}()
|
|
|
|
// Wait long enough for the subprocess to actually start before
|
|
// canceling. Without this, exec.CommandContext can race the
|
|
// kill against Start and produce a different error path.
|
|
time.Sleep(150 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatal("RunBackup did not return within 15s of cancel")
|
|
}
|
|
|
|
// Locate the job.finished envelope and check its status.
|
|
envs := tx.snapshot()
|
|
var finEnv api.Envelope
|
|
var found bool
|
|
for _, e := range envs {
|
|
if e.Type == api.MsgJobFinished {
|
|
finEnv = e
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("no job.finished envelope was sent")
|
|
}
|
|
var fin api.JobFinishedPayload
|
|
if err := finEnv.UnmarshalPayload(&fin); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if fin.Status != api.JobCancelled {
|
|
t.Fatalf("status: got %q, want %q", fin.Status, api.JobCancelled)
|
|
}
|
|
if fin.ExitCode != 130 {
|
|
t.Errorf("exit_code: got %d, want 130 (POSIX cancel convention)", fin.ExitCode)
|
|
}
|
|
// The error message should be empty for canceled jobs (see runner.sendFinished).
|
|
if !strings.HasPrefix(fin.Error, "") || fin.Error != "" {
|
|
t.Errorf("error: got %q, want empty for canceled jobs", fin.Error)
|
|
}
|
|
}
|