Files
gio/cmd/gogio/wayland_test.go
T
Daniel Martí 12ce899bb5 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>
2019-11-02 18:12:46 +01:00

235 lines
5.4 KiB
Go

// 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{})
}