Repository navigation
Expand file tree
/
Copy pathtray_menu_test.go
More file actions
156 lines (138 loc) · 5.48 KB
/
Copy pathtray_menu_test.go
File metadata and controls
156 lines (138 loc) · 5.48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package main
import (
"os"
"regexp"
"strings"
"testing"
)
// The notification-area menu is Win32 and cannot be clicked from a test, so this
// checks the one thing that went wrong without anyone noticing: an item was drawn
// in the menu and the dispatcher had no case for it. Clicking "Exit pingping" did
// nothing at all, in the one mode where it was offered, for every release so far.
//
// Nothing here compiles tray_windows.go — the file is Windows-only and this test
// reads it as text — so it runs on every platform and on every CI job, which is
// the point. A menu you cannot click from a test can still be read.
var (
reMenuAdd = regexp.MustCompile(`add\(h,\s*mf\w+,\s*(id\w+)\s*,`)
reMenuCase = regexp.MustCompile(`case\s+(id\w+)\s*:`)
)
func trayMenuSource(t *testing.T) string { return readSource(t, "tray_windows.go") }
// readSource reads a file from the package directory. Several tests here check
// things the compiler cannot: that a menu item has a handler, that a workflow
// still greps for the string a check emits.
func readSource(t *testing.T, name string) string {
t.Helper()
b, err := os.ReadFile(name)
if err != nil {
t.Fatalf("reading %s: %v", name, err)
}
return string(b)
}
func TestEveryTrayMenuItemIsHandled(t *testing.T) {
src := trayMenuSource(t)
handled := map[string]bool{}
for _, m := range reMenuCase.FindAllStringSubmatch(src, -1) {
handled[m[1]] = true
}
drawn := map[string]bool{}
for _, m := range reMenuAdd.FindAllStringSubmatch(src, -1) {
drawn[m[1]] = true
}
if len(drawn) < 5 {
t.Fatalf("only found %d menu items; the pattern that finds them has drifted "+
"from the code and this test is no longer checking anything", len(drawn))
}
for id := range drawn {
// A separator carries no command, and the language block is dispatched
// by range rather than by a case, because langOrder decides its size.
if id == "idLangBase" || id == "idNone" {
continue
}
if !handled[id] {
t.Errorf("%s is drawn in the menu but command() has no case for it: "+
"clicking it does nothing", id)
}
}
}
// Whatever else changes about the menu, there has to be a way out of it. An icon
// with no exit is at its worst precisely when the service is down and the icon is
// useless — which is when someone most wants it gone.
func TestTrayAlwaysOffersAWayOut(t *testing.T) {
src := trayMenuSource(t)
menu := src[strings.Index(src, "func (t *tray) showMenu()"):]
if end := strings.Index(menu, "\nfunc "); end > 0 {
menu = menu[:end]
}
if !strings.Contains(menu, "idExit") {
t.Fatal("showMenu draws no exit item")
}
// It must not be behind the service check. That is how it went missing: the
// item existed, and an installed copy never saw it.
for _, line := range strings.Split(menu, "\n") {
s := strings.TrimSpace(line)
if strings.HasPrefix(s, "if !t.service") || strings.HasPrefix(s, "if t.service {") {
rest := menu[strings.Index(menu, line):]
block := rest
if end := strings.Index(rest, "\n\t}"); end > 0 {
block = rest[:end]
}
// A mode-dependent LABEL is fine; a mode-dependent presence is not.
if strings.Contains(block, "idExit") && !strings.Contains(block, "else") {
t.Fatal("the exit item is inside a one-sided service check, so one " +
"mode gets a menu with no way out")
}
}
}
}
// Two menu items sharing a command id means clicking one silently performs the
// other. The compiler only notices when both have a case in the same switch —
// idQuitAll was given 1009, which idSettings already held, and that collision
// was caught by a duplicate-case error rather than by anything checking ids.
func TestTrayMenuIdsAreUnique(t *testing.T) {
src := trayMenuSource(t)
block := src[strings.Index(src, "idOpenConsole"):]
if end := strings.Index(block, "idLangBase"); end > 0 {
block = block[:end]
}
re := regexp.MustCompile(`(id\w+)\s*=\s*(\d+)`)
seen := map[string]string{}
found := 0
for _, m := range re.FindAllStringSubmatch(block, -1) {
name, val := m[1], m[2]
found++
if prev, dup := seen[val]; dup {
t.Errorf("%s and %s are both %s: one of them silently runs the other", prev, name, val)
}
seen[val] = name
}
if found < 5 {
t.Fatalf("only found %d menu ids; the pattern has drifted from the code", found)
}
}
// DestroyWindow may only be called by the thread that created the window, and
// this one lives on a locked OS thread running the message loop. A call from a
// goroutine fails silently — which is what "Stop pingping and exit the tray"
// did for a release: it stopped the service and left the icon on screen.
//
// So there is exactly one DestroyWindow, in the teardown that already runs on
// that thread, and everything else posts. This counts rather than reasons,
// because the failure mode is silence.
func TestOnlyOneDestroyWindowAndNotInACommand(t *testing.T) {
src := trayMenuSource(t)
if n := strings.Count(src, "procDestroyWindow.Call"); n != 1 {
t.Errorf("found %d DestroyWindow calls, want exactly 1 (in remove()); "+
"anything reachable from a goroutine must post WM_CLOSE instead", n)
}
cmd := src[strings.Index(src, "func (t *tray) command("):]
if end := strings.Index(cmd, "\nfunc "); end > 0 {
cmd = cmd[:end]
}
if strings.Contains(cmd, "procDestroyWindow.Call") {
t.Error("command() calls DestroyWindow directly; it must go through quit() " +
"so a handler that later moves into a goroutine does not break silently")
}
if !strings.Contains(src, "func (t *tray) quit()") {
t.Error("quit() is gone; the cross-thread-safe exit path is the invariant here")
}
}