mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 08:25:34 +00:00
4e71f195ab
This vastly simplifies our code, and saves us the ugly math. While at it, establish that a TestDriver must have a white background, which is already satisfied by both existing implementations. Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
// TODO(mvdan): come up with an end-to-end platform interface, including methods
|
|
// like "take screenshot" or "close app", so that we can run the same tests on
|
|
// all supported platforms without writing them many times.
|
|
|
|
package main_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/xgb"
|
|
"github.com/BurntSushi/xgb/xproto"
|
|
"github.com/BurntSushi/xgbutil"
|
|
"github.com/BurntSushi/xgbutil/xgraphics"
|
|
)
|
|
|
|
type X11TestDriver struct {
|
|
t *testing.T
|
|
|
|
// conn holds the connection to X.
|
|
conn *xgbutil.XUtil
|
|
}
|
|
|
|
func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) (cleanups []func()) {
|
|
d.t = t_
|
|
|
|
// 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()))
|
|
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, display)
|
|
if _, err := exec.LookPath(xprog); err != nil {
|
|
d.t.Skipf("%s needed to run with -headless=%t", xprog, *headless)
|
|
}
|
|
|
|
// First, build the app.
|
|
dir, err := ioutil.TempDir("", "gio-endtoend-x11")
|
|
if err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
cleanups = append(cleanups, func() { os.RemoveAll(dir) })
|
|
|
|
bin := filepath.Join(dir, "red")
|
|
cmd := exec.Command("go", "build", "-tags", "nowayland", "-o="+bin, path)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
d.t.Fatalf("could not build app: %s:\n%s", err, out)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
cleanups = append(cleanups, 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.t.Fatal(err)
|
|
}
|
|
cleanups = append(cleanups, cancel)
|
|
cleanups = append(cleanups, func() {
|
|
// Give Xserver a chance to exit gracefully, cleaning up
|
|
// after itself in /tmp. After 10ms, the deferred cancel
|
|
// above will signal an os.Kill.
|
|
cmd.Process.Signal(os.Interrupt)
|
|
time.Sleep(10 * time.Millisecond)
|
|
})
|
|
|
|
// Wait for up to 1s (100 * 10ms) for the X server to be ready.
|
|
for i := 0; ; i++ {
|
|
time.Sleep(10 * time.Millisecond)
|
|
// This socket path isn't terribly portable, but the xgb
|
|
// library we use does the same, and we only really care
|
|
// about Linux here.
|
|
socket := fmt.Sprintf("/tmp/.X11-unix/X%s", display[1:])
|
|
if _, err := os.Stat(socket); err == nil {
|
|
break
|
|
}
|
|
if i >= 100 {
|
|
d.t.Fatalf("timed out waiting for %s", socket)
|
|
}
|
|
}
|
|
|
|
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.t.Error(err)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
// Then, start our program on the X server above.
|
|
{
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cmd := exec.CommandContext(ctx, bin)
|
|
out := &bytes.Buffer{}
|
|
cmd.Env = append(os.Environ(), "DISPLAY="+display)
|
|
cmd.Stdout = out
|
|
cmd.Stderr = out
|
|
if err := cmd.Start(); err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
cleanups = append(cleanups, cancel)
|
|
wg.Add(1)
|
|
go func() {
|
|
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
|
|
// Print all output and error.
|
|
io.Copy(os.Stdout, out)
|
|
d.t.Error(err)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
// Finally, connect to the X server.
|
|
xgb.Logger.SetOutput(testLogWriter{d.t})
|
|
xgbutil.Logger.SetOutput(testLogWriter{d.t})
|
|
conn, err := xgbutil.NewConnDisplay(display)
|
|
if err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
d.conn = conn
|
|
cleanups = append(cleanups, func() {
|
|
conn.Conn().Close()
|
|
// TODO(mvdan): Figure out a way to remove this sleep
|
|
// without introducing a panic. The xgb code will
|
|
// encounter a panic if the Xorg server exits before xgb
|
|
// has shut down fully.
|
|
// See: https://github.com/BurntSushi/xgb/pull/44
|
|
time.Sleep(10 * time.Millisecond)
|
|
})
|
|
|
|
// Wait for the gio app to render.
|
|
// TODO(mvdan): do this properly, e.g. via waiting for log lines
|
|
// from the gio program.
|
|
time.Sleep(400 * time.Millisecond)
|
|
|
|
return cleanups
|
|
}
|
|
|
|
func (d *X11TestDriver) Screenshot() image.Image {
|
|
img, err := xgraphics.NewDrawable(d.conn, xproto.Drawable(d.conn.RootWin()))
|
|
if err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
return img
|
|
}
|
|
|
|
func TestX11(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
runEndToEndTest(t, &X11TestDriver{})
|
|
}
|
|
|
|
// testLogWriter is a bit of a hack to redirect libraries that use a *log.Logger
|
|
// variable to instead send their logs to t.Logf.
|
|
//
|
|
// Since *log.Logger isn't an interface and can only take an io.Writer, all we
|
|
// can do is implement an io.Writer that sends its output to t.Logf. We end up
|
|
// with duplicate log prefixes, but that doesn't seem so bad.
|
|
type testLogWriter struct {
|
|
t *testing.T
|
|
}
|
|
|
|
func (w testLogWriter) Write(p []byte) (n int, err error) {
|
|
w.t.Logf("%s", p)
|
|
return len(p), nil
|
|
}
|