Skip to content

test: verify exact pixel output for every codec + fail CI on silently-skipped suites#72

Merged
sedghi merged 36 commits into
mainfrom
test-pixel-correctness
Jul 9, 2026
Merged

test: verify exact pixel output for every codec + fail CI on silently-skipped suites#72
sedghi merged 36 commits into
mainfrom
test-pixel-correctness

Conversation

@sedghi

@sedghi sedghi commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary

Every codec test suite now verifies exact decoded pixel data against independently verified references, CI fails loudly instead of silently skipping suites when a dist is missing, and the workflow got faster (scoped benches, single test job, dependency caching).

Review restructure: this PR targets main directly and is a pure test/CI change — zero src/ modifications. Every source fix (the first round formerly in #71 and the second round this suite uncovered) is consolidated in #73, stacked on this branch, together with the tests that require it. The split is empirical: the wasm packages were rebuilt from main's C++ with CI's emsdk 3.1.74 image and the whole workspace run against unmodified sources — every failing test moved to #73 with its fix, everything that passes stayed here. Merge this first; #73 retargets to main automatically.

Before this change, a decoder returning garbage of the correct size would pass CI for: libjpeg-turbo-8bit, libjpeg-turbo-12bit (no tests at all), every lossy/near-lossless path, and every codec dispatched through dicom-codec (RLE and JPEG Lossless had no pixel verification anywhere in the repo).

Real bug found by the new tests

jpeg-lossless-decoder-js (the JPEG Lossless SV1 path, transfer syntax .70) decodes the final pixel of the CT fixture as 0 instead of -2000.

Verified three independent ways: the RLE decode, the JPEG Lossless Process 14 decode, and DCMTK's dcmdjpeg all produce bit-identical pixels for the same slice — only the JS SV1 path differs, in exactly one sample. The suite pins this with an all-but-last-pixel exact compare plus an it.fails() sentinel that flips when upstream fixes it (worth filing against rii-mango/JPEGLosslessDecoderJS).

Pixel-correctness coverage

Package Before After
libjpeg-turbo-8bit decode checked length only (the RAW reference was loaded but never compared) byte-exact vs RAW; encode round-trip error bounded (measured maxAbsDiff 6 / mean 0.26 → asserted ≤ 10 / ≤ 1)
libjpeg-turbo-12bit no pixel test RAW reference committed and independently verified (bit-identical to DCMTK dcmdjpeg, re-derivable via tools/fixture-verification); the decode suite rides with the decoder fixes in #73
charls CT1 and near-lossless .81 checked dims/length only CT1 byte-exact vs new CT1.RAW; .81 byte-exact vs pinned golden
openjpeg lossy .91 checked dims/length only byte-exact vs pinned golden
openjphjs 11 of 13 fixtures untested new corpus test pins the SHA-256 of decoded pixels for all 13 j2c fixtures (MG/MR/NM/RG/SC/XA modalities)
dicom-codec all transfer syntaxes checked byteLength only every syntax that decodes on main byte-compares decoded pixels, including RLE and JPEG Lossless which were verified nowhere else (12-bit .51, planar RLE, codecFactory cleanup and J2K encode round-trip tests ride with their fixes in #73)
little/big-endian already exact unchanged here; 32-bit typing/alignment and 1-bit support land with their fixes in #73

How the golden references were verified

  • CT1/CT2 cross-codec validation: charls, openjpeg and openjphjs decode their CT1/CT2 fixtures to bit-identical bytes — three independent codecs agree, so the shared RAWs are trustworthy.
  • 12-bit: wasm decode is bit-identical to DCMTK's dcmdjpeg (0 of 262144 samples differ).
  • RLE / JPEG Lossless: RLE decode == Process 14 decode == DCMTK decode, committed as dicom-codec/test/fixtures/raw/CT-512x512.raw.
  • Lossy paths (openjpeg .91, charls .81): decoding is deterministic, so the decoder's own output is pinned as a regression golden — identical across the asm.js / wasm / decode-only build variants.
  • All goldens were generated from dists built with the same emscripten/emsdk:3.1.74 image CI uses.

CI: no more silently green

There were 14 skipIf(!isBuilt) gates; if artifact upload/replay ever broke, vitest skipped everything and the job stayed green. Each suite now has an it.runIf(process.env.CI) guard that fails when a dist is missing in CI (verified: hiding charls/dist fails 3 tests instead of green-with-14-skips). Local dev without builds still skips gracefully.

Consequence: any package change now builds all packages, because dicom-codec's integration tests decode through every sibling dist. This also closes the hole where a dicom-codec-only PR previously tested nothing (all suites skipped silently). Docs-only changes still skip everything.

Endian packages

The 32-bit typing/alignment fixes and the big-endian 32-bit/1-bit support (with their tests) are in #73 — this PR leaves both packages' sources untouched.

Workflow speed

Measured baseline: ~5–7 min wall; critical path = slowest build (~2 min) → codspeed-bench (3.4 min).

  • CodSpeed benches only the packages the PR touched (new bench output from detect-changes). Benches run under valgrind — the slowest job — and a PR only needs deltas for what it changed; main/workflow_dispatch still bench everything to keep full baselines.
  • Single test job for the whole vitest workspace instead of an 8-way matrix — the per-package jobs were ~90% runner setup (checkout, node, artifact download, install) around ~20 s of vitest.
  • node_modules cached keyed on yarn.lock (install skipped on hit) instead of only the yarn tarball cache; installs ran ~10× per run before.
  • Shallow checkout in build jobs (full history is only needed by detect-changes).

⚠️ If branch protection requires the old test (<package>) check contexts, they need updating to the single test check.

Deliberately left out

  • build.sh still compiles openjphjs and libjpeg-turbo-12bit with -DCMAKE_BUILD_TYPE=Debug. That ships unoptimized wasm and skews bench numbers, but flipping it silently shifts every CodSpeed baseline and fix: Decode partial htj2k stream #68 touches the openjphjs build — should be its own PR with a deliberate baseline re-seed.
  • Prebaking a build container (emsdk + cmake + yarn) would save ~30–60 s per build job but needs a ghcr image published first — follow-up.

Test plan

  • CI=1 npx vitest run — 20 files, 164 tests, all pass, all guards executed (against wasm rebuilt from main's C++ with CI's emsdk image; dist-size and browser smoke also pass against those builds)
  • Negative test: hiding a dist makes the CI run fail instead of skip
  • Scoped bench command verified locally (lerna run bench --scope ... runs only scoped packages; packages without a bench script are skipped cleanly)
  • Workflow YAML parses; job graph unchanged (detect-changes → build → test / codspeed-bench)

Summary by CodeRabbit

  • New Features

    • Expanded browser smoke coverage across codec builds and added new benchmark runs (simulation + walltime-style checks).
    • Added additional regression, matrix, and heap-stability test coverage for multiple codecs.
  • Bug Fixes

    • Strengthened CI so missing build artifacts fail loudly instead of being silently skipped.
    • Improved test/bench reliability under simulation by reducing noisy unhandled-error reporting.
  • Chores

    • Updated CI workflow to run builds in parallel and add dist-size regression gating and fixture verification documentation/tools.

sedghi added 11 commits July 7, 2026 09:51
…-skipped suites

Pixel correctness:
- libjpeg-turbo-8bit: byte-compare decode against the RAW reference
  (previously only length was checked) and bound encode round-trip error
  (measured maxAbsDiff 6 / meanAbsDiff 0.26; asserted <= 10 / <= 1)
- libjpeg-turbo-12bit: add RAW reference (verified bit-identical against
  DCMTK dcmdjpeg) and byte-compare decode against it
- charls: add CT1.RAW (cross-validated bit-for-bit against openjpeg and
  openjph decodes of the same slice) and a pinned golden for the
  near-lossless .81 path; byte-compare both
- openjpeg: pin the lossy .91 decode as a golden and byte-compare
- openjphjs: corpus test pinning SHA-256 of decoded pixels for all 13
  j2c fixtures (MG/MR/NM/RG/SC/XA modalities)
- dicom-codec: every transfer syntax now byte-compares decoded pixels
  against cross-validated references. RLE and JPEG Lossless Process 14
  outputs were verified identical to each other and to DCMTK.
- documents a real upstream bug found by these tests:
  jpeg-lossless-decoder-js decodes the final pixel of the SV1 fixture as
  0 instead of -2000 (DCMTK confirms the fixture is correct). Pinned via
  an all-but-last-pixel compare plus an it.fails() sentinel that flips
  when upstream fixes it.

Endian packages:
- big-endian: handle 32-bit (swap32 -> Float32Array) and 1-bit frames,
  mirroring little-endian; previously 32-bit fell through and left
  pixelData undefined
- little-endian: fix Float32Array alignment check (offset % 4, not % 2)
- exact-value tests for both, including unaligned byteOffset cases

CI:
- tests now fail loudly in CI when a dist is missing instead of
  silently skipping entire suites (it.runIf(process.env.CI) guards)
- any package change builds all packages: dicom-codec integration
  decodes through every sibling dist, so partial builds would trip the
  guards; docs-only changes still skip everything
- single test job for the whole vitest workspace (per-package test jobs
  were ~90% runner setup)
- CodSpeed benches only the packages the PR touched (main still
  re-benches everything for full baselines)
- node_modules cached keyed on yarn.lock; shallow checkout for builds
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bfdf075e-8c6d-460a-9a74-ce6c7473e8d2

📥 Commits

Reviewing files that changed from the base of the PR and between a8fbc9d and aaa5154.

📒 Files selected for processing (1)
  • .github/workflows/pr-checks.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/pr-checks.yml

📝 Walkthrough

Walkthrough

The PR revises CI orchestration, benchmark/docs handling, package-level benchmark/test behavior, and adds standalone fixture-verification decoders plus a runner for byte-exact comparisons.

Changes

CI Pipeline and Supporting Tooling

Layer / File(s) Summary
PR-checks workflow restructure
.github/workflows/pr-checks.yml
Changes triggers, change detection, build/test caching, browser smoke execution, and CodSpeed simulation/walltime benchmark jobs.
Benchmarking docs and dependency bumps
BENCHMARKING.md, package.json
Updates the benchmark documentation and root dependency versions for the new CodSpeed/Vitest/Playwright setup.
Dist-size regression gate
tools/dist-size/baseline.json, tools/dist-size/check.js
Adds artifact-size baselines and a CLI checker with update and tolerance logic.
Browser smoke test tooling
tools/browser-smoke/blank.html, tools/browser-smoke/run.js
Adds a local browser smoke harness that loads codec builds in Chromium and compares decoded hashes to committed references.

Codec vitest configuration, benchmark batching, and regression tests

Layer / File(s) Summary
CodSpeed unhandled-error suppression
packages/*/vitest.config.mjs
Adds CodSpeed-gated dangerouslyIgnoreUnhandledErrors settings across package Vitest configs.
Benchmark batching and wasm output suppression
packages/*/bench/decode.bench.js, packages/dicom-codec/bench/dispatch.bench.js
Batches decode and instantiate benchmarks, silences wasm output, and adds a dicom-codec dispatch benchmark.
charls decode, matrix, and heap-stability tests
packages/charls/test/*.js
Adds byte-exact decode checks, matrix coverage, CI artifact gating, and heap-stability tests.
dicom-codec dispatch, integration, and transcode tests
packages/dicom-codec/test/*.js
Adds CI artifact checks, byte-exact frame comparisons, and encode/transcode plus getPixelData contract coverage.
libjpeg-turbo-8bit decode and heap-stability tests
packages/libjpeg-turbo-8bit/test/*.js
Adds new fixtures, tighter lossy regression bounds, CI artifact gating, and heap-stability checks.
openjpeg API, corpus, decode, and heap-stability tests
packages/openjpeg/test/*.js
Adds API, corpus, decode, encode, and heap-stability coverage with pinned outputs.
openjphjs API, corpus, matrix, decode, and heap-stability tests
packages/openjphjs/test/*.js
Adds HTJ2K API, corpus, matrix, decode, and heap-stability coverage with pinned outputs.

Estimated code review effort: 4 (Complex) | ~75 minutes

Independent fixture-verification decoders

Layer / File(s) Summary
Fixture verification README
tools/fixture-verification/README.md
Documents verification scope, coverage, limitations, and discovered bugs.
Standalone reference decoders
tools/fixture-verification/jls.js, jpegdct.js, jpll.js, rle.js
Implements from-scratch JPEG-LS, sequential-DCT, JPEG Lossless, and DICOM RLE decoders.
Fixture verification runner
tools/fixture-verification/run-all.js
Runs the standalone decoders against committed fixtures and compares their bytes to RAW references.

Estimated code review effort: 4 (Complex) | ~60 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: exact pixel-output verification and CI failures for suites that would otherwise skip silently.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test-pixel-correctness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

#### 🎉 Hooray! `codspeed-node` just leveled up to 5.7.1!

A heads-up, this is a breaking change and it might affect your current performance baseline a bit. But here's the exciting part - it's packed with new, cool features and promises improved result stability 🥳!
Curious about what's new? Visit our releases page to delve into all the awesome details about this new version.

⚡ 1 improved benchmark
❌ 1 (👁 1) regressed benchmark
✅ 35 untouched benchmarks
🆕 69 new benchmarks
⏩ 13 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation decode CT2.j2k (.90 lossless 5-3, 512x512x16bit) — warm 66.1 ms 62.7 ms +5.46%
🆕 Simulation encode to JPEG 2000 Lossless (.90) N/A 2.5 s N/A
🆕 Simulation encode to JPEG-LS Lossless (.80) N/A 698.9 ms N/A
🆕 Simulation transcode JPEG-LS -> J2K (.80 -> .90) N/A 3.4 s N/A
🆕 Simulation 8-bit passthrough, 512x512 x100 N/A 86.5 µs N/A
🆕 Simulation instantiate+destroy JpegLSDecoder x50 N/A 406.4 µs N/A
🆕 Simulation instantiate+destroy JpegLSEncoder x50 N/A 393.7 µs N/A
🆕 Simulation instantiate+destroy JPEGDecoder x50 N/A 381.5 µs N/A
🆕 Simulation instantiate+destroy JPEGEncoder x50 N/A 396.3 µs N/A
🆕 Simulation 16-bit signed, 512x512 x100 N/A 129.4 µs N/A
🆕 Simulation 16-bit unsigned, 512x512 x100 N/A 119.9 µs N/A
🆕 Simulation 32-bit float, 512x512 x100 N/A 135.5 µs N/A
🆕 Simulation 8-bit passthrough, 512x512 x100 N/A 107.6 µs N/A
🆕 Simulation instantiate+destroy J2KDecoder x50 N/A 399.6 µs N/A
🆕 Simulation instantiate+destroy J2KEncoder x50 N/A 384.3 µs N/A
🆕 Simulation instantiate+destroy HTJ2KDecoder x50 N/A 2.3 ms N/A
🆕 Simulation instantiate+destroy HTJ2KEncoder x50 N/A 447.5 µs N/A
🆕 WallTime 16-bit signed + swap, 512x512 N/A 2 ms N/A
🆕 WallTime 16-bit unsigned + swap, 512x512 N/A 804 µs N/A
🆕 WallTime decode CT-512x512-near-lossless.JLS (.81 near-lossless) — cold N/A 11.6 ms N/A
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing test-pixel-correctness (aaa5154) with main (04c3e87)

Open in CodSpeed

Footnotes

  1. 13 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Independent implementations of JPEG-LS (T.87 regular+run mode, NEAR>=0),
JPEG Lossless (T.81 SOF3, predictors 1-7), DICOM RLE (PS3.5 Annex G) and
sequential DCT JPEG (SOF0/SOF1 with libjpeg's exact islow IDCT constants),
sharing no code with the codecs under test. run-all.js binary-compares
their output against every committed RAW reference: 12/12 byte-exact.

Also re-confirms the jpeg-lossless-decoder-js SV1 last-pixel bug from a
fourth independent decoder: the from-scratch SOF3 decoder produces -2000
for the final sample, matching DCMTK/RLE/Process-14 and the committed
reference.
@sedghi

sedghi commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

Independent fixture verification

Added tools/fixture-verification/: from-scratch decoders (sharing zero code with the wasm codecs, jpeg-lossless-decoder-js, or dicom-codec's RLE) that binary-compare against every committed RAW reference:

  • JPEG-LS (T.87, regular + run mode, NEAR≥0) — verifies charls CT1/CT2/near-lossless
  • JPEG Lossless (T.81 SOF3, predictors 1–7) — verifies the dicom-codec CT slice via both .jpll fixtures
  • DICOM RLE (PS3.5 Annex G) — same slice, second confirmation
  • Sequential DCT JPEG (SOF0 + restart intervals, SOF1 12-bit, libjpeg-exact islow IDCT) — verifies the 8-bit and 12-bit raws

node tools/fixture-verification/run-all.js12/12 checks byte-exact. The J2K/HTJ2K CT1/CT2 raws are validated transitively (asserted byte-identical to charls' — all three codecs encode the same slices).

The from-scratch SOF3 decoder also independently reproduces the correct last pixel (−2000) for the SV1 fixture, making it the fourth independent confirmation of the jpeg-lossless-decoder-js last-pixel bug.

Not independently verifiable by construction (documented in the tool's README): the lossy J2K golden (9/7 wavelet output is implementation-defined) and the openjphjs corpus SHA-256 pins beyond CT1/CT2 — those remain regression goldens of the build's own deterministic output.

sedghi added 15 commits July 7, 2026 18:28
Review findings on the 12-bit/openjpeg changes:

- libjpeg-turbo-12bit: decode() forced JCS_GRAYSCALE unconditionally, so
  a color (multi-component) 12-bit JPEG would silently drop its chroma
  channels and report componentCount=1. Now rejects num_components != 1
  after jpeg_read_header with cleanup + throw. Test splices the grayscale
  fixture into a syntactically valid 3-component JPEG and asserts the
  decoder throws.

- openjpeg: J2KEncoder::encode() swallowed opj_setup_encoder /
  opj_start_compress / opj_encode / opj_end_compress failures with bare
  returns. With the bounded BufferStream, an undersized output estimate
  surfaces through exactly those return values — and the JS caller would
  read back the full pre-sized allocation as a successful encode. All
  failure paths now free the codec/stream/image, empty encoded_, and
  throw. Also frees those handles on the success path, which previously
  leaked them on every encode. Test forces opj_setup_encoder failure
  (41 resolutions > OpenJPEG's max 33) and asserts encode() throws and
  the encoded buffer is empty.
'Different runtime environments detected' on PR comparisons: every
controllable axis already matched between BASE and HEAD (runner image
20260628.225.1, CodSpeedHQ/action v4.18.2, node 22.23.1 — verified from
the job logs), so the mismatch is GitHub's runner CPU lottery: standard
runners land randomly on Intel Xeon 8370C or AMD EPYC 7763, whose cache
sizes and ISA extensions make glibc execute different code paths,
shifting instruction counts even in simulation mode. See
https://codspeed.io/blog/unrelated-benchmark-regression

Mitigations:
- push trigger now fires on main only. PR branches were running the
  whole workflow twice per commit (push + pull_request) and uploading
  two CodSpeed measurements per commit, each on random hardware —
  doubling both CI usage and the odds of cross-CPU comparisons. main
  pushes still seed the baseline; PRs keep their pull_request runs.
- codspeed-bench logs lscpu before benching so any future environment
  warning is diagnosable from the job log.

The residual case (PR run and baseline run landing on different CPU
models) is inherent to shared runners; the durable fix would be CodSpeed
macro runners or GitHub larger runners.
…ation

Dual-instrument setup: simulation stays the blocking regression gate
(deterministic <1% drift, catches small algorithmic slips); a new
advisory codspeed-walltime job measures real wall-clock on CodSpeed's
ARM64 bare-metal macro runners, covering simulation's blind spots
(real cache/branch behavior, and the pure-JS endian packages where the
no-JIT simulation model diverges most from production V8).

- upgrade vitest 2.1.9 -> 3.2.7, @vitest/coverage-v8 -> 3.2.4 and
  @codspeed/vitest-plugin 4.0.1 -> 5.7.1 (walltime requires plugin >= 5,
  which requires vitest >= 3.2); all 123 tests pass on the new stack
- walltime benches run sequentially (--concurrency 1): parallel
  processes contend for cores and add wall-clock noise, unlike
  instruction counting
- walltime job is continue-on-error while the macro-runner setup beds
  in, with timeout-minutes: 30 in case runner pickup stalls
- cache keys now include runner.os/arch: macro runners are ARM64 and
  sharing keys with x64 jobs would restore broken native binaries
- BENCHMARKING.md documents the two-instrument model
vitest 3 pulls vite 7, whose engines check (>=20.19) rejects the node
20.18.0 bundled in emscripten/emsdk:3.1.74, failing yarn install in
every build job. setup-node puts node 22 on PATH for yarn/webpack;
emcc keeps using the node binary pinned in its own emsdk config.
- vitest 3 enforces a 60s worker RPC timeout; under valgrind with
  lerna --parallel, 8 bench processes on 4 cores starved the openjpeg
  and dicom-codec suites past it. Sequential benching changes only wall
  time — instruction counts are contention-immune.
- runs-on: codspeed-macro queues up to 24h when macro runners aren't
  provisioned for the org (job timeouts don't cover queue time), so the
  walltime job now requires CODSPEED_MACRO_ENABLED=true. Set it after
  enabling macro runners for the org on app.codspeed.io.
New dist-size job compares every shipped artifact (js/wasm/mem, both raw
and gzip level-9 sizes) against tools/dist-size/baseline.json and fails
when anything grows beyond max(1%, 1 KiB), so a PR cannot increase codec
binary size unintentionally. New or missing artifacts also fail, keeping
additions deliberate.

The baseline was generated from CI-built artifacts (gh run download of
run 28905144867), not local builds — the Debug-built wasm embeds source
paths, so only CI output is a stable ground truth. Intentional size
changes rerun 'node tools/dist-size/check.js --update' against CI
artifacts and commit the diff, making growth visible in review.
Under valgrind the entire vitest process (main and forked worker) runs
~60x slow while vitest 3's hard-coded 60s birpc timer counts real
seconds, so large suites structurally hit 'Timeout calling onTaskUpdate'
AFTER their benches complete — serializing lerna didn't help because the
timeout is intra-process. The benches finish and upload correctly; only
the exit code was polluted. Forward --dangerouslyIgnoreUnhandledErrors
to vitest for the simulation job only (walltime runs native speed and
stays strict) and restore --parallel, which was never the culprit.
yarn 1 mangles '--'-forwarded flags (lerna received a bare
--dangerouslyIgnoreUnhandledErrors and rejected it), so set the option
in each package's vitest config instead, gated on
CODSPEED_RUNNER_MODE === 'simulation' — the env var the CodSpeed runner
itself sets. Walltime and regular test runs stay strict.
…uppression

The CodSpeed runner exports CODSPEED_RUNNER_MODE as 'instrumentation' on
some versions and 'simulation' on newer ones (@codspeed/core accepts
both), so gate on CODSPEED_ENV being set and mode != walltime instead of
an exact 'simulation' match. Verified all three behaviors locally with a
synthetic unhandled error: suppressed under instrumentation/simulation,
strict under walltime, strict without CodSpeed.
…035)

Coverage added:
- openjpeg: corpus test activates 15 shipped-but-unused fixtures with
  committed RAW references — 8/10/12/15/16-bit, signed and unsigned,
  3-component color (US1, VL1, VL4, VL6) — all byte-exact, plus the
  0-decomposition CT1 variant.
- charls: 3-component interleaved color (ILV=sample), 8-bit and 16-bit
  unsigned grayscale, and the shipped 12-bit SC1 fixture (SHA-pinned).
- openjphjs: color with and without the reversible color transform (the
  RCT path was flagged untested in the source), 8-bit and 12-bit gray.
- libjpeg-turbo-8bit: color 4:2:0 YCbCr decode (DCMTK-verified golden)
  and a progressive SOF2 decode (Pillow-verified golden).
- dicom-codec: color JPEG .50, color RLE in both planar configurations,
  8-bit JPEG-LS .80.

Fixtures are generated deterministically from committed sources by
tools/fixture-verification/gen/ (US1.RAW RGB frame, CT2.RAW transforms);
lossless tests re-derive their references, so no golden files are needed
except the two lossy JPEG raws (DCMTK/Pillow verified).

Two real bugs found and fixed by these tests:
- HTJ2KEncoder.hpp: row stride computed as bitsPerSample/8, truncating
  to 1 byte for 9..15-bit samples — every row after the first was read
  from the wrong offset when encoding 12-bit data (openjphjs rebuilt).
- dicom-codec adaptImageInfo dropped planarConfiguration, which made the
  RLE plane-sequential decode path (decode8Planar) unreachable through
  the public decode() API.
An emsdk bump edits only this workflow; a vitest/plugin bump edits only
the root manifest and lockfile. Neither matched any packages/<pkg>/ path,
so detect-changes skipped the entire pipeline — the exact PRs that change
every compiled byte or every measurement ran zero CI. Toolchain paths now
force packages=ALL and bench=ALL (a full before/after sweep is precisely
what a toolchain bump needs). Docs-only changes still skip. The workflow
header documents the expected dist-size baseline / lossy-golden
regeneration procedure for such bumps.
- openjpeg: header/coding-parameter getters pinned after decode()
  (5 decomps, LRCP, 1 layer, 64x64 blocks, grayscale for CT1);
  decodeSubResolution levels 1-2 pinned — the level-1 output is
  cross-validated byte-identical with openjphjs' decodeSubResolution of
  the same slice (shared 5/3 LL band); setProgressionOrder and lossy
  setQuality/setCompressionRatio proven observable with a PSNR floor.
- openjphjs: readHeader-only introspection (populates frameInfo without
  decoding) and decodeSubResolution(1) pinned to the cross-validated hash.
- dicom-codec: encode() round-trips for .90/.80, transcode() .80->.90
  preserves pixels exactly, getPixelData typed-array contract pinned.

Three known defects documented as it.fails sentinels / comments that flip
when fixed:
- openjpeg readHeader() populates nothing — getters return zeros or
  uninitialized memory until decode() runs
- openjpeg getIsReversible() reports true for the irreversible 9-7 stream
- openjpeg encoder setters blockDimensions/tileSize/tileOffset/precincts/
  downSample are stored but never applied to opj_cparameters
…s 018)

- Size bounds on every lossless encode round-trip (floor 0.5x catches
  silent truncation, ceiling 1.10x catches compression-ratio
  regressions), with measured constants dated in comments: charls CT2
  115504, openjpeg CT1 174404, openjphjs CT1 185183, 8bit jpeg400 63975.
- PSNR floor (48 dB, measured 53.6) on the 8-bit JPEG round-trip.
- charls near-lossless: exact T.87 spec bound maxAbsDiff <= NEAR asserted
  for NEAR 1..3, plus proof it is actually lossy.
- libjpeg-turbo-12bit gets its first benches (cold/warm decode) and a
  bench script — previously invisible to CodSpeed, so a toolchain bump's
  full sweep measured nothing for it.
- dicom-codec: dispatcher-level encode (.80/.90) and transcode
  (.80->.90) benches.
Repeated decode/delete, encode/delete and failing-decode cycles must
leave HEAP8 capacity byte-identical after a settling warmup. Emscripten
arenas grow but never shrink, so any native leak (the class fixed in
plan 002, commit 3179998 and the PR #72 encoder cleanup) becomes
monotonic capacity growth. Loops are sized against the 50 MiB
INITIAL_MEMORY slack — verified to fail when a delete() call is removed
(100 leaked charls decoders grow the heap ~22 MiB). Error-path loops use
garbage headers (fast rejection) rather than truncated streams (seconds
of recovery each) so they can iterate enough to exceed the slack.
tools/browser-smoke/run.js serves the repo over local HTTP (correct
application/wasm MIME so streaming compilation runs), loads each of the
11 build variants in headless Chromium, decodes the reference fixture in
the page, and compares the decoded pixels' SHA-256 against the committed
RAW. Catches the emscripten glue regressions node tests cannot see (wasm
URL resolution, fetch loading, MIME fallbacks) — the first thing emsdk
bumps break. New advisory browser-smoke CI job (continue-on-error while
it beds in) with a cached Chromium install. All 11 variants pass
locally.
@sedghi

sedghi commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Upgrade-readiness test suite (plans 033–039)

Six more commits make emsdk/upstream-codec bumps fully regression-checked. Run 28915465838: 13/13 jobs green — including the two new gates (browser-smoke, and a full-sweep run triggered by the new toolchain rule itself). Workspace is now 21 test files / 190 tests (was 10/123), all pixel-verified.

  • CI: toolchain-only diffs (emsdk tag, root lockfile, workflows) now force the full build/test/bench sweep — previously they skipped CI entirely.
  • Color coverage (was zero): openjpeg US1/VL* activated byte-exact vs shipped RAWs; new color fixtures for charls (pylibjpeg-verified), HTJ2K (both color-transform modes), 8-bit JPEG (DCMTK-verified golden), RLE planar/interleaved.
  • Bit-depth matrix: openjpeg 8/10/12/15/16-bit corpus (15 fixtures); charls 8/12/16u; HTJ2K 8/12; progressive JPEG (Pillow-verified).
  • API surface: header getters, decodeSubResolution (openjpeg ↔ openjph cross-validated byte-identical), encoder setters, transcode(), getPixelData.
  • Encoder quality: size floor/ceiling bounds, PSNR floors, exact maxAbsDiff ≤ NEAR for near-lossless; 12-bit package benched for the first time.
  • Heap stability: decode/encode/error loops assert the wasm arena stops growing (verified to fail when a delete() is removed).
  • Browser smoke: all 11 build variants decode hash-correct in headless Chromium (advisory job).

Three real bugs found by the new tests, fixed here:

  1. HTJ2KEncoder.hpp row stride truncated bitsPerSample/8 → every row after the first corrupted when encoding 9–15-bit images.
  2. dicom-codec adaptImageInfo dropped planarConfigurationdecode8Planar unreachable via the public API.
  3. openjpeg introspection: readHeader() populates nothing (uninitialized getter reads), getIsReversible() wrong for lossy streams, five dead encoder setters — all pinned as it.fails sentinels that flip when fixed.

sedghi added 3 commits July 8, 2026 09:03
…as identical

Previously only files whose bytes drifted from baseline produced output,
so a fully-green run printed nothing per package and it looked like only
the drifting package had been checked. Every file now gets an ok line,
with byte-identical files labeled 'identical' to distinguish them from
sub-0.005% drift that rounds to +0.00%.

Claude-Session: https://claude.ai/code/session_017xiEYAJH7wzPnwNpnoGqet
A single instantiate+destroy (~60us) or typed-array-view decode (<1us)
is smaller than the fixed per-bench harness overhead (wrapper frames,
task attribution) plus the simulation cache model's CPU-to-CPU
variation, so those benches flagged spurious regressions on every
harness upgrade or runner-hardware change while the ms-scale decode
benches stayed clean through both. Loop the fragile bodies (x50 for
wasm lifecycle benches, x100 for the little-endian view benches and the
big-endian 8-bit passthrough) so the measured work dominates the noise
floor. The big-endian 16-bit benches do real per-pixel swap work on a
shared buffer (batching would just re-swap it) and stay unbatched.

Bench names carry the batch factor (x50/x100), so CodSpeed will report
these as new benchmarks and retire the old names - a one-time baseline
reset for the six fragile benches.

Claude-Session: https://claude.ai/code/session_017xiEYAJH7wzPnwNpnoGqet
Comment thread packages/big-endian/src/index.js Outdated
Comment thread packages/dicom-codec/src/codecs/index.js Outdated
Comment thread packages/libjpeg-turbo-12bit/src/JPEGDecoder.hpp Outdated
Comment thread packages/little-endian/src/index.js Outdated
Comment thread packages/openjpeg/src/J2KEncoder.hpp Outdated
Comment thread packages/openjphjs/src/HTJ2KEncoder.hpp Outdated
Comment thread packages/big-endian/src/index.js Outdated
Comment thread packages/big-endian/src/index.js Outdated
const length = pixelData.length;
// Float32Array views must be 4-byte aligned; shift unaligned data
if (offset % 4) {
arrayBuffer = arrayBuffer.slice(offset);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably an exception, not a just a shift since it isn't clear where the actual data is any longer.

Comment thread packages/big-endian/src/index.js Outdated
Comment thread packages/big-endian/test/decode.test.js Outdated
expect(Array.from(imageFrame.pixelData)).toEqual([1.5, -2.25, 3.75])
})

it("realigns 32-bit pixel data when byteOffset is not 4-byte aligned", () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really problematic - it should throw since it is completely unclear what this means.

sedghi added 3 commits July 8, 2026 12:12
…nly as fallback

Review feedback from wayfarer3130: BitsAllocated=32 PixelData is integer
data (signed per PixelRepresentation); float applies only to float pixel
data elements. Decode now returns Uint32Array/Int32Array accordingly and
falls back to Float32Array when pixelRepresentation is absent, matching
cornerstone3D's decodeLittleEndian. Applied to big-endian, little-endian,
and dicom-codec's littleEndian getPixelData.

Also documents that 1-bit data must be frame-extracted by the caller and
why 32-bit views may need realignment to a 4-byte boundary.
The little-endian package picked up V8 JIT dump files (12 MB) and local
CodSpeed run outputs that were never meant to be tracked.
Review follow-up: this PR mixed pixel-correctness tests with source
fixes the tests uncovered. To keep it reviewable as a pure test/CI
change, every source fix (JS decoders, dicom-codec factory/dispatch,
C++ decoder/encoder hardening) moves to a follow-up PR together with
the tests that require it.

Classification is empirical: the four wasm packages were rebuilt from
main's C++ (same emsdk 3.1.74 image as CI) and the whole workspace run
against unmodified sources; the 27 failing tests moved out with their
fixes, the 146 that pass stay (164 including CI-only guards, all green,
dist-size gate and browser smoke verified against those builds):

- big-endian: 1-bit passthrough and all 32-bit decode tests
- little-endian: 32-bit integer typing and 4-byte realignment tests
- dicom-codec: planar RLE output, codecFactory cleanup-on-throw, 12-bit
  dispatch, and both J2K encode round-trips (encode reads the result
  after Wasm memory is freed without the codecFactory fix)
- libjpeg-turbo-12bit: whole suite + bench + vitest config (decoder is
  unusable on main: RGBA heap overflow and a broken package entry point)
- openjpeg: tiny-buffer throw, encoder-failure throw, encoder heap leak
- openjphjs: 12-bit encoder round-trip (row-stride fix)
- browser-smoke: 12-bit variants (hash-compare needs the fixed decoder)

The 12-bit RAW reference stays: tools/fixture-verification/run-all.js
validates it independently of the wasm codecs.
…es), not main

The previous split commit reverted sources to main, but this PR is
stacked on the fixes branch (#71) which already carries the first round
of codec fixes — so the diff showed reversions of #71's work
(codecFactory, 12-bit decoder, openjpeg guards, dispatcher wiring).

Sources are now pinned to the fixes tree (zero src delta in this PR)
and the classification was re-run empirically against wasm rebuilt from
the fixes branch's C++: 17 tests depend on fixes this branch itself
introduced and move to the follow-up PR; everything that #71's fixes
already satisfy stays here, including the 12-bit decode suite, the
dispatcher 12-bit integration test, the codecFactory cleanup test, the
openjpeg small-buffer guard tests and the 12-bit browser-smoke variants.

Moved out (fail without this branch's own fixes):
- big-endian 1-bit/32-bit tests, little-endian 32-bit typing/realign
- dicom-codec planar RLE and both J2K encode round-trips
- openjpeg encoder-failure throw and encode/delete heap stability
- openjphjs 12-bit encoder round-trip
- libjpeg-turbo-12bit multi-component rejection + bench

Full workspace green with CI=1 (177/177); dist-size and browser smoke
pass against dists built from the fixes branch's C++.
sedghi added a commit that referenced this pull request Jul 8, 2026
Second-round fixes found by the pixel-correctness suite (#72), split out
so that PR stays a pure test/CI change. First-round fixes (12-bit decode
path, codecFactory instance cleanup, openjpeg decoder guards and
BufferStream bounds) are already in #71 — this PR carries only what the
test branch itself introduced, each fix together with the tests that
fail without it (verified against wasm rebuilt from the fixes branch's
C++ with CI's emsdk 3.1.74 image):

- openjpeg: J2KEncoder throws on setup/compress failure instead of
  silently returning, with the encoded buffer zeroed (callers previously
  read back a garbage pre-sized allocation as a successful encode);
  frees codec/stream/image on every exit path (repeated encodes grew the
  wasm heap monotonically); sizes the output buffer with headroom so
  clamped writes surface as errors instead of truncation. Pinned by the
  encoder-failure-throw tests, the encode/delete heap-stability test,
  and both dicom-codec J2K round-trips (encode .90 and transcode
  .80->.90), which fail byte-exactness without this rework.
- openjphjs: HTJ2KEncoder rounds bytesPerPixel UP; bitsPerSample/8
  truncated to 1 for 9..15-bit samples, halving the row stride so every
  row after the first was read from the wrong offset (12-bit encodes
  corrupted). Pinned by the 12-bit encoder round-trip.
- libjpeg-turbo-12bit: fail closed on multi-component input — forcing
  JCS_GRAYSCALE on a color 12-bit JPEG silently discards chroma and
  reports componentCount=1. Pinned by the multi-component rejection
  test; also adds the CodSpeed bench and its package script.
- dicom-codec: adaptImageInfo preserves planarConfiguration
  (decode8Planar was unreachable; PlanarConfiguration=1 RLE silently
  produced interleaved output). Pinned by the planar RLE test.
- little-endian/big-endian: 32-bit pixel data decodes to
  Uint32Array/Int32Array per pixelRepresentation with Float32Array only
  as the no-pixelRepresentation fallback (review feedback from
  wayfarer3130, matching cornerstone3D's decodeLittleEndian); 32-bit
  views realign to 4-byte boundaries (the old offset % 2 check threw a
  RangeError at offset % 4 == 2); big-endian gains 1-bit passthrough
  and byte-swapped 32-bit support. Same typing fix applied to
  dicom-codec's littleEndian getPixelData.
@sedghi

sedghi commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

Restructured per review: this PR is now tests/CI only — the diff contains zero src/ changes relative to its base (fixes). The source fixes this branch had introduced moved to #73 (stacked on this branch) together with the 17 tests that fail without them. Classification was empirical: wasm rebuilt from the base branch's C++ with the same emsdk 3.1.74 image CI uses, full workspace run against base sources, failing tests moved out. Everything already covered by #71's fixes (12-bit decode suite, dispatcher 12-bit integration, codecFactory cleanup test, openjpeg small-buffer guards, 12-bit browser-smoke variants) stays here. Merge order: #71 -> #72 -> #73.

Reverts the fixes-branch-relative classification: #71 is being closed in
favor of a single consolidated fixes PR stacked on this one, and this PR
retargets to main. With no fix PR below it, this branch must hold only
tests that pass against plain main sources — which is exactly the state
this restores (verified earlier against wasm rebuilt from main's C++
with CI's emsdk 3.1.74 image: 164/164 with CI=1, dist-size gate and
browser smoke green).
@sedghi sedghi changed the base branch from fixes to main July 8, 2026 16:56
sedghi added a commit that referenced this pull request Jul 8, 2026
All source fixes for the codec packages in one PR, stacked on the
pixel-correctness test PR (#72): the first round formerly on the fixes
branch (#71, closed in favor of this) plus the second round found by
the new test suite. Each fix travels with the tests that fail without
it — classification was empirical, running the full workspace against
wasm rebuilt from unfixed C++ with CI's emsdk 3.1.74 image.

First round (formerly #71):
- libjpeg-turbo-12bit: decode as single-component grayscale into 16-bit
  output. Forcing JCS_EXT_RGBA sized the buffer for 1 sample/pixel
  while libjpeg wrote 4 (heap overflow), and Uint8ClampedArray
  flattened 12-bit samples to 255. Fix the package entry points
  (dist/libjpegturbo12js.js) so the package is requireable at all, and
  wire the decoder into dicom-codec's dispatcher (.51), which
  previously threw 'Decoder not found'.
- openjpeg: decoder rejects <4-byte input and unsupported component
  counts, frees handles on the rejection path; BufferStream
  write/skip/seek callbacks are bounds-checked.
- dicom-codec: codecFactory reads results before delete() and frees
  decoder/encoder instances in finally, so failures no longer leak
  wasm instances.
- overflow-checked decoded-buffer sizing on wasm32 (openjpeg,
  openjphjs, libjpeg-turbo-8bit).

Second round (found by the pixel-correctness suite):
- openjpeg: J2KEncoder throws on setup/compress failure instead of
  silently returning a garbage pre-sized buffer, frees
  codec/stream/image on every exit path (repeated encodes grew the
  wasm heap monotonically), and sizes the output buffer with headroom.
- openjphjs: HTJ2KEncoder rounds bytesPerPixel UP; bitsPerSample/8
  truncated to 1 for 9..15-bit samples, halving the row stride and
  corrupting every row after the first in 12-bit encodes.
- libjpeg-turbo-12bit: fail closed on multi-component input instead of
  silently discarding chroma; add the CodSpeed bench.
- dicom-codec: adaptImageInfo preserves planarConfiguration
  (decode8Planar was unreachable; PlanarConfiguration=1 RLE silently
  produced interleaved output).
- little-endian/big-endian: 32-bit pixel data decodes to
  Uint32Array/Int32Array per pixelRepresentation with Float32Array
  only as the no-pixelRepresentation fallback (review feedback from
  wayfarer3130, matching cornerstone3D's decodeLittleEndian); 32-bit
  views realign to 4-byte boundaries; big-endian gains 1-bit
  passthrough and byte-swapped 32-bit support. Same typing fix applied
  to dicom-codec's littleEndian getPixelData.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
tools/dist-size/check.js (1)

44-70: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

collectDists logic is solid.

The function correctly handles both local (packages/*/dist) and CI artifact (dist-*) layouts, filters to tracked file types, and measures both raw and gzip sizes. The early-exit on missing directories and empty file lists prevents false failures for packages without builds.

One minor edge case: if --artifacts is passed without a value (line 42), path.resolve(undefined) will throw an unhandled TypeError. This is a documented CLI tool with a known call path in CI, so the risk is low, but a one-line guard would improve robustness for local usage.

🛡️ Optional guard for missing --artifacts value
 const artifactsDir = artIdx >= 0 ? path.resolve(args[artIdx + 1]) : null;
+if (artIdx >= 0 && !args[artIdx + 1]) {
+  console.error("--artifacts requires a directory argument");
+  process.exit(1);
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/dist-size/check.js` around lines 44 - 70, Add a small validation guard
in collectDists’ call path for the --artifacts option so path.resolve is never
called with an undefined value. Check the parsed artifacts argument before
resolving it, and fall back to the local packages/*/dist flow or emit a clear
usage/error message if the value is missing. Keep the change near the
artifactsDir setup in tools/dist-size/check.js so the collectDists logic remains
unchanged.
packages/dicom-codec/test/color-and-depth.test.js (1)

16-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

frameBytes is duplicated across three test files.

The same frameBytes helper appears identically in color-and-depth.test.js, integration.test.js, and transcode-and-pixeldata.test.js. Consider extracting it to a shared test utility (e.g., packages/dicom-codec/test/helpers.js) to prevent drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dicom-codec/test/color-and-depth.test.js` around lines 16 - 18, The
frameBytes helper is duplicated across multiple DICOM codec test files, so
extract the shared implementation from color-and-depth.test.js,
integration.test.js, and transcode-and-pixeldata.test.js into a common test
utility such as a helpers module. Update the affected tests to import and use
the shared frameBytes helper instead of keeping separate copies, so any future
changes happen in one place.
packages/dicom-codec/test/integration.test.js (1)

27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting frameBytes to a shared test helper.

frameBytes is duplicated verbatim across integration.test.js, color-and-depth.test.js, and transcode-and-pixeldata.test.js. A shared test/helpers/frame-bytes.js would centralize the logic if it ever needs updating. This is low priority for test utilities but worth noting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dicom-codec/test/integration.test.js` around lines 27 - 33, The
frameBytes helper is duplicated across multiple test files, so extract it into a
shared test utility to keep the Buffer conversion logic in one place. Create a
common helper such as frameBytes in a shared test/helpers module, then update
integration.test.js and the other tests that currently define their own
frameBytes to import and use the shared helper instead. Keep the helper
signature and behavior unchanged so existing tests continue to work.
.github/workflows/pr-checks.yml (1)

262-262: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Set persist-credentials: false on checkout steps that don't need git operations.

The dist-size, codspeed-bench, and codspeed-walltime jobs check out the repo but never push, commit, or run git commands afterward. By default actions/checkout@v4 persists the GITHUB_TOKEN in .git/config, which is unnecessary here and widens the attack surface if a third-party action or script reads it. Add persist-credentials: false to these three checkout steps. The detect-changes and build jobs should keep the default since they need git for diffs and submodule init.

🔒 Proposed fix
       - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
       - name: Download all built dists

Apply the same addition to the checkout steps in codspeed-bench (line 337) and codspeed-walltime (line 453).

Also applies to: 337-337, 453-453

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-checks.yml at line 262, The checkout steps for
dist-size, codspeed-bench, and codspeed-walltime persist the GitHub token even
though those jobs do not need git operations. Update the actions/checkout@v4
steps in the relevant job blocks to set persist-credentials to false, using the
checkout step names in the workflow as the anchor; keep the default behavior
unchanged for detect-changes and build since they rely on git access.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tools/browser-smoke/run.js`:
- Around line 90-130: The browser smoke test script can exit before cleanup if
`chromium.launch()`, `browser.newPage()`, or `fs.readFileSync` throws, leaving
Chromium or the server running. Refactor the async main flow in `run.js` so the
whole startup-and-loop section is wrapped with a top-level try/finally (or
equivalent) that always calls `browser.close()` and `server.close()`, and move
`browser.newPage()` into the per-variant try block alongside the other variant
work. Also add explicit handling around `chromium.launch()` so launch failures
from `browser` surface with a clear failure message instead of an unhandled
rejection.

In `@tools/fixture-verification/jls.js`:
- Around line 136-137: The buffer swap in jls.js leaves prev[0] carrying the
prior line’s first-sample value, which makes the first-column upper-left
neighbor wrong for y >= 2. Update the line-processing logic around the for-loop
and the swap/reset sequence so that after swapping prev and cur, prev[0] is
explicitly reset to 0 before the next iteration. Keep the fix localized to the
buffer-handling code used for the T.87 context calculation so Rc stays zero at
x=0.

---

Nitpick comments:
In @.github/workflows/pr-checks.yml:
- Line 262: The checkout steps for dist-size, codspeed-bench, and
codspeed-walltime persist the GitHub token even though those jobs do not need
git operations. Update the actions/checkout@v4 steps in the relevant job blocks
to set persist-credentials to false, using the checkout step names in the
workflow as the anchor; keep the default behavior unchanged for detect-changes
and build since they rely on git access.

In `@packages/dicom-codec/test/color-and-depth.test.js`:
- Around line 16-18: The frameBytes helper is duplicated across multiple DICOM
codec test files, so extract the shared implementation from
color-and-depth.test.js, integration.test.js, and
transcode-and-pixeldata.test.js into a common test utility such as a helpers
module. Update the affected tests to import and use the shared frameBytes helper
instead of keeping separate copies, so any future changes happen in one place.

In `@packages/dicom-codec/test/integration.test.js`:
- Around line 27-33: The frameBytes helper is duplicated across multiple test
files, so extract it into a shared test utility to keep the Buffer conversion
logic in one place. Create a common helper such as frameBytes in a shared
test/helpers module, then update integration.test.js and the other tests that
currently define their own frameBytes to import and use the shared helper
instead. Keep the helper signature and behavior unchanged so existing tests
continue to work.

In `@tools/dist-size/check.js`:
- Around line 44-70: Add a small validation guard in collectDists’ call path for
the --artifacts option so path.resolve is never called with an undefined value.
Check the parsed artifacts argument before resolving it, and fall back to the
local packages/*/dist flow or emit a clear usage/error message if the value is
missing. Keep the change near the artifactsDir setup in tools/dist-size/check.js
so the collectDists logic remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f82fe27-2209-4220-8e49-81b1bec5b34e

📥 Commits

Reviewing files that changed from the base of the PR and between 04c3e87 and a8fbc9d.

⛔ Files ignored due to path filters (4)
  • packages/libjpeg-turbo-8bit/test/fixtures/jpeg/US1-color-420.jpg is excluded by !**/*.jpg
  • tools/fixture-verification/gen/derive.mjs is excluded by !**/gen/**
  • tools/fixture-verification/gen/generate-fixtures.mjs is excluded by !**/gen/**
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (60)
  • .github/workflows/pr-checks.yml
  • BENCHMARKING.md
  • package.json
  • packages/big-endian/bench/decode.bench.js
  • packages/big-endian/vitest.config.mjs
  • packages/charls/bench/decode.bench.js
  • packages/charls/test/decode.test.js
  • packages/charls/test/fixtures/CT-512x512-near-lossless.RAW
  • packages/charls/test/fixtures/CT1.RAW
  • packages/charls/test/fixtures/CT2-gray16u.jls
  • packages/charls/test/fixtures/CT2-gray8.jls
  • packages/charls/test/fixtures/US1-color-ilv-sample.jls
  • packages/charls/test/heap-stability.test.js
  • packages/charls/test/matrix.test.js
  • packages/charls/vitest.config.mjs
  • packages/dicom-codec/bench/dispatch.bench.js
  • packages/dicom-codec/test/color-and-depth.test.js
  • packages/dicom-codec/test/dispatch.test.js
  • packages/dicom-codec/test/fixtures/raw/CT-512x512.raw
  • packages/dicom-codec/test/fixtures/rle/US1-color.rle
  • packages/dicom-codec/test/integration.test.js
  • packages/dicom-codec/test/transcode-and-pixeldata.test.js
  • packages/dicom-codec/vitest.config.mjs
  • packages/libjpeg-turbo-12bit/test/fixtures/raw/CT-512x512-12bit.raw
  • packages/libjpeg-turbo-8bit/bench/decode.bench.js
  • packages/libjpeg-turbo-8bit/test/decode.test.js
  • packages/libjpeg-turbo-8bit/test/fixtures/raw/US1-color-420.raw
  • packages/libjpeg-turbo-8bit/test/fixtures/raw/jpeg400jfif-new.raw
  • packages/libjpeg-turbo-8bit/test/heap-stability.test.js
  • packages/libjpeg-turbo-8bit/vitest.config.mjs
  • packages/little-endian/bench/decode.bench.js
  • packages/little-endian/vitest.config.mjs
  • packages/openjpeg/bench/decode.bench.js
  • packages/openjpeg/test/api.test.js
  • packages/openjpeg/test/corpus.test.js
  • packages/openjpeg/test/decode.test.js
  • packages/openjpeg/test/fixtures/raw/CT-512x512-lossy.raw
  • packages/openjpeg/test/heap-stability.test.js
  • packages/openjpeg/vitest.config.mjs
  • packages/openjphjs/bench/decode.bench.js
  • packages/openjphjs/test/api.test.js
  • packages/openjphjs/test/corpus.test.js
  • packages/openjphjs/test/decode.test.js
  • packages/openjphjs/test/fixtures/j2c/CT2-gray12.j2c
  • packages/openjphjs/test/fixtures/j2c/CT2-gray8.j2c
  • packages/openjphjs/test/fixtures/j2c/US1-color-ct.j2c
  • packages/openjphjs/test/fixtures/j2c/US1-color-nct.j2c
  • packages/openjphjs/test/heap-stability.test.js
  • packages/openjphjs/test/matrix.test.js
  • packages/openjphjs/vitest.config.mjs
  • tools/browser-smoke/blank.html
  • tools/browser-smoke/run.js
  • tools/dist-size/baseline.json
  • tools/dist-size/check.js
  • tools/fixture-verification/README.md
  • tools/fixture-verification/jls.js
  • tools/fixture-verification/jpegdct.js
  • tools/fixture-verification/jpll.js
  • tools/fixture-verification/rle.js
  • tools/fixture-verification/run-all.js

Comment thread tools/browser-smoke/run.js
Comment thread tools/fixture-verification/jls.js

@wayfarer3130 wayfarer3130 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manually reviewed and also got 1 finding from clod that I'd appreciate your resolving before commit.
The one change I'd request before merge is adding tools/browser-smoke/ and tools/fixture-verification/ to TOOLCHAIN_PATHS so the new tooling can't rot untested

Review follow-up: tools/browser-smoke/ drives the browser-smoke job and
the test suites import reference derivations from
tools/fixture-verification/gen/, but neither directory was in
TOOLCHAIN_PATHS — a PR touching only them matched no package path and
skipped CI entirely, letting the tooling rot untested.
@sedghi

sedghi commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Done in aaa5154 — added tools/browser-smoke/ and tools/fixture-verification/ to TOOLCHAIN_PATHS, so a change to either forces the full build/test/bench pipeline. Both are genuinely load-bearing: browser-smoke drives the browser-smoke job, and the test suites import reference derivations from tools/fixture-verification/gen/.

@sedghi sedghi merged commit fcfb2c2 into main Jul 9, 2026
16 checks passed
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.

2 participants