//go:build mxl_integration package mxlfabrics_test import ( "errors" "sync" "testing" "time" "github.com/qvest-digital/go-mxl/fabrics" "github.com/qvest-digital/go-mxl/mxl" ) const testAudioFlowID = "b3bb5be7-9fe9-4324-a5bb-4c70e1084449" const testAudioFlow = `{ "description": "MXL Player Fabrics SHM audio integration test", "id": "b3bb5be7-9fe9-4324-a5bb-4c70e1084449", "format": "urn:x-nmos:format:audio", "label": "Fabrics SHM test audio", "tags": { "urn:x-nmos:tag:grouphint/v1.0": [ "mxl-player-fabrics-test:Audio" ] }, "parents": [], "media_type": "audio/float32", "sample_rate": { "numerator": 48000 }, "channel_count": 2, "bit_depth": 32 }` type sampleCompletion struct { head uint64 count int } func TestSampleTransferSHM(t *testing.T) { sourceInstance := newTestDomain(t) targetInstance := newTestDomain(t) sourceWriter, _, err := sourceInstance.NewWriter(testAudioFlow) if err != nil { t.Fatalf("create source writer: %v", err) } t.Cleanup(func() { if err := sourceWriter.Close(); err != nil { t.Errorf("close source writer: %v", err) } }) sourceReader, err := sourceInstance.NewReader(testAudioFlowID) if err != nil { t.Fatalf("create source reader: %v", err) } t.Cleanup(func() { if err := sourceReader.Close(); err != nil { t.Errorf("close source reader: %v", err) } }) targetWriter, _, err := targetInstance.NewWriter(testAudioFlow) if err != nil { t.Fatalf("create target writer: %v", err) } t.Cleanup(func() { if err := targetWriter.Close(); err != nil { t.Errorf("close target writer: %v", err) } }) sourceFabrics, err := fabrics.NewInstance(sourceInstance) if err != nil { t.Fatalf("create source Fabrics instance: %v", err) } t.Cleanup(func() { if err := sourceFabrics.Close(); err != nil { t.Errorf("close source Fabrics instance: %v", err) } }) targetFabrics, err := fabrics.NewInstance(targetInstance) if err != nil { t.Fatalf("create target Fabrics instance: %v", err) } t.Cleanup(func() { if err := targetFabrics.Close(); err != nil { t.Errorf("close target Fabrics instance: %v", err) } }) target, err := targetFabrics.NewTarget() if err != nil { t.Fatalf("create target: %v", err) } t.Cleanup(func() { if err := target.Close(); err != nil { t.Errorf("close target: %v", err) } }) targetInfo, err := target.Setup(fabrics.TargetConfig{ Interface: requireSHMInterface(t, targetFabrics), Writer: targetWriter, }) if err != nil { t.Fatalf("set up target: %v", err) } t.Cleanup(func() { if err := targetInfo.Close(); err != nil { t.Errorf("close target info: %v", err) } }) initiator, err := sourceFabrics.NewInitiator() if err != nil { t.Fatalf("create initiator: %v", err) } t.Cleanup(func() { if err := initiator.Close(); err != nil { t.Errorf("close initiator: %v", err) } }) if err := initiator.Setup(fabrics.InitiatorConfig{ Interface: requireSHMInterface(t, sourceFabrics), Reader: sourceReader, }); err != nil { t.Fatalf("set up initiator: %v", err) } if err := initiator.AddTarget(targetInfo); err != nil { t.Fatalf("add target: %v", err) } connectDeadline := time.Now().Add(5 * time.Second) for { if time.Now().After(connectDeadline) { t.Fatal("SHM initiator did not become ready") } _, _, err := target.ReadSamplesNonBlocking() if err != nil && !errors.Is(err, fabrics.ErrNotReady) { t.Fatalf("progress target during setup: %v", err) } err = initiator.MakeProgressNonBlocking() if err == nil { break } if !errors.Is(err, fabrics.ErrNotReady) { t.Fatalf("progress initiator during setup: %v", err) } time.Sleep(time.Millisecond) } const sampleCount = 480 head := mxl.CurrentIndex(sourceWriter.Config().Common.GrainRate) sourceSamples, err := sourceWriter.OpenSamples(head, sampleCount) if err != nil { t.Fatalf("open source samples: %v", err) } for channel := uint64(0); channel < sourceSamples.ChannelCount; channel++ { first, second, err := sourceSamples.ChannelFragments(channel) if err != nil { t.Fatalf("get source channel %d fragments: %v", channel, err) } var position uint64 for _, fragment := range [][]byte{first, second} { for offset := range fragment { fragment[offset] = byte((position + head + channel) & 0xff) position++ } } } if err := sourceSamples.Commit(); err != nil { t.Fatalf("commit source samples: %v", err) } deadline := time.Now().Add(5 * time.Second) received := make(chan sampleCompletion, 1) targetErrors := make(chan error, 1) stopTarget := make(chan struct{}) var targetWG sync.WaitGroup targetWG.Add(1) go func() { defer targetWG.Done() for { select { case <-stopTarget: return default: } receivedHead, receivedCount, err := target.ReadSamplesNonBlocking() switch { case err == nil: received <- sampleCompletion{head: receivedHead, count: receivedCount} return case errors.Is(err, fabrics.ErrNotReady): time.Sleep(time.Millisecond) default: targetErrors <- err return } } }() defer func() { close(stopTarget) targetWG.Wait() }() for { err := initiator.TransferSamples(head, sampleCount) if err == nil { break } if !errors.Is(err, fabrics.ErrNotReady) { t.Fatalf("transfer samples: %v", err) } if time.Now().After(deadline) { t.Fatal("timed out enqueueing SHM sample transfer") } err = initiator.MakeProgressNonBlocking() if err != nil && !errors.Is(err, fabrics.ErrNotReady) { t.Fatalf("make initiator progress while enqueueing: %v", err) } time.Sleep(time.Millisecond) } var completion sampleCompletion for { if time.Now().After(deadline) { t.Fatal("SHM sample transfer timed out") } err := initiator.MakeProgressNonBlocking() if err != nil && !errors.Is(err, fabrics.ErrNotReady) { t.Fatalf("make initiator progress: %v", err) } select { case completion = <-received: goto receivedSamples case err := <-targetErrors: t.Fatalf("read target completion: %v", err) default: time.Sleep(time.Millisecond) } } receivedSamples: if completion.head != head { t.Fatalf("received sample head = %d, want %d", completion.head, head) } if completion.count != sampleCount { t.Fatalf("received sample count = %d, want %d", completion.count, sampleCount) } targetSamples, err := targetWriter.OpenSamples(head, sampleCount) if err != nil { t.Fatalf("open transferred samples: %v", err) } defer func() { if err := targetSamples.Cancel(); err != nil { t.Errorf("cancel transferred samples: %v", err) } }() for channel := uint64(0); channel < targetSamples.ChannelCount; channel++ { first, second, err := targetSamples.ChannelFragments(channel) if err != nil { t.Fatalf("get target channel %d fragments: %v", channel, err) } var position uint64 for _, fragment := range [][]byte{first, second} { for _, got := range fragment { want := byte((position + head + channel) & 0xff) if got != want { t.Fatalf("channel %d sample byte %d = %d, want %d", channel, position, got, want) } position++ } } } }