github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/x11_test.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package main_test 4 5 import ( 6 "bytes" 7 "context" 8 "fmt" 9 "image" 10 "image/png" 11 "io" 12 "math/rand" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "sync" 17 "time" 18 ) 19 20 type X11TestDriver struct { 21 driverBase 22 23 display string 24 } 25 26 func (d *X11TestDriver) Start(path string) { 27 // First, build the app. 28 bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red") 29 flags := []string{"build", "-tags", "nowayland", "-o=" + bin} 30 if raceEnabled { 31 flags = append(flags, "-race") 32 } 33 flags = append(flags, path) 34 cmd := exec.Command("go", flags...) 35 if out, err := cmd.CombinedOutput(); err != nil { 36 d.Fatalf("could not build app: %s:\n%s", err, out) 37 } 38 39 var wg sync.WaitGroup 40 d.Cleanup(wg.Wait) 41 42 d.startServer(&wg, d.width, d.height) 43 44 // Then, start our program on the X server above. 45 { 46 ctx, cancel := context.WithCancel(context.Background()) 47 cmd := exec.CommandContext(ctx, bin) 48 cmd.Env = []string{"DISPLAY=" + d.display} 49 output, err := cmd.StdoutPipe() 50 if err != nil { 51 d.Fatal(err) 52 } 53 cmd.Stderr = cmd.Stdout 54 d.output = output 55 if err := cmd.Start(); err != nil { 56 d.Fatal(err) 57 } 58 d.Cleanup(cancel) 59 wg.Add(1) 60 go func() { 61 if err := cmd.Wait(); err != nil && ctx.Err() == nil { 62 d.Error(err) 63 } 64 wg.Done() 65 }() 66 } 67 68 // Wait for the gio app to render. 69 d.waitForFrame() 70 } 71 72 func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) { 73 // Pick a random display number between 1 and 100,000. Most machines 74 // will only be using :0, so there's only a 0.001% chance of two 75 // concurrent test runs to run into a conflict. 76 rnd := rand.New(rand.NewSource(time.Now().UnixNano())) 77 d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1) 78 79 var xprog string 80 xflags := []string{ 81 "-wr", // we want a white background; the default is black 82 } 83 if *headless { 84 xprog = "Xvfb" // virtual X server 85 xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height)) 86 } else { 87 xprog = "Xephyr" // nested X server as a window 88 xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height)) 89 } 90 xflags = append(xflags, d.display) 91 92 d.needPrograms( 93 xprog, // to run the X server 94 "scrot", // to take screenshots 95 "xdotool", // to send input 96 ) 97 ctx, cancel := context.WithCancel(context.Background()) 98 cmd := exec.CommandContext(ctx, xprog, xflags...) 99 combined := &bytes.Buffer{} 100 cmd.Stdout = combined 101 cmd.Stderr = combined 102 if err := cmd.Start(); err != nil { 103 d.Fatal(err) 104 } 105 d.Cleanup(cancel) 106 d.Cleanup(func() { 107 // Give it a chance to exit gracefully, cleaning up 108 // after itself. After 10ms, the deferred cancel above 109 // will signal an os.Kill. 110 cmd.Process.Signal(os.Interrupt) 111 time.Sleep(10 * time.Millisecond) 112 }) 113 114 // Wait for the X server to be ready. The socket path isn't 115 // terribly portable, but that's okay for now. 116 withRetries(d.T, time.Second, func() error { 117 socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:]) 118 _, err := os.Stat(socket) 119 return err 120 }) 121 122 wg.Add(1) 123 go func() { 124 if err := cmd.Wait(); err != nil && ctx.Err() == nil { 125 // Print all output and error. 126 io.Copy(os.Stdout, combined) 127 d.Error(err) 128 } 129 wg.Done() 130 }() 131 } 132 133 func (d *X11TestDriver) Screenshot() image.Image { 134 cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout") 135 cmd.Env = []string{"DISPLAY=" + d.display} 136 out, err := cmd.CombinedOutput() 137 if err != nil { 138 d.Errorf("%s", out) 139 d.Fatal(err) 140 } 141 img, err := png.Decode(bytes.NewReader(out)) 142 if err != nil { 143 d.Fatal(err) 144 } 145 return img 146 } 147 148 func (d *X11TestDriver) xdotool(args ...interface{}) string { 149 d.Helper() 150 strs := make([]string, len(args)) 151 for i, arg := range args { 152 strs[i] = fmt.Sprint(arg) 153 } 154 cmd := exec.Command("xdotool", strs...) 155 cmd.Env = []string{"DISPLAY=" + d.display} 156 out, err := cmd.CombinedOutput() 157 if err != nil { 158 d.Errorf("%s", out) 159 d.Fatal(err) 160 } 161 return string(bytes.TrimSpace(out)) 162 } 163 164 func (d *X11TestDriver) Click(x, y int) { 165 d.xdotool("mousemove", "--sync", x, y) 166 d.xdotool("click", "1") 167 168 // Wait for the gio app to render after this click. 169 d.waitForFrame() 170 }