You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/videosink/handler_test.go

276 lines
6.1 KiB
Go

// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package videosink
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"sync"
"testing"
"time"
)
type testCase struct {
name string
opt Options
target string
wantMediaType string
onImage func(*testing.T, image.Image)
}
func (tc *testCase) validatePart(t *testing.T, part *multipart.Part) {
t.Helper()
contentLength, err := strconv.ParseInt(part.Header.Get("Content-Length"), 10, 32)
if err != nil {
t.Errorf("Parsing Content-Length header failed: %v", err)
}
decodeFunc := func(io.Reader) (image.Image, error) {
return nil, errors.New("unknown image format")
}
if mediaType, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")); err != nil {
t.Errorf("ParseMediaType() failed: %v", err)
} else if mediaType != tc.wantMediaType {
t.Errorf("Got content-type %q, want %q", mediaType, tc.wantMediaType)
} else {
switch mediaType {
case "image/png":
decodeFunc = png.Decode
case "image/jpeg":
decodeFunc = jpeg.Decode
}
}
if content, err := ioutil.ReadAll(part); err != nil {
t.Errorf("ReadAll() failed: %v", err)
} else if got, want := len(content), int(contentLength); got != want {
t.Errorf("Read %d bytes, Content-Length header is %d", got, want)
} else if img, err := decodeFunc(bytes.NewReader(content)); err != nil {
t.Errorf("Decoding image failed: %v", err)
} else if got, want := img.Bounds().Size(), (image.Point{tc.opt.Width, tc.opt.Height}); got != want {
t.Errorf("Got image size %v, want %v", got, want)
} else if tc.onImage != nil {
tc.onImage(t, img)
}
if err := part.Close(); err != nil {
t.Errorf("Close() failed: %v", err)
}
}
func (tc *testCase) validateResponse(t *testing.T, resp *http.Response) {
t.Helper()
if got, want := resp.StatusCode, http.StatusOK; got != want {
t.Errorf("ServeHTTP() status %d, want %d", got, want)
}
if mediaType, mediaParams, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil {
t.Errorf("ParseMediaType() failed: %v", err)
} else if got, want := mediaType, "multipart/x-mixed-replace"; got != want {
t.Errorf("Content-Type is %q, want %q", got, want)
} else if boundary, ok := mediaParams["boundary"]; !(ok && len(boundary) > 50) {
t.Errorf("Insufficient boundary: %s", boundary)
} else {
mr := multipart.NewReader(resp.Body, boundary)
for {
if part, err := mr.NextPart(); errors.Is(err, io.EOF) {
break
} else if err != nil {
t.Errorf("NextPart() failed: %v", err)
} else {
tc.validatePart(t, part)
}
}
if _, err := mr.NextPart(); !(errors.Is(err, io.EOF) || strings.HasSuffix(err.Error(), " EOF")) {
t.Errorf("Reading beyond last part didn't fail with EOF: %v", err)
}
}
}
func TestMultipartResponse(t *testing.T) {
for _, tc := range []testCase{
{
name: "defaults",
opt: Options{
Width: 120,
Height: 200,
Format: DefaultFormat,
},
target: "/",
wantMediaType: "image/png",
},
{
name: "default PNG",
opt: Options{
Width: 4,
Height: 4,
Format: PNG,
},
target: "/",
wantMediaType: "image/png",
},
{
name: "default JPEG",
opt: Options{
Width: 200,
Height: 100,
Format: JPEG,
},
target: "/",
wantMediaType: "image/jpeg",
},
{
name: "format param PNG",
opt: Options{
Width: 234,
Height: 123,
Format: JPEG,
},
target: "/?format=png",
wantMediaType: "image/png",
},
{
name: "format param JPEG",
opt: Options{
Width: 123,
Height: 456,
Format: PNG,
},
target: "/?format=jpeg",
wantMediaType: "image/jpeg",
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
t.Cleanup(cancel)
d := New(&tc.opt)
srv := httptest.NewServer(d)
t.Cleanup(srv.Close)
t.Cleanup(srv.CloseClientConnections)
quit := make(chan struct{})
remaining := 10
tc.onImage = func(*testing.T, image.Image) {
if remaining == 0 {
tc.onImage = nil
defer close(quit)
if err := d.Halt(); err != nil {
t.Errorf("Halt() failed: %v", err)
}
} else {
remaining--
}
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
if err := d.Draw(d.Bounds(), image.Black, image.Point{}); err != nil {
t.Errorf("Draw() failed: %v", err)
}
select {
case <-quit:
return
case <-ctx.Done():
return
default:
}
time.Sleep(10 * time.Millisecond)
}
}()
if resp, err := srv.Client().Get(srv.URL + tc.target); err != nil {
t.Errorf("Get() failed: %v", err)
} else {
tc.validateResponse(t, resp)
}
if t.Failed() {
cancel()
}
wg.Wait()
})
}
}
func TestRequestStatus(t *testing.T) {
for _, tc := range []struct {
method string
target string
wantStatus int
}{
{
target: "/?format=",
wantStatus: http.StatusOK,
},
{
target: "/?format=bmp",
wantStatus: http.StatusBadRequest,
},
{
method: http.MethodPost,
target: "/",
wantStatus: http.StatusMethodNotAllowed,
},
} {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
d := New(&Options{
Width: 16,
Height: 16,
})
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
t.Cleanup(cancel)
srv := httptest.NewServer(d)
t.Cleanup(srv.Close)
t.Cleanup(srv.CloseClientConnections)
req, err := http.NewRequestWithContext(ctx, tc.method, srv.URL+tc.target, nil)
if err != nil {
t.Errorf("NewRequest() failed: %v", err)
}
if resp, err := srv.Client().Do(req); err != nil {
t.Errorf("Get() failed: %v", err)
} else if got, want := resp.StatusCode, tc.wantStatus; got != want {
t.Errorf("Request for %s %s returned status %d (%s), want %d",
req.Method, req.URL.String(), got, resp.Status, want)
}
})
}
}