windows_test.go raw

   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  }
 153