wayland_test.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package main_test
   4  
   5  import (
   6  	"bufio"
   7  	"bytes"
   8  	"context"
   9  	"fmt"
  10  	"image"
  11  	"image/png"
  12  	"os"
  13  	"os/exec"
  14  	"path/filepath"
  15  	"regexp"
  16  	"strings"
  17  	"sync"
  18  	"text/template"
  19  	"time"
  20  )
  21  
  22  type WaylandTestDriver struct {
  23  	driverBase
  24  
  25  	runtimeDir string
  26  	socket     string
  27  	display    string
  28  }
  29  
  30  // No bars or anything fancy. Just a white background with our dimensions.
  31  var tmplSwayConfig = template.Must(template.New("").Parse(`
  32  output * bg #FFFFFF solid_color
  33  output * mode {{.Width}}x{{.Height}}
  34  default_border none
  35  `))
  36  
  37  var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
  38  
  39  func (d *WaylandTestDriver) Start(path string) {
  40  	// We want os.Environ, so that it can e.g. find $DISPLAY to run within
  41  	// X11. wlroots env vars are documented at:
  42  	// https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
  43  	env := os.Environ()
  44  	if *headless {
  45  		env = append(env, "WLR_BACKENDS=headless")
  46  	}
  47  
  48  	d.needPrograms(
  49  		"sway",    // to run a wayland compositor
  50  		"grim",    // to take screenshots
  51  		"swaymsg", // to send input
  52  	)
  53  
  54  	// First, build the app.
  55  	dir := d.tempDir("gio-endtoend-wayland")
  56  	bin := filepath.Join(dir, "red")
  57  	flags := []string{"build", "-tags", "nox11", "-o=" + bin}
  58  	if raceEnabled {
  59  		flags = append(flags, "-race")
  60  	}
  61  	flags = append(flags, path)
  62  	cmd := exec.Command("go", flags...)
  63  	if out, err := cmd.CombinedOutput(); err != nil {
  64  		d.Fatalf("could not build app: %s:\n%s", err, out)
  65  	}
  66  
  67  	conf := filepath.Join(dir, "config")
  68  	f, err := os.Create(conf)
  69  	if err != nil {
  70  		d.Fatal(err)
  71  	}
  72  	defer f.Close()
  73  	if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
  74  		d.width, d.height,
  75  	}); err != nil {
  76  		d.Fatal(err)
  77  	}
  78  
  79  	d.socket = filepath.Join(dir, "socket")
  80  	env = append(env, "SWAYSOCK="+d.socket)
  81  	d.runtimeDir = dir
  82  	env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
  83  
  84  	var wg sync.WaitGroup
  85  	d.Cleanup(wg.Wait)
  86  
  87  	// First, start sway.
  88  	{
  89  		ctx, cancel := context.WithCancel(context.Background())
  90  		cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
  91  		cmd.Env = env
  92  		stderr, err := cmd.StderrPipe()
  93  		if err != nil {
  94  			d.Fatal(err)
  95  		}
  96  		if err := cmd.Start(); err != nil {
  97  			d.Fatal(err)
  98  		}
  99  		d.Cleanup(cancel)
 100  		d.Cleanup(func() {
 101  			// Give it a chance to exit gracefully, cleaning up
 102  			// after itself. After 10ms, the deferred cancel above
 103  			// will signal an os.Kill.
 104  			cmd.Process.Signal(os.Interrupt)
 105  			time.Sleep(10 * time.Millisecond)
 106  		})
 107  
 108  		// Wait for sway to be ready. We probably don't need a deadline
 109  		// here.
 110  		br := bufio.NewReader(stderr)
 111  		for {
 112  			line, err := br.ReadString('\n')
 113  			if err != nil {
 114  				d.Fatal(err)
 115  			}
 116  			if m := rxSwayReady.FindStringSubmatch(line); m != nil {
 117  				d.display = m[1]
 118  				break
 119  			}
 120  		}
 121  
 122  		wg.Add(1)
 123  		go func() {
 124  			if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
 125  				// Don't print all stderr, since we use --verbose.
 126  				// TODO(mvdan): if it's useful, probably filter
 127  				// errors and show them.
 128  				d.Error(err)
 129  			}
 130  			wg.Done()
 131  		}()
 132  	}
 133  
 134  	// Then, start our program on the sway compositor above.
 135  	{
 136  		ctx, cancel := context.WithCancel(context.Background())
 137  		cmd := exec.CommandContext(ctx, bin)
 138  		cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
 139  		output, err := cmd.StdoutPipe()
 140  		if err != nil {
 141  			d.Fatal(err)
 142  		}
 143  		cmd.Stderr = cmd.Stdout
 144  		d.output = output
 145  		if err := cmd.Start(); err != nil {
 146  			d.Fatal(err)
 147  		}
 148  		d.Cleanup(cancel)
 149  		wg.Add(1)
 150  		go func() {
 151  			if err := cmd.Wait(); err != nil && ctx.Err() == nil {
 152  				d.Error(err)
 153  			}
 154  			wg.Done()
 155  		}()
 156  	}
 157  
 158  	// Wait for the gio app to render.
 159  	d.waitForFrame()
 160  }
 161  
 162  func (d *WaylandTestDriver) Screenshot() image.Image {
 163  	cmd := exec.Command("grim", "/dev/stdout")
 164  	cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
 165  	out, err := cmd.CombinedOutput()
 166  	if err != nil {
 167  		d.Errorf("%s", out)
 168  		d.Fatal(err)
 169  	}
 170  	img, err := png.Decode(bytes.NewReader(out))
 171  	if err != nil {
 172  		d.Fatal(err)
 173  	}
 174  	return img
 175  }
 176  
 177  func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
 178  	strs := []string{"--socket", d.socket}
 179  	for _, arg := range args {
 180  		strs = append(strs, fmt.Sprint(arg))
 181  	}
 182  	cmd := exec.Command("swaymsg", strs...)
 183  	if out, err := cmd.CombinedOutput(); err != nil {
 184  		d.Errorf("%s", out)
 185  		d.Fatal(err)
 186  	}
 187  }
 188  
 189  func (d *WaylandTestDriver) Click(x, y int) {
 190  	d.swaymsg("seat", "-", "cursor", "set", x, y)
 191  	d.swaymsg("seat", "-", "cursor", "press", "button1")
 192  	d.swaymsg("seat", "-", "cursor", "release", "button1")
 193  
 194  	// Wait for the gio app to render after this click.
 195  	d.waitForFrame()
 196  }
 197