Files
TeleportFling/internal/flinger/flinger_test.go
T
petere 583274d9e6 feat: add bitrate measurement and quality presets
- sender tracks payload bytes; engine measures stream bitrate over a
  sliding window and exposes it in Status
- GUI status label and tray tooltip show the live bitrate (e.g. 36 Mbps)
- add a Preset dropdown (Low/Medium/High/Ultra) that sets quality+fps
  together and applies live via SetConfig

Verified: 36 Mbps shown for Medium (quality 70 @ 30fps) matches the
wire measurement.
2026-09-19 17:53:49 +01:00

249 lines
5.8 KiB
Go

package flinger
import (
"errors"
"net"
"testing"
"time"
)
// TestEnginePatternStartStop runs the engine with the synthetic pattern source
// and verifies it produces frames on the wire and stops cleanly.
func TestEnginePatternStartStop(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "pattern"
cfg.Port = 19756 // fixed high port for the test
eng, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
eng.Start()
defer eng.Stop()
// Connect a raw receiver and read until the engine reports frames sent.
conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(eng.sender.Port()))
if err != nil {
t.Fatalf("dial: %v", err)
}
defer func() { _ = conn.Close() }()
// The video loop sends ~30fps; wait for the counter to advance.
deadline := time.Now().Add(3 * time.Second)
_ = conn.SetReadDeadline(deadline)
// Drain whatever arrives while waiting for the frame counter to move.
go func() {
buf := make([]byte, 64*1024)
for {
if _, err := conn.Read(buf); err != nil {
return
}
}
}()
for time.Now().Before(deadline) {
if eng.Status().Frames > 0 {
break
}
time.Sleep(20 * time.Millisecond)
}
if eng.Status().Frames == 0 {
t.Error("expected frames to be counted")
}
}
// TestEngineConfigDefaults verifies DefaultConfig is sane.
func TestEngineConfigDefaults(t *testing.T) {
c := DefaultConfig()
if c.Port != 9756 {
t.Errorf("default port = %d, want 9756", c.Port)
}
if c.Source != "screen" {
t.Errorf("default source = %q, want screen", c.Source)
}
if c.Quality < 1 || c.Quality > 100 {
t.Errorf("default quality %d out of range", c.Quality)
}
}
// TestNewRejectsBadSource ensures invalid sources fail fast.
func TestNewRejectsBadSource(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "bogus"
if _, err := New(cfg); err == nil {
t.Error("expected error for unknown source")
}
}
// TestSetErrStatus verifies runtime errors are exposed via Status and can be
// cleared.
func TestSetErrStatus(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "pattern"
cfg.Port = 19757
eng, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if st := eng.Status(); st.Err != nil {
t.Fatalf("expected no error initially, got %v", st.Err)
}
sentinel := errors.New("test capture failure")
eng.setErr(sentinel)
if st := eng.Status(); st.Err != sentinel {
t.Errorf("expected sentinel error, got %v", st.Err)
}
eng.setErr(nil)
if st := eng.Status(); st.Err != nil {
t.Errorf("expected cleared error, got %v", st.Err)
}
}
// TestValidate rejects out-of-range values.
func TestValidate(t *testing.T) {
bad := []func(*Config){
func(c *Config) { c.Port = 0 },
func(c *Config) { c.Port = 70000 },
func(c *Config) { c.Quality = 0 },
func(c *Config) { c.Quality = 101 },
func(c *Config) { c.FPS = 0 },
func(c *Config) { c.StreamIndex = -1 },
}
for i, mutate := range bad {
c := DefaultConfig()
mutate(&c)
if err := c.Validate(); err == nil {
t.Errorf("case %d: expected validation error", i)
}
}
if err := DefaultConfig().Validate(); err != nil {
t.Errorf("default config should validate: %v", err)
}
}
// TestBitrateMeasurement verifies the engine reports a non-zero bitrate once
// it has been streaming for a bit.
func TestBitrateMeasurement(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "pattern"
cfg.Port = 19759
eng, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
eng.Start()
defer eng.Stop()
// Connect a receiver so packets actually flow, and wait for the bitrate
// window to produce a measurement.
conn, err := net.Dial("tcp", "127.0.0.1:"+itoa(eng.sender.Port()))
if err != nil {
t.Fatalf("dial: %v", err)
}
defer func() { _ = conn.Close() }()
go func() {
buf := make([]byte, 64*1024)
for {
if _, err := conn.Read(buf); err != nil {
return
}
}
}()
deadline := time.Now().Add(4 * time.Second)
for time.Now().Before(deadline) {
if eng.Status().Bitrate > 0 {
return
}
time.Sleep(200 * time.Millisecond)
}
t.Error("bitrate stayed 0 after 4s of streaming")
}
// TestFormatBitrate checks the human-readable bitrate formatting.
func TestFormatBitrate(t *testing.T) {
cases := []struct {
bps int64
want string
}{
{500, "500 bps"},
{5_000, "5 kbps"},
{5_000_000, "5.0 Mbps"},
}
for _, c := range cases {
if got := formatBitrate(c.bps); got != c.want {
t.Errorf("formatBitrate(%d) = %q, want %q", c.bps, got, c.want)
}
}
}
func itoa(v int) string {
if v == 0 {
return "0"
}
var buf [20]byte
i := len(buf)
for v > 0 {
i--
buf[i] = byte('0' + v%10)
v /= 10
}
return string(buf[i:])
}
// TestSetConfig verifies live config updates apply FPS/quality and reject
// invalid values, while preserving fields that can't change live.
func TestSetConfig(t *testing.T) {
cfg := DefaultConfig()
cfg.Source = "pattern"
cfg.Port = 19758
eng, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
// Update FPS/quality live.
newCfg := DefaultConfig()
newCfg.Source = "screen" // must be ignored (requires restart)
newCfg.Port = 9999 // must be ignored
newCfg.Quality = 95
newCfg.FPS = 60
if err := eng.SetConfig(newCfg); err != nil {
t.Fatalf("SetConfig: %v", err)
}
got := eng.getCfg()
if got.Quality != 95 {
t.Errorf("quality = %d, want 95", got.Quality)
}
if got.FPS != 60 {
t.Errorf("fps = %d, want 60", got.FPS)
}
// Source/port preserved.
if got.Source != "pattern" {
t.Errorf("source = %q, want pattern (unchanged live)", got.Source)
}
if got.Port != 19758 {
t.Errorf("port = %d, want 19758 (unchanged live)", got.Port)
}
// Invalid config is rejected and current settings kept.
if err := eng.SetConfig(Config{Quality: 200}); err == nil {
t.Error("expected error for invalid quality")
}
got = eng.getCfg()
if got.Quality != 95 {
t.Errorf("quality changed after rejected update: %d", got.Quality)
}
}