Skip to content

Commit b0e8873

Browse files
donislawdevclaude
andauthored
guard: a window binary with no window in it says so, fails, and stays off stdout (#53)
* ci: binaries on demand, from whatever branch you pick Somebody reports a bug, the fix lands on a branch, and they want to try it before there is a release. Clicking Run workflow builds that branch and leaves the binaries on the run page for fourteen days. Three owner decisions shape it. A choice input, defaulting to the command line binaries only: those cross compile to five platforms on one runner in about two minutes, while the window needs three real runners and CGO. No test gate, because the whole point is speed and the branch has its own CI on its own pull request. And fourteen days rather than the default ninety, because an unsigned binary should not sit for a quarter of a year behind a link somebody can pass on as if it were official. It is deliberately not shaped like a release, so an archive from here cannot be mistaken for one. The name carries the COMMIT rather than the version, and that is not a style choice: internal/version is a const and cannot be stamped at link time, so a build from a fix branch reports whatever version that branch inherited. The file name is the only place that can tell the truth about which code this is. Every archive also carries UNOFFICIAL-BUILD.txt saying the same in words, for whoever unpacks it a month later with no memory of where it came from - not signed, no attestation, and what the version string inside does and does not mean. The note is a script rather than a heredoc because two jobs on four runners write it, and a note that says one thing in one archive and something else in the other is worse than no note. Three guards, five mutations, all caught. The platform list is the one fact this shares with the release, so it is read out of both workflows and compared. A workflow that quietly built four of five platforms would leave somebody's machine unserved, and nothing would say so, because a missing platform looks like a build that did not run. The other two are about the failure mode that matters here, which is a person trusting a file they should not: nothing in this workflow may be granted write, no step may mention a way to publish, and both packaging jobs must write the note - counted rather than found, because one job losing its call would leave the other one proving nothing about it. Measured rather than assumed: the command line loop was run locally against this tree and packaged all five targets in 26 seconds, and the archive holds the binary, the licence, the notices, the readme and the note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * guard: a window binary with no window in it says so, fails, and stays off stdout internal/gui/gui.go was the only production file in the tree that no guard executed - measured with a coverage map, after subtracting the blocks init() lights up on its own. The gap was not in gui.go: run_nocgo.go carries a four part D6 message and an exit code, and nothing checked that a binary ever produced either. The catalogue guard knows the sentence exists as a text entry, which is a different fact. It cannot be an ordinary test. The guard binary is compiled with cgo, so run resolves to run_cgo.go and calling gui.Run here would try to open a real window. So it builds the binary the way somebody without a C compiler builds it, with CGO_ENABLED=0, and runs it. Measured at 1.7 s, because disabling cgo excludes the toolkit rather than compiling it. Three contracts, and the third had no guard anywhere: the message reaches standard error out of internal/gui/text, the exit code is 1, and standard output stays empty. That last one is on the regression surface for the command line and the window binary is its other half. Two mutations, both caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent cc08ed1 commit b0e8873

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

‎internal/guard/nowindow_test.go‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package guard
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
"testing"
12+
13+
"github.com/donislawdev/TestingFilesGenerator/internal/gui/text"
14+
)
15+
16+
// A window binary with no window in it says so, fails, and leaves standard
17+
// output empty.
18+
//
19+
// Why this exists, and how it was found. internal/gui/gui.go was the only
20+
// production file in the tree that no guard executed at all - measured on
21+
// 2026-09-05 with tools/impact.py, which after subtracting the blocks that
22+
// init() lights up on its own reported exactly one such file out of 166.
23+
// Following it found the real gap next door rather than in gui.go itself:
24+
// internal/gui/run_nocgo.go carries a four part D6 message and an exit code,
25+
// and nothing checked that a binary ever produced either. The catalogue guard
26+
// knows the sentence exists as a text entry. That is a different fact.
27+
//
28+
// Why it has to build a binary rather than call gui.Run. The guard binary is
29+
// compiled WITH cgo, so run resolves to run_cgo.go, and calling gui.Run from
30+
// here would try to open a real window on somebody's desktop. The only way to
31+
// reach the other half of that build tag is to build it the way the person who
32+
// meets this message builds it, which is with CGO_ENABLED=0.
33+
//
34+
// Measured 2026-09-05: that build costs 1.7 s, because disabling cgo excludes
35+
// the whole graphics toolkit instead of compiling it.
36+
//
37+
// Three contracts, and the third is the one nothing anywhere covered:
38+
//
39+
// - the message reaches standard error, and it is the one from
40+
// internal/gui/text rather than a sentence written twice,
41+
// - the exit code is 1, because this is a run that did not do what it was
42+
// asked to do,
43+
// - standard output stays EMPTY. This project keeps "a failed run writes
44+
// nothing to standard output" on its regression surface for the command
45+
// line. The window binary is the other half of that rule and had no guard,
46+
// which matters because a script reading a report from stdout cannot tell
47+
// an empty answer from a broken one.
48+
func TestABuildWithNoWindowInItSaysSoAndKeepsStandardOutputEmpty(t *testing.T) {
49+
// A build that runs and fails is fatal below. Only the absence of the
50+
// toolchain is a skip, because a guard that cannot be run is not a guard
51+
// that passed - and tools/linux-check.py runs this suite in a container
52+
// with no Go in it by design.
53+
if _, err := exec.LookPath("go"); err != nil {
54+
t.Skipf("no Go toolchain here, so no binary can be built to run: %v", err)
55+
}
56+
57+
name := "tfg-gui-nocgo"
58+
if runtime.GOOS == "windows" {
59+
name += ".exe"
60+
}
61+
built := filepath.Join(t.TempDir(), name)
62+
63+
// The tag comes from the file, never from memory - that is what
64+
// TestEverythingThatCompilesUsReadsTheBuildTagsFromOnePlace is about.
65+
build := exec.Command("go", "build", "-tags", buildTags(), "-o", built, "./cmd/tfg-gui")
66+
build.Dir = filepath.Join("..", "..")
67+
build.Env = append(os.Environ(), "CGO_ENABLED=0")
68+
if out, err := build.CombinedOutput(); err != nil {
69+
t.Fatalf("building the window binary with cgo disabled: %v\n%s\n"+
70+
"That build is the one somebody gets without a C compiler, so it has to "+
71+
"compile even though it cannot draw anything.", err, out)
72+
}
73+
74+
// No linker flags on purpose. The shipped window binary is built for the
75+
// windows subsystem so it opens without a console, and that is guarded
76+
// elsewhere. Here the question is what the code SAYS, so it is built plain
77+
// and its streams can be read.
78+
run := exec.Command(built)
79+
var stdout, stderr bytes.Buffer
80+
run.Stdout = &stdout
81+
run.Stderr = &stderr
82+
err := run.Run()
83+
84+
code := 0
85+
var exit *exec.ExitError
86+
if errors.As(err, &exit) {
87+
code = exit.ExitCode()
88+
} else if err != nil {
89+
t.Fatalf("running the window binary built without cgo: %v", err)
90+
}
91+
92+
if code != 1 {
93+
t.Errorf("a window binary with no window in it exited %d and it has to exit 1.\n"+
94+
"Why it matters: this is the only thing somebody building from source without "+
95+
"a C compiler ever sees, and a zero here tells their script the window opened.\n"+
96+
"Where it lives: internal/gui/run_nocgo.go.", code)
97+
}
98+
99+
if stdout.Len() != 0 {
100+
t.Errorf("a failed run wrote %d byte(s) to standard output: %q\n"+
101+
"Why it matters: standard output is where a machine reads a report from. "+
102+
"A run that did nothing has to leave it empty, so silence and a report "+
103+
"cannot be confused. The command line keeps this rule and the window "+
104+
"binary is the other half of it.", stdout.Len(), stdout.String())
105+
}
106+
107+
if !strings.Contains(stderr.String(), text.NoWindowInThisBuild) {
108+
t.Errorf("the binary did not say what a build with no window says.\n"+
109+
"wanted, from internal/gui/text: %q\n"+
110+
"got on standard error: %q\n"+
111+
"Why it matters: without this sentence the binary exits 1 with nothing to "+
112+
"read, and the reason - no C support, so no OpenGL - is exactly what the "+
113+
"person cannot guess.", text.NoWindowInThisBuild, stderr.String())
114+
}
115+
}

0 commit comments

Comments
 (0)