package flinger import ( "errors" "net" "testing" "time" "teleportfling/internal/capture" ) // 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) } } } // TestScaleBGRA verifies downsampling produces the expected dimensions and // preserves the dominant colour of a solid frame. func TestScaleBGRA(t *testing.T) { src := &capture.VideoFrame{ Pix: make([]byte, 100*80*4), Width: 100, Height: 80, Stride: 100 * 4, } // Fill with solid red (BGRA: B=0, G=0, R=255). for i := 0; i+4 <= len(src.Pix); i += 4 { src.Pix[i], src.Pix[i+1], src.Pix[i+2], src.Pix[i+3] = 0, 0, 255, 255 } dst := &capture.VideoFrame{ Pix: make([]byte, 50*40*4), Width: 50, Height: 40, Stride: 50 * 4, } scaleBGRA(src, dst) if dst.Width != 50 || dst.Height != 40 { t.Errorf("dst dims = %dx%d, want 50x40", dst.Width, dst.Height) } // Check a few pixels are solid red. for _, idx := range []int{0, 4, 100, 200} { if dst.Pix[idx] != 0 || dst.Pix[idx+1] != 0 || dst.Pix[idx+2] != 255 { t.Errorf("pixel %d not red: B=%d G=%d R=%d", idx, dst.Pix[idx], dst.Pix[idx+1], dst.Pix[idx+2]) } } } // TestEngineScalePreservesNative verifies scaleFrame returns the original // frame at scale 1.0. func TestEngineScalePreservesNative(t *testing.T) { cfg := DefaultConfig() cfg.Source = "pattern" cfg.Port = 19760 eng, err := New(cfg) if err != nil { t.Fatalf("New: %v", err) } frame := &capture.VideoFrame{Pix: make([]byte, 4*4*4), Width: 4, Height: 4, Stride: 16} if got := eng.scaleFrame(frame, 1.0); got != frame { t.Error("scale 1.0 should return the original frame") } if got := eng.scaleFrame(frame, 0.5); got == frame { t.Error("scale 0.5 should return a new frame") } } 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) } }