mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-02 07:57:29 +00:00
6fedfaf3af
Now that we use tip due to breaking changes in Go's JS APIs, let's replace our hacky cleanup list code with 1.14's upcoming testing.T.Cleanup. Adding more deliberate uses of tip would ususally be best avoided, but these will only affect developers working on gio's tests, not regular users of gio. While at it, remove some debug t.Logf calls I forgot to remove. Signed-off-by: Daniel Martí <mvdan@mvdan.cc>
205 lines
4.6 KiB
Go
205 lines
4.6 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package main_test
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type X11TestDriver struct {
|
|
t *testing.T
|
|
|
|
frameNotifs chan bool
|
|
|
|
display string
|
|
}
|
|
|
|
func (d *X11TestDriver) Start(t_ *testing.T, path string, width, height int) {
|
|
d.frameNotifs = make(chan bool, 1)
|
|
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()))
|
|
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.t.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.t.Cleanup(func() { os.RemoveAll(dir) })
|
|
|
|
bin := filepath.Join(dir, "red")
|
|
flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
|
|
if raceEnabled {
|
|
flags = append(flags, "-race")
|
|
}
|
|
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)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
d.t.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.t.Fatal(err)
|
|
}
|
|
d.t.Cleanup(cancel)
|
|
d.t.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 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 it's
|
|
// okay for now.
|
|
socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.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)
|
|
cmd.Env = []string{"DISPLAY=" + d.display}
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
stderr := &bytes.Buffer{}
|
|
cmd.Stderr = stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
d.t.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)
|
|
}
|
|
wg.Done()
|
|
}()
|
|
go func() {
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if line == "frame ready" {
|
|
d.frameNotifs <- true
|
|
}
|
|
}
|
|
}()
|
|
|
|
}
|
|
|
|
// Wait for the gio app to render.
|
|
<-d.frameNotifs
|
|
}
|
|
|
|
func (d *X11TestDriver) Screenshot() image.Image {
|
|
cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
|
|
cmd.Env = []string{"DISPLAY=" + d.display}
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
d.t.Errorf("%s", out)
|
|
d.t.Fatal(err)
|
|
}
|
|
img, err := png.Decode(bytes.NewReader(out))
|
|
if err != nil {
|
|
d.t.Fatal(err)
|
|
}
|
|
return img
|
|
}
|
|
|
|
func (d *X11TestDriver) xdotool(args ...interface{}) {
|
|
strs := make([]string, len(args))
|
|
for i, arg := range args {
|
|
strs[i] = fmt.Sprint(arg)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
func (d *X11TestDriver) Click(x, y int) {
|
|
d.xdotool("mousemove", x, y)
|
|
d.xdotool("click", "1")
|
|
|
|
// Wait for the gio app to render after this click.
|
|
<-d.frameNotifs
|
|
}
|