mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 18:35:34 +00:00
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>
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
image: debian/unstable # TODO(mvdan): switch back to testing once sway hits that repo
|
image: debian/testing
|
||||||
packages:
|
packages:
|
||||||
- curl
|
- curl
|
||||||
- pkg-config
|
- pkg-config
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -55,12 +54,7 @@ func (d *AndroidTestDriver) Start(path string, width, height int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First, build the app.
|
// First, build the app.
|
||||||
dir, err := ioutil.TempDir("", "gio-endtoend-android")
|
apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
|
||||||
if err != nil {
|
|
||||||
d.Fatal(err)
|
|
||||||
}
|
|
||||||
d.Cleanup(func() { os.RemoveAll(dir) })
|
|
||||||
apk := filepath.Join(dir, "e2e.apk")
|
|
||||||
|
|
||||||
// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
|
// 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.
|
// Consider options in the future. On the plus side, this is simple.
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -45,6 +48,10 @@ type TestDriver interface {
|
|||||||
type driverBase struct {
|
type driverBase struct {
|
||||||
*testing.T
|
*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
|
frameNotifs chan bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,3 +261,22 @@ func (d *driverBase) waitForFrame() {
|
|||||||
d.Fatalf("timed out waiting for a frame to be ready")
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -34,12 +32,7 @@ func (d *JSTestDriver) Start(path string, width, height int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First, build the app.
|
// First, build the app.
|
||||||
dir, err := ioutil.TempDir("", "gio-endtoend-js")
|
dir := d.tempDir("gio-endtoend-js")
|
||||||
if err != nil {
|
|
||||||
d.Fatal(err)
|
|
||||||
}
|
|
||||||
d.Cleanup(func() { os.RemoveAll(dir) })
|
|
||||||
|
|
||||||
// TODO(mvdan): This is inefficient, as we link the gogio tool every time.
|
// 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.
|
// Consider options in the future. On the plus side, this is simple.
|
||||||
cmd := exec.Command("go", "run", ".", "-target=js", "-o="+dir, path)
|
cmd := exec.Command("go", "run", ".", "-target=js", "-o="+dir, path)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -47,23 +46,14 @@ func (d *WaylandTestDriver) Start(path string, width, height int) {
|
|||||||
env = append(env, "WLR_BACKENDS=headless")
|
env = append(env, "WLR_BACKENDS=headless")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, prog := range []string{
|
d.needPrograms(
|
||||||
"sway", // to run a wayland compositor
|
"sway", // to run a wayland compositor
|
||||||
"grim", // to take screenshots
|
"grim", // to take screenshots
|
||||||
"swaymsg", // to send input
|
"swaymsg", // to send input
|
||||||
} {
|
)
|
||||||
if _, err := exec.LookPath(prog); err != nil {
|
|
||||||
d.Skipf("%s needed to run", prog)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, build the app.
|
// First, build the app.
|
||||||
dir, err := ioutil.TempDir("", "gio-endtoend-wayland")
|
dir := d.tempDir("gio-endtoend-wayland")
|
||||||
if err != nil {
|
|
||||||
d.Fatal(err)
|
|
||||||
}
|
|
||||||
d.Cleanup(func() { os.RemoveAll(dir) })
|
|
||||||
|
|
||||||
bin := filepath.Join(dir, "red")
|
bin := filepath.Join(dir, "red")
|
||||||
flags := []string{"build", "-tags", "nox11", "-o=" + bin}
|
flags := []string{"build", "-tags", "nox11", "-o=" + bin}
|
||||||
if raceEnabled {
|
if raceEnabled {
|
||||||
|
|||||||
+64
-75
@@ -10,7 +10,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -26,43 +25,8 @@ type X11TestDriver struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *X11TestDriver) Start(path string, width, height int) {
|
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.
|
// First, build the app.
|
||||||
dir, err := ioutil.TempDir("", "gio-endtoend-x11")
|
bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
|
||||||
if err != nil {
|
|
||||||
d.Fatal(err)
|
|
||||||
}
|
|
||||||
d.Cleanup(func() { os.RemoveAll(dir) })
|
|
||||||
|
|
||||||
bin := filepath.Join(dir, "red")
|
|
||||||
flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
|
flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
|
||||||
if raceEnabled {
|
if raceEnabled {
|
||||||
flags = append(flags, "-race")
|
flags = append(flags, "-race")
|
||||||
@@ -76,43 +40,7 @@ func (d *X11TestDriver) Start(path string, width, height int) {
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
d.Cleanup(wg.Wait)
|
d.Cleanup(wg.Wait)
|
||||||
|
|
||||||
// First, start the X server.
|
d.startServer(wg, width, height)
|
||||||
{
|
|
||||||
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()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Then, start our program on the X server above.
|
// Then, start our program on the X server above.
|
||||||
{
|
{
|
||||||
@@ -155,6 +83,67 @@ func (d *X11TestDriver) Start(path string, width, height int) {
|
|||||||
d.waitForFrame()
|
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 {
|
func (d *X11TestDriver) Screenshot() image.Image {
|
||||||
cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
|
cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
|
||||||
cmd.Env = []string{"DISPLAY=" + d.display}
|
cmd.Env = []string{"DISPLAY=" + d.display}
|
||||||
@@ -184,7 +173,7 @@ func (d *X11TestDriver) xdotool(args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *X11TestDriver) Click(x, y int) {
|
func (d *X11TestDriver) Click(x, y int) {
|
||||||
d.xdotool("mousemove", x, y)
|
d.xdotool("mousemove", "--sync", x, y)
|
||||||
d.xdotool("click", "1")
|
d.xdotool("click", "1")
|
||||||
|
|
||||||
// Wait for the gio app to render after this click.
|
// Wait for the gio app to render after this click.
|
||||||
|
|||||||
Reference in New Issue
Block a user