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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ because it turns other people's test suites red.

### Fixed

- **The window now warns when a run's record will be too big for this build to
read back.** The command line has said this since the ceiling was measured.
The window said nothing at all, so somebody who generated 25 000 files from it
was left with a directory that `tfg verify` and `tfg cleanup` both refuse -
and the manifest is the only authority over what may be removed, so those
files could never be cleaned up by this tool again.

It appears in two places, because the window cannot speak in the middle of a
run: under Preview, which is the window's answer to `--dry-run`, and again
when a run finishes. The second is the one that matters, since Preview is a
button somebody may never press. It comes straight after the line saying what
the run did, ahead of any other note.

The run itself still works and is still not refused. What was missing was that
nobody was told.

- **`tfg verify` no longer calls another run's files "extra".** A directory is
allowed to hold more than one run - that is what `output.manifest` is for -
and verifying one of them reported every file the other had written as a file
Expand Down
142 changes: 142 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Contributing

Thank you for looking. This is a small project with a narrow purpose, so the
most useful thing this file can do is tell you, before you spend an evening on
something, what it will and will not take.

Everything here runs on your own machine. There is no server, no account and no
telemetry, and that is a design decision rather than a stage the project has not
reached yet.

## The quickest ways to help

- **Report what broke.** A file that a real reader would not open, a size that
came out wrong, a message that sent you the wrong way. The issue forms are at
[Issues](https://github.com/donislawdev/TestingFilesGenerator/issues/new/choose).
- **Translate the window.** See [Translations](#translations) below. No Go
needed.
- **Say what a manifest should have told you.** This tool exists to say how a
system under test ought to react to a file. If it stayed quiet about something
you had to work out by hand, that is worth an issue.
- **Send code.** See below for what it has to satisfy.

## Building it

Needs Go 1.27.0 or newer, and nothing else for the command line tool.

```
git clone https://github.com/donislawdev/TestingFilesGenerator
cd TestingFilesGenerator
go build -tags "$(cat .github/build-tags)" ./cmd/tfg
go test -tags "$(cat .github/build-tags)" ./...
```

**The build tag is not optional and it is not decoration.** A build without it
does not compile and says why. It turns off an assembly path in the AVIF encoder
that reads past the end of a buffer, and the files produced are the same either
way. Read the tag from the file rather than typing it, because it is the file
the workflows read too.

The desktop window is a second binary, `./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 test suite takes a few minutes. Most of it is one package, `internal/guard`,
which is where every test that defends a promise of the product lives.

## Translations

The window reads its words from a catalogue, and English is one file in it:
`internal/gui/text/locale/en.json`. To add a language, copy that file to
`<code>.json` next to it, using the language code, and translate the values.

```json
"ButtonCancel": {
"description": "The words on a button.",
"other": "Cancel"
}
```

Four things to know before you start:

- **Translate `other`, and leave `description` alone.** The description is a note
to you about where the words appear, and it is generated from the source.
- **A placeholder such as `{{.Count}}` has to stay spelled exactly that way.**
It is where a number or a name is put in. You may move it inside the sentence.
- **Plural forms are yours to decide.** English needs `one` and `other`. If your
language needs `few`, `many` or `zero`, add them beside the English ones. The
code hands over the number and both English forms, so a language with more
forms than English is not something you have to work around.
- **Anything you leave out falls back to English**, so a half finished
catalogue leaves English sentences rather than empty ones.

**One limit, said plainly so you do not waste an evening: the window does not
offer a language switch yet.** A catalogue you contribute is built into the
program and is loaded, and English is still what it answers in, until the
setting that chooses a language lands. If you would rather wait for that, say so
in an issue and it will be weighed as a reason to do it sooner.

The command line is **English only** and stays that way, translations included.
It goes into scripts and CI logs that other people read, and a message that
changes language with the machine it ran on is a message nobody can search for.

## What a change has to satisfy

These are not style preferences. Each one is a promise the tool makes to people
whose test suites depend on it.

- **A file is the size that was asked for, to the byte, or it is an error.**
Never a size close to it, never a quiet rounding. A batch of ten thousand
files is not something anybody checks by hand.
- **The same recipe and the same seed produce the same bytes**, on every
operating system. Anything that moves the bytes of a generated file is a
breaking change, because it turns other people's test suites red. It is
allowed, and it needs a note in the changelog and a version bump, and the
version is bumped by the maintainer rather than in the pull request.
- **Nothing is written over, and nothing is deleted that a manifest does not
list.** This tool runs in directories that belong to other people.
- **Silence is banned.** A file that was not produced, a name the filesystem
would not take, a limit the tool invented for you - each of those has to be
visible in the output and in the manifest. A manifest that quietly skipped ten
files looks complete and becomes a false result in somebody's test run.
- **No outgoing network connection, ever.** No telemetry, no update check, no
fetching anything while it runs. A test checks this by asking the compiler
what the binaries link, so a pull request that adds it will not get past CI.
- **A new dependency needs a reason and a licence check.** The project is
GPL-3.0, which rules some licences out, and the command line binary currently
has two dependencies in total. Say in the pull request why the standard
library will not do.
- **Every change in behaviour comes with a test that fails without it.** A green
suite is not evidence on its own. The question to answer in the pull request
is "which test would go red if this change were undone".
- **Words a user reads are English, with a flat hyphen and no semicolons.** That
covers the README, the changelog, `--help`, every message, and every comment
in the code. A test enforces the punctuation.
- **An error message says four things**: what happened, why, what value would be
accepted, and what to do instead.

## Sending a pull request

- Branch from `main` and open the pull request against `main`. Nothing is pushed
to `main` directly, including by the maintainer.
- Keep it to one subject. A pull request that fixes a bug and tidies three
unrelated files is one that cannot be reviewed or reverted cleanly.
- CI has to be green. It builds and tests on Windows, Linux and macOS, runs the
linters, and checks the dependency and supply chain gates.
- The commit message and the pull request are public and permanent. Do not put
anything in them you would not show a stranger.
- Pull requests are merged by the maintainer, as a squash.

## Security

Do not open an issue for a vulnerability. [SECURITY.md](SECURITY.md) says how to
report one privately and what is in scope.

## Conduct and licence

By taking part you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).

Contributions are licensed under **GPL-3.0-or-later**, the same licence as the
rest of the project. Files the tool generates are yours, and carry no licence
from us at all - run `tfg license` and it says so.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,10 @@ Found a problem or want a format? The
[issue tracker](https://github.com/donislawdev/TestingFilesGenerator/issues) is
open, and so is the discussion about what gets built next.

Want to help? [CONTRIBUTING.md](CONTRIBUTING.md) says what a change has to
satisfy before it can go in, and how to translate the window into another
language without writing any Go.

## 🔒 Everything inside a generated file is made up

The contents are synthesised from a seed. Names, addresses, e-mail addresses, IP
Expand Down
4 changes: 4 additions & 0 deletions internal/guard/ascii_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ func asciiRequired(rel string) bool {
var englishFiles = []string{
"README.md",
"CHANGELOG.md",
// Added 2026-09-07 with the file itself. It is repository text somebody
// reads before they have a checkout, which is the same criterion as the two
// above - the place, not the reader.
"CONTRIBUTING.md",
// The notices that travel with a release binary. Added 2026-08-04 with the
// file itself: it is repository text a user reads, so it belongs here by
// the same criterion as the two above. The licence texts it quotes sit in
Expand Down
43 changes: 25 additions & 18 deletions internal/guard/buildfloor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@ import (
// directive came out. So the pin now lives in GO_VERSION in the workflows, and
// this holds the floor against every one of them.
//
// The third copy is the one a person reads before they have the repository.
// README.md states the minimum in prose, and prose is the copy that rots, so it
// is held here too. Same arrangement as the build tags in
// The remaining copies are the ones a person reads before they have the
// repository. README.md states the minimum in prose, CONTRIBUTING.md states it
// again since 2026-09-07, and prose is the copy that rots - so both are held
// here. Same arrangement as the build tags in
// TestTheInstallInstructionsCarryTheBuildTags and for the same reason: one file
// holds the fact, everything inside a checkout reads it, and the copies that
// have to live outside get a guard instead.
Expand All @@ -48,7 +49,7 @@ import (
// strings is precisely where this class of defect hides. Two strings either
// match or they do not, and that is checkable without a comparison anybody has
// to be right about.
func TestTheBuildFloorIsThePinnedToolchainAndTheReadmeSaysSo(t *testing.T) {
func TestTheBuildFloorIsThePinnedToolchainAndTheDocumentsSaySo(t *testing.T) {
root := repoRoot(t)
floor := goModFloor(t, root)

Expand Down Expand Up @@ -76,13 +77,19 @@ func TestTheBuildFloorIsThePinnedToolchainAndTheReadmeSaysSo(t *testing.T) {
"floor. That is where the pin lives now that go.mod has no toolchain directive.")
}

stated, at, text := readmeMinimumGo(t, root)
if stated != floor {
t.Errorf("README.md line %d tells somebody they need Go %s and go.mod admits Go %s:\n"+
// Every document that states the requirement, not only the shop window.
// CONTRIBUTING.md states it too since 2026-09-07, and a second copy of a
// number is a second thing to keep in step.
for _, name := range filesThatTellSomebodyHowToBuild {
stated, at, text := minimumGoIn(t, root, name)
if stated == floor {
continue
}
t.Errorf("%s line %d tells somebody they need Go %s and go.mod admits Go %s:\n"+
" %s\n"+
"What to do: make the sentence name %s. It is read before anybody has the "+
"repository, so it is the one copy nothing else can correct.",
at, stated, floor, strings.TrimSpace(text), floor)
name, at, stated, floor, strings.TrimSpace(text), floor)
}
}

Expand Down Expand Up @@ -123,17 +130,17 @@ func workflowFiles(t *testing.T, root string) []string {
return found
}

// readmeMinimumGo returns the version README.md names as the minimum, the line
// it sits on and that line's text.
// minimumGoIn returns the version a document names as the minimum, the line it
// sits on and that line's text.
//
// Matched on the sentence rather than on a position, because a position moves
// the first time somebody adds a paragraph above it.
func readmeMinimumGo(t *testing.T, root string) (string, int, string) {
func minimumGoIn(t *testing.T, root, name string) (string, int, string) {
t.Helper()

raw, err := os.ReadFile(filepath.Join(root, "README.md"))
raw, err := os.ReadFile(filepath.Join(root, name))
if err != nil {
t.Fatalf("reading README.md: %v", err)
t.Fatalf("reading %s: %v", name, err)
}

want := regexp.MustCompile(`Needs Go ([0-9][0-9.]*)`)
Expand All @@ -146,15 +153,15 @@ func readmeMinimumGo(t *testing.T, root string) (string, int, string) {
// A second sentence naming a version is two answers to one question,
// and the guard would then check whichever came last.
if found != "" {
t.Fatalf("README.md names a minimum Go version twice, on lines %d and %d, so "+
"there is no single sentence to hold against go.mod", at, i+1)
t.Fatalf("%s names a minimum Go version twice, on lines %d and %d, so "+
"there is no single sentence to hold against go.mod", name, at, i+1)
}
found, at, text = m[1], i+1, line
}
if found == "" {
t.Fatal(`README.md has no "Needs Go <version>" sentence, so this guard checked ` +
"nothing. It is the only statement of the requirement somebody sees before " +
"they clone.")
t.Fatalf(`%s has no "Needs Go <version>" sentence, so this guard checked `+
"nothing. It is a statement of the requirement somebody sees before "+
"they clone.", name)
}
return found, at, text
}
48 changes: 33 additions & 15 deletions internal/guard/buildtags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,37 +180,55 @@ func TestABuildWithoutTheBuildTagsRefusesAndSaysWhy(t *testing.T) {
}
}

// The install instructions carry the tags the build needs.
// filesThatTellSomebodyHowToBuild are the ones carrying a build command a
// person types before they have a checkout.
//
// Everything this project runs reads the tags from one file. That cannot work
// for a command somebody copies out of a document on the web, so these carry
// the tag themselves - and a copy is a thing to keep honest, which is what the
// two guards below are for.
//
// One file names the tags and everything else reads it, which works for the
// commands this project runs and cannot work for a command somebody types
// before they have the repository. So README.md carries the tag itself, and
// this is what keeps that copy honest.
// CONTRIBUTING.md joined on 2026-09-07 with the file itself. A guide telling
// somebody to build without the tag hands them a binary whose AVIF encoder
// reads past the end of a buffer, and the guide is the first thing a
// contributor follows.
var filesThatTellSomebodyHowToBuild = []string{"README.md", "CONTRIBUTING.md"}

// The install instructions carry the tags the build needs.
func TestTheInstallInstructionsCarryTheBuildTags(t *testing.T) {
tags := buildTags()
raw, err := os.ReadFile(filepath.Join("..", "..", "README.md"))
if err != nil {
t.Fatalf("reading README.md: %v", err)
checked := 0
for _, name := range filesThatTellSomebodyHowToBuild {
raw, err := os.ReadFile(filepath.Join("..", "..", name))
if err != nil {
t.Fatalf("reading %s: %v", name, err)
}
checked += tagsOnEveryBuildLine(t, name, string(raw), tags)
}
if checked == 0 {
t.Fatal("no line of those files was recognised as an install or build command, so this " +
"guard checked nothing")
}
}

// tagsOnEveryBuildLine checks one document and says how many commands it read.
func tagsOnEveryBuildLine(t *testing.T, name, body, tags string) int {
t.Helper()
checked := 0
for i, line := range strings.Split(string(raw), "\n") {
for i, line := range strings.Split(body, "\n") {
if !strings.Contains(line, "go install") && !strings.Contains(line, "go build ") {
continue
}
checked++
if strings.Contains(line, buildTagsFileName) || strings.Contains(line, "-tags "+tags) {
continue
}
t.Errorf("README.md line %d tells somebody to build without the build tags:\n %s\n"+
t.Errorf("%s line %d tells somebody to build without the build tags:\n %s\n"+
"What to do: add -tags %q to it, or read them from %s where the command is run "+
"inside a checkout. Without them the AVIF encoder reads past the end of a buffer.",
i+1, strings.TrimSpace(line), tags, buildTagsFileName)
}
if checked == 0 {
t.Fatal("no line of README.md was recognised as an install or build command, so this " +
"guard checked nothing")
name, i+1, strings.TrimSpace(line), tags, buildTagsFileName)
}
return checked
}

// The size that crashed, encoded here so a build that lost the tag says so
Expand Down
9 changes: 8 additions & 1 deletion internal/guard/community_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ func TestNothingThatDirectsAPersonPointsAtAnotherProject(t *testing.T) {
if err != nil {
t.Fatalf("walking .github: %v", err)
}
files = append(files, filepath.Join(root, "SECURITY.md"))
// SECURITY.md and CONTRIBUTING.md sit beside .github rather than in it, and
// both point somebody somewhere. The contributing guide arrived on
// 2026-09-07 carrying four links to this repository, which is exactly the
// shape this guard exists for: these files get written by copying one from
// another project, and a link left pointing at the old one reads perfectly.
files = append(files,
filepath.Join(root, "SECURITY.md"),
filepath.Join(root, "CONTRIBUTING.md"))

checked := 0
for _, p := range files {
Expand Down
Loading
Loading