Skip to content

ci: run the tests from just, and stop linting twice - #2

Merged
apostasie merged 9 commits into
mainfrom
work
Aug 16, 2026
Merged

ci: run the tests from just, and stop linting twice#2
apostasie merged 9 commits into
mainfrom
work

Conversation

@apostasie

Copy link
Copy Markdown

No description provided.

apostasie and others added 9 commits August 15, 2026 16:09
  test: was empty, so ci.yaml tested nothing. Wire it to the shared unit
  and race recipes, add bench, and report whether mkfs.erofs is reachable:
  the image-backed tests skip themselves without it, and under the
  hermetic PATH they always do — a green run that covered 37 fewer tests
  should say so rather than look identical to a full one.

  Drop the inherited lint job: it ran an action-supplied golangci-lint
  v2.1 while aqua.yaml pins v2.12.2. Rename the rest to
  erofs-utils-integration, which is what it is — the coverage needing an
  Drop go-test-platforms.yml (ci.yaml's matrix covers more, and now runs
  the 386 leg too) and codeql.yml: the baseline SAST posture is golangci's
  gosec/staticcheck plus govulncheck per GOOS, and limen wants CodeQL as
  default setup opted into via limen.yaml, not a workflow pinned by tag.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
  Rebase the Windows patch stack onto the new tree. The v1.9.2 relicense
  (Apache-2.0 -> MIT) rewrote every SPDX header the patches used as
  context, so the stack is regenerated against v1.9.3; hunks are written
  context-free (full remove/re-add blocks) so the patch files themselves
  stay whitespace-clean. The code the patches inject is unchanged.

  Drop 004-windows-gzran-zlib-guard: v1.9.3 guards the zlib include in
  lib/gzran.c itself, so the patch is upstreamed.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  MSVCRT never fills st_ino, and mkfs deduplicates hardlinks by
  (st_dev, st_ino) with no nlink gate — so on Windows every entry
  collided with the root directory's inode 0 and images built from
  local trees came out self-referential (/a.txt aliased the root nid;
  TestReadReferenceImage walked a.txt/a.txt/... until the path bound).

  Replace the CRT stat family in posix_compat.h with Win32-native
  implementations: _ino_t is widened to 64 bits before any CRT header
  runs, st_ino/st_dev come from the NTFS file index and volume serial
  (the identity Windows hardlinks actually share, so dedup is now
  correct rather than disabled), lstat detects symlink reparse points,
  and readlink extracts the target from the reparse data — the old
  stub just warned and failed. The stat/fstat/lstat macros never call
  the CRT versions, whose struct layout still has the 16-bit field.

  Add -D_GNU_SOURCE to the cross-compile make lines: v1.9.3's rebuild.c
  uses asprintf, which llvm-mingw does not expose without it (gcc-mingw
  does regardless).

  Verified on a Windows 11 ARM64 VM against an aarch64 llvm-mingw
  build of erofs-utils v1.9.3: the full go test suite passes, including
  both TestReadReferenceImage legs with their symlink assertions —
  coverage Windows never had. x86_64-w64-mingw32 compile-checked.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: apostasie <spam_blackhole@farcloser.world>
  v1.9.3's configure auto-probes libxml2 for the new OCI/S3 remotes and
  adds it to LIBS globally. PKG_CONFIG_PATH only prepends to the host's
  pkg-config search path, so the ubuntu runner's libxml-2.0.pc leaked
  into the cross build and mkfs failed to link with -lxml2.

  Use PKG_CONFIG_LIBDIR instead, which replaces the search path: only
  the mingw sysroot (where the cross-compiled lz4 installs its .pc) is
  visible, so every host-library auto-probe now fails closed — this
  fixes the whole class, not just libxml2. Pass --without-libxml2
  explicitly as well, since the oracle has no use for image remotes.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
…oval

  Removal existed only as the whiteout machinery behind CopyFrom(Merge):
  a source tree had to spell deletions as AUFS .wh. files. Expose it as
  Writer.Remove (one name; ErrDirNotEmpty for a populated directory,
  fs.ErrNotExist when missing, fs.ErrInvalid for "/") and RemoveAll
  (recursive; missing is not an error, "/" is refused, traversing a
  non-directory is ErrNotDirectory). Both refuse the file currently open
  from Create.

  Removal now has unlink(2) semantics for hard links. Removing an alias
  decrements the target's link count as before; removing the target
  while aliases survive used to leave the inode with no owner and fail
  Close with "target no longer exists" — a real gap for overlay merging,
  where a layer can legitimately white out one name of a multiply-linked
  file. unlinkEntry now promotes the lowest-pathed surviving alias to
  carry the inode and repoints the rest at it, so nothing downstream sees
  a dangling linkTo, and the choice is deterministic so identical inputs
  still yield identical images.

  Remove deliberately does not go through lookup(): that resolves a link
  name to the shared inode, right for metadata and wrong for unlink,
  where the name itself is what goes away.

  Tests: the removal matrix from erofs/go-erofs (file, empty and
  populated dirs, missing, root, symlink, alias, canonical promotion,
  all-aliases, RemoveAll recursive/missing/non-dir ancestor/root/file/
  hardlink inside), plus fork-specific coverage of a Merge whiteout
  hitting a hardlink target, order-independence of alias promotion, and
  open-file protection.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  The previous stat family redefined _ino_t as 64-bit and named
  `struct stat` in its prototypes. Both are wrong on the toolchain CI
  actually uses (Ubuntu's mingw-w64 v11, msvcrt): there the CRT itself
  rewrites `stat`/`fstat` to `_stat64`/`_fstat64` under
  _FILE_OFFSET_BITS=64, so the struct behind the name depends on which
  header ran first in a given translation unit, and st_ino has more than
  one definition site (sys/types.h and wchar.h) that a single guard does
  not cover. mkfs ended up with several struct stat layouts in one
  binary: st_size written at one offset and read at another came back
  as zero, which is the "dir/b.txt = \"\" / empty symlink target" CI
  saw. The v12+ UCRT toolchains I had verified on agree with themselves,
  which is why the ARM64 VM run passed and CI did not.

  Rework the shims so no CRT type is ever redefined and no prototype
  names struct stat: a Win32 core fills a POD of our own (__pc_finfo),
  and stat/lstat are statement-expression macros that copy it into the
  caller's struct at the call site — whatever layout that TU has, the
  fields land at its offsets by construction. fstat is an object-like
  macro to a void*-taking function (erofs_vfops has a member named
  fstat, so a function-like macro cannot be used). The CRT's own
  redirect is left in force and its target names taken over, so `struct
  stat` keeps meaning the same thing everywhere.

  The 64-bit NTFS file index — the identity hard links share, which the
  CRT's 16-bit st_ino cannot hold — now travels beside the struct:
  __pc_lstat_id returns it, and patch 001 makes erofs_iget_from_local
  key the hardlink hash on it (widening erofs_iget's ino parameter on
  Windows). Hardlink dedup is therefore correct, not merely not-broken.

  Pin MAX_BLOCK_SIZE=16384 in every configure call. configure caps the
  block size at the build host's page size, bumped to 16K only when the
  build CPU is aarch64 — so the same source produced a 4K-capped mkfs on
  x86_64 runners and a 16K one elsewhere, and the 16384 leg of
  TestReadReferenceImage silently skipped on CI.

  Verified against a byte-equivalent CI build (ubuntu:24.04 container,
  apt mingw-w64 → gcc 13 / mingw-w64 v11 / msvcrt) run on a Windows 11
  ARM64 VM under x64 emulation: full go test suite passes, both
  TestReadReferenceImage legs included. Re-verified the aarch64
  llvm-mingw (UCRT) build on the same VM: still passes, so both header
  generations are covered.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  FuzzWideOpen went red with "context deadline exceeded" at 10.10s —
  exactly fuzztime — after a clean corpus pass and ten seconds of
  fuzzing with no failing input written. That is the coordinator
  cancelling a worker caught mid-iteration as the budget expires, not a
  finding: a real fuzz failure always leaves the input under
  testdata/fuzz/<Target>/. Wide targets are the most exposed since each
  iteration is a ReadDir over 200 entries.

  Make the step decide from the crasher: a non-zero exit with a new file
  under testdata/fuzz/<Target> is a failure and is reported as such; a
  non-zero exit with no new file is retried once, and only a second
  hiccup in a row fails the job. Real bugs still fail — and now with an
  explicit ::error naming the crasher, instead of being lost among
  timing noise.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
  The Fuzz job prebuilt one test binary with `go test -c` and drove each
  target through -test.fuzz on it. Only the go tool's own fuzz build
  compiles in the coverage counters, so that binary carried none — the
  engine said so on every run ("not built with coverage instrumentation
  ... may be inefficient") — and what ran was blind mutation of the
  seeds: no feedback about which inputs reach new code, which is the
  entire mechanism by which a fuzzer works its way toward the deep
  parser corners the audit work has been hardening. Forty-five targets
  times ten seconds of that was barely more than a second seed-corpus
  pass, presented as continuous fuzzing.

  Drive each target with `go test -fuzz='^T$' -run='^$'` instead. The
  per-target rebuild the old comment wanted to avoid is a cached second
  or two after the first target warms it.

  Cache GOCACHE/fuzz across runs. Fuzzing is only cumulative if the
  generated corpus survives: each run then starts from every input
  earlier runs found interesting rather than from the seeds, so ten
  seconds per run compounds into depth over months. Key on the fuzz
  sources so a changed target restarts its own corpus; restore-keys keep
  the rest.

  Upload testdata/fuzz on failure. The log names the target; the input
  file is what reproduces the bug locally.

Signed-off-by: apostasie <spam_blackhole@farcloser.world>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@apostasie
apostasie merged commit 0f99ab3 into main Aug 16, 2026
14 checks passed
@apostasie
apostasie deleted the work branch August 16, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant