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:
Daniel Martí
2019-11-02 17:07:13 +00:00
committed by Elias Naur
parent 5eeaadccea
commit 12ce899bb5
3 changed files with 266 additions and 38 deletions
+28 -16
View File
@@ -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)
}
+234
View File
@@ -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
View File
@@ -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
}