168 lines
6.0 KiB
Go
168 lines
6.0 KiB
Go
package gateway
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestQualificationCatalogMatchesSection7(t *testing.T) {
|
|
media := qualificationMediaProfiles()
|
|
if len(media) != 3 {
|
|
t.Fatalf("media profile count = %d, want 3", len(media))
|
|
}
|
|
wantMedia := []qualificationMediaProfile{
|
|
{Name: "1080p60-h264", Codec: "h264", BitrateKbps: 20000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
|
{Name: "1440p120-hevc", Codec: "hevc", BitrateKbps: 50000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
|
{Name: "4k60-hevc", Codec: "hevc", BitrateKbps: 80000, Duration: 10 * time.Minute, Warmup: time.Second, PacketBytes: 1179},
|
|
}
|
|
if !reflect.DeepEqual(media, wantMedia) {
|
|
t.Fatalf("media profiles = %#v, want %#v", media, wantMedia)
|
|
}
|
|
|
|
impairments := qualificationImpairmentProfiles()
|
|
wantImpairments := []qualificationImpairmentProfile{
|
|
{Name: "baseline", RTT: 20 * time.Millisecond},
|
|
{Name: "latency", RTT: 150 * time.Millisecond},
|
|
{Name: "jitter", RTT: 50 * time.Millisecond, Jitter: 30 * time.Millisecond},
|
|
{Name: "loss", RTT: 50 * time.Millisecond, Jitter: 10 * time.Millisecond, LossPercent: 5},
|
|
{Name: "reorder", RTT: 100 * time.Millisecond, Jitter: 10 * time.Millisecond, LossPercent: 1, Reorder: true},
|
|
{Name: "constrained", RTT: 50 * time.Millisecond, Jitter: 10 * time.Millisecond, LossPercent: 2, Reorder: true, CapacitySteps: []int{25, 50}},
|
|
}
|
|
if !reflect.DeepEqual(impairments, wantImpairments) {
|
|
t.Fatalf("impairment profiles = %#v, want %#v", impairments, wantImpairments)
|
|
}
|
|
}
|
|
|
|
func TestQualificationProtocolVersionIsExplicitAndImmutable(t *testing.T) {
|
|
const version = "v1.0.0-phase3c-gateway-rc.6"
|
|
t.Setenv("VERSEVDI_QUALIFICATION_PROTOCOL_VERSION", version)
|
|
got, err := qualificationProtocolVersion()
|
|
if err != nil || got != version {
|
|
t.Fatalf("qualificationProtocolVersion() = %q, want %q", got, version)
|
|
}
|
|
for _, invalid := range []string{"", "unknown", "v1", " v1.0.0", "v1.0.0+mutable"} {
|
|
t.Setenv("VERSEVDI_QUALIFICATION_PROTOCOL_VERSION", invalid)
|
|
if _, err := qualificationProtocolVersion(); err == nil {
|
|
t.Fatalf("qualificationProtocolVersion() accepted %q", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestQualificationOutputAndStatisticsFailClosed(t *testing.T) {
|
|
if err := validateQualificationOutputDir("relative/evidence"); err == nil {
|
|
t.Fatal("relative evidence directory was accepted")
|
|
}
|
|
if err := validateQualificationOutputDir(filepath.Join(t.TempDir(), "evidence")); err != nil {
|
|
t.Fatalf("absolute evidence directory rejected: %v", err)
|
|
}
|
|
|
|
summary, err := summarizeQualificationSamples([]time.Duration{
|
|
time.Millisecond, 2 * time.Millisecond, 3 * time.Millisecond,
|
|
4 * time.Millisecond, 5 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Count != 5 || summary.Min != time.Millisecond || summary.Median != 3*time.Millisecond ||
|
|
summary.P90 != 5*time.Millisecond || summary.P95 != 5*time.Millisecond ||
|
|
summary.P99 != 5*time.Millisecond || summary.Max != 5*time.Millisecond ||
|
|
summary.Mean != 3*time.Millisecond || summary.Histogram["le_5ms"] != 5 {
|
|
t.Fatalf("summary = %#v", summary)
|
|
}
|
|
if err := enforceQualificationProcessingGate(summary); err != nil {
|
|
t.Fatalf("5 ms p95 rejected: %v", err)
|
|
}
|
|
summary.P95++
|
|
if err := enforceQualificationProcessingGate(summary); err == nil {
|
|
t.Fatal("p95 above 5 ms was accepted")
|
|
}
|
|
}
|
|
|
|
func TestQualificationShortProcessingWritesRawArtifact(t *testing.T) {
|
|
profile := qualificationMediaProfile{
|
|
Name: "smoke", Codec: "h264", BitrateKbps: 1000,
|
|
Duration: 200 * time.Millisecond, Warmup: time.Millisecond, PacketBytes: 100,
|
|
}
|
|
rawPath := filepath.Join(t.TempDir(), "processing.csv.gz")
|
|
summary, err := runQualificationProcessing(profile, rawPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if summary.Count < 1 || summary.RawSamplesSHA256 == "" || summary.RawSamplesBytes < 1 {
|
|
t.Fatalf("processing summary = %#v", summary)
|
|
}
|
|
file, err := os.Open(rawPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer file.Close()
|
|
reader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reader.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(string(raw), "elapsed_ns,processing_ns\n") || strings.Count(string(raw), "\n") != int(summary.Count)+1 {
|
|
t.Fatalf("raw sample rows do not match summary count: %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestQualificationProcessingPreservesPayload(t *testing.T) {
|
|
payload := qualificationPayload(qualificationMediaProfiles()[0])
|
|
processed, elapsed, err := processQualificationPayload(7, payload)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(processed, payload) {
|
|
t.Fatal("encoded payload mutated")
|
|
}
|
|
if elapsed <= 0 {
|
|
t.Fatalf("processing duration = %s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestQualificationImpairmentIsDeterministicAndBounded(t *testing.T) {
|
|
profile := qualificationImpairmentProfiles()[3]
|
|
first, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], 10_000)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], 10_000)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(first, second) {
|
|
t.Fatalf("impairment run is not deterministic:\n%#v\n%#v", first, second)
|
|
}
|
|
if first.Sent != 10_000 || first.Delivered+first.Dropped != first.Sent ||
|
|
first.ObservedLossPercent < 4.8 || first.ObservedLossPercent > 5.2 ||
|
|
first.MaxQueuePackets > qualificationImpairmentQueuePackets {
|
|
t.Fatalf("impairment observation = %#v", first)
|
|
}
|
|
if _, err := runQualificationImpairment(profile, qualificationMediaProfiles()[0], qualificationImpairmentMaxPackets+1); err == nil {
|
|
t.Fatal("unbounded impairment packet count was accepted")
|
|
}
|
|
}
|
|
|
|
func TestQualificationUsesPublicQUICAndProductionPacer(t *testing.T) {
|
|
qualificationTraverseProfiles(t, qualificationMediaProfiles())
|
|
evidence, err := qualificationPacerEvidence()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(evidence.PerFlowBytes) != 8 || len(evidence.CapacitySteps) != 2 || evidence.JainIndex < 0.99 {
|
|
t.Fatalf("pacer evidence = %#v", evidence)
|
|
}
|
|
}
|