diff --git a/.github/tfg-in-work.gif b/.github/tfg-in-work.gif
index 94d2f7e3..4f2e4e73 100644
Binary files a/.github/tfg-in-work.gif and b/.github/tfg-in-work.gif differ
diff --git a/.github/window-formats.png b/.github/window-formats.png
new file mode 100644
index 00000000..36b78eba
Binary files /dev/null and b/.github/window-formats.png differ
diff --git a/.github/window-presets.png b/.github/window-presets.png
new file mode 100644
index 00000000..56f70216
Binary files /dev/null and b/.github/window-presets.png differ
diff --git a/.github/window-settings.png b/.github/window-settings.png
new file mode 100644
index 00000000..f6005277
Binary files /dev/null and b/.github/window-settings.png differ
diff --git a/.github/window-several-batches.png b/.github/window-several-batches.png
new file mode 100644
index 00000000..937a18a8
Binary files /dev/null and b/.github/window-several-batches.png differ
diff --git a/.github/window.png b/.github/window.png
index 6a7fa026..f8006334 100644
Binary files a/.github/window.png and b/.github/window.png differ
diff --git a/README.md b/README.md
index 1369f961..9228e86c 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@ needs it finds out it exists.
- **Cost nothing and stay out of your way** - GPL-3.0, and the files you
generate are yours with no strings attached.
-
+

@@ -56,17 +56,17 @@ This README is also the manual. The short version is above the line, the full
reference is below it.
- [What it can do](#-what-it-can-do)
+- [Install](#-install)
+- [Quick start](#-quick-start)
- [Formats it generates](#-formats-it-generates)
+- [Presets](#-presets)
- [The problem it solves](#-the-problem-it-solves)
- [What makes it different](#-what-makes-it-different)
-- [Install](#-install)
-- [Quick start](#-quick-start)
- [Reference](#reference)
- [Commands](#️-commands)
- [Recipes](#-recipes)
- [Formats in detail](#-formats-in-detail)
- [The manifest](#-the-manifest)
- - [Presets](#-presets)
- [The desktop window](#️-the-desktop-window)
- [Using it in CI](#️-using-it-in-ci)
- [Questions](#-questions)
@@ -74,6 +74,86 @@ reference is below it.
- [Everything inside a generated file is made up](#-everything-inside-a-generated-file-is-made-up)
- [Licence](#-licence)
+## 📦 Install
+
+**Download a binary.** Take the archive for your system from the
+[releases page](https://github.com/donislawdev/TestingFilesGenerator/releases),
+unpack it and run it. `tfg` is the command line, `tfg-gui` is the desktop
+window. The Windows and macOS downloads are signed, so they start without a
+warning about an unknown developer. The Linux ones are not, because desktop
+Linux has no equivalent to sign them with.
+
+**With Go installed:**
+
+```
+go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest
+```
+
+**From source.** Needs Go 1.27.0 or newer, and nothing else:
+
+```
+git clone https://github.com/donislawdev/TestingFilesGenerator
+cd TestingFilesGenerator
+go build -tags "$(cat .github/build-tags)" ./cmd/tfg
+```
+
+**The tag is not optional.** The AVIF encoder has an assembly path that reads
+past the end of a buffer and takes the process down on some picture sizes, and
+the tag turns it off. Building without it does not compile, and says so. The
+files it produces are the same either way.
+
+The desktop window is a second binary,
+`go build -tags "$(cat .github/build-tags)" ./cmd/tfg-gui`. It draws
+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 OpenGL 2.1 to draw. On Windows the archive carries a
+software renderer for machines whose graphics driver offers none - Mesa
+llvmpipe, in an `opengl` folder next to `tfg-gui.exe` - and the window uses it
+by itself when the driver refuses: a virtual machine without 3D acceleration,
+a remote desktop, a server. Drawn that way it is slower and says so on its
+About screen. On a machine with a driver the folder is not touched unless
+you ask: `tfg-gui --software-gl` loads the renderer on any Windows machine,
+which is the way to see the window as a machine without a driver sees it. Keep
+the folder next to the program: without it, and without a driver, the window
+says what it looked for in a dialog and on standard error, and exits 1. Linux
+has Mesa in the system and macOS has never lacked what the toolkit needs, so
+nothing of the kind ships there, and there the flag says so and changes
+nothing. The command line needs no graphics driver and does everything the
+window does.
+
+## 🚀 Quick start
+
+**1. Make a file.** One PNG, exactly two megabytes:
+
+```
+tfg generate --format png --size 2mb --out ./out
+```
+
+**2. Make a lot of files.** Ten thousand log files, each between one and eight
+kilobytes, with the sizes drawn from the seed so tomorrow gives the same set.
+**Give each run its own directory** - the manifest is the only record of what a
+run wrote, so the tool refuses to write a second one over it:
+
+```
+tfg generate --format log --size-range 1kb-8kb --count 10000 --out ./logs
+```
+
+**3. Check them, then remove them.**
+
+```
+tfg verify ./logs/manifest.json
+tfg cleanup ./logs/manifest.json --yes
+```
+
+```
+logs matches ./logs/manifest.json: 10000 files checked
+```
+
+**Sizes count in 1024s**, the way your file manager does, so `2mb` means
+2097152 bytes. A plain byte count works too: `--size 2097152`.
+
## 📁 Formats it generates
Twenty six, and every one is a **real file of that format** - it opens in the
@@ -94,6 +174,32 @@ Most of them take settings of their own - image dimensions, JPEG quality, PDF
page count, rows and columns in a spreadsheet, what goes inside an archive. See
[format settings](#per-format-settings).
+## 🧪 Presets
+
+A preset is a ready-made set of files that answers one common testing question,
+so you do not have to design the set yourself. Every file in it says what it is
+for, and the manifest says how your system should react to it:
+
+| preset | the question it answers |
+|---|---|
+| `empty-and-minimal` | Does a file that is valid and as small as the format allows get through? |
+| `filename-handling` | Will my system store, show and give back a file name it did not expect? |
+| `size-boundaries` | Is a size limit enforced exactly where it is declared? |
+| `tabular-import` | Does my table import survive what real tools export? |
+| `text-encoding` | Does my reader know which encoding a file is in, or is it guessing? |
+| `upload-validation` | Does my upload form take what it should and turn the rest away? |
+
+```
+tfg preset show size-boundaries
+tfg generate --preset size-boundaries --limit 10mb --out ./limits
+```
+
+`show` tells you what the set would cost before you build it, and says outright
+when a number is a placeholder of ours rather than a limit of yours. Presets are
+ordinary recipes underneath - `tfg preset eject size-boundaries` prints the
+recipe and you edit it from there. The Presets screen of the window offers the
+same ones.
+
## 🤔 The problem it solves
You are testing software that accepts files from people. Sooner or later you
@@ -162,86 +268,6 @@ And where the right answer genuinely depends on your own policy, the manifest
says `unspecified` instead of inventing one. A generator that guesses produces
false failures, and a suite that cries wolf gets switched off.
-## 📦 Install
-
-**Download a binary.** Take the archive for your system from the
-[releases page](https://github.com/donislawdev/TestingFilesGenerator/releases),
-unpack it and run it. `tfg` is the command line, `tfg-gui` is the desktop
-window. The Windows and macOS downloads are signed, so they start without a
-warning about an unknown developer. The Linux ones are not, because desktop
-Linux has no equivalent to sign them with.
-
-**With Go installed:**
-
-```
-go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest
-```
-
-**From source.** Needs Go 1.27.0 or newer, and nothing else:
-
-```
-git clone https://github.com/donislawdev/TestingFilesGenerator
-cd TestingFilesGenerator
-go build -tags "$(cat .github/build-tags)" ./cmd/tfg
-```
-
-**The tag is not optional.** The AVIF encoder has an assembly path that reads
-past the end of a buffer and takes the process down on some picture sizes, and
-the tag turns it off. Building without it does not compile, and says so. The
-files it produces are the same either way.
-
-The desktop window is a second binary,
-`go build -tags "$(cat .github/build-tags)" ./cmd/tfg-gui`. It draws
-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 OpenGL 2.1 to draw. On Windows the archive carries a
-software renderer for machines whose graphics driver offers none - Mesa
-llvmpipe, in an `opengl` folder next to `tfg-gui.exe` - and the window uses it
-by itself when the driver refuses: a virtual machine without 3D acceleration,
-a remote desktop, a server. Drawn that way it is slower and says so on its
-About screen. On a machine with a driver the folder is not touched unless
-you ask: `tfg-gui --software-gl` loads the renderer on any Windows machine,
-which is the way to see the window as a machine without a driver sees it. Keep
-the folder next to the program: without it, and without a driver, the window
-says what it looked for in a dialog and on standard error, and exits 1. Linux
-has Mesa in the system and macOS has never lacked what the toolkit needs, so
-nothing of the kind ships there, and there the flag says so and changes
-nothing. The command line needs no graphics driver and does everything the
-window does.
-
-## 🚀 Quick start
-
-**1. Make a file.** One PNG, exactly two megabytes:
-
-```
-tfg generate --format png --size 2mb --out ./out
-```
-
-**2. Make a lot of files.** Ten thousand log files, each between one and eight
-kilobytes, with the sizes drawn from the seed so tomorrow gives the same set.
-**Give each run its own directory** - the manifest is the only record of what a
-run wrote, so the tool refuses to write a second one over it:
-
-```
-tfg generate --format log --size-range 1kb-8kb --count 10000 --out ./logs
-```
-
-**3. Check them, then remove them.**
-
-```
-tfg verify ./logs/manifest.json
-tfg cleanup ./logs/manifest.json --yes
-```
-
-```
-logs matches ./logs/manifest.json: 10000 files checked
-```
-
-**Sizes count in 1024s**, the way your file manager does, so `2mb` means
-2097152 bytes. A plain byte count works too: `--size 2097152`.
-
---
# Reference
@@ -633,25 +659,6 @@ produced the file, and `summary.by_target` counts the files each target came to.
A recipe with several targets can therefore be checked target by target without
reading file names.
-## 🧪 Presets
-
-A preset is a ready made set of files that answers a common testing question, so
-you do not have to design the set yourself:
-
-```
-tfg preset list
-tfg preset show size-boundaries
-tfg generate --preset size-boundaries --limit 10mb --out ./limits
-```
-
-`show` tells you what the set would cost before you build it, and says outright
-when a number is a placeholder of ours rather than a limit of yours. Presets are
-ordinary recipes underneath - `tfg preset eject size-boundaries` prints the
-recipe and you edit it from there.
-
-`tfg preset list` names every preset your build ships, and the Presets
-screen of the window offers the same ones.
-
## 🖥️ The desktop window
The same engine with a window on it, for the testing that is not scripted. It is
@@ -663,6 +670,15 @@ Four screens - one batch, presets, several batches at once, and about. It shows
what a run would cost before writing anything, reports progress while it runs,
and can be cancelled part way without leaving a half written file behind.
+
+
+
+
+
+
+
+
+
It does not open a recipe file yet. Recipes are a command line thing for now,
and the window builds its batches in the form.
diff --git a/internal/guard/edgeexample_test.go b/internal/guard/edgeexample_test.go
new file mode 100644
index 00000000..3e1e223e
--- /dev/null
+++ b/internal/guard/edgeexample_test.go
@@ -0,0 +1,130 @@
+package guard
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/cli"
+)
+
+// The three files every first page shows - one byte under a 1 MB limit, the
+// limit, one byte over - are what size-boundaries actually writes.
+//
+// They stand in four places a stranger reads before anything else: the social
+// card, the README, and the site's first page in both languages. All four
+// carry them as literals, and the card says outright that everything on it is
+// real. Nothing asked the program. An outside review of #148 named it, and
+// asking showed it was already half untrue: the card before that one printed
+// "tfg generate --preset size-boundaries --limit 1mb", which the program
+// refuses, because the default spread would need a file of 0 B.
+//
+// So this runs the command the README and the site print, as a dry run, and
+// holds each place to the answer: every file on the line of its name, with its
+// byte count and the outcome the manifest declares for it.
+func TestTheLimitExampleIsWhatThePresetWrites(t *testing.T) {
+ const command = "tfg generate --preset size-boundaries --limit 1mb --spread 1B --format pdf --out ./edges"
+ args := append(strings.Fields(strings.TrimPrefix(command, "tfg ")), "--dry-run", "--json")
+ args[len(args)-3] = filepath.Join(t.TempDir(), "edges")
+ var out, errOut bytes.Buffer
+ if code := cli.Run(context.Background(), args, &out, &errOut); code != cli.ExitOK {
+ t.Fatalf("the command the pages print ended with %d:\n%s", code, errOut.String())
+ }
+ var manifest struct {
+ Files []struct {
+ Name string `json:"name"`
+ Bytes int64 `json:"bytes"`
+ Expected struct {
+ Outcome string `json:"outcome"`
+ } `json:"expected"`
+ } `json:"files"`
+ }
+ if err := json.Unmarshal(out.Bytes(), &manifest); err != nil {
+ t.Fatalf("reading the dry run's manifest: %v", err)
+ }
+ if len(manifest.Files) != 3 {
+ t.Fatalf("the command writes %d files, and every page shows three", len(manifest.Files))
+ }
+
+ root := repoRoot(t)
+ places := []struct {
+ file string
+ spaced bool
+ outcomes map[string]string
+ command bool
+ }{
+ {"web/templates/social.html", true, map[string]string{"accept": "accept", "reject": "reject"}, false},
+ {"README.md", false, map[string]string{"accept": "**accept**", "reject": "**reject**"}, true},
+ {"web/content/en/index.html", false, map[string]string{"accept": "accept", "reject": "reject"}, true},
+ {"web/content/pl/index.html", false, map[string]string{"accept": "przyjąć", "reject": "odrzucić"}, true},
+ }
+ for _, place := range places {
+ body, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(place.file)))
+ if err != nil {
+ t.Fatalf("reading %s: %v", place.file, err)
+ }
+ text := string(body)
+ if place.command && !strings.Contains(text, command) {
+ t.Errorf("%s no longer prints %q, so the files it shows come from a command this guard does not run",
+ place.file, command)
+ }
+ for _, f := range manifest.Files {
+ line := lineNaming(text, f.Name)
+ if line == "" {
+ t.Errorf("%s does not show %s, and the command writes it", place.file, f.Name)
+ continue
+ }
+ size := strconv.FormatInt(f.Bytes, 10)
+ if place.spaced {
+ size = spacedBytes(f.Bytes) + " B"
+ }
+ if !strings.Contains(line, size) {
+ t.Errorf("%s shows %s without its %s:\n%s", place.file, f.Name, size, line)
+ }
+ word, ok := place.outcomes[f.Expected.Outcome]
+ if !ok {
+ t.Errorf("the manifest declares %q for %s, and %s has no word for it", f.Expected.Outcome, f.Name, place.file)
+ continue
+ }
+ if !strings.Contains(line, word) {
+ t.Errorf("%s shows %s without the outcome the manifest declares (%s):\n%s",
+ place.file, f.Name, word, line)
+ }
+ }
+ }
+}
+
+// lineNaming is the one line of a page that shows a file, or nothing when no
+// line or more than one does - a name twice on a page is two claims, and this
+// checks one.
+func lineNaming(text, name string) string {
+ var found []string
+ for _, line := range strings.Split(text, "\n") {
+ if strings.Contains(line, name) {
+ found = append(found, line)
+ }
+ }
+ if len(found) != 1 {
+ return ""
+ }
+ return found[0]
+}
+
+// spacedBytes writes a byte count the way the social card does, in groups of
+// three.
+func spacedBytes(n int64) string {
+ s := strconv.FormatInt(n, 10)
+ var b strings.Builder
+ for i, r := range s {
+ if i > 0 && (len(s)-i)%3 == 0 {
+ b.WriteByte(' ')
+ }
+ b.WriteRune(r)
+ }
+ return b.String()
+}
diff --git a/internal/guard/readmepresets_test.go b/internal/guard/readmepresets_test.go
new file mode 100644
index 00000000..d6280fd5
--- /dev/null
+++ b/internal/guard/readmepresets_test.go
@@ -0,0 +1,86 @@
+package guard
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/donislawdev/TestingFilesGenerator/internal/preset"
+)
+
+// The README's table of presets names every preset the program registers,
+// with the question that preset answers, and names nothing else.
+//
+// Until 2026-09-29 the section named no preset at all and sent the reader to
+// tfg preset list, because a list typed by hand goes stale and nothing
+// compared one with the registry. The owner asked for the list on the first
+// page a visitor reads - a generator of single files loses to the ones in a
+// browser, and the presets are what those do not have. This is the comparison
+// that makes the list safe to keep: a seventh preset turns it red until the
+// table names it, and a preset renamed or removed turns it red until its row
+// goes. The question is compared word for word, because it is the one sentence
+// the program itself prints for each preset.
+func TestTheReadmeListsEveryPresetItShips(t *testing.T) {
+ body, err := os.ReadFile(filepath.Join(repoRoot(t), "README.md"))
+ if err != nil {
+ t.Fatalf("reading the README: %v", err)
+ }
+ const heading = "## 🧪 Presets"
+ text := string(body)
+ start := strings.Index(text, heading)
+ if start < 0 {
+ t.Fatalf("the README has no %q heading, so this guard has nothing to read", heading)
+ }
+ end := strings.Index(text[start+len(heading):], "\n## ")
+ if end < 0 {
+ t.Fatal("the presets section runs to the end of the README, which means the heading after it moved")
+ }
+ section := text[start : start+len(heading)+end]
+
+ rows := map[string]string{}
+ for _, line := range strings.Split(section, "\n") {
+ if !strings.HasPrefix(line, "| `") {
+ continue
+ }
+ cells := strings.Split(strings.Trim(line, "| "), " | ")
+ if len(cells) != 2 {
+ t.Errorf("a row of the presets table has %d cells, and it has two - the preset and "+
+ "its question:\n%s", len(cells), line)
+ continue
+ }
+ id := strings.Trim(cells[0], "`")
+ if _, twice := rows[id]; twice {
+ t.Errorf("the presets table has two rows for %s, and the second would hide what the "+
+ "first says from this guard", id)
+ }
+ rows[id] = strings.TrimSpace(cells[1])
+ }
+ if len(rows) == 0 {
+ t.Fatal("the presets section has no table rows - this guard would pass against any README ever written")
+ }
+
+ registered := preset.All()
+ if len(registered) == 0 {
+ t.Fatal("no preset is registered - this guard would pass without checking anything")
+ }
+ known := map[string]bool{}
+ for _, p := range registered {
+ known[p.ID] = true
+ question, ok := rows[p.ID]
+ if !ok {
+ t.Errorf("%s is registered and the table under %q does not name it, so the first page "+
+ "a visitor reads offers fewer presets than the binary ships", p.ID, heading)
+ continue
+ }
+ if question != p.Question {
+ t.Errorf("the table says %s answers %q, and the preset says it answers %q",
+ p.ID, question, p.Question)
+ }
+ }
+ for id := range rows {
+ if !known[id] {
+ t.Errorf("the table under %q names %s, and no preset of that name is registered", heading, id)
+ }
+ }
+}
diff --git a/internal/guard/socialpicture_test.go b/internal/guard/socialpicture_test.go
index b048f059..76bec17b 100644
--- a/internal/guard/socialpicture_test.go
+++ b/internal/guard/socialpicture_test.go
@@ -106,7 +106,10 @@ func TestTheSocialPictureShowsTheCardAsItIsNow(t *testing.T) {
" TFG_WRITE_SITE=1 go test ./internal/guard/ -run TestTheSiteSaysWhatTheToolSays\n"+
" TFG_WRITE_SOCIAL_STAMP=1 go test ./internal/guard/ -run TestTheSocialPicture\n"+
"Then LOOK at web/assets/social-preview.png. If git says it did not change, "+
- "the camera photographed the old card.",
+ "the camera photographed the old card.\n"+
+ "And upload it to GitHub by hand - Settings, General, Social preview. GitHub keeps "+
+ "a copy of its own that nothing here writes to, and until 2026-09-29 it said 24 "+
+ "formats while this picture said 26. tools/release-check.py compares the two.",
was, now)
}
}
diff --git a/web/assets/social-preview.png b/web/assets/social-preview.png
index edc1df7f..26f103d0 100644
Binary files a/web/assets/social-preview.png and b/web/assets/social-preview.png differ
diff --git a/web/content/en/index.html b/web/content/en/index.html
index be6f03a9..52d613f9 100644
--- a/web/content/en/index.html
+++ b/web/content/en/index.html
@@ -12,7 +12,7 @@
Generate real test files at any exact size
- The desktop window, set up to write a batch of files. The same engine runs behind the command line.
diff --git a/web/content/pl/index.html b/web/content/pl/index.html
index ce757e64..61fadb10 100644
--- a/web/content/pl/index.html
+++ b/web/content/pl/index.html
@@ -12,7 +12,7 @@
Generuj pliki testowe o zadanym rozmiarze
- Okno programu przygotowane do zapisania wsadu plików. Za wierszem poleceń stoi ten sam silnik.
diff --git a/web/public/assets/social-preview.png b/web/public/assets/social-preview.png
index edc1df7f..26f103d0 100644
Binary files a/web/public/assets/social-preview.png and b/web/public/assets/social-preview.png differ
diff --git a/web/public/assets/window.png b/web/public/assets/window.png
index 6a7fa026..f8006334 100644
Binary files a/web/public/assets/window.png and b/web/public/assets/window.png differ
diff --git a/web/public/index.html b/web/public/index.html
index 08bc4bea..c8e0f833 100644
--- a/web/public/index.html
+++ b/web/public/index.html
@@ -101,7 +101,7 @@
Generate real test files at any exact size
- The desktop window, set up to write a batch of files. The same engine runs behind the command line.
diff --git a/web/public/pl/index.html b/web/public/pl/index.html
index 6aad5d7c..e5dc5e32 100644
--- a/web/public/pl/index.html
+++ b/web/public/pl/index.html
@@ -101,7 +101,7 @@