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

### Added

- **How much memory a run may use is yours to set.** `--memory` on
`generate`, `validate` and `preset show`, and Memory on the window's
Preferences screen: `auto` (half of what this machine or container gives
the program), a size such as `8gb`, or `off`. Pictures and films wait for
room rather than all being held at once. A plan is refused at this
setting rather than at a fixed 2 GiB, and `auto` never goes below that.
Files come out byte for byte the same.

- **A Docker image.** `ghcr.io/donislawdev/tfg` holds the Linux command line
of a release and nothing else, for x64 and Arm, and runs with no network. It
is made from the published archive once its checksum and attestation check
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,12 @@ tfg generate --format txt --size 1mb settings come from the flags
| `--clean` | turn off the self describing label written inside each file |
| `--dry-run` | count and show, write nothing at all |
| `--json` | write the manifest to standard output |
| `--memory <m>` | how much memory the run may use: `auto` (the default, half of what this machine gives the program), a size such as `8gb`, or `off` for no limit. Pictures wait for room rather than all being held at once, and a plan larger than this is refused before anything is written. Under `auto`, a plan of up to 2.0 GB is never refused. Also on `validate` and `preset show` |

### `tfg validate`

```
tfg validate <recipe.yaml> [--json]
tfg validate <recipe.yaml> [--json] [--memory <m>]
```

Checks a recipe and writes nothing. Reports **every** problem at once rather
Expand Down
11 changes: 8 additions & 3 deletions internal/cli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ Flags:
func generate(ctx context.Context, args []string, out, errOut io.Writer) int {
var g generateOpts
fs, usage := generateFlagSet(errOut, &g)
// Beside the options rather than among them: it says nothing about the
// files, only about the machine, and generateOpts is at the crowding band.
var memory memoryValue
memoryFlag(fs, &memory)
if helpRequested(args) {
usage(out)
return ExitOK
Expand Down Expand Up @@ -156,6 +160,7 @@ func generate(ctx context.Context, args []string, out, errOut io.Writer) int {
DryRun: g.dryRun,
Command: "tfg " + strings.Join(args2(args), " "),
ManifestName: defaultManifestName,
Memory: memory.Memory,
}

var (
Expand Down Expand Up @@ -446,9 +451,9 @@ func produce(ctx context.Context, targets []engine.Target, opt engine.Options, g
bar.clear()
}

for _, n := range res.Manifest.Notes() {
fmt.Fprintf(errOut, "note: %s\n", n)
}
// What the run says about the memory it ran in comes first: it is about
// the run rather than about any one file, and it is said once.
sayNotes(append(saidAll(res.RunNotes), res.Manifest.Notes()...), errOut)

if runErr != nil {
fmt.Fprintf(errOut, "tfg: %s%s\n", describeError(runErr), whatSurvived(runErr, res))
Expand Down
50 changes: 50 additions & 0 deletions internal/cli/memoryflag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cli

import (
"flag"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
)

// --memory, on every command that plans: generate, validate and preset show.
//
// One function registers it on all three, so the flag cannot read one way on
// one command and another on the next, and core.ParseMemory reads it, so the
// flag and the box on the window's Preferences screen give the same answer to
// the same text - docs/STABILITY-REVIEW-2026-10-26.md section 10.8.
//
// The help is a fixed sentence with no number from this machine in it. The
// site prints the help of every command, and a number read off the machine
// that built the site would be a number about nobody's machine. The floor of
// the plan under auto is a number of the program rather than of the machine,
// so it is said, from the constant that sets it.

// memoryHelp is what --help says about the flag.
var memoryHelp = "how much memory this run may use: auto (half of what this machine gives the program), " +
"a size such as 8gb, or off for no limit. Pictures wait for room rather than all being held at once, " +
"and a plan larger than this is refused before anything is written. " +
"Under auto, a plan of up to " + core.HumanBytes(core.MaxPlanBytes) + " is never refused"

// memoryValue is the flag's value, auto until somebody writes one.
type memoryValue struct{ core.Memory }

func (m *memoryValue) String() string {
if m == nil {
return core.Memory{}.String()
}
return m.Memory.String()
}

func (m *memoryValue) Set(text string) error {
parsed, err := core.ParseMemory(text)
if err != nil {
return err
}
m.Memory = parsed
return nil
}

// memoryFlag puts --memory on a command, its value landing in v.
func memoryFlag(fs *flag.FlagSet, v *memoryValue) {
fs.Var(v, "memory", memoryHelp)
}
13 changes: 11 additions & 2 deletions internal/cli/preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type budget struct {
Formats []string `json:"formats"`
}

func budgetOf(ctx context.Context, e *preset.Expansion) (budget, error) {
func budgetOf(ctx context.Context, e *preset.Expansion, memory core.Memory) (budget, error) {
rec, err := recipe.Parse(e.Source, e.Preset.ID)
if err != nil {
return budget{}, err
Expand All @@ -77,7 +77,7 @@ func budgetOf(ctx context.Context, e *preset.Expansion) (budget, error) {
targets = append(targets, engineTarget(t, t.Label))
seen[t.Format] = true
}
planned, err := engine.PlanContext(ctx, targets, engine.Options{OutDir: rec.Output.Dir, Seed: rec.Seed})
planned, err := engine.PlanContext(ctx, targets, engine.Options{OutDir: rec.Output.Dir, Seed: rec.Seed, Memory: memory})
if err != nil {
return budget{}, err
}
Expand Down Expand Up @@ -399,3 +399,12 @@ func targetsFromPreset(fs *flag.FlagSet, g *generateOpts, given map[string]bool,
opt.Preset = record(expanded)
return targetsFromParsedRecipe(rec, hash, g, given, opt), ExitOK
}

// saidAll is sentences as the command line prints them.
func saidAll(said []core.Said) []string {
out := make([]string, 0, len(said))
for _, s := range said {
out = append(out, s.String())
}
return out
}
10 changes: 9 additions & 1 deletion internal/cli/presetcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,17 +188,25 @@ Usage:
tfg preset show size-boundaries
tfg preset show size-boundaries --limit 20mb --format png
tfg preset show size-boundaries --json

Flags, beside the preset's own parameters:
-json
write the answer as JSON to standard output
-memory value
`+memoryHelp+` (default auto)
`)
}
var asJSON bool
var memory memoryValue
expanded, code := presetFlagSet("show", args, out, errOut, usage, func(fs *flag.FlagSet) {
fs.BoolVar(&asJSON, "json", false, "write the answer as JSON to standard output")
memoryFlag(fs, &memory)
})
if expanded == nil {
return code
}

b, err := budgetOf(ctx, expanded)
b, err := budgetOf(ctx, expanded, memory.Memory)
if err != nil {
fmt.Fprintf(errOut, "tfg: %s\n", describeError(err))
return classify(err)
Expand Down
33 changes: 25 additions & 8 deletions internal/cli/recipecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,34 @@ func loadRecipe(path string, errOut io.Writer) (*preset.Read, string, int) {
return read, hash, ExitOK
}

// validate runs the checks a run would run and writes nothing at all, so it
// suits a pre commit hook.
func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
// validateFlags is what validate is told besides the recipe.
type validateFlags struct {
asJSON bool
memory memoryValue
}

// validateFlagSet is the flags of validate and its help, the way
// generateFlagSet is for generate.
func validateFlagSet(errOut io.Writer, v *validateFlags) (*flag.FlagSet, func(io.Writer)) {
fs := flag.NewFlagSet("validate", flag.ContinueOnError)
fs.SetOutput(errOut)
asJSON := fs.Bool("json", false, "write the result as JSON, with every problem separately")
fs.BoolVar(&v.asJSON, "json", false, "write the result as JSON, with every problem separately")
memoryFlag(fs, &v.memory)
usage := func(w io.Writer) {
fmt.Fprint(w, "tfg validate - check a recipe and write nothing.\n\nUsage:\n tfg validate <recipe.yaml>\n\nFlags:\n")
fs.SetOutput(w)
fs.PrintDefaults()
fs.SetOutput(errOut)
}
fs.Usage = func() { usage(errOut) }
return fs, usage
}

// validate runs the checks a run would run and writes nothing at all, so it
// suits a pre commit hook.
func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
var v validateFlags
fs, usage := validateFlagSet(errOut, &v)
if helpRequested(args) {
usage(out)
return ExitOK
Expand All @@ -77,7 +92,7 @@ func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
return ExitUsage
}

read, hash, code := loadRecipeReporting(path, *asJSON, errOut)
read, hash, code := loadRecipeReporting(path, v.asJSON, errOut)
if code != ExitOK {
return code
}
Expand All @@ -92,17 +107,19 @@ func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
for _, t := range rec.Targets {
targets = append(targets, engineTarget(t, t.Label))
}
planned, err := engine.PlanContext(ctx, targets, planningOptions(rec))
opt := planningOptions(rec)
opt.Memory = v.memory.Memory
planned, err := engine.PlanContext(ctx, targets, opt)
if err != nil {
return planningRefusal(err, path, *asJSON, errOut)
return planningRefusal(err, path, v.asJSON, errOut)
}

// A file that builds on a preset says so here the way the manifest will
// say it, and its notes go where they go on a run: to a person, on
// standard error, before the files exist as well as after.
sayNotes(read.Notes(), errOut)

if *asJSON {
if v.asJSON {
return writeJSON(out, errOut, validateReport{
Recipe: path, Valid: true, RecipeHash: hash,
Targets: len(rec.Targets), Files: len(planned),
Expand Down
Loading
Loading