Skip to content

Commit f3a77ca

Browse files
test: read a served package's stderr under a lock, and wait for its diagnostics
The harness writes its diagnostics from its own thread, after the answer has gone, so TestAnInstalledPackageRefusesACallWithNoIdentity read stderr before the refusal reached it and failed intermittently. The capture was also a bare strings.Builder written by exec's copy goroutine while the test read it, a data race under -race. The capture is now a mutex-guarded writer, and the one assertion on its content waits for the diagnostic with require.Eventually. The other reads only print it in a failure message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f8a56d9 commit f3a77ca

1 file changed

Lines changed: 26 additions & 3 deletions

File tree

‎qualification_test.go‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"os/exec"
1616
"path/filepath"
1717
"strings"
18+
"sync"
1819
"testing"
1920
"time"
2021

@@ -293,7 +294,10 @@ def handle(context: Context, input) -> dict:
293294
require.NotEqual(t, basev0.RunnableServedOutcome_SERVED_SUCCEEDED, none.GetOutcome())
294295
require.False(t, corerunnable.ServedOutcomeIsCertain(none.GetOutcome()),
295296
"a refused call is not the operation asserting anything")
296-
require.Contains(t, harness.stderr.String(), "Work Context")
297+
// The harness writes its diagnostics from its own thread, after the answer
298+
// has gone, so the refusal reaches stderr shortly after it reaches the caller.
299+
require.Eventually(t, func() bool { return strings.Contains(harness.stderr.String(), "Work Context") },
300+
5*time.Second, 20*time.Millisecond, "the refusal was not explained on stderr:\n%s", harness.stderr.String())
297301

298302
declined := harness.call(t, corerunnable.ServedInvokeProcedure, map[string]any{"text": "declined"})
299303
require.Equal(t, basev0.RunnableServedOutcome_SERVED_OWNER_FAILED, declined.GetOutcome())
@@ -372,10 +376,29 @@ func declare(t *testing.T, ctx context.Context, dir string) *resources.Runnable
372376
type served struct {
373377
address string
374378
process *exec.Cmd
375-
stderr *strings.Builder
379+
stderr *capture
376380
pkg *basev0.RunnablePackage
377381
}
378382

383+
// capture is a process's stderr as a test reads it while the process is still
384+
// writing: exec copies into it from a goroutine of its own.
385+
type capture struct {
386+
mu sync.Mutex
387+
written strings.Builder
388+
}
389+
390+
func (c *capture) Write(p []byte) (int, error) {
391+
c.mu.Lock()
392+
defer c.mu.Unlock()
393+
return c.written.Write(p)
394+
}
395+
396+
func (c *capture) String() string {
397+
c.mu.Lock()
398+
defer c.mu.Unlock()
399+
return c.written.String()
400+
}
401+
379402
// serve starts the installed package and waits until it answers. The port is
380403
// chosen by the operating system and handed over the way a placement hands one
381404
// over: nothing here picks a number.
@@ -386,7 +409,7 @@ func serve(t *testing.T, root string, argv []string, pkg *basev0.RunnablePackage
386409
address := listener.Addr().String()
387410
require.NoError(t, listener.Close())
388411

389-
stderr := &strings.Builder{}
412+
stderr := &capture{}
390413
process := exec.Command(filepath.Join(root, argv[0]), argv[1:]...)
391414
process.Dir = root
392415
process.Env = append(os.Environ(), corerunnable.ListenAddressEnv+"="+address)

0 commit comments

Comments
 (0)