github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/windows_test.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package main_test 4 5 import ( 6 "context" 7 "image" 8 "io" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "sync" 14 "time" 15 16 "golang.org/x/image/draw" 17 ) 18 19 // Wine is tightly coupled with X11 at the moment, and we can reuse the same 20 // methods to automate screenshots and clicks. The main difference is how we 21 // build and run the app. 22 23 // The only quirk is that it seems impossible for the Wine window to take the 24 // entirety of the X server's dimensions, even if we try to resize it to take 25 // the entire display. It seems to want to leave some vertical space empty, 26 // presumably for window decorations or the "start" bar on Windows. To work 27 // around that, make the X server 50x50px bigger, and crop the screenshots back 28 // to the original size. 29 30 type WineTestDriver struct { 31 X11TestDriver 32 } 33 34 func (d *WineTestDriver) Start(path string) { 35 d.needPrograms("wine") 36 37 // First, build the app. 38 bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe") 39 flags := []string{"build", "-o=" + bin} 40 if raceEnabled { 41 if runtime.GOOS != "windows" { 42 // cross-compilation disables CGo, which breaks -race. 43 d.Skipf("can't cross-compile -race for Windows; skipping") 44 } 45 flags = append(flags, "-race") 46 } 47 flags = append(flags, path) 48 cmd := exec.Command("go", flags...) 49 cmd.Env = os.Environ() 50 cmd.Env = append(cmd.Env, "GOOS=windows") 51 if out, err := cmd.CombinedOutput(); err != nil { 52 d.Fatalf("could not build app: %s:\n%s", err, out) 53 } 54 55 var wg sync.WaitGroup 56 d.Cleanup(wg.Wait) 57 58 // Add 50x50px to the display dimensions, as discussed earlier. 59 d.startServer(&wg, d.width+50, d.height+50) 60 61 // Then, start our program via Wine on the X server above. 62 { 63 cacheDir, err := os.UserCacheDir() 64 if err != nil { 65 d.Fatal(err) 66 } 67 // Use a wine directory separate from the default ~/.wine, so 68 // that the user's winecfg doesn't affect our test. This will 69 // default to ~/.cache/gio-e2e-wine. We use the user's cache, 70 // to reuse a previously set up wineprefix. 71 wineprefix := filepath.Join(cacheDir, "gio-e2e-wine") 72 73 // First, ensure that wineprefix is up to date with wineboot. 74 // Wait for this separately from the first frame, as setting up 75 // a new prefix might take 5s on its own. 76 env := []string{ 77 "DISPLAY=" + d.display, 78 "WINEDEBUG=fixme-all", // hide "fixme" noise 79 "WINEPREFIX=" + wineprefix, 80 81 // Disable wine-gecko (Explorer) and wine-mono (.NET). 82 // Otherwise, if not installed, wineboot will get stuck 83 // with a prompt to install them on the virtual X 84 // display. Moreover, Gio doesn't need either, and wine 85 // is faster without them. 86 "WINEDLLOVERRIDES=mscoree,mshtml=", 87 } 88 { 89 start := time.Now() 90 cmd := exec.Command("wine", "wineboot", "-i") 91 cmd.Env = env 92 // Use a combined output pipe instead of CombinedOutput, 93 // so that we only wait for the child process to exit, 94 // and we don't need to wait for all of wine's 95 // grandchildren to exit and stop writing. This is 96 // relevant as wine leaves "wineserver" lingering for 97 // three seconds by default, to be reused later. 98 stdout, err := cmd.StdoutPipe() 99 if err != nil { 100 d.Fatal(err) 101 } 102 cmd.Stderr = cmd.Stdout 103 if err := cmd.Run(); err != nil { 104 io.Copy(os.Stderr, stdout) 105 d.Fatal(err) 106 } 107 d.Logf("set up WINEPREFIX in %s", time.Since(start)) 108 } 109 110 ctx, cancel := context.WithCancel(context.Background()) 111 cmd := exec.CommandContext(ctx, "wine", bin) 112 cmd.Env = env 113 output, err := cmd.StdoutPipe() 114 if err != nil { 115 d.Fatal(err) 116 } 117 cmd.Stderr = cmd.Stdout 118 d.output = output 119 if err := cmd.Start(); err != nil { 120 d.Fatal(err) 121 } 122 d.Cleanup(cancel) 123 wg.Add(1) 124 go func() { 125 if err := cmd.Wait(); err != nil && ctx.Err() == nil { 126 d.Error(err) 127 } 128 wg.Done() 129 }() 130 } 131 // Wait for the gio app to render. 132 d.waitForFrame() 133 134 // xdotool seems to fail at actually moving the window if we use it 135 // immediately after Gio is ready. Why? 136 // We can't tell if the windowmove operation worked until we take a 137 // screenshot, because the getwindowgeometry op reports the 0x0 138 // coordinates even if the window wasn't moved properly. 139 // A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that. 140 // TODO(mvdan): revisit this, when you have a spare three hours. 141 time.Sleep(400 * time.Millisecond) 142 id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio") 143 d.xdotool("windowmove", "--sync", id, 0, 0) 144 } 145 146 func (d *WineTestDriver) Screenshot() image.Image { 147 img := d.X11TestDriver.Screenshot() 148 // Crop the screenshot back to the original dimensions. 149 cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height)) 150 draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src) 151 return cropped 152 }