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

### Fixed

- **A damaged file no longer claims to carry its label.** Its manifest entry
said `label_embedded: true`, although the damage comes after the label is
written and can overwrite it. On a text file, `zero-head` zeroes the start of
the label itself. A damaged file now says `false`, in its `properties` too,
with a `label_not_guaranteed` note saying why. An undamaged file is
described as before, and `manifest_version` does not change.
- **A recipe asking for more than a million files in all is refused before
it takes the memory.** Targets were held to a million each and the run only
once all their sizes were laid out, so `tfg validate` took 2.5 GB to refuse
Expand Down
4 changes: 3 additions & 1 deletion internal/damage/damage.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
// What this package does NOT do is decide whether a damaged file is worth
// writing. That is the witness rule - a pair with no judge able to refuse the
// result is a pair this tool does not offer - and it belongs with the oracles,
// because it is a measurement rather than a declaration.
// because it is a measurement rather than a declaration. The guard
// TestEveryDamageIsRefusedByTheStructuralCheckInEveryFormat takes that
// measurement for every format and every damage registered here.
package damage

import (
Expand Down
18 changes: 17 additions & 1 deletion internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io/fs"
"maps"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -728,6 +729,21 @@ func (res *Result) entryFor(f PlannedFile, sha string, materialized bool, failur
}

label, _ := f.Plan.Properties[format.PropertyLabelEmbedded].(bool)
props := f.Plan.Properties
if label && f.Damaged() {
// The generator wrote the label and a damage came after it. Where the
// label sits differs by format - the first line of a text, pixels in
// a picture, a part inside a package - so whether a damage reached it
// cannot be said once for all of them. Measured 2026-11-09: zero-head
// on txt overwrote the label it sat beside, and the manifest still
// claimed it. So a damaged file never claims its label, and the note
// says why (docs/DAMAGE-ETAP-2-2026-11-09.md section 4.7).
label = false
props = maps.Clone(props)
props[format.PropertyLabelEmbedded] = false
notes = append(notes, res.note("label_not_guaranteed",
core.Says("engine.LabelNotGuaranteed", "The label was written before the file was damaged, so the damage may have overwritten it.")))
}

e := manifest.File{
ID: f.ID,
Expand All @@ -741,7 +757,7 @@ func (res *Result) entryFor(f PlannedFile, sha string, materialized bool, failur
Seed: core.SeedLabel(f.Seed),
Generator: manifest.GeneratorRef{Name: f.Desc.ID, Version: f.Desc.GeneratorVersion},
Determinism: string(f.Plan.Determinism),
Properties: f.Plan.Properties,
Properties: props,
LabelEmbedded: label,
Damage: damageFor(f),
Notes: notes,
Expand Down
82 changes: 82 additions & 0 deletions internal/guard/damagelabel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package guard

import (
"path/filepath"
"slices"
"testing"

"github.com/donislawdev/TestingFilesGenerator/internal/damage"
"github.com/donislawdev/TestingFilesGenerator/internal/engine"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
"github.com/donislawdev/TestingFilesGenerator/internal/manifest"
)

// labelNotGuaranteed is the note a damaged file carries in place of a claim
// to its label.
const labelNotGuaranteed = "label_not_guaranteed"

// TestADamagedFileNeverClaimsItsLabel holds the manifest to what it can know
// about a label after a damage.
//
// Measured 2026-11-09: zero-head on txt overwrote the label at the start of
// the text, and the manifest still said label_embedded: true. Where a label
// sits differs by format, so the rule is the same for all of them. A damaged
// file says false, in both places the manifest says it, and a note says why.
//
// Every format, each beside a healthy file of the same format and size. The
// healthy one is the control: a format that never claims its label would pass
// this guard without the rule being there, so the guard also counts the
// formats where the claim was really taken away.
func TestADamagedFileNeverClaimsItsLabel(t *testing.T) {
dir := t.TempDir()
chain := damage.Chain{{ID: damage.ZeroHead}}

withdrawn := 0
for _, f := range format.All() {
out := filepath.Join(dir, f.ID)
targets := []engine.Target{
{ID: "good", Format: f.ID, Sizes: engine.Uniform(1, witnessSize), Label: true},
{ID: "broken", Format: f.ID, Sizes: engine.Uniform(1, witnessSize), Damage: chain, Label: true},
}
opt := engine.Options{OutDir: out, Seed: 5, ManifestName: engine.DefaultManifestName}
planned, err := engine.Plan(targets, opt)
if err != nil {
t.Fatalf("%s: planning: %v", f.ID, err)
}
res, err := engine.Run(t.Context(), planned, opt)
if err != nil {
t.Fatalf("%s: running: %v", f.ID, err)
}
entries := map[string]manifest.File{}
for _, m := range res.Manifest.Files {
entries[m.TargetID] = m
}
good, broken := entries["good"], entries["broken"]
if good.Name == "" || broken.Name == "" {
t.Fatalf("%s: the run did not write both files", f.ID)
}

if broken.LabelEmbedded {
t.Errorf("%s: a damaged file claims label_embedded: true.\n"+
"Reason: the damage came after the label was written and may have overwritten it,\n"+
"so the manifest cannot know the label is still there.", f.ID)
}
if claim, ok := broken.Properties[format.PropertyLabelEmbedded].(bool); ok && claim {
t.Errorf("%s: a damaged file claims label_embedded: true in its properties, "+
"while the field beside them says false", f.ID)
}
noted := slices.ContainsFunc(broken.Notes, func(n manifest.Note) bool { return n.Code == labelNotGuaranteed })
if noted != good.LabelEmbedded {
t.Errorf("%s: the healthy file claims its label: %t, and the damaged one carries the note %s: %t.\n"+
"Reason: the note says why a label that was written is no longer claimed, so it\n"+
"belongs exactly where a claim was taken away.", f.ID, good.LabelEmbedded, labelNotGuaranteed, noted)
}
if good.LabelEmbedded {
withdrawn++
}
}
if withdrawn == 0 {
t.Fatal("no healthy file claimed its label, so this guard took nothing away and proved nothing")
}
t.Logf("the claim was taken away in %d format(s)", withdrawn)
}
65 changes: 3 additions & 62 deletions internal/guard/damagerefused_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,53 +13,14 @@ import (
"github.com/donislawdev/TestingFilesGenerator/internal/cli"
"github.com/donislawdev/TestingFilesGenerator/internal/damage"
"github.com/donislawdev/TestingFilesGenerator/internal/engine"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
_ "github.com/donislawdev/TestingFilesGenerator/internal/format/all"
"github.com/donislawdev/TestingFilesGenerator/internal/manifest"
"github.com/donislawdev/TestingFilesGenerator/internal/oracle"
"github.com/donislawdev/TestingFilesGenerator/internal/recipe"
)

// A damaged file has to be REFUSED, which is the reverse of everything else in
// this package.
//
// Every other oracle guard here asks "does a judge accept what we wrote". This
// one asks the opposite, and it is the whole reason the damage axis is worth
// having: a broken file no reader complains about is a file nobody can write a
// test against, so it is a file this tool has no business producing.
//
// The formats are the text ones plus png, because those are the ones whose
// judges run without an external tool being installed. That is a limit of what
// can be asked on any machine rather than of the damage: the full sweep across
// all twenty four lives in tools/probes/corruptwitness, which is where the
// witness matrix is measured.
func TestADamagedFileIsRefusedByAJudge(t *testing.T) {
dir := t.TempDir()

for _, id := range []string{"txt", "md", "json", "xml", "csv", "html", "png"} {
t.Run(id, func(t *testing.T) {
good, broken := writeGoodAndBroken(t, dir, id)

// The control first. Without it this test passes for a build whose
// judge refuses everything, which would be the loudest possible
// way of proving nothing.
if res := oracle.Strict(id, good); res.Available && res.Err != nil {
t.Fatalf("the undamaged file was refused, so this row says nothing about damage: %v\n%s",
res.Err, res.Output)
}

res := oracle.Strict(id, broken)
if !res.Available {
t.Skipf("no structural check for %s on this machine", id)
}
if res.Err == nil {
t.Errorf("the damaged file was ACCEPTED. A broken file every reader takes is one "+
"nobody can write a test against, and the manifest calls it expected: reject.\n%s",
res.Output)
}
})
}
}
// Whether a damaged file is refused by a judge is asked of every format and
// every damage in damagewitness_test.go, which replaced the seven formats
// asked here until 2026-11-09.

// The manifest of a damaged run says what was broken and expects a rejection.
//
Expand Down Expand Up @@ -157,26 +118,6 @@ type discard struct{}

func (discard) Write(p []byte) (int, error) { return len(p), nil }

// writeGoodAndBroken produces one file of this format twice, once whole and
// once damaged, and returns both paths.
func writeGoodAndBroken(t *testing.T, dir, id string) (good, broken string) {
t.Helper()

d, err := format.Get(id)
if err != nil {
t.Fatal(err)
}
size := d.SmallestAccepted(format.Request{}) + 4096

goodDir := filepath.Join(dir, id+"-good")
brokenDir := filepath.Join(dir, id+"-broken")
goodMan := runOne(t, goodDir, id, size, nil)
brokenMan := runOne(t, brokenDir, id, size, damage.Chain{{ID: damage.ZeroHead}})

return filepath.Join(goodDir, goodMan.Files[0].Name),
filepath.Join(brokenDir, brokenMan.Files[0].Name)
}

// runDamaged is one png of a comfortable size, with the chain given.
func runDamaged(t *testing.T, dir, id string, chain damage.Chain) *manifest.Manifest {
t.Helper()
Expand Down
Loading
Loading