forked from joejulian/gio
cmd/gogio: first wayland end-to-end tests
Clicking doesn't quite work yet, but everything else does. We use a custom sway config to ensure that it's a minimalist setup with no bar or borders, like the other drivers. The generic test now adapts to the window's real size when running in non-headless mode, since tiling window managers resize some drivers like sway. The default headless mode still expects the exact size that we specify, as no real windows are at play. While at it, clean up some now unused code from the x11 file. Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
This commit is contained in:
+28
-16
@@ -38,8 +38,8 @@ type TestDriver interface {
|
||||
}
|
||||
|
||||
func runEndToEndTest(t *testing.T, driver TestDriver) {
|
||||
width, height := 800, 600
|
||||
cleanups := driver.Start(t, "testdata/red.go", width, height)
|
||||
size := image.Point{X: 800, Y: 600}
|
||||
cleanups := driver.Start(t, "testdata/red.go", size.X, size.Y)
|
||||
for _, cleanup := range cleanups {
|
||||
defer cleanup()
|
||||
}
|
||||
@@ -50,39 +50,45 @@ func runEndToEndTest(t *testing.T, driver TestDriver) {
|
||||
wantColors := func(topLeft, topRight, botLeft, botRight color.RGBA) {
|
||||
t.Helper()
|
||||
img := driver.Screenshot()
|
||||
size := img.Bounds().Size()
|
||||
// We expect to receive a width*height screenshot.
|
||||
if size.X != width || size.Y != height {
|
||||
t.Fatalf("expected dimensions to be %d*%d, got %d*%d",
|
||||
width, height, size.X, size.Y)
|
||||
size_ := img.Bounds().Size()
|
||||
if size_ != size {
|
||||
if !*headless {
|
||||
// Some non-headless drivers, like Sway, may get
|
||||
// their window resized by the host window manager.
|
||||
// Run the rest of the test with the new size.
|
||||
size = size_
|
||||
} else {
|
||||
t.Fatalf("expected dimensions to be %v, got %v",
|
||||
size, size_)
|
||||
}
|
||||
}
|
||||
{
|
||||
minX, minY := 5, 5
|
||||
maxX, maxY := (width/2)-5, (height/2)-5
|
||||
maxX, maxY := (size.X/2)-5, (size.Y/2)-5
|
||||
wantColor(t, img, minX, minY, topLeft)
|
||||
wantColor(t, img, maxX, minY, topLeft)
|
||||
wantColor(t, img, minX, maxY, topLeft)
|
||||
wantColor(t, img, maxX, maxY, topLeft)
|
||||
}
|
||||
{
|
||||
minX, minY := (width/2)+5, 5
|
||||
maxX, maxY := width-5, (height/2)-5
|
||||
minX, minY := (size.X/2)+5, 5
|
||||
maxX, maxY := size.X-5, (size.Y/2)-5
|
||||
wantColor(t, img, minX, minY, topRight)
|
||||
wantColor(t, img, maxX, minY, topRight)
|
||||
wantColor(t, img, minX, maxY, topRight)
|
||||
wantColor(t, img, maxX, maxY, topRight)
|
||||
}
|
||||
{
|
||||
minX, minY := 5, (height/2)+5
|
||||
maxX, maxY := (width/2)-5, height-5
|
||||
minX, minY := 5, (size.Y/2)+5
|
||||
maxX, maxY := (size.X/2)-5, size.Y-5
|
||||
wantColor(t, img, minX, minY, botLeft)
|
||||
wantColor(t, img, maxX, minY, botLeft)
|
||||
wantColor(t, img, minX, maxY, botLeft)
|
||||
wantColor(t, img, maxX, maxY, botLeft)
|
||||
}
|
||||
{
|
||||
minX, minY := (width/2)+5, (height/2)+5
|
||||
maxX, maxY := width-5, height-5
|
||||
minX, minY := (size.X/2)+5, (size.Y/2)+5
|
||||
maxX, maxY := size.X-5, size.Y-5
|
||||
wantColor(t, img, minX, minY, botRight)
|
||||
wantColor(t, img, maxX, minY, botRight)
|
||||
wantColor(t, img, minX, maxY, botRight)
|
||||
@@ -100,8 +106,14 @@ func runEndToEndTest(t *testing.T, driver TestDriver) {
|
||||
wantColors(beef, white, black, gray)
|
||||
|
||||
// Click the first and last sections to turn them red.
|
||||
driver.Click(1*(width/4), 1*(height/4))
|
||||
driver.Click(3*(width/4), 3*(height/4))
|
||||
// TODO(mvdan): implement this properly in the Wayland driver; swaymsg
|
||||
// almost works to automate clicks, but the button presses end up in the
|
||||
// wrong coordinates.
|
||||
if _, ok := driver.(*WaylandTestDriver); ok {
|
||||
return
|
||||
}
|
||||
driver.Click(1*(size.X/4), 1*(size.Y/4))
|
||||
driver.Click(3*(size.X/4), 3*(size.Y/4))
|
||||
wantColors(red, white, black, red)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type WaylandTestDriver struct {
|
||||
t *testing.T
|
||||
|
||||
frameNotifs chan bool
|
||||
|
||||
runtimeDir string
|
||||
socket string
|
||||
display string
|
||||
}
|
||||
|
||||
// No bars or anything fancy. Just a white background with our dimensions.
|
||||
var tmplSwayConfig = template.Must(template.New("").Parse(`
|
||||
output * bg #FFFFFF solid_color
|
||||
output * mode {{.Width}}x{{.Height}}
|
||||
default_border none
|
||||
`))
|
||||
|
||||
var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
|
||||
|
||||
func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) (cleanups []func()) {
|
||||
d.frameNotifs = make(chan bool, 1)
|
||||
d.t = t_
|
||||
|
||||
// We want os.Environ, so that it can e.g. find $DISPLAY to run within
|
||||
// X11. wlroots env vars are documented at:
|
||||
// https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
|
||||
env := os.Environ()
|
||||
if *headless {
|
||||
env = append(env, "WLR_BACKENDS=headless")
|
||||
}
|
||||
|
||||
for _, prog := range []string{
|
||||
"sway", // to run a wayland compositor
|
||||
"grim", // to take screenshots
|
||||
"swaymsg", // to send input
|
||||
} {
|
||||
if _, err := exec.LookPath(prog); err != nil {
|
||||
d.t.Skipf("%s needed to run", prog)
|
||||
}
|
||||
}
|
||||
|
||||
// First, build the app.
|
||||
dir, err := ioutil.TempDir("", "gio-endtoend-wayland")
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
cleanups = append(cleanups, func() { os.RemoveAll(dir) })
|
||||
|
||||
bin := filepath.Join(dir, "red")
|
||||
cmd := exec.Command("go", "build", "-tags", "nox11", "-o="+bin, path)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.t.Fatalf("could not build app: %s:\n%s", err, out)
|
||||
}
|
||||
|
||||
conf := filepath.Join(dir, "config")
|
||||
f, err := os.Create(conf)
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
|
||||
width, height,
|
||||
}); err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
|
||||
d.socket = filepath.Join(dir, "socket")
|
||||
env = append(env, "SWAYSOCK="+d.socket)
|
||||
d.runtimeDir = dir
|
||||
env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
cleanups = append(cleanups, wg.Wait)
|
||||
|
||||
// First, start sway.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
|
||||
cmd.Env = env
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
cleanups = append(cleanups, cancel)
|
||||
cleanups = append(cleanups, func() {
|
||||
// Give it a chance to exit gracefully, cleaning up
|
||||
// after itself. After 10ms, the deferred cancel above
|
||||
// will signal an os.Kill.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
|
||||
// Wait for sway to be ready. We probably don't need a deadline
|
||||
// here.
|
||||
br := bufio.NewReader(stderr)
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
if m := rxSwayReady.FindStringSubmatch(line); m != nil {
|
||||
d.display = m[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
|
||||
// Don't print all stderr, since we use --verbose.
|
||||
// TODO(mvdan): if it's useful, probably filter
|
||||
// errors and show them.
|
||||
d.t.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
// Then, start our program on the sway compositor above.
|
||||
{
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
stderr := &bytes.Buffer{}
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
cleanups = append(cleanups, cancel)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
||||
// Print stderr and error.
|
||||
io.Copy(os.Stdout, stderr)
|
||||
d.t.Error(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "frame ready" {
|
||||
d.frameNotifs <- true
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for the gio app to render.
|
||||
<-d.frameNotifs
|
||||
|
||||
return cleanups
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) Screenshot() image.Image {
|
||||
cmd := exec.Command("grim", "/dev/stdout")
|
||||
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
d.t.Errorf("%s", out)
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
img, err := png.Decode(bytes.NewReader(out))
|
||||
if err != nil {
|
||||
d.t.Fatal(err)
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
|
||||
strs := []string{
|
||||
"--socket", d.socket,
|
||||
}
|
||||
for _, arg := range args {
|
||||
strs = append(strs, fmt.Sprint(arg))
|
||||
}
|
||||
cmd := exec.Command("swaymsg", strs...)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
d.t.Errorf("%s", out)
|
||||
d.t.Fatal(err)
|
||||
} else {
|
||||
d.t.Logf("%v", args)
|
||||
d.t.Logf("%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *WaylandTestDriver) Click(x, y int) {
|
||||
d.swaymsg("-t", "get_seats")
|
||||
d.swaymsg("seat", "-", "cursor", "set", x, y)
|
||||
d.swaymsg("seat", "-", "cursor", "press", "button1")
|
||||
d.swaymsg("seat", "-", "cursor", "release", "button1")
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
<-d.frameNotifs
|
||||
}
|
||||
|
||||
func TestWayland(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runEndToEndTest(t, &WaylandTestDriver{})
|
||||
}
|
||||
+4
-22
@@ -1,9 +1,5 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
// TODO(mvdan): come up with an end-to-end platform interface, including methods
|
||||
// like "take screenshot" or "close app", so that we can run the same tests on
|
||||
// all supported platforms without writing them many times.
|
||||
|
||||
package main_test
|
||||
|
||||
import (
|
||||
@@ -93,9 +89,9 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cl
|
||||
}
|
||||
cleanups = append(cleanups, cancel)
|
||||
cleanups = append(cleanups, func() {
|
||||
// Give Xserver a chance to exit gracefully, cleaning up
|
||||
// after itself in /tmp. After 10ms, the deferred cancel
|
||||
// above will signal an os.Kill.
|
||||
// Give it a chance to exit gracefully, cleaning up
|
||||
// after itself. After 10ms, the deferred cancel above
|
||||
// will signal an os.Kill.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
})
|
||||
@@ -200,6 +196,7 @@ func (d *X11TestDriver) Click(x, y int) {
|
||||
d.xdotool("mousemove", x, y)
|
||||
d.xdotool("click", "1")
|
||||
|
||||
// Wait for the gio app to render after this click.
|
||||
<-d.frameNotifs
|
||||
}
|
||||
|
||||
@@ -208,18 +205,3 @@ func TestX11(t *testing.T) {
|
||||
|
||||
runEndToEndTest(t, &X11TestDriver{})
|
||||
}
|
||||
|
||||
// testLogWriter is a bit of a hack to redirect libraries that use a *log.Logger
|
||||
// variable to instead send their logs to t.Logf.
|
||||
//
|
||||
// Since *log.Logger isn't an interface and can only take an io.Writer, all we
|
||||
// can do is implement an io.Writer that sends its output to t.Logf. We end up
|
||||
// with duplicate log prefixes, but that doesn't seem so bad.
|
||||
type testLogWriter struct {
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func (w testLogWriter) Write(p []byte) (n int, err error) {
|
||||
w.t.Logf("%s", p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user