x11_test.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package main_test
   4  
   5  import (
   6  	"bytes"
   7  	"context"
   8  	"fmt"
   9  	"image"
  10  	"image/png"
  11  	"io"
  12  	"math/rand"
  13  	"os"
  14  	"os/exec"
  15  	"path/filepath"
  16  	"sync"
  17  	"time"
  18  )
  19  
  20  type X11TestDriver struct {
  21  	driverBase
  22  
  23  	display string
  24  }
  25  
  26  func (d *X11TestDriver) Start(path string) {
  27  	// First, build the app.
  28  	bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
  29  	flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
  30  	if raceEnabled {
  31  		flags = append(flags, "-race")
  32  	}
  33  	flags = append(flags, path)
  34  	cmd := exec.Command("go", flags...)
  35  	if out, err := cmd.CombinedOutput(); err != nil {
  36  		d.Fatalf("could not build app: %s:\n%s", err, out)
  37  	}
  38  
  39  	var wg sync.WaitGroup
  40  	d.Cleanup(wg.Wait)
  41  
  42  	d.startServer(&wg, d.width, d.height)
  43  
  44  	// Then, start our program on the X server above.
  45  	{
  46  		ctx, cancel := context.WithCancel(context.Background())
  47  		cmd := exec.CommandContext(ctx, bin)
  48  		cmd.Env = []string{"DISPLAY=" + d.display}
  49  		output, err := cmd.StdoutPipe()
  50  		if err != nil {
  51  			d.Fatal(err)
  52  		}
  53  		cmd.Stderr = cmd.Stdout
  54  		d.output = output
  55  		if err := cmd.Start(); err != nil {
  56  			d.Fatal(err)
  57  		}
  58  		d.Cleanup(cancel)
  59  		wg.Add(1)
  60  		go func() {
  61  			if err := cmd.Wait(); err != nil && ctx.Err() == nil {
  62  				d.Error(err)
  63  			}
  64  			wg.Done()
  65  		}()
  66  	}
  67  
  68  	// Wait for the gio app to render.
  69  	d.waitForFrame()
  70  }
  71  
  72  func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
  73  	// Pick a random display number between 1 and 100,000. Most machines
  74  	// will only be using :0, so there's only a 0.001% chance of two
  75  	// concurrent test runs to run into a conflict.
  76  	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
  77  	d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
  78  
  79  	var xprog string
  80  	xflags := []string{
  81  		"-wr", // we want a white background; the default is black
  82  	}
  83  	if *headless {
  84  		xprog = "Xvfb" // virtual X server
  85  		xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
  86  	} else {
  87  		xprog = "Xephyr" // nested X server as a window
  88  		xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
  89  	}
  90  	xflags = append(xflags, d.display)
  91  
  92  	d.needPrograms(
  93  		xprog,     // to run the X server
  94  		"scrot",   // to take screenshots
  95  		"xdotool", // to send input
  96  	)
  97  	ctx, cancel := context.WithCancel(context.Background())
  98  	cmd := exec.CommandContext(ctx, xprog, xflags...)
  99  	combined := &bytes.Buffer{}
 100  	cmd.Stdout = combined
 101  	cmd.Stderr = combined
 102  	if err := cmd.Start(); err != nil {
 103  		d.Fatal(err)
 104  	}
 105  	d.Cleanup(cancel)
 106  	d.Cleanup(func() {
 107  		// Give it a chance to exit gracefully, cleaning up
 108  		// after itself. After 10ms, the deferred cancel above
 109  		// will signal an os.Kill.
 110  		cmd.Process.Signal(os.Interrupt)
 111  		time.Sleep(10 * time.Millisecond)
 112  	})
 113  
 114  	// Wait for the X server to be ready. The socket path isn't
 115  	// terribly portable, but that's okay for now.
 116  	withRetries(d.T, time.Second, func() error {
 117  		socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
 118  		_, err := os.Stat(socket)
 119  		return err
 120  	})
 121  
 122  	wg.Add(1)
 123  	go func() {
 124  		if err := cmd.Wait(); err != nil && ctx.Err() == nil {
 125  			// Print all output and error.
 126  			io.Copy(os.Stdout, combined)
 127  			d.Error(err)
 128  		}
 129  		wg.Done()
 130  	}()
 131  }
 132  
 133  func (d *X11TestDriver) Screenshot() image.Image {
 134  	cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
 135  	cmd.Env = []string{"DISPLAY=" + d.display}
 136  	out, err := cmd.CombinedOutput()
 137  	if err != nil {
 138  		d.Errorf("%s", out)
 139  		d.Fatal(err)
 140  	}
 141  	img, err := png.Decode(bytes.NewReader(out))
 142  	if err != nil {
 143  		d.Fatal(err)
 144  	}
 145  	return img
 146  }
 147  
 148  func (d *X11TestDriver) xdotool(args ...interface{}) string {
 149  	d.Helper()
 150  	strs := make([]string, len(args))
 151  	for i, arg := range args {
 152  		strs[i] = fmt.Sprint(arg)
 153  	}
 154  	cmd := exec.Command("xdotool", strs...)
 155  	cmd.Env = []string{"DISPLAY=" + d.display}
 156  	out, err := cmd.CombinedOutput()
 157  	if err != nil {
 158  		d.Errorf("%s", out)
 159  		d.Fatal(err)
 160  	}
 161  	return string(bytes.TrimSpace(out))
 162  }
 163  
 164  func (d *X11TestDriver) Click(x, y int) {
 165  	d.xdotool("mousemove", "--sync", x, y)
 166  	d.xdotool("click", "1")
 167  
 168  	// Wait for the gio app to render after this click.
 169  	d.waitForFrame()
 170  }
 171