225 lines
8.0 KiB
Go
225 lines
8.0 KiB
Go
package main
|
|
|
|
// The tray's testable half. The systray plumbing cannot run here — there is no
|
|
// notification area on a Linux CI box — so what is checked is everything that
|
|
// does not need one: the wording, the build tag that keeps the darwin release
|
|
// alive, and the icons themselves.
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTrayStateLine(t *testing.T) {
|
|
up := func(n int) []uiPairing {
|
|
var p []uiPairing
|
|
for i := 0; i < n; i++ {
|
|
p = append(p, uiPairing{Listening: true})
|
|
}
|
|
return p
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
in uiStatus
|
|
want string
|
|
}{
|
|
{"nothing paired", uiStatus{}, "No host paired yet"},
|
|
{"all served", uiStatus{Pairings: up(3)}, "Serving 3 host(s)"},
|
|
{"some served", uiStatus{Pairings: append(up(1), uiPairing{})}, "Serving 1 of 2 hosts"},
|
|
{"none served", uiStatus{Pairings: []uiPairing{{}, {}}}, "Serving 0 of 2 hosts"},
|
|
// The whole point of this case: an unanchored networked box serves
|
|
// NOTHING (anchor.go), so a host count there would be true about
|
|
// pairings and a lie about the machine.
|
|
{"networked, unanchored", uiStatus{Pairings: up(3), AnchorRequired: true},
|
|
"Not anchored — answering nothing"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := trayStateLine(c.in); got != c.want {
|
|
t.Errorf("%s: got %q, want %q", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestShutdownRequestIsIdempotent(t *testing.T) {
|
|
// A menu item can be clicked twice before the daemon winds down, and closing
|
|
// a closed channel panics. sync.Once is the whole fix; this is the proof.
|
|
requestShutdown()
|
|
requestShutdown()
|
|
select {
|
|
case <-shutdownCh:
|
|
default:
|
|
t.Fatal("shutdownCh was not closed — the tray's Stop would do nothing")
|
|
}
|
|
}
|
|
|
|
// THE ONE THAT MATTERS MOST. fyne.io/systray needs cgo on darwin (measured:
|
|
// `undefined: nativeLoop`), and deploy/publish.sh builds all six
|
|
// targets in ONE alpine container at CGO_ENABLED=0. So an import that escapes
|
|
// the Windows build tag does not fail here, or in `go build`, or in `go vet` —
|
|
// it fails inside the publish container, for two of six targets, at release
|
|
// time. This test is where that gets caught instead.
|
|
func TestTheSystrayImportStaysBehindTheWindowsTag(t *testing.T) {
|
|
entries, err := os.ReadDir(".")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
darwin := build.Default
|
|
darwin.GOOS = "darwin"
|
|
|
|
found := false
|
|
for _, e := range entries {
|
|
name := e.Name()
|
|
if e.IsDir() || !strings.HasSuffix(name, ".go") {
|
|
continue
|
|
}
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, name, nil, parser.ImportsOnly)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
imports := false
|
|
for _, im := range f.Imports {
|
|
p, _ := strconv.Unquote(im.Path.Value)
|
|
if p == "fyne.io/systray" || strings.HasPrefix(p, "fyne.io/systray/") {
|
|
imports = true
|
|
}
|
|
}
|
|
if !imports {
|
|
continue
|
|
}
|
|
found = true
|
|
onDarwin, err := darwin.MatchFile(".", name)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
if onDarwin {
|
|
t.Errorf("%s imports fyne.io/systray and IS compiled into a darwin build. "+
|
|
"That library needs cgo on darwin, and deploy/publish.sh cross-builds "+
|
|
"all six targets in one container with CGO_ENABLED=0 — so this breaks "+
|
|
"darwin/amd64 and darwin/arm64 at PUBLISH time, not here. Put the "+
|
|
"import behind `//go:build windows`.", name)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("no file imports fyne.io/systray — either the tray was removed (delete " +
|
|
"this test with it) or it moved somewhere this check no longer looks, " +
|
|
"which would make the check a green light that proves nothing")
|
|
}
|
|
}
|
|
|
|
// A tray icon that fails to decode leaves a blank space in the notification
|
|
// area, which looks exactly like the bastion not running — the one thing this
|
|
// whole feature exists to make unambiguous.
|
|
func TestTheTrayIconsAreRealIcons(t *testing.T) {
|
|
for _, state := range []string{"ok", "warn", "bad"} {
|
|
path := "uiassets/tray-" + state + ".ico"
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Errorf("%s: %v", path, err)
|
|
continue
|
|
}
|
|
if len(b) < 22 {
|
|
t.Errorf("%s is %d bytes — not an icon", path, len(b))
|
|
continue
|
|
}
|
|
// ICONDIR: reserved=0, type=1 (icon), count
|
|
if binary.LittleEndian.Uint16(b[0:2]) != 0 || binary.LittleEndian.Uint16(b[2:4]) != 1 {
|
|
t.Errorf("%s does not start with an ICONDIR header", path)
|
|
continue
|
|
}
|
|
count := int(binary.LittleEndian.Uint16(b[4:6]))
|
|
if count == 0 {
|
|
t.Errorf("%s declares no images", path)
|
|
continue
|
|
}
|
|
has16 := false
|
|
for i := 0; i < count; i++ {
|
|
off := 6 + i*16
|
|
if off+16 > len(b) {
|
|
t.Errorf("%s: directory entry %d runs past the file", path, i)
|
|
break
|
|
}
|
|
if b[off] == 16 { // width; 0 would mean 256
|
|
has16 = true
|
|
}
|
|
}
|
|
// 16px is the size Windows actually draws in the notification area at
|
|
// 100% scaling. An icon file without one gets downscaled from whatever
|
|
// is nearest and turns to mush.
|
|
if !has16 {
|
|
t.Errorf("%s carries no 16x16 image — that is the size the tray draws", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// THE CONTEXT SURFACE HAS TO BE REACHABLE WITHOUT A TERMINAL, AND IT IS CALLED
|
|
// CONTEXT.
|
|
//
|
|
// A person looking at the tray menu expects to find the way in there, and the
|
|
// entry is named for what it is: we are not serving files, we are serving
|
|
// context built from files, and the App decides what becomes context on the
|
|
// network. Files are what you look at; context is what you decide they
|
|
// become, and the deciding is what this surface is for. The label is asserted
|
|
// because it is the product's noun, not a caption.
|
|
//
|
|
// Source-read for the reason console_test.go gives: the platform that runs the
|
|
// suite is Linux, and tray_windows.go is the file that carries the menu.
|
|
func TestTheTrayOffersTheContextSurface(t *testing.T) {
|
|
src := readSourceFile(t, "tray_windows.go")
|
|
if !strings.Contains(src, "AddMenuItem") {
|
|
t.Fatal("the tray builds no menu at all any more — this check would pass for ever")
|
|
}
|
|
if !strings.Contains(src, "Your context") {
|
|
t.Error("the tray menu has no entry for the context surface. It is the only way " +
|
|
"in without typing a command, and typing a command is what the tray exists " +
|
|
"to replace.")
|
|
}
|
|
if strings.Contains(src, "Your files") {
|
|
t.Error("the tray menu calls it \"Your files\". We do not serve files, we serve " +
|
|
"the context built from them — the surface exists so a person can choose " +
|
|
"which files become context on their network.")
|
|
}
|
|
if !strings.Contains(src, "openContextSurface") {
|
|
t.Error("nothing in the tray calls openContextSurface, so the menu entry — if there " +
|
|
"is one — does nothing when clicked")
|
|
}
|
|
}
|
|
|
|
// And it starts a SEPARATE process rather than serving from the daemon. On a
|
|
// one-person Windows desktop both would work; on a networked box the daemon is
|
|
// root, and serving files from it would render somebody's files under an
|
|
// authority they never granted.
|
|
func TestTheContextSurfaceIsStartedAsItsOwnProcess(t *testing.T) {
|
|
src := readSourceFile(t, "tray.go")
|
|
i := strings.Index(src, "func openContextSurface")
|
|
if i < 0 {
|
|
t.Fatal("openContextSurface is gone — this check would pass for ever")
|
|
}
|
|
body := src[i:]
|
|
// j indexes into body[1:], so the slice bound is j+1.
|
|
// Harmless as written — it only trimmed one byte early — but a slice bound
|
|
// that is quietly wrong is the kind of thing the next person copies.
|
|
if j := strings.Index(body[1:], "\nfunc "); j > 0 {
|
|
body = body[:j+1]
|
|
}
|
|
if !strings.Contains(body, `exec.Command(exe, "context")`) {
|
|
t.Error("the tray does not launch the `context` verb as a child process. The " +
|
|
"security argument for that surface is that the walk is done BY the user, " +
|
|
"so it cannot be served from a daemon that may be root.")
|
|
}
|
|
if !strings.Contains(body, "hideConsole") {
|
|
t.Error("the child is started without hideConsole, so clicking a tray item " +
|
|
"flashes a console window — the defect the tray was built to end")
|
|
}
|
|
if strings.Contains(body, "cmd.Wait()") && !strings.Contains(body, "go func()") {
|
|
t.Error("the tray waits on the child, which freezes the menu for as long as " +
|
|
"the person has their files open")
|
|
}
|
|
}
|