github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/wayland_test.go (about)

     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  }