diff --git a/cmd/gogio/android_test.go b/cmd/gogio/android_test.go index 65713923..9fe87f86 100644 --- a/cmd/gogio/android_test.go +++ b/cmd/gogio/android_test.go @@ -15,14 +15,11 @@ import ( "path/filepath" "regexp" "strings" - "testing" "time" ) type AndroidTestDriver struct { - t *testing.T - - frameNotifs chan bool + driverBase sdkDir string adbPath string @@ -30,13 +27,10 @@ type AndroidTestDriver struct { var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`) -func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) { - d.frameNotifs = make(chan bool, 1) - d.t = t_ - +func (d *AndroidTestDriver) Start(path string, width, height int) { d.sdkDir = os.Getenv("ANDROID_HOME") if d.sdkDir == "" { - d.t.Skipf("Android SDK is required; set $ANDROID_HOME") + d.Skipf("Android SDK is required; set $ANDROID_HOME") } d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb") @@ -44,10 +38,10 @@ func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) devices := rxAdbDevice.FindAllSubmatch(devOut, -1) switch len(devices) { case 0: - d.t.Skipf("no Android devices attached via adb; skipping") + d.Skipf("no Android devices attached via adb; skipping") case 1: default: - d.t.Skipf("multiple Android devices attached via adb; skipping") + d.Skipf("multiple Android devices attached via adb; skipping") } // If the device is attached but asleep, it's probably just charging. @@ -57,29 +51,29 @@ func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) d.adb("shell", "dumpsys", "power"), []byte(" mWakefulness=Awake"), ) { - d.t.Skipf("Android device isn't awake; skipping") + d.Skipf("Android device isn't awake; skipping") } // First, build the app. dir, err := ioutil.TempDir("", "gio-endtoend-android") if err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(func() { os.RemoveAll(dir) }) + 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. // Consider options in the future. On the plus side, this is simple. cmd := exec.Command("go", "run", ".", "-target=android", "-appid="+appid, "-o="+apk, path) if out, err := cmd.CombinedOutput(); err != nil { - d.t.Fatalf("could not build app: %s:\n%s", err, out) + d.Fatalf("could not build app: %s:\n%s", err, out) } // Make sure the app isn't installed already, and try to uninstall it // when we finish. Previous failed test runs might have left the app. d.tryUninstall() d.adb("install", apk) - d.t.Cleanup(d.tryUninstall) + d.Cleanup(d.tryUninstall) // Force our e2e app to be fullscreen, so that the android system bar at // the top doesn't mess with our screenshots. @@ -101,13 +95,13 @@ func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) ) logcat, err := cmd.StdoutPipe() if err != nil { - d.t.Fatal(err) + d.Fatal(err) } cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(cancel) + d.Cleanup(cancel) go func() { scanner := bufio.NewScanner(logcat) for scanner.Scan() { @@ -130,14 +124,14 @@ func (d *AndroidTestDriver) Start(t_ *testing.T, path string, width, height int) time.Sleep(500 * time.Millisecond) // Wait for the gio app to render. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } func (d *AndroidTestDriver) Screenshot() image.Image { out := d.adb("shell", "screencap", "-p") img, err := png.Decode(bytes.NewReader(out)) if err != nil { - d.t.Fatal(err) + d.Fatal(err) } return img } @@ -150,7 +144,7 @@ func (d *AndroidTestDriver) tryUninstall() { // The package is not installed. Don't log anything. return } - d.t.Logf("could not uninstall: %v\n%s", err, out) + d.Logf("could not uninstall: %v\n%s", err, out) } } @@ -162,8 +156,8 @@ func (d *AndroidTestDriver) adb(args ...interface{}) []byte { cmd := exec.Command(d.adbPath, strs...) out, err := cmd.CombinedOutput() if err != nil { - d.t.Errorf("%s", out) - d.t.Fatal(err) + d.Errorf("%s", out) + d.Fatal(err) } return out } @@ -172,5 +166,5 @@ func (d *AndroidTestDriver) Click(x, y int) { d.adb("shell", "input", "tap", x, y) // Wait for the gio app to render after this click. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } diff --git a/cmd/gogio/e2e_test.go b/cmd/gogio/e2e_test.go index b7f6a9ef..5d296178 100644 --- a/cmd/gogio/e2e_test.go +++ b/cmd/gogio/e2e_test.go @@ -20,14 +20,15 @@ const appid = "localhost.gogio.endtoend" // tests on. None of its methods return any errors, as the errors are directly // reported to testing.T via methods like Fatal. type TestDriver interface { - // Start provides the test driver with a testing.T, as well as the path - // to the Gio app to use for the test. The driver should attempt to run - // the app with the given width and height, and the platform's + initBase(*testing.T) + + // Start opens the Gio app found at path. The driver should attempt to + // run the app with the given width and height, and the platform's // background should be white. // // When the function returns, the gio app must be ready to use on the // platform, with its initial frame fully drawn. - Start(t *testing.T, path string, width, height int) + Start(path string, width, height int) // Screenshot takes a screenshot of the Gio app on the platform. Screenshot() image.Image @@ -38,6 +39,17 @@ type TestDriver interface { Click(x, y int) } +type driverBase struct { + *testing.T + + frameNotifs chan bool +} + +func (d *driverBase) initBase(t *testing.T) { + d.T = t + d.frameNotifs = make(chan bool, 1) +} + func TestEndToEnd(t *testing.T) { if testing.Short() { t.Skipf("end-to-end tests tend to be slow") @@ -66,9 +78,11 @@ func TestEndToEnd(t *testing.T) { } func runEndToEndTest(t *testing.T, driver TestDriver) { + driver.initBase(t) + size := image.Point{X: 800, Y: 600} t.Log("starting driver and gio app") - driver.Start(t, "testdata/red.go", size.X, size.Y) + driver.Start("testdata/red.go", size.X, size.Y) // The colors are split in four rectangular sections. Check the corners // of each of the sections. We check the corners left to right, top to @@ -146,8 +160,8 @@ func wantColor(t *testing.T, img image.Image, x, y int, want color.Color) { } } -func waitForFrame(t *testing.T, frameNotifs <-chan bool) { - t.Helper() +func (d *driverBase) waitForFrame() { + d.Helper() // Unfortunately, there isn't a way to select on a test failing, since // testing.T doesn't have anything like a context or a "done" channel. @@ -158,8 +172,8 @@ func waitForFrame(t *testing.T, frameNotifs <-chan bool) { // For now, a static short timeout is better than nothing. 2s is plenty // for our simple test app to render on any device. select { - case <-frameNotifs: + case <-d.frameNotifs: case <-time.After(5 * time.Second): - t.Fatalf("timed out waiting for a frame to be ready") + d.Fatalf("timed out waiting for a frame to be ready") } } diff --git a/cmd/gogio/js_test.go b/cmd/gogio/js_test.go index 3b7df634..b8fdcfcd 100644 --- a/cmd/gogio/js_test.go +++ b/cmd/gogio/js_test.go @@ -14,7 +14,6 @@ import ( "os" "os/exec" "strings" - "testing" "github.com/chromedp/cdproto/runtime" "github.com/chromedp/chromedp" @@ -23,34 +22,29 @@ import ( ) type JSTestDriver struct { - t *testing.T - - frameNotifs chan bool + driverBase // ctx is the chromedp context. ctx context.Context } -func (d *JSTestDriver) Start(t_ *testing.T, path string, width, height int) { - d.frameNotifs = make(chan bool, 1) - d.t = t_ - +func (d *JSTestDriver) Start(path string, width, height int) { if raceEnabled { - d.t.Skipf("js/wasm doesn't support -race; skipping") + d.Skipf("js/wasm doesn't support -race; skipping") } // First, build the app. dir, err := ioutil.TempDir("", "gio-endtoend-js") if err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(func() { os.RemoveAll(dir) }) + d.Cleanup(func() { os.RemoveAll(dir) }) // 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) if out, err := cmd.CombinedOutput(); err != nil { - d.t.Fatalf("could not build app: %s:\n%s", err, out) + d.Fatalf("could not build app: %s:\n%s", err, out) } // Second, start Chrome. @@ -75,21 +69,21 @@ func (d *JSTestDriver) Start(t_ *testing.T, path string, width, height int) { ) actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) - d.t.Cleanup(cancel) + d.Cleanup(cancel) ctx, cancel := chromedp.NewContext(actx, // Send all logf/errf calls to t.Logf - chromedp.WithLogf(d.t.Logf), + chromedp.WithLogf(d.Logf), ) - d.t.Cleanup(cancel) + d.Cleanup(cancel) d.ctx = ctx if err := chromedp.Run(ctx); err != nil { if errors.Is(err, exec.ErrNotFound) { - d.t.Skipf("test requires Chrome to be installed: %v", err) + d.Skipf("test requires Chrome to be installed: %v", err) return } - d.t.Fatal(err) + d.Fatal(err) } chromedp.ListenTarget(ctx, func(ev interface{}) { switch ev := ev.(type) { @@ -111,7 +105,7 @@ func (d *JSTestDriver) Start(t_ *testing.T, path string, width, height int) { } args.Write(arg.Value) } - d.t.Logf("console %s: %s", ev.Type, args.String()) + d.Logf("console %s: %s", ev.Type, args.String()) } } }) @@ -119,17 +113,17 @@ func (d *JSTestDriver) Start(t_ *testing.T, path string, width, height int) { // Third, serve the app folder, set the browser tab dimensions, and // navigate to the folder. ts := httptest.NewServer(http.FileServer(http.Dir(dir))) - d.t.Cleanup(ts.Close) + d.Cleanup(ts.Close) if err := chromedp.Run(ctx, chromedp.EmulateViewport(int64(width), int64(height)), chromedp.Navigate(ts.URL), ); err != nil { - d.t.Fatal(err) + d.Fatal(err) } // Wait for the gio app to render. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } func (d *JSTestDriver) Screenshot() image.Image { @@ -137,11 +131,11 @@ func (d *JSTestDriver) Screenshot() image.Image { if err := chromedp.Run(d.ctx, chromedp.CaptureScreenshot(&buf), ); err != nil { - d.t.Fatal(err) + d.Fatal(err) } img, err := png.Decode(bytes.NewReader(buf)) if err != nil { - d.t.Fatal(err) + d.Fatal(err) } return img } @@ -150,9 +144,9 @@ func (d *JSTestDriver) Click(x, y int) { if err := chromedp.Run(d.ctx, chromedp.MouseClickXY(float64(x), float64(y)), ); err != nil { - d.t.Fatal(err) + d.Fatal(err) } // Wait for the gio app to render after this click. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } diff --git a/cmd/gogio/wayland_test.go b/cmd/gogio/wayland_test.go index a57f8a88..d10ea827 100644 --- a/cmd/gogio/wayland_test.go +++ b/cmd/gogio/wayland_test.go @@ -18,15 +18,12 @@ import ( "regexp" "strings" "sync" - "testing" "text/template" "time" ) type WaylandTestDriver struct { - t *testing.T - - frameNotifs chan bool + driverBase runtimeDir string socket string @@ -42,10 +39,7 @@ default_border none var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) -func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) { - d.frameNotifs = make(chan bool, 1) - d.t = t_ - +func (d *WaylandTestDriver) Start(path string, width, height int) { // We want os.Environ, so that it can e.g. find $DISPLAY to run within // X11. wlroots env vars are documented at: // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md @@ -60,16 +54,16 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) "swaymsg", // to send input } { if _, err := exec.LookPath(prog); err != nil { - d.t.Skipf("%s needed to run", prog) + d.Skipf("%s needed to run", prog) } } // First, build the app. dir, err := ioutil.TempDir("", "gio-endtoend-wayland") if err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(func() { os.RemoveAll(dir) }) + d.Cleanup(func() { os.RemoveAll(dir) }) bin := filepath.Join(dir, "red") flags := []string{"build", "-tags", "nox11", "-o=" + bin} @@ -79,19 +73,19 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) flags = append(flags, path) cmd := exec.Command("go", flags...) if out, err := cmd.CombinedOutput(); err != nil { - d.t.Fatalf("could not build app: %s:\n%s", err, out) + d.Fatalf("could not build app: %s:\n%s", err, out) } conf := filepath.Join(dir, "config") f, err := os.Create(conf) if err != nil { - d.t.Fatal(err) + d.Fatal(err) } defer f.Close() if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ width, height, }); err != nil { - d.t.Fatal(err) + d.Fatal(err) } d.socket = filepath.Join(dir, "socket") @@ -100,7 +94,7 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir) var wg sync.WaitGroup - d.t.Cleanup(wg.Wait) + d.Cleanup(wg.Wait) // First, start sway. { @@ -112,10 +106,10 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) log.Fatal(err) } if err := cmd.Start(); err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(cancel) - d.t.Cleanup(func() { + 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. @@ -129,7 +123,7 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) for { line, err := br.ReadString('\n') if err != nil { - d.t.Fatal(err) + d.Fatal(err) } if m := rxSwayReady.FindStringSubmatch(line); m != nil { d.display = m[1] @@ -143,7 +137,7 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) // Don't print all stderr, since we use --verbose. // TODO(mvdan): if it's useful, probably filter // errors and show them. - d.t.Error(err) + d.Error(err) } wg.Done() }() @@ -156,20 +150,20 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} stdout, err := cmd.StdoutPipe() if err != nil { - d.t.Fatal(err) + d.Fatal(err) } stderr := &bytes.Buffer{} cmd.Stderr = stderr if err := cmd.Start(); err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(cancel) + d.Cleanup(cancel) wg.Add(1) go func() { if err := cmd.Wait(); err != nil && ctx.Err() == nil { // Print stderr and error. io.Copy(os.Stdout, stderr) - d.t.Error(err) + d.Error(err) } wg.Done() }() @@ -185,7 +179,7 @@ func (d *WaylandTestDriver) Start(t_ *testing.T, path string, width, height int) } // Wait for the gio app to render. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } func (d *WaylandTestDriver) Screenshot() image.Image { @@ -193,12 +187,12 @@ func (d *WaylandTestDriver) Screenshot() image.Image { cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} out, err := cmd.CombinedOutput() if err != nil { - d.t.Errorf("%s", out) - d.t.Fatal(err) + d.Errorf("%s", out) + d.Fatal(err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { - d.t.Fatal(err) + d.Fatal(err) } return img } @@ -210,8 +204,8 @@ func (d *WaylandTestDriver) swaymsg(args ...interface{}) { } cmd := exec.Command("swaymsg", strs...) if out, err := cmd.CombinedOutput(); err != nil { - d.t.Errorf("%s", out) - d.t.Fatal(err) + d.Errorf("%s", out) + d.Fatal(err) } } @@ -221,5 +215,5 @@ func (d *WaylandTestDriver) Click(x, y int) { d.swaymsg("seat", "-", "cursor", "release", "button1") // Wait for the gio app to render after this click. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } diff --git a/cmd/gogio/x11_test.go b/cmd/gogio/x11_test.go index e23e6557..5774e093 100644 --- a/cmd/gogio/x11_test.go +++ b/cmd/gogio/x11_test.go @@ -16,22 +16,16 @@ import ( "os/exec" "path/filepath" "sync" - "testing" "time" ) type X11TestDriver struct { - t *testing.T - - frameNotifs chan bool + driverBase display string } -func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { - d.frameNotifs = make(chan bool, 1) - d.t = t_ - +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. @@ -57,16 +51,16 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { "xdotool", // to send input } { if _, err := exec.LookPath(prog); err != nil { - d.t.Skipf("%s needed to run", prog) + d.Skipf("%s needed to run", prog) } } // First, build the app. dir, err := ioutil.TempDir("", "gio-endtoend-x11") if err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(func() { os.RemoveAll(dir) }) + d.Cleanup(func() { os.RemoveAll(dir) }) bin := filepath.Join(dir, "red") flags := []string{"build", "-tags", "nowayland", "-o=" + bin} @@ -76,11 +70,11 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { flags = append(flags, path) cmd := exec.Command("go", flags...) if out, err := cmd.CombinedOutput(); err != nil { - d.t.Fatalf("could not build app: %s:\n%s", err, out) + d.Fatalf("could not build app: %s:\n%s", err, out) } var wg sync.WaitGroup - d.t.Cleanup(wg.Wait) + d.Cleanup(wg.Wait) // First, start the X server. { @@ -90,10 +84,10 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { cmd.Stdout = combined cmd.Stderr = combined if err := cmd.Start(); err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(cancel) - d.t.Cleanup(func() { + 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. @@ -111,7 +105,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { break } if i >= 100 { - d.t.Fatalf("timed out waiting for %s", socket) + d.Fatalf("timed out waiting for %s", socket) } } @@ -120,7 +114,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { if err := cmd.Wait(); err != nil && ctx.Err() == nil { // Print all output and error. io.Copy(os.Stdout, combined) - d.t.Error(err) + d.Error(err) } wg.Done() }() @@ -133,21 +127,21 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { cmd.Env = []string{"DISPLAY=" + d.display} stdout, err := cmd.StdoutPipe() if err != nil { - d.t.Fatal(err) + d.Fatal(err) } stderr := &bytes.Buffer{} cmd.Stderr = stderr if err := cmd.Start(); err != nil { - d.t.Fatal(err) + d.Fatal(err) } - d.t.Cleanup(cancel) + d.Cleanup(cancel) wg.Add(1) go func() { if err := cmd.Wait(); err != nil && ctx.Err() == nil { // Print stderr and error. io.Copy(os.Stdout, stderr) - d.t.Error(err) + d.Error(err) } wg.Done() }() @@ -164,7 +158,7 @@ func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) { } // Wait for the gio app to render. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() } func (d *X11TestDriver) Screenshot() image.Image { @@ -172,12 +166,12 @@ func (d *X11TestDriver) Screenshot() image.Image { cmd.Env = []string{"DISPLAY=" + d.display} out, err := cmd.CombinedOutput() if err != nil { - d.t.Errorf("%s", out) - d.t.Fatal(err) + d.Errorf("%s", out) + d.Fatal(err) } img, err := png.Decode(bytes.NewReader(out)) if err != nil { - d.t.Fatal(err) + d.Fatal(err) } return img } @@ -190,8 +184,8 @@ func (d *X11TestDriver) xdotool(args ...interface{}) { cmd := exec.Command("xdotool", strs...) cmd.Env = []string{"DISPLAY=" + d.display} if out, err := cmd.CombinedOutput(); err != nil { - d.t.Errorf("%s", out) - d.t.Fatal(err) + d.Errorf("%s", out) + d.Fatal(err) } } @@ -200,5 +194,5 @@ func (d *X11TestDriver) Click(x, y int) { d.xdotool("click", "1") // Wait for the gio app to render after this click. - waitForFrame(d.t, d.frameNotifs) + d.waitForFrame() }