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) + } +}