Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,17 @@ because it turns other people's test suites red.

### Fixed

- **A window the graphics toolkit could not create is a refusal that says
why, not a process with no window in it.** On a Windows machine whose
graphics driver offers no OpenGL 2.1 - a virtual machine without 3D
acceleration, measured on Windows Server 2025 under VirtualBox - the
window binary opened nothing, said nothing, and stayed in the task list
until the session ended. It now says what did not happen, quotes the
driver's reason as the toolkit reported it, points at the command line
(which needs no graphics driver), and exits 1 - on standard error and in
a system dialog, because the window that would have carried the message
is what failed.

- **The switch between the three ways of stating a size freezes with the
rest of the form while a run is going.** It stayed live: during a run,
choosing another way rebuilt the size boxes under a form drawn as frozen.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ through OpenGL and reaches it through C, so that one needs a C compiler and is
built natively on each system. Built without one it still compiles, and says on
start that it has no window in it and that everything is on the command line.

The window needs a graphics driver that provides OpenGL 2.1. A Windows
virtual machine without 3D acceleration has none - there the window binary
says so in a dialog and on standard error, and exits 1. The command line
needs no graphics driver and does everything the window does.

## 🚀 Quick start

**1. Make a file.** One PNG, exactly two megabytes:
Expand Down
34 changes: 19 additions & 15 deletions internal/guard/guitext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,22 +82,26 @@ var notWords = map[string]string{
`"png"`: "a format id in a comment example rather than a label",
`"uxtheme.dll"`: "a Windows library this program loads, a file name and not a word",
`"kernel32.dll"`: "a Windows library this program loads, a file name and not a word",
`"user32.dll"`: "a Windows library this program loads for the dialog it shows when it has no window, a file name and not a word",
`"GetProcAddress"`: "the Win32 entry point looked up by name, an identifier and not a word",
`"windows"`: "a system name asked of the compiler",
`"linux"`: "a system name asked of the compiler",
`"darwin"`: "a system name asked of the compiler",
`"GOOS="`: "an environment variable handed to the compiler",
`"CGO_ENABLED=1"`: "an environment variable handed to the compiler",
`"true"`: "compared against a declared default, never shown",
`"\n"`: "what joins the lines of the status area, not a word",
`"\n\n"`: "the blank line between two refusals the form could not place, not a word",
`"Ag"`: "a sample measured to find how tall a row must be - an ascender and a descender, never drawn",
`"%s: %w"`: "how one error is wrapped around another, both already worded",
`"•"`: "the marker in front of a list item, a shape rather than a word",
`"panel"`: "our name for a colour, in the palette the toolkit asks by name",
`"lift"`: "our name for what the pointer does to the filled button, a colour the palette holds",
`"shade"`: "our name for what a press does to the filled button, a colour the palette holds",
`"fyneDo"`: "a migration flag the toolkit reads, never shown",
`"MessageBoxW"`: "the Win32 entry point that shows a system dialog, looked up by name, an identifier and not a word",
`"Cause:"`: "the word the toolkit's logger puts in front of the error it was handed, read as an anchor " +
"to find the toolkit's own sentence and never shown - see CauseFrom in opening.go",
`"windows"`: "a system name asked of the compiler",
`"linux"`: "a system name asked of the compiler",
`"darwin"`: "a system name asked of the compiler",
`"GOOS="`: "an environment variable handed to the compiler",
`"CGO_ENABLED=1"`: "an environment variable handed to the compiler",
`"true"`: "compared against a declared default, never shown",
`"\n"`: "what joins the lines of the status area, not a word",
`"\n\n"`: "the blank line between two refusals the form could not place, not a word",
`"Ag"`: "a sample measured to find how tall a row must be - an ascender and a descender, never drawn",
`"%s: %w"`: "how one error is wrapped around another, both already worded",
`"•"`: "the marker in front of a list item, a shape rather than a word",
`"panel"`: "our name for a colour, in the palette the toolkit asks by name",
`"lift"`: "our name for what the pointer does to the filled button, a colour the palette holds",
`"shade"`: "our name for what a press does to the filled button, a colour the palette holds",
`"fyneDo"`: "a migration flag the toolkit reads, never shown",
// The application's own name and id are its identity rather than prose.
// An application is not renamed in another language, and the desktop uses
// both to decide which running program this is. What a person READS in the
Expand Down
2 changes: 2 additions & 0 deletions internal/guard/hardening_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ func TestALibraryIsOnlyLoadedLazilyWhenTheSystemAlreadyHasIt(t *testing.T) {
// rather than remembered: the KnownDLLs registry key on 2026-09-06.
knownDLLs := map[string]string{
"kernel32.dll": "a KnownDLL, always already mapped - free space asks it for GetDiskFreeSpaceExW",
"user32.dll": "a KnownDLL, measured in the registry on 2026-09-16 among the same thirty seven - " +
"the window binary asks it for MessageBoxW when the toolkit could not open a window (O218)",
}

// Where a load may name something this cannot read, and why. A path worked
Expand Down
1 change: 1 addition & 0 deletions internal/guard/notelemetry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ var allowedURLs = map[string]string{
var allowedLibraries = map[string]string{
"kernel32.dll": "free disk space, and GetProcAddress for the call below",
"uxtheme.dll": "the dark window menu, called by ordinal - see darkmenus_windows.go",
"user32.dll": "the system dialog shown when the toolkit could not open a window - see dialog_windows.go",
}

// socketCalls are the low level calls that open or use a socket. syscall itself
Expand Down
278 changes: 278 additions & 0 deletions internal/guard/windowrefused_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package guard

import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strings"
"testing"

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver"
"fyne.io/fyne/v2/test"

"github.com/donislawdev/TestingFilesGenerator/internal/gui"
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
)

// A window the toolkit could not create is a refusal, not a process with no
// window in it.
//
// Measured on 2026-09-16 on a Windows Server 2025 guest without 3D
// acceleration (O218): the toolkit's window creation fails with "WGL: The
// driver does not appear to support OpenGL", it logs that to a standard
// error the windows-subsystem binary does not have, and Run then waits
// forever on a channel nobody closes. No window, no message, no exit code -
// untouchable rule 6 broken three times at once.
//
// The decision is taken from the window's STATE: the driver's NativeWindow
// answers with a zero handle on every platform when the window was never
// created. That is what this pins, with a stand-in window whose handle is
// whatever the case says - the test driver has no OpenGL and cannot be made
// to fail the real way, so the seam is the only place a guard can reach.
//
// The real thing is checked the way the rest of run_cgo.go is, by a run of
// the binary: docs/GUI-NO-OPENGL-2026-09-16.md.

// plainWindow is a window that records being shown and cannot say whether
// it has a native one - the shape of the test driver.
type plainWindow struct {
fyne.Window
calls []string
}

func (w *plainWindow) Show() { w.calls = append(w.calls, "show") }

// nativeWindow is a window whose driver answers NativeWindow with whatever
// context the case gave it.
type nativeWindow struct {
plainWindow
context any
}

func (w *nativeWindow) RunNative(f func(any)) { f(w.context) }

func TestAWindowTheToolkitGaveNoWindowIsRefusedAndNeverRun(t *testing.T) {
test.NewApp()
t.Cleanup(func() { test.NewApp() })

// Every platform context the driver hands out, empty and filled. The
// filled ones use 1 rather than a real handle: the question is zero or
// not, and a real handle is what the toolkit has when it has a window.
refused := map[string]any{
"windows": driver.WindowsWindowContext{},
"mac": driver.MacWindowContext{},
"x11": driver.X11WindowContext{},
"wayland": driver.WaylandWindowContext{},
}
opened := map[string]any{
"windows": driver.WindowsWindowContext{HWND: 1},
"mac": driver.MacWindowContext{NSWindow: 1},
"x11": driver.X11WindowContext{WindowHandle: 1},
"wayland": driver.WaylandWindowContext{WaylandSurface: 1},
}

for name, context := range refused {
w := &nativeWindow{plainWindow: plainWindow{Window: test.NewWindow(nil)}, context: context}
code := gui.OpenOrRefuse(w, func() { w.calls = append(w.calls, "run") }, func() { w.calls = append(w.calls, "refuse") })
if got := strings.Join(w.calls, ","); got != "show,refuse" {
t.Errorf("%s, no native window: the calls were %q and they have to be show,refuse.\n"+
"Show first, because only after it can the toolkit have failed - and then the "+
"refusal, and NOT the loop, which on such a machine never ends.", name, got)
}
if code != 1 {
t.Errorf("%s, no native window: exit code %d, and a window that could not open exits 1 "+
"like a build with no window in it.", name, code)
}
}

for name, context := range opened {
w := &nativeWindow{plainWindow: plainWindow{Window: test.NewWindow(nil)}, context: context}
code := gui.OpenOrRefuse(w, func() { w.calls = append(w.calls, "run") }, func() { w.calls = append(w.calls, "refuse") })
if got := strings.Join(w.calls, ","); got != "show,run" {
t.Errorf("%s, native window present: the calls were %q and they have to be show,run - "+
"the same two calls ShowAndRun makes, in the same order.", name, got)
}
if code != 0 {
t.Errorf("%s, native window present: exit code %d after a run that returned normally.", name, code)
}
}

// A driver that cannot answer at all, and one that answers with a
// context this code does not know: both are taken as a window that
// opened, because nothing about them is known to have failed. The test
// driver is the first kind, and every guard drawing a screen rests on it.
plain := &plainWindow{Window: test.NewWindow(nil)}
if code := gui.OpenOrRefuse(plain, func() { plain.calls = append(plain.calls, "run") }, func() { plain.calls = append(plain.calls, "refuse") }); code != 0 || strings.Join(plain.calls, ",") != "show,run" {
t.Errorf("a window whose driver has no NativeWindow: code %d, calls %q - it has to run, "+
"because the test driver is such a window and it draws every screen this package checks.",
code, strings.Join(plain.calls, ","))
}
unknown := &nativeWindow{plainWindow: plainWindow{Window: test.NewWindow(nil)}, context: struct{}{}}
if code := gui.OpenOrRefuse(unknown, func() { unknown.calls = append(unknown.calls, "run") }, func() { unknown.calls = append(unknown.calls, "refuse") }); code != 0 || strings.Join(unknown.calls, ",") != "show,run" {
t.Errorf("a window whose driver answers with an unknown context: code %d, calls %q - "+
"cannot tell is not the same as failed.", code, strings.Join(unknown.calls, ","))
}
}

// The three lines the toolkit wrote on the guest, verbatim from the
// measurement, with the module path the way a -trimpath build spells it.
const toolkitSaid = "2026/09/16 12:06:56 Fyne error: window creation error\n" +
"2026/09/16 12:06:56 Cause: APIUnavailable: WGL: The driver does not appear to support OpenGL\n" +
"2026/09/16 12:06:56 At: fyne.io/fyne/v2@v2.8.1/internal/driver/glfw/driver.go:180\n"

func TestTheToolkitsOwnReasonReachesTheSentenceAndItsAbsenceDoesNotBreakIt(t *testing.T) {
wantCause := "APIUnavailable: WGL: The driver does not appear to support OpenGL"
if got := gui.CauseFrom(toolkitSaid); got != wantCause {
t.Errorf("the cause read from what the toolkit logged is %q, wanted %q.\n"+
"That line is the one thing on the guest that names the driver, and it is the "+
"toolkit's sentence rather than ours, so it is quoted.", got, wantCause)
}
if got := gui.CauseFrom("2026/09/16 12:06:56 Fyne error: window creation error\n"); got != "" {
t.Errorf("a log with no cause line gave %q, and it has to give nothing - the refusal "+
"does not depend on the detail.", got)
}

// No catalogue loaded: every sentence answers with its English then, which
// is what a build whose catalogue failed to read answers too.
with := text.WindowRefused(wantCause)
without := text.WindowRefused("")
for name, sentence := range map[string]string{"with a cause": with, "without one": without} {
for _, must := range []string{"tfg --help", "OpenGL 2.1"} {
if !strings.Contains(sentence, must) {
t.Errorf("the refusal %s does not say %q: %q\n"+
"Four parts, D6 - what did not happen, why, what works instead, what to do about it.", name, must, sentence)
}
}
if strings.Contains(sentence, "{{") {
t.Errorf("the refusal %s carries an unfilled template: %q", name, sentence)
}
}
if !strings.Contains(with, wantCause) {
t.Errorf("the refusal with a cause does not quote it: %q", with)
}
if strings.Contains(without, "said:") {
t.Errorf("the refusal without a cause quotes an empty one: %q", without)
}
}

// addressedFirst is the name of the variable whose address is the first
// argument of call, as in io.MultiWriter(&said, ...), or nothing.
func addressedFirst(call *ast.CallExpr) string {
if len(call.Args) == 0 {
return ""
}
first, ok := call.Args[0].(*ast.UnaryExpr)
if !ok || first.Op != token.AND {
return ""
}
if buffer, ok := first.X.(*ast.Ident); ok {
return buffer.Name
}
return ""
}

// stringOf is the name of x in a call shaped f(x.String()), or nothing.
func stringOf(call *ast.CallExpr) string {
if len(call.Args) != 1 {
return ""
}
inner, ok := call.Args[0].(*ast.CallExpr)
if !ok {
return ""
}
sel, ok := inner.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "String" {
return ""
}
if buffer, ok := sel.X.(*ast.Ident); ok {
return buffer.Name
}
return ""
}

// The window binary goes through the seam above and never through
// ShowAndRun, and the copy of the toolkit's log is written before the
// toolkit's own stream is - a multi-writer stops at the first writer that
// fails, and the stream a windows-subsystem binary does not have is exactly
// such a writer. Read from the source, because run_cgo.go is behind cgo and
// the test driver cannot fail the way the guest did.
func TestTheWindowBinaryOpensThroughTheRefusalSeam(t *testing.T) {
root := repoRoot(t)
fset := token.NewFileSet()
seamCalls, showAndRun := 0, []string{}
// The buffer the cause is read from, and the buffer the copy of the log
// goes to. They have to be the same variable - named by the code, not
// by this guard, because "the first writer is an address" passed for
// the address of any buffer at all (an outside review of the pull
// request named it), and a copy into some other buffer leaves the one
// CauseFrom reads empty.
readFrom, copiedTo := "", ""

err := filepath.WalkDir(filepath.Join(root, "internal", "gui"), func(path string, d fs.DirEntry, walkErr error) error {
if walkErr != nil || d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return walkErr
}
file, perr := parser.ParseFile(fset, path, nil, 0)
if perr != nil {
return perr
}
rel := filepath.ToSlash(strings.TrimPrefix(path, root+string(filepath.Separator)))
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
switch fun := call.Fun.(type) {
case *ast.SelectorExpr:
switch fun.Sel.Name {
case "ShowAndRun":
showAndRun = append(showAndRun, rel)
case "MultiWriter":
if pkg, ok := fun.X.(*ast.Ident); ok && pkg.Name == "io" && strings.HasSuffix(rel, "run_cgo.go") {
copiedTo = addressedFirst(call)
}
}
case *ast.Ident:
if !strings.HasSuffix(rel, "run_cgo.go") {
return true
}
switch fun.Name {
case "OpenOrRefuse":
seamCalls++
case "CauseFrom":
readFrom = stringOf(call)
}
}
return true
})
return nil
})
if err != nil {
t.Fatalf("walking internal/gui: %v", err)
}

if len(showAndRun) != 0 {
t.Errorf("ShowAndRun is called in %v.\n"+
"It is Show then Run with no question between them, and on a machine where the "+
"toolkit cannot create a window it runs a loop nothing ends (O218). Open through "+
"gui.OpenOrRefuse instead.", showAndRun)
}
if seamCalls != 1 {
t.Errorf("run_cgo.go calls OpenOrRefuse %d time(s), and the window binary opens through "+
"it exactly once - that is the seam the guard above reaches.", seamCalls)
}
if readFrom == "" {
t.Fatalf("run_cgo.go does not read the cause with CauseFrom(<buffer>.String()), so this " +
"guard cannot tell which buffer the copy of the toolkit's log has to reach.")
}
if copiedTo != readFrom {
t.Errorf("run_cgo.go hands io.MultiWriter %q first, and CauseFrom reads %q.\n"+
"io.MultiWriter stops at the first writer that fails. The toolkit's own stream is "+
"standard error, which a windows-subsystem binary does not have, so the copy has to "+
"come first - and it has to be the buffer the cause is read from, or that buffer stays "+
"empty and the refusal says nothing about the driver.", copiedTo, readFrom)
}
}
10 changes: 10 additions & 0 deletions internal/gui/dialog_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//go:build !windows

package gui

// sayInADialog has nothing to add outside Windows: a binary there keeps its
// standard error, so the sentence that the window could not open reaches
// the terminal it was started from. A person who started it from a file
// manager sees nothing, and that is written down as not done rather than
// papered over - the system dialog that would answer it differs by desktop.
func sayInADialog(string, string) {}
Loading
Loading