From cea8dc374b928455279e2becb6341ba7bd6291f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 29 Oct 2019 17:28:06 +0000 Subject: [PATCH] cmd/gogio: add the first end-to-end X11 test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right now it's very similar to the JS test on Chrome. Like it, this one just runs the "red.go" gio app, takes a screenshot, and expects to see red. It also supports the -headless flag; when true, Xvfb is used and it's entirely headless and hidden. Otherwise, Xephyr is used and once can see the test in action. If the tool isn't installed, the test is skipped. We need to add xgb as a dependency, so that we can connect to the X server and interact with it, like taking screenshots. Finally, this is an initial version, and a number of TODOs are left for a later time. They'll get fixed in follow-up patches. While at it, start making all tests parallel, since the end-to-end tests take about a second each and neither are very cpu-intensive. Signed-off-by: Daniel Martí --- cmd/go.mod | 4 ++ cmd/go.sum | 7 ++ cmd/gogio/js_test.go | 24 ++++--- cmd/gogio/main_test.go | 2 + cmd/gogio/x11_test.go | 151 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 cmd/gogio/x11_test.go diff --git a/cmd/go.mod b/cmd/go.mod index 86699fe5..ca231580 100644 --- a/cmd/go.mod +++ b/cmd/go.mod @@ -4,6 +4,10 @@ go 1.13 require ( gioui.org v0.0.0-20191029130630-4641607cd6a4 + github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect + github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect + github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 + github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 github.com/chromedp/cdproto v0.0.0-20191009033829-c22f49c9ff0a github.com/chromedp/chromedp v0.5.1 golang.org/x/image v0.0.0-20190802002840-cff245a6509b diff --git a/cmd/go.sum b/cmd/go.sum index 122d738c..2ff656c9 100644 --- a/cmd/go.sum +++ b/cmd/go.sum @@ -1,7 +1,14 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= gioui.org v0.0.0-20191029130630-4641607cd6a4 h1:WKYMON5AGU5/Xs5ezSWq9i/hiobSXBsQlsPSVXybScM= gioui.org v0.0.0-20191029130630-4641607cd6a4/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY= +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= +github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= +github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 h1:O/r2Sj+8QcMF7V5IcmiE2sMFV2q3J47BEirxbXJAdzA= +github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= github.com/chromedp/cdproto v0.0.0-20191009033829-c22f49c9ff0a h1:AuIGvB6IuWpMEdfKQ+t77D6dzLpNftzxAsktehYyWn8= github.com/chromedp/cdproto v0.0.0-20191009033829-c22f49c9ff0a/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g= github.com/chromedp/chromedp v0.5.1 h1:PAqhoCWCHzRphYnmmxLSiYk7EEwDplCm4woTCCaV2cQ= diff --git a/cmd/gogio/js_test.go b/cmd/gogio/js_test.go index c348d19e..88bbdad8 100644 --- a/cmd/gogio/js_test.go +++ b/cmd/gogio/js_test.go @@ -7,6 +7,7 @@ import ( "context" "errors" "flag" + "image" "image/png" "io/ioutil" "net/http" @@ -25,6 +26,8 @@ import ( var headless = flag.Bool("headless", true, "run end-to-end tests in headless mode") func TestJSOnChrome(t *testing.T) { + t.Parallel() + // First, build the app. dir, err := ioutil.TempDir("", "gio-endtoend-js") if err != nil { @@ -124,14 +127,15 @@ func TestJSOnChrome(t *testing.T) { t.Fatalf("expected dimensions to be %d*%d, got %d*%d", wantSize, wantSize, size.X, size.Y) } - wantColor := func(x, y int, r, g, b, a uint32) { - color := img.At(x, y) - r_, g_, b_, a_ := color.RGBA() - if r_ != r || g_ != g || b_ != b || a_ != a { - t.Errorf("got 0x%04x%04x%04x%04x at (%d,%d), want 0x%04x%04x%04x%04x", - r_, g_, b_, a_, x, y, r, g, b, a) - } - } - wantColor(5, 5, 0xffff, 0x0, 0x0, 0xffff) - wantColor(595, 595, 0xffff, 0x0, 0x0, 0xffff) + wantColor(t, img, 5, 5, 0xffff, 0x0, 0x0, 0xffff) + wantColor(t, img, 595, 595, 0xffff, 0x0, 0x0, 0xffff) +} + +func wantColor(t *testing.T, img image.Image, x, y int, r, g, b, a uint32) { + color := img.At(x, y) + r_, g_, b_, a_ := color.RGBA() + if r_ != r || g_ != g || b_ != b || a_ != a { + t.Errorf("got 0x%04x%04x%04x%04x at (%d,%d), want 0x%04x%04x%04x%04x", + r_, g_, b_, a_, x, y, r, g, b, a) + } } diff --git a/cmd/gogio/main_test.go b/cmd/gogio/main_test.go index 4baa1128..efb441e0 100644 --- a/cmd/gogio/main_test.go +++ b/cmd/gogio/main_test.go @@ -9,6 +9,8 @@ type expval struct { } func TestAppID(t *testing.T) { + t.Parallel() + tests := []expval{ {"example", "localhost.example"}, {"example.com", "com.example"}, diff --git a/cmd/gogio/x11_test.go b/cmd/gogio/x11_test.go new file mode 100644 index 00000000..cdc552e8 --- /dev/null +++ b/cmd/gogio/x11_test.go @@ -0,0 +1,151 @@ +// 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" + "io" + "io/ioutil" + "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" +) + +func TestX11(t *testing.T) { + t.Parallel() + + // TODO(mvdan): pick a random one between a large pool, and retry if + // it's already taken. + const display = ":15" + + var xprog string + xflags := []string{"-wr"} + if *headless { + xprog = "Xvfb" // virtual X server + xflags = append(xflags, "-screen", "0", "600x600x24") + } else { + xprog = "Xephyr" // nested X server as a window + xflags = append(xflags, "-screen", "600x600") + } + xflags = append(xflags, display) + if _, err := exec.LookPath(xprog); err != nil { + 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 { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + bin := filepath.Join(dir, "red") + cmd := exec.Command("go", "build", "-o="+bin, "testdata/red.go") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("could not build app: %s:\n%s", err, out) + } + + var wg sync.WaitGroup + defer wg.Wait() + + // First, start the X server. + { + ctx, cancel := context.WithCancel(context.Background()) + cmd := exec.CommandContext(ctx, xprog, xflags...) + out := &bytes.Buffer{} + cmd.Stdout = out + cmd.Stderr = out + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + defer cancel() + // TODO(mvdan): properly wait for the display to be ready instead. + time.Sleep(200 * time.Millisecond) + wg.Add(1) + go func() { + if err := cmd.Wait(); err != nil && ctx.Err() == nil { + // Print all output and error. + io.Copy(os.Stdout, out) + 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 { + t.Fatal(err) + } + defer 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) + t.Error(err) + } + wg.Done() + }() + } + + // Finally, run our tests. A connection to the X server is used to + // interact with it. + { + if !testing.Verbose() { + xgb.Logger.SetOutput(ioutil.Discard) + xgbutil.Logger.SetOutput(ioutil.Discard) + } + xu, err := xgbutil.NewConnDisplay(display) + if err != nil { + t.Fatal(err) + } + defer func() { + xu.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(20 * 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(200 * time.Millisecond) + + img, err := xgraphics.NewDrawable(xu, xproto.Drawable(xu.RootWin())) + if err != nil { + t.Fatal(err) + } + + size := img.Bounds().Size() + wantSize := 600 // 300px at 2.0 scaling factor + if size.X != wantSize || size.Y != wantSize { + t.Fatalf("expected dimensions to be %d*%d, got %d*%d", + wantSize, wantSize, size.X, size.Y) + } + wantColor(t, img, 5, 5, 0xffff, 0x0, 0x0, 0xffff) + wantColor(t, img, 595, 595, 0xffff, 0x0, 0x0, 0xffff) + } +}