test(pdf-codec): work toward a genuine 100% mutation score - #1270
Draft
Mearman wants to merge 92 commits into
Draft
test(pdf-codec): work toward a genuine 100% mutation score#1270Mearman wants to merge 92 commits into
Mearman wants to merge 92 commits into
Conversation
Mearman
force-pushed
the
feat/100-percent-mutation-pdf-codec
branch
4 times, most recently
from
September 14, 2026 08:44
3dbbf36 to
aa136a6
Compare
…-machine contention Individual crypto and font tests carried per-test timeout overrides (60s, then briefly 300s) sized against an isolated, lightly-loaded run. Under Stryker's mutation-instrumented dry run, contended against this shared machine's other concurrent work, both AES-256 key-derivation tests and an unrelated CFF font-parsing test missed timeouts far larger than their own uninstrumented cost, proving the slowdown is general contention rather than any one test's own logic. Replace the scattered per-test overrides with a single UNIT_TEST_TIMEOUT_MS applied to the whole unit project in vitest.config.ts, carried through explicitly into vitest.mutation.config.ts since that file replaces the base config's test block rather than merging into it. Raise pdf-codec's dryRunTimeoutMinutes so the whole dry run has room for several worst-case tests landing in the same run.
… time The instrumented unit suite's heaviest tests (AES-256 key derivation, the whole-Unicode-range glyphId enumeration) are already measured to exceed their own generous timeout under this shared machine's contention; running several Stryker workers concurrently multiplies that same contention rather than avoiding it. Scoped to this package's own stryker.config.ts, per PackageStrykerOptions.concurrency, rather than lowering the workspace-wide default every other package's mutation run would then pay for.
…surface buildSfnt's and buildGsubTable's tag-writing loops iterated a hardcoded 4-character bound and wrote each byte manually; since a Uint8Array coerces an out-of-range charCodeAt(4) to 0 and the byte was already 0, an off-by-one bound mutation was byte-for-byte indistinguishable from the original. Replace both loops with TextEncoder().encode(tag) plus a single Uint8Array.set, which has no bound to mutate at all. buildContextFormat1/buildContextFormat2 always passed empty backtrack and lookahead arrays into the shared chained/non-chained SequenceRule builder, but the non-chained branch never reads them -- the arrays were live but their contents unobservable. Split the builder into buildSequenceRuleBytes (plain, input only) and buildChainSequenceRuleBytes (backtrack/input/lookahead), so the non-chained callers no longer construct fields nothing consumes. buildCmapTable indexed a parallel `encoded` array by position and guarded the lookup with a throw for undefined, even though the array is built by mapping over `subtables` one-to-one and can never actually be short. Zip spec and encoded bytes into one array up front and iterate that instead, removing the unreachable guard entirely. buildGdefTable computed `sets` from `markGlyphSets ?? []` unconditionally, but the fallback only matters when markGlyphSets is undefined, which is exactly when the value is never read (the IIFE that reads it only runs when markGlyphSets is defined). Pass markGlyphSets directly into the IIFE instead of materialising the fallback.
gsub-table.test.ts and gdef-table.test.ts only exercise these builders indirectly through a real reader, which tolerates a wrong offset or operator as long as the resulting bytes still parse into something plausible. Add direct tests against every builder's raw output -- buildSfnt's directory records, all three cmap subtable formats (including sorting mappings given out of insertion order and a non-power-of-two segCount for format 4's searchRange/entrySelector/ rangeShift), post v2/v3, coverage/single-subst/ligature sorting and byte placement, the plain vs chained SequenceRule bodies, format 3's chained and plain layouts, GSUB's per-feature table placement and lookup markFilteringSet width across all three flag cases, and GDEF's v1.0/v1.2 header selection -- closing the coverage/precision gap the indirect tests left around each builder's own arithmetic. Also removes buildGdefTable's now-redundant `withSets` variable, missed when the previous commit collapsed its two `=== undefined` checks into one.
…tant gaps buildCoverageFormat2 had no direct test at all, leaving every field write and the running coverageIndex accumulation across ranges unverified. Add a test with two ranges asserting the second range's coverage index carries the first range's real glyph count forward. buildFormat0's explicit view.setUint16(0, 0) wrote a value the buffer already held from Uint8Array's own zero-initialization -- removing the call changes nothing observable, so delete it rather than leave a mutation target with no real behaviour to test. putSequenceRuleTail's return value was unused by its one caller, leaving the arithmetic that computed it untestable by construction. Inline it into buildSequenceRuleBytes (its only caller) and drop the dead return entirely, since a "shared" tail with exactly one caller was never actually shared. Strengthen the markFilteringSet lookup tests to check the lookup's own byte length and the untouched subtable content, not just the recorded offsets -- a wrongly-forced markFilteringSetWidth can leave the recorded offsets self-consistent while still corrupting or mis-sizing the bytes that follow. Also assert feature 0's own lookupIndices values in the multi-feature layout test, previously only checked for feature 1.
The KSA's state[i] = i loop bounded i < STATE_SIZE, but a typed array silently drops an out-of-range integer-index write -- state[256] = 256 on a 256-entry Uint8Array is a no-op -- so a loop bound mutated to i <= STATE_SIZE produced byte-for-byte the same state array, an equivalent mutant no test could ever distinguish. Uint8Array.from's own length argument builds the identical array with no comparison operator for a mutation to target.
The big-endian bit-length loop stopped early once bitLength reached 0, via `i < lengthBytes && bitLength > 0`. padded is already zero-filled, so writing 0 % 256 into the remaining length-field bytes is a no-op, and a JS number's own 2^53 precision ceiling never needs more than 7 of SHA-256's 8 (or SHA-512's 16) length bytes to represent -- no reachable message ever runs the loop far enough for the bitLength > 0 half of the guard to be what stops it. Drop it and let the loop run its full lengthBytes iterations unconditionally.
Every magnitude that could round to the literal string "-0" at NUMBER_DECIMAL_PLACES -- including -0 itself -- already satisfies abs(n) < NUMBER_EPSILON and returns "0" from the guard above, since NUMBER_EPSILON is exactly one unit in the last of those decimal places. toFixed can only produce "-0.0000" for a magnitude below half that unit, which is caught by the same guard. The ternary comparing the stripped string against "-0" was therefore dead code no input could ever reach.
…y method buildEncryptor's own method parameter is already narrowed to Extract<CipherMethod, "rc4" | "aes"> (every SCHEME_SPECS entry only ever carries one of those two), so the "identity" branch inside applyEncryptMethod could never be reached through any real call path in this module. Narrow its parameter to match and delete the dead branch, rather than leave a comparison no test could ever exercise.
isBlack computed (a + b) % 2 === 0 from a = x/2|0 and b = y/2|0. Sum and difference of two integers always share the same parity, so an ArithmeticOperator mutation to a - b produces byte-for-byte the same checkerboard no decoded bitmap could ever distinguish. Compare (a & 1) against (b & 1) directly instead, leaving no arithmetic operator for that mutation to target.
…apping The outer per-component loop bounded c < fixture.componentCount, but an off-by-one bound there would silently append one extra all-zero plane (every index inside it reads past the end of `bytes`, and `?? 0` swallows the resulting undefined) -- a difference visible only in the returned array's own length, which nothing calling this helper actually re-checks. Build the planes with Array.from's own length argument instead of a counted for-loop, removing the vulnerable comparison outright.
The existing test only checked the thrown value's constructor, leaving both string literals passed to the DOMException constructor unverified.
The existing CFF2 case (header size 5, no valid Name INDEX past it) also fails for reasons unrelated to the majorVersion check, so removing that check entirely left the test still passing. Add a case with majorVersion 2 but an otherwise valid CFF 1.0 layout -- headerSize 4, a readable Name INDEX, a plain Top DICT -- where the version check is the only thing standing between it and a wrongly-defined probe result.
…trics parseHhea's numberOfHMetrics === 0 guard had no test forcing it: every existing case used a real font whose hhea always declares at least one metric. Patch Carlito's own hhea table directly (a new patchU16InTable helper alongside the existing dropTable/truncateTable) to zero that field and confirm loadEmbeddedFace refuses the font.
Nothing exercised the columns <= 0 || rows <= 0 early return, including the negative-rows case, which the ||-composed guard treats identically to zero.
The round-trip fixture never included a form field, leaving LayoutFormFieldSchema's own z.enum(fieldType) array (and every other field on the schema) unparsed by any test in this file. Add a text field and a group with one nested checkbox child to the fixture.
Neither had a test: no call ever passed an explicit level to deflate
(so the { level } object literal it builds had no coverage), and
MAX_INFLATE_OUTPUT_BYTES's throw was unreached by any real input. Mock
unzlibSync's return value for the size-guard case rather than actually
decompressing half a gigabyte on every one of this suite's mutation
runs -- the guard only ever reads the result's .length.
readFilespec's own missing-stream warning had no test: every existing filespec fixture either resolved a real embedded stream or never declared /EF at all. Add a catalog /AF entry whose /EF resolves to an empty dict, and assert both the dropped attachment and the emitted diagnostic.
…rals
Four literals -- the empty stream dict "<< >>", marked-content "EMC",
PDF version "1.4", and the Helvetica font dict -- were each retyped
verbatim at every one of dozens of call sites across independent
fixture functions. Since every copy is its own separate string literal
to the type checker (and to Stryker's mutation testing, its own
separate mutation target), a single fixture author typo in any one
copy would silently diverge from the rest with nothing to catch it.
Name each one once (EMPTY_DICT, EMC, PDF_1_4, HELVETICA_FONT_DICT) and
reference it everywhere it recurred, matching the existing
HELLO_CONTENT constant's own pattern. Also drop the redundant explicit
.header("1.7") argument at call sites that were only ever restating
FixtureBuilder.header's own default value.
isTrueTypeCollection's hasBytes(bytes, 0, 4) && u32(...) check had two
survivable mutants: forcing either side to a bare `true` made every
parse failure misreport as a TrueType Collection, and the existing
"generic parse failure" test could not catch it because the source
label it asserted on ("not-a-font.bin") also appears verbatim inside
the TTC message. Assert the actual generic wording instead, and add a
buffer too short to hold even the 4-byte tag -- hasBytes' own job is
keeping that case from ever reaching u32, which would throw past this
file's bounds rather than yield a clean FontFaceParseError.
Also assert FontFaceParseError's own .name, which nothing checked.
parseHmtx had no test file at all -- every existing exercise of it went through embedded-font.ts's own guard, which already refuses a font before hmtx's zero-metrics and missing-table throws could ever run. Cover advance-width lookup, the last-entry fallback for glyph IDs past numberOfHMetrics, both missing-table throws, and the zero-metrics throw directly against hand-built hhea/hmtx bytes.
…fallback Every existing font fixture named an explicit /BaseFont, leaving the ?? "Helvetica" default unreachable for both the simple and composite font builders. Also cover readCidWidths' malformed-leading-operand recovery (i++; continue), which had no test where a /W array actually contained a non-numeric c/cFirst entry.
…case The one existing filled-double-stroke test never set fillRule, so the evenodd -> "f*" branch had no coverage. Also cover averageNormal's zero-length case directly: an open path that goes out and immediately reverses along the same line gives its shared vertex two exactly opposite chord normals, which sum to the zero vector rather than a divide-by-zero -- that vertex stays at its original coordinates on both offset copies while the two open ends still move along their own single chord's normal.
decodeGenericRegion/decodeRefinementRegion had no test file of their own; every exercise came through jbig2.ts's own segment parser, which always masks GBTEMPLATE/GRTEMPLATE to the 2-bit/1-bit range the real template tables cover -- their own out-of-range guards were dead from that one call path. Both functions are exported, so a direct caller is not bound by that masking; call each with an out-of-range template directly and assert the resulting Jbig2UnsupportedError.
Every existing embedding test used the vendored Carlito Regular, an upright design, leaving the italicAngleDegrees !== 0 branch untested. Build the same object group from the vendored Caladea Italic instead and assert the descriptor's own Flags carries the ITALIC bit.
Every exported fixture function feeds FixtureBuilder well-formed dicts and object numbers that genuinely exist, leaving its own /Length- insertion regex, xref padding, offsetOf's misuse guard, and the maxObjNum arithmetic in /Size and the xref subsection header with no route to direct coverage. Export the class and test it against adversarial input directly: a nested dict to prove /Length lands at the true end rather than the first '>>' encountered, dicts with no or trailing whitespace around the final '>>' to prove the anchor and quantifier are both load-bearing, a fixed-width offset assertion to prove padStart's zero-pad character actually pads, and an unwritten object number to prove offsetOf's guard actually throws. Also remove FixtureBuilder's rawBytes method, which no fixture in this file has ever called.
…nd-built programs The vendored STIX Two Math font is a well-formed program from a real font toolchain, so its charstrings never reach execute()'s or executeEscaped()'s own interpreter limits and malformed-input paths: subroutine nesting past the spec's own depth limit, a glyph whose operator count runs past the per-glyph ceiling, an operand stack overrun, a hintmask whose mask bytes run past the end of the charstring, a reserved operator byte, callsubr/callgsubr with no index on the stack or no matching subroutine, and endchar's own four-argument seac-like form. Add cffFontWithCharstrings, a fixture builder that wraps caller-supplied charstrings (and an optional Private DICT with a Local Subrs INDEX) in an otherwise real CFF program, and drive every one of those paths directly, plus the success path of a glyph drawn through a real local subroutine and an implicit vstem list ahead of a hintmask.
…a fake GlyfTable The vendored Carlito face is a well-formed program from a real font toolchain, so decodeSimpleContours' own malformed-input paths and decodeOutline's composite-recursion limit never arise from walking it: a glyph truncated before its end-point array, end points that do not strictly increase, a glyph truncated before its flags array, a repeat flag with no count byte or a count that overruns the point total, a short- or long-form X/Y coordinate truncated before its own bytes, a composite chain recursing past the depth limit, an unreadable component list, a point-matched component, and a nested component's own decode failure propagating up through its parent. Add a fake GlyfTable that supplies simple-glyph bytes and composite component records directly, sidestepping both the real sfnt/glyf container and the composite record's own byte format, since decodeGlyphOutline reaches both only through GlyfTable's interface. Also cover the point-to-contour assignment directly with a hand-built two-contour glyph, which the real-font tests above only ever exercise incidentally.
…mInfo fields Extracts embedded-font-write.test.ts's own assemblePdf/AllocatedObject helper into test-support/write-pdf-fixture.ts so math-font-write.test.ts can build the same kind of fixture without duplicating it, dropping the unreachable "object never written" guard in the process: both current callers number their objects contiguously from 1, so the offset can be recorded inline as each object is written rather than looked up afterwards from a map that could theoretically miss. Also asserts the embedded CIDFontType2's own CIDSystemInfo Registry and Ordering decode to "Adobe" and "Identity" -- previously only Supplement was checked, leaving the two string literals with no test proving their actual content.
… and ToUnicode filtering math-font-write.ts had no test file at all (0% branch/function coverage): nothing exercised buildMathFontObjects, so its Type0/CIDFontType0 shape, its FontDescriptor's design-unit-to- glyph-space scaling, its /W array's own sort-by-glyph-ID, its FontFile3 compression, or its dropping of code-point-less glyphs from the ToUnicode CMap had ever run. Uses a synthetic MathFont with a non-1000 unitsPerEm (2048, a power of two so every scaled value is an exactly representable double) for the descriptor arithmetic, since the real vendored STIX Two Math font is drawn on a 1000-unit em and would make the scale factor an identity -- indistinguishable from a font with no scaling applied at all.
…nd stroke items The existing suite only ever exercised the "assembled-glyphs" item kind (54% statement coverage, 0% of writeGlyphRun/writeRule/writeStroke). Adds direct coverage of the other three MathLayoutItem kinds: an ordinary glyph run's own CID encoding (including skipping a character with no glyph in the font's cmap, and emitting nothing when every character is unmapped), a filled rule's top-left-to-bottom-edge re-anchoring, and a stroke's moveto/lineto sequence (including the under-two-points no-op case).
…cycle guard navigation.ts's own destination and outline logic (74% statement, 53% branch coverage) had several branches no fixture-based test ever reached: five of the eight display types (FitH/FitV/FitR/FitB/FitBH/FitBV), a bare non-negative-integer page number, a page element the page-index lookup can't place, an unrecognised or missing display type name, a duplicate name in the /Names /Dests tree specifically (as opposed to the old-style /Dests dictionary), the dest1/dest2/dest3 minting collision loop, an outline item's own missing /Title, its /A /GoTo destination path, and -- most importantly -- the outline cycle guard, never exercised at all. Calls parseDestination/createDestinationRegistry/readOutline directly against hand-built PdfObject values and a small ref-table resolver, rather than growing the existing FixtureBuilder-based PDF fixture to cover every one of these combinations by hand.
…he /Dests dictionary loop destsDict.entries is a Map, whose own key uniqueness already guarantees every name the old-style /Dests dictionary loop sees is distinct within that loop -- a dictionary literal's own duplicate keys, if the source bytes had any, were already collapsed to last-wins by the parser that built this Map, long before createDestinationRegistry ever runs. The duplicate check that loop carried could never observe a true duplicate, unlike the /Names /Dests name-tree walk immediately after it, which genuinely can encounter the same name from two different leaf nodes. Also strengthens the surrounding tests: several assertions only checked a diagnostic's code, not its message, letting a StringLiteral mutation on the message text survive; several others used toEqual against an object where an optional field's absence versus an explicit undefined value are the exact thing under test, which toEqual treats as equal and toStrictEqual does not.
…nt-write's byte packing Several survived mutants traced back to test fixtures whose chosen glyph IDs or code units happened to share a zero byte with the mutated one, making the mutation invisible: a two-Latin- letter glyph run where both CIDs fit under 0xff never exercises the high-byte offset for a second CID, and a surrogate pair whose low surrogate's own low byte is 0x00 never exercises the low-byte offset for a second UTF-16 code unit. Swaps in characters whose bytes are actually non-zero at the positions under test, and adds a two-point stroke (the boundary a "fewer than two points" check must not also exclude) and a synthetic font that deliberately collides two code points onto one glyph ID (proving collectUsedGlyphs' first-write-wins guard, which the real font's own injective cmap can never exercise).
…rithmetic, and dict keys FLAG_SERIF was never set in any test (every vendored face used elsewhere is a sans family); adds a dedicated case using the real, vendored Caladea (a genuine serif face). The subset tag's own comma separator and its base-26 letter-extraction direction had no test able to tell a comma-joined glyph list from a concatenated one, or floor-division from multiplication -- adds a collision pair for the former and an independently-computed expected tag (via the package's own already-tested crc32()) for the latter. The FontDescriptor's own /Type key, /StemV, and the CIDFontType2 dict's own /Type key were never read back at all; the /FontBBox check used optional chaining that let a blanked-out key vacuously pass with the array read as undefined instead of failing.
…y computed offset Replaces a pre-sized Uint8Array written at manually computed offsets (2 + i * 2, 3 + i * 2) with a plain array appended to in sequence, then converted once at the end. Removes the computed-offset arithmetic entirely rather than getting it right: the result's length now falls out of how many bytes were actually appended, instead of being asserted up front and then relied on to match.
Only axisHeightPt and fractionRuleThicknessPt were checked against the vendored STIXTwoMath-Regular.otf's real values; the other 25 *Pt fields metricsAt derives from math-table.ts's MATH_VALUE_RECORD_INDEX table went unchecked, so a wrong index (pointing a field at a neighbouring MathValueRecord slot) would leave axisHeight/fractionRuleThickness correct while every other constant silently read the wrong value. Expected design-unit values come from a standalone script reading the font's own sfnt bytes directly, the same independent verification method the surrounding test file's own top comment describes.
…function that reads it ENUMERATED_COLOUR_SPACES was a module-level constant, evaluated once at import time -- Stryker's per-test coverage analysis attributes a mutation to such static code to whichever single test happens to trigger the first import, not to the tests that actually exercise the enumerated-colour-space branch, so a wrong lookup table could silently ship undetected. Moving the Map literal inside readColourSpecification makes its construction run per call, so a mutation is correctly attributed to the tests that call it.
…ingDefaults PROGRESSION_ORDERS was a module-level constant, evaluated once at import time -- Stryker's per-test coverage analysis attributes a mutation to such static code to whichever single test happens to trigger the first import, not to the tests that actually decode a COD marker's progression order, so a wrong entry could silently ship undetected. Moving the array inside readCodingDefaults makes its construction run per call, so a mutation is correctly attributed to the tests that call it.
…Filter LIFT_ALPHA/BETA/GAMMA/DELTA/K were module-level constants, evaluated once at import time -- Stryker's per-test coverage analysis attributes a mutation to such static code to whichever single test happens to trigger the first import, not to the tests that actually exercise the irreversible 9-7 filter, so a wrong lifting coefficient (a sign flip on LIFT_ALPHA survived undetected this way) could silently ship. Moving the constants inside inverse97Filter makes their construction run per call, so a mutation is correctly attributed to the tests that call it.
Mearman
force-pushed
the
feat/100-percent-mutation-pdf-codec
branch
from
September 14, 2026 14:53
d05507d to
fb45c14
Compare
…nds check looksLikeBareCodestream's own data.length >= 4 check is redundant: with noUncheckedIndexedAccess, an out-of-bounds byte read is already undefined, and undefined === 0xff is already false, so the four comparisons already reject a short input on their own. readChannelDefinitions' end - start < 2 guard and readColourSpecification's end - start < 3 guard are likewise redundant: both functions' own later checks (entry + 6 > end, and the >= 7 / > 3 thresholds each branch needs) already refuse to act on a payload too short to satisfy them, whatever garbage a short read produces first. readBox never returns a box whose nextBoxStart fails to advance past its own offset -- it throws instead when a declared length would undercut its own header -- so the || box.nextBoxStart <= offset half of both box-walking loops' termination checks was unreachable.
…00 decoder Covers the isolated-byte and length-boundary cases looksLikeBareCodestream's own comparison chain needs, the extended (64-bit) box length's truncation and nonzero-high-word paths, a box declaring a length shorter than its own header, an image header box shorter than 14 bytes, a component count and a channel-definition type spanning both bytes of their field, a channel-definition count read from its own field rather than an adjacent header byte, a colr box too short for its own method byte and each method's own minimum length, two colr boxes (first wins) and a method the decoder does not recognise, a cmap-only palette box, two jp2c boxes (first wins), and the signature-box-recognised-but-truncated and signature-box-absent-but-box-shaped cases the "neither codestream nor box" / "no contiguous codestream" error messages depend on.
…ock-size check Exports MarkerCursor so its own bounds-checking and 32-bit assembly can be tested directly: readHeaderSegment, its sole production caller, already re-derives and re-checks segmentEnd against cursor.data.length before ever calling bytes(), so the length it passes always already satisfies position + length <= data.length on its own, leaving no way to observe that half of bytes()'s guard except by driving the cursor directly. Drops the codeBlockWidthExp > 10 and codeBlockHeightExp > 10 checks from readCodingStyleParameters: each exponent has a floor of 2 (from the SPcod "transmitted value + 2" encoding a few lines above), so either one alone exceeding 10 already puts codeBlockWidthExp + codeBlockHeightExp past 12 (11 + 2 = 13), which the sum check right below already throws for.
…rsor edge cases Adds a direct MarkerCursor suite (uint32 assembly, the bytes() bounds check's own length < 0 and overflow paths, and its position advancing past a read slice) alongside a hand-built minimal-codestream constructor for every header-segment guard a real encoder's own output never trips: SIZ's zero-component, short-component-list, no-area and zero-tile checks; COD's undefined-transform, code-block-area, progression-order and zero-layer checks; QCD's undefined-style check; a marker segment shorter than its own length field; COC/QCC/POC/RGN/PPT recording their own overrides; an unexpected SOC/SOD inside the main header; a main header missing COD or QCD; a tile-part header ending without SOD or running into a second SOT; a Psot shorter than its own header; the trailing-EOC trim on a tile-part's own data; and a tile-part header overriding only COD, or only QCD, or neither.
…e for testing Each of these three functions has a real, meaningful contract of its own (coordinate placement, symmetric extension, one-dimensional synthesis), but every one of their guards against a degenerate input -- mirrorIndex's length <= 1, synthesiseLine's length <= 0 -- is already unreachable through their sole production callers: inverseDwt53Level/97Level's own width <= 0 || height <= 0 guard returns before either ever gets called with i1 - i0 that small. Exporting them lets a direct test drive that input rather than removing the guard a future caller might still need.
…x-arithmetic cases Adds direct interleave/mirrorIndex/synthesiseLine suites for the guards and loop bounds only reachable that way (see the sibling refactor commit), zero-width/zero-height cases for both inverseDwt53Level and inverseDwt97Level, and two non-square, nonzero-origin reconstructions (one flat, one a single high-pass sample at an odd row and column) that distinguish an output-index mutant adding an axis origin back in from one correctly subtracting it -- indistinguishable from a flat signal or a zero origin alone, which every existing test before this one used.
…t cases Adds a component index wide enough to need its own two-byte field (257+ components), a signed SIZ component depth, a derived-style quantization's own step sizes, explicit per-resolution-level precincts from both COD and COC, a non-Latin COM registration that must not surface as a comment, an otherwise-unhandled marker segment (TLM) skipped without recording anything, the exact SOT length-mismatch message, a tile-part header running into EOC rather than SOD, a Psot landing exactly on an empty tile-part's own data with nothing to trim, and the three ways a tile-part's trailing bytes can fail to match the EOC signature without being trimmed. Also fixes two existing tile-part-override assertions that checked a field read back as undefined without checking it was genuinely absent as a key, which a mutant that always spread both cod and qcd together could satisfy by coincidence.
readTilePart, this function's sole caller, always passes a start sitting immediately after a real SOD marker (0xFF 0x93). Whenever the resulting range is under 2 bytes, at least one of the two positions the byte comparisons check falls on that marker's own fixed bytes instead of on real tile-part data -- and 0x93 can never be mistaken for 0xD9 -- so the comparisons already refuse a too-short range on their own, with no need to measure it first. Drops the now-unused start parameter along with it.
…ent boundaries Adds a hasOwn check for the tile-part-header-overrides-nothing case (the same gap the sibling COD-only/QCD-only tests were already fixed for: a field read back undefined doesn't prove it's genuinely absent as a key), a quantization step-size loop that stops exactly at its own segment boundary rather than one iteration short of needing another pair, a marker segment whose declared length runs exactly to the codestream's own end, an otherwise-unhandled marker segment (TLM) whose own body is deliberately shaped like a registration-1 COM segment so a mutant that misreads it as one would surface as a spurious comment, and a tile-part whose data is exactly the 2-byte EOC signature and nothing else.
…e-dimension guard interleave's own loops, sized from the same u0/u1/v0/v1, never iterate when width or height is non-positive (subbandBounds collapses each such range to an empty one), and both reconstruction loops below are bounded by width/height directly, so they no-op the same way. All a non-positive dimension could still threaten is scratch's own allocation, now floored at 0 the same way output's already is a few lines above -- removing the one remaining reason a caller needed the guard at all. Rewrites mirrorIndex's own negative-offset normalisation as the standard double modulo instead of a separate negative-offset branch: JS's % result already follows the sign of its dividend, so folding it into [0, period) this way needs no comparison of its own, and produces the identical result for every input the branching version did.
…ning edges Adds direct inverse53Filter/inverse97Filter suites that fill a buffer with a sentinel value distinguishable from anything either filter's own arithmetic would compute, then read back exactly which cells changed -- pinning each filter's own loop bounds directly rather than through the much larger surface of a full 2D reconstruction. Adds a scratch-allocation crash regression test for inverseDwt53Level/97Level covering the grossly-inverted-bounds case the sibling refactor commit's removed guard used to handle, and strengthens the fill-loop test to compare synthesiseLine's own fed source indices against mirrorIndex itself (already independently verified correct) rather than only their count and range.
…o a testable primitive HOR_SR's row loop writes each row at row * width, which for row === height lands exactly on output's own one-past-the-end index -- silently absorbed by TypedArray semantics (an out-of-bounds write is a no-op there, an out-of-bounds read is undefined) regardless of what that row's own reconstruction would have computed. A wrong loop bound is therefore unobservable through either function's own returned array, no matter what input a test supplies. Extracting the loop into times(), an exported, directly callable primitive, makes its own call count and argument sequence observable on their own terms instead.
…2/F-13 boundaries Adds a direct times() suite (call count and argument sequence, including zero and negative counts). Fixes the fill-loop/mirrorIndex comparison test's own length-2 bounds, whose period-2 mirroring makes i0 + k and i0 - k indistinguishable by parity alone, by widening it to length 4. Pins F-12's and F-13's own outermost cells (n = last + 1 and n = last, respectively) against exact Float32Array values computed independently from the same constants and equations the production code uses: both lie within F-8/F-9's own already-touched range, so only their specific numeric contribution, not which cells changed at all, can show whether either pass's own loop reached that last iteration.
…ound-trip mirrorIndex immediately computed position - i0 as its own first step, so every caller had to add i0 back on only for this function to subtract it straight back out. Taking the offset from i0 directly removes that round-trip and, as a side effect, removes the one call site (i0 + k) that could never actually be distinguished from a caller mistakenly writing i0 - k: mirroring about i0 is symmetric in the offset by definition, so offset and -offset always mirror identically regardless of which one a caller happens to pass in.
…et-from-i0 signature Updates every call site for mirrorIndex's new offsetFromI0 parameter, adds a nonzero-i0 case that genuinely exercises the difference between an offset and an absolute position (every prior case used i0 = 0, where the two coincide), and simplifies the fill-loop ground-truth comparison now that the call site passes k directly rather than i0 + k.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Working through pdf-codec's Stryker mutation baseline toward a genuine 100% score, no suppression comments.
First landed change: the unit suite's crypto and font tests were timing out under Stryker's instrumented dry run on this shared, heavily-loaded dev machine, not because of a defect in any one test but because instrumentation overhead scales with contention this machine has a lot of. Replaced scattered per-test timeout overrides with a single package-wide UNIT_TEST_TIMEOUT_MS.
Baseline mutation score and breakThreshold derivation still to come once a full run completes.