604 lines
22 KiB
Go
604 lines
22 KiB
Go
package display
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/xgb"
|
|
"github.com/BurntSushi/xgb/shape"
|
|
"github.com/BurntSushi/xgb/xproto"
|
|
)
|
|
|
|
func TestX11FullscreenArgsOrderByRole(t *testing.T) {
|
|
below := strings.Join(x11FullscreenArgs(0x1a2b, RoleBelow), " ")
|
|
if !strings.Contains(below, "add,fullscreen,below") {
|
|
t.Fatalf("args %q must request fullscreen with below stacking", below)
|
|
}
|
|
if !strings.Contains(below, "0x00001a2b") {
|
|
t.Fatalf("args %q must reference the kiosk window id", below)
|
|
}
|
|
|
|
above := strings.Join(x11FullscreenArgs(0x1a2b, RoleAbove), " ")
|
|
if strings.Contains(above, "below") {
|
|
t.Fatalf("args %q must never stack the OSD overlay below video", above)
|
|
}
|
|
if !strings.Contains(above, "add,fullscreen,above") {
|
|
t.Fatalf("args %q must request fullscreen with above stacking", above)
|
|
}
|
|
}
|
|
|
|
func TestX11SessionRedrawsOnlyOnExpose(t *testing.T) {
|
|
if !x11WantsRedraw(xproto.ExposeEvent{}) {
|
|
t.Fatal("expose events must trigger a redraw")
|
|
}
|
|
if x11WantsRedraw(xproto.MapNotifyEvent{}) {
|
|
t.Fatal("map notify events must not trigger a redraw")
|
|
}
|
|
}
|
|
|
|
func TestX11OpenerAppliesFullscreenArgsThroughHook(t *testing.T) {
|
|
var got [][]string
|
|
opener := X11Opener{Fullscreen: func(args []string) error {
|
|
got = append(got, args)
|
|
return errors.New("wmctrl missing")
|
|
}}
|
|
|
|
if err := opener.applyFullscreen(x11FullscreenArgs(0x42, RoleBelow)); err == nil {
|
|
t.Fatal("hook error must propagate for logging")
|
|
}
|
|
if len(got) != 1 || !strings.Contains(strings.Join(got[0], " "), "add,fullscreen,below") {
|
|
t.Fatalf("hook args %v must request fullscreen with below stacking", got)
|
|
}
|
|
}
|
|
|
|
func TestClassifyVideoChangeTracksVideoWindowLifecycle(t *testing.T) {
|
|
const own = xproto.Window(0x10)
|
|
const video = xproto.Window(0x20)
|
|
const other = xproto.Window(0x30)
|
|
|
|
change, w := classifyVideoChange(xproto.MapNotifyEvent{Window: video}, own)
|
|
if change != videoAppeared || w != video {
|
|
t.Fatalf("map of a foreign window must announce video, got %v %v", change, w)
|
|
}
|
|
|
|
change, _ = classifyVideoChange(xproto.MapNotifyEvent{Window: own}, own)
|
|
if change != videoUnrelated {
|
|
t.Fatalf("map of the kiosk window must be unrelated, got %v", change)
|
|
}
|
|
|
|
change, w = classifyVideoChange(xproto.UnmapNotifyEvent{Window: video}, own)
|
|
if change != videoDisappeared || w != video {
|
|
t.Fatalf("unmap must announce a video end candidate, got %v %v", change, w)
|
|
}
|
|
|
|
change, _ = classifyVideoChange(xproto.UnmapNotifyEvent{Window: own}, own)
|
|
if change != videoUnrelated {
|
|
t.Fatalf("unmap of the kiosk window must be unrelated, got %v", change)
|
|
}
|
|
|
|
change, w = classifyVideoChange(xproto.UnmapNotifyEvent{Window: other}, own)
|
|
if change != videoDisappeared || w != other {
|
|
t.Fatalf("unmap of a foreign window must be an end candidate, got %v %v", change, w)
|
|
}
|
|
|
|
change, w = classifyVideoChange(xproto.DestroyNotifyEvent{Window: video}, own)
|
|
if change != videoDisappeared || w != video {
|
|
t.Fatalf("destroy must announce a video end candidate, got %v %v", change, w)
|
|
}
|
|
|
|
change, _ = classifyVideoChange(xproto.ExposeEvent{}, own)
|
|
if change != videoUnrelated {
|
|
t.Fatalf("expose must not change video presence, got %v", change)
|
|
}
|
|
}
|
|
|
|
func TestAddAndRemoveVideoWindowTrackPresenceTransitions(t *testing.T) {
|
|
const video = xproto.Window(0x20)
|
|
const second = xproto.Window(0x30)
|
|
windows := make(map[xproto.Window]struct{})
|
|
|
|
if !addVideoWindow(windows, video) {
|
|
t.Fatal("first video window must change presence")
|
|
}
|
|
if addVideoWindow(windows, video) {
|
|
t.Fatal("re-adding the tracked window must not change presence")
|
|
}
|
|
if addVideoWindow(windows, second) {
|
|
t.Fatal("a second video window must not change presence")
|
|
}
|
|
if removeVideoWindow(windows, second) {
|
|
t.Fatal("removing one of two video windows must not change presence")
|
|
}
|
|
if !removeVideoWindow(windows, video) {
|
|
t.Fatal("removing the last video window must change presence")
|
|
}
|
|
if removeVideoWindow(windows, video) {
|
|
t.Fatal("removing an untracked window must not change presence")
|
|
}
|
|
if len(windows) != 0 {
|
|
t.Fatalf("set %v must be empty after all video ended", windows)
|
|
}
|
|
}
|
|
|
|
func TestEWMHNonVideoWindowTypesExcludeDesktopHelpers(t *testing.T) {
|
|
for _, name := range []string{"DOCK", "DESKTOP", "TOOLBAR", "MENU", "UTILITY", "SPLASH", "DIALOG", "NOTIFICATION", "TOOLTIP"} {
|
|
if !ewmhNonVideoTypes[name] {
|
|
t.Fatalf("window type %q must not count as video", name)
|
|
}
|
|
}
|
|
for _, name := range []string{"NORMAL", "", "VIDEO_PLAY"} {
|
|
if ewmhNonVideoTypes[name] {
|
|
t.Fatalf("window type %q must count as video", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUTF16Char2bEncodesPolishFallbackText(t *testing.T) {
|
|
chars := utf16Char2b(wantFallbackText, maxImageText16Units)
|
|
want := []xproto.Char2b{
|
|
{Byte1: 0, Byte2: 'B'}, {Byte1: 0, Byte2: 'r'}, {Byte1: 0, Byte2: 'a'}, {Byte1: 0, Byte2: 'k'}, {Byte1: 0, Byte2: ' '},
|
|
{Byte1: 0, Byte2: 'p'}, {Byte1: 0, Byte2: 'o'},
|
|
{Byte1: 0x01, Byte2: 0x42}, // ł
|
|
{Byte1: 0x01, Byte2: 0x05}, // ą
|
|
{Byte1: 0, Byte2: 'c'}, {Byte1: 0, Byte2: 'z'}, {Byte1: 0, Byte2: 'e'}, {Byte1: 0, Byte2: 'n'}, {Byte1: 0, Byte2: 'i'}, {Byte1: 0, Byte2: 'a'}, {Byte1: 0, Byte2: ' '},
|
|
{Byte1: 0, Byte2: 'W'}, {Byte1: 0, Byte2: 'F'}, {Byte1: 0, Byte2: 'B'},
|
|
}
|
|
if len(chars) != len(want) {
|
|
t.Fatalf("units=%v want %v", chars, want)
|
|
}
|
|
for i := range want {
|
|
if chars[i] != want[i] {
|
|
t.Fatalf("unit %d=%v want %v", i, chars[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUTF16Char2bTruncatesToOneRequestLength(t *testing.T) {
|
|
if chars := utf16Char2b(wantFallbackText, 10); len(chars) != 10 {
|
|
t.Fatalf("units=%d want 10", len(chars))
|
|
}
|
|
}
|
|
|
|
func TestUTF16Char2bNeverSplitsSurrogatePairs(t *testing.T) {
|
|
// U+1F3A5 encodes as a surrogate pair (0xD83C, 0xDFA5): a cut that would
|
|
// land between the two units must back off instead of splitting it.
|
|
if chars := utf16Char2b("a🎥", 2); len(chars) != 1 || chars[0].Byte2 != 'a' {
|
|
t.Fatalf("units=%v want only the leading ASCII unit", chars)
|
|
}
|
|
if chars := utf16Char2b("🎥", 1); len(chars) != 0 {
|
|
t.Fatalf("units=%v want none, the pair does not fit", chars)
|
|
}
|
|
if chars := utf16Char2b("🎥", 2); len(chars) != 2 {
|
|
t.Fatalf("units=%v want the intact pair", chars)
|
|
}
|
|
if chars := utf16Char2b(wantFallbackText, 0); len(chars) != 0 {
|
|
t.Fatalf("units=%v want none for a zero limit", chars)
|
|
}
|
|
}
|
|
|
|
func TestFoldToASCIIStripsPolishDiacritics(t *testing.T) {
|
|
if got := foldToASCII(wantFallbackText); got != "Brak polaczenia WFB" {
|
|
t.Fatalf("foldToASCII=%q want %q", got, "Brak polaczenia WFB")
|
|
}
|
|
}
|
|
|
|
func TestFoldToASCIIKeepsPlainASCIIUntouched(t *testing.T) {
|
|
if got := foldToASCII("plain text"); got != "plain text" {
|
|
t.Fatalf("foldToASCII=%q want %q", got, "plain text")
|
|
}
|
|
}
|
|
|
|
func TestX11FontPatternRequestsUnicodeFixedFont(t *testing.T) {
|
|
if !strings.Contains(iso10646FontPattern, "fixed") {
|
|
t.Fatalf("font pattern %q must select a fixed font", iso10646FontPattern)
|
|
}
|
|
if !strings.Contains(iso10646FontPattern, "iso10646") {
|
|
t.Fatalf("font pattern %q must select a Unicode encoding for the exact fallback text", iso10646FontPattern)
|
|
}
|
|
}
|
|
|
|
func TestLayerMarkerIdentifiesOwnLayerWindows(t *testing.T) {
|
|
if layerMarkerValue(RoleBelow) == layerMarkerValue(RoleAbove) {
|
|
t.Fatal("roles must carry distinct marker payloads")
|
|
}
|
|
for _, role := range []LayerRole{RoleBelow, RoleAbove} {
|
|
if !markerIsGroundstation(layerMarkerValue(role)) {
|
|
t.Fatalf("marker %q must identify our own window", layerMarkerValue(role))
|
|
}
|
|
}
|
|
for _, foreign := range []string{"", "video", "desktop", "groundstation-fake"} {
|
|
if markerIsGroundstation(foreign) {
|
|
t.Fatalf("foreign marker payload %q must not identify our windows", foreign)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarkerWalkBoundsStayBoundedAndCoverFrameDepth(t *testing.T) {
|
|
depth, nodes := markerWalkBounds()
|
|
if depth <= 0 || nodes <= 0 {
|
|
t.Fatalf("marker walk bounds depth=%d nodes=%d must be positive", depth, nodes)
|
|
}
|
|
if nodes < depth {
|
|
t.Fatalf("node budget %d must cover at least a depth-%d ancestor chain", nodes, depth)
|
|
}
|
|
// A reparenting window manager puts each of our two layer windows inside
|
|
// its own frame, so the walk must reach our window below one frame.
|
|
if depth < 2 {
|
|
t.Fatalf("marker walk depth %d must cover our window below a window-manager frame", depth)
|
|
}
|
|
}
|
|
|
|
func TestEWMHWindowTypeAnnouncesOverlayRoleOnly(t *testing.T) {
|
|
if name := ewmhWindowTypeName(RoleAbove); !strings.Contains(name, "DOCK") {
|
|
t.Fatalf("OSD overlay window type %q must be the always-above helper role", name)
|
|
}
|
|
if name := ewmhWindowTypeName(RoleBelow); name != "" {
|
|
t.Fatalf("fallback canvas must keep announcing no window type, got %q", name)
|
|
}
|
|
}
|
|
|
|
func TestPresentedVideoDecisionExcludesOwnLayersAndHelpers(t *testing.T) {
|
|
video := windowFacts{inputOutput: true, viewable: true}
|
|
if !isPresentedVideoWindow(video) {
|
|
t.Fatal("a presented output window must count as video")
|
|
}
|
|
for name, facts := range map[string]windowFacts{
|
|
"input-only": {viewable: true},
|
|
"override-redirect": {inputOutput: true, viewable: true, overrideRedirect: true},
|
|
"unmapped": {inputOutput: true},
|
|
"auxiliary": {inputOutput: true, viewable: true, auxiliary: true},
|
|
"groundstation-canvas": {inputOutput: true, viewable: true, groundstation: true},
|
|
"groundstation-osd": {inputOutput: true, viewable: true, groundstation: true},
|
|
} {
|
|
if isPresentedVideoWindow(facts) {
|
|
t.Fatalf("%s window must never count as video", name)
|
|
}
|
|
}
|
|
// Window-manager frames around our layer windows carry no marker of
|
|
// their own; groundstationLayerWindow finds the marker below the frame,
|
|
// so framed Ground Station layers land in these same excluded facts.
|
|
}
|
|
|
|
func TestStackOrderByRoleKeepsLayersOnTheirSideOfVideo(t *testing.T) {
|
|
if mode := stackModeForVideoAppear(RoleBelow); mode != xproto.StackModeBelow {
|
|
t.Fatalf("canvas must park below appearing video, got %v", mode)
|
|
}
|
|
if mode := stackModeForVideoAppear(RoleAbove); mode != xproto.StackModeAbove {
|
|
t.Fatalf("OSD overlay must rise above appearing video, got %v", mode)
|
|
}
|
|
if !raisesOnVideoEnd(RoleBelow) {
|
|
t.Fatal("canvas must raise itself when video ends so the fallback returns")
|
|
}
|
|
if raisesOnVideoEnd(RoleAbove) {
|
|
t.Fatal("OSD overlay is already above video and must not restack on video end")
|
|
}
|
|
}
|
|
|
|
func TestOSDEventMaskNeverSelectsDeviceInput(t *testing.T) {
|
|
if osdEventMask&deviceInputEventMask != 0 {
|
|
t.Fatalf("OSD event mask must not select pointer or key input: %b", osdEventMask)
|
|
}
|
|
if osdEventMask&xproto.EventMaskExposure == 0 {
|
|
t.Fatal("OSD event mask must still select exposure for redraws")
|
|
}
|
|
}
|
|
|
|
func TestStackReassertKeepsTheOSDOnTop(t *testing.T) {
|
|
// While video is live the overlay must sit above the tracked video
|
|
// window; while video is absent the overlay must rise to the top,
|
|
// because the fallback canvas raises itself to the top when video ends
|
|
// and would otherwise bury the overlay's text. Only the OSD overlay
|
|
// re-asserts; the canvas and video windows are never moved by this.
|
|
windows := map[xproto.Window]struct{}{0x20: {}}
|
|
|
|
if sibling, mode, ok := stackReassert(RoleAbove, true, 0x20, windows); !ok || sibling != 0x20 || mode != xproto.StackModeAbove {
|
|
t.Fatalf("sibling=%v mode=%v ok=%v want above the tracked video window", sibling, mode, ok)
|
|
}
|
|
if sibling, mode, ok := stackReassert(RoleAbove, false, 0, nil); !ok || sibling != 0 || mode != xproto.StackModeAbove {
|
|
t.Fatalf("sibling=%v mode=%v ok=%v want a top raise while video is absent", sibling, mode, ok)
|
|
}
|
|
if _, _, ok := stackReassert(RoleBelow, true, 0x20, windows); ok {
|
|
t.Fatal("the fallback canvas must never re-assert stacking")
|
|
}
|
|
if _, _, ok := stackReassert(RoleAbove, true, 0x30, windows); ok {
|
|
t.Fatal("a stale top video window must not be restacked against")
|
|
}
|
|
if _, _, ok := stackReassert(RoleAbove, true, 0, windows); ok {
|
|
t.Fatal("no tracked video window means no video-relative restack")
|
|
}
|
|
}
|
|
|
|
func TestCreateWindowValuesOrderMatchesTheValueMask(t *testing.T) {
|
|
// The CreateWindow values must follow the mask bit order (back pixel,
|
|
// event mask, cursor) so the server reads each value correctly; the
|
|
// blank cursor hides the pointer over the layer from its first frame.
|
|
mask, values := createWindowValues(0x100, 0x200, 0xc0de)
|
|
if mask != xproto.CwBackPixel|xproto.CwEventMask|xproto.CwCursor {
|
|
t.Fatalf("mask=%b want back pixel, event mask and cursor bits", mask)
|
|
}
|
|
if len(values) != 3 || values[0] != 0x100 || values[1] != 0x200 || values[2] != 0xc0de {
|
|
t.Fatalf("values=%v want [backPixel eventMask cursor]", values)
|
|
}
|
|
}
|
|
|
|
func TestFullscreenStateAtomsOrderByRole(t *testing.T) {
|
|
// Fullscreen is requested as a pre-map _NET_WM_STATE property so a
|
|
// compliant window manager maps each layer genuinely fullscreen and
|
|
// undecorated from its first frame; a post-map client message alone
|
|
// leaves a decorated, repositioned window on the screen at startup.
|
|
below := fullscreenStateAtoms(RoleBelow)
|
|
if len(below) != 2 || below[0] != "_NET_WM_STATE_FULLSCREEN" || below[1] != "_NET_WM_STATE_BELOW" {
|
|
t.Fatalf("canvas state atoms=%v want fullscreen below", below)
|
|
}
|
|
above := fullscreenStateAtoms(RoleAbove)
|
|
if len(above) != 2 || above[0] != "_NET_WM_STATE_FULLSCREEN" || above[1] != "_NET_WM_STATE_ABOVE" {
|
|
t.Fatalf("OSD state atoms=%v want fullscreen above", above)
|
|
}
|
|
}
|
|
|
|
// scriptedCursorX scripts the checked blank-cursor request surface: it
|
|
// records every operation in order, can fail any single operation, and
|
|
// records reclaim calls — reproducing the production request-order and
|
|
// resource-lifecycle discipline without a server.
|
|
type scriptedCursorX struct {
|
|
failAt string
|
|
ops []string
|
|
freed []string
|
|
depths []uint8
|
|
}
|
|
|
|
func (s *scriptedCursorX) CreatePixmapChecked(pid xproto.Pixmap, _ xproto.Drawable, depth uint8, _, _ uint16) error {
|
|
s.depths = append(s.depths, depth)
|
|
s.ops = append(s.ops, "pixmap:"+strconv.FormatUint(uint64(pid), 10))
|
|
if s.failAt == s.ops[len(s.ops)-1] {
|
|
return fmt.Errorf("bad pixmap request")
|
|
}
|
|
if s.failAt == "bitmap-depth" && depth != 1 {
|
|
return fmt.Errorf("cursor bitmaps must be depth 1")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *scriptedCursorX) CreateGCChecked(gc xproto.Gcontext, _ xproto.Drawable) error {
|
|
s.ops = append(s.ops, "gc")
|
|
if s.failAt == "gc" {
|
|
return fmt.Errorf("bad gc request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *scriptedCursorX) PolyFillRectangleChecked(drawable xproto.Drawable, _ xproto.Gcontext, _ []xproto.Rectangle) error {
|
|
s.ops = append(s.ops, "fill:"+strconv.FormatUint(uint64(drawable), 10))
|
|
if s.failAt == s.ops[len(s.ops)-1] {
|
|
return fmt.Errorf("bad fill request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *scriptedCursorX) FreeGC(gc xproto.Gcontext) {
|
|
s.ops = append(s.ops, "free-gc")
|
|
s.freed = append(s.freed, "gc")
|
|
}
|
|
|
|
func (s *scriptedCursorX) CreateCursorChecked(cid xproto.Cursor, _, _ xproto.Pixmap) error {
|
|
s.ops = append(s.ops, "cursor")
|
|
if s.failAt == "cursor" {
|
|
return fmt.Errorf("bad cursor request")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *scriptedCursorX) FreePixmap(pid xproto.Pixmap) {
|
|
s.freed = append(s.freed, "pixmap:"+strconv.FormatUint(uint64(pid), 10))
|
|
}
|
|
|
|
func TestBlankCursorBuildsCheckedDepthOneBitmapsInOrder(t *testing.T) {
|
|
// Cursor source and mask must be 1-bit bitmaps — the depth mismatch of
|
|
// the previous implementation is what made CreateCursor fail on the
|
|
// device and poisoned the window id. Every request is checked at its
|
|
// own operation, in order, and the pixmaps and context are reclaimed
|
|
// once the cursor exists.
|
|
x := &scriptedCursorX{}
|
|
cursor, err := blankCursorOver(x, 0x1, 1, 0x20, 0x21, 0x30, 0x40)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cursor != 0x40 {
|
|
t.Fatalf("cursor=%d want 0x40", cursor)
|
|
}
|
|
want := []string{"pixmap:32", "pixmap:33", "gc", "fill:32", "fill:33", "free-gc", "cursor"}
|
|
if strings.Join(x.ops, ",") != strings.Join(want, ",") {
|
|
t.Fatalf("ops=%v want %v", x.ops, want)
|
|
}
|
|
for _, depth := range x.depths {
|
|
if depth != 1 {
|
|
t.Fatalf("cursor bitmap depth=%d want 1", depth)
|
|
}
|
|
}
|
|
if strings.Join(x.freed, ",") != "gc,pixmap:32,pixmap:33" {
|
|
t.Fatalf("freed=%v want the context and both bitmaps reclaimed", x.freed)
|
|
}
|
|
}
|
|
|
|
func TestBlankCursorRejectsNonBitmapDepths(t *testing.T) {
|
|
// The device regression: cursor bitmaps created at the root depth made
|
|
// the unchecked CreateCursor fail asynchronously, which poisoned the
|
|
// window id and surfaced only later as BadWindow on the shape request.
|
|
// The checked sequence must reject such a depth at its own operation.
|
|
x := &scriptedCursorX{failAt: "bitmap-depth"}
|
|
cursor, err := blankCursorOver(x, 0x1, 24, 0x20, 0x21, 0x30, 0x40)
|
|
if err == nil {
|
|
t.Fatal("a non-bitmap depth must fail the cursor sequence")
|
|
}
|
|
if cursor != 0 {
|
|
t.Fatal("a failed sequence must not return a cursor")
|
|
}
|
|
if len(x.ops) != 1 || x.ops[0] != "pixmap:32" {
|
|
t.Fatalf("ops=%v want the failure to stop the sequence at the first request", x.ops)
|
|
}
|
|
}
|
|
|
|
func TestBlankCursorAttributesFailureToItsOwnOperation(t *testing.T) {
|
|
cases := []struct {
|
|
failAt string
|
|
want string
|
|
}{
|
|
{"pixmap:32", "create cursor source bitmap"},
|
|
{"pixmap:33", "create cursor mask bitmap"},
|
|
{"gc", "create cursor graphics context"},
|
|
{"fill:32", "clear cursor source bitmap"},
|
|
{"fill:33", "clear cursor mask bitmap"},
|
|
{"cursor", "create blank cursor"},
|
|
}
|
|
for _, c := range cases {
|
|
x := &scriptedCursorX{failAt: c.failAt}
|
|
cursor, err := blankCursorOver(x, 0x1, 1, 0x20, 0x21, 0x30, 0x40)
|
|
if err == nil {
|
|
t.Fatalf("failAt=%q must fail", c.failAt)
|
|
}
|
|
if cursor != 0 {
|
|
t.Fatalf("failAt=%q must not return a cursor", c.failAt)
|
|
}
|
|
if !strings.Contains(err.Error(), c.want) {
|
|
t.Fatalf("failAt=%q error=%v want attribution %q", c.failAt, err, c.want)
|
|
}
|
|
// No operation after the failed one may run — resource reclamation
|
|
// (free-gc, pixmap frees) is the expected cleanup, not a dependent
|
|
// request.
|
|
for i, op := range x.ops {
|
|
if op == c.failAt {
|
|
for _, later := range x.ops[i+1:] {
|
|
if strings.HasPrefix(later, "free-") || strings.HasPrefix(later, "pixmap:") {
|
|
continue
|
|
}
|
|
t.Fatalf("failAt=%q: dependent operation ran after failure: %v", c.failAt, later)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateWindowValuesOmitCursorWhenUnavailable(t *testing.T) {
|
|
// A failed cursor setup must never poison the window: with no cursor,
|
|
// the window maps with the default cursor and the value list drops the
|
|
// cursor bit entirely.
|
|
mask, values := createWindowValues(0x100, 0x200, 0)
|
|
if mask != xproto.CwBackPixel|xproto.CwEventMask {
|
|
t.Fatalf("mask=%b want back pixel and event mask only", mask)
|
|
}
|
|
if len(values) != 2 || values[0] != 0x100 || values[1] != 0x200 {
|
|
t.Fatalf("values=%v want [backPixel eventMask]", values)
|
|
}
|
|
}
|
|
|
|
func TestTransparentShapesEmptyInputAndOutput(t *testing.T) {
|
|
var input, output bool
|
|
for _, kind := range transparentShapeKinds() {
|
|
switch kind {
|
|
case shape.SkInput:
|
|
input = true
|
|
case shape.SkBounding:
|
|
output = true
|
|
}
|
|
}
|
|
if !input || !output {
|
|
t.Fatalf("shape kinds %v must empty both the input and the bounding region", transparentShapeKinds())
|
|
}
|
|
}
|
|
|
|
func TestOSDTextRectCoversTheGlyphCellAroundTheBaseline(t *testing.T) {
|
|
rect := osdTextRect(32, 32, 9, 6, 11, 3)
|
|
if rect.X != 30 || rect.Y != 19 {
|
|
t.Fatalf("origin=(%d,%d) want (30,19): the region must start left of and above the baseline", rect.X, rect.Y)
|
|
}
|
|
if rect.Width != 9*6+4 || rect.Height != 11+3+4 {
|
|
t.Fatalf("size=%dx%d want %dx%d with padding", rect.Width, rect.Height, 9*6+4, 11+3+4)
|
|
}
|
|
if osdTextRect(0, 0, 1, 6, 11, 3).Width == 0 {
|
|
t.Fatal("a one-unit text must still produce a visible region")
|
|
}
|
|
}
|
|
|
|
func TestX11SessionRecordsOSDTextRegionsOnlyAboveVideo(t *testing.T) {
|
|
above := &x11Session{role: RoleAbove, fontCell: 6, fontAscent: 11, fontDescent: 3}
|
|
above.recordOSDText(32, 32, 9)
|
|
above.recordOSDText(32, 64, 5)
|
|
if len(above.drawn) != 2 {
|
|
t.Fatalf("drawn=%v want one region per OSD text", above.drawn)
|
|
}
|
|
if above.drawn[0] != osdTextRect(32, 32, 9, 6, 11, 3) {
|
|
t.Fatalf("first region=%v want the glyph cell of the first text", above.drawn[0])
|
|
}
|
|
|
|
above.resetOSDRegions()
|
|
if len(above.drawn) != 0 {
|
|
t.Fatalf("drawn=%v want the recorded regions reset on clear", above.drawn)
|
|
}
|
|
|
|
below := &x11Session{role: RoleBelow, fontCell: 6, fontAscent: 11, fontDescent: 3}
|
|
below.recordOSDText(32, 32, 9)
|
|
if len(below.drawn) != 0 {
|
|
t.Fatalf("drawn=%v want none: the fallback canvas never punches output shapes", below.drawn)
|
|
}
|
|
}
|
|
|
|
func TestX11SessionWaitEventRefreshesOnItsInterval(t *testing.T) {
|
|
// The OSD layer redraws on its own slower cadence: refresh must return
|
|
// redraw even with no X events pending, so template data — the latest
|
|
// published snapshot — appears without video or exposure churn.
|
|
session := &x11Session{
|
|
poll: time.Millisecond,
|
|
refresh: 2 * time.Millisecond,
|
|
pollEvent: func() (xgb.Event, xgb.Error) { return nil, nil },
|
|
probe: func() error { return nil },
|
|
}
|
|
|
|
done := make(chan eventResult, 1)
|
|
go func() {
|
|
redraw, err := session.WaitEvent(context.Background())
|
|
done <- eventResult{redraw: redraw, err: err}
|
|
}()
|
|
select {
|
|
case result := <-done:
|
|
if result.err != nil {
|
|
t.Fatalf("WaitEvent=%v", result.err)
|
|
}
|
|
if !result.redraw {
|
|
t.Fatal("the refresh interval must request a redraw")
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("WaitEvent never returned the refresh redraw")
|
|
}
|
|
}
|
|
|
|
func TestX11SessionWaitEventWithoutRefreshKeepsWaiting(t *testing.T) {
|
|
probes := 0
|
|
session := &x11Session{
|
|
poll: time.Millisecond,
|
|
pollEvent: func() (xgb.Event, xgb.Error) { return nil, nil },
|
|
probe: func() error {
|
|
probes++
|
|
return nil
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
redraw, err := session.WaitEvent(ctx)
|
|
if !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("WaitEvent=%v want clean cancellation without a refresh interval", err)
|
|
}
|
|
if redraw {
|
|
t.Fatal("no refresh interval must mean no refresh redraws")
|
|
}
|
|
if probes == 0 {
|
|
t.Fatal("liveness probes must continue while idle")
|
|
}
|
|
}
|