diff --git a/.github/race-weights b/.github/race-weights index 79e2a3e1..d9578771 100644 --- a/.github/race-weights +++ b/.github/race-weights @@ -3,11 +3,14 @@ # heaviest first, and sends every test not named here by the checksum of its # name, counted at the "unlisted" weight. # -# Taken on 2026-11-03 from eight parts of two runs, each test at the longest -# time any of them took. The race job runs on whatever processor the runner -# is given, and the same part took 689 s on one and 1202 s on another, every -# test of it slower by the same factor. The longest time is the slowest -# processor's, so the parts are even where they are closest to the timeout. +# Taken on 2026-11-03 from eight parts of two runs, and on 2026-11-12 from the +# four parts of one more, each test at the longest time any of them took. The +# race job runs on whatever processor the runner is given, and the same part +# took 689 s on one and 1202 s on another, every test of it slower by the same +# factor. The longest time is the slowest processor's, so the parts are even +# where they are closest to the timeout. A time from a faster processor is +# scaled up by its part: on 2026-11-12 the listed tests of two parts on an AMD +# EPYC 9V74 took 1/1.20 and 1/1.15 of their weights. # Listed: every test that took 10 s or more. "unlisted" is the mean of the rest. # # A test that is not here and takes 60 s or more fails its part with the line @@ -16,50 +19,57 @@ unlisted 0.58 348.3 TestAFormatRefusesASizeItCannotDescribe 343.3 TestAFilmStoppedHalfWayLeavesNoHelperRunning 329.9 TestEveryFormatHitsTheOrderedSizeExactly +167.5 TestACompressedZipIsEitherWrittenAtItsSizeOrRefusedInWords 162.6 TestEveryScreenStillDrawsItsStoredPicture -155.5 TestACompressedZipIsEitherWrittenAtItsSizeOrRefusedInWords 142.2 TestEveryTileOfAFilmIsGav1dsCodingOfItsPixelsAndDecodesAsItDoesAlone +134.0 TestEveryAvifRungStillFitsInsideItsDeclaredCeilings 130.0 TestTheMinimumThisToolPrintsStandsWhateverTheSeedIs -117.9 TestFreeBoxesCarryPaddingOfAnyLength +124.3 TestFreeBoxesCarryPaddingOfAnyLength 99.9 TestEveryBase64TextSizeIsExact 93.8 TestAFilmPictureLargerThanAnyAV1LevelIsRefusedNotBroken -73.6 TestEveryAvifRungStillFitsInsideItsDeclaredCeilings -68.4 TestEveryFormatsMapIsTheOneItsStructureReadsBack -67.9 TestADeclaredDefaultIsTheOneTheFormatUses +84.3 TestEveryFormatsMapIsTheOneItsStructureReadsBack +68.2 TestADeclaredDefaultIsTheOneTheFormatUses +65.9 TestNoGeneratorHoldsTheWholeFileInMemory 65.6 TestADamagedFileIsExactlyTheSizeAskedFor -64.3 TestNoTrackedFileCarriesPrivateContent -63.8 TestNoGeneratorHoldsTheWholeFileInMemory -60.8 TestEveryDamageIsRefusedByTheStructuralCheckInEveryFormat +64.6 TestNoTrackedFileCarriesPrivateContent +62.2 TestEveryDamageIsRefusedByTheStructuralCheckInEveryFormat 57.3 TestAFilmSaysItCarriesItsLabelOnlyWhenAPictureShowsItWhole +55.6 TestEveryJxlRungStillFitsInsideItsDeclaredCeiling +55.6 TestTheArchiveScanLooksIntoTheArchivesInside 55.3 TestAFilmOfANamedSizeKeepsEveryPictureInsideItsReserve -53.4 TestTheArchiveScanLooksIntoTheArchivesInside -38.5 TestAContainerStopsBetweenTheFilesItPlans +52.2 TestChoosingAToolLaysTheScreenOutAsIfItOpenedWithIt +38.6 TestAContainerStopsBetweenTheFilesItPlans 37.5 TestEveryPictureNamedAfterARefusalShowsOne +36.7 TestJxlStillFitsWhenTheQualityIsNotTheOneTheCeilingsWereMeasuredAt 36.7 TestTheArchiveScanReadsTheListArchiveZipReads 35.9 TestTheArchiveScanFindsEveryTrapItNames -35.5 TestJxlStillFitsWhenTheQualityIsNotTheOneTheCeilingsWereMeasuredAt -30.8 TestOurOwnGeneratorsHaveNotDrifted +34.0 TestOurOwnGeneratorsHaveNotDrifted 29.9 TestTheSameSeedGivesTheSameBytes -26.8 TestEveryJxlRungStillFitsInsideItsDeclaredCeiling -26.6 TestTheMinimumStandsWhateverTheSeedIsUnderEveryDeclaredChoice +27.9 TestTheMinimumStandsWhateverTheSeedIsUnderEveryDeclaredChoice +25.9 TestEveryLinkOnTheSiteLeadsSomewhere 24.9 TestEveryEMLSettingSurvivesItsReferenceTool -24.2 TestEveryLinkOnTheSiteLeadsSomewhere 24.2 TestAPictureSaysItCarriesItsLabelOnlyWhenItsTextIsWhole +23.2 TestAnAVIFLargerThanOneAV1TileSurvivesItsReferenceTool +23.1 TestACutTakesTheShareOfTheContentItIsAskedFor +22.4 TestTheLadderCeilingIsAboveEveryPictureTheTopRungMakes +22.2 TestTheSiteSaysWhatTheToolSays 20.9 TestAFilmsProgressMovesWithItsPicturesNotItsPadding 20.7 TestAFilmHoldsTheFramesTheManifestDeclares 20.7 TestTheWindowBinaryStartsWithoutAConsole -19.9 TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan -19.9 TestTheSiteSaysWhatTheToolSays -19.8 TestChoosingAToolLaysTheScreenOutAsIfItOpenedWithIt +20.3 TestAFilmWhoseSquareCoversItsLabelSaysSoInItsPlan 19.8 TestTheArchiveScanHoldsNoMoreOfANameThanTheName +17.4 TestThePresetScreenKeepsTheNameAFileIsSentUnder +17.0 TestTheSmallestDamagedFileIsTakenAndOneByteLessIsNot +16.9 TestEveryFormatSurvivesItsReferenceTool +16.7 TestEveryLanguageDescribesEverythingTheProgramCanProduce 16.6 TestEveryLanguageDescribesEveryDamage 16.3 TestTheSameWorkDoneAgainLeavesTheWindowAsItWas 16.2 TestABuildWithNoWindowInItSaysSoAndKeepsStandardOutputEmpty -16.0 TestEveryLanguageDescribesEverythingTheProgramCanProduce 15.4 TestAvifStillFitsWhenTheQualityIsNotTheOneTheCeilingsWereMeasuredAt 15.1 TestABuildWithoutTheBuildTagsRefusesAndSaysWhy 14.9 TestAShortcutCannotStartASecondRunDuringTheFirst 14.6 TestTheLicenceCommandListsWhatTheBinaryLinks -14.4 TestEveryFormatSurvivesItsReferenceTool 12.2 TestTheMemoryFlagReachesThePlanOnEveryCommandThatPlans +12.1 TestTheManifestOfARunPastTheOldCeilingIsVerifiedAndCleanedUp +12.0 TestTheUploadSetSendsFourFilesUnderNamesThatLeaveTheDirectory 11.2 TestTheCommandLineHoldsTheWholeRunToItsMemorySetting diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ec616ab..a6481483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1116,14 +1116,16 @@ jobs: github.event_name == 'workflow_dispatch' || needs.touched.outputs.concurrency == 'true' runs-on: ubuntu-latest - # Four parts since 2026-09-24, the owner's decision. The whole package under + # Five parts, the owner's decision. In one process the whole package under # the detector took 32 to 42 minutes, and six of the twelve runs on # 2026-09-23 and 2026-09-24 were killed at the ceiling with no data race in # their logs. Both ceilings had been raised once already, on 2026-08-31 - # the job's from 30 to 45 minutes and Go's from 25 to 40 - for the same # reason, and a problem that keeps coming back is the wrong shape rather - # than a missing notch. The guards run one after another - none calls - # t.Parallel - so four processes share the time between them, and every + # than a missing notch. Four parts weighed 1074 s each by the weights of + # 2026-11-12, past 85% of their twenty minutes, and two of them took 1055 + # and 1067 s that day. The guards run one after another - none calls + # t.Parallel - so the processes share the time between them, and every # test still runs under the detector once, in one of them. # # Every part reports. One red part does not cancel the others, whose tests @@ -1139,7 +1141,7 @@ jobs: strategy: fail-fast: false matrix: - part: [0, 1, 2, 3] + part: [0, 1, 2, 3, 4] timeout-minutes: 35 env: # The one thing in this project that needs a C toolchain. Linux runners @@ -1227,8 +1229,8 @@ jobs: # and stays below the job's ceiling, so a slow part fails as a test with # a name rather than as a killed job with no output. Go's ten minutes # killed this job on 2026-08-25, before a timeout was stated here. - # By the weights the heaviest part is 961 s on the slowest processor - # measured, and twenty minutes is 1.25 times that. + # By the weights the heaviest part is 861 s on the slowest processor + # measured, and twenty minutes is 1.39 times that. # # The processor's counters and the clock are read just before the # tests, for the step after this one (O298). diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e6de164..3244b77b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,13 +73,19 @@ because it turns other people's test suites red. outcome is `unspecified`, offers both answers in the new `policy_options` field and says why. `tfg damage ` lists those formats. - **A file cut short says where the cut fell and how much of its content is - left.** In many formats a large file is mostly padding after its content, - so `--damage truncate:missing=90` on a 1 MB PNG still leaves the whole - picture. The `truncate` entry in the manifest's `damage` now carries `cut`: - `in` (`content`, `padding`, `structure` or `end`), `content_bytes` and - `content_kept_bytes`. A cut in the padding also gets the note - `cut_in_padding`. In a compressed tar.gz a cut in the padding is reported as - a cut in the content. + left.** In some formats a large file is mostly padding beside its content, + so a cut can take all of the content or none of it. The `truncate` entry in + the manifest's `damage` now carries `cut`: `in` (`content`, `padding`, + `structure` or `end`) and `content_kept_bytes`, and `content_bytes` where + the length of the content is known. A cut in the padding also gets the + note `cut_in_padding`. In a compressed tar.gz a cut in the padding is + reported as a cut in the content. +- **`truncate` cuts a PNG's picture by the percentage you set.** + `--damage truncate:missing=90` on a 1 MB PNG used to cut only padding and + leave the whole picture. Now it removes 90% of the picture's content as + well, so a viewer that shows what is left shows only the top of the + picture, the way an interrupted upload leaves it. Files nothing cuts are + unchanged. - **tfg is on npm, for tests in Node.** `npm install --save-dev testing-files-generator` gives Playwright, Vitest diff --git a/README.md b/README.md index 5860a58d..69f91854 100644 --- a/README.md +++ b/README.md @@ -522,11 +522,14 @@ how many more there are, and the manifest lists every one. The label is still written before the damage, so the damage can overwrite it, and the manifest does not promise it is there. -A cut falls on whatever the file holds at that point. A large picture, sound, -document or archive is mostly padding after its content, so `truncate` can leave -the content whole. The manifest says where each cut fell: the `truncate` entry -carries `cut`, with `in` (`content`, `padding`, `structure` or `end`), -`content_bytes` and `content_kept_bytes`. A cut in the padding also gets the note +In most formats `truncate` removes the same percentage of the content as of the +file: `missing=50` leaves about the top half of a PNG's picture, half of a WAV's sound +or half of a text's lines, the way an interrupted upload does. In some formats a +large file is mostly padding beside its content, so a cut there can take all of the +content or none of it. The manifest says where each cut fell: the `truncate` entry +carries `cut`, with `in` (`content`, `padding`, `structure` or `end`) and +`content_kept_bytes`, and with `content_bytes`, how long the content of the whole +file is, where that is known. A cut in the padding also gets the note `cut_in_padding`. The smallest file a damage can be given follows its settings, so the column is diff --git a/internal/damage/truncate.go b/internal/damage/truncate.go index a9dc1383..b387b8af 100644 --- a/internal/damage/truncate.go +++ b/internal/damage/truncate.go @@ -48,14 +48,18 @@ func init() { // anyone says it, and the sentence below names the percent. Min: fewestMissing, Max: mostMissing, Default: strconv.Itoa(defaultMissing), - // The last sentence is the limit a person cannot see from the - // outside: in 13 formats of 32 the padding stands after the - // content, so a large file cut at any share keeps its whole - // picture or sound (docs/TRUNCATE-2026-11-11.md). The manifest - // says per file where the cut fell. + // The last two sentences are what a person cannot see from the + // outside. In most formats the cut removes the same percentage + // of the picture, sound or text, png among them since it lays + // its padding out for the cut. In the rest the padding stands + // only on one side of the content, so a large file cut at any + // share keeps all of it or none (docs/TRUNCATE-2026-11-11.md + // section 7.1, and notYetCutAtTheShare in internal/guard for + // which). The manifest says per file where the cut fell. Detail: "How much of the whole file is gone, as a percentage. " + "At least this much is cut - a few bytes more where the format cannot end exactly there. " + - "A large file is often mostly padding after its content, so the content can stay whole - " + + "In most formats the cut removes the same percentage of the content. " + + "In some, a large file is mostly padding beside its content, so the cut can take all of the content or none - " + "the manifest says what each cut left.", }, }, diff --git a/internal/engine/damagedformat.go b/internal/engine/damagedformat.go index 36f8b70e..bd33bc47 100644 --- a/internal/engine/damagedformat.go +++ b/internal/engine/damagedformat.go @@ -100,9 +100,15 @@ func (g *damagedGenerator) PlanContext(ctx context.Context, r format.Request) (f // size bytes on disk, and answers the request it asked with. // // The label inside it states the file on disk, which is size - not the size -// the generator writes, nor one stretch moves it to. +// the generator writes, nor one stretch moves it to. Where a damage cuts the +// tail, the request says where, so a format can lay its content out for the +// cut. A size stretch moves up keeps the same cut, and so a smaller share of +// the content, which is the "at least" missing promises. func (g *damagedGenerator) whole(ctx context.Context, r format.Request, want, size int64) (format.Plan, format.Request, error) { r.Bytes, r.OnDisk = want, size + if _, kept, ok := g.chain.Cut(size); ok { + r.CutAt = kept + } p, err := g.plan(ctx, r) if err != nil && g.chain.Stretches() { p, err = g.stretch(ctx, r, err) diff --git a/internal/format/format.go b/internal/format/format.go index d209eabe..548e8b2d 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -680,6 +680,18 @@ type Request struct { // asks the generator for more than it keeps. Zero means Bytes is the size // on disk. Read for the label alone, like Damage. OnDisk int64 + // CutAt is where a damage will cut the file this request writes, as the + // number of its first bytes that stay, or zero when nothing cuts it. + // + // A format whose padding can stand on both sides of its content splits + // the padding so that the cut keeps the same share of the content as of + // the whole file, CutAt of Bytes. Otherwise a large file is mostly padding + // after its content, and a cut of any share leaves the content whole. + // Bytes is still the whole file and the plan still writes all of it, so + // the bytes after the cut are the ones the generator never gets to. A + // format that leaves this alone writes as it always did, and the cut the + // manifest reports says so (docs/TRUNCATE-2026-11-11.md section 7.2). + CutAt int64 // Properties are format specific settings, straight from the recipe. Properties map[string]string } diff --git a/internal/format/png/cut.go b/internal/format/png/cut.go new file mode 100644 index 00000000..28b9bc33 --- /dev/null +++ b/internal/format/png/cut.go @@ -0,0 +1,159 @@ +package png + +import ( + "context" + "fmt" + "io" + "math/bits" + + "github.com/donislawdev/TestingFilesGenerator/internal/core" + "github.com/donislawdev/TestingFilesGenerator/internal/format" +) + +// writeForACut writes a file a damage will cut after its first m.cutAt bytes, +// with the padding split around the picture so that the cut keeps the share of +// the picture it keeps of the whole file (format.Request.CutAt). +// +// With all of the padding behind the picture, as an uncut file has it, a large +// file cut at any share kept the whole picture and lost only padding. Before +// the picture and behind it are both places PNG allows an ancillary chunk - +// only the gap between two IDAT chunks is not. Pillow, FFmpeg and the Windows +// Imaging Component return the same pixels either way, and Tk and GDI+ read +// the picture (docs/TRUNCATE-2026-11-11.md section 7.3). +// +// The picture streams out as the encoder writes it, as in Write, so the cut +// stops the encoder where it falls instead of after the whole picture. The +// padding in front of it is sized from its length, so a plan that did not +// measure the picture measures it here first. That is the fast path in +// chooseSize, which only ever takes the top rung of the ladder, a couple of +// milliseconds to encode. +func writeForACut(ctx context.Context, w io.Writer, m memo, whole int64) error { + if !m.bodyKnown { + body, err := encodedBodySize(m) + if err != nil { + return err + } + m.body, m.bodyKnown = body, true + } + pad := whole - m.body - iendSize + if pad < chunkOverhead || pad-chunkOverhead > maxChunkData { + return core.Defect(fmt.Errorf("png: the picture encodes to %d B, which leaves %d B of padding for a %d B file", m.body, pad, whole)) + } + front := frontPadding(pad, m.body-headSize, m.cutAt, whole) + markAroundThePicture(w, m.body, front, pad-front) + + // Both padding chunks draw from the file's seed. Nobody reads padding, and + // the one behind the picture lies past any cut. + split := &afterTheHead{ctx: ctx, w: w, seed: m.seed, head: headSize, front: front} + holder := &tailHolder{w: split, keep: iendSize} + if err := encode(holder, m); err != nil { + return err + } + if holder.written != m.body { + return core.Defect(fmt.Errorf("png: the picture encoded to %d B where planning said %d B", holder.written, m.body)) + } + if string(holder.tail[4:8]) != "IEND" { + return core.Defect(fmt.Errorf("png: the encoded stream does not end with IEND")) + } + if back := pad - front; back > 0 { + if err := writePaddingChunk(ctx, w, paddingChunk, m.seed, back-chunkOverhead); err != nil { + return err + } + } + _, err := w.Write(holder.tail) + return err +} + +// afterTheHead passes the encoded picture on, and writes the padding that goes +// in front of the picture's data once the first head bytes - the signature and +// the header chunk - are out. +type afterTheHead struct { + ctx context.Context + w io.Writer + seed uint64 + // head is how many bytes are still to pass before the padding. + head int64 + // front is the padding, its chunk's header and checksum included, and 0 + // once it is written or when there is none. + front int64 +} + +func (a *afterTheHead) Write(p []byte) (int, error) { + n := 0 + if a.head > 0 { + k := min(a.head, int64(len(p))) + written, err := a.w.Write(p[:k]) + n, a.head = written, a.head-int64(written) + if err != nil || a.head > 0 { + return n, err + } + p = p[k:] + } + if a.front > 0 { + front := a.front + a.front = 0 + if err := writePaddingChunk(a.ctx, a.w, paddingChunk, a.seed, front-chunkOverhead); err != nil { + return n, err + } + } + if len(p) == 0 { + return n, nil + } + written, err := a.w.Write(p) + return n + written, err +} + +// frontPadding is how much of the padding, chunk headers included, goes in +// front of the picture, so that a cut after the first cutAt bytes of a whole +// file keeps content*cutAt/whole bytes of the picture's content. +// +// A chunk costs twelve bytes before any padding, so each side is nothing or at +// least that. Where the chunks cannot land exactly, the front grows, which +// moves the picture later and cuts more of it - the "at least" missing +// promises. The one way it can keep more is a tail too short for the content +// the cut should take: there all of the padding is in front, and the cut keeps +// up to a chunk's worth more than the share (docs/TRUNCATE-2026-11-11.md +// section 7.4). The manifest's cut reports what was kept either way. +func frontPadding(pad, content, cutAt, whole int64) int64 { + if pad == 0 || cutAt <= 0 || cutAt >= whole { + return 0 + } + // content*cutAt in 128 bits, because a file near the top of what one chunk + // carries times a cut near it passes an int64. The quotient fits, because + // cutAt is less than whole. + hi, lo := bits.Mul64(uint64(content), uint64(cutAt)) + kept, _ := bits.Div64(hi, lo, uint64(whole)) + front := cutAt - headSize - int64(kept) + switch { + case front <= 0: + return 0 + case front < chunkOverhead: + front = chunkOverhead + } + if back := pad - front; back < chunkOverhead { + // Behind the picture there is room for nothing or for less than a + // chunk, so the whole padding goes in front. + return pad + } + return front +} + +// markAroundThePicture marks the map of a file whose padding is split around +// its picture: the header, the padding in front, the picture, the padding +// behind and the closing chunk. A side with no padding has no chunk and no +// region. +func markAroundThePicture(w io.Writer, body, front, back int64) { + format.Mark(w, format.RoleStructure, 0, headSize) + at := int64(headSize) + if front > 0 { + format.Mark(w, format.RolePadding, at, front) + at += front + } + format.Mark(w, format.RoleContent, at, body-headSize) + at += body - headSize + if back > 0 { + format.Mark(w, format.RolePadding, at, back) + at += back + } + format.Mark(w, format.RoleEnd, at, iendSize) +} diff --git a/internal/format/png/png.go b/internal/format/png/png.go index 357e7987..935270ea 100644 --- a/internal/format/png/png.go +++ b/internal/format/png/png.go @@ -132,6 +132,10 @@ type memo struct { // exactly on the requested size. padData int64 withPad bool + // cutAt is where a damage will cut the file, zero when nothing does + // (format.Request.CutAt). writeForACut splits the padding around the + // picture for it. + cutAt int64 } // checkJointLimits asks the registry's own declaration about a pair of @@ -170,6 +174,7 @@ func (generator) Plan(r format.Request) (format.Plan, error) { if err != nil { return format.Plan{}, err } + m.cutAt = r.CutAt w, h := m.width, m.height body := m.body @@ -279,6 +284,9 @@ func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { return ctx.Err() default: } + if m.withPad && m.cutAt > 0 && m.cutAt < p.Bytes { + return writeForACut(ctx, w, m, p.Bytes) + } // The map, before the bytes wherever planning measured the picture. The // picture's chunks are the content, their headers and checksums with them. diff --git a/internal/format/registry.go b/internal/format/registry.go index 62679884..7af74b13 100644 --- a/internal/format/registry.go +++ b/internal/format/registry.go @@ -149,8 +149,9 @@ func RequestKey(id string, r Request) (string, bool) { var b strings.Builder fmt.Fprintf(&b, "%q %d %t %t %d", id, r.Bytes, r.SizeFromContents, r.Label, r.Seed) // The damage and the size on disk are words of the label, so they change - // its length and with it the smallest file that carries it. - fmt.Fprintf(&b, " %d %q", r.OnDisk, r.Damage) + // its length and with it the smallest file that carries it. Where the cut + // falls changes how a format lays the file out. + fmt.Fprintf(&b, " %d %q %d", r.OnDisk, r.Damage, r.CutAt) for _, name := range names { fmt.Fprintf(&b, " %q=%q", name, r.Properties[name]) } diff --git a/internal/guard/actionbarheight_test.go b/internal/guard/actionbarheight_test.go index 8f78256a..5d73bfbf 100644 --- a/internal/guard/actionbarheight_test.go +++ b/internal/guard/actionbarheight_test.go @@ -258,9 +258,9 @@ func screenInAWindow(t *testing.T, tab string) (fyne.CanvasObject, fyne.Window) func screenInAWindowWithHost(t *testing.T, tab string) (fyne.CanvasObject, fyne.Window, *fakeHost) { t.Helper() - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) host := newFakeHost(t) window.Open(host) diff --git a/internal/guard/actionbarpadding_test.go b/internal/guard/actionbarpadding_test.go index d2bd8087..44f32159 100644 --- a/internal/guard/actionbarpadding_test.go +++ b/internal/guard/actionbarpadding_test.go @@ -4,7 +4,6 @@ import ( "testing" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/widget" "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" @@ -25,9 +24,9 @@ import ( // Asserted as arithmetic on the part rather than as a number: what has to hold // is that the bar costs its content plus one inset, whatever the inset is. func TestTheActionBarCostsItsContentPlusOneInset(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) inside := widget.NewLabel("what a run has to say") bar := parts.ActionBar(nil, inside) @@ -50,9 +49,9 @@ func TestTheActionBarCostsItsContentPlusOneInset(t *testing.T) { // why the form does not jump when Generate is pressed, and taking it out is // the cheapest way to make a bar shorter - see O101 and O118. func TestTheActionBarStillReservesRoomForARun(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) // A HIDDEN child, which is the whole point: a hidden widget costs no height // in this toolkit, so the bar shrinks the moment a run stops talking and diff --git a/internal/guard/cutmanifest_test.go b/internal/guard/cutmanifest_test.go index cb4af7a2..6095025e 100644 --- a/internal/guard/cutmanifest_test.go +++ b/internal/guard/cutmanifest_test.go @@ -3,6 +3,7 @@ package guard import ( "fmt" "os" + "strconv" "strings" "testing" @@ -19,7 +20,8 @@ type cutCase struct { missing string // in is the role of the first byte cut away. in string - // kept says how much of the content is left: "all", "none" or "some". + // kept says how much of the content is left: "all", "none", "some" (a + // text, all of it on disk) or "share" (the share missing leaves). kept string // note is the word that tells the two notes of a cut in the padding // apart, empty when the cut gets none. @@ -32,18 +34,20 @@ type cutCase struct { // A file cut short says in its manifest where the cut fell and how much of // the content is left, and gets a note when the cut missed the content. // -// The three shapes a cut takes (docs/TRUNCATE-2026-11-11.md section 2). A PNG -// of 1 MiB is nearly all padding after its picture, so 90% missing still -// leaves the whole picture. An icon keeps its padding BEFORE its picture, so a -// cut there takes all of it. A text is its words, so the cut falls in them. -// Two cuts in a row end the file where the second one does, and the cut is -// said of that one. A damage that cuts nothing has no cut to report. +// The shapes a cut takes (docs/TRUNCATE-2026-11-11.md sections 2 and 7). A +// GIF of 1 MiB is nearly all padding after its picture, so 90% missing still +// leaves the whole picture. A PNG splits its padding around the picture for a +// cut, so the cut takes the share asked for. An icon keeps its padding BEFORE +// its picture, so a cut there takes all of it. A text is its words, so the cut +// falls in them. Two cuts in a row end the file where the second one does, and +// the cut is said of that one. A damage that cuts nothing has no cut to report. // // Canary: the check below must refuse a cut that names the wrong role, and // one that claims content it does not have. func TestACutFileSaysWhereTheCutFellAndWhatIsLeft(t *testing.T) { cases := []cutCase{ - {name: "picture", format: "png", size: 1 << 20, missing: "90", in: "padding", kept: "all", note: "after the content"}, + {name: "behind", format: "gif", size: 1 << 20, missing: "90", in: "padding", kept: "all", note: "after the content"}, + {name: "picture", format: "png", size: 1 << 20, missing: "90", in: "content", kept: "share"}, // At half: the picture of 256x256 takes about 260 KB at the end of the // file, and a tenth of a megabyte cut away falls inside it. {name: "icon", format: "ico", size: 1 << 20, missing: "50", in: "padding", kept: "none", note: "before the content"}, @@ -88,17 +92,17 @@ func TestACutFileSaysWhereTheCutFellAndWhatIsLeft(t *testing.T) { } } - // Canary: the picture's entry, told the cut fell in its content, and the + // Canary: the GIF's entry, told the cut fell in its content, and the // text's, told none of its content is left. - picture := byTarget["picture"] - if len(picture.Damage) == 1 && picture.Damage[0].Cut != nil { + behind := byTarget["behind"] + if len(behind.Damage) == 1 && behind.Damage[0].Cut != nil { wrong := cases[0] wrong.in = "content" - if len(cutProblems(wrong, picture)) == 0 { + if len(cutProblems(wrong, behind)) == 0 { t.Errorf("canary: a cut in the padding passed for a cut in the content") } } - text := cases[2] + text := cases[3] text.kept = "none" if len(cutProblems(text, byTarget["text"])) == 0 { t.Errorf("canary: a text that kept half its words passed for one that kept none") @@ -134,6 +138,11 @@ func cutProblems(c cutCase, f manifest.File) []string { out = append(out, fmt.Sprintf("%d B of content kept, not none", kept)) case c.kept == "some" && (kept != f.Bytes || whole <= kept): out = append(out, fmt.Sprintf("%d B of %d B of content kept, where a %d B file of nothing but content keeps all of it and loses the rest", kept, whole, f.Bytes)) + case c.kept == "share": + missing, _ := strconv.ParseInt(c.missing, 10, 64) + if problem := shareProblem(missing, f); problem != "" { + out = append(out, problem) + } } var said []string for _, n := range f.Notes { diff --git a/internal/guard/cutshare_test.go b/internal/guard/cutshare_test.go new file mode 100644 index 00000000..81d56409 --- /dev/null +++ b/internal/guard/cutshare_test.go @@ -0,0 +1,220 @@ +package guard + +import ( + "fmt" + "maps" + "slices" + "strconv" + "strings" + "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" +) + +// notYet is a format whose cut does not take the share of its content yet: +// why, and a file it misses the share in - its size and the share cut. +type notYet struct { + why string + size int64 + missing int64 +} + +// notYetCutAtTheShare is every format whose cut does not yet take the share +// of the content that missing asks for (docs/TRUNCATE-2026-11-11.md section +// 7.1, measured 2026-11-12). +// +// A ratchet rather than a list of exceptions: each format here is cut once, in +// the file named, and a format that takes the share there is red until it +// leaves the list, so the list cannot go stale in either direction. A format +// that leaves is held to every size and share, and so is a new format unless +// it is added here with its reason. +var notYetCutAtTheShare = map[string]notYet{ + "gif": {paddingBehind, 64 << 10, 50}, + "avif": {paddingBehind, 64 << 10, 50}, + "jxl": {paddingBehind, 64 << 10, 50}, + "mp4": {paddingBehind, 64 << 10, 50}, + "webm": {paddingBehind, 64 << 10, 50}, + "pdf": {paddingBehind, 64 << 10, 50}, + "zip": {paddingBehind, 64 << 10, 50}, + "docx": {paddingBehind, 64 << 10, 50}, + "xlsx": {paddingBehind, 64 << 10, 50}, + "pptx": {paddingBehind, 64 << 10, 50}, + "targz": {paddingBehind, 64 << 10, 50}, + "jpg": {paddingInFront, 64 << 10, 50}, + "ico": {paddingInFront, 64 << 10, 50}, + // Both take the share at 64 KiB and above. At 4 KiB what stands behind the + // picture is more than a small share, so a cut of 1% keeps all of it. + "webp": {smallBehind, 4 << 10, 1}, + "tiff": {smallBehind, 4 << 10, 1}, +} + +const ( + paddingBehind = "its padding stands only behind its content, so a large file cut at any share keeps all of it" + paddingInFront = "its padding stands only in front of its content, so a large file cut at any share keeps none of it" + smallBehind = "in a small file the padding or directory behind its content is more than a small share" +) + +// shareSlack is how far, in points of a percent, the share of the content a +// cut takes may be from the share missing asks for. Rounding to whole chunks, +// frames and records moves it by a few bytes, and a header or a short tail +// beside the content moves the share of the content from the share of the +// file by a little more in a small file. +const shareSlack = 1.0 + +// shareSizes and shareMissing are the files the sweep cuts of a format held to +// the share: two sizes, so a picture planned with and without measuring it is +// cut, and three shares. +// +// 64 KiB is already a file of mostly padding in every format that pads behind +// its content - png keeps a picture of about 6 KB in it. Measured 2026-11-12: +// each format listed for where its padding stands misses the share at 64 KiB +// as it does at 10 MiB, so a megabyte would cost the race job of CI about a +// minute and say nothing new. +var ( + shareSizes = []int64{4 << 10, 64 << 10} + shareMissing = []int64{1, 50, 90} +) + +// A cut takes the share of the file's content that missing asks for, in every +// format that does not say otherwise. +// +// The meaning of missing a person reads off the result: half of the picture, +// half of the sound, half of the words. A cut that falls in padding takes +// nothing a reader sees whatever the share, and in a large file most of the +// bytes can be padding (docs/TRUNCATE-2026-11-11.md section 1). The manifest's +// cut is the measurement, and the map behind it is held to the file's +// structure by TestEveryFormatsMapIsTheOneItsStructureReadsBack. +// +// Canaries: a cut that kept all its content must be reported, and so must a +// format listed as not yet cutting at the share that does. +func TestACutTakesTheShareOfTheContentItIsAskedFor(t *testing.T) { + missingOf := map[string]int64{} + var targets []engine.Target + for _, f := range format.All() { + for _, c := range shareCases(f.ID, notYetCutAtTheShare) { + chain := damage.Chain{{ID: damage.Truncate, Values: damage.Values{damage.SettingMissing: strconv.FormatInt(c[1], 10)}}} + if c[0] < engine.SmallestDamaged(f, chain, format.Request{Label: true}) { + continue + } + id := shareID(f.ID, c) + missingOf[id] = c[1] + targets = append(targets, engine.Target{ID: id, Format: f.ID, Sizes: engine.Uniform(1, c[0]), Label: true, Damage: chain}) + } + } + opt := engine.Options{OutDir: t.TempDir(), Seed: 7, ManifestName: engine.DefaultManifestName} + planned, err := engine.Plan(targets, opt) + if err != nil { + t.Fatalf("planning: %v", err) + } + res, err := engine.Run(t.Context(), planned, opt) + if err != nil { + t.Fatalf("running: %v", err) + } + + problems := map[string]string{} + for _, m := range res.Manifest.Files { + missing, ok := missingOf[m.TargetID] + if !ok || m.Failed { + t.Errorf("%s: no file was written (%s)", m.TargetID, m.Error) + continue + } + problems[m.TargetID] = shareProblem(missing, m) + } + for _, problem := range shareVerdicts(problems, notYetCutAtTheShare) { + t.Error(problem) + } + + // Canaries. + whole := int64(1000) + kept := manifest.File{Damage: []manifest.Damage{{Cut: &manifest.Cut{In: "content", ContentBytes: &whole, ContentKeptBytes: whole}}}} + if shareProblem(50, kept) == "" { + t.Errorf("canary: a cut that kept all its content passed for one that took half") + } + listed := maps.Clone(notYetCutAtTheShare) + listed["txt"] = notYet{paddingBehind, 64 << 10, 50} + if !slices.ContainsFunc(shareVerdicts(problems, listed), func(s string) bool { return strings.HasPrefix(s, "txt:") }) { + t.Errorf("canary: txt listed as not cutting at the share, which it does, was not reported") + } +} + +// shareCases is the files the sweep cuts of a format, each as its size and the +// share cut: every size and share, or the one file named for a format listed +// as not taking the share yet. +func shareCases(id string, notYetAt map[string]notYet) [][2]int64 { + if n, listed := notYetAt[id]; listed { + return [][2]int64{{n.size, n.missing}} + } + var out [][2]int64 + for _, size := range shareSizes { + for _, missing := range shareMissing { + out = append(out, [2]int64{size, missing}) + } + } + return out +} + +func shareID(format string, c [2]int64) string { + return fmt.Sprintf("%s-%d-%d", format, c[0], c[1]) +} + +// shareVerdicts is every problem with the sweep, from what each file cut got +// wrong by its id, empty when it took the share: a format that cut nothing, a +// format held to the share that missed it, and a format listed as not yet +// cutting at the share that took it. +func shareVerdicts(problems map[string]string, notYetAt map[string]notYet) []string { + var out []string + for _, f := range format.All() { + cut := 0 + var missed []string + for _, c := range shareCases(f.ID, notYetAt) { + id := shareID(f.ID, c) + problem, ok := problems[id] + if !ok { + continue + } + cut++ + if problem != "" { + missed = append(missed, id+": "+problem) + } + } + _, listed := notYetAt[f.ID] + switch { + case cut == 0: + out = append(out, f.ID+": no file of it was cut, so the share was never asked") + case listed && len(missed) == 0: + out = append(out, f.ID+": took the share in the file notYetCutAtTheShare says it misses it in - take it off the list") + case !listed: + out = append(out, missed...) + } + } + for id := range notYetAt { + if _, err := format.Get(id); err != nil { + out = append(out, id+": listed in notYetCutAtTheShare and not a format") + } + } + return out +} + +// shareProblem says how the cut of f misses the share missing asks for, or +// nothing when it takes it. +func shareProblem(missing int64, f manifest.File) string { + if len(f.Damage) != 1 || f.Damage[0].Cut == nil { + return fmt.Sprintf("no cut is reported: %+v", f.Damage) + } + cut := f.Damage[0].Cut + if cut.In != "content" { + return fmt.Sprintf("the cut fell in the %s", cut.In) + } + if cut.ContentBytes == nil || *cut.ContentBytes <= 0 { + return "the length of the content is not stated" + } + whole := *cut.ContentBytes + gone := 100 * float64(whole-cut.ContentKeptBytes) / float64(whole) + if gone < float64(missing)-shareSlack || gone > float64(missing)+shareSlack { + return fmt.Sprintf("%.2f%% of the content is gone, where %d%% was asked for", gone, missing) + } + return "" +} diff --git a/internal/guard/damagelength_test.go b/internal/guard/damagelength_test.go index 13b6991e..c76a59f2 100644 --- a/internal/guard/damagelength_test.go +++ b/internal/guard/damagelength_test.go @@ -193,17 +193,19 @@ func checkLength(t *testing.T, tg engine.Target, m manifest.File, path string, w // TestATruncatedFileIsTheStartOfTheWholeOne holds D11 for truncate, and the // early stop with it. // -// A truncated file is the first bytes of the whole file the same recipe would -// write at the size the cut starts from - byte for byte, in every format. That -// is what makes the damage reproducible from its recipe, and it is also what -// proves stopping the generator early changes nothing: a generator writes from -// the front, so the bytes before the stop are the bytes it would have written -// anyway. A generator that wrote differently once stopped would turn this red. +// A truncated file is the first bytes of the whole file its plan writes, byte +// for byte, in every format, and that whole is as long as the cut asks for. +// That is what proves stopping the generator early changes nothing: a +// generator writes from the front, so the bytes before the stop are the bytes +// it would have written anyway. A generator that wrote differently once +// stopped would turn this red. // -// The whole file comes from a second run with the same seed and target id, so -// both files are drawn from the same seed. A size the cut would have to move -// up to - a band the format cannot write - is skipped, and the guard asserts -// that most formats were compared. +// The whole of its OWN plan, not the file the recipe writes without the +// damage: a format may lay a file out for the cut it is told of, padding in +// front of its content as well as behind it (format.Request.CutAt, +// docs/TRUNCATE-2026-11-11.md section 7.2). The file nothing damages is held +// to its bytes by the golden guards. The cut is written by a run and the whole +// by a second plan of the same target, so the two also have to plan alike. func TestATruncatedFileIsTheStartOfTheWholeOne(t *testing.T) { dir := t.TempDir() chain := damage.Chain{{ID: damage.Truncate}} @@ -212,37 +214,71 @@ func TestATruncatedFileIsTheStartOfTheWholeOne(t *testing.T) { if err != nil { t.Fatal(err) } - compared := 0 for _, f := range format.All() { - // Without the label, because a cut file's label states the size it - // keeps and the damage, and the whole file's states neither - the - // two differ on purpose (TestADamagedFileStatesItsSizeOnDiskAndItsDamage), - // and what this guard holds is every byte under them. + // Without the label, so the bytes under it are compared rather than a + // line every damaged file words differently. cut := engine.Target{ID: "same", Format: f.ID, Sizes: engine.Uniform(1, size), Damage: chain} - full := engine.Target{ID: "same", Format: f.ID, Sizes: engine.Uniform(1, whole)} - if _, err := engine.Plan([]engine.Target{full}, engine.Options{OutDir: t.TempDir(), Seed: lengthSeed}); err != nil { - // A size this format cannot write exactly: the cut started from - // the next one it can, which is a different whole file. - continue - } - _, cutPath := lengthRun(t, filepath.Join(dir, f.ID, "cut"), []engine.Target{cut}) - _, fullPath := lengthRun(t, filepath.Join(dir, f.ID, "whole"), []engine.Target{full}) + _, cutPath := lengthRun(t, filepath.Join(dir, f.ID), []engine.Target{cut}) got, err := os.ReadFile(cutPath["same"]) if err != nil { t.Fatal(err) } - all, err := os.ReadFile(fullPath["same"]) - if err != nil { - t.Fatal(err) + if problem := startOfTheWhole(got, wholeOfItsPlan(t, cut), size, whole); problem != "" { + t.Errorf("%s: %s", f.ID, problem) } - if int64(len(all)) != whole || !bytes.Equal(got, all[:size]) { - t.Errorf("%s: the truncated file is not the first %d B of the %d B file the same recipe writes", f.ID, size, whole) + } + + // The check on files it has to refuse, and on the one it has to take. A + // cut that left nothing is a prefix of any file. + sample := []byte("whole whole whole whole ") + for _, c := range []struct { + name string + got []byte + whole int64 + }{ + {"a cut that left nothing", nil, 24}, + {"a byte changed under the cut", []byte("whole whxle "), 24}, + {"a whole shorter than the cut asks for", sample[:12], 25}, + } { + if startOfTheWhole(c.got, sample, 12, c.whole) == "" { + t.Errorf("canary: %s passed as the start of the whole file", c.name) } - compared++ } - if compared < len(format.All())*3/4 { - t.Errorf("only %d of %d formats were compared, so this guard says little", compared, len(format.All())) + if problem := startOfTheWhole(sample[:12], sample, 12, 24); problem != "" { + t.Errorf("canary: the start of the whole file was refused: %s", problem) + } +} + +// startOfTheWhole is what is wrong with got as a file cut to its first size +// bytes from all, a whole the cut asks to be at least whole bytes long, or "" +// when nothing is. +func startOfTheWhole(got, all []byte, size, whole int64) string { + switch { + case int64(len(got)) != size: + return fmt.Sprintf("the truncated file is %d B where the cut keeps %d B", len(got), size) + case !bytes.HasPrefix(all, got): + return fmt.Sprintf("the truncated file is not the first %d B of the %d B file its plan writes", size, len(all)) + case int64(len(all)) < whole: + // A format that cannot write the whole the cut asks for starts from + // the next size it can, so the whole is never shorter. + return fmt.Sprintf("its plan writes %d B where the cut asks for at least %d B", len(all), whole) + } + return "" +} + +// wholeOfItsPlan is the whole file a damaged target's plan writes, with no +// damage on it - what the engine hands the chain before the chain cuts it. +func wholeOfItsPlan(t *testing.T, tg engine.Target) []byte { + t.Helper() + planned, err := engine.Plan([]engine.Target{tg}, engine.Options{OutDir: t.TempDir(), Seed: lengthSeed}) + if err != nil || len(planned) != 1 { + t.Fatalf("%s: planning: %v", tg.Format, err) + } + var all bytes.Buffer + if err := planned[0].Desc.Generator.Write(t.Context(), &all, planned[0].Plan); err != nil { + t.Fatalf("%s: writing the whole: %v", tg.Format, err) } + return all.Bytes() } // TestAppendedJunkIsTheWholeFileAndThenTheSameBytesEveryTime holds D11 and diff --git a/internal/guard/detailpopup_test.go b/internal/guard/detailpopup_test.go index eaa6b763..e7b2f29c 100644 --- a/internal/guard/detailpopup_test.go +++ b/internal/guard/detailpopup_test.go @@ -36,8 +36,8 @@ import ( // well. Through the canvas both questions are asked at once, because a hover // aimed at a button that is not where the canvas draws it opens nothing. func TestTheLongerExplanationOpensWhenAsked(t *testing.T) { - app := test.NewApp() - defer test.NewApp() + app := newTestApp() + defer newTestApp() app.Settings().SetTheme(parts.Theme()) content, c := laidOutWindow(t) @@ -148,8 +148,8 @@ func drawnCentre(t *testing.T, o fyne.CanvasObject, label string) fyne.Position // it stayed green through both versions, and both were reported by somebody // moving a mouse. func TestTheExplanationNeverUsesTheOverlayLayer(t *testing.T) { - app := test.NewApp() - defer test.NewApp() + app := newTestApp() + defer newTestApp() app.Settings().SetTheme(parts.Theme()) content, c := laidOutWindow(t) @@ -245,8 +245,8 @@ func namedOnScreen(o fyne.CanvasObject) string { // on, and since 2026-09-24 that is the card the owner chose from three drawn // side by side (floatsAsACard) - both had been reported as a plain grey block. func TestTheExplanationFloatsOnTheSurfaceAnOpenListDoes(t *testing.T) { - app := test.NewApp() - defer test.NewApp() + app := newTestApp() + defer newTestApp() app.Settings().SetTheme(parts.Theme()) content, c := laidOutWindow(t) diff --git a/internal/guard/dropdown_test.go b/internal/guard/dropdown_test.go index 2bab554d..42194016 100644 --- a/internal/guard/dropdown_test.go +++ b/internal/guard/dropdown_test.go @@ -86,9 +86,9 @@ func TestALetterTypedAtTheOpenListMovesTheKeyboard(t *testing.T) { // exactly a menu without one, which this asserts rather than assumes. func aMenuWithoutAFilter(t *testing.T) (*parts.Chooser, fyne.Window) { t.Helper() - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) menu := parts.NewChooser([]string{"csv", "pdf", "png", "txt", "wav"}, nil) if menu.Filtered { diff --git a/internal/guard/filemap_test.go b/internal/guard/filemap_test.go index 60babd81..2975f9b3 100644 --- a/internal/guard/filemap_test.go +++ b/internal/guard/filemap_test.go @@ -21,6 +21,19 @@ import ( // line for png, and avif, jxl and the films take it at every size. const mapSize = 1 << 20 +// cutMapSize and cutMapSmall are the sizes a file laid out for a cut is +// written at. 64 KiB, for every setting, is above the 16 KiB line and mostly +// padding already - a megabyte cost the race job of CI about 16 s more for +// nothing new. 8 KiB, with the settings at their defaults, is under the line, +// so png lays out a picture it measured while planning, and has room for +// padding, which the smallest file has none of. The race job is at the edge of +// its timeout (TestTheRacePartsWeighAboutTheSame), which is why not both sizes +// for every setting. +const ( + cutMapSize = 64 << 10 + cutMapSmall = 8 << 10 +) + // mappedFile is one file written through a map recorder, with the map its // generator marked. type mappedFile struct { @@ -34,7 +47,9 @@ type mappedFile struct { // The map a generator marks while it writes is the map its file's structure // reads back, for every format, every value of every setting that picks a -// shape, at the smallest size and a large one. +// shape, at the smallest size and a large one as a file nothing cuts, and laid +// out for a cut half way and written whole (format.Request.CutAt), where a +// format may put padding on both sides of its content. // // The map is what the manifest's damage[].cut is worked out from, and the // offsets in it are the generator's own word (internal/format/layout.go). The @@ -66,7 +81,16 @@ func TestEveryFormatsMapIsTheOneItsStructureReadsBack(t *testing.T) { for _, from := range []int64{smallest, max(smallest, mapSize)} { size := healthySizeFrom(t, f, setting, from) name := fmt.Sprintf("%s-%d-%d", f.ID, n, size) - files = append(files, writeMapped(t, dir, f, name, setting, size)) + files = append(files, writeMapped(t, dir, f, name, setting, size, 0)) + } + cuts := []int64{cutMapSize} + if n == 0 { + cuts = append(cuts, cutMapSmall) + } + for _, from := range cuts { + cut := healthySizeFrom(t, f, setting, max(smallest, from)) + name := fmt.Sprintf("%s-%d-%d-cut", f.ID, n, cut) + files = append(files, writeMapped(t, dir, f, name, setting, cut, cut/2)) } } if len(files) == before { @@ -86,7 +110,7 @@ func TestEveryFormatsMapIsTheOneItsStructureReadsBack(t *testing.T) { t.Fatalf("reading the maps back: %v", err) } - padded, canaried := 0, false + padded, split, canaried := 0, 0, false for i, m := range files { if got[i].Error != "" { t.Errorf("%s: the structure gives no map: %s", m.name, got[i].Error) @@ -103,6 +127,9 @@ func TestEveryFormatsMapIsTheOneItsStructureReadsBack(t *testing.T) { if countRole(m.marked, format.RolePadding) > 0 { padded++ } + if paddingOnBothSides(m.marked) { + split++ + } if !canaried && len(m.marked) >= 2 { canaried = true moved := slices.Clone(m.marked) @@ -128,13 +155,17 @@ func TestEveryFormatsMapIsTheOneItsStructureReadsBack(t *testing.T) { if padded == 0 { t.Errorf("no file of %d had padding in its map, so the role a cut misses most was never asked", len(files)) } - t.Logf("%d files, %d with padding", len(files), padded) + if split == 0 { + t.Errorf("no file of %d had padding on both sides of its content, so a layout for a cut was never read back", len(files)) + } + t.Logf("%d files, %d with padding, %d of them on both sides of the content", len(files), padded, split) } -// writeMapped writes one healthy file of f through a map recorder. -func writeMapped(t *testing.T, dir string, f format.Descriptor, name string, setting map[string]string, size int64) mappedFile { +// writeMapped writes one healthy file of f through a map recorder, laid out +// for a cut at cutAt when that is not zero. +func writeMapped(t *testing.T, dir string, f format.Descriptor, name string, setting map[string]string, size, cutAt int64) mappedFile { t.Helper() - p, err := f.Generator.Plan(format.Request{Bytes: size, Label: true, Seed: 7, Properties: setting}) + p, err := f.Generator.Plan(format.Request{Bytes: size, Label: true, Seed: 7, CutAt: cutAt, Properties: setting}) if err != nil { t.Fatalf("%s: planning: %v", name, err) } @@ -235,3 +266,11 @@ func mapNames(m [][3]any) string { } return strings.Join(parts, " ") } + +// paddingOnBothSides says whether a map has padding before its content and +// after it, the layout a format writes for a cut (format.Request.CutAt). +func paddingOnBothSides(m [][3]any) bool { + at := slices.IndexFunc(m, func(r [3]any) bool { return r[0] == string(format.RoleContent) }) + isPadding := func(r [3]any) bool { return r[0] == string(format.RolePadding) } + return at > 0 && slices.ContainsFunc(m[:at], isPadding) && slices.ContainsFunc(m[at+1:], isPadding) +} diff --git a/internal/guard/formatlist_test.go b/internal/guard/formatlist_test.go index 998abc94..539662e9 100644 --- a/internal/guard/formatlist_test.go +++ b/internal/guard/formatlist_test.go @@ -317,9 +317,9 @@ func TestTheRowTheKeyboardIsOnIsAlwaysInSight(t *testing.T) { // leaving widget.List, and red on widget.List as well. A box holding no value // is real: a chooser starts that way (the catalogue has one of the formats). func TestEmptyingTheFilterOfAListWithNothingChosenDrawsEveryRowAgain(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) list := parts.NewOpenList(format.IDs(), "", func(string, bool) {}, func(bool) {}) list.KindOf = parts.KindOfFile diff --git a/internal/guard/formcolumns_test.go b/internal/guard/formcolumns_test.go index 9f603e77..8b7004f0 100644 --- a/internal/guard/formcolumns_test.go +++ b/internal/guard/formcolumns_test.go @@ -266,7 +266,7 @@ func TestARefusalStandsUnderItsRowAcrossIt(t *testing.T) { // pixel. Disabled, the menu keeps the toolkit's disabled arrow, so a menu // frozen for a run does not look live. func TestAMenuShowsItsArrowInTheAccent(t *testing.T) { - test.NewTempApp(t) + newTempTestApp(t) menu := parts.NewChooser([]string{"avif", "png"}, nil) if got := arrowColourOf(t, menu); got != theme.ColorNamePrimary { t.Errorf("a menu draws its arrow in %q, not in the accent", got) diff --git a/internal/guard/fyneclock_test.go b/internal/guard/fyneclock_test.go new file mode 100644 index 00000000..f0a1530f --- /dev/null +++ b/internal/guard/fyneclock_test.go @@ -0,0 +1,144 @@ +package guard + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/test" + "fyne.io/fyne/v2/theme" + + "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" +) + +// newTestApp is test.NewApp for a guard, with the toolkit's clock read before +// anything is drawn. Every guard that needs an application takes it here. +// +// Fyne stamps how long a widget's renderer lives from a sample of the clock it +// takes only when something is painted, and a renderer lives a minute +// (internal/cache in Fyne 2.8.2: setAlive, Clean, ValidDuration). After a +// guard of more than a minute that paints nothing - a film under the race +// detector took 102 s - every renderer the next guard makes is born expired. +// Its first Capture destroys them, paints renderers made again with nothing +// laid out, and the picture comes out blank (docs/REVIEW-236-2026-11-12.md). +// Capturing an empty canvas takes a fresh sample. +func newTestApp() fyne.App { + app := test.NewApp() + readFyneClock() + return app +} + +// newTempTestApp is test.NewTempApp with the clock read, as newTestApp is. +func newTempTestApp(t testing.TB) fyne.App { + app := test.NewTempApp(t) + readFyneClock() + return app +} + +// readFyneClock has Fyne sample its clock, the only way a test can: a capture +// calls the cache's clean-up, which takes the sample first. +func readFyneClock() { + test.NewCanvas().Capture() +} + +// TestAGuardThatDrawsAfterALongPauseStillDraws holds newTestApp to what it is +// for, in a process of its own with a renderer's life cut to FYNE_CACHE=300ms +// and a pause longer than that before anything is drawn - the state a long +// guard leaves the next one in. A box and a button drawn after test.NewApp +// alone have to come out blank, or the pause no longer reaches the state this +// is about. Drawn after newTestApp, they have to be there. +func TestAGuardThatDrawsAfterALongPauseStillDraws(t *testing.T) { + const name = "TestAGuardThatDrawsAfterALongPauseStillDraws" + if how := os.Getenv(pauseChild); how != "" { + drawAfterAPause(t, how) + return + } + for how, want := range map[string]string{"plain": "box drawn=false", "fresh": "box drawn=true"} { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + run := exec.CommandContext(ctx, os.Args[0], "-test.run=^"+name+"$", "-test.v", "-test.count=1") + run.Env = append(os.Environ(), pauseChild+"="+how, "FYNE_CACHE=300ms") + out, err := run.CombinedOutput() + cancel() + if err != nil || !strings.Contains(string(out), "--- PASS: "+name) { + t.Fatalf("the %s child did not pass (%v):\n%s", how, err, out) + } + if !strings.Contains(string(out), want) { + t.Errorf("drawn after a pause past a renderer's life with the %s application, the picture is not %s:\n%s", how, want, out) + } + } +} + +const pauseChild = "TFG_FYNE_PAUSE_CHILD" + +// drawAfterAPause is the child: nothing drawn since the process started, a +// pause past a renderer's life, then the box and button of +// TestAButtonBesideABoxIsDrawnAsTallAsTheBox, captured once. +func drawAfterAPause(t *testing.T, how string) { + t.Helper() + time.Sleep(400 * time.Millisecond) + var app fyne.App + if how == "fresh" { + app = newTestApp() + } else { + app = test.NewApp() + } + app.Settings().SetTheme(parts.Theme()) + e := parts.NewEntry() + e.SetText("/tfg/out") + boxed, _ := parts.WithRing(e) + b := parts.NewButton(parts.Secondary, "Choose...", func() {}) + w := test.NewTempWindow(t, container.NewWithoutLayout(boxed, b)) + height := boxed.MinSize().Height + boxed.Resize(fyne.NewSize(140, height)) + boxed.Move(fyne.NewPos(10, 10)) + b.Resize(fyne.NewSize(100, height)) + b.Move(fyne.NewPos(170, 10)) + w.Resize(fyne.NewSize(300, 60)) + picture := w.Canvas().Capture() + // The box apart from the button: the button is drawn either way, so a + // picture with anything in it says nothing (measured on 2026-11-12, a + // renderer born expired: the box 0 rows, the button 29). + page := parts.PaletteColour(theme.ColorNameBackground, theme.VariantDark) + box := false + for y := 0; y < picture.Bounds().Dy(); y++ { + box = box || !sameColour(picture.At(60, y), page) + } + t.Logf("box drawn=%v", box) +} + +// TestEveryGuardTakesItsApplicationWithTheClockRead holds every guard to +// newTestApp: a guard that calls test.NewApp itself draws blank whenever it +// runs after a long one, and only under the race detector, where guards are +// slow enough. +func TestEveryGuardTakesItsApplicationWithTheClockRead(t *testing.T) { + files, err := filepath.Glob("*_test.go") + if err != nil || len(files) < 100 { + t.Fatalf("found %d guard files (%v), so this read nothing", len(files), err) + } + for _, f := range files { + if f == "fyneclock_test.go" { + continue + } + for _, call := range rawAppCalls(readFile(t, f)) { + t.Errorf("%s calls %s - take the application from newTestApp or newTempTestApp", f, call) + } + } + if got := rawAppCalls("func f() {\n\tapp := test.NewApp()\n\tdefer test.NewTempApp(t)\n}\n"); len(got) != 2 { + t.Errorf("canary: a source with both calls was read as %v", got) + } +} + +var rawAppCall = regexp.MustCompile(`\btest\.(NewApp|NewTempApp)\(`) + +// rawAppCalls is every call in src that makes an application without reading +// the clock. +func rawAppCalls(src string) []string { + return rawAppCall.FindAllString(src, -1) +} diff --git a/internal/guard/leftedge_test.go b/internal/guard/leftedge_test.go index a2f06dba..d0b31bae 100644 --- a/internal/guard/leftedge_test.go +++ b/internal/guard/leftedge_test.go @@ -4,7 +4,6 @@ import ( "testing" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/test" "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" @@ -18,9 +17,9 @@ import ( // same sentence parts.Theme carries about pictures. See observation O121. func ourTheme(t *testing.T) { t.Helper() - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) } // Everything a person reads on a screen starts on one left edge. diff --git a/internal/guard/listedge_test.go b/internal/guard/listedge_test.go index 2879ed0c..8445e6ea 100644 --- a/internal/guard/listedge_test.go +++ b/internal/guard/listedge_test.go @@ -26,9 +26,9 @@ import ( // is. A screen where everything happens to fit proves nothing about a screen // where it does not. func TestAnOpenListStaysInsideTheWindow(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) values := format.IDs() if len(values) < 8 { @@ -94,9 +94,9 @@ func TestAnOpenListStaysInsideTheWindow(t *testing.T) { // resizing it alone left the list its full height and did nothing at all // (O113). func TestAnOpenListIsCutToTheRoomThereIs(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) values := format.IDs() if len(values) < 8 { @@ -158,9 +158,9 @@ func popUpIn(o fyne.CanvasObject) *widget.PopUp { // The two guards above open real lists and hold the emergency half - a box at // the foot still goes upward, and a cramped window still cuts the list. func TestAListOpensDownwardWheneverAFewRowsFitUnderTheBox(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) row := parts.ListRowHeight() const box = 31 diff --git a/internal/guard/livecheck_test.go b/internal/guard/livecheck_test.go index a56aef26..fbec31e0 100644 --- a/internal/guard/livecheck_test.go +++ b/internal/guard/livecheck_test.go @@ -7,7 +7,6 @@ import ( "testing" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/widget" "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" @@ -270,8 +269,8 @@ func TestTypingIsStillCheckedAfterARunHasFinished(t *testing.T) { // preset switch took 0.7 s at the first press and 6.1 s at the twentieth, // docs/GUI-MEMORY-2026-09-23.md section 2.2. func TestAControlRegisteredAgainReportsOnceUnderItsLatestAddress(t *testing.T) { - test.NewApp() - t.Cleanup(func() { test.NewApp() }) + newTestApp() + t.Cleanup(func() { newTestApp() }) box := parts.NewEntry() menu := parts.NewChooser([]string{"one", "two"}, nil) diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index cef2b58c..c6bbf000 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -360,8 +360,8 @@ ( "an Office package goes back to writing its sizes after the data instead of in the header", "internal/format/opc/opc.go", - "entry, err := zw.CreateRaw(head)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif withPadding {", - "entry, err := zw.CreateHeader(head)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif withPadding {", + "entry, err := zw.CreateRaw(head)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := zm.MarkEntry(", + "entry, err := zw.CreateHeader(head)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := zm.MarkEntry(", "TestEveryFormatSurvivesItsReferenceTool", ), ( @@ -3048,8 +3048,8 @@ # rests on, mutated. "the archive starts compressing, so its planned size stops being real", "internal/format/targz/size.go", - "gzip.NewWriterLevel(w, m.squeeze.Level)", - "gzip.NewWriterLevel(w, gzip.BestSpeed)", + "gzip.NewWriterLevel(sm, m.squeeze.Level)", + "gzip.NewWriterLevel(sm, gzip.BestSpeed)", "TestTarGzSizeArithmeticSurvivesEveryShape", ), ( @@ -7518,8 +7518,8 @@ ( "a JPEG XL rung declares a ceiling below what that rung actually codes to", "internal/format/jxl/jxl.go", - " {80, 60, 1267}, {40, 30, 731}, {20, 15, 452}, {8, 6, 222},", - " {80, 60, 100}, {40, 30, 731}, {20, 15, 452}, {8, 6, 222},", + " {80, 60, 1267, 1267}, {40, 30, 731, 731}, {20, 15, 452, 452}, {8, 6, 222, 222},", + " {80, 60, 100, 1267}, {40, 30, 731, 731}, {20, 15, 452, 452}, {8, 6, 222, 222},", "TestEveryJxlRungStillFitsInsideItsDeclaredCeiling", ), ( @@ -7609,8 +7609,8 @@ ( "asking for compression quietly stores everything and only grows the padding", "internal/format/targz/size.go", - "gzip.NewWriterLevel(w, m.squeeze.Level)", - "gzip.NewWriterLevel(w, m.squeeze.Level*0)", + "gzip.NewWriterLevel(sm, m.squeeze.Level)", + "gzip.NewWriterLevel(sm, m.squeeze.Level*0)", "TestAskingForCompressionActuallyCompresses", ), ( diff --git a/internal/guard/mutationcoverage_test.go b/internal/guard/mutationcoverage_test.go index a7025293..ac5250c8 100644 --- a/internal/guard/mutationcoverage_test.go +++ b/internal/guard/mutationcoverage_test.go @@ -151,9 +151,16 @@ var provenByProbe = map[string]string{ "the heaviest seeds: the first ceiling of 640x480 set back to 16389 (red, 17314 B) and the second to 18722 (red, 20024 B). " + "Without those seeds both stayed green (docs/REVIEW-234-2026-11-12.md).", "TestEveryFormatsMapIsTheOneItsStructureReadsBack": "its own canaries - the first boundary moved by a byte must not pass, a mark for bytes already written must be " + - "reported, a sweep with no padding is a failure - and broken on 2026-11-12 by marking the png padding a byte late: red.", + "reported, a sweep with no padding or with no padding on both sides of a content is a failure - and broken on 2026-11-12 by marking the png " + + "padding a byte late: red, and by marking the picture of a png laid out for a cut a byte late: red at 8 KiB and at 64 KiB.", "TestACutFileSaysWhereTheCutFellAndWhatIsLeft": "its own canary - a cut named in the wrong role, and content claimed gone that is there, must not pass - " + "and broken on 2026-11-12 by reporting every cut in the content: red for the picture and the icon.", + "TestACutTakesTheShareOfTheContentItIsAskedFor": "its own canaries - a cut that kept all its content must not pass for one that took half, and txt listed as " + + "not cutting at the share must be reported - and broken on 2026-11-12 by never telling png where its cut falls: red in all six of its files.", + "TestAGuardThatDrawsAfterALongPauseStillDraws": "its own canary - the box drawn after test.NewApp alone has to come out blank, or the pause " + + "does not reach the state - and broken on 2026-11-12 by making readFyneClock do nothing: red, the box drawn after newTestApp was blank too.", + "TestEveryGuardTakesItsApplicationWithTheClockRead": "its own canary - a source with test.NewApp and test.NewTempApp has to give both calls - and " + + "a reach check on the number of guard files it read.", "TestTheRecordOfACutRunIsNeverLongerThanItsForecast": "its own reach check - a run with no cut in the padding is a failure - and broken on 2026-11-12 " + "by leaving the cut out of the forecast: red, a record of 9638 B told 8130 B.", "TestADamageLabelIsBoundedAndSaysHowManyItLeftOut": "its own canary - no chain up to 200 damages shortened is a failure - and broken on 2026-11-12 by letting damage.Chain.Label return the whole chain: red.", diff --git a/internal/guard/openlist_test.go b/internal/guard/openlist_test.go index b95b5ae5..182bed82 100644 --- a/internal/guard/openlist_test.go +++ b/internal/guard/openlist_test.go @@ -54,8 +54,8 @@ func TestAnOpenListIsToldFromTheFormBehindIt(t *testing.T) { // rather than asking the widget what it would build. A menu built correctly and // never shown looks identical from the widget's side. func TestTheOpenListMarksTheValueThatIsChosen(t *testing.T) { - app := test.NewApp() - defer test.NewApp() + app := newTestApp() + defer newTestApp() app.Settings().SetTheme(parts.Theme()) host := newFakeHost(t) diff --git a/internal/guard/partscatalogue_test.go b/internal/guard/partscatalogue_test.go index 88b77490..1a7cb69c 100644 --- a/internal/guard/partscatalogue_test.go +++ b/internal/guard/partscatalogue_test.go @@ -95,9 +95,9 @@ func TestTheCatalogueCoversEveryPartAndNothingElse(t *testing.T) { // while showing nothing. Measured on the rendered tree rather than asserted // from the flag the builder set, which is GUI rule 10. func TestEveryCatalogueStateDrawsDifferently(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - defer test.NewApp() + defer newTestApp() drawn := 0 for _, e := range catalogue.Entries() { diff --git a/internal/guard/pointerfocus_test.go b/internal/guard/pointerfocus_test.go index 44136912..34ec87de 100644 --- a/internal/guard/pointerfocus_test.go +++ b/internal/guard/pointerfocus_test.go @@ -113,9 +113,9 @@ func TestAPressMovesTheKeyboardWithoutDrawingItsMark(t *testing.T) { // aimed at one segment needs a position, and the segment widths are the // control's own to know. func TestAPressOnASegmentedSwitchPutsTheKeyboardOnItQuietly(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) s := parts.NewSegments([]string{"one", "two", "three"}, nil) w := test.NewWindow(s) @@ -268,9 +268,9 @@ func screenOnACanvasOfHeight(t *testing.T, height float32) (fyne.Canvas, fyne.Ca // opens with. Without them the sizes are the test driver's rather than the // window's, and a press aimed with one set of numbers at a screen laid out // with the other lands beside the control. - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) host := newFakeHost(t) window.Open(host) diff --git a/internal/guard/reachability_test.go b/internal/guard/reachability_test.go index a4b4683d..fabf33c9 100644 --- a/internal/guard/reachability_test.go +++ b/internal/guard/reachability_test.go @@ -86,8 +86,8 @@ func allTabs() []string { func TestEveryButtonAPersonCanSeeIsReallyPressable(t *testing.T) { for _, tab := range allTabs() { t.Run(tab, func(t *testing.T) { - test.NewApp() - defer test.NewApp() + newTestApp() + defer newTestApp() content, canvas := laidOutWindow(t) screen := selectTab(t, content, tab) @@ -147,8 +147,8 @@ func TestEveryButtonAPersonCanSeeIsReallyPressable(t *testing.T) { func TestTabbingReachesTheControlsAndSaysInWhatOrder(t *testing.T) { for _, tab := range allTabs() { t.Run(tab, func(t *testing.T) { - test.NewApp() - defer test.NewApp() + newTestApp() + defer newTestApp() content, canvas := laidOutWindow(t) screen := selectTab(t, content, tab) diff --git a/internal/guard/screenpixels_test.go b/internal/guard/screenpixels_test.go index 6df95508..b66d5ca1 100644 --- a/internal/guard/screenpixels_test.go +++ b/internal/guard/screenpixels_test.go @@ -872,12 +872,12 @@ func renderScene(t *testing.T, sc screenScene) (image.Image, string) { func drawScene(t *testing.T, sc screenScene) (image.Image, string) { t.Helper() - app := test.NewApp() + app := newTestApp() // The test driver ships a deliberately garish theme so colour changes show // up. That is the wrong theme for a picture meant to be what somebody sees, // and getting it wrong produces a reference that looks like a defect. app.Settings().SetTheme(parts.Theme()) - defer test.NewApp() + defer newTestApp() host := newFakeHost(t) var tab fyne.CanvasObject diff --git a/internal/guard/softwarerenderer_test.go b/internal/guard/softwarerenderer_test.go index 9b816c56..551c6965 100644 --- a/internal/guard/softwarerenderer_test.go +++ b/internal/guard/softwarerenderer_test.go @@ -10,8 +10,6 @@ import ( "strings" "testing" - "fyne.io/fyne/v2/test" - "github.com/donislawdev/TestingFilesGenerator/internal/gui" "github.com/donislawdev/TestingFilesGenerator/internal/gui/text" "github.com/donislawdev/TestingFilesGenerator/internal/gui/window" @@ -271,8 +269,8 @@ func TestTheRendererIsLoadedRendererFirstFromBesideTheProgram(t *testing.T) { // slower, and the person should be able to read why in the place they go to // read what this program is. Untouchable rule 6, both halves. func TestTheAboutScreenSaysWhenTheWindowIsDrawnInSoftware(t *testing.T) { - test.NewApp() - t.Cleanup(func() { test.NewApp() }) + newTestApp() + t.Cleanup(func() { newTestApp() }) host := newFakeHost(t) if shown := textIn(window.About(host)); strings.Contains(shown, text.DrawingWithSoftwareRenderer()) { diff --git a/internal/guard/switchedoff_test.go b/internal/guard/switchedoff_test.go index af07a458..5550ce39 100644 --- a/internal/guard/switchedoff_test.go +++ b/internal/guard/switchedoff_test.go @@ -31,9 +31,9 @@ import ( // thing in it on or off, and a comparison of the whole box would be decided by // the words and blind to the edge. func TestAControlSwitchedOffIsNeverBrighterThanAtRest(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) type control struct { name string @@ -78,9 +78,9 @@ func TestAControlSwitchedOffIsNeverBrighterThanAtRest(t *testing.T) { // its tick whatever it held, so "Label in each file" ticked read as unticked // for the length of every run. func TestASwitchThatIsOffStillShowsItsValue(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) picture := func(on bool) image.Image { s := parts.NewToggle(nil) diff --git a/internal/guard/tabstrip_test.go b/internal/guard/tabstrip_test.go index fcfcaf6c..fa889a16 100644 --- a/internal/guard/tabstrip_test.go +++ b/internal/guard/tabstrip_test.go @@ -38,9 +38,9 @@ import ( // reason - or, worse, caught half of the chosen word in both bands and gone // green for no reason, which is what it did that morning. func TestTheTabSomebodyIsOnIsTheOneThatStandsOut(t *testing.T) { - app := test.NewApp() + app := newTestApp() app.Settings().SetTheme(parts.Theme()) - t.Cleanup(func() { test.NewApp() }) + t.Cleanup(func() { newTestApp() }) host := newFakeHost(t) window.Open(host) diff --git a/internal/guard/typeface_test.go b/internal/guard/typeface_test.go index 843352a0..6acadc17 100644 --- a/internal/guard/typeface_test.go +++ b/internal/guard/typeface_test.go @@ -5,7 +5,6 @@ import ( "testing" "fyne.io/fyne/v2" - "fyne.io/fyne/v2/test" "fyne.io/fyne/v2/theme" "github.com/donislawdev/TestingFilesGenerator/internal/gui/parts" @@ -20,8 +19,8 @@ func TestTheWindowIsSetInInter(t *testing.T) { const sentence = "Files of one format and one size, as many as you need." size := parts.Theme().Size(theme.SizeNameText) - app := test.NewApp() - defer test.NewApp() + app := newTestApp() + defer newTestApp() app.Settings().SetTheme(theme.DefaultTheme()) toolkit := fyne.MeasureText(sentence, size, fyne.TextStyle{}) toolkitBold := fyne.MeasureText(sentence, size, fyne.TextStyle{Bold: true}) diff --git a/internal/guard/windowrefused_test.go b/internal/guard/windowrefused_test.go index 51e6d3d5..e6471afc 100644 --- a/internal/guard/windowrefused_test.go +++ b/internal/guard/windowrefused_test.go @@ -56,8 +56,8 @@ type nativeWindow struct { func (w *nativeWindow) RunNative(f func(any)) { f(w.context) } func TestAWindowTheToolkitGaveNoWindowIsRefusedAndNeverRun(t *testing.T) { - test.NewApp() - t.Cleanup(func() { test.NewApp() }) + newTestApp() + t.Cleanup(func() { newTestApp() }) // Every platform context the driver hands out, empty and filled. The // filled ones use 1 rather than a real handle: the question is zero or diff --git a/internal/gui/text/locale/registry/en.json b/internal/gui/text/locale/registry/en.json index 0b810f0d..8a8fc968 100644 --- a/internal/gui/text/locale/registry/en.json +++ b/internal/gui/text/locale/registry/en.json @@ -1296,8 +1296,8 @@ }, "Detail.damage/truncate.missing": { "description": "The sentence under the missing setting of the truncate damage, after what it takes.", - "hash": "sha256-11e2c87619ac", - "other": "How much of the whole file is gone, as a percentage. At least this much is cut - a few bytes more where the format cannot end exactly there. A large file is often mostly padding after its content, so the content can stay whole - the manifest says what each cut left." + "hash": "sha256-912187dc5311", + "other": "How much of the whole file is gone, as a percentage. At least this much is cut - a few bytes more where the format cannot end exactly there. In most formats the cut removes the same percentage of the content. In some, a large file is mostly padding beside its content, so the cut can take all of the content or none - the manifest says what each cut left." }, "Detail.damage/zero-head.bytes": { "description": "The sentence under the bytes setting of the zero-head damage, after what it takes.", diff --git a/internal/gui/text/locale/registry/pl.json b/internal/gui/text/locale/registry/pl.json index 6702c8f7..ef1ef471 100644 --- a/internal/gui/text/locale/registry/pl.json +++ b/internal/gui/text/locale/registry/pl.json @@ -258,7 +258,7 @@ "Choice.winding.reversed": { "hash": "sha256-bc64f64fb479", "other": "odwrócony" }, "Choice.winding.rfc7946": { "hash": "sha256-38755035559d", "other": "rfc7946" }, "Detail.damage/append-junk.bytes": { "hash": "sha256-4403339e8eb1", "other": "Ile losowych bajtów trafia za koniec pliku. Poniżej 64 niektóre formaty wychodzą z uszkodzeniem, na które żaden czytnik nie narzeka." }, - "Detail.damage/truncate.missing": { "hash": "sha256-11e2c87619ac", "other": "Jaka część całego pliku znika, w procentach. Ucięte jest co najmniej tyle - o kilka bajtów więcej tam, gdzie format nie może się skończyć dokładnie w tym miejscu. Duży plik to często głównie dopełnienie za treścią, więc treść może zostać cała - manifest mówi, co zostawiło każde cięcie." }, + "Detail.damage/truncate.missing": { "hash": "sha256-912187dc5311", "other": "Jaka część całego pliku znika, w procentach. Ucięte jest co najmniej tyle - o kilka bajtów więcej tam, gdzie format nie może się skończyć dokładnie w tym miejscu. W większości formatów cięcie usuwa ten sam procent treści. W niektórych duży plik to głównie dopełnienie obok treści, więc cięcie może zabrać całą treść albo nic z niej - manifest mówi, co zostawiło każde cięcie." }, "Detail.damage/zero-head.bytes": { "hash": "sha256-8700126f6712", "other": "Ile bajtów na początku zostaje wyzerowanych. Poniżej czterech niektóre formaty wychodzą z uszkodzeniem, na które żaden czytnik nie narzeka." }, "Detail.format/avif.height": { "hash": "sha256-4b336c279584", "other": "Jak wysoki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/avif.quality": { "hash": "sha256-f6e1c3e1af83", "other": "Ile szczegółów zachowuje obraz. Wyższa wartość wygląda lepiej i zajmuje więcej pliku, zostawiając mniej miejsca na dopełnienie." },