diff --git a/internal/cli/cli.go b/internal/cli/cli.go index fe0e927..d4d4d2e 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -20,8 +20,6 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/engine" "github.com/donislawdev/TestingFilesGenerator/internal/format" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" - "github.com/donislawdev/TestingFilesGenerator/internal/legal" - "github.com/donislawdev/TestingFilesGenerator/internal/version" ) // Exit codes are a frozen contract. Changing what one means is a breaking @@ -71,52 +69,16 @@ func Run(ctx context.Context, args []string, out, errOut io.Writer) int { return ExitUsage } - switch args[0] { - case "generate": - return generate(ctx, args[1:], out, errOut) - case "validate": - return validate(ctx, args[1:], out, errOut) - case "verify": - return verify(ctx, args[1:], out, errOut) - case "cleanup": - return cleanup(ctx, args[1:], out, errOut) - case "recipe": - return recipeCmd(args[1:], out, errOut) - case "preset": - return presetCmd(ctx, args[1:], out, errOut) - case "formats": - return formats(args[1:], out, errOut) - case "damage": - return damageCmd(args[1:], out, errOut) - case "--version", "version": - fmt.Fprintln(out, version.Version) - return ExitOK - case "--license", "--licence", "license", "licence": - // Both spellings. The tool writes British English and half the people - // who reach for this will type the American one, and being right about - // spelling at the cost of answering is not a trade worth making. - // - // An answer, so it goes to out and ends with zero, the same as version. - fmt.Fprint(out, version.LicenceNotice) - // And then what this particular binary actually carries. The notice - // above points at a file, which is no help to somebody holding only - // the binary - a download of the window is one file and the notices - // are not in it. The list is read out of the build's own record, so - // it describes the binary being asked rather than the source tree it - // came from: the command line answers with two libraries, the window - // with twenty-seven and the fonts they bring. - printCarried(out, legal.CarriedHere()) - return ExitOK - case "--help", "-h", "help": - // Asking is not a mistake, so the answer goes where answers go and - // "tfg --help | less" works. - usage(out) - return ExitOK - default: - fmt.Fprintf(errOut, "tfg: unknown command %q.\n\n", args[0]) - usage(errOut) - return ExitUsage + // One list rather than a switch beside a help text, since 2026-09-09. See + // the comment on the command type in commands.go for what that cost. + for _, c := range commands() { + if c.matches(args[0]) { + return c.Run(ctx, args[1:], out, errOut) + } } + fmt.Fprintf(errOut, "tfg: unknown command %q.\n\n", args[0]) + usage(errOut) + return ExitUsage } // helpRequested reports whether these arguments explicitly ask for help. @@ -142,29 +104,6 @@ func helpRequested(args []string) bool { return false } -func usage(w io.Writer) { - fmt.Fprint(w, `tfg - generate test files and know how the system under test should react. - -Commands: - generate produce files, from a recipe or from flags - validate check a recipe and write nothing - verify check a directory against a manifest - cleanup remove the files a manifest lists - recipe fmt print a recipe in its settled shape - preset build a set of files from a named test question - formats list the formats this build supports - damage list the ways this build can break a file on purpose - version print the tool version - license print the licence and what it means for generated files - -Run "tfg --help" for the flags of one command. -`) -} - -// The licence notice this command prints lives in internal/version, on the -// bottom layer, because the window shows the same text and the two surfaces -// cannot import each other. See version.LicenceNotice. - // defaultManifestName mirrors the engine, which has to know the name to keep // a run from writing over an earlier one's record. const defaultManifestName = engine.DefaultManifestName diff --git a/internal/cli/commands.go b/internal/cli/commands.go new file mode 100644 index 0000000..a24c257 --- /dev/null +++ b/internal/cli/commands.go @@ -0,0 +1,195 @@ +// Part of package cli. See cli.go. +package cli + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/donislawdev/TestingFilesGenerator/internal/legal" + "github.com/donislawdev/TestingFilesGenerator/internal/version" +) + +// command is one verb of the command line: what the dispatch matches, what the +// help prints about it, and what runs. +// +// One declaration rather than two lists, since 2026-09-09. The help used to be +// a raw string beside a switch, and nothing compared them - measured that day, +// the dispatch offered eight verbs taking arguments while the help printed +// ten, and the four differences were all legitimate, so no naive comparison +// could have told a real gap from those. tfg damage had already reached the +// program and the website while the site's own list was a hand copy of this +// help. O201. +// +// The parts are separate because they are genuinely different questions: +// +// - Verb is the one word the dispatch matches and the help prints. +// - Aliases reach the same place and are NOT printed. Four spellings of +// licence exist because half the people who want it type the American one, +// and being right about spelling at the cost of answering is not a trade +// worth making. Printing all four would make the help a spelling lesson. +// - Shown replaces Verb in the help where the two differ. Only recipe does: +// the dispatch branches on "recipe", and "recipe fmt" is what a person +// types, because recipe on its own does nothing. +// - Summary empty means reachable but not listed, which is help itself. A +// reader looking at the help does not need to be told it exists. +type command struct { + Verb string + Aliases []string + Shown string + Summary string + Run func(ctx context.Context, args []string, out, errOut io.Writer) int +} + +// name is what the help prints for this command. +func (c command) name() string { + if c.Shown != "" { + return c.Shown + } + return c.Verb +} + +// matches says whether this is the command somebody asked for. +func (c command) matches(word string) bool { + if word == c.Verb { + return true + } + for _, alias := range c.Aliases { + if word == alias { + return true + } + } + return false +} + +// commands is every verb, in the order the help lists them. +// +// A function rather than a package variable, because help is in here and its +// body renders this list - which as a variable would be an initialisation +// cycle. Rebuilding eleven entries per invocation costs nothing next to the +// process start that precedes it. +// +// Three of these take no context and are wrapped rather than changed. The +// wrapper is where that shows, instead of five signatures being widened for a +// table. +func commands() []command { + return []command{ + {Verb: "generate", Summary: "produce files, from a recipe or from flags", Run: generate}, + {Verb: "validate", Summary: "check a recipe and write nothing", Run: validate}, + {Verb: "verify", Summary: "check a directory against a manifest", Run: verify}, + {Verb: "cleanup", Summary: "remove the files a manifest lists", Run: cleanup}, + { + Verb: "recipe", Shown: "recipe fmt", + Summary: "print a recipe in its settled shape", + Run: func(_ context.Context, args []string, out, errOut io.Writer) int { + return recipeCmd(args, out, errOut) + }, + }, + {Verb: "preset", Summary: "build a set of files from a named test question", Run: presetCmd}, + { + Verb: "formats", Summary: "list the formats this build supports", + Run: func(_ context.Context, args []string, out, errOut io.Writer) int { + return formats(args, out, errOut) + }, + }, + { + Verb: "damage", Summary: "list the ways this build can break a file on purpose", + Run: func(_ context.Context, args []string, out, errOut io.Writer) int { + return damageCmd(args, out, errOut) + }, + }, + { + Verb: "version", Aliases: []string{"--version"}, + Summary: "print the tool version", + Run: func(_ context.Context, _ []string, out, _ io.Writer) int { + fmt.Fprintln(out, version.Version) + return ExitOK + }, + }, + { + Verb: "license", + Aliases: []string{"--license", "--licence", "licence"}, + Summary: "print the licence and what it means for generated files", + Run: licenceCmd, + }, + { + // Listed nowhere, reachable three ways. Somebody reading the help + // has already found it. + Verb: "help", Aliases: []string{"--help", "-h"}, + Run: func(_ context.Context, _ []string, out, _ io.Writer) int { + // Asking is not a mistake, so the answer goes where answers go + // and "tfg --help | less" works. + usage(out) + return ExitOK + }, + }, + } +} + +// Command is what one verb is called and what the help says about it. +// +// Exported without the function that runs it, because what a caller outside +// this package can want is the FACTS - a guard checking that the website +// repeats them, or that every verb is watched. Handing out the behaviour as +// well would let something outside the command line invoke half of it. +type Command struct { + // Name is how the help prints it, which is "recipe fmt" where the verb is + // "recipe". Verb is the word the dispatch matches. + Name string + Verb string + // Summary is empty for a command the help does not list. + Summary string +} + +// Commands is every verb this build answers to, in the order the help lists +// them. +func Commands() []Command { + all := commands() + out := make([]Command, 0, len(all)) + for _, c := range all { + out = append(out, Command{Name: c.name(), Verb: c.Verb, Summary: c.Summary}) + } + return out +} + +// licenceCmd prints the licence and then what this particular binary carries. +// +// The notice points at a file, which is no help to somebody holding only the +// binary - a download of the window is one file and the notices are not in it. +// The list is read out of the build's own record, so it describes the binary +// being asked rather than the source tree it came from: the command line +// answers with two libraries, the window with twenty-seven and the fonts they +// bring. +// +// An answer, so it goes to out and ends with zero, the same as version. +func licenceCmd(_ context.Context, _ []string, out, _ io.Writer) int { + fmt.Fprint(out, version.LicenceNotice) + printCarried(out, legal.CarriedHere()) + return ExitOK +} + +// usage prints what this tool is and what it can be asked to do. +// +// The command block is rendered from the declaration above rather than written +// out here, so a verb added tomorrow appears without anybody remembering to +// add it. The column is worked out from the longest name for the same reason: +// a number here would be a second copy of "the longest is recipe fmt" and +// would go stale the day something longer arrives. +func usage(w io.Writer) { + fmt.Fprint(w, "tfg - generate test files and know how the system under test should react.\n\nCommands:\n") + widest := 0 + for _, c := range commands() { + if c.Summary != "" && len(c.name()) > widest { + widest = len(c.name()) + } + } + for _, c := range commands() { + if c.Summary == "" { + continue + } + pad := strings.Repeat(" ", widest-len(c.name())+2) + fmt.Fprintf(w, " %s%s%s\n", c.name(), pad, c.Summary) + } + fmt.Fprint(w, "\nRun \"tfg --help\" for the flags of one command.\n") +} diff --git a/internal/guard/commanddeclaration_test.go b/internal/guard/commanddeclaration_test.go new file mode 100644 index 0000000..55a4f9e --- /dev/null +++ b/internal/guard/commanddeclaration_test.go @@ -0,0 +1,244 @@ +package guard + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/cli" +) + +// The command line describes itself from one declaration, and the documents +// that repeat it are held to it. +// +// Three lists said the same thing and nothing compared any two of them, which +// is O201. The block tfg --help prints was a raw string beside the switch that +// dispatched. The website copied that block by hand. The flag table in README +// was typed out separately from the flag set generate builds. Measured on +// 2026-09-09: README was missing --damage, a flag added to the program that +// same day, and nothing said so. +// +// The help is rendered from the declaration now, so the first of those cannot +// drift at all. The other two are documents, which no amount of structure can +// generate, so they get guards - and both are asked with a rule rather than a +// list of exceptions, because a list of exceptions is a guard with a hole. + +// commandsTheToolPrints is the command names out of tfg --help, which is the +// text the page is a copy of. +// +// Asked of the help rather than of the router beside it on purpose: the help +// is what a visitor compares the page against, so agreeing with anything else +// would prove the wrong thing. +func commandsTheToolPrints(t *testing.T) []string { + t.Helper() + var out, errOut bytes.Buffer + if code := cli.Run(context.Background(), []string{"--help"}, &out, &errOut); code != cli.ExitOK { + t.Fatalf("tfg --help ended with %d rather than %d, so there is no list to compare against", + code, cli.ExitOK) + } + names, err := commandNamesIn(out.String()) + if err != nil { + t.Fatalf("reading the command block out of tfg --help: %v", err) + } + return names +} + +// commandNamesIn takes the names out of the Commands: block of a help text. +// +// Split on two spaces rather than on the first one, because "recipe fmt" is a +// command whose name has a space in it - splitting on the first would name a +// command the router does not have. +// +// Finding nothing is an error rather than an empty list, and that is the whole +// reason this is a function of its own. A guard that quietly stopped finding +// the block would compare the page against nothing and stay green while +// proving it - which is the failure this file exists to make impossible. +func commandNamesIn(help string) ([]string, error) { + const header = "Commands:" + _, rest, found := strings.Cut(help, header+"\n") + if !found { + return nil, fmt.Errorf("no %q line in the help text", header) + } + var names []string + for _, line := range strings.Split(rest, "\n") { + if !strings.HasPrefix(line, " ") { + break + } + name, _, split := strings.Cut(strings.TrimPrefix(line, " "), " ") + if !split { + return nil, fmt.Errorf("the line %q under %s has no summary beside the name", line, header) + } + names = append(names, name) + } + if len(names) == 0 { + return nil, fmt.Errorf("the %s block is empty", header) + } + return names, nil +} + +// Every command the help lists can actually be run. +// +// The pair to it below is what makes this worth having: on its own this would +// pass for a declaration listing one command and hiding ten. +func TestEveryCommandTheHelpListsIsOneTheToolAnswersTo(t *testing.T) { + listed := 0 + for _, c := range cli.Commands() { + if c.Summary == "" { + continue + } + listed++ + // The first word of the PRINTED name, not the verb beside it in the + // declaration. That difference is the whole point: a reader types what + // the help shows them, so asking the declaration about its own verb + // would pass for an entry printing one word and dispatching on + // another. Mutation said so - the first version of this asked c.Verb + // and stayed green against exactly that swap. + // + // First word, because "recipe fmt" is printed as two and the dispatch + // branches on the first. + typed, _, _ := strings.Cut(c.Name, " ") + var out, errOut bytes.Buffer + // The word alone. Some need an operation or a file after it and will + // say so, which is an answer - what would mean the help is lying is + // "unknown command". + cli.Run(context.Background(), []string{typed}, &out, &errOut) + if strings.Contains(errOut.String(), "unknown command") { + t.Errorf("the help lists %q and the tool does not know %q", c.Name, typed) + } + } + if listed == 0 { + t.Fatal("the help lists nothing, so this guard would pass against any tree") + } + t.Logf("%d command(s) listed in the help", listed) +} + +// A command the help does not list does nothing except print that help. +// +// This is the other half, and it is a rule rather than an exception for the +// one command that has no summary today. Anything unlisted is either help +// itself - which a reader of the help has already found - or a verb somebody +// forgot to describe, and only the second is a defect. Comparing the output +// against the help tells them apart without naming anybody. +func TestACommandTheHelpDoesNotListOnlyPrintsTheHelp(t *testing.T) { + var wanted bytes.Buffer + cli.Run(context.Background(), []string{"--help"}, &wanted, &bytes.Buffer{}) + if wanted.Len() == 0 { + t.Fatal("the help printed nothing, so there is nothing to compare against") + } + + unlisted := 0 + for _, c := range cli.Commands() { + if c.Summary != "" { + continue + } + unlisted++ + var out, errOut bytes.Buffer + if code := cli.Run(context.Background(), []string{c.Verb}, &out, &errOut); code != cli.ExitOK { + t.Errorf("%q is not listed in the help and does not end with %d, so it is a command nobody is told about", + c.Verb, cli.ExitOK) + } + if out.String() != wanted.String() { + t.Errorf("%q is not listed in the help and does something other than print it, so it is a command nobody is told about", + c.Verb) + } + } + t.Logf("%d command(s) reachable but unlisted", unlisted) +} + +// The flag table in README names every flag generate has, and no others. +// +// README is what somebody reads before they have the binary, so a flag missing +// from it is a feature that does not exist for that reader. Measured on +// 2026-09-09: --damage was in the program, in the help and on the website, and +// not in this table - the same class as the site listing nine commands out of +// ten, one document further out. +// +// Both directions, because a row for a flag that was removed sends somebody to +// type something the tool will refuse. +func TestTheReadmeFlagTableNamesEveryFlagGenerateTakes(t *testing.T) { + declared := flagsOfGenerate(t) + if len(declared) == 0 { + t.Fatal("generate declared no flags, so this guard would pass against any tree") + } + documented := flagsInTheReadmeTable(t) + + for name := range declared { + if !documented[name] { + t.Errorf("generate takes --%s and the README flag table does not mention it", name) + } + } + for name := range documented { + if !declared[name] { + t.Errorf("the README flag table offers --%s and generate does not take it", name) + } + } + t.Logf("%d flag(s) declared, %d documented", len(declared), len(documented)) +} + +// flagsOfGenerate asks the command for its own flag set rather than reading +// the source, so a flag added by any route is covered. +// +// It runs generate with --help, which builds the set and prints it, and reads +// the names back out of what it printed. The alternative was exporting the set +// itself, which would put a seam in the command line for a test to hold. +func flagsOfGenerate(t *testing.T) map[string]bool { + t.Helper() + var out, errOut bytes.Buffer + if code := cli.Run(context.Background(), []string{"generate", "--help"}, &out, &errOut); code != cli.ExitOK { + t.Fatalf("generate --help ended with %d: %s", code, errOut.String()) + } + // The flag package prints " -name" at the head of each entry, one per + // flag, whatever its type. + found := map[string]bool{} + for _, line := range strings.Split(out.String(), "\n") { + if !strings.HasPrefix(line, " -") { + continue + } + name, _, _ := strings.Cut(strings.TrimPrefix(line, " -"), " ") + if name != "" { + found[name] = true + } + } + // A sanity check on the shape rather than on the count: flag.PrintDefaults + // is what this reads, and a change in its format would leave the map empty + // and every comparison below trivially true. + if !found["format"] { + t.Fatalf("no --format among %d name(s) read back, so the help format changed and this reads nothing", len(found)) + } + return found +} + +var readmeFlagRow = regexp.MustCompile(`(?m)^\| ` + "`" + `--([a-z-]+)`) + +// flagsInTheReadmeTable reads the flag names out of the generate table. +// +// Bounded to the table under the generate heading rather than the whole file, +// because every other command documents its own flags further down and those +// are not this table's business. +func flagsInTheReadmeTable(t *testing.T) map[string]bool { + t.Helper() + body, err := os.ReadFile(filepath.Join(repoRoot(t), "README.md")) + if err != nil { + t.Skipf("no README here: %v", err) + } + text := string(body) + const first = "| `--format `" + start := strings.Index(text, first) + if start < 0 { + t.Fatalf("the generate flag table does not start with %q any more, so this guard reads nothing", first) + } + end := strings.Index(text[start:], "\n\n") + if end < 0 { + t.Fatal("the generate flag table does not end") + } + found := map[string]bool{} + for _, m := range readmeFlagRow.FindAllStringSubmatch(text[start:start+end], -1) { + found[m[1]] = true + } + return found +} diff --git a/internal/guard/damagecommand_test.go b/internal/guard/damagecommand_test.go index 4dc6e49..3ba6c1f 100644 --- a/internal/guard/damagecommand_test.go +++ b/internal/guard/damagecommand_test.go @@ -1,10 +1,9 @@ package guard import ( + "bytes" + "context" "encoding/json" - "go/ast" - "go/parser" - "go/token" "strconv" "strings" "testing" @@ -271,9 +270,9 @@ func TestTheMachineReadableDamageListCarriesTheWholeDeclaration(t *testing.T) { // exceptions would have had nine entries against ten commands, and a ratchet // that is mostly excuse teaches its reader to skip it. func TestEveryCommandTakingItsOwnArgumentsIsWatchedForHelp(t *testing.T) { - dispatched := commandsInTheDispatch(t) + dispatched := commandsTakingTheirOwnArguments(t) if len(dispatched) == 0 { - t.Fatal("no dispatching case was found, so this guard would pass against any tree") + t.Fatal("not one verb refused an unknown flag, so this guard would pass against any tree") } watched := map[string]bool{} @@ -289,56 +288,28 @@ func TestEveryCommandTakingItsOwnArgumentsIsWatchedForHelp(t *testing.T) { t.Logf("%d command(s) take their own arguments", len(dispatched)) } -// commandsInTheDispatch reads the verbs of cli.Run that hand on their -// arguments, by asking the source rather than by running anything. -func commandsInTheDispatch(t *testing.T) []string { +// commandsTakingTheirOwnArguments is every verb whose own flag set reads what +// follows it, found by ASKING each one rather than by reading the source. +// +// It read the dispatch out of cli.go with go/ast until 2026-09-09, and the +// switch it parsed no longer exists - the verbs live in one declaration now, +// which is what O201 was about. Rewriting the parser to walk that declaration +// instead would have kept a reader of syntax where a measurement will do. +// +// The measurement is an unknown flag. A command with its own flag set refuses +// it with ExitUsage, and one that ignores what follows answers normally - +// measured 2026-09-09 across all eleven verbs, splitting them eight to three +// with no exception to write down. That is the same split the parser produced, +// arrived at by watching behaviour instead of shape. +func commandsTakingTheirOwnArguments(t *testing.T) []string { t.Helper() - file, err := parser.ParseFile(token.NewFileSet(), "../cli/cli.go", nil, 0) - if err != nil { - t.Fatalf("reading the dispatch: %v", err) - } - var out []string - ast.Inspect(file, func(n ast.Node) bool { - clause, ok := n.(*ast.CaseClause) - if !ok { - return true - } - if !handsOnItsArguments(clause) { - return true + for _, c := range cli.Commands() { + var stdout, stderr bytes.Buffer + code := cli.Run(context.Background(), []string{c.Verb, "--nosuchflag"}, &stdout, &stderr) + if code == cli.ExitUsage { + out = append(out, c.Verb) } - for _, expr := range clause.List { - lit, ok := expr.(*ast.BasicLit) - if !ok || lit.Kind != token.STRING { - continue - } - name, err := strconv.Unquote(lit.Value) - if err != nil || strings.HasPrefix(name, "-") { - continue - } - out = append(out, name) - } - return true - }) - return out -} - -// handsOnItsArguments reports whether this case passes args[1:] to something, -// which is what having flags of its own looks like from the outside. -func handsOnItsArguments(clause *ast.CaseClause) bool { - found := false - for _, stmt := range clause.Body { - ast.Inspect(stmt, func(n ast.Node) bool { - slice, ok := n.(*ast.SliceExpr) - if !ok { - return true - } - ident, ok := slice.X.(*ast.Ident) - if ok && ident.Name == "args" && slice.High == nil { - found = true - } - return true - }) } - return found + return out } diff --git a/internal/guard/site_test.go b/internal/guard/site_test.go index ed4d7a1..ea86b7b 100644 --- a/internal/guard/site_test.go +++ b/internal/guard/site_test.go @@ -2,11 +2,9 @@ package guard import ( "bytes" - "context" "encoding/json" "encoding/xml" "errors" - "fmt" "io" "os" "path/filepath" @@ -92,59 +90,6 @@ func exitCodesInOrder() []int { } } -// commandsTheToolPrints is the command names out of tfg --help, which is the -// text the page is a copy of. -// -// Asked of the help rather than of the router beside it on purpose: the help -// is what a visitor compares the page against, so agreeing with anything else -// would prove the wrong thing. -func commandsTheToolPrints(t *testing.T) []string { - t.Helper() - var out, errOut bytes.Buffer - if code := cli.Run(context.Background(), []string{"--help"}, &out, &errOut); code != cli.ExitOK { - t.Fatalf("tfg --help ended with %d rather than %d, so there is no list to compare against", - code, cli.ExitOK) - } - names, err := commandNamesIn(out.String()) - if err != nil { - t.Fatalf("reading the command block out of tfg --help: %v", err) - } - return names -} - -// commandNamesIn takes the names out of the Commands: block of a help text. -// -// Split on two spaces rather than on the first one, because "recipe fmt" is a -// command whose name has a space in it - splitting on the first would name a -// command the router does not have. -// -// Finding nothing is an error rather than an empty list, and that is the whole -// reason this is a function of its own. A guard that quietly stopped finding -// the block would compare the page against nothing and stay green while -// proving it - which is the failure this file exists to make impossible. -func commandNamesIn(help string) ([]string, error) { - const header = "Commands:" - _, rest, found := strings.Cut(help, header+"\n") - if !found { - return nil, fmt.Errorf("no %q line in the help text", header) - } - var names []string - for _, line := range strings.Split(rest, "\n") { - if !strings.HasPrefix(line, " ") { - break - } - name, _, split := strings.Cut(strings.TrimPrefix(line, " "), " ") - if !split { - return nil, fmt.Errorf("the line %q under %s has no summary beside the name", line, header) - } - names = append(names, name) - } - if len(names) == 0 { - return nil, fmt.Errorf("the %s block is empty", header) - } - return names, nil -} - // factsFromTheProgram fills every number the pages may state. func factsFromTheProgram(t *testing.T) site.Facts { t.Helper() @@ -464,6 +409,26 @@ func TestEveryLanguageDescribesEverythingTheProgramCanProduce(t *testing.T) { t.Errorf("the command %q has no summary in %s, so the page would list it blank", name, lang.Code) } } + // And the English page says what the program says, word for word. + // + // The rows above ask whether a summary EXISTS, which is all that can + // be asked of a translation - the Polish page describes the same + // command in Polish on purpose. English is the language the program + // itself writes in, so there the two are copies of one sentence and + // nothing was comparing them. Added 2026-09-09 with O201: the site + // gained its own copy of these sentences that morning, and a copy + // nobody checks is the defect this whole file exists for. + if lang.Code == "en" { + for _, c := range cli.Commands() { + if c.Summary == "" { + continue + } + if said := lang.Commands[c.Name]; said != c.Summary { + t.Errorf("tfg --help describes %q as %q and the English page says %q", + c.Name, c.Summary, said) + } + } + } for name := range lang.Commands { if !slices.Contains(facts.Commands, name) { t.Errorf("%s describes a command %q that tfg --help does not print", lang.Code, name)