F-tools: the runner's views consumer is proven on the real bus; the heartbeat is asked of the bus only when a bundle wants it, with no race; a seat record says when the bus wrote it; the beat test counts ticks, not sleeps

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 16:26:50 +01:00
parent 76e025dbbc
commit db3b5fe63d
9 changed files with 290 additions and 32 deletions
@@ -1,11 +1,13 @@
package runtime
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
@@ -157,3 +159,30 @@ func TestALongRunningBundleThatExitsIsStartedAgain(t *testing.T) {
func fmtSprintf(format string, args ...any) string { return fmt.Sprintf(format, args...) }
func errorf(format string, args ...any) error { return fmt.Errorf(format, args...) }
// A bundle loaded again sets its heartbeat handler while the consumer's loop hands a word on: no data race (-race),
// and the heartbeat is asked of the bus only while some bundle holds a handler.
func TestAHeartbeatHandlerSetAgainWhileWordsAreHandedOnIsNotARace(t *testing.T) {
m := &moduleEvents{}
all := &consumers{logf: t.Logf, of: map[string]*moduleEvents{"mod": m}}
slot := &beatSlot{}
m.beats = []*beatSlot{slot}
if m.beatAsked() {
t.Fatal("a heartbeat is asked while no bundle gave a handler")
}
var got atomic.Int32
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 200; i++ {
slot.set(func(json.RawMessage) { got.Add(1) })
}
}()
for i := 0; i < 200; i++ {
all.handBeat(m, bus.Beat{At: time.Now()})
}
<-done
if !m.beatAsked() {
t.Error("a bundle gave a handler and no heartbeat is asked")
}
}