Conversation
Mearman
force-pushed
the
feat/100-percent-mutation-pdf-codec
branch
7 times, most recently
from
September 14, 2026 19:39
21c66d4 to
3961095
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.
loadFace took a separate name string purely to key its cache Map, with no other use -- every StringLiteral mutant on those five names was unobservable through any of the exported getters, since nothing else ever looked up that key. Each face's deflated-base64 constant is already a unique identifier, so keying the cache by that directly removes the redundant parameter along with every mutant on it, and a new test-support/fonts.test.ts covers the caching behaviour itself (identical instance on a repeat call, real bytes on a first call, distinct bytes across two different faces).
… to their one caller Both were module-level constants read by exactly one function each. A module-level initializer only runs once per process, so any mutation to it is only ever active during that single, already-passed evaluation -- no later test run can observe a difference no matter what it asserts, since the correctly-evaluated value is what every subsequent call sees regardless of which mutant is nominally active. Moving each into its one caller's body makes it re-evaluate per call, where it is reachable again, and a new case-insensitivity test for stripStyleSuffix's regex flag covers the one gap that reachability alone didn't already close.
Adds diagnostic-message assertions to the existing bfchar/bfrange truncation tests, plus new cases for: a stray non-end keyword mid bfchar/bfrange section (proving the end check is genuinely comparing against the exact keyword, not any keyword), a bfchar/bfrange entry whose destination or high-end token isn't a hex string, a bfrange single destination too short to carry one UTF-16BE code unit, a base unit whose high byte is non-zero (the previous fixture's 0x0041 base unit couldn't distinguish a wrong byte offset from the right one since its high byte was already zero), a bfrange destination that is neither a hex string nor an array, and an array destination truncated before its closing bracket.
…and cover every branch SEMANTIC_SUBTYPES and OWNED_ELSEWHERE_SUBTYPES were module-level Sets each read by only that one function, which put every one of their entries beyond Stryker's reach for the same reason recorded for font-style.ts. Moves both into the function body. Also adds the coverage the file was missing entirely: a bare Link/FileAttachment/Widget/Popup annotation is genuinely skipped, a non-Text annotation is never mistaken for the presenter-notes marker just because its /T matches, an annotation missing /Contents, /T, and /M omits those keys outright rather than carrying them as undefined, markupFields rejects both a too-short and an empty (below-8-but- already-a-multiple-of-8) /QuadPoints, and a missing /Rect reports its diagnostic with the right code and message before the entry is dropped. roundtrip.test.ts's existing hidden-notes test now also asserts the annotation itself never leaks into the annotations list, not only that its kind is excluded from visible LayoutItems.
Every existing caller round-trips assemblePdf's output through this package's own readPdf, which tolerates a mangled xref table, missing trailer/startxref/%%EOF markers, and wrong offsets via its recovery scan -- so none of them could ever observe assemblePdf itself writing the wrong marker, sort order, xref count, offset width, or trailer field. Adds a dedicated suite that decodes the raw output bytes and asserts on them directly: ascending object order regardless of input order, the exact header/marker literals, the xref subsection count, each entry's fixed-width zero-padded offset pointing at that object's real byte position, and the trailer's /Size and /Root values.
…gainst its own subtable write The eager markFilteringSet write and the subtable-placement loop target the same byte offset whenever no markFilteringSet slot is reserved, and the loop always runs after the write -- so a wrong guard there is invisible for every lookup with at least one subtable, since the real subtable bytes simply overwrite whatever the guard wrote first. A lookup with zero subtables has nothing to overwrite it with, so the same wrong guard instead writes two bytes straight past the end of a table sized for no such slot. Simplifies the flag check itself: bitwise AND already coerces an absent flag to 0, so the explicit undefined check was redundant with no observable behaviour of its own, and removing it removes an unkillable mutant along with it. A new empty-subtables test covers the guard directly.
Every real standard-14 AFM defines a width for every WinAnsi-mapped glyph, so this guard's throw is unreachable through the public API with real data -- it exists purely as a caller-invariant check against a future data gap. STANDARD_METRICS is already exported for testing (the monospace short-circuit spy above it does the same), so this deletes one real widths entry, exercises the guard, and restores it.
…/CFF-table guards Adds a dedicated test-support/cff.test.ts (none existed before) that pins cffIndex's offSize selection at each exact boundary (0xff, 0xffff, 0xffffff) rather than relying on incidental coverage from unrelated charstring tests. Extracts stixMathCffBytes's two guard clauses into a new cffTableFromSfnt so they can be driven directly against a small synthetic sfnt built with buildSfnt -- neither guard is reachable through the one real 691 KB vendored asset, which always parses successfully. Also stops cffFontWithCharstrings's local-subrs default from going through an always-unreachable ?? fallback: hasPrivate is already exactly the same check as the default's own condition, so narrowing directly on options.localSubrs lets TypeScript rule the fallback value out rather than leaving dead code behind it.
…erence ranking buildCmapLookup's preferenceRank ranked format 12 subtables and a (3, 10)/(0, *) platform preference among competing subtables, but no test ever built a format 12 subtable or a font with more than one candidate subtable, leaving that ranking and the whole format 12 reader (parseFormat12, its own header/group-array truncation guards, and its forEachMapping clamp to the last valid Unicode code point) unexercised. Also covers format 4's own idRangeOffset !== 0 glyph-index-array path (only the idDelta-only path had a fixture), a subtable in an unsupported format being dropped without disturbing its siblings, and a cmap whose own subtable-record array or an individual record's offset doesn't fit the table.
…operators Extends the hand-built charstring fixtures to cover paths the vendored real STIX Two Math font's own well-formed charstrings never reach: the flex family (escape 12 35/34/36/37) and each one's own too-short-stack guard, hmoveto and vmoveto (the font's own charstrings apparently never use either, always preferring rmoveto), the 16.16 fixed-point and positive/truncated 16-bit integer operand forms, callgsubr's own global-subroutine and bias selection (previously only exercised through callsubr's local one), a failure several levels deep in the charstring still propagating even once the glyph has already drawn something, vvcurveto's leading cross-axis delta applying to only the first of several curves, and the exact off-by-one boundaries of the subroutine-nesting depth, operand-stack size, per-glyph operation ceiling, and the rlineto/rcurveline/rlinecurve/hstem-width-parity loops. Also adds the Global Subrs INDEX's own medium-to-large bias threshold at 33900 entries, the mirror of the existing Local Subrs 1240-entry test. Moves the shared boundsOfOnlyGlyph fixture helper to module scope so both charstring-interpreter describe blocks can use it, and adds an enc() helper that picks whichever of the two numeric operand encodings a given value needs.
…PFB form readCffCharset and readCffEncoding each read three on-disk shapes (format 0/1/2 for the charset, format 0/1 plus an optional supplement for the encoding), but every existing fixture built only format 0 of each, and the predefined ISOAdobe charset and predefined StandardEncoding paths (a font stating neither operator at all) had no fixture either. Also covers a PFB-segmented Type 1 program (a 6-byte binary segment header ahead of the same cleartext this module already reads for a bare PFA program) and a program whose cleartext header never reaches an eexec marker at all. Generalises cffFontWithBuiltinEncoding's own fixture builder to choose the charset and Encoding format, and to add the Encoding's own supplementary code -> SID entries, rather than always emitting format 0 of each.
…uccess observable A charstring that draws nothing reports undefined from a successful walk and from a failed one alike, so a boundary test that never draws (the operand- stack, operation-count, and truncated-operand cases) cannot actually tell a correct exact-boundary success apart from an off-by-one bug that rejects it one iteration early -- both assert the same toBeUndefined(). Adds a trailing line draw after each boundary so success produces a real, checkable box, and extends the existing "failure still propagates once something is drawn" case to the operation ceiling, operand-stack overflow, subroutine-depth overflow, and a truncated operand, which shared the identical blind spot.
WINANSI_GLYPH_NAMES defines a glyph name for every code in FIRST_CHAR..LAST_CHAR (unassigned CP1252 positions get a placeholder name like "bullet" rather than an empty string), and every standard-14 AFM table defines a width for every glyph name that table can produce. widthOfCode() therefore never throws across the full range for any of the 12 faces, so the "no WinAnsi glyph mapping" fallback to a 0 width was unreachable dead code. buildFontObjects now calls widthOfCode directly.
…ace, and embedded formulas writePdf's own doc.metadata fields (title/author/subject/keywords/creator/ createdIso/modifiedIso), computeFontFlags' fixed-pitch/serif/italic/force-bold bits, the full real AFM Widths array, and prepareJpegImage's colour-space and CMYK /Decode-inversion branches had no coverage at all. options.formulas -- writePdf's own side channel for embedded-math-font content -- had never been exercised through writePdf itself, only through math-content-write.ts and math-font-write.ts's own lower-level unit tests, leaving the actual object allocation, resource-dict wiring, and per-page content-stream routing untested.
destinationViewArray's fitH/fitV/fitR/fitB/fitBH/fitBV branches (and the plain 'fit' case), each with and without their own optional coordinates, had no coverage -- only the default 'xyz' view was ever round-tripped through an internal link. Also covers resolveDestinationArray's own "page index beyond the document" guard, which had no test at all.
…axis extrema rmoveto had no test at all, and neither hmoveto nor vmoveto was tested with its own optional leading width operand present -- takeWidth's evenArgs=false branch (moveto's own arity-based width detection) was entirely unexercised. includeCubicAxis's own root-finding had two branches with no direct hand-built coverage: the genuinely non-degenerate quadratic case with two distinct real roots, and the degenerate (a === 0) linear fallback a curve with collinear control points on one axis produces. The real STIX Two Math font's own glyphs exercise curve extrema in general, but not these two specific coefficient shapes.
…rities endchar's own arity rule (its leading width shows up as exactly 1 or 5 operands, distinct from every other stack-clearing operator's takeWidth-based detection) only had a test for the 4-operand bare-seac case. A bare width (1 operand) and a width-plus-seac (5 operands) were both untested, so neither of those two boundary values on stack.length was actually exercised.
… boundaries
A scoped mutation run against write.ts (after the earlier coverage additions)
surfaced a large batch of Survived mutants in code that was now reached but
not precisely asserted:
- Image XObject dict entries (Type/Subtype/BitsPerComponent/Columns/Rows/
BlackIs1) were exercised but never checked, so a wrong or blanked-out key
name went unnoticed.
- The CMYK JPEG /Decode inversion only ever ran against 4-component assets,
so the "info.components === 4" guard itself was never independently
proven -- a 3-component asset with a matching Adobe transform now confirms
the guard, not just the transform value, gates the inversion.
- Font and image resource naming ("/F1", "/Im1") was only checked for
existence, never for which underlying object each name actually pointed
at, so removing the sort-by-name/sort-by-id step left the tests green.
- Several "only when non-empty" branches (optional-content ON/OFF arrays,
AcroForm's own field-count guard) were exercised exclusively with
non-empty input, so the boundary itself was never distinguished from an
unconditional branch.
- A group AcroForm field carrying more than one widget must stay a single
object (multi-widget splitting is a terminal-field concept); nothing had
ever exercised a group with more than one widget to prove that guard is
real, as opposed to redundant.
- objectContainsReference's dict and stream branches were exercised only
through an array-shaped residue row.
…-by-name preparePassthroughImage's own /Type, /Subtype, /ColorSpace, and /BitsPerComponent entries were never checked directly, and the JBIG2-vs-JPX branch (objectContainsReference's own filter === "jbig2" check) had no test proving a JPX asset gets neither key -- only that a JBIG2 one gets both. resolveDestinationArray's own destination lookup was only ever exercised with a single-entry destinations table, so a predicate that ignored the name entirely and returned the first entry would have passed unnoticed; the internal-link Annot dict's own /Type, /Border, and error-message text were similarly unchecked.
…rm /FT and /Ff The outline tree's own /Type, /Parent, /Prev, /Next, /First, /Last, and /Count entries were exercised by the existing round-trip test but never checked directly -- read.ts's own outline walk doesn't depend on most of them, so a wrong or blanked-out key name went unnoticed. An attachment's optional /Desc entry was only ever exercised with a description present, so the "carries no description" branch was never distinguished from an unconditional one; radio, button, and signature field types had no test naming their own /FT value; and the /Ff flag bits (read-only, pushbutton, radio, combo) had no test at all, independently or combined.
checked/value only had one combination exercised (checked: true with no value); the field's own three other meaningfully distinct outcomes -- an unchecked box, a box with neither flag set, and an explicit export value overriding checked in either direction -- had no test naming the /V PDF name each one actually produces.
…ibute /Type /StructElem, /P (parent reference), and the per-element /Lang override were exercised by the existing round-trip test but never checked directly -- read.ts's own structure walk doesn't depend on /Type or /P at all, so a wrong or blanked-out key name went unnoticed, and no test carried a language attribute at all.
…guard Summing zero extender parts is exactly 0, and extenders.length * minConnectorOverlap is exactly 0 too when extenders is empty, so growthPerRepeat is always 0 in that case and the growthPerRepeat <= 0 guard already returns the same minimum on its own. Add a test proving the no-parts case still returns undefined rather than a hollow zero-size construction.
The regex's capturing group is not itself optional, so a successful match always populates match[1] (with the empty string in the degenerate zero-width case) -- there is no absent-group case for the ?? "" fallback to actually handle.
…'s own placement offset No real vendored composite in this suite's own fonts ever sets SCALED_COMPONENT_OFFSET (bit 11) without also setting UNSCALED_COMPONENT_OFFSET (bit 12) -- Microsoft's own OpenType toolchain never emits that combination, only Apple's does -- so this placement path was only reachable through a hand-built fixture.
Exercises the unreadable-sfnt, missing-required-table, and no-readable-cmap-subtable error paths -- invariant checks on this package's own build output, never reachable through the real vendored font. loadMathFont() caches its result in a module-scoped variable, so each case uses vi.resetModules() plus a dynamic re-import to get a clean, uncached instance, with vi.doMock failing exactly one real dependency while every other real parser still runs underneath it.
…-naming gaps Adds direct coverage, against a synthetic catalog, for an /OCGs entry that fails to resolve to a dictionary (reported and skipped), and for mintLayerName skipping an already-claimed layerN name so two distinct groups can never collide onto the same layer.
…rser Builds a minimal but structurally real 'MATH' table field-by-field -- the 10-byte header, a zero-filled MathConstants subtable, a zero-filled MathGlyphInfo subtable, and an optional MathVariants subtable built from a per-axis coverage/construction description -- the same not-mocked, real-byte-parsing approach cmap-table.test.ts's own buildFontWithCmapSubtable already uses.
…Date Covers decodePdfString's UTF-16BE-with-BOM, plain-ASCII, and zero-byte paths, and parsePdfDate's undefined/non-date/full/partial inputs -- including ISO 32000-1 7.9.4's every-field-after-the-year default and the per-field partial-default cases (a year-only date, a year+month+day date, and a date with a sign and hour but no offset minute).
Mearman
force-pushed
the
feat/100-percent-mutation-pdf-codec
branch
from
September 15, 2026 14:16
9d0f97e to
7858055
Compare
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.
Progress notes (round 3)
Fixed cff-bounds.ts's charstring-interpreter gaps from 230 down to 122 (scoped
--mutate src/cff-bounds.ts --forceruns, verified before/after). Also added cmap-table.ts format-12/preference-ranking coverage and builtin-encoding.ts charset/encoding-format coverage (unit-tested, not yet re-verified with a scoped Stryker run this round).Two things worth knowing before continuing on cff-bounds.ts:
A "must draw first" blind spot recurs across the file. A charstring that draws nothing reports
undefinedfrom a successful walk and a failed one alike, so a test for a failure path (a truncated operand, an interpreter ceiling, a reserved operator) that never draws anything first cannot actually distinguish correct behaviour from areturn true/return falseswap — both assert the sametoBeUndefined(). Every remaining "refuses X" test that doesn't already draw something before hitting the failure should get adrawFirstprefix and an assertion that the earlier box is lost, following the pattern in the "propagates a failure...even after this glyph has already drawn something" test.A few remaining survivors look like genuine equivalent mutants, confirmed by literally applying the mutation and running the whole suite (not just reasoning about it):
takeWidth's own leading-width shift (lines ~247-256): every caller either reads only the tail of the stack (moveto family — removing a front element never changes the last k values) or only the stack's length viaMath.floor(n/2)(the hint operators — floor(n/2) == floor((n-1)/2) whenever a width was actually present, i.e. n is odd), andendchar's own separate width check doesn't cross its own<4seac boundary differently either way. Emptying the whole function body still passes all 1867 package tests.if (b0 > CHARSTRING_OPERATOR_LIMIT) return falseis redundant with the switch statement's owndefault: return falsea few lines down, since every valid operator is documented and enforced to be ≤31 — any byte that reaches this check and fails to decode as an operand necessarily also fails to match any switch case.flex1's owndx/dyaccumulators (lines 674-675) are only ever consumed throughMath.abs(dx) > Math.abs(dy); flipping+=to-=negates a sum that starts at 0, andMath.absof a negation equals the original, so the comparison's outcome is unconditionally unchanged.None of these have an obvious simplification that wouldn't itself reduce robustness for a case this module doesn't currently exercise (e.g.
takeWidthremains correct per spec even though no current caller happens to observe its effect) — flagging for a fresh look rather than deleting working, spec-motivated logic to chase the metric.Remaining in cff-bounds.ts: mostly the curve-extrema math (
includeCubicAxis's degenerate/discriminant branches), a handful of loop-boundary arithmetic mutants in the multi-curve operators, and thereadPrivateSubrs/header-validation edge cases. The rest of the package (write.ts, gsub-table.ts, the image codecs, etc.) hasn't been touched yet.