~eliasnaur/gio

b064899967b1c7b953a1aa429a4041c850c52a47 — Daniel Martí a month ago 48eb5c6
cmd/gogio: groundwork for Windows e2e tests on Wine

First, move from debian unstable to testing, since sway was promoted to
testing as of earlier this week.

Second, use the --sync option when using xdotool to move an X11 mouse.
This makes the command block until the mouse has finished moving to the
specified location, removing a potential race with the following
'xdotool click' command.

Third, deduplicate some logic into driverBase: tempDir to create a
temporary directory within a test, and needPrograms to skip a test if
the required programs aren't available.

Lastly, split the code that starts the X11 server into a method, so that
the future Wine e2e driver can reuse it. Since Wine is tightly coupled
with X11, we can reuse a good part of the code, including the X11 server
and the xdotool mechanisms.

We also add a TODO to perhaps improve the handling of the app's output
under each of the e2e test cases.

Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
M .builds/linux.yml => .builds/linux.yml +1 -1
@@ 1,4 1,4 @@
image: debian/unstable # TODO(mvdan): switch back to testing once sway hits that repo
image: debian/testing
packages:
 - curl
 - pkg-config

M cmd/gogio/android_test.go => cmd/gogio/android_test.go +1 -7
@@ 9,7 9,6 @@ import (
	"fmt"
	"image"
	"image/png"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"


@@ 55,12 54,7 @@ func (d *AndroidTestDriver) Start(path string, width, height int) {
	}

	// First, build the app.
	dir, err := ioutil.TempDir("", "gio-endtoend-android")
	if err != nil {
		d.Fatal(err)
	}
	d.Cleanup(func() { os.RemoveAll(dir) })
	apk := filepath.Join(dir, "e2e.apk")
	apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")

	// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
	// Consider options in the future. On the plus side, this is simple.

M cmd/gogio/e2e_test.go => cmd/gogio/e2e_test.go +26 -0
@@ 8,6 8,9 @@ import (
	"fmt"
	"image"
	"image/color"
	"io/ioutil"
	"os"
	"os/exec"
	"strings"
	"testing"
	"time"


@@ 45,6 48,10 @@ type TestDriver interface {
type driverBase struct {
	*testing.T

	// TODO(mvdan): Make this lower-level, so that each driver can simply
	// send us each line of output from the app. That will let us
	// deduplicate some code, and also show app output as test logs in a
	// consistent way.
	frameNotifs chan bool
}



@@ 254,3 261,22 @@ func (d *driverBase) waitForFrame() {
		d.Fatalf("timed out waiting for a frame to be ready")
	}
}

func (d *driverBase) needPrograms(names ...string) {
	d.Helper()
	for _, name := range names {
		if _, err := exec.LookPath(name); err != nil {
			d.Skipf("%s needed to run", name)
		}
	}
}

func (d *driverBase) tempDir(name string) string {
	d.Helper()
	dir, err := ioutil.TempDir("", name)
	if err != nil {
		d.Fatal(err)
	}
	d.Cleanup(func() { os.RemoveAll(dir) })
	return dir
}

M cmd/gogio/js_test.go => cmd/gogio/js_test.go +1 -8
@@ 8,10 8,8 @@ import (
	"errors"
	"image"
	"image/png"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"strings"



@@ 34,12 32,7 @@ func (d *JSTestDriver) Start(path string, width, height int) {
	}

	// First, build the app.
	dir, err := ioutil.TempDir("", "gio-endtoend-js")
	if err != nil {
		d.Fatal(err)
	}
	d.Cleanup(func() { os.RemoveAll(dir) })

	dir := d.tempDir("gio-endtoend-js")
	// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
	// Consider options in the future. On the plus side, this is simple.
	cmd := exec.Command("go", "run", ".", "-target=js", "-o="+dir, path)

M cmd/gogio/wayland_test.go => cmd/gogio/wayland_test.go +3 -13
@@ 10,7 10,6 @@ import (
	"image"
	"image/png"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"


@@ 47,23 46,14 @@ func (d *WaylandTestDriver) Start(path string, width, height int) {
		env = append(env, "WLR_BACKENDS=headless")
	}

	for _, prog := range []string{
	d.needPrograms(
		"sway",    // to run a wayland compositor
		"grim",    // to take screenshots
		"swaymsg", // to send input
	} {
		if _, err := exec.LookPath(prog); err != nil {
			d.Skipf("%s needed to run", prog)
		}
	}
	)

	// First, build the app.
	dir, err := ioutil.TempDir("", "gio-endtoend-wayland")
	if err != nil {
		d.Fatal(err)
	}
	d.Cleanup(func() { os.RemoveAll(dir) })

	dir := d.tempDir("gio-endtoend-wayland")
	bin := filepath.Join(dir, "red")
	flags := []string{"build", "-tags", "nox11", "-o=" + bin}
	if raceEnabled {

M cmd/gogio/x11_test.go => cmd/gogio/x11_test.go +64 -75
@@ 10,7 10,6 @@ import (
	"image"
	"image/png"
	"io"
	"io/ioutil"
	"math/rand"
	"os"
	"os/exec"


@@ 26,43 25,8 @@ type X11TestDriver struct {
}

func (d *X11TestDriver) Start(path string, width, height int) {
	// Pick a random display number between 1 and 100,000. Most machines
	// will only be using :0, so there's only a 0.001% chance of two
	// concurrent test runs to run into a conflict.
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)

	var xprog string
	xflags := []string{
		"-wr", // we want a white background; the default is black
	}
	if *headless {
		xprog = "Xvfb" // virtual X server
		xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
	} else {
		xprog = "Xephyr" // nested X server as a window
		xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
	}
	xflags = append(xflags, d.display)

	for _, prog := range []string{
		xprog,     // to run the X server
		"scrot",   // to take screenshots
		"xdotool", // to send input
	} {
		if _, err := exec.LookPath(prog); err != nil {
			d.Skipf("%s needed to run", prog)
		}
	}

	// First, build the app.
	dir, err := ioutil.TempDir("", "gio-endtoend-x11")
	if err != nil {
		d.Fatal(err)
	}
	d.Cleanup(func() { os.RemoveAll(dir) })

	bin := filepath.Join(dir, "red")
	bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
	flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
	if raceEnabled {
		flags = append(flags, "-race")


@@ 76,43 40,7 @@ func (d *X11TestDriver) Start(path string, width, height int) {
	var wg sync.WaitGroup
	d.Cleanup(wg.Wait)

	// First, start the X server.
	{
		ctx, cancel := context.WithCancel(context.Background())
		cmd := exec.CommandContext(ctx, xprog, xflags...)
		combined := &bytes.Buffer{}
		cmd.Stdout = combined
		cmd.Stderr = combined
		if err := cmd.Start(); err != nil {
			d.Fatal(err)
		}
		d.Cleanup(cancel)
		d.Cleanup(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 the X server to be ready. The socket path isn't
		// terribly portable, but that's okay for now.
		withRetries(d.T, time.Second, func() error {
			socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
			_, err := os.Stat(socket)
			return err
		})

		wg.Add(1)
		go func() {
			if err := cmd.Wait(); err != nil && ctx.Err() == nil {
				// Print all output and error.
				io.Copy(os.Stdout, combined)
				d.Error(err)
			}
			wg.Done()
		}()
	}
	d.startServer(wg, width, height)

	// Then, start our program on the X server above.
	{


@@ 155,6 83,67 @@ func (d *X11TestDriver) Start(path string, width, height int) {
	d.waitForFrame()
}

func (d *X11TestDriver) startServer(wg sync.WaitGroup, width, height int) {
	// Pick a random display number between 1 and 100,000. Most machines
	// will only be using :0, so there's only a 0.001% chance of two
	// concurrent test runs to run into a conflict.
	rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
	d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)

	var xprog string
	xflags := []string{
		"-wr", // we want a white background; the default is black
	}
	if *headless {
		xprog = "Xvfb" // virtual X server
		xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
	} else {
		xprog = "Xephyr" // nested X server as a window
		xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
	}
	xflags = append(xflags, d.display)

	d.needPrograms(
		xprog,     // to run the X server
		"scrot",   // to take screenshots
		"xdotool", // to send input
	)
	ctx, cancel := context.WithCancel(context.Background())
	cmd := exec.CommandContext(ctx, xprog, xflags...)
	combined := &bytes.Buffer{}
	cmd.Stdout = combined
	cmd.Stderr = combined
	if err := cmd.Start(); err != nil {
		d.Fatal(err)
	}
	d.Cleanup(cancel)
	d.Cleanup(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 the X server to be ready. The socket path isn't
	// terribly portable, but that's okay for now.
	withRetries(d.T, time.Second, func() error {
		socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
		_, err := os.Stat(socket)
		return err
	})

	wg.Add(1)
	go func() {
		if err := cmd.Wait(); err != nil && ctx.Err() == nil {
			// Print all output and error.
			io.Copy(os.Stdout, combined)
			d.Error(err)
		}
		wg.Done()
	}()
}

func (d *X11TestDriver) Screenshot() image.Image {
	cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
	cmd.Env = []string{"DISPLAY=" + d.display}


@@ 184,7 173,7 @@ func (d *X11TestDriver) xdotool(args ...interface{}) {
}

func (d *X11TestDriver) Click(x, y int) {
	d.xdotool("mousemove", x, y)
	d.xdotool("mousemove", "--sync", x, y)
	d.xdotool("click", "1")

	// Wait for the gio app to render after this click.