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  }