detached e34ffa6

test(ui): harden gtk display probe tests

commit
e34ffa650d93eaa593988ca259237e215ed54cf9
author
brice · · 2026-06-20 13:14 UTC
parent
1d86c5b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +148 -39
M internal/ui/app_browser_launch_test.go +148 -39
@@ -7,6 +7,7 @@ import (
77 "errors"
88 "io"
99 "math"
10+ "net"
1011 "os"
1112 "path/filepath"
1213 "reflect"
@@ -81,7 +82,28 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
8182
8283 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
8384
85+func testPathIsSocket(path string) bool {
86+ info, err := os.Stat(path)
87+ return err == nil && info.Mode()&os.ModeSocket != 0
88+}
89+
90+func testGDKBackendAllows(backend string) bool {
91+ configured := os.Getenv("GDK_BACKEND")
92+ if configured == "" {
93+ return true
94+ }
95+ for _, candidate := range strings.Split(configured, ",") {
96+ if strings.TrimSpace(candidate) == backend {
97+ return true
98+ }
99+ }
100+ return false
101+}
102+
84103 func testHasUsableWaylandDisplay() bool {
104+ if !testGDKBackendAllows("wayland") {
105+ return false
106+ }
85107 waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
86108 if waylandDisplay == "" {
87109 return false
@@ -91,7 +113,7 @@ func testHasUsableWaylandDisplay() bool {
91113 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
92114 }
93115 for _, candidate := range candidates {
94- if _, err := os.Stat(candidate); err == nil {
116+ if testPathIsSocket(candidate) {
95117 return true
96118 }
97119 }
@@ -99,6 +121,9 @@ func testHasUsableWaylandDisplay() bool {
99121 }
100122
101123 func testHasUsableX11Display() bool {
124+ if !testGDKBackendAllows("x11") {
125+ return false
126+ }
102127 display := os.Getenv("DISPLAY")
103128 if display == "" {
104129 return false
@@ -114,7 +139,7 @@ func testHasUsableX11Display() bool {
114139 x11SocketCandidates = append(x11SocketCandidates, fallback)
115140 }
116141 for _, candidate := range x11SocketCandidates {
117- if _, err := os.Stat(candidate); err == nil {
142+ if testPathIsSocket(candidate) {
118143 return true
119144 }
120145 }
@@ -192,8 +217,8 @@ func testXauthorityHasDisplay(data []byte, displayNum string) bool {
192217 if !ok {
193218 return false
194219 }
195- if string(num) == displayNum {
196- return len(name) > 0 && len(cookie) > 0
220+ if string(num) == displayNum && len(name) > 0 && len(cookie) > 0 {
221+ return true
197222 }
198223 data = rest
199224 }
@@ -212,12 +237,49 @@ func testReadXauthorityField(data []byte) ([]byte, []byte, bool) {
212237 return data[:fieldLen], data[fieldLen:], true
213238 }
214239
215-// testNeedsX11Auth reports whether GTK is expected to use the X11 backend,
216-// meaning X11 authorization must be available. When a usable Wayland display
217-// is present and GDK_BACKEND is not forced to x11, X11 auth is not required
218-// because GTK will use the Wayland backend.
240+// testNeedsX11Auth reports whether GTK may need the X11 backend and therefore
241+// requires X11 authorization before GTK initialization. Wayland-only backend
242+// configuration never needs X11 auth; X11 is required when it is the only usable
243+// allowed backend.
219244 func testNeedsX11Auth() bool {
220- return !testHasUsableWaylandDisplay() || os.Getenv("GDK_BACKEND") == "x11"
245+ if !testGDKBackendAllows("x11") {
246+ return false
247+ }
248+ return !testGDKBackendAllows("wayland") || !testHasUsableWaylandDisplay()
249+}
250+
251+func testCreateUnixSocket(t *testing.T, path string) {
252+ t.Helper()
253+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
254+ t.Fatal(err)
255+ }
256+ listener, err := net.Listen("unix", path)
257+ if err != nil {
258+ t.Fatal(err)
259+ }
260+ t.Cleanup(func() {
261+ _ = listener.Close()
262+ _ = os.Remove(path)
263+ })
264+}
265+
266+func testUnusedX11DisplayNumber(t *testing.T) string {
267+ t.Helper()
268+ for display := 90; display < 300; display++ {
269+ displayNum := strconv.Itoa(display)
270+ if !testPathIsSocket("/tmp/.X11-unix/X" + displayNum) {
271+ return displayNum
272+ }
273+ }
274+ t.Fatal("could not find an unused X11 display number")
275+ return ""
276+}
277+
278+func testCreateX11Socket(t *testing.T, displayNum string) {
279+ t.Helper()
280+ tmpDir := t.TempDir()
281+ t.Setenv("TMPDIR", tmpDir)
282+ testCreateUnixSocket(t, filepath.Join(tmpDir, ".X11-unix", "X"+displayNum))
221283 }
222284
223285 func requireGTKDisplayApp(t *testing.T) *gtk.Application {
@@ -227,8 +289,7 @@ func requireGTKDisplayApp(t *testing.T) *gtk.Application {
227289 if !hasWayland && !hasX11 {
228290 t.Skip("GTK display not available (no DISPLAY/WAYLAND_DISPLAY socket)")
229291 }
230- // Only require X11 auth when the X11 backend will be used:
231- // no Wayland (fallback to X11), or GDK_BACKEND explicitly set to x11.
292+ // Only require X11 auth when X11 is the only usable allowed backend.
232293 if testNeedsX11Auth() && !testHasX11Auth() {
233294 t.Skip("GTK display not available (X11 authorization unavailable)")
234295 }
@@ -254,16 +315,17 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
254315 })
255316
256317 t.Run("DISPLAY numeric without socket", func(t *testing.T) {
257- t.Setenv("DISPLAY", ":99")
318+ displayNum := testUnusedX11DisplayNumber(t)
319+ t.Setenv("DISPLAY", ":"+displayNum)
258320 t.Setenv("WAYLAND_DISPLAY", "")
259- // No /tmp/.X11-unix/X99 socket exists
260321 if got := testHasUsableGTKDisplay(); got {
261322 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
262323 }
263324 })
264325
265326 t.Run("DISPLAY with screen without socket", func(t *testing.T) {
266- t.Setenv("DISPLAY", ":99.0")
327+ displayNum := testUnusedX11DisplayNumber(t)
328+ t.Setenv("DISPLAY", ":"+displayNum+".0")
267329 t.Setenv("WAYLAND_DISPLAY", "")
268330 if got := testHasUsableGTKDisplay(); got {
269331 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
@@ -287,13 +349,15 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
287349 })
288350
289351 t.Run("DISPLAY with existing socket", func(t *testing.T) {
290- t.Setenv("DISPLAY", ":0")
352+ displayNum := testUnusedX11DisplayNumber(t)
353+ tmpDir := t.TempDir()
354+ t.Setenv("TMPDIR", tmpDir)
355+ t.Setenv("DISPLAY", ":"+displayNum)
291356 t.Setenv("WAYLAND_DISPLAY", "")
292- // /tmp/.X11-unix/X0 exists on this machine
293- got := testHasUsableGTKDisplay()
294- // We can't guarantee the socket exists in all environments,
295- // but this verifies it doesn't crash.
296- t.Logf("testHasUsableGTKDisplay() = %v (depends on /tmp/.X11-unix/X0)", got)
357+ testCreateUnixSocket(t, filepath.Join(tmpDir, ".X11-unix", "X"+displayNum))
358+ if got := testHasUsableGTKDisplay(); !got {
359+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with existing X11 socket", got)
360+ }
297361 })
298362
299363 t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
@@ -301,9 +365,7 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
301365 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
302366 tmpDir := t.TempDir()
303367 socketPath := filepath.Join(tmpDir, "wayland-1")
304- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
305- t.Fatal(err)
306- }
368+ testCreateUnixSocket(t, socketPath)
307369 // XDG_RUNTIME_DIR controls where Wayland sockets are looked up
308370 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
309371 if got := testHasUsableGTKDisplay(); !got {
@@ -323,9 +385,7 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
323385 t.Run("WAYLAND_DISPLAY absolute path with existing socket", func(t *testing.T) {
324386 t.Setenv("DISPLAY", "")
325387 socketPath := filepath.Join(t.TempDir(), "wayland.sock")
326- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
327- t.Fatal(err)
328- }
388+ testCreateUnixSocket(t, socketPath)
329389 t.Setenv("WAYLAND_DISPLAY", socketPath)
330390 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent") // Should be ignored for absolute paths
331391 if got := testHasUsableGTKDisplay(); !got {
@@ -518,6 +578,21 @@ func Test_testHasX11Auth_MalformedRecords(t *testing.T) {
518578 t.Errorf("testHasX11Auth() = %v, want true for third entry matching display :3", got)
519579 }
520580 })
581+
582+ t.Run("invalid matching entry does not hide later valid match", func(t *testing.T) {
583+ t.Setenv("DISPLAY", ":3")
584+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
585+ var buf bytes.Buffer
586+ writeXauthRecord(&buf, "3", "", []byte{0x01, 0x02, 0x03, 0x04})
587+ writeXauthRecord(&buf, "3", "MIT-MAGIC-COOKIE-1", []byte{0x09, 0x0a, 0x0b, 0x0c})
588+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
589+ t.Fatal(err)
590+ }
591+ t.Setenv("XAUTHORITY", authFile)
592+ if got := testHasX11Auth(); !got {
593+ t.Errorf("testHasX11Auth() = %v, want true for later valid matching display", got)
594+ }
595+ })
521596 }
522597
523598 func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
@@ -532,9 +607,7 @@ func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
532607 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
533608 tmpDir := t.TempDir()
534609 socketPath := filepath.Join(tmpDir, "wayland-1")
535- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
536- t.Fatal(err)
537- }
610+ testCreateUnixSocket(t, socketPath)
538611 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
539612 if got := testHasUsableWaylandDisplay(); !got {
540613 t.Errorf("testHasUsableWaylandDisplay() = %v, want true with existing wayland socket", got)
@@ -543,9 +616,7 @@ func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
543616
544617 t.Run("WAYLAND_DISPLAY absolute path", func(t *testing.T) {
545618 socketPath := filepath.Join(t.TempDir(), "wayland.sock")
546- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
547- t.Fatal(err)
548- }
619+ testCreateUnixSocket(t, socketPath)
549620 t.Setenv("WAYLAND_DISPLAY", socketPath)
550621 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent")
551622 if got := testHasUsableWaylandDisplay(); !got {
@@ -563,7 +634,8 @@ func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
563634 })
564635
565636 t.Run("DISPLAY numeric without socket", func(t *testing.T) {
566- t.Setenv("DISPLAY", ":99")
637+ displayNum := testUnusedX11DisplayNumber(t)
638+ t.Setenv("DISPLAY", ":"+displayNum)
567639 if got := testHasUsableX11Display(); got {
568640 t.Errorf("testHasUsableX11Display() = %v, want false", got)
569641 }
@@ -577,15 +649,54 @@ func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
577649 })
578650 }
579651
652+func Test_testHasUsableGTKDisplay_GDKBackendSelection(t *testing.T) {
653+ t.Run("GDK_BACKEND=x11 ignores Wayland-only socket", func(t *testing.T) {
654+ t.Setenv("GDK_BACKEND", "x11")
655+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
656+ tmpDir := t.TempDir()
657+ testCreateUnixSocket(t, filepath.Join(tmpDir, "wayland-1"))
658+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
659+ t.Setenv("DISPLAY", ":"+testUnusedX11DisplayNumber(t))
660+ if got := testHasUsableGTKDisplay(); got {
661+ t.Errorf("testHasUsableGTKDisplay() = %v, want false when x11 is forced without an X11 socket", got)
662+ }
663+ })
664+
665+ t.Run("GDK_BACKEND=wayland ignores X11-only socket", func(t *testing.T) {
666+ displayNum := testUnusedX11DisplayNumber(t)
667+ t.Setenv("GDK_BACKEND", "wayland")
668+ t.Setenv("DISPLAY", ":"+displayNum)
669+ t.Setenv("WAYLAND_DISPLAY", "wayland-missing")
670+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
671+ testCreateX11Socket(t, displayNum)
672+ if got := testHasUsableGTKDisplay(); got {
673+ t.Errorf("testHasUsableGTKDisplay() = %v, want false when wayland is forced without a Wayland socket", got)
674+ }
675+ })
676+
677+ t.Run("GDK_BACKEND=wayland,x11 allows X11 fallback", func(t *testing.T) {
678+ displayNum := testUnusedX11DisplayNumber(t)
679+ t.Setenv("GDK_BACKEND", "wayland,x11")
680+ t.Setenv("DISPLAY", ":"+displayNum)
681+ t.Setenv("WAYLAND_DISPLAY", "wayland-missing")
682+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
683+ testCreateX11Socket(t, displayNum)
684+ if got := testHasUsableGTKDisplay(); !got {
685+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with X11 fallback socket", got)
686+ }
687+ if got := testNeedsX11Auth(); !got {
688+ t.Errorf("testNeedsX11Auth() = %v, want true when X11 is the only usable backend", got)
689+ }
690+ })
691+}
692+
580693 func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
581694 t.Run("Wayland available forces no X11 auth", func(t *testing.T) {
582695 t.Setenv("GDK_BACKEND", "")
583696 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
584697 tmpDir := t.TempDir()
585698 socketPath := filepath.Join(tmpDir, "wayland-1")
586- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
587- t.Fatal(err)
588- }
699+ testCreateUnixSocket(t, socketPath)
589700 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
590701 t.Setenv("DISPLAY", ":0")
591702 if got := testNeedsX11Auth(); got {
@@ -598,9 +709,7 @@ func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
598709 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
599710 tmpDir := t.TempDir()
600711 socketPath := filepath.Join(tmpDir, "wayland-1")
601- if err := os.WriteFile(socketPath, nil, 0644); err != nil {
602- t.Fatal(err)
603- }
712+ testCreateUnixSocket(t, socketPath)
604713 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
605714 if got := testNeedsX11Auth(); !got {
606715 t.Errorf("testNeedsX11Auth() = %v, want true when GDK_BACKEND=x11", got)
@@ -7,6 +7,7 @@ import (
7 "errors"7 "errors"
8 "io"8 "io"
9 "math"9 "math"
10+ "net"
10 "os"11 "os"
11 "path/filepath"12 "path/filepath"
12 "reflect"13 "reflect"
@@ -81,7 +82,28 @@ func (r *fakeSessionStateRepo) GetAllSnapshots(context.Context) ([]*entity.Sessi
81 82
82 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }83 func (r *fakeSessionStateRepo) GetTotalSnapshotsSize(context.Context) (int64, error) { return 0, nil }
83 84
85+func testPathIsSocket(path string) bool {
86+ info, err := os.Stat(path)
87+ return err == nil && info.Mode()&os.ModeSocket != 0
88+}
89+
90+func testGDKBackendAllows(backend string) bool {
91+ configured := os.Getenv("GDK_BACKEND")
92+ if configured == "" {
93+ return true
94+ }
95+ for _, candidate := range strings.Split(configured, ",") {
96+ if strings.TrimSpace(candidate) == backend {
97+ return true
98+ }
99+ }
100+ return false
101+}
102+
84 func testHasUsableWaylandDisplay() bool {103 func testHasUsableWaylandDisplay() bool {
104+ if !testGDKBackendAllows("wayland") {
105+ return false
106+ }
85 waylandDisplay := os.Getenv("WAYLAND_DISPLAY")107 waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
86 if waylandDisplay == "" {108 if waylandDisplay == "" {
87 return false109 return false
@@ -91,7 +113,7 @@ func testHasUsableWaylandDisplay() bool {
91 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)113 candidates = append([]string{filepath.Join(runtimeDir, waylandDisplay)}, candidates...)
92 }114 }
93 for _, candidate := range candidates {115 for _, candidate := range candidates {
94- if _, err := os.Stat(candidate); err == nil {116+ if testPathIsSocket(candidate) {
95 return true117 return true
96 }118 }
97 }119 }
@@ -99,6 +121,9 @@ func testHasUsableWaylandDisplay() bool {
99 }121 }
100 122
101 func testHasUsableX11Display() bool {123 func testHasUsableX11Display() bool {
124+ if !testGDKBackendAllows("x11") {
125+ return false
126+ }
102 display := os.Getenv("DISPLAY")127 display := os.Getenv("DISPLAY")
103 if display == "" {128 if display == "" {
104 return false129 return false
@@ -114,7 +139,7 @@ func testHasUsableX11Display() bool {
114 x11SocketCandidates = append(x11SocketCandidates, fallback)139 x11SocketCandidates = append(x11SocketCandidates, fallback)
115 }140 }
116 for _, candidate := range x11SocketCandidates {141 for _, candidate := range x11SocketCandidates {
117- if _, err := os.Stat(candidate); err == nil {142+ if testPathIsSocket(candidate) {
118 return true143 return true
119 }144 }
120 }145 }
@@ -192,8 +217,8 @@ func testXauthorityHasDisplay(data []byte, displayNum string) bool {
192 if !ok {217 if !ok {
193 return false218 return false
194 }219 }
195- if string(num) == displayNum {220+ if string(num) == displayNum && len(name) > 0 && len(cookie) > 0 {
196- return len(name) > 0 && len(cookie) > 0221+ return true
197 }222 }
198 data = rest223 data = rest
199 }224 }
@@ -212,12 +237,49 @@ func testReadXauthorityField(data []byte) ([]byte, []byte, bool) {
212 return data[:fieldLen], data[fieldLen:], true237 return data[:fieldLen], data[fieldLen:], true
213 }238 }
214 239
215-// testNeedsX11Auth reports whether GTK is expected to use the X11 backend,240+// testNeedsX11Auth reports whether GTK may need the X11 backend and therefore
216-// meaning X11 authorization must be available. When a usable Wayland display241+// requires X11 authorization before GTK initialization. Wayland-only backend
217-// is present and GDK_BACKEND is not forced to x11, X11 auth is not required242+// configuration never needs X11 auth; X11 is required when it is the only usable
218-// because GTK will use the Wayland backend.243+// allowed backend.
219 func testNeedsX11Auth() bool {244 func testNeedsX11Auth() bool {
220- return !testHasUsableWaylandDisplay() || os.Getenv("GDK_BACKEND") == "x11"245+ if !testGDKBackendAllows("x11") {
246+ return false
247+ }
248+ return !testGDKBackendAllows("wayland") || !testHasUsableWaylandDisplay()
249+}
250+
251+func testCreateUnixSocket(t *testing.T, path string) {
252+ t.Helper()
253+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
254+ t.Fatal(err)
255+ }
256+ listener, err := net.Listen("unix", path)
257+ if err != nil {
258+ t.Fatal(err)
259+ }
260+ t.Cleanup(func() {
261+ _ = listener.Close()
262+ _ = os.Remove(path)
263+ })
264+}
265+
266+func testUnusedX11DisplayNumber(t *testing.T) string {
267+ t.Helper()
268+ for display := 90; display < 300; display++ {
269+ displayNum := strconv.Itoa(display)
270+ if !testPathIsSocket("/tmp/.X11-unix/X" + displayNum) {
271+ return displayNum
272+ }
273+ }
274+ t.Fatal("could not find an unused X11 display number")
275+ return ""
276+}
277+
278+func testCreateX11Socket(t *testing.T, displayNum string) {
279+ t.Helper()
280+ tmpDir := t.TempDir()
281+ t.Setenv("TMPDIR", tmpDir)
282+ testCreateUnixSocket(t, filepath.Join(tmpDir, ".X11-unix", "X"+displayNum))
221 }283 }
222 284
223 func requireGTKDisplayApp(t *testing.T) *gtk.Application {285 func requireGTKDisplayApp(t *testing.T) *gtk.Application {
@@ -227,8 +289,7 @@ func requireGTKDisplayApp(t *testing.T) *gtk.Application {
227 if !hasWayland && !hasX11 {289 if !hasWayland && !hasX11 {
228 t.Skip("GTK display not available (no DISPLAY/WAYLAND_DISPLAY socket)")290 t.Skip("GTK display not available (no DISPLAY/WAYLAND_DISPLAY socket)")
229 }291 }
230- // Only require X11 auth when the X11 backend will be used:292+ // Only require X11 auth when X11 is the only usable allowed backend.
231- // no Wayland (fallback to X11), or GDK_BACKEND explicitly set to x11.
232 if testNeedsX11Auth() && !testHasX11Auth() {293 if testNeedsX11Auth() && !testHasX11Auth() {
233 t.Skip("GTK display not available (X11 authorization unavailable)")294 t.Skip("GTK display not available (X11 authorization unavailable)")
234 }295 }
@@ -254,16 +315,17 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
254 })315 })
255 316
256 t.Run("DISPLAY numeric without socket", func(t *testing.T) {317 t.Run("DISPLAY numeric without socket", func(t *testing.T) {
257- t.Setenv("DISPLAY", ":99")318+ displayNum := testUnusedX11DisplayNumber(t)
319+ t.Setenv("DISPLAY", ":"+displayNum)
258 t.Setenv("WAYLAND_DISPLAY", "")320 t.Setenv("WAYLAND_DISPLAY", "")
259- // No /tmp/.X11-unix/X99 socket exists
260 if got := testHasUsableGTKDisplay(); got {321 if got := testHasUsableGTKDisplay(); got {
261 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)322 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
262 }323 }
263 })324 })
264 325
265 t.Run("DISPLAY with screen without socket", func(t *testing.T) {326 t.Run("DISPLAY with screen without socket", func(t *testing.T) {
266- t.Setenv("DISPLAY", ":99.0")327+ displayNum := testUnusedX11DisplayNumber(t)
328+ t.Setenv("DISPLAY", ":"+displayNum+".0")
267 t.Setenv("WAYLAND_DISPLAY", "")329 t.Setenv("WAYLAND_DISPLAY", "")
268 if got := testHasUsableGTKDisplay(); got {330 if got := testHasUsableGTKDisplay(); got {
269 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)331 t.Errorf("testHasUsableGTKDisplay() = %v, want false", got)
@@ -287,13 +349,15 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
287 })349 })
288 350
289 t.Run("DISPLAY with existing socket", func(t *testing.T) {351 t.Run("DISPLAY with existing socket", func(t *testing.T) {
290- t.Setenv("DISPLAY", ":0")352+ displayNum := testUnusedX11DisplayNumber(t)
353+ tmpDir := t.TempDir()
354+ t.Setenv("TMPDIR", tmpDir)
355+ t.Setenv("DISPLAY", ":"+displayNum)
291 t.Setenv("WAYLAND_DISPLAY", "")356 t.Setenv("WAYLAND_DISPLAY", "")
292- // /tmp/.X11-unix/X0 exists on this machine357+ testCreateUnixSocket(t, filepath.Join(tmpDir, ".X11-unix", "X"+displayNum))
293- got := testHasUsableGTKDisplay()358+ if got := testHasUsableGTKDisplay(); !got {
294- // We can't guarantee the socket exists in all environments,359+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with existing X11 socket", got)
295- // but this verifies it doesn't crash.360+ }
296- t.Logf("testHasUsableGTKDisplay() = %v (depends on /tmp/.X11-unix/X0)", got)
297 })361 })
298 362
299 t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {363 t.Run("WAYLAND_DISPLAY with existing socket", func(t *testing.T) {
@@ -301,9 +365,7 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
301 t.Setenv("WAYLAND_DISPLAY", "wayland-1")365 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
302 tmpDir := t.TempDir()366 tmpDir := t.TempDir()
303 socketPath := filepath.Join(tmpDir, "wayland-1")367 socketPath := filepath.Join(tmpDir, "wayland-1")
304- if err := os.WriteFile(socketPath, nil, 0644); err != nil {368+ testCreateUnixSocket(t, socketPath)
305- t.Fatal(err)
306- }
307 // XDG_RUNTIME_DIR controls where Wayland sockets are looked up369 // XDG_RUNTIME_DIR controls where Wayland sockets are looked up
308 t.Setenv("XDG_RUNTIME_DIR", tmpDir)370 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
309 if got := testHasUsableGTKDisplay(); !got {371 if got := testHasUsableGTKDisplay(); !got {
@@ -323,9 +385,7 @@ func Test_testHasUsableGTKDisplay_PureProbe(t *testing.T) {
323 t.Run("WAYLAND_DISPLAY absolute path with existing socket", func(t *testing.T) {385 t.Run("WAYLAND_DISPLAY absolute path with existing socket", func(t *testing.T) {
324 t.Setenv("DISPLAY", "")386 t.Setenv("DISPLAY", "")
325 socketPath := filepath.Join(t.TempDir(), "wayland.sock")387 socketPath := filepath.Join(t.TempDir(), "wayland.sock")
326- if err := os.WriteFile(socketPath, nil, 0644); err != nil {388+ testCreateUnixSocket(t, socketPath)
327- t.Fatal(err)
328- }
329 t.Setenv("WAYLAND_DISPLAY", socketPath)389 t.Setenv("WAYLAND_DISPLAY", socketPath)
330 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent") // Should be ignored for absolute paths390 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent") // Should be ignored for absolute paths
331 if got := testHasUsableGTKDisplay(); !got {391 if got := testHasUsableGTKDisplay(); !got {
@@ -518,6 +578,21 @@ func Test_testHasX11Auth_MalformedRecords(t *testing.T) {
518 t.Errorf("testHasX11Auth() = %v, want true for third entry matching display :3", got)578 t.Errorf("testHasX11Auth() = %v, want true for third entry matching display :3", got)
519 }579 }
520 })580 })
581+
582+ t.Run("invalid matching entry does not hide later valid match", func(t *testing.T) {
583+ t.Setenv("DISPLAY", ":3")
584+ authFile := filepath.Join(t.TempDir(), ".Xauthority")
585+ var buf bytes.Buffer
586+ writeXauthRecord(&buf, "3", "", []byte{0x01, 0x02, 0x03, 0x04})
587+ writeXauthRecord(&buf, "3", "MIT-MAGIC-COOKIE-1", []byte{0x09, 0x0a, 0x0b, 0x0c})
588+ if err := os.WriteFile(authFile, buf.Bytes(), 0644); err != nil {
589+ t.Fatal(err)
590+ }
591+ t.Setenv("XAUTHORITY", authFile)
592+ if got := testHasX11Auth(); !got {
593+ t.Errorf("testHasX11Auth() = %v, want true for later valid matching display", got)
594+ }
595+ })
521 }596 }
522 597
523 func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {598 func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
@@ -532,9 +607,7 @@ func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
532 t.Setenv("WAYLAND_DISPLAY", "wayland-1")607 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
533 tmpDir := t.TempDir()608 tmpDir := t.TempDir()
534 socketPath := filepath.Join(tmpDir, "wayland-1")609 socketPath := filepath.Join(tmpDir, "wayland-1")
535- if err := os.WriteFile(socketPath, nil, 0644); err != nil {610+ testCreateUnixSocket(t, socketPath)
536- t.Fatal(err)
537- }
538 t.Setenv("XDG_RUNTIME_DIR", tmpDir)611 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
539 if got := testHasUsableWaylandDisplay(); !got {612 if got := testHasUsableWaylandDisplay(); !got {
540 t.Errorf("testHasUsableWaylandDisplay() = %v, want true with existing wayland socket", got)613 t.Errorf("testHasUsableWaylandDisplay() = %v, want true with existing wayland socket", got)
@@ -543,9 +616,7 @@ func Test_testHasUsableWaylandDisplay_PureProbe(t *testing.T) {
543 616
544 t.Run("WAYLAND_DISPLAY absolute path", func(t *testing.T) {617 t.Run("WAYLAND_DISPLAY absolute path", func(t *testing.T) {
545 socketPath := filepath.Join(t.TempDir(), "wayland.sock")618 socketPath := filepath.Join(t.TempDir(), "wayland.sock")
546- if err := os.WriteFile(socketPath, nil, 0644); err != nil {619+ testCreateUnixSocket(t, socketPath)
547- t.Fatal(err)
548- }
549 t.Setenv("WAYLAND_DISPLAY", socketPath)620 t.Setenv("WAYLAND_DISPLAY", socketPath)
550 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent")621 t.Setenv("XDG_RUNTIME_DIR", "/nonexistent")
551 if got := testHasUsableWaylandDisplay(); !got {622 if got := testHasUsableWaylandDisplay(); !got {
@@ -563,7 +634,8 @@ func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
563 })634 })
564 635
565 t.Run("DISPLAY numeric without socket", func(t *testing.T) {636 t.Run("DISPLAY numeric without socket", func(t *testing.T) {
566- t.Setenv("DISPLAY", ":99")637+ displayNum := testUnusedX11DisplayNumber(t)
638+ t.Setenv("DISPLAY", ":"+displayNum)
567 if got := testHasUsableX11Display(); got {639 if got := testHasUsableX11Display(); got {
568 t.Errorf("testHasUsableX11Display() = %v, want false", got)640 t.Errorf("testHasUsableX11Display() = %v, want false", got)
569 }641 }
@@ -577,15 +649,54 @@ func Test_testHasUsableX11Display_PureProbe(t *testing.T) {
577 })649 })
578 }650 }
579 651
652+func Test_testHasUsableGTKDisplay_GDKBackendSelection(t *testing.T) {
653+ t.Run("GDK_BACKEND=x11 ignores Wayland-only socket", func(t *testing.T) {
654+ t.Setenv("GDK_BACKEND", "x11")
655+ t.Setenv("WAYLAND_DISPLAY", "wayland-1")
656+ tmpDir := t.TempDir()
657+ testCreateUnixSocket(t, filepath.Join(tmpDir, "wayland-1"))
658+ t.Setenv("XDG_RUNTIME_DIR", tmpDir)
659+ t.Setenv("DISPLAY", ":"+testUnusedX11DisplayNumber(t))
660+ if got := testHasUsableGTKDisplay(); got {
661+ t.Errorf("testHasUsableGTKDisplay() = %v, want false when x11 is forced without an X11 socket", got)
662+ }
663+ })
664+
665+ t.Run("GDK_BACKEND=wayland ignores X11-only socket", func(t *testing.T) {
666+ displayNum := testUnusedX11DisplayNumber(t)
667+ t.Setenv("GDK_BACKEND", "wayland")
668+ t.Setenv("DISPLAY", ":"+displayNum)
669+ t.Setenv("WAYLAND_DISPLAY", "wayland-missing")
670+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
671+ testCreateX11Socket(t, displayNum)
672+ if got := testHasUsableGTKDisplay(); got {
673+ t.Errorf("testHasUsableGTKDisplay() = %v, want false when wayland is forced without a Wayland socket", got)
674+ }
675+ })
676+
677+ t.Run("GDK_BACKEND=wayland,x11 allows X11 fallback", func(t *testing.T) {
678+ displayNum := testUnusedX11DisplayNumber(t)
679+ t.Setenv("GDK_BACKEND", "wayland,x11")
680+ t.Setenv("DISPLAY", ":"+displayNum)
681+ t.Setenv("WAYLAND_DISPLAY", "wayland-missing")
682+ t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
683+ testCreateX11Socket(t, displayNum)
684+ if got := testHasUsableGTKDisplay(); !got {
685+ t.Errorf("testHasUsableGTKDisplay() = %v, want true with X11 fallback socket", got)
686+ }
687+ if got := testNeedsX11Auth(); !got {
688+ t.Errorf("testNeedsX11Auth() = %v, want true when X11 is the only usable backend", got)
689+ }
690+ })
691+}
692+
580 func Test_testNeedsX11Auth_PureProbe(t *testing.T) {693 func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
581 t.Run("Wayland available forces no X11 auth", func(t *testing.T) {694 t.Run("Wayland available forces no X11 auth", func(t *testing.T) {
582 t.Setenv("GDK_BACKEND", "")695 t.Setenv("GDK_BACKEND", "")
583 t.Setenv("WAYLAND_DISPLAY", "wayland-1")696 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
584 tmpDir := t.TempDir()697 tmpDir := t.TempDir()
585 socketPath := filepath.Join(tmpDir, "wayland-1")698 socketPath := filepath.Join(tmpDir, "wayland-1")
586- if err := os.WriteFile(socketPath, nil, 0644); err != nil {699+ testCreateUnixSocket(t, socketPath)
587- t.Fatal(err)
588- }
589 t.Setenv("XDG_RUNTIME_DIR", tmpDir)700 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
590 t.Setenv("DISPLAY", ":0")701 t.Setenv("DISPLAY", ":0")
591 if got := testNeedsX11Auth(); got {702 if got := testNeedsX11Auth(); got {
@@ -598,9 +709,7 @@ func Test_testNeedsX11Auth_PureProbe(t *testing.T) {
598 t.Setenv("WAYLAND_DISPLAY", "wayland-1")709 t.Setenv("WAYLAND_DISPLAY", "wayland-1")
599 tmpDir := t.TempDir()710 tmpDir := t.TempDir()
600 socketPath := filepath.Join(tmpDir, "wayland-1")711 socketPath := filepath.Join(tmpDir, "wayland-1")
601- if err := os.WriteFile(socketPath, nil, 0644); err != nil {712+ testCreateUnixSocket(t, socketPath)
602- t.Fatal(err)
603- }
604 t.Setenv("XDG_RUNTIME_DIR", tmpDir)713 t.Setenv("XDG_RUNTIME_DIR", tmpDir)
605 if got := testNeedsX11Auth(); !got {714 if got := testNeedsX11Auth(); !got {
606 t.Errorf("testNeedsX11Auth() = %v, want true when GDK_BACKEND=x11", got)715 t.Errorf("testNeedsX11Auth() = %v, want true when GDK_BACKEND=x11", got)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close