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

     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  }