streamtop 1.1.0

HLS/DASH/IPTV stream diagnostics TUI and CLI
Documentation
# End-to-end verification for streamtop v1.1.x (hermetic, no paid services).
# Native PowerShell harness mirroring tests/e2e_verify.sh (no WSL/Git Bash).
$ErrorActionPreference = 'Stop'

$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $Root

$Pass = 0
$Fail = 0
$Tmp = Join-Path $env:TEMP "streamtop-e2e-$PID"
New-Item -ItemType Directory -Force -Path $Tmp | Out-Null

$MockProc = $null
$PromProc = $null

function Log([string]$Msg) { Write-Host "[e2e] $Msg" }
function Pass([string]$Msg) { $script:Pass++; Log "PASS: $Msg" }
function Fail([string]$Msg) { $script:Fail++; Write-Host "[e2e] FAIL: $Msg" -ForegroundColor Red }

function Need-Cmd([string]$Name) {
    if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
        Fail "missing required command: $Name"
        exit 1
    }
}

function Get-JsonField([string]$Path, [string]$Expr) {
    $doc = [System.IO.File]::ReadAllText($Path) | ConvertFrom-Json
    $cur = $doc
    foreach ($part in $Expr.Trim('.').Split('.')) {
        if ($part) { $cur = $cur.$part }
    }
    if ($null -eq $cur) { return $null }
    if ($cur -is [bool]) { return $cur.ToString().ToLower() }
    return $cur.ToString()
}

function Run-Summary([string]$Url, [string[]]$ExtraArgs) {
    $Out = Join-Path $Tmp 'summary.json'
    $Err = Join-Path $Tmp 'stderr.txt'
    $args = @($Url) + $ExtraArgs + @('--summary', '--summary-format', 'json', '--timeout', '8')
    $stdout = & $Streamtop @args 2> $Err
    $exit = $LASTEXITCODE
    if ($stdout) {
        $jsonLine = $stdout | Where-Object { $_ -match '^\s*\{' } | Select-Object -First 1
        if ($jsonLine) {
            [System.IO.File]::WriteAllText($Out, $jsonLine.Trim(), [System.Text.UTF8Encoding]::new($false))
        }
    }
    if (-not (Test-Path $Out) -or (Get-Item $Out).Length -eq 0) {
        Fail "no summary JSON for $Url ($($ExtraArgs -join ' '))"
        if (Test-Path $Err) { Get-Content $Err | Write-Host }
        return $null
    }
    & python $Root/tests/e2e/validate_summary.py $Out | Out-Null
    if ($LASTEXITCODE -ne 0) {
        Fail "schema validation for $Url"
        return $null
    }
    return $Out
}

try {
    Need-Cmd python
    if (-not (Get-Command python3 -ErrorAction SilentlyContinue)) {
        Set-Alias -Name python3 -Value python -Scope Script -ErrorAction SilentlyContinue
    }

    Log 'Building streamtop release binary'
    cargo build --release --quiet
    if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }

    $Streamtop = Join-Path $Root 'target/release/streamtop.exe'
    if (-not (Test-Path $Streamtop)) {
        $Streamtop = Join-Path $Root 'target/release/streamtop'
    }
    if (-not (Test-Path $Streamtop)) {
        Fail "binary missing at $Streamtop"
        exit 1
    }

    Log 'Starting hermetic mock servers (HTTP/SRT/RTMP)'
    $MockProc = Start-Process -FilePath python `
        -ArgumentList @("$Root/tests/e2e/mock_all.py") `
        -PassThru -WindowStyle Hidden
    Start-Sleep -Seconds 2

    $Base = 'http://127.0.0.1:8765'
    $Tr101Url = "$Base/tr101290/live.m3u8"
    $SeiUrl = "$Base/sei/live.m3u8"
    $HlsUrl = "$Base/live.m3u8"
    $LlUrl = "$Base/ll-hls/master.m3u8"
    $DashUrl = "$Base/dash/live.mpd"
    $SrtUrl = 'srt://127.0.0.1:9000'
    $RtmpUrl = 'rtmp://127.0.0.1:1935/live/stream'

    Log 'TR 101 290 compliance summary'
    $Out = Run-Summary $Tr101Url @('--tr101290', '--probe-headers')
    if ($Out) {
        $P1 = Get-JsonField $Out 'tr101290.p1_violations'
        $P2 = Get-JsonField $Out 'tr101290.p2_violations'
        if (($P1 -match '^\d+$') -and ($P2 -match '^\d+$') -and ([int]$P1 -gt 0 -or [int]$P2 -gt 0)) {
            Pass "tr101290 violations reported (P1=$P1 P2=$P2)"
        } else {
            Fail "expected tr101290.p1_violations or p2_violations > 0 (P1=$P1 P2=$P2)"
        }
    }

    Log 'SEI probe summary'
    $Out = Run-Summary $SeiUrl @('--probe-sei', '--probe-headers')
    if ($Out) {
        $C608 = Get-JsonField $Out 'sei_metadata.cea608_present'
        $Hdr = Get-JsonField $Out 'sei_metadata.hdr10_present'
        $c608Ok = ($C608 -eq 'True' -or $C608 -eq 'true')
        $hdrOk = ($Hdr -eq 'True' -or $Hdr -eq 'true')
        if ($c608Ok -and $hdrOk) {
            Pass 'sei_metadata captions and HDR detected'
        } else {
            Fail "expected sei_metadata.cea608_present and hdr10_present (c608=$C608 hdr=$Hdr)"
        }
    }

    Log 'Synthetic QoE summary'
    $Out = Run-Summary $HlsUrl @('--simulate-player', '--throttle-kbps', '2000', '--simulated-rtt-ms', '120', '--probe-headers')
    if ($Out) {
        $Risk = Get-JsonField $Out 'synthetic_qoe.rebuffer_risk_score'
        if (($Risk -match '^\d+$') -and [int]$Risk -ge 0 -and [int]$Risk -le 100) {
            Pass "synthetic_qoe.rebuffer_risk_score=$Risk"
        } else {
            Fail "rebuffer_risk_score out of range: $Risk"
        }
    }

    Log 'SRT ingest summary'
    $Out = Run-Summary $SrtUrl @()
    if ($Out) {
        $Proto = Get-JsonField $Out 'ingest_stats.protocol'
        $Rtt = Get-JsonField $Out 'ingest_stats.rtt_ms'
        if ($Proto -eq 'srt' -and $Rtt -and $Rtt -ne 'null') {
            Pass "SRT ingest_stats protocol=$Proto rtt_ms=$Rtt"
        } else {
            Fail "SRT ingest_stats missing (protocol=$Proto rtt=$Rtt)"
        }
    }

    Log 'RTMP ingest summary'
    $Out = Run-Summary $RtmpUrl @()
    if ($Out) {
        $Proto = Get-JsonField $Out 'ingest_stats.protocol'
        $Conn = Get-JsonField $Out 'ingest_stats.connected'
        $connOk = ($Conn -eq 'True' -or $Conn -eq 'true')
        if ($Proto -eq 'rtmp' -and $connOk) {
            Pass 'RTMP ingest connected'
        } else {
            Fail "RTMP ingest_stats (protocol=$Proto connected=$Conn)"
        }
    }

    Log 'LL-HLS fMP4 smoke'
    $Out = Run-Summary $LlUrl @('--probe-headers')
    if ($Out) {
        $Seg = Get-JsonField $Out 'saw_segment'
        if ($Seg -eq 'True' -or $Seg -eq 'true') {
            Pass 'LL-HLS saw_segment'
        } else {
            Fail 'LL-HLS did not fetch a segment'
        }
    }

    Log 'DASH live MPD smoke'
    $Out = Run-Summary $DashUrl @('--probe-headers', '--probe-drm')
    if ($Out) { Pass 'DASH summary schema valid' }

    Log 'Webhook SSRF protection'
    $prevEap = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    & $Streamtop $HlsUrl --webhook 'http://169.254.169.254/latest/meta-data' --timeout 2 1>$null 2>$null
    $Rc = $LASTEXITCODE
    $ErrorActionPreference = $prevEap
    if ($Rc -ne 0) {
        Pass "metadata webhook blocked (exit $Rc)"
    } else {
        Fail 'metadata webhook should be blocked'
    }

    Log 'Prometheus metrics auth'
    $PromProc = Start-Process -FilePath $Streamtop `
        -ArgumentList @(
            $HlsUrl, '--simulate-player', '--tr101290',
            '--prometheus', '9184', '--metrics-token', 'test-token',
            '--probe-headers'
        ) `
        -PassThru -WindowStyle Hidden
    Start-Sleep -Seconds 4

    try {
        try {
            Invoke-WebRequest -Uri 'http://127.0.0.1:9184/metrics' -UseBasicParsing | Out-Null
            Fail 'expected metrics 401 without token, got 200'
        } catch {
            $code = [int]$_.Exception.Response.StatusCode
            if ($code -eq 401) {
                Pass 'metrics 401 without token'
            } else {
                Fail "expected metrics 401 without token, got $code"
            }
        }

        $Headers = @{ Authorization = 'Bearer test-token' }
        $Auth = Invoke-WebRequest -Uri 'http://127.0.0.1:9184/metrics' -Headers $Headers -UseBasicParsing
        if ($Auth.StatusCode -eq 200) {
            Pass 'metrics 200 with bearer token'
        } else {
            Fail "expected metrics 200 with token, got $($Auth.StatusCode)"
        }

        $Metrics = $Auth.Content
        if ($Metrics -match 'streamtop_qoe_rebuffer_risk') {
            Pass 'metric streamtop_qoe_rebuffer_risk present'
        } else {
            Fail 'missing streamtop_qoe_rebuffer_risk'
        }
        if ($Metrics -match 'streamtop_tr101290_p1_violations_total') {
            Pass 'metric streamtop_tr101290_p1_violations_total present'
        } else {
            Fail 'missing tr101290 p1 metric'
        }
    } catch {
        Fail "Prometheus probe failed: $_"
    }
}
finally {
    if ($PromProc -and -not $PromProc.HasExited) {
        Stop-Process -Id $PromProc.Id -Force -ErrorAction SilentlyContinue
    }
    if ($MockProc -and -not $MockProc.HasExited) {
        Stop-Process -Id $MockProc.Id -Force -ErrorAction SilentlyContinue
    }
    Remove-Item -Recurse -Force -Path $Tmp -ErrorAction SilentlyContinue
}

Log "Results: $Pass passed, $Fail failed"
if ($Fail -gt 0) { exit 1 }
exit 0