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
33 changes: 33 additions & 0 deletions .github/osv-scanner.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,36 @@ reason = "semgrep pins mcp==1.29.0 in every release, and the advisory is about a
id = "GHSA-xcq7-p3pf-p88h"
ignoreUntil = 2026-11-15
reason = "semgrep pins opentelemetry-api~=1.37.0 in every release, and the advisory is about parsing an inbound tracestate header in a server, which semgrep scan never runs"

# Three more advisories against mcp 1.29.0, published 2026-11-02, all fixed in
# the same 1.30.0 as the first entry above.
#
# semgrep 1.183.0 of 2026-10-29, the newest, still asks for mcp==1.29.0, so no
# set pip can install holds the fix. Nor can mcp be left out of the install:
# semgrep's command line imports mcp.server.auth.settings when it starts, and
# without the package `semgrep --version` itself fails (measured 2026-11-03 in
# python:3.14-slim with this directory's requirements).
#
# Two are about the MCP client - its HTTP transports following a redirect to
# another host with the request's headers and body, and its fetching of $ref
# addresses a server names in an output schema. The third is about an MCP
# server accepting a bearer token issued for a different resource server. The
# job runs `semgrep scan` and nothing else, which starts neither a client nor
# a server, on a runner that lives for one scan.
#
# The same date as the entries above: all of them wait on a semgrep that moves
# its pin, and all of them come back on one reminder.
[[IgnoredVulns]]
id = "GHSA-5h93-6whr-6q8j"
ignoreUntil = 2026-11-15
reason = "semgrep pins mcp==1.29.0 in every release, and the advisory is about the MCP client following cross-origin redirects, which semgrep scan never starts"

[[IgnoredVulns]]
id = "GHSA-rwrf-2pqf-9j8j"
ignoreUntil = 2026-11-15
reason = "semgrep pins mcp==1.29.0 in every release, and the advisory is about the MCP client fetching server-chosen $ref addresses, which semgrep scan never starts"

[[IgnoredVulns]]
id = "GHSA-w4fh-qvv9-3v23"
ignoreUntil = 2026-11-15
reason = "semgrep pins mcp==1.29.0 in every release, and the advisory is about an MCP server checking bearer tokens, which semgrep scan never starts"
58 changes: 58 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1170,6 +1170,24 @@ jobs:
sudo apt-get install -y --no-install-recommends libgl1-mesa-dev libwayland-dev libx11-dev libxkbcommon-dev xorg-dev
shell: bash

- name: the machine this part runs on
# O298, 2026-11-03. Part 2 ran past its twenty minutes in one run of
# three since 2026-10-26, main included, while parts 0, 1 and 3 of the
# same runs took the time they always take. In a slow run every test of
# the part from the fourth on is about 1.75 times slower, the single
# threaded ones that read files among them. The log names the runner's
# image and region and nothing about the machine, so a slower machine
# and something inside the part could not be told apart. This says what
# the machine is, and the step after the tests says what its processor
# did meanwhile - the next slow run answers which of the two it was.
run: |
set -euo pipefail
echo "processors: $(nproc)"
lscpu | grep -E '^(Model name|Thread\(s\) per core|Core\(s\) per socket|Socket\(s\)|CPU max MHz|L3 cache|Hypervisor vendor|Virtualization type):' || true
free -m
echo "load average: $(cat /proc/loadavg)"
shell: bash

- name: test this part under the race detector
# A data race is the one defect class here that nothing else notices. It
# does not change a size, and on the run that happens to interleave the
Expand Down Expand Up @@ -1206,6 +1224,9 @@ jobs:
# 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.
# Twenty minutes is 1.8 times the slowest part measured.
#
# The processor's counters and the clock are read just before the
# tests, for the step after this one (O298).
env:
PART: ${{ strategy.job-index }}
PARTS: ${{ strategy.job-total }}
Expand All @@ -1232,6 +1253,8 @@ jobs:
echo "With this few tests, use fewer parts in strategy.matrix.part."
exit 1
fi
head -n 1 /proc/stat > stat-before.txt
date +%s > time-before.txt
go test -tags "$(cat .github/build-tags)" ./internal/guard/ -count=1 -race -timeout 20m -v -run "^($(paste -sd'|' mine.txt))\$" 2>&1 | tee part.log
missing=$(comm -23 <(sort mine.txt) <(sed -n 's/^=== RUN \([^/]*\)$/\1/p' part.log | sort -u))
if [ -n "$missing" ]; then
Expand All @@ -1242,6 +1265,41 @@ jobs:
fi
shell: bash

- name: what the processor did while this part ran
# The other half of the step that says what the machine is (O298). The
# counters are the whole machine's, read just before the tests and
# again now. iowait is time the processors sat idle while disk I/O was
# pending, not time a test spent waiting for it, and the kernel calls
# the counter unreliable. steal depends on the platform: Microsoft
# documents it as not applicable to Azure, so nought there proves
# nothing about the host. The processes are ranked by the processor
# time each has used since it started, which ELAPSED dates, so one the
# part left running stands out from the runner's own. Runs after red
# tests as well, because a slow part is a red one.
if: ${{ !cancelled() }}
run: |
set -euo pipefail
if [ ! -s stat-before.txt ]; then
echo "the tests of this part never started, so there is nothing to compare"
exit 0
fi
read -r _ user0 nice0 system0 idle0 iowait0 irq0 softirq0 steal0 _ < stat-before.txt
read -r _ user1 nice1 system1 idle1 iowait1 irq1 softirq1 steal1 _ < <(head -n 1 /proc/stat)
busy=$(( (user1 + nice1 + system1 + irq1 + softirq1) - (user0 + nice0 + system0 + irq0 + softirq0) ))
idle=$(( idle1 - idle0 ))
iowait=$(( iowait1 - iowait0 ))
steal=$(( steal1 - steal0 ))
total=$(( busy + idle + iowait + steal ))
echo "over $(( $(date +%s) - $(cat time-before.txt) )) s on $(nproc) processors:"
if [ "$total" -gt 0 ]; then
echo " busy $(( 100 * busy / total ))%, idle $(( 100 * idle / total ))%, idle with disk I/O pending (iowait) $(( 100 * iowait / total ))%, steal (platform dependent) $(( 100 * steal / total ))%"
fi
echo "load average: $(cat /proc/loadavg)"
echo "the processes still running now, by processor time used since each started (ELAPSED):"
ps -eo pid,ppid,etime,time,pcpu,comm --sort=-time > processes.txt
head -n 12 processes.txt
shell: bash

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- name: the other packages under the race detector
# Every package outside internal/guard, in part 0 alone. None holds a
# test today - the first run took one second for all of them - and one
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ because it turns other people's test suites red.
1 GB, 500 000 text files are written where 400 000 were killed. Files come
out byte for byte the same.

- **A run that will hold more memory than its setting gives it says so before
it starts.** The note names both sizes and comes before the first file is
written on the command line, and after Preview or the run in the window, so
a run the system ends still leaves the reason in its log. In a container of
1 GB, 600 000 text files were ended after fifteen minutes with nothing said.
The same run finished where the container had swap, so it is warned rather
than refused.

- **A Docker image.** `ghcr.io/donislawdev/tfg` holds the Linux command line
of a release and nothing else, for x64 and Arm, and runs with no network. It
is made from the published archive once its checksum and attestation check
Expand Down Expand Up @@ -125,6 +133,11 @@ because it turns other people's test suites red.
- **Built with Go 1.27.3.** It fixes a vulnerability in `crypto/x509` of the
Go standard library, which only the desktop window links. The command line
tool was not affected. Building from source now needs Go 1.27.3.
- **The window is built with `golang.org/x/net` 0.62.0.** The version before
had two published vulnerabilities in the package that reads and writes HTML.
The window uses the package only to read the SVG pictures it draws. Neither
vulnerability is in code the window reaches, and the command line tool does
not contain the package at all. No generated file changes.

## [0.5.0] - 2026-10-20

Expand Down
6 changes: 5 additions & 1 deletion THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ module, was compared with the one at the version before and is byte for byte
the same. No module entered or left the graph, so the counts above did not
move.

`golang.org/x/net` went to v0.62.0 on 2026-11-03 on its own, because two
published advisories name v0.60.0. Its licence file is byte for byte the one
at v0.60.0, and no module entered or left the graph.

Two arrived later than the rest. `github.com/FyshOS/fancyfs` came in on the
same day with the folder picker, because the toolkit's dialog package
imports it to decorate folder icons. Its licence was read before it was
Expand Down Expand Up @@ -285,7 +289,7 @@ is the module's own MIT, carried unchanged.
| `github.com/srwiley/rasterx` | v0.0.0-20220730225603-2ab79fcdd4ef | BSD-3-Clause | (c) 2018, Steven R Wiley |
| `github.com/yuin/goldmark` | v1.8.2 | MIT | (c) 2019 Yusuke Inuzuka |
| `golang.org/x/image` | v0.46.0 | BSD-3-Clause | 2009 The Go Authors. |
| `golang.org/x/net` | v0.60.0 | BSD-3-Clause | 2009 The Go Authors. |
| `golang.org/x/net` | v0.62.0 | BSD-3-Clause | 2009 The Go Authors. |
| `golang.org/x/sys` | v0.49.0 | BSD-3-Clause | 2009 The Go Authors. |

The five licence texts follow, one copy each. They differ only in the copyright
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ require (
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
golang.org/x/image v0.46.0 // indirect
golang.org/x/net v0.60.0 // indirect
golang.org/x/net v0.62.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
Expand Down
1 change: 1 addition & 0 deletions internal/cli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ func produce(ctx context.Context, targets []engine.Target, opt engine.Options, g
echoBoundaries(targets, planned, errOut)
forecast, warned := echoManifestReach(ctx, planned, opt, errOut)
opt.Record = forecast
echoHold(planned, opt, errOut)

if g.dryRun {
fmt.Fprintln(errOut, "dry run - nothing was written.")
Expand Down
19 changes: 19 additions & 0 deletions internal/cli/record.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,25 @@ func echoManifestReach(ctx context.Context, planned []engine.PlannedFile, opt en
return &record, true
}

// echoHold says so when a run will hold more than its memory setting gives it
// (O296, engine.PredictHold says why and what was measured).
//
// Before the first byte, and that is the point of it: a run the system ends
// prints nothing afterwards, so the sentence has to be in the log already. A
// note rather than a refusal, because the same run finishes where there is swap
// to carry it. And on a dry run, for the reason echoManifestReach gives.
func echoHold(planned []engine.PlannedFile, opt engine.Options, errOut io.Writer) {
hold := engine.PredictHold(len(planned), opt)
if !hold.Over {
return
}
fmt.Fprintf(errOut,
"note: this run will hold about %s in memory by its end, more than the %s the memory setting gives it, "+
"so it will run slower and the system may end it before the manifest is written. "+
"Split the run, or give it more with --memory if this machine has the room.\n",
core.HumanBytes(hold.Holds), core.HumanBytes(hold.Room))
}

// echoManifestOutcome says what the manifest came to, when that is not what
// the run was told before its first byte.
//
Expand Down
18 changes: 7 additions & 11 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,17 +196,6 @@ func (o Options) availableBytes(path string) (int64, error) {
return core.AvailableBytes(path)
}

// memoryLimits is the memory setting worked out on this machine. Asked once
// by planning and once by the run, rather than carried between them, because
// the two are separate calls and a caller may plan without running.
func (o Options) memoryLimits() core.MemoryLimits {
machine := o.MachineMemory
if machine == nil {
machine = core.MachineMemory
}
return o.Memory.On(machine)
}

// Result is what a run produced.
type Result struct {
Manifest *manifest.Manifest
Expand Down Expand Up @@ -242,6 +231,12 @@ type Result struct {
// saved knows the real length afterwards, in Record of SaveRecord.
Record RecordSize

// Hold is what this run was worked out to hold at its end, against what
// its memory setting gives it (PredictHold), before its first byte. A
// window says it after a preview and after a run, and the command line
// says it before the run from its own forecast.
Hold HoldForecast

// reservation is the run's hold on its manifest name, taken before the
// first file and handed to SaveRecord, which saves through it. Nil for a
// dry run and for a run refused before it reserved anything.
Expand Down Expand Up @@ -533,6 +528,7 @@ func Run(ctx context.Context, files []PlannedFile, opt Options) (*Result, error)
}
limits := opt.memoryLimits()
res.noteMemory(limits)
res.Hold = predictHold(len(files), limits)

if opt.DryRun {
// Nothing is written. Every entry is what would have been produced,
Expand Down
85 changes: 85 additions & 0 deletions internal/engine/holdforecast.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Part of package engine. See engine.go.
package engine

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

// Whether a run will hold more than its memory setting gives it, said before
// its first byte (O296, docs/FILE-CEILING-2026-11-02.md section 13).
//
// A run holds its whole plan to the end, and every file adds its answer and its
// manifest entry as it finishes. The plan is already weighed while it is built
// (planmemory.go), and what the files add is not, so a run of 600 000 text files
// in a container of 1 GB passed every check, wrote for fifteen minutes and was
// ended by the system with no sentence and no manifest. With Docker's default
// swap the same run finished, so this says and does not refuse: whether a run
// past its setting is ended depends on swap the program cannot see.
//
// Measured against the setting rather than against the machine. Under auto the
// setting is half the machine, and that is also the limit the runtime is handed
// (processmemory.go): past it the collector works harder and the run is slower,
// which is certain, and the system may end it, which is not. In 1 GB, 300 000
// text files held about 420 MB and finished at the usual speed, 400 000 and
// 500 000 held more than the 512 MiB and finished more slowly, and 600 000 held
// about 840 MB and was ended.

// entryHolds is what one file adds to the memory of a run from the moment it is
// written to the end of the run: its answer and its manifest entry.
//
// Measured 2026-11-03 with the runsize probe, 20 000 files each, as the live
// heap after writing less the live heap after planning: 520 B a file for txt,
// png and zip, 519 for docx and 518 for pdf. The entry does not grow with the
// format, because the properties it carries are the target's own map, shared by
// reference. TestAFileAddsWhatTheForecastSaysToTheRunsMemory keeps the number
// honest.
const entryHolds = 520

// memoryLimits is the memory setting worked out on this machine. Asked once
// by planning and once by the run, rather than carried between them, because
// the two are separate calls and a caller may plan without running.
//
// Here rather than in engine.go since 2026-11-03, when the forecast below took
// that file past its ceiling: this file is about what a run holds against its
// setting, and this is where the setting meets the machine.
func (o Options) memoryLimits() core.MemoryLimits {
machine := o.MachineMemory
if machine == nil {
machine = core.MachineMemory
}
return o.Memory.On(machine)
}

// HoldForecast is how much a run will hold at its end, against what its memory
// setting gives it.
type HoldForecast struct {
// Holds is the heap as the run starts, its plan among it, and what every
// file will add as it is written.
Holds int64
// Room is what the memory setting gives the run. Nought when it gives no
// limit to measure against - off, or a machine that could not be asked.
Room int64
// Over says Holds is past Room.
Over bool
}

// PredictHold works out what a run of this many files will hold, for the
// sentence both surfaces say before the first byte.
//
// The heap is read the cheap way first, and that reading is never less than
// what is live, so a run it says has room has room. Only a run that may not is
// read again after a collection. That costs what heapInUse says it does, and it
// is paid by a run that is about to be warned.
func PredictHold(files int, opt Options) HoldForecast {
return predictHold(files, opt.memoryLimits())
}

func predictHold(files int, limits core.MemoryLimits) HoldForecast {
if limits.Run <= 0 || limits.Unknown != nil {
return HoldForecast{}
}
added := int64(files) * entryHolds
if held := int64(heapHeldAtMost()) + added; held <= limits.Run {
return HoldForecast{Holds: held, Room: limits.Run}
}
holds := int64(heapInUse()) + added
return HoldForecast{Holds: holds, Room: limits.Run, Over: holds > limits.Run}
}
Loading
Loading