|
| 1 | +package guard |
| 2 | + |
| 3 | +import ( |
| 4 | + "strings" |
| 5 | + "testing" |
| 6 | + |
| 7 | + "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" |
| 8 | + "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" |
| 9 | + "github.com/donislawdev/TestingFilesGenerator/internal/manifest" |
| 10 | +) |
| 11 | + |
| 12 | +// The window says the same thing the command line says about a record too big |
| 13 | +// to read back. |
| 14 | +// |
| 15 | +// Observation O184, measured on 2026-09-06: TooLargeToReadBack had two callers, |
| 16 | +// both in internal/cli and internal/manifest, and NOT ONE in internal/gui. A |
| 17 | +// person who generated 25 000 files from the window was told nothing at all, |
| 18 | +// and was left with a directory that tfg verify and tfg cleanup both refuse - |
| 19 | +// the manifest is the only authority over what may be removed, so a manifest |
| 20 | +// that cannot be read is a set of files with no owner. |
| 21 | +// |
| 22 | +// It is the kind of parity gap D1 loses most easily. Not something the engine |
| 23 | +// can do from one surface and not the other, which is what the parity guard |
| 24 | +// looks for, but something one surface SAYS and the other does not. |
| 25 | +// |
| 26 | +// The note was written down as a question about the manifest schema, on the |
| 27 | +// grounds that notes are per file and this one is per run. It is not. The |
| 28 | +// command line does not read this off the manifest either - it works it out |
| 29 | +// from the plan and prints it before the first byte - and manifest.TooLarge- |
| 30 | +// ToReadBack was put where it is exactly so the two surfaces could not come to |
| 31 | +// different conclusions about one run. What was missing was a caller. |
| 32 | + |
| 33 | +// overTheCeiling is a file count whose manifest this build would refuse. |
| 34 | +// |
| 35 | +// Worked out from the estimate rather than written here, for the reason the |
| 36 | +// command line guard beside it gives: a guard carrying its own copy of a limit |
| 37 | +// goes stale the day somebody changes the real one, and says nothing while it |
| 38 | +// does. |
| 39 | +func overTheCeiling(t *testing.T) int { |
| 40 | + t.Helper() |
| 41 | + over := int(manifest.MaxBytes/manifest.BytesPerEntry) + 1000 |
| 42 | + if _, tooBig := manifest.TooLargeToReadBack(over, 0); !tooBig { |
| 43 | + t.Fatalf("%d entries was not judged too large, so this guard would prove nothing", over) |
| 44 | + } |
| 45 | + return over |
| 46 | +} |
| 47 | + |
| 48 | +// previewOf presses Preview for a run of count files and gives back what the |
| 49 | +// screen said. |
| 50 | +// |
| 51 | +// It REFUSES to return a refusal, and that is the whole reason it exists. The |
| 52 | +// first version of the pair below set a size the default format will not take - |
| 53 | +// the window opens on the first format in the registry, which is avif, and |
| 54 | +// 200 B is far under what a picture needs. Both previews were turned down, so |
| 55 | +// the negative half passed while proving nothing: a screen that says "check the |
| 56 | +// settings marked above" says nothing about a manifest ceiling either. |
| 57 | +func previewOf(t *testing.T, count string) string { |
| 58 | + t.Helper() |
| 59 | + content, w, host := screenInAWindowWithHost(t, text.TabOneTarget()) |
| 60 | + |
| 61 | + // txt rather than whatever the window opens on, for two reasons. The size |
| 62 | + // below has to be one the format takes, and planning twenty two thousand |
| 63 | + // pictures would encode twenty two thousand pictures - png, jpg, gif and |
| 64 | + // avif all do that while planning. |
| 65 | + picker, ok := controlUnder(content, text.FieldFormat()).(*parts.Chooser) |
| 66 | + if !ok { |
| 67 | + t.Fatal("the format field is not a list to choose from, so this guard read the wrong tree") |
| 68 | + } |
| 69 | + picker.SetSelected("txt") |
| 70 | + |
| 71 | + // Small files, because what is being asked about is the number of ENTRIES |
| 72 | + // rather than the number of bytes. A preview writes nothing either way. |
| 73 | + fill(t, content, text.FieldSize(), "200b") |
| 74 | + fill(t, content, text.FieldCount(), count) |
| 75 | + |
| 76 | + press(t, content, text.ButtonPreview()) |
| 77 | + // This preview is accepted, so it answers from a worker. Joined before the |
| 78 | + // status line is read - see join. |
| 79 | + join(host) |
| 80 | + settle(content, w) |
| 81 | + |
| 82 | + _, status := runMessages(content) |
| 83 | + if status == nil { |
| 84 | + t.Fatal("the screen has no status line, so this guard read the wrong tree") |
| 85 | + } |
| 86 | + // Matched on the tail of the preview's own sentence, the way the action bar |
| 87 | + // guard does it, so this cannot be satisfied by a refusal. |
| 88 | + marker := text.PreviewCost(1, nil, "1 B") |
| 89 | + tail := marker[strings.LastIndex(marker, " ")+1:] |
| 90 | + if !strings.Contains(status.Text, tail) { |
| 91 | + t.Fatalf("the preview of %s files was not accepted, so nothing here was asked about the manifest.\nIt said:\n%s", |
| 92 | + count, status.Text) |
| 93 | + } |
| 94 | + return status.Text |
| 95 | +} |
| 96 | + |
| 97 | +// runOf presses Generate rather than Preview, and gives back what the screen |
| 98 | +// said when it finished. |
| 99 | +// |
| 100 | +// It exists because the preview is OPTIONAL. Somebody who presses Generate |
| 101 | +// straight away never sees the preview's answer, and that person is exactly the |
| 102 | +// one observation O184 is about - they end up with a directory nothing in this |
| 103 | +// toolset can read or clean. The window cannot say anything in the middle of a |
| 104 | +// run, so the end of the run is the only place left. |
| 105 | +// |
| 106 | +// It writes files, which is why this is the one guard here that does. Twenty |
| 107 | +// two thousand of them at 200 B, into a directory that goes away with the test. |
| 108 | +func runOf(t *testing.T, count string) string { |
| 109 | + t.Helper() |
| 110 | + content, w, host := screenInAWindowWithHost(t, text.TabOneTarget()) |
| 111 | + |
| 112 | + picker, ok := controlUnder(content, text.FieldFormat()).(*parts.Chooser) |
| 113 | + if !ok { |
| 114 | + t.Fatal("the format field is not a list to choose from, so this guard read the wrong tree") |
| 115 | + } |
| 116 | + picker.SetSelected("txt") |
| 117 | + fill(t, content, text.FieldSize(), "200b") |
| 118 | + fill(t, content, text.FieldCount(), count) |
| 119 | + fill(t, content, text.FieldOutputDir(), t.TempDir()) |
| 120 | + |
| 121 | + press(t, content, text.ButtonGenerate()) |
| 122 | + join(host) |
| 123 | + settle(content, w) |
| 124 | + |
| 125 | + _, status := runMessages(content) |
| 126 | + if status == nil { |
| 127 | + t.Fatal("the screen has no status line, so this guard read the wrong tree") |
| 128 | + } |
| 129 | + // The run has to have HAPPENED. A refused run says nothing about a |
| 130 | + // manifest either, and a guard that cannot tell those apart is the shape |
| 131 | + // this project has recorded as passing without reaching the code. |
| 132 | + if !strings.Contains(status.Text, text.Written(0)[strings.LastIndex(text.Written(0), " ")+1:]) { |
| 133 | + t.Fatalf("the run of %s files did not finish, so nothing here was asked about the manifest.\nIt said:\n%s", |
| 134 | + count, status.Text) |
| 135 | + } |
| 136 | + return status.Text |
| 137 | +} |
| 138 | + |
| 139 | +// warningAbout is the fixed half of the sentence, without the two numbers. |
| 140 | +// |
| 141 | +// Taken from the text package rather than typed here, so a reworded warning |
| 142 | +// does not quietly stop being checked. |
| 143 | +func warningAbout(t *testing.T) string { |
| 144 | + t.Helper() |
| 145 | + marker := text.ManifestTooLargeToRead("SIZE", "LIMIT") |
| 146 | + at := strings.Index(marker, "SIZE") |
| 147 | + if at < 0 { |
| 148 | + t.Fatal("the warning does not carry the size it was given, so this guard cannot find its fixed half") |
| 149 | + } |
| 150 | + return marker[:at] |
| 151 | +} |
| 152 | + |
| 153 | +func TestTheWindowSaysWhenItsManifestWillBeTooBigToReadBack(t *testing.T) { |
| 154 | + said := previewOf(t, itoa(overTheCeiling(t))) |
| 155 | + if !strings.Contains(said, warningAbout(t)) { |
| 156 | + t.Errorf("a preview of %d files said nothing about the record being too big to read back.\n"+ |
| 157 | + "The command line has said this since 2026-08-26. Somebody who does the same from the window "+ |
| 158 | + "gets a directory that neither Verify nor Clean up can read, and no warning.\nIt said:\n%s", |
| 159 | + overTheCeiling(t), said) |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +// And a run that was never previewed says it too, which is the case that |
| 164 | +// matters most. |
| 165 | +// |
| 166 | +// The preview is a button somebody may not press. The warning has to reach the |
| 167 | +// person who pressed Generate and nothing else, because they are the one left |
| 168 | +// with the directory. |
| 169 | +func TestAFinishedRunInTheWindowSaysItsManifestIsTooBigToReadBack(t *testing.T) { |
| 170 | + said := runOf(t, itoa(overTheCeiling(t))) |
| 171 | + if !strings.Contains(said, warningAbout(t)) { |
| 172 | + t.Errorf("a finished run of %d files said nothing about the record being too big to read back.\n"+ |
| 173 | + "That directory now has a manifest neither Verify nor Clean up can read, and nobody was told.\nIt said:\n%s", |
| 174 | + overTheCeiling(t), said) |
| 175 | + } |
| 176 | + // The line somebody pressed the button for stays first. The room for these |
| 177 | + // messages is a ceiling and the message scrolls inside it. |
| 178 | + if first := strings.SplitN(said, "\n", 2)[0]; strings.Contains(first, warningAbout(t)) { |
| 179 | + t.Errorf("the warning took the first line from the outcome:\n%s", said) |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +// A run this build CAN read back stays quiet. |
| 184 | +// |
| 185 | +// Without this the guard above passes on a window that warns about every run, |
| 186 | +// which teaches somebody to stop reading the line - the same reason the command |
| 187 | +// line has this pair rather than only the first half. |
| 188 | +func TestAnOrdinaryPreviewSaysNothingAboutTheManifestCeiling(t *testing.T) { |
| 189 | + said := previewOf(t, "100") |
| 190 | + if strings.Contains(said, warningAbout(t)) { |
| 191 | + t.Errorf("a hundred files drew the warning about the record being too big:\n%s", said) |
| 192 | + } |
| 193 | +} |
| 194 | + |
| 195 | +// Both surfaces judge the same run the same way. |
| 196 | +// |
| 197 | +// The window reads the answer off the document a dry run builds, and the |
| 198 | +// command line works it out from the plan before anything is written. Two paths |
| 199 | +// to one number, and what makes two paths acceptable is that they go through |
| 200 | +// one predicate. Asked at the boundary, which is the only place a disagreement |
| 201 | +// would show. |
| 202 | +func TestBothSurfacesJudgeTheSameRunTheSameWay(t *testing.T) { |
| 203 | + for _, entries := range []int{ |
| 204 | + int(manifest.MaxBytes / manifest.BytesPerEntry), |
| 205 | + int(manifest.MaxBytes/manifest.BytesPerEntry) + 1, |
| 206 | + } { |
| 207 | + _, fromThePlan := manifest.TooLargeToReadBack(entries, 0) |
| 208 | + |
| 209 | + m := manifest.New("testing-files-generator", "0.0.0-dev", "run_x", "tfg generate", 1, "linux", "amd64") |
| 210 | + for i := 0; i < entries; i++ { |
| 211 | + m.Add(manifest.File{Path: "f.txt", Materialized: true}) |
| 212 | + } |
| 213 | + _, fromTheDocument := m.ReadBackReach() |
| 214 | + |
| 215 | + if fromThePlan != fromTheDocument { |
| 216 | + t.Errorf("at %d entries the plan says %v and the document says %v.\n"+ |
| 217 | + "The command line answers from the first and the window from the second, so one run "+ |
| 218 | + "would be warned about on one surface and not on the other", |
| 219 | + entries, fromThePlan, fromTheDocument) |
| 220 | + } |
| 221 | + } |
| 222 | +} |
0 commit comments