From b064899967b1c7b953a1aa429a4041c850c52a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Sun, 1 Mar 2020 15:47:59 +0000 Subject: [PATCH] cmd/gogio: groundwork for Windows e2e tests on Wine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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í --- .builds/linux.yml | 2 +- cmd/gogio/android_test.go | 8 +-- cmd/gogio/e2e_test.go | 26 +++++++ cmd/gogio/js_test.go | 9 +-- cmd/gogio/wayland_test.go | 16 +---- cmd/gogio/x11_test.go | 139 ++++++++++++++++++-------------------- 6 files changed, 96 insertions(+), 104 deletions(-) diff --git a/.builds/linux.yml b/.builds/linux.yml index 220a51a1..c4999802 100644 --- a/.builds/linux.yml +++ b/.builds/linux.yml @@ -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 diff --git a/cmd/gogio/android_test.go b/cmd/gogio/android_test.go index 9fe87f86..e9045012 100644 --- a/cmd/gogio/android_test.go +++ b/cmd/gogio/android_test.go @@ -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. diff --git a/cmd/gogio/e2e_test.go b/cmd/gogio/e2e_test.go index c6f5f066..79d6b259 100644 --- a/cmd/gogio/e2e_test.go +++ b/cmd/gogio/e2e_test.go @@ -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 +} diff --git a/cmd/gogio/js_test.go b/cmd/gogio/js_test.go index b8fdcfcd..3ccf7a2d 100644 --- a/cmd/gogio/js_test.go +++ b/cmd/gogio/js_test.go @@ -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) diff --git a/cmd/gogio/wayland_test.go b/cmd/gogio/wayland_test.go index 8288a670..cb1a64b4 100644 --- a/cmd/gogio/wayland_test.go +++ b/cmd/gogio/wayland_test.go @@ -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 { diff --git a/cmd/gogio/x11_test.go b/cmd/gogio/x11_test.go index 4dd9fa73..458cdba1 100644 --- a/cmd/gogio/x11_test.go +++ b/cmd/gogio/x11_test.go @@ -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.