Skip to content

Commit 3a1ab2f

Browse files
committed
test(cmaf-ingest): poll for async writes instead of fixed sleeps (fix Windows flakiness)
TestReceivingMediaLiveInput and TestReceivingNonIdealInput waited for the receiver's asynchronous segment/manifest writes with fixed 50ms time.Sleep calls, which intermittently failed on the slower Windows CI runner (e.g. 'manifest_timeline_nr.mpd should now exist'). Replace the sleeps with polling helpers: waitForFile (file exists with content) and waitForMPD (parses and reaches an expected state via mpdReady), so re-write checks wait for the new content rather than reading stale data. The 'should not exist yet' negatives now follow a positive poll instead of a sleep, and a duplicated triple-nested manifest re-read block is collapsed. Polls time out after ~10s. Once net/http works under testing/synctest (golang/go#76608, ~Go 1.27), these can become a synctest bubble with synctest.Wait() for deterministic, instant coordination.
1 parent 7f975ad commit 3a1ab2f

2 files changed

Lines changed: 124 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
hook (triggered by both `*.templ` and `*_templ.go` changes) keep the generated code in
3737
sync, and the generated `*_templ.go` files are excluded from `golangci-lint`.
3838

39+
### Fixed
40+
41+
- cmaf-ingest-receiver tests: replaced fixed `time.Sleep` waits for the receiver's asynchronous
42+
writes with polling (`EventuallyWithT`), fixing intermittent failures on the Windows CI runner.
43+
3944
## [1.10.0] - 2026-06-15
4045

4146
### Changed

cmd/cmaf-ingest-receiver/app/run_test.go

Lines changed: 119 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,9 @@ func TestReceivingMediaLiveInput(t *testing.T) {
110110
wg.Done()
111111
}()
112112
wg.Wait()
113-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
114-
// Check that first media segment has been written
113+
// Check that first media segment has been written (poll: the write is asynchronous)
115114
for _, trd := range testTrackData {
116-
assert.True(t, testFileExists(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", trd.minNr-c.startNr, trd.ext))),
115+
assert.True(t, waitForFile(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", trd.minNr-c.startNr, trd.ext))),
117116
fmt.Sprintf("%s/%d%s should exist", trd.trName, trd.minNr-c.startNr, trd.ext))
118117
}
119118

@@ -129,13 +128,13 @@ func TestReceivingMediaLiveInput(t *testing.T) {
129128
wg.Done()
130129
}()
131130
wg.Wait()
132-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing
133-
assert.True(t, !testFileExists(filepath.Join(dstDir, "content_info.json")), "content_info.json should not exist yet")
134-
// Check that the second media segment has been written
131+
// Check that the second media segment has been written (poll: the write is asynchronous)
135132
for _, trd := range testTrackData {
136-
assert.True(t, testFileExists(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", trd.minNr+1-c.startNr, trd.ext))),
133+
assert.True(t, waitForFile(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", trd.minNr+1-c.startNr, trd.ext))),
137134
fmt.Sprintf("%s/%d%s should exist", trd.trName, trd.minNr+1-c.startNr, trd.ext))
138135
}
136+
// content_info.json is only written once two segments share a duration, so not yet
137+
assert.False(t, testFileExists(filepath.Join(dstDir, "content_info.json")), "content_info.json should not exist yet")
139138
// Send the third media segments. Since the duration of segment 2 and 3 are the same, manifest and content_info
140139
// should have been written unless shifted.
141140
wg = sync.WaitGroup{}
@@ -149,10 +148,11 @@ func TestReceivingMediaLiveInput(t *testing.T) {
149148
wg.Done()
150149
}()
151150
wg.Wait()
152-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
151+
// Poll for the manifest (written asynchronously); the SegmentTimeline-number MPD must not
152+
// appear until a later segment triggers it.
153+
assert.True(t, waitForFile(filepath.Join(dstDir, "manifest.mpd")), "manifest.mpd should exist")
153154
ch, ok := receiver.channelMgr.GetChannel(chName)
154155
assert.True(t, ok, "channel should exist")
155-
assert.True(t, testFileExists(filepath.Join(dstDir, "manifest.mpd")), "manifest.mpd should exist")
156156
require.False(t, testFileExists(filepath.Join(dstDir, timelineNrMPD)), "manifest_time_nrß.mpd should not exist")
157157

158158
var videoTrackData trTestData
@@ -161,15 +161,19 @@ func TestReceivingMediaLiveInput(t *testing.T) {
161161
videoTrackData = trd
162162
}
163163
}
164-
if !ch.isShifted() {
165-
firstSeqNr, ok := ch.segTimesGen.getBufferFirstSeqNr("video")
166-
assert.True(t, ok, "segment data buffer should have items")
167-
assert.Equal(t, firstSeqNr, uint32(videoTrackData.minNr+1-c.startNr),
168-
"first sequence number in segment data buffer should be the second segment")
169-
} else {
170-
nrItems := ch.segTimesGen.getBufferNrItems("video")
171-
assert.Equal(t, 0, int(nrItems), "shifted segment data buffer should be empty")
172-
}
164+
// The segment-data buffer is updated asynchronously as segments are processed, so poll it
165+
// to its expected state rather than reading it at a single fixed moment.
166+
require.EventuallyWithT(t, func(ct *assert.CollectT) {
167+
if !ch.isShifted() {
168+
firstSeqNr, ok := ch.segTimesGen.getBufferFirstSeqNr("video")
169+
assert.True(ct, ok, "segment data buffer should have items")
170+
assert.Equal(ct, firstSeqNr, uint32(videoTrackData.minNr+1-c.startNr),
171+
"first sequence number in segment data buffer should be the second segment")
172+
} else {
173+
nrItems := ch.segTimesGen.getBufferNrItems("video")
174+
assert.Equal(ct, 0, int(nrItems), "shifted segment data buffer should be empty")
175+
}
176+
}, asyncWriteTimeout, asyncWritePollTick)
173177

174178
// Send the fourth media segments.
175179
// If shifted, there should be segments in the SegmentTimel line MPD, if not, just one segment.
@@ -183,29 +187,23 @@ func TestReceivingMediaLiveInput(t *testing.T) {
183187
wg.Done()
184188
}()
185189
wg.Wait()
186-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
187-
assert.True(t, testFileExists(filepath.Join(dstDir, timelineNrMPD)), "manifest_timeline_nr.mpd should now exist")
188-
data, err := os.ReadFile(filepath.Join(dstDir, timelineNrMPD))
189-
require.NoError(t, err)
190-
manifest, err := mpd.MPDFromBytes(data)
191-
require.NoError(t, err)
192-
assert.Equal(t, 1, len(manifest.Periods), "there should be 1 period")
193-
assert.Equal(t, 2, len(manifest.Periods[0].AdaptationSets), "there should be 2 adaptation sets")
190+
// Poll until the SegmentTimeline-number MPD has been written to the expected state.
191+
expStartNr, expNrSegments := 896605655, 3
192+
if ch.isShifted() { // Just one segment
193+
expStartNr, expNrSegments = 896605657, 1
194+
}
195+
manifest := waitForMPD(t, filepath.Join(dstDir, timelineNrMPD), func(m *mpd.MPD) bool {
196+
return mpdReady(m, 2, expStartNr)
197+
})
194198
for _, as := range manifest.Periods[0].AdaptationSets {
195199
stl := as.SegmentTemplate
196200
assert.NotNil(t, stl, "segment template should exist")
197201
nrSegments := 0
198202
for _, s := range stl.SegmentTimeline.S {
199203
nrSegments += int(s.R) + 1
200204
}
201-
if ch.isShifted() { // Just one segment
202-
require.Equal(t, 896605657, int(*stl.StartNumber), "start number should be 896605657")
203-
require.Equal(t, 1, nrSegments, "number of segments should be 1")
204-
} else {
205-
require.Equal(t, 896605655, int(*stl.StartNumber), "start number should be 896605655")
206-
require.Equal(t, 3, nrSegments, "number of segments should be 3")
207-
208-
}
205+
assert.Equal(t, expStartNr, int(*stl.StartNumber), "start number")
206+
assert.Equal(t, expNrSegments, nrSegments, "number of segments")
209207
}
210208

211209
// Check that the manifest fetched from the file server is the same as the one in the storage
@@ -344,7 +342,7 @@ func TestReceivingNonIdealInput(t *testing.T) {
344342
t.Fatalf("Unknown content type %s", contentType)
345343
}
346344
assert.Equal(t, 0, int(ch.startTime), "startTime should be zero")
347-
assert.True(t, testFileExists(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", 8090, trd.ext))))
345+
assert.True(t, waitForFile(filepath.Join(dstDir, trd.trName, fmt.Sprintf("%d%s", 8090, trd.ext))))
348346
}
349347

350348
// Send second media segments and check that they have been received
@@ -359,7 +357,6 @@ func TestReceivingNonIdealInput(t *testing.T) {
359357
}()
360358
wg.Wait()
361359
nextNr++
362-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
363360
// Check that second segment has been written to nr 8091, except for second video track which has been updated
364361
// It is updated because data from the first video track is used to determine the shift.
365362
seqNr := 8091
@@ -368,16 +365,12 @@ func TestReceivingNonIdealInput(t *testing.T) {
368365
seqNr = 449002889
369366
}
370367
fName := fmt.Sprintf("%d%s", seqNr, trd.ext)
371-
assert.True(t, testFileExists(filepath.Join(dstDir, trd.trName, fName)), fmt.Sprintf("%s should exist", fName))
368+
assert.True(t, waitForFile(filepath.Join(dstDir, trd.trName, fName)), fmt.Sprintf("%s should exist", fName))
372369
}
373-
// Check that metadata has been written (two segments with same duration)
374-
assert.True(t, testFileExists(filepath.Join(dstDir, "manifest.mpd")), "manifest.mpd should exist")
375-
data, err := os.ReadFile(filepath.Join(dstDir, "manifest.mpd"))
376-
require.NoError(t, err)
377-
manifest, err := mpd.MPDFromBytes(data)
378-
require.NoError(t, err)
379-
assert.Equal(t, 1, len(manifest.Periods), "there should be 1 period")
380-
assert.Equal(t, 4, len(manifest.Periods[0].AdaptationSets), "there should be 4 adaptation sets")
370+
// Check that metadata has been written (two segments with same duration); poll for the async write
371+
manifest := waitForMPD(t, filepath.Join(dstDir, "manifest.mpd"), func(m *mpd.MPD) bool {
372+
return mpdReady(m, 4, -1)
373+
})
381374
for _, as := range manifest.Periods[0].AdaptationSets {
382375
switch as.ContentType {
383376
case "video":
@@ -417,40 +410,14 @@ func TestReceivingNonIdealInput(t *testing.T) {
417410
}()
418411
wg.Wait()
419412
nextNr++
420-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
421-
assert.True(t, testFileExists(filepath.Join(dstDir, timelineNrMPD)), "manifest_timeline_nr.mpd should now exist")
422-
data, err = os.ReadFile(filepath.Join(dstDir, timelineNrMPD))
423-
require.NoError(t, err)
424-
manifest, err = mpd.MPDFromBytes(data)
425-
require.NoError(t, err)
426-
assert.Equal(t, 1, len(manifest.Periods), "there should be 1 period")
427-
assert.Equal(t, 4, len(manifest.Periods[0].AdaptationSets), "there should be 4 adaptation sets")
413+
// Poll until the SegmentTimeline-number MPD has been written (startNumber settles to 449002890).
414+
manifest = waitForMPD(t, filepath.Join(dstDir, timelineNrMPD), func(m *mpd.MPD) bool {
415+
return mpdReady(m, 4, 449002890)
416+
})
428417
for _, as := range manifest.Periods[0].AdaptationSets {
429418
stl := as.SegmentTemplate
430419
assert.NotNil(t, stl, "segment template should exist")
431-
assert.True(t, testFileExists(filepath.Join(dstDir, timelineNrMPD)), "manifest_timeline_nr.mpd should now exist")
432-
data, err = os.ReadFile(filepath.Join(dstDir, timelineNrMPD))
433-
require.NoError(t, err)
434-
manifest, err = mpd.MPDFromBytes(data)
435-
require.NoError(t, err)
436-
assert.Equal(t, 1, len(manifest.Periods), "there should be 1 period")
437-
assert.Equal(t, 4, len(manifest.Periods[0].AdaptationSets), "there should be 4 adaptation sets")
438-
for _, as := range manifest.Periods[0].AdaptationSets {
439-
stl := as.SegmentTemplate
440-
assert.NotNil(t, stl, "segment template should exist")
441-
assert.Equal(t, 449002890, int(*stl.StartNumber))
442-
data, err = os.ReadFile(filepath.Join(dstDir, timelineNrMPD))
443-
require.NoError(t, err)
444-
manifest, err = mpd.MPDFromBytes(data)
445-
require.NoError(t, err)
446-
assert.Equal(t, 1, len(manifest.Periods), "there should be 1 period")
447-
assert.Equal(t, 4, len(manifest.Periods[0].AdaptationSets), "there should be 4 adaptation sets")
448-
for _, as := range manifest.Periods[0].AdaptationSets {
449-
stl := as.SegmentTemplate
450-
assert.NotNil(t, stl, "segment template should exist")
451-
assert.Equal(t, 449002890, int(*stl.StartNumber), "start number should be 449002890")
452-
}
453-
}
420+
assert.Equal(t, 449002890, int(*stl.StartNumber), "start number should be 449002890")
454421
}
455422

456423
// Next, let us have a jump in the sequence numbers, so segmentTimes should have one new number.
@@ -467,14 +434,10 @@ func TestReceivingNonIdealInput(t *testing.T) {
467434
}()
468435
wg.Wait()
469436
nextNr++
470-
time.Sleep(50 * time.Millisecond) // Need to finish the asynchronous writing of metadata
471-
require.NoError(t, err)
472-
assert.True(t, testFileExists(filepath.Join(dstDir, timelineNrMPD)))
473-
data, err = os.ReadFile(filepath.Join(dstDir, timelineNrMPD))
474-
require.NoError(t, err)
475-
manifest, err = mpd.MPDFromBytes(data)
476-
require.NoError(t, err)
477-
assert.Equal(t, 4, len(manifest.Periods[0].AdaptationSets), "there should be 4 adaptation sets")
437+
// Poll until the SegmentTimeline-number MPD has been re-written (startNumber settles to 449002892).
438+
manifest = waitForMPD(t, filepath.Join(dstDir, timelineNrMPD), func(m *mpd.MPD) bool {
439+
return mpdReady(m, 4, 449002892)
440+
})
478441
for _, as := range manifest.Periods[0].AdaptationSets {
479442
stl := as.SegmentTemplate
480443
assert.NotNil(t, stl, "segment template should exist")
@@ -581,6 +544,78 @@ func testFileExists(filePath string) bool {
581544
return !info.IsDir()
582545
}
583546

547+
// The receiver writes segments and manifests asynchronously after the upload request returns, so
548+
// tests must wait for those writes before asserting on them. The helpers below poll for that
549+
// instead of using a fixed sleep, which flaked on the slower Windows CI runner.
550+
//
551+
// TODO: once net/http works under testing/synctest (golang/go#76608, expected ~Go 1.27), replace
552+
// the polling with a synctest bubble and synctest.Wait() for deterministic, instant coordination.
553+
554+
// Poll budget and cadence for those async writes. The tick comfortably exceeds the time to read and
555+
// parse one of these small manifests; testify's EventuallyWithT runs the condition serially (it
556+
// waits for one call to return before scheduling the next tick), so the tick never overlaps a parse.
557+
const (
558+
asyncWriteTimeout = 10 * time.Second
559+
asyncWritePollTick = 50 * time.Millisecond
560+
)
561+
562+
// waitForFile polls (up to asyncWriteTimeout) for filePath to exist with content, returning true once it does.
563+
func waitForFile(filePath string) bool {
564+
deadline := time.Now().Add(asyncWriteTimeout)
565+
for {
566+
if info, err := os.Stat(filePath); err == nil && !info.IsDir() && info.Size() > 0 {
567+
return true
568+
}
569+
if time.Now().After(deadline) {
570+
return false
571+
}
572+
time.Sleep(asyncWritePollTick)
573+
}
574+
}
575+
576+
// mpdReady reports whether m has exactly nrAS adaptation sets in a single period and, when startNr
577+
// is >= 0, that every adaptation set's SegmentTemplate carries startNr as its StartNumber. It is the
578+
// "settled state" predicate used to wait past an in-progress or stale (re)write.
579+
func mpdReady(m *mpd.MPD, nrAS, startNr int) bool {
580+
if len(m.Periods) != 1 || len(m.Periods[0].AdaptationSets) != nrAS {
581+
return false
582+
}
583+
if startNr < 0 {
584+
return true
585+
}
586+
for _, as := range m.Periods[0].AdaptationSets {
587+
stl := as.SegmentTemplate
588+
if stl == nil || stl.StartNumber == nil || int(*stl.StartNumber) != startNr {
589+
return false
590+
}
591+
}
592+
return true
593+
}
594+
595+
// waitForMPD polls (up to asyncWriteTimeout) until filePath parses into an MPD satisfying cond, then returns it,
596+
// so callers can run their detailed assertions on a settled manifest. cond lets a caller wait for a
597+
// specific state (e.g. an updated startNumber), not just the file's first appearance — a plain
598+
// existence check would read stale content on a re-write.
599+
func waitForMPD(t *testing.T, filePath string, cond func(*mpd.MPD) bool) *mpd.MPD {
600+
t.Helper()
601+
var manifest *mpd.MPD
602+
require.EventuallyWithT(t, func(c *assert.CollectT) {
603+
data, err := os.ReadFile(filePath)
604+
if !assert.NoError(c, err) {
605+
return
606+
}
607+
m, err := mpd.MPDFromBytes(data)
608+
if !assert.NoError(c, err) {
609+
return
610+
}
611+
if !assert.True(c, cond(m), "manifest %s is not yet in the expected state", filePath) {
612+
return
613+
}
614+
manifest = m
615+
}, asyncWriteTimeout, asyncWritePollTick)
616+
return manifest
617+
}
618+
584619
func addOrigInitSegments(srcDir, chName, tmpDir string, ttd []trTestData) error {
585620
for _, trd := range ttd {
586621
srcInitPath := filepath.Join(srcDir, chName, trd.trName, "init_org"+trd.ext)

0 commit comments

Comments
 (0)