Conversation
Mearman
force-pushed
the
feat/100-percent-mutation-odf.js
branch
6 times, most recently
from
September 14, 2026 15:00
ed21689 to
787e714
Compare
…boundaries isBareNewlineRun's own nine-field AND-chain (deciding whether a "\n" run is a genuine text:p paragraph break or a formatted text:line-break) had no direct coverage at all: existing tests only exercised the "all fields absent" and "bold + hyperlink together" shapes, leaving every other field's own undefined-check untested. Adds one test per field plus the bare-newline case. formatOdfDuration's regex and fraction branches, writeCellValueAttributes' boolean/currency/exactValue branches, and cellSourceRuns' empty-displayText fallback get the same direct coverage. Exports canonicalColor/canonicalCellFill/canonicalRun/canonicalCellValue/ canonicalCell/canonicalCells/canonicalColumns/canonicalRows/ canonicalSheetImage/canonicalImages/canonicalPrintSettings/ canonicalDataValidations/canonicalConditionalFormatStyle/ canonicalConditionalFormats and pins each directly against a literal expected value: normaliseOdsContent applies every one of these to BOTH sides of the write-round-trip suite's own equality check (the real reader's output and the original document, each normalised the same way), so a mutation confined to one of these helpers changes both sides in lockstep and was invisible to that comparison -- only a direct, one-sided assertion can observe it.
canonicalCellFill's own pattern-fill fixtures used "gray50", which is
not a member of ContentCellPatternTypeSchema (the enum spells Excel's
own fixed-density greys as "mediumGray"/"darkGray"/"lightGray" through
"gray0625", never a percentage-style name). canonicalPrintSettings'
fitToPages/repeatRows/repeatColumns fixtures used field names from a
different schema shape (widthPages/heightPages, startRow/endRow,
startColumn/endColumn) rather than the real {width, height} and
{start, end} shapes ContentSheetPrintSettingsSchema actually declares.
…r-kind fill fields regularPolygonSubpath/fixedPresetSubpath's own trig and midpoint arithmetic had only a vertex-count assertion (it.each over diamond/triangle/pentagon/ hexagon/octagon), never the actual coordinates, so every arithmetic operator in the angle/radius formulas could flip with nothing to catch it. Adds exact coordinates for isosceles-triangle, right-triangle, and a full six-point hexagon check against the same trig formula the source uses. roundedRectSubpath's own eight segments (four straight edges, four cubic corner arcs) were pinned only at the start point; the other seven segments' own w-radiusPt/h-radiusPt/radiusPt-k/radiusPt+k arithmetic had no coverage at all. Adds the full segment-by-segment coordinate check, plus the two boundary cases (readRoundRectangleRadiusPt degrading to a plain rect for a zero-width viewBox and for a resolved radius of zero) neither had a test. fillPattern/fillOpacity's own "carry through only when defined" pair is duplicated identically across readDrawRectVector, readDrawEllipseVector, readDrawPathVector, and readCustomShapeVector, but every existing test exercised it through draw:rect alone -- the other three vector kinds' own copies were untested. Adds one fillOpacity case per remaining kind.
…op two dead closeListPlan calls
canonicalParagraph/canonicalCell/canonicalTable/canonicalMetadata/
canonicalImage are applied identically to both sides of every
odt/odp/odg/draw-vector writer's own round-trip equality check, so a
mutation confined to one of them changes both sides in lockstep and
was invisible to that comparison -- exactly the odf.js/typed/ods/
write.ts precedent this mirrors for the odt/odp/odg-facing sibling
module. Adds direct, one-sided unit coverage for every field-by-field
branch, the colSpan/rowSpan covered-grid loop's own off-by-one
boundaries, and the per-cell list-run scoping canonicalTable threads
through a shared ListPlanState.
Also removes two closeListPlan(listState) calls (canonicalTable's own
leading call, and the one immediately after canonicalCell inside the
per-cell loop): both are unconditionally overwritten by a later
closeListPlan before anything could ever observe their own effect --
the leading call by the per-cell close that precedes every cell's
canonicalCell invocation (including the first), and the post-cell
call by either the next cell's own leading close or, for the last
cell, the table's own trailing close. Neither was reachable by any
test because neither can ever change behaviour; deleting them removes
the mutation opportunity rather than chasing an equivalent mutant.
Simplifies canonicalParagraph's own protected-boundary set for
segmentOdfParagraphRunsMapped: the merge loop only ever tests
protectedBoundaries.has(index) for index in [0, runs.length), so a
stated {0, runs.length} pair contributed nothing to either branch's
outcome (index 0 never merges regardless, having no preceding group
yet; index === runs.length is never reached by that loop). The false
branch now passes an empty Set with no array literal to mutate; the
true branch keeps only the construct extents' own interior
boundaries, which are the one real source of boundary positions the
merge decision ever consults.
…ial directly parseOdfAngleDeg had no test coverage at all -- deg/grad/rad conversion, the bare-number-defaults-to-degrees case, and the malformed-input path were all unexercised. Adds direct coverage for every branch, including the grad/rad conversion constants. Exports and directly tests isLengthUnit and expandExponential rather than only reaching them through parseOdfLength/formatOdfNumber: both are pure predicates/formatters whose own internal branches (the six-way unit check, expandExponential's pointIndex <= 0 / >= digits.length boundaries) were only ever exercised with values the caller's own regex had already narrowed, leaving several branches equivalent in practice. Direct string-input tests pin the exact pointIndex boundary cases (0 exactly, digits.length exactly, strictly between, negative) that were previously unreachable through formatOdfLength's own floating-point call sites. Replaces expandExponential's and parseOdfAngleDeg's own sign/integerDigits/exponent === undefined re-checks with a plain comment plus non-null assertions: each of those regex groups has no `?` quantifier of its own (only the alternation inside them does), so none can ever actually be undefined once the enclosing match is non-null -- the re-check was dead code no input could reach, not a guard against a real failure mode.
…t.ts list.ts had no test file of its own at all: resolveOdfListKind, mintOdfListNumId, buildOdfListStyle, writeOdfList, listKindOf, canonicalNumId, planListMembership/closeListPlan, and readOdfListParagraphs were exercised only incidentally through odt/odp round-trip fixtures, which never happened to hit several of their own branches (a level-1-only style-kind check, the nested-list write-side's own level-jump/dedent handling, the run-boundary sentinel planListMembership mints for a membership with no incoming numId). Adds a dedicated suite covering every exported function directly: each of the three list-kind resolutions (ordered/bullet/bullet-via- image) and their negative cases, the ten-level style builder's own per-level indent arithmetic, writeOdfList's nesting/dedent/level-clamp behaviour, and the list-plan run-boundary semantics closeListPlan and a changed incoming numId both need to preserve.
…ression.ts skipExpression/takeExpression (the balanced-paren/brace/quote-aware expression splitter both typed/ods/data-validation.ts's table:condition reading and typed/ods/conditional-format.ts's calcext:condition reading share) had no test file of its own -- both readers exercised it only through their own real-world condition-string fixtures, which never happened to cover brace nesting, single-quoted strings, an unterminated quoted string running to the end of the text, or a whitespace-only span correctly yielding undefined rather than an empty string.
…floor A full run after this batch's own survivor kills (canonicalise.ts, units.ts, metadata.ts, list.ts, expression.ts) measures 75.72% of 8444 valid mutants, up from the 71.31% baseline this threshold was originally derived from. Still provisional -- the bulk of this package's own files carry their own unkilled mutants and have not been touched yet.
…ckage's XML sniff hasUtf8Bom's compound bytes.length >= 3 && bytes[0] === 0xef && ... condition mutated to several sub-clause "true"/"false" variants that survived: through looksLikeXml alone, a wrongly-detected BOM and a correctly-rejected one routinely land on the same XML/binary verdict downstream (a too-short array ends the scan at the same byte either way), so no test built only on the final classification could distinguish them. Extracting it into its own exported, directly tested function pins every boundary (a too-short array, each byte individually mismatched) against its actual boolean return value instead. Also replaces the four-way whitespace disjunction (b === 0x20 || b === 0x09 || ...) with a Set.has() check, and the manual index/while loop with a for...of over bytes.subarray(start): both removed a class of survivable sub-expression and loop-boundary mutants outright rather than chasing them through indirect byte-array engineering, since a byte either belongs to the whitespace set or doesn't and a subarray iteration carries no explicit index comparison to mutate.
…ms.ts readOdfFormDefinitions and readOdfFormControlConstructs had no dedicated test file at all, only indirect exercise through odt/odb round-trip fixtures that never varied every optional attribute, control-tag mapping, or checkbox/listbox branch independently. Adds direct coverage for every branch: each optional field on a form definition and on a control (present and absent), nested form:form as subForm vs control, form:properties/text-node exclusion from both control and subForm scanning, every CONTROL_TYPE_BY_TAG mapping, the unmapped-tag richText degrade with whole-element residue, current-value/value precedence, checkbox/radio's current-state-derived checked field (and its absence on every other control type), listbox option label/value precedence and the neither-present skip, and per-control form:properties residue. Switches this file's toEqual assertions on absence to toStrictEqual: toEqual treats an explicitly-set `field: undefined` the same as the field being absent, so an `if (x !== undefined) descriptor.field = x` mutated to `if (true)` was invisible to a toEqual comparison even though it changes the object's own own-property set. Raises odf.js's mutation break threshold to the re-measured floor now that typed/shared/forms.ts has zero survived and zero no-coverage mutants of its own.
…eaders Adds direct unit coverage for readUint16LE/readUint32LE/localFileHeaderNames/ assertMimetypeEntryLayout: truncated-input throws (including negative-offset isolation of the first missing byte), exact-boundary reads, multi-entry offset arithmetic through a non-zero extra field and compressed size, and each of assertMimetypeEntryLayout's six field checks. Also replaces the per-byte "b0 === undefined || b1 === undefined || ..." guards in both readers with a single [offset, offset + byteCount) range check. The per-byte form could never be killed in full: a real Uint8Array's undefined region is always a contiguous prefix or suffix, so no input can isolate an interior byte (b1 of 4, say) as the sole missing one, leaving that comparison an unreachable, equivalent mutant. The range check has no interior case to isolate.
…lpers Adds direct unit coverage for the five kind-narrowing functions (wordprocessingPackage/presentationPackage/spreadsheetPackage/ drawingPackage/formulaPackage), each checked both on a matching-kind package (returns it unchanged, no throw) and a wrong-kind one (throws the exact "expected a ... package, got ..." message) -- neither path was ever exercised by odf.js's own reader suites, which only ever hand these functions a correctly-kinded result. Also covers assertPackageRoundTrip's three checks individually, each isolated so exactly one fails while the other two still pass: a schema-invalid tree via a malformed `fonts` field (schema-checked but read by neither flattenTree nor factorStyles, which carries an existing value through verbatim rather than recomputing it); a tree that flattens to something other than the given content; and a tree carrying an extra, unreferenced styles-table entry that a fresh mint of its own flattened content would not reproduce.
Pins buildXml's pi/declaration/element/text/comment/cdata node mapping directly, including the empty-array shape for pi and declaration nodes that fast-xml-parser's ordered builder reads only from a node's own ":@" attributes rather than its array value, and the throw path when the underlying XMLBuilder does not return a string. Extends the package's existing no-deprecated exemption for the deprecated XMLBuilder class from build.ts to its own test file, which necessarily references the identical class to reach BUILDER's shared prototype.
Factors the pi/declaration cases' shared "ignored by fast-xml-builder" array literal into one constant typed as the empty tuple `readonly []`, turning a content mutation there into a type error rather than a silent, unobservable survivor (fast-xml-builder never reads either node shape's own array value). Exports toOrderedNode so a test can pin the exact intermediate ordered-node shape directly, in particular that an attribute-less element's object carries no ":@" key at all rather than one holding an empty object, a distinction the built XML string never renders differently, so no test on buildXml's own output could observe it.
Covers PNG/JPEG/GIF87a/GIF89a magic-byte detection, the too-short and empty-input cases, and SVG sniffing from either an XML prolog or a bare root tag with leading whitespace, including the deliberate window-size cap that lets a root element sitting past the first kilobyte go undetected rather than scanning an unboundedly large file. Removes startsWith's separate "bytes too short" guard: an out-of-bounds Uint8Array read is undefined, which never strictly equals a real signature byte, so the comparison loop already returns false for a too-short input on its own, the guard produced no outcome the loop didn't already produce.
Pins bytesToBase64 and base64ToBytes against the classic Wikipedia "Man"/"Many hands..." progressive vectors, one per length mod 4 so every padding branch is exercised both true and false, plus the whitespace-stripping clean-up regex, the invalid-padding-position throw, and the fixed-size scratch buffer silently bounding a malformed, non-4-multiple-length decode rather than growing to fit it.
…text columnLettersToIndex had no direct test at all: adds cases for a valid uppercase reference and the three invalid shapes (lowercase, a trailing digit, empty) that must return undefined rather than delegate to the schema helper. Sharpens TableCursor's repeat-count error assertions from a generic /positive integer/ pattern match to the caller name itself (TableCursor.nextCell / TableCursor.nextRow), since the generic pattern alone can't tell the two call sites' own error text apart.
… mixed case document-schema.js's own columnLettersToIndex uppercases its input before validating, so it alone can't distinguish "aA" or "Aa" from "AA"; each of odf.js's own ^ and $ anchors, if dropped, would let exactly one of those two mixed-case strings reach the schema helper undetected instead of being rejected up front.
Adds negative, non-canonical-spelling (leading zero), and exact-zero text:c cases to getOdfSpaceCount's own guard, each isolating one of its three disjuncts; adds a child-carrying bookmark/marker case to measureOdfNodeLength and decodeOdfText, since an empty-children marker can't tell "recursed into nothing" apart from "never recursed". Drops the space-run scanner's redundant `end < text.length` bound: an out-of-range string index is undefined, which is never `=== SPACE`, so the comparison loop already stops there on its own.
…old.ts Pins createOdfPackage's exact XML declaration, office:version stamping (both the given version and the DEFAULT_ODF_VERSION default), the body element nesting inside office:body, and the mimetype part it writes. Covers both of odfPartContainer's throw paths directly: a part path that resolves to a non-XML part, and an XML part with no container matching the requested tag, alongside the already-implicit success path returning a real container.
…der.ts Covers parseBorderEdge's whitespace tolerance (leading/trailing, and a run of several spaces collapsing to one separator), its wrong-token- count/unparseable-length/unparseable-colour/zero-or-negative-width rejections, the none/hidden marker, an unmapped style token leaving style unset, and formatBorderEdge's own solid-style default.
Pins resolveOdfListKind's undefined-style-name short-circuit against a package carrying a real, matchable, ordered list-style whose own style:name attribute is absent (so attrValue coincidentally also resolves to undefined), and extends the existing "only a level-1 child counts" case from the ordered path to the bullet and image paths, each previously untested. Pins buildOdfListStyle's ordered branch to also carry the indent properties every level already carries on the bullet branch, and adds a readOdfListParagraphs case where an item child's tag is neither text:p/text:h nor text:list, carrying its own text:list-item/text:p descendants specifically so a wrongly-permissive recursion would surface them. Drops writeOdfList's redundant tag check on an existing host: every element this function ever pushes onto an "enclosing" list's children is already a text:list-item via its own construction, so an element found there carries no other tag to distinguish from it.
Adds cases for rotate() called with no argument, an unmodelled function's own args never being parsed as translate's just because they happen to look like valid lengths, and a run of several spaces between translate's two arguments collapsing to one separator. Drops parseOdfTransform's redundant name/argsRaw undefined guard: FUNCTION_PATTERN's two capture groups are both plain, non-optional captures, so a successful match always populates both. Replaces the split-then-filter empty-string removal with an explicit empty-argsRaw check, the only case split can actually misbehave on given FUNCTION_PATTERN's own surrounding whitespace trim.
Types the shared "no arguments" empty array as the tuple `readonly []`, matching build.ts's own NO_ORDERED_CONTENT pattern: a content mutation there is now a type error, not a silent, unobservable survivor. Drops rotate's separate angleArg-undefined guard: Number(undefined) is NaN, which the isFinite check right below already rejects identically to a genuinely present but unparseable angle.
"sdt" is not a member of ContentControlType; use "richText", a real non-index control type, for the pass-through-unchanged test case.
A first verification pass against the earlier direct-coverage batch left 20 mutants standing, every one masked by an inner guard, a default-descriptor fallback, or vitest's own toEqual treating an explicit undefined property the same as an absent one, rather than by missing coverage outright: - toHaveProperty checks replace toEqual wherever the assertion needs to distinguish a genuinely absent optional field from one explicitly set to undefined. - The paragraph-edge guard test for a mismatched half.parent now uses a parent that genuinely contains the half element, so a bypassed guard would actually reach edgePosition instead of being caught by the unrelated indexOf === -1 check right after it. - Every pairOdfMarkerHalves/resolveOdfMarkerEvents start/end-count test now gives its halves a real resolving descriptor, so a bypassed length check would actually build an extent rather than being masked by the unrelated "descriptor resolved to undefined" guard further down. - insertOdfConstructMarkers' sort-order test now passes its two extents in the wrong initial order, so a collapsed comparator (a stable sort's no-op) is distinguishable from a real sort. - collectOdfNamedExpressions' missing-name test now supplies a real cell-range-address, and gains the named-expression-side counterpart of its existing named-range baseCellAddress-absent test. - canonicalOdfConstructDescriptor and odfRunConstructWriteKind each gain a test proving their own compound guard's clauses are independently load-bearing, not merely redundant with each other. - odfIndexWrapperTag's -source suffix check gains a case (a tag whose last seven characters are NOT "-source" but whose first characters, once blindly sliced, spell a real wrapper tag by coincidence) that a weakened endsWith check would silently accept.
…e branches The second verification pass left one Survived mutant and nine NoCoverage ones -- code paths no existing test (direct or indirect) ever exercised at all: - compareOdfExtents' start-index comparison had no test proving it is the PRIMARY sort key rather than merely correlated with the end-index one the existing outermost-first test already covers; adds a case where the two clauses would sort oppositely. - addOdfPackageResidue's concatenation branch (a key that already holds a value) had no test at all -- every existing caller only ever added a fresh key. - isContentBearingNode's catch-all false for a non-text, non-element node (a comment, in practice) had no test -- every existing fixture built only text and element nodes. - parseOdfFieldInstruction's own throw, writeOdfChangePoint, and writeOdfAnnotationHalf's dateIso branch had no direct test at all. - writeOdfTrackedChanges' defensive throw for a change kind with no ODF region spelling (moveFrom/moveTo, refused by every real caller before reaching this function) is exercised directly, past the type's own restriction to insertion/deletion/formatChange.
…Threshold comment Keeps the comment's own inventory of packages still carrying unkilled mutants accurate now that shared/constructs.ts has reached zero survivors and zero no-coverage mutants of its own.
A db:server-database with no db:type attribute now has direct
coverage: readOdbInventory resolves its connection as a bare
{ type: "external" } with no url, exercising the early return this
RNG-derived, never-empirically-observed code path previously had no
test for at all.
Two constructs had no test-observable difference between their two branches, so no test could ever kill a mutant on them: - walkDrawShapes and walkDrawPageContent each special-cased an empty draw:g transform-function list to reuse groupFunctions unchanged rather than spreading it; spreading an empty ownFunctions ahead of groupFunctions produces identical content either way, so the special case was a pure allocation micro-optimisation with no behavioural effect. Both now always spread. - parseOdfPercentUnit's PERCENT_PATTERN capturing group has no `?` quantifier, so it always matches once the regex itself matches; the undefined check on match[1] was unreachable at runtime and existed only to satisfy noUncheckedIndexedAccess. Replaced with the same match[1]! non-null assertion typed/shared/units.ts's parseOdfLength and parseOdfAngleDeg already use for the identical mandatory-capturing-group guarantee.
Direct unit coverage for every previously-untested branch in readFrameAltText, readFloatPosition/readDrawImageBlock, readDrawFrameContent's text-box child dispatch, embedded-object residue, readDrawFrame's flowPositioning opt-in, the gradient/hatch draw:style validation and optional angle/rotation fields, and the dash-pattern dots1/dots1-length/distance/dots2 boundary conditions: - An svg:title or svg:desc present but empty falls through exactly like an absent one, distinguishing the length>0 check from a bare presence check on both elements. - A frame's text:anchor-type resolves into a real floatPosition (kept the attribute name itself observable), and is genuinely absent as a key, not present-but-undefined, when there is none. - A draw:text-box child that is neither text:p nor text:list is skipped without consuming a list numId, proven by the numId a following genuine text:list gets. - A chart embedded object's own residue lands on the block's source; every other embedded kind carries no source key at all. - readDrawFrame's flowPositioning parameter defaults to false and only applies when the caller opts in; walkDrawShapes never passes true, so an odp frame with no svg:x/svg:y is dropped rather than read at its own box origin. - A resolved gradient/hatch definition with a missing or unrecognised draw:style still yields no fillPattern, matching an unresolvable name; angleDeg/rotationDeg are omitted as keys, not set to undefined, when draw:angle/draw:rotation are absent. - A dash definition's dots1/dots1-length/distance boundary values (non-positive, negative) each independently blank the whole pattern; a present-but-non-positive dots2 keeps the single-length pattern with dots2/dots2LengthPt genuinely absent.
…/table gaps Adds coverage the mutation suite's own incremental cache had been silently skipping for several rounds, spanning formatServerDatabaseUrl, the component-collection walker, and table-name collection in typed/odb/read.ts: - formatServerDatabaseUrl: a hostname with no port omits the port suffix rather than always including one, and a db:server-database with neither a hostname nor a local-socket-name falls through to the bare scheme[:///name] forms rather than the hostname branch. - readConnectionInfo's db:file-based-database and db:server-database branches genuinely omit `url` (not merely leave it undefined) when the underlying href/format helper returns undefined -- asserted with toStrictEqual, since toEqual treats a present-but-undefined property as equivalent to an absent one and so could not actually prove this. - collectTableNames deduplicates a table name that appears in both db:table-representations and db:schema-definition/db:table-definitions, and reads db:schema-definition's own table-definitions path at all -- previously exercised by no test whatsoever. - collectQueryDefinitions' db:escape-processing reads a real "true" value, not just a "false" one indistinguishable from a hardcoded default. - collectComponents skips a stray child that is neither db:component nor db:component-collection instead of misreading it as one. - resolveOdbComponent's thrown message names "(none)" rather than a bare empty string when a .odb declares zero components of the requested kind.
…ndant self-mapping guard renameQName now tests indexOf's own "not found" sentinel with colon === -1 instead of colon < 0, since indexOf never returns another negative value -- the looser relational check only left an unreachable colon <= 0 case for a genuinely empty declared prefix, which the prefixes map never produces. prefixRenames no longer skips recording a prefix that is already canonical: renaming a declared prefix onto itself is a no-op wherever the map is read (renameQName's own canonical === undefined branch is the only consumer), so the extra comparison bought nothing but an allocation skip.
…-direction gaps Forward direction: an inch-shaped token inside a name-suffixed or xlink:href attribute, a default (unprefixed) xmlns declaration, a non-xmlns attribute whose value coincidentally matches a namespace URI, office:class dropped only on a document root, a paragraph's text:level, a cell's validation-name and a text-value element's value attributes (plus their negative cases outside those elements), a style:column's margin pair, table:sub-table, form:property's boolean flag, an unsplit style:properties for an unclassified family, an unwrapped office:body with no office:class, an XML part with no root element, a manifest entry resolved by skipping a decoy element/non-root entry/attribute, a same-shaped element outside the manifest part itself, mimetype synthesis only when resolvable and only when not already present, and an already-ODF package left untouched by the OpenOffice.org 1.x rules. Reverse direction: the mirrored default-xmlns, rootless-part, and manifest-decoy cases; office:body recursing into its own children when unwrapped by no recognised genre; fo:keep-with-next passed through unmodified for a value outside always/auto; the plain RENAMED_ATTRIBUTES table, form:control-implementation/form:text-style-name, and office:value-type's form:property special case, all reversed by name; text:note-ref and text:notes-configuration split by their own note-class, and a missing note-class defaulting to footnote; a cell's content-validation-name, a text-value element's attributes, and a style:column's indent pair reversed, each with its own negative case; a draw:frame with more than one frame-shaped child only unwrapping the first; a package-internal href that already starts with # never being double-prefixed; an unresolvable text:list left unrenamed; and office:meta's children left alone with no meta:keyword to rewrap.
…guard already makes redundant reverseNoteTag and reverseNoteBodyOrCitation are each only ever called with a tag already narrowed to one of their own checked values, so the final if-check and its "return undefined" fallback can never fire -- both now return the last branch unconditionally, and the caller no longer needs a `?? renamedTag` for a value that can't be undefined. reverseTransformElement's own explicit office:body return produced byte-identical output to the generic tag/attribute handling at the bottom of the same function (office:body has no REVERSE_RENAMED_ELEMENTS entry and is not a DOCUMENT_ROOT_ELEMENTS member), so the branch is gone and office:body now falls through to that shared path instead. wrapMetaKeywords's own length-0 early return produced the same result as the general loop below it (every node fails the meta:keyword check and is pushed through unchanged), so the guard is gone too. reverseTransformElement's own genre-child lookup and documentClassOf's now share one firstGenreElement helper instead of two separate inline type predicates, since both wanted the identical "first child that is an element with a recognised genre tag" query.
…everse-direction gaps Forward: office:class resolved from its own literal attribute when no prefix aliases the office namespace, never through a decoy prefix bound to something else or a coincidentally class-shaped attribute name, and a decoy manifest:file-entry left alone when synthesising the mimetype media type. Reverse: a genre child unwrapped only for office:body itself, not any element whose own first child happens to share a genre tag; listKind threaded down only from a genuine enclosing text:list; an inch-shaped token still protected inside a name-suffixed or xlink:href attribute; text:outline-level, table:is-sub-table, and the style:column indent pair each reversed only on their own real element, never a same-shaped attribute elsewhere; and a decoy manifest entry, tag, or attribute left alone by both the media-type-resolution and rewrite passes.
Mearman
force-pushed
the
feat/100-percent-mutation-odf.js
branch
from
September 14, 2026 20:02
d3614a8 to
da5f83d
Compare
Each of the three index-advancing branches (paren/brace recursion, quoted-string close) could overshoot text.length by one when its own terminator search ran to the end of the string, and the function's final return then silently substituted a hardcoded text.length regardless of what index actually held. Clamping each advance and returning index itself instead makes the loop's own boundary arithmetic load-bearing again: a future off-by-one there now surfaces as a wrong return value rather than being masked by the fallback.
…tion branches Neither branch of sheetCellStyle had direct coverage through the public write path: a plain cell with no background, borders, or alignment must get no table:style-name at all, and a cell that does carry decoration must actually emit its own style:table-cell-properties on a minted style:style, not just skip the early-return.
writeValidationMessage's display/title/body guards and canonicalValidationKey's showInputMessage/showErrorMessage fingerprint fields had no direct coverage: a rule's help/error messages must appear with the right display/title/message-type only when their underlying fields are actually set, table:allow-empty-cell must appear only for an explicit allowBlank:false, and two rules differing only in showInputMessage or showErrorMessage must intern as separate definitions rather than merging into one.
…ches None of containsBlanks/notContainsBlanks, priority, stopIfTrue, aboveAverage's stdDev, iconSet's reverse flag, or an unsupported 'num' cfvo threshold had a test exercising the actual refusal -- each is a distinct reason writeSheet throws for rather than silently dropping, and each also needed a matching "does not refuse" case on the otherwise-identical supported shape to prove the guard is genuinely conditional rather than always tripping.
bumpRow/bumpColumn's own hand-written "is this genuinely bigger" comparison was a redundant guard: reassigning maxRow/maxColumn to a value no larger than what it already holds is a no-op regardless of whether that comparison uses > or >=, so no test can ever distinguish the two. Math.max removes the comparison as a separately mutable expression entirely rather than leaving an unkillable one behind.
…lFormats array The existing dataBar fixture always set showValue explicitly, so the undefined-vs-defined check that decides whether calcext:show-value gets written at all had no case proving the attribute is genuinely omitted when the field is never set. Separately, writeConditionalFormats' own empty-array guard was only ever exercised via conditionalFormats being undefined, never via an explicit empty array reaching the same early return.
… remaining branches readOdbInventory: synthetic fully-populated embedded package and the two real-fixture describe blocks each called readOdbInventory once directly in the describe body, so the call executed exactly once during Vitest's collection phase rather than per test. Stryker's per-test coverage analysis treats that as static, module-load-time code, which makes every mutant inside readOdbInventory unkillable by any individual it() no matter how many assertions exist -- moving the call into a beforeEach ties its execution to whichever test is actually running. That alone killed 5 of 9 survivors. The remaining four needed real coverage: a stray child under db:queries that isn't db:query-collection but contains a nested db:query of its own (proving the recursion check looks at the tag, not just at whether children exist), the equivalent case for db:component under db:forms, and a resolveOdbComponent error with two available names to prove the join separator is ", " rather than a bare concatenation.
… write-side paths readCellStyleDecoration had no direct test coverage at all for verticalAlignment, alignment, or multi-element cascading/accumulation, and the entire write side (tableColumnStyle, tableRowStyle, tableCellStyle, writeCellBlocks, writeOdfTable) had none either -- every mutant in that half of the file was either Survived or NoCoverage. Adds direct tests for both. Also simplifies readRepeatCount: Number.parseInt always returns an integer or NaN, and NaN compared with > is always false like every other NaN comparison, so the separate Number.isInteger guard was checking something > 0 already covers on its own -- removing it drops an unkillable mutation opportunity (isInteger is true whenever parsed isn't NaN, and false only when parsed is NaN, so no reachable state distinguishes && from ||) rather than leaving a redundant condition for a test to fake-cover.
canonicalRun had no dedicated tests at all -- every canonicalParagraph test exercised it only through a single-field RUN fixture, so none of its seven per-field guards (bold/italic/underline/strike/fontFamily/ sizePt/color/hyperlink) or the canonicalColor delegation ever had a test proving that field actually passes through. Adds one test per field plus an all-fields-at-once case proving none clobbers another.
writeOdb's own suite was entirely round-trip tests against real fixtures (write then read back, compare to the original inventory), which cannot observe a mutation that changes what gets WRITTEN in a way the reader's own inverse tolerates or the fixture never exercises -- the same failure mode typed/shared/canonicalise.ts's own top-of- file note describes for write/read pairs in general. Adds direct, one-sided assertions against the raw written XML for: the two connection-type names in the no-url error message, the four optional child elements staying entirely absent when every list is empty, db:as-template/db:escape-processing each writing the exact true/false the caller stated (and nothing when absent), table names writing in order, and a connection's url landing verbatim in db:connection-resource regardless of connection type.
…-vectors.ts writeDrawVector/canonicalDrawVector had no dedicated test file at all -- only indirect exercise through typed/odg/write.test.ts's own round-trip suite, which cannot observe a mutation that changes what gets WRITTEN in a way the reader's own inverse tolerates or the round-trip fixtures never happen to exercise (the same failure mode typed/shared/canonicalise.ts's own top-of-file note describes). Adds direct, one-sided assertions against the raw written XML for vectorGraphicStyleName's fill/fillRule/stroke branches (including the two refusal paths: a dotted stroke style and a non-positive stroke width), zIndexAttrs' resolved-vs-fallback paint order, each of the four writeDrawVector element shapes (including the path element's two refusal paths: no subpaths, and a non-positive frame dimension), and writeDrawVectors' own baseIndex-plus-position arithmetic -- plus direct coverage of canonicalDrawVector's own per-field canonicalising rules (rotationDeg-zero collapse, color quantisation, absent-stroke- style defaulting, fillRule/subpath carrying, and per-kind field identity).
nextNoteOrdinal/nextAnnotationOrdinal each only ever had a single unnamed note/annotation in any existing test, so the counter's own post-increment could run backwards (or not advance at all) without any test noticing -- the minted name a SINGLE call produces is identical either way. Adds a two-construct case for each counter, asserting the second unnamed note/annotation gets a genuinely higher-numbered name than the first, not the same name reused or a decreasing one.
This module (the OpenOffice.org 1.x style:properties splitter/merger) had no dedicated test file at all -- every reader that touches it only exercises it indirectly through a whole-document round trip. Adds direct tests for propertyTypesForContainer's own routing branches (container-tag lookup, style:style/style:default-style family lookup, and each of its three undefined-returning cases), splitStyleProperties' first-match-wins routing and its fallback to the first candidate for an unrecognised attribute, its own per- candidate element emission (including omitting an empty candidate), its non-element-child skip, its child-element routing, both OpenOffice.org compound-attribute expansions (style:text-underline and style:text-crossing-out, each with a listed and an unlisted value), the fo:keep-with-next boolean-to-keyword rewrite, and mergeStyleProperties' found/not-found result and its multi-child concatenation and rest-preservation behaviour.
isXmlNode is a hand-written recursive structural guard (z.custom's own validator, since a genuinely recursive schema collapses to unknown under z.lazy in this pinned zod version) with no direct tests at all -- every place it runs is exercised only as a side effect of parsing a real XML document, which never constructs the malformed shapes a mutant here would need a test to distinguish. Covers every branch: non-object/unrecognised-type rejection, each of text/cdata/comment/declaration/pi/element's own required-field checks, an element's own attributes/children array-type and per-item validity checks, and two levels of recursive descent (a valid deeply nested tree, and one whose innermost grandchild is malformed).
parseXml had no direct unit tests at all -- every reader test in this package exercises it only indirectly, through a whole XML document string, which never isolates a single node kind's own mapping. Adds direct assertions, each against a minimal real XML string, for every reachable parseNode branch: a bare element, an element's text content, a comment, a CDATA section, the leading declaration and its attributes, a non-xml processing instruction, attribute parsing and ordering, nested-element recursion with mixed element/text children, and that entities and whitespace are carried through unencoded and untrimmed (this package's own lossless-round-trip contract).
…-shapes.ts Every function here (planShapeContent, frameGeometryAttrs, odfZIndexOf, writeDrawFrame, canonicalDrawShape, writeDrawShapes) was previously only exercised indirectly through odp/write.test.ts and odg/write.test.ts's own whole-document round-trip suites, which cannot observe a mutation that changes what gets written in a way the reader's own inverse tolerates.
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.
Works through odf.js's mutation survivors from its own first CI-measured baseline (71.31% of 8442 valid mutants, timeout share 1.3%). This is the largest package under this effort so far by a wide margin, so this PR lands real, verified progress rather than a finished 100% run.
So far:
A full re-run after this batch confirms 76.52% of 8451 valid mutants, up from the 71.31% baseline (timeout share still ~1.3%).
breakThresholdis raised to 74 (the derived floor for this measured score), still provisional.Remaining work, tracked honestly rather than hidden: the bulk of odf.js's other files still carry their own share of the roughly 2000 currently-unkilled mutants (Survived + NoCoverage) and have not been touched yet, ordered by rough size:
typed/ods/write.ts(~213),ooo1/transform.ts(~197),typed/shared/constructs.ts(~155),typed/shared/paragraph.ts(~126),typed/ods/conditional-format.ts(~111),typed/draw/shapes.ts(~105),typed/odt/write.ts(~94),typed/ods/read.ts(~84),typed/odb/report.ts(~83),typed/shared/table.ts(~64),typed/odt/read.ts(~61),typed/odp/write.ts(~53),typed/draw/write-shapes.ts(~50),typed/ods/data-validation.ts(~50),manifest.ts(~38),model/node.ts(~36),typed/odb/read.ts(~34),xml/parse.ts(~33),package-io/read.ts(~27),typed/odb/write.ts(~25), and many smaller files.No Stryker disable comments anywhere in this package (verified by grep before every commit and again before this update).