github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/wayland_test.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package main_test 4 5 import ( 6 "bufio" 7 "bytes" 8 "context" 9 "fmt" 10 "image" 11 "image/png" 12 "os" 13 "os/exec" 14 "path/filepath" 15 "regexp" 16 "strings" 17 "sync" 18 "text/template" 19 "time" 20 ) 21 22 type WaylandTestDriver struct { 23 driverBase 24 25 runtimeDir string 26 socket string 27 display string 28 } 29 30 // No bars or anything fancy. Just a white background with our dimensions. 31 var tmplSwayConfig = template.Must(template.New("").Parse(` 32 output * bg #FFFFFF solid_color 33 output * mode {{.Width}}x{{.Height}} 34 default_border none 35 `)) 36 37 var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`) 38 39 func (d *WaylandTestDriver) Start(path string) { 40 // We want os.Environ, so that it can e.g. find $DISPLAY to run within 41 // X11. wlroots env vars are documented at: 42 // https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md 43 env := os.Environ() 44 if *headless { 45 env = append(env, "WLR_BACKENDS=headless") 46 } 47 48 d.needPrograms( 49 "sway", // to run a wayland compositor 50 "grim", // to take screenshots 51 "swaymsg", // to send input 52 ) 53 54 // First, build the app. 55 dir := d.tempDir("gio-endtoend-wayland") 56 bin := filepath.Join(dir, "red") 57 flags := []string{"build", "-tags", "nox11", "-o=" + bin} 58 if raceEnabled { 59 flags = append(flags, "-race") 60 } 61 flags = append(flags, path) 62 cmd := exec.Command("go", flags...) 63 if out, err := cmd.CombinedOutput(); err != nil { 64 d.Fatalf("could not build app: %s:\n%s", err, out) 65 } 66 67 conf := filepath.Join(dir, "config") 68 f, err := os.Create(conf) 69 if err != nil { 70 d.Fatal(err) 71 } 72 defer f.Close() 73 if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{ 74 d.width, d.height, 75 }); err != nil { 76 d.Fatal(err) 77 } 78 79 d.socket = filepath.Join(dir, "socket") 80 env = append(env, "SWAYSOCK="+d.socket) 81 d.runtimeDir = dir 82 env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir) 83 84 var wg sync.WaitGroup 85 d.Cleanup(wg.Wait) 86 87 // First, start sway. 88 { 89 ctx, cancel := context.WithCancel(context.Background()) 90 cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose") 91 cmd.Env = env 92 stderr, err := cmd.StderrPipe() 93 if err != nil { 94 d.Fatal(err) 95 } 96 if err := cmd.Start(); err != nil { 97 d.Fatal(err) 98 } 99 d.Cleanup(cancel) 100 d.Cleanup(func() { 101 // Give it a chance to exit gracefully, cleaning up 102 // after itself. After 10ms, the deferred cancel above 103 // will signal an os.Kill. 104 cmd.Process.Signal(os.Interrupt) 105 time.Sleep(10 * time.Millisecond) 106 }) 107 108 // Wait for sway to be ready. We probably don't need a deadline 109 // here. 110 br := bufio.NewReader(stderr) 111 for { 112 line, err := br.ReadString('\n') 113 if err != nil { 114 d.Fatal(err) 115 } 116 if m := rxSwayReady.FindStringSubmatch(line); m != nil { 117 d.display = m[1] 118 break 119 } 120 } 121 122 wg.Add(1) 123 go func() { 124 if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") { 125 // Don't print all stderr, since we use --verbose. 126 // TODO(mvdan): if it's useful, probably filter 127 // errors and show them. 128 d.Error(err) 129 } 130 wg.Done() 131 }() 132 } 133 134 // Then, start our program on the sway compositor above. 135 { 136 ctx, cancel := context.WithCancel(context.Background()) 137 cmd := exec.CommandContext(ctx, bin) 138 cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} 139 output, err := cmd.StdoutPipe() 140 if err != nil { 141 d.Fatal(err) 142 } 143 cmd.Stderr = cmd.Stdout 144 d.output = output 145 if err := cmd.Start(); err != nil { 146 d.Fatal(err) 147 } 148 d.Cleanup(cancel) 149 wg.Add(1) 150 go func() { 151 if err := cmd.Wait(); err != nil && ctx.Err() == nil { 152 d.Error(err) 153 } 154 wg.Done() 155 }() 156 } 157 158 // Wait for the gio app to render. 159 d.waitForFrame() 160 } 161 162 func (d *WaylandTestDriver) Screenshot() image.Image { 163 cmd := exec.Command("grim", "/dev/stdout") 164 cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display} 165 out, err := cmd.CombinedOutput() 166 if err != nil { 167 d.Errorf("%s", out) 168 d.Fatal(err) 169 } 170 img, err := png.Decode(bytes.NewReader(out)) 171 if err != nil { 172 d.Fatal(err) 173 } 174 return img 175 } 176 177 func (d *WaylandTestDriver) swaymsg(args ...interface{}) { 178 strs := []string{"--socket", d.socket} 179 for _, arg := range args { 180 strs = append(strs, fmt.Sprint(arg)) 181 } 182 cmd := exec.Command("swaymsg", strs...) 183 if out, err := cmd.CombinedOutput(); err != nil { 184 d.Errorf("%s", out) 185 d.Fatal(err) 186 } 187 } 188 189 func (d *WaylandTestDriver) Click(x, y int) { 190 d.swaymsg("seat", "-", "cursor", "set", x, y) 191 d.swaymsg("seat", "-", "cursor", "press", "button1") 192 d.swaymsg("seat", "-", "cursor", "release", "button1") 193 194 // Wait for the gio app to render after this click. 195 d.waitForFrame() 196 }