435 lines
11 KiB
Go
435 lines
11 KiB
Go
package capture
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"image"
|
|
"image/color"
|
|
"image/jpeg"
|
|
"testing"
|
|
"time"
|
|
|
|
"overlord-client/cmd/agent/config"
|
|
rt "overlord-client/cmd/agent/runtime"
|
|
"overlord-client/cmd/agent/wire"
|
|
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
"nhooyr.io/websocket"
|
|
)
|
|
|
|
type recordingWriter struct {
|
|
msgs [][]byte
|
|
types []websocket.MessageType
|
|
}
|
|
|
|
func (w *recordingWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
|
|
w.types = append(w.types, messageType)
|
|
w.msgs = append(w.msgs, append([]byte(nil), p...))
|
|
return nil
|
|
}
|
|
|
|
func TestCaptureAndSend_NoDisplays(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
activeDisplays = func() int { return 0 }
|
|
t.Cleanup(func() { activeDisplays = originalCount })
|
|
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
if err := CaptureAndSend(context.Background(), env); err != nil {
|
|
t.Fatalf("expected nil error when no displays, got %v", err)
|
|
}
|
|
if len(writer.msgs) != 0 {
|
|
t.Fatalf("expected no messages when no displays, got %d", len(writer.msgs))
|
|
}
|
|
}
|
|
|
|
func TestCaptureAndSend_SendsFrame(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
originalCapture := captureDisplayFn
|
|
activeDisplays = func() int { return 1 }
|
|
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
|
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
|
|
|
img.Set(0, 0, color.RGBA{R: 200, G: 10, B: 10, A: 255})
|
|
return img, nil
|
|
}
|
|
t.Cleanup(func() {
|
|
activeDisplays = originalCount
|
|
captureDisplayFn = originalCapture
|
|
})
|
|
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
if err := CaptureAndSend(context.Background(), env); err != nil {
|
|
t.Fatalf("CaptureAndSend returned error: %v", err)
|
|
}
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("expected one message written, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var frame wire.Frame
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
|
t.Fatalf("decode frame: %v", err)
|
|
}
|
|
if frame.Type != "frame" {
|
|
t.Fatalf("unexpected frame type: %s", frame.Type)
|
|
}
|
|
if frame.Header.Format != "jpeg" {
|
|
t.Fatalf("unexpected frame format: %s", frame.Header.Format)
|
|
}
|
|
if len(frame.Data) == 0 {
|
|
t.Fatalf("expected jpeg data to be present")
|
|
}
|
|
|
|
if _, err := jpeg.Decode(bytes.NewReader(frame.Data)); err != nil {
|
|
t.Fatalf("jpeg payload did not decode: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestActualScreenCapture(t *testing.T) {
|
|
displayCount := safeDisplayCount()
|
|
if displayCount == 0 {
|
|
t.Log("WARNING: No displays detected, skipping real screen capture test")
|
|
return
|
|
}
|
|
|
|
t.Logf("Testing screen capture with %d display(s)", displayCount)
|
|
|
|
img, err := safeCaptureDisplay(0)
|
|
if err != nil {
|
|
t.Logf("WARNING: Failed to capture display 0: %v", err)
|
|
return
|
|
}
|
|
|
|
if img == nil {
|
|
t.Log("WARNING: Capture returned nil image")
|
|
return
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
t.Errorf("Invalid image dimensions: %dx%d", bounds.Dx(), bounds.Dy())
|
|
return
|
|
}
|
|
|
|
t.Logf("Successfully captured %dx%d image from display 0", bounds.Dx(), bounds.Dy())
|
|
|
|
hasVariation := false
|
|
firstPixel := img.RGBAAt(0, 0)
|
|
for y := 0; y < bounds.Dy() && !hasVariation; y++ {
|
|
for x := 0; x < bounds.Dx(); x++ {
|
|
pixel := img.RGBAAt(x, y)
|
|
if pixel != firstPixel {
|
|
hasVariation = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasVariation {
|
|
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
|
|
}
|
|
|
|
quality := jpegQuality()
|
|
frame, encodeDur, err := buildFrame(img, 0, quality)
|
|
if err != nil {
|
|
t.Errorf("Failed to build frame from captured image: %v", err)
|
|
return
|
|
}
|
|
|
|
t.Logf("Encoded frame: size=%d bytes, encode_time=%s, format=%s",
|
|
len(frame.Data), encodeDur, frame.Header.Format)
|
|
|
|
decodedImg, err := jpeg.Decode(bytes.NewReader(frame.Data))
|
|
if err != nil {
|
|
t.Errorf("Failed to decode JPEG frame: %v", err)
|
|
return
|
|
}
|
|
|
|
if decodedImg.Bounds().Dx() <= 0 || decodedImg.Bounds().Dy() <= 0 {
|
|
t.Errorf("Decoded image has invalid dimensions: %dx%d",
|
|
decodedImg.Bounds().Dx(), decodedImg.Bounds().Dy())
|
|
}
|
|
}
|
|
|
|
func TestMultipleDisplays(t *testing.T) {
|
|
displayCount := safeDisplayCount()
|
|
if displayCount == 0 {
|
|
t.Log("WARNING: No displays detected, skipping multi-display test")
|
|
return
|
|
}
|
|
|
|
if displayCount == 1 {
|
|
t.Log("Only one display detected, testing single display")
|
|
}
|
|
|
|
for i := 0; i < displayCount; i++ {
|
|
t.Run("Display"+string(rune('0'+i)), func(t *testing.T) {
|
|
img, err := safeCaptureDisplay(i)
|
|
if err != nil {
|
|
t.Logf("WARNING: Failed to capture display %d: %v", i, err)
|
|
return
|
|
}
|
|
|
|
if img == nil {
|
|
t.Logf("WARNING: Capture returned nil for display %d", i)
|
|
return
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
t.Logf("Display %d: %dx%d", i, bounds.Dx(), bounds.Dy())
|
|
|
|
if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
|
|
t.Errorf("Display %d has invalid dimensions", i)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSupportsCapture(t *testing.T) {
|
|
hasDisplays := supportsCapture()
|
|
displayCount := safeDisplayCount()
|
|
|
|
if hasDisplays && displayCount == 0 {
|
|
t.Error("supportsCapture returned true but displayCount is 0")
|
|
}
|
|
|
|
if !hasDisplays && displayCount > 0 {
|
|
t.Error("supportsCapture returned false but displayCount > 0")
|
|
}
|
|
|
|
t.Logf("Capture support: %v (displays: %d)", hasDisplays, displayCount)
|
|
}
|
|
|
|
func TestCaptureWithDifferentDisplayIndices(t *testing.T) {
|
|
displayCount := safeDisplayCount()
|
|
if displayCount == 0 {
|
|
t.Skip("No displays available")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
index int
|
|
valid bool
|
|
}{
|
|
{"ValidFirst", 0, true},
|
|
{"ValidLast", displayCount - 1, true},
|
|
{"InvalidNegative", -1, false},
|
|
{"InvalidTooHigh", displayCount + 10, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
img, err := safeCaptureDisplay(tt.index)
|
|
|
|
if tt.valid {
|
|
if err != nil {
|
|
t.Errorf("Expected valid capture, got error: %v", err)
|
|
}
|
|
if img == nil {
|
|
t.Error("Expected image, got nil")
|
|
}
|
|
} else {
|
|
|
|
if img != nil && err == nil {
|
|
t.Logf("WARNING: Invalid index %d returned image instead of error", tt.index)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFrameFPS(t *testing.T) {
|
|
|
|
resetStats()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
statFrames.Add(1)
|
|
}
|
|
|
|
fps := frameFPS(time.Now())
|
|
if fps < 0 {
|
|
t.Errorf("FPS should not be negative, got %d", fps)
|
|
}
|
|
|
|
t.Logf("Calculated FPS: %d", fps)
|
|
}
|
|
|
|
func TestJPEGQuality(t *testing.T) {
|
|
quality := jpegQuality()
|
|
if quality < 1 || quality > 100 {
|
|
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
|
|
}
|
|
t.Logf("JPEG quality: %d", quality)
|
|
}
|
|
|
|
func TestCaptureAndSend_DisplayOutOfRange(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
originalCapture := captureDisplayFn
|
|
activeDisplays = func() int { return 2 }
|
|
capturedDisplay := -1
|
|
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
|
capturedDisplay = displayIndex
|
|
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
|
return img, nil
|
|
}
|
|
t.Cleanup(func() {
|
|
activeDisplays = originalCount
|
|
captureDisplayFn = originalCapture
|
|
})
|
|
|
|
tests := []struct {
|
|
name string
|
|
selectedDisplay int
|
|
expectedDisplay int
|
|
}{
|
|
{"NegativeDisplay", -1, 0},
|
|
{"TooHighDisplay", 10, 0},
|
|
{"ValidDisplay", 1, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
capturedDisplay = -1
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{},
|
|
SelectedDisplay: tt.selectedDisplay,
|
|
}
|
|
|
|
if err := CaptureAndSend(context.Background(), env); err != nil {
|
|
t.Fatalf("CaptureAndSend failed: %v", err)
|
|
}
|
|
|
|
if capturedDisplay != tt.expectedDisplay {
|
|
t.Errorf("expected capture from display %d, got %d", tt.expectedDisplay, capturedDisplay)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Errorf("expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNow_DisableCapture(t *testing.T) {
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{
|
|
Conn: writer,
|
|
Cfg: config.Config{DisableCapture: true},
|
|
}
|
|
|
|
if err := Now(context.Background(), env); err != nil {
|
|
t.Fatalf("Now with DisableCapture failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("expected 1 black frame message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var frame wire.Frame
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
|
t.Fatalf("failed to decode frame: %v", err)
|
|
}
|
|
|
|
if frame.Type != "frame" {
|
|
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
|
|
}
|
|
|
|
img, err := jpeg.Decode(bytes.NewReader(frame.Data))
|
|
if err != nil {
|
|
t.Fatalf("failed to decode JPEG: %v", err)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
if bounds.Dx() != 64 || bounds.Dy() != 64 {
|
|
t.Errorf("expected 64x64 black frame, got %dx%d", bounds.Dx(), bounds.Dy())
|
|
}
|
|
}
|
|
|
|
func TestNow_NoDisplays(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
activeDisplays = func() int { return 0 }
|
|
t.Cleanup(func() { activeDisplays = originalCount })
|
|
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
|
|
if err := Now(context.Background(), env); err != nil {
|
|
t.Fatalf("Now with no displays failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 0 {
|
|
t.Errorf("expected no messages when no displays, got %d", len(writer.msgs))
|
|
}
|
|
}
|
|
|
|
func TestNow_Success(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
originalCapture := captureDisplayFn
|
|
activeDisplays = func() int { return 1 }
|
|
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
|
img := image.NewRGBA(image.Rect(0, 0, 4, 4))
|
|
return img, nil
|
|
}
|
|
t.Cleanup(func() {
|
|
activeDisplays = originalCount
|
|
captureDisplayFn = originalCapture
|
|
})
|
|
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
|
|
if err := Now(context.Background(), env); err != nil {
|
|
t.Fatalf("Now failed: %v", err)
|
|
}
|
|
|
|
if len(writer.msgs) != 1 {
|
|
t.Fatalf("expected 1 message, got %d", len(writer.msgs))
|
|
}
|
|
|
|
var frame wire.Frame
|
|
if err := msgpack.Unmarshal(writer.msgs[0], &frame); err != nil {
|
|
t.Fatalf("failed to decode frame: %v", err)
|
|
}
|
|
|
|
if frame.Type != "frame" {
|
|
t.Errorf("expected frame type 'frame', got '%s'", frame.Type)
|
|
}
|
|
}
|
|
|
|
func TestMonitorCount(t *testing.T) {
|
|
count := MonitorCount()
|
|
|
|
// no monitor means weird
|
|
if count < 1 {
|
|
t.Errorf("expected at least 1, got %d", count)
|
|
}
|
|
|
|
t.Logf("MonitorCount returned: %d", count)
|
|
}
|
|
|
|
func TestCaptureAndSend_CancelledContext(t *testing.T) {
|
|
originalCount := activeDisplays
|
|
originalCapture := captureDisplayFn
|
|
activeDisplays = func() int { return 1 }
|
|
captureDisplayFn = func(displayIndex int) (*image.RGBA, error) {
|
|
img := image.NewRGBA(image.Rect(0, 0, 2, 2))
|
|
return img, nil
|
|
}
|
|
t.Cleanup(func() {
|
|
activeDisplays = originalCount
|
|
captureDisplayFn = originalCapture
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // fuck no
|
|
|
|
writer := &recordingWriter{}
|
|
env := &rt.Env{Conn: writer, Cfg: config.Config{}}
|
|
|
|
// SHOULD handle gracefully, may or may not send depending on timing :sob:
|
|
_ = CaptureAndSend(ctx, env)
|
|
}
|