# Configuration for cargo-mutants (https://mutants.rs/).
#
# Run `cargo mutants` locally to use this config. `cargo mutants --in-diff <file>`
# only mutates lines covered by the supplied unified diff; CI uses that for PRs.

# Pure entry points / boilerplate — nothing worth mutating.
exclude_globs = [
    "src/main.rs",
]

# Skip trait impls that are pure delegations or where mutants are typically
# equivalent (Debug/From/source). The Display impl in `error.rs` is exercised
# by `tests::display_covers_all_variants`, so we leave it mutable.
#
# We also skip the human-facing help / format-name strings. These are
# documentation surfaces, not behavior: pinning them down would require
# snapshot tests whose only purpose is to fail when the help text is edited,
# which is the opposite of what we want. The wording is reviewed manually.
#
# IMPORTANT: cargo-mutants matches `exclude_re` against the *mutant name*
# (e.g. `src/cli.rs:211:5: replace print_help with ()`), not the source
# declaration line. So patterns must anchor on the `replace <name>` form,
# never on `fn <name>`. Trait-impl mutants embed the impl header in the
# mutant name (e.g. `<impl std::error::Error for Error>::source`), so the
# `impl ...` patterns below match correctly.
exclude_re = [
    "impl .*Debug for",
    "impl From<.*> for Error",
    # `Error::source` impl (trait method on `std::error::Error`).
    "<impl std::error::Error for Error>::source",
    # CLI help printers (config-disassembler top-level CLI and the
    # xml-disassembler binary). All emit human-readable usage text to
    # stderr and have no other observable behavior; pinning them down
    # would require capture-stderr snapshot tests whose only purpose
    # is to fail when the help wording is edited, which is the exact
    # opposite of what we want. `print_usage` was the last help
    # printer not yet excluded; the full-sweep run 25823260907
    # flagged its `replace print_usage with ()` mutant as missed.
    "replace print_help",
    "replace print_format_help",
    "replace print_same_format_help",
    "replace print_cross_format_help",
    "replace print_usage",
    # Helper that joins format names for the `<fmt>` line in help output.
    "replace format_list",
    # `Format::display_name` is the human-friendly format label used in
    # help text and error messages; `Format::supported_format_list` is the
    # human-readable comma-joined name list for error contexts. Both are
    # cosmetic.
    "replace Format::display_name",
    "replace Format::supported_format_list",
    # `value_to_string` and `attr_value_to_string` both have explicit
    # `Value::Number` / `Value::Bool` match arms whose only purpose is to
    # short-circuit the trailing `_ => serde_json::to_string(v)
    # .unwrap_or_default()` fallback. For both Number and Bool the
    # fallback is byte-identical to the explicit arm:
    #   * `serde_json::to_string(&Value::Number(42.into()))` -> `"42"`
    #     (same as `Number::to_string()`).
    #   * `serde_json::to_string(&Value::Bool(true))` -> `"true"`
    #     (same as `bool::to_string()`).
    # Deleting these match arms is therefore an equivalent mutation --
    # output is unchanged for every realistic JSON number / bool.
    # The `Value::Null` arm is *not* equivalent (it returns `""` vs the
    # serde fallback's `"null"`) and stays mutable; the
    # `value_to_string` Null mutant is killed by the strengthened
    # `build_xml_string_null_child_value` test (see PR 3 of the
    # follow-up cleanup) and `attr_value_to_string` Null is killed by
    # the existing `attr_value_null_converted_to_empty_string` test.
    "src/xml/builders/build_xml_string\\.rs:.*delete match arm Value::Number",
    "src/xml/builders/build_xml_string\\.rs:.*delete match arm Value::Bool",
    "src/xml/builders/extract_root_attributes\\.rs:.*delete match arm Value::Number",
    "src/xml/builders/extract_root_attributes\\.rs:.*delete match arm Value::Bool",
    # Every mutant on `parse_reassemble_args` -- equivalent to a
    # function-level `#[mutants::skip]`, but expressed here so we
    # don't have to add the `mutants` crate as a dependency just for
    # one attribute.
    #
    # Background. `parse_reassemble_args` returns
    # `(Option<&str>, Option<&str>, bool)` and is consumed by
    # `run_reassemble` (`src/xml/cli.rs`) as:
    #
    #     let (path, extension, post_purge) = parse_reassemble_args(args);
    #     let path = path.unwrap_or(".");
    #     handler.reassemble(path, extension.or(Some("xml")), post_purge).await?;
    #
    # `ReassembleXmlFileHandler::reassemble` ultimately reaches
    # (`src/xml/handlers/reassemble.rs:277`):
    #
    #     if post_purge { fs::remove_dir_all(file_path).await.ok(); }
    #
    # Whenever a mutant on `parse_reassemble_args` yields
    # `(path: None, _, post_purge: true)` for the input the
    # integration test
    # `run_reassemble_on_existing_directory_succeeds` actually passes
    # (typically a single positional dir, e.g.
    # `["xml-disassembler", "reassemble", "<tempdir>"]`), the call
    # collapses to `reassemble(".", Some("xml"), true)`. Because
    # `cargo test` runs with cwd set to the cargo-mutants scratch
    # directory (`/tmp/cargo-mutants-config-disassembler-XXX.tmp/`),
    # `fs::remove_dir_all(".")` then wipes the *entire scratch tree
    # including `src/xml/cli.rs` itself*. cargo-mutants records the
    # mutant as `Caught` (the test panics), but the next mutant's
    # `Apply` step finds the source file gone and the worker dies
    # with `".../src/xml/cli.rs" does not exist, refusing to create
    # it`, aborting the remaining ~600 mutants in the sweep.
    #
    # `cargo mutants --list --file src/xml/cli.rs` shows 16 mutants
    # currently land on this function (15 `FnValue` and 1
    # `BinaryOperator`). At least two have proven destructive in CI:
    #
    #   1. FnValue `(None, None, true)` (run 25818276284 / PR #42)
    #   2. BinaryOperator `replace == with !=` at 196:16, which
    #      flips `if arg == "--postpurge"` so that *every* non-
    #      `--postpurge` arg sets `post_purge = true` and `path`
    #      never gets assigned -- yielding `(None, None, true)` for
    #      the single-positional input above (run 25820780519 /
    #      PR #43).
    #
    # A narrower exclude that targeted only the
    # `(None, *, true)` `FnValue` shape (PR #43) just shifted the
    # destructive case to the `BinaryOperator` mutant. Any single
    # boolean guard added at the `remove_dir_all` site would itself
    # be one `==`/`||`-flip mutation away from being destructive
    # again. Skipping the whole function is the only durable fix
    # that doesn't require adding a `#[mutants::skip]` runtime
    # attribute or restructuring the integration test.
    #
    # Semantic coverage is preserved by two existing unit tests in
    # `src/xml/cli.rs::tests`:
    #
    #   * `parse_reassemble_args_picks_path_extension_and_flag`
    #     -- asserts `(Some("some/dir"), Some("json"), true)` for
    #     `["some/dir", "json", "--postpurge"]`.
    #   * `parse_reassemble_args_defaults_and_extra_args_ignored`
    #     -- asserts `(None, None, false)` for `[]` and
    #     `(Some("dir"), Some("xml"), _)` for `["dir", "xml", "extra"]`.
    #
    # These cover both the all-default path and the positional /
    # `--postpurge` flag-recognition path; any logic bug in this
    # function that affects real callers will fail one of them in CI
    # regardless of cargo-mutants.
    "src/xml/cli\\.rs:.*parse_reassemble_args",
    # `build_disassembled_files_unified` chooses between an explicit
    # `order_xml_element_keys(&leaf_content, &key_order)` (grouped-by-tag)
    # and a raw `Value::Object(leaf_content.clone())` (every other strategy)
    # for the leaf bundle's content. Flipping `if strategy == "grouped-by-tag"`
    # to `!=` is observationally equivalent because:
    #
    #   1. `serde_json::Map` is built with `preserve_order` in this crate,
    #      so the map iterates in insertion order.
    #   2. `leaf_content` is populated by the outer key loop in
    #      `disassemble_element_keys`, which iterates `key_order` and
    #      inserts each leaf key in that exact order. Its keys are a
    #      strict subset of `key_order` in `key_order` order.
    #   3. `order_xml_element_keys(content, key_order)` walks `key_order`
    #      and copies any matching key from `content` into a fresh map.
    #      Given (2), the resulting map has the same keys in the same
    #      order as `leaf_content.clone()`.
    #
    # The two branches therefore produce structurally identical JSON for
    # every realistic input. The explicit reorder is kept as defensive
    # code in case the insertion-order invariant is ever broken upstream,
    # so we exclude this one mutant rather than delete the call.
    "src/xml/builders/build_disassembled_files\\.rs:.*: replace == with != in build_disassembled_files_unified",
    # Infinite-loop hazards on loop-bound predicates. Each of the
    # mutants below collapses the loop's exit condition into one that
    # the loop body can never satisfy, hanging the test under
    # mutation rather than producing observably-wrong output:
    #
    #   * `leading_comment_start` (`src/disassemble.rs:509` and
    #     `src/reassemble.rs:231`): `while start > 0`. Replacing `>`
    #     with `==` makes the loop run forever whenever `start == 0`,
    #     because the body re-derives `previous_line_start == 0` and
    #     leaves `start` at 0. There's no way to "kill" this mutant
    #     without a test that explicitly bounds wall-clock time;
    #     cargo-mutants already detects it via timeout but reports
    #     it under exit code 3, failing CI even though every other
    #     mutant on these functions is caught.
    #   * `digit_width` (`src/disassemble.rs:619`): `while n >= 10`.
    #     Replacing `>=` with `<` makes the loop run forever for any
    #     `n < 10`, because the body's `n /= 10` keeps `n` at 0
    #     forever. Same shape, same outcome: pure infinite-loop
    #     hazard with no observable correct-behaviour test.
    #
    # All other mutants on `leading_comment_start` and `digit_width`
    # remain enabled and are caught by the existing JSON
    # disassemble/reassemble unit tests.
    "src/disassemble\\.rs:509:17: replace > with == in leading_comment_start",
    "src/disassemble\\.rs:619:13: replace >= with < in digit_width",
    "src/reassemble\\.rs:231:17: replace > with == in leading_comment_start",
]

# Give the suite extra wall-clock budget under mutation; minimums prevent flaky
# kills on fast unit tests that happen to time out under contention.
timeout_multiplier = 2.0
minimum_test_timeout = 60.0

# Match CI test invocation so feature-gated code paths are exercised.
additional_cargo_args = ["--all-features"]
