cargo-mutants-25.0.0/.cargo/audit.toml000064400000000000000000000003411046102023000156460ustar 00000000000000[advisories] ignore = [ # Intractable Unix TZ issues; not obviously important in Conserve "RUSTSEC-2020-0071", "RUSTSEC-2020-0159", # Only relevant with a custom allocator, which we don't use "RUSTSEC-2021-0145", ] cargo-mutants-25.0.0/.cargo/mutants.toml000064400000000000000000000003061046102023000162340ustar 00000000000000# cargo-mutants configuration error_values = ["::anyhow::anyhow!(\"mutated!\")"] exclude_globs = ["src/console.rs"] profile = "mutants" # Build without debug symbols cargo-mutants-25.0.0/.cargo_vcs_info.json0000644000000001360000000000100137040ustar { "git": { "sha1": "16c4feea228b649a1509de6186e805b089c290aa" }, "path_in_vcs": "" }cargo-mutants-25.0.0/.codespell.dict000064400000000000000000000000251046102023000153660ustar 00000000000000assertino->assertion cargo-mutants-25.0.0/.codespell.words000064400000000000000000000000121046102023000155750ustar 00000000000000crate ser cargo-mutants-25.0.0/.codespellrc000064400000000000000000000002031046102023000147670ustar 00000000000000[codespell] ignore-words = .codespell.words skip = target,.git,mutants.out* builtin = clear,rare,code dictionary = .codespell.dict cargo-mutants-25.0.0/.devcontainer/Dockerfile000064400000000000000000000010171046102023000172240ustar 00000000000000# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/rust/.devcontainer/base.Dockerfile # [Choice] Debian OS version (use bullseye on local arm64/Apple Silicon): buster, bullseye ARG VARIANT="buster" FROM mcr.microsoft.com/vscode/devcontainers/rust:0-${VARIANT} # [Optional] Uncomment this section to install additional packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends cargo-mutants-25.0.0/.devcontainer/devcontainer.json000064400000000000000000000033731046102023000206150ustar 00000000000000// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.238.1/containers/rust { "name": "Rust", "build": { "dockerfile": "Dockerfile", "args": { // Use the VARIANT arg to pick a Debian OS version: buster, bullseye // Use bullseye when on local on arm64/Apple Silicon. "VARIANT": "bullseye" } }, "runArgs": ["--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined"], // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. "vscode": { // Set *default* container specific settings.json values on container create. "settings": { "lldb.executable": "/usr/bin/lldb", // VS Code don't watch files under ./target "files.watcherExclude": { "**/target/**": true }, "rust-analyzer.checkOnSave.command": "clippy" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ "vadimcn.vscode-lldb", "mutantdino.resourcemonitor", "rust-lang.rust-analyzer", "tamasfe.even-better-toml", "serayuzgur.crates" ] } }, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. // "postCreateCommand": "rustc --version", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { "git": "os-provided", "github-cli": "latest", "ghcr.io/meaningful-ooo/devcontainer-features/fish:1": {} } } cargo-mutants-25.0.0/.gitattributes000064400000000000000000000000431046102023000153640ustar 00000000000000tests/snapshots/*.snap -whitespace cargo-mutants-25.0.0/.github/FUNDING.yml000064400000000000000000000013631046102023000156540ustar 00000000000000# These are supported funding model platforms github: sourcefrog # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username ko_fi: poolie # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry #liberapay: # Replace with a single Liberapay username #issuehunt: # Replace with a single IssueHunt username #otechie: # Replace with a single Otechie username #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] cargo-mutants-25.0.0/.github/workflows/actionlint.yml000064400000000000000000000013521046102023000207610ustar 00000000000000name: actionlint on: pull_request: paths: - ".github/workflows/*.yml" - "examples/workflows/*.yml" push: branches: - main paths: - ".github/workflows/*.yml" - "examples/workflows/*.yml" workflow_dispatch: permissions: contents: read pull-requests: write jobs: action-lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check workflow files shell: bash run: | bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) # shellcheck disable=SC2046 ./actionlint -color $(find .github/workflows examples/workflows -name '*.yml' -type f ! -name release.yml -print) cargo-mutants-25.0.0/.github/workflows/book.yml000064400000000000000000000012021046102023000175410ustar 00000000000000name: Build book on: push: branches: - main workflow_dispatch: pull_request: branches: - main paths: - .github/workflows/book.yml - book/** jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install mdbook and mdbook-linkcheck uses: taiki-e/install-action@v2 with: tool: mdbook, mdbook-linkcheck - name: Build mdbook run: | mdbook build book - name: Archive book output uses: actions/upload-artifact@v4 if: always() with: name: book path: book/book cargo-mutants-25.0.0/.github/workflows/install.yml000064400000000000000000000006361046102023000202670ustar 00000000000000name: Test install from crates.io permissions: contents: read on: push: paths: - ".github/workflows/install.yml" schedule: - cron: "17 0 * * 1" jobs: cargo-install: strategy: matrix: locked: ["", "--locked"] fail-fast: false runs-on: ubuntu-latest steps: - name: cargo-install run: | cargo install cargo-mutants ${{ matrix.locked }} cargo-mutants-25.0.0/.github/workflows/release-book.yml000064400000000000000000000021541046102023000211660ustar 00000000000000# Publish the book to https://mutants.rs/ when a new release is created. name: Release book on: push: branches: - main # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write jobs: deploy: if: github.repository == 'sourcefrog/cargo-mutants' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} concurrency: # Allow one concurrent deployment group: "pages" cancel-in-progress: true runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install mdbook uses: taiki-e/install-action@v2 with: tool: mdbook, mdbook-linkcheck - name: Setup Pages uses: actions/configure-pages@v5 - name: Build book run: | mdbook build book - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: "book/book/html" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 cargo-mutants-25.0.0/.github/workflows/release.yml000064400000000000000000000263131046102023000202410ustar 00000000000000# Copyright 2022-2024, axodotdev # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: # # * checks for a Git Tag that looks like a release # * builds artifacts with cargo-dist (archives, installers, hashes) # * uploads those artifacts to temporary workflow zip # * on success, uploads the artifacts to a GitHub Release # # Note that the GitHub Release will be created with a generated # title/body based on your changelogs. name: Release permissions: contents: write # This task will run whenever you push a git tag that looks like a version # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION # must be a Cargo-style SemVer Version (must have at least major.minor.patch). # # If PACKAGE_NAME is specified, then the announcement will be for that # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). # # If PACKAGE_NAME isn't specified, then the announcement will be for all # (cargo-dist-able) packages in the workspace with that version (this mode is # intended for workspaces with only one dist-able package, or with all dist-able # packages versioned/released in lockstep). # # If you push multiple tags at once, separate instances of this workflow will # spin up, creating an independent announcement for each one. However, GitHub # will hard limit this to 3 tags per commit, as it will assume more tags is a # mistake. # # If there's a prerelease-style suffix to the version, then the release(s) # will be marked as a prerelease. on: push: tags: - '**[0-9]+.[0-9]+.[0-9]+*' pull_request: jobs: # Run 'cargo dist plan' (or host) to determine what tasks we need to do plan: runs-on: ubuntu-latest outputs: val: ${{ steps.plan.outputs.manifest }} tag: ${{ !github.event.pull_request && github.ref_name || '' }} tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} publishing: ${{ !github.event.pull_request }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* # but also really annoying to build CI around when it needs secrets to work right.) - id: plan run: | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json echo "cargo dist ran successfully" cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json # Build and packages all the platform-specific things build-local-artifacts: name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) # Let the initial task tell us to not run (currently very blunt) needs: - plan if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false # Target platforms/runners are computed by cargo-dist in create-release. # Each member of the matrix has the following arguments: # # - runner: the github runner # - dist-args: cli flags to pass to cargo dist # - install-dist: expression to run to install cargo-dist on the runner # # Typically there will be: # - 1 "global" task that builds universal installers # - N "local" tasks that build each platform's binaries and platform-specific installers matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} runs-on: ${{ matrix.runner }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json steps: - name: enable windows longpaths run: | git config --global core.longpaths true - uses: actions/checkout@v4 with: submodules: recursive - uses: swatinem/rust-cache@v2 with: key: ${{ join(matrix.targets, '-') }} - name: Install cargo-dist run: ${{ matrix.install_dist }} # Get the dist-manifest - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - name: Install dependencies run: | ${{ matrix.packages_install }} - name: Build artifacts run: | # Actually do builds and make zips and whatnot cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json echo "cargo dist ran successfully" - id: cargo-dist name: Post-build # We force bash here just because github makes it really hard to get values up # to "real" actions without writing to env-vars, and writing to env-vars has # inconsistent syntax between shell and powershell. shell: bash run: | # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Build and package all the platform-agnostic(ish) things build-global-artifacts: needs: - plan - build-local-artifacts runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true - id: cargo-dist shell: bash run: | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json echo "cargo dist ran successfully" # Parse out what we just built and upload it to scratch storage echo "paths<> "$GITHUB_OUTPUT" jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" uses: actions/upload-artifact@v4 with: name: artifacts-build-global path: | ${{ steps.cargo-dist.outputs.paths }} ${{ env.BUILD_MANIFEST_NAME }} # Determines if we should publish/announce host: needs: - plan - build-local-artifacts - build-global-artifacts # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-20.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: Install cargo-dist run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.14.1/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 with: pattern: artifacts-* path: target/distrib/ merge-multiple: true # This is a harmless no-op for GitHub Releases, hosting for that happens in "announce" - id: host shell: bash run: | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json echo "artifacts uploaded and released successfully" cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" uses: actions/upload-artifact@v4 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it announce: needs: - plan - host # use "always() && ..." to allow us to wait for all publish jobs while # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' }} runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 with: submodules: recursive - name: "Download GitHub Artifacts" uses: actions/download-artifact@v4 with: pattern: artifacts-* path: artifacts merge-multiple: true - name: Cleanup run: | # Remove the granular manifests rm -f artifacts/*-dist-manifest.json - name: Create GitHub Release uses: ncipollo/release-action@v1 with: tag: ${{ needs.plan.outputs.tag }} name: ${{ fromJson(needs.host.outputs.val).announcement_title }} body: ${{ fromJson(needs.host.outputs.val).announcement_github_body }} prerelease: ${{ fromJson(needs.host.outputs.val).announcement_is_prerelease }} artifacts: "artifacts/*" cargo-mutants-25.0.0/.github/workflows/scorecards-analysis.yml000064400000000000000000000040001046102023000225570ustar 00000000000000name: Scorecards supply-chain security # Run only manually, because the answers don't change often and the results have # many unhelpful positives. on: workflow_dispatch: # Only the default branch is supported. # branch_protection_rule: # schedule: # - cron: '38 17 * * 6' # push: # branches: [ main ] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecards analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write actions: read contents: read steps: - name: "Checkout code" uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@c8416b0b2bf627c349ca92fc8e3de51a64b005cf # v1.0.2 with: results_file: results.sarif results_format: sarif # Read-only PAT token. To create it, # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} # Publish the results to enable scorecard badges. For more details, see # https://github.com/ossf/scorecard-action#publishing-results. # For private repositories, `publish_results` will automatically be set to `false`, # regardless of the value entered here. publish_results: true # Upload the results as artifacts (optional). - name: "Upload artifact" uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # v1.0.26 with: sarif_file: results.sarif cargo-mutants-25.0.0/.github/workflows/tests.yml000064400000000000000000000173411046102023000177640ustar 00000000000000name: Tests permissions: contents: read on: pull_request: paths: - ".cargo/*.toml" - ".github/workflows/tests.yml" - "Cargo.*" - "mutants_attrs/**" - "src/**" - "testdata/**" - "tests/**" push: branches: - main # Actions doesn't support YAML references, so it's repeated here paths: - ".cargo/*.toml" - ".github/workflows/tests.yml" - "Cargo.*" - "mutants_attrs/**" - "src/**" - "testdata/**" - "tests/**" # see https://matklad.github.io/2021/09/04/fast-rust-builds.html env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 CI: 1 RUST_BACKTRACE: short RUSTFLAGS: "-W rust-2021-compatibility" RUSTUP_MAX_RETRIES: 10 CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT: 60 jobs: # Before anything else, run a quick test on just stable: this is significantly # faster than Windows or macOS and should catch most issues, and lets us get # started on the longer-running mutants and other tests. # # Also, build a Linux binary that we can use for the later mutants runs. quick-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: dtolnay/rust-toolchain@master with: toolchain: beta components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - name: Show Cargo and rustc version run: | cargo --version rustc --version - name: Build run: cargo build --all-targets - uses: taiki-e/install-action@v2 name: Install nextest using install-action with: tool: nextest - name: Test run: cargo test --workspace - name: Check rustfmt run: cargo fmt --all --check - name: Check clippy run: cargo clippy --all-targets --all-features -- -D warnings - name: Check typos uses: crate-ci/typos@master - name: Build release binary run: cargo build --release - name: Upload binary artifact uses: actions/upload-artifact@v4 with: name: cargo-mutants-linux path: | target/release/cargo-mutants test: needs: [quick-test] strategy: matrix: os: [macOS-latest, ubuntu-latest, windows-latest] version: [stable, nightly, "1.78"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.version }} components: rustfmt, clippy - name: Show Cargo and rustc version run: | cargo --version rustc --version - uses: Swatinem/rust-cache@v2 - name: rustfmt run: cargo fmt --all -- --check - uses: taiki-e/install-action@v2 name: Install nextest using install-action with: tool: nextest - name: Build run: cargo build --all-targets - name: Test run: cargo test --workspace - name: Check rustfmt run: cargo fmt --all --check - name: Check clippy if: matrix.version == 'stable' # Clippy checks can vary between versions in a way that makes it a bit # fiddly to satisfy them all, so only insist that they pass on stable. run: cargo clippy --all-targets --all-features -- -D warnings - run: cargo update - name: Test after cargo update run: cargo test --workspace - name: Downgrade to minimal versions if: matrix.version == 'nightly' run: cargo +nightly -Zdirect-minimal-versions update - name: Test on minimal versions if: matrix.version == 'nightly' run: cargo test - name: Install locked run: cargo install --path . --locked - name: Install unlocked run: cargo install --path . tests-from-tarball: needs: [quick-test] strategy: matrix: os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@nightly - uses: Swatinem/rust-cache@v2 - run: cargo package --no-verify - name: Upload package artifact uses: actions/upload-artifact@v4 with: name: cargo-mutants-package path: | target/package - name: Unpack package run: | cd target/package ls -l tar xvf cargo-mutants*.crate - name: Install nextest using install-action uses: taiki-e/install-action@v2 with: tool: nextest - name: Run tests from package run: | cd target/package/cargo-mutants-*.*.[0-9] cargo test pr-mutants: runs-on: ubuntu-latest if: github.event_name == 'pull_request' needs: [quick-test] strategy: matrix: test_tool: [cargo, nextest] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Relative diff run: | git branch -av git diff origin/${{ github.base_ref }}.. | tee git.diff - uses: dtolnay/rust-toolchain@master with: toolchain: beta - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 name: Install nextest using install-action with: tool: nextest - name: Download cargo-mutants binary uses: actions/download-artifact@v4 with: name: cargo-mutants-linux - name: Install cargo-mutants binary run: | install cargo-mutants ~/.cargo/bin/ - name: Mutants in-diff # Normally this would have --in-place, but for the sake of exercising more cases, it does not. run: > cargo mutants --no-shuffle -vV --in-diff git.diff --test-tool=${{matrix.test_tool}} --timeout=500 --build-timeout=500 --exclude=windows.rs --exclude=console.rs - name: Archive mutants.out uses: actions/upload-artifact@v4 if: always() with: name: mutants-incremental-${{ matrix.test_tool}}.out path: mutants.out cargo-mutants: runs-on: ubuntu-latest needs: [quick-test] strategy: fail-fast: false # We want to get all the mutant failures matrix: shard: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] test_tool: [cargo] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: beta - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 name: Install nextest using install-action with: tool: nextest - name: Download cargo-mutants binary uses: actions/download-artifact@v4 with: name: cargo-mutants-linux - name: Install cargo-mutants binary run: | install cargo-mutants ~/.cargo/bin/ - name: Mutants # Skip baselines because this job only runs after the baseline has been separately run. run: > cargo mutants --no-shuffle -vV --shard ${{ matrix.shard }}/10 --test-tool ${{ matrix.test_tool }} --baseline=skip --timeout=500 --build-timeout=500 --in-place --exclude=windows.rs --exclude=console.rs - name: Archive mutants.out uses: actions/upload-artifact@v4 if: always() with: name: mutants-${{matrix.test_tool}}-shard${{matrix.shard}}.out path: mutants.out overall-result: needs: [quick-test, test, tests-from-tarball, pr-mutants, cargo-mutants] runs-on: ubuntu-latest if: always() steps: - name: Successful workflow if: ${{ !(contains(needs.*.result, 'failure')) }} run: exit 0 - name: Failing workflow if: ${{ contains(needs.*.result, 'failure') }} run: exit 1 cargo-mutants-25.0.0/.gitignore000064400000000000000000000001321046102023000144600ustar 00000000000000target/ mutants.out mutants.out.old .cargo/config.toml wiki .vscode/ book/book *.snap.new cargo-mutants-25.0.0/.markdownlint.jsonc000064400000000000000000000003531046102023000163220ustar 00000000000000{ "default": true, // Allow lines that are all italics or bold; they're not meant to be headings "MD036": false, "MD013": false, // Allow long lines "MD033": false // Allow inline HTML, needed for
etc } cargo-mutants-25.0.0/.vscode/settings.json000064400000000000000000000000721046102023000165670ustar 00000000000000{ "rust-analyzer.showUnlinkedFileNotification": false } cargo-mutants-25.0.0/CITATION.cff000064400000000000000000000010131046102023000143610ustar 00000000000000abstract: Find inadequately-tested code that can be removed without any tests failing. authors: - alias: sourcefrog family-names: Pool given-names: Martin cff-version: 1.2.0 date-released: 2024-01-13 keywords: - Rust - cargo - coverage - development-tools::testing - mutants - mutation-testing - testing license: MIT message: Please cite this project using these information. repository-code: https://github.com/sourcefrog/cargo-mutants title: cargo-mutants url: https://mutants.rs/ version: 24.9.0 cargo-mutants-25.0.0/CODE_OF_CONDUCT.md000064400000000000000000000002031046102023000152660ustar 00000000000000# The Rust Code of Conduct The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html). cargo-mutants-25.0.0/CONTRIBUTING.md000064400000000000000000000107341046102023000147320ustar 00000000000000# Contributing to cargo-mutants If you're interested in adding a feature or fixing a bug, thank you! Please start by reading this document and opening a Github discussion or bug about the thing you want to do, to avoid wasted work. void wasted work, and feel free to talk or ask about the approach. Please also read the [DESIGN.md](DESIGN.md) file for technical information not specifically about posting contributions. ## Code of Conduct This project is conducted in accord with the [Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct). ## Try it on a new tree One of the most helpful things you can do is to try cargo-mutants on a new tree: either your own project or an important open source project: * Did cargo-mutants hang, error, or otherwise fail to run? If so a bug with reproduction instructions would be very helpful. * Were the mutants interesting or helpful in understanding coverage or test quality? Did it generate any new tests that were accepted into the tree? ## Rust Style Generally, variables and parameters should be the `snake_case` version of their type name: `source_tree: SourceTree`. However if that would be unclear or ambiguous, use a different name that does not repeat the type: `src: &Path, dest: &Path`. Try to keep one major class or separation of concern per mod, with implementation details being private. However, fields that would have trivial getters and that don't break the abstraction can be `public`, since this crate does not provide a library API. `public` is used mostly as a marker that something is an implementation detail of a module. Please run `cargo fmt` and `cargo clippy`. These are checked in CI. ## Testing Of course, please add tests for new features or bug fixes. See also the _Testing_ section of [the design doc](DESIGN.md). ### Running the tests cargo-mutants tests require [`cargo-nextest`](https://nexte.st/) to be installed, so that they can exercise `--test-tool=nextest`. cargo-mutants tests can be run under either `cargo test` or `cargo nextest run`. ### Test naming Tests should have names that read like English sentences (or subsentences) asserting a fact about how the program behaves, like `copy_testdata_doesnt_include_build_artifacts`. It's fine if this makes the test function names relatively long. However, also try to avoid "noise" or low-value words in test names. `show_version` is clear enough and does not need to be `when_run_with_show_version_it_prints_the_version_to_stdout`. As with other code, if you feel you need to add a comment to explain what it does, then first consider whether the test can have a better name. If the test exercises a particular test tree, option, or function, make sure that name literally occurs within the test name. ### Insta snapshots Many tests use [Insta](https://insta.rs) to assert the expected output. Insta makes it easy to review and accept changes to expected output, either when there's a real functional change or when for example line numbers or output formatting changes. To conveniently review changed output, `cargo install cargo-insta` and then run `cargo insta test --review` etc. ### Test performance CLI tests spawn a new process which can be slightly slow, especially on Windows, and especially if they actually test the mutants. Try to keep `testdata` trees reasonably minimal for whatever they're intended to test. ### Debugging code under test When the tests run, `cargo test` runs a test suite binary from `target`, which then runs `cargo-mutants` as a subprocess. (Or, in some cases, it runs `cargo` which runs `cargo-mutants`.) As a result, attaching a debugger to the test binary will let you see the code that launches the subprocess and that inspects the output, but it won't let you step through cargo-mutants itself, which is probably the most interesting part. Probably the easiest path is to just make note of the command run by the test, and then run that command yourself, under a debugger, outside of the test suite. For example, `./target/debug/cargo-mutants -d ./testdata/factorial --list`. You may wish to turn off the timeouts with `-t 0`. ## Generating new mutations The largest area for new work at the moment is in generating new mutations. Most of the code for this is in `visit.rs`. If you look in `mutants.out/debug.log` you can see messages like `Return type is not recognized, trying Default`. These might be good places to add a new more specific pattern. Also `mutants.out/unviable.txt` might suggest ways to generate new patterns that are viable. cargo-mutants-25.0.0/Cargo.lock0000644000001346340000000000100116720ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", "windows-sys 0.59.0", ] [[package]] name = "anyhow" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "assert_cmd" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" dependencies = [ "anstyle", "bstr", "doc-comment", "libc", "predicates", "predicates-core", "predicates-tree", "wait-timeout", ] [[package]] name = "assert_matches" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" dependencies = [ "memchr", "regex-automata 0.4.9", "serde", ] [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecount" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "camino" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" dependencies = [ "serde", ] [[package]] name = "cargo-mutants" version = "25.0.0" dependencies = [ "anyhow", "assert_cmd", "assert_matches", "camino", "cargo_metadata", "clap", "clap_complete", "color-print", "console", "cp_r", "ctrlc", "fastrand", "fs2", "globset", "humantime", "ignore", "indoc", "insta", "itertools", "jobserver", "lazy_static", "mutants", "nix", "num_cpus", "nutmeg", "patch", "path-slash", "predicates", "pretty_assertions", "proc-macro2", "quote", "regex", "rusty-fork", "serde", "serde_json", "similar", "strum", "syn", "tempfile", "test-log", "time", "toml", "tracing", "tracing-appender", "tracing-subscriber", "walkdir", "whoami", ] [[package]] name = "cargo-platform" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", ] [[package]] name = "cargo_metadata" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8769706aad5d996120af43197bf46ef6ad0fda35216b4505f926a365a232d924" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", "thiserror 2.0.4", ] [[package]] name = "cc" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "shlex", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.52.6", ] [[package]] name = "clap" version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", ] [[package]] name = "clap_builder" version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size 0.4.1", ] [[package]] name = "clap_complete" version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "color-print" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" dependencies = [ "color-print-proc-macro", ] [[package]] name = "color-print-proc-macro" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" dependencies = [ "nom", "proc-macro2", "quote", "syn", ] [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "console" version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", "windows-sys 0.52.0", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cp_r" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "837ca07dfd27a2663ac7c4701bb35856b534c2a61dd47af06ccf65d3bec79ebc" dependencies = [ "filetime", ] [[package]] name = "crossbeam-channel" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "ctrlc" version = "3.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" dependencies = [ "nix", "windows-sys 0.59.0", ] [[package]] name = "deranged" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" [[package]] name = "doc-comment" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "env_filter" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", ] [[package]] name = "env_logger" version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", "env_filter", "log", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "fastrand" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" [[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.59.0", ] [[package]] name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" dependencies = [ "num-traits", ] [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fs2" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" dependencies = [ "libc", "winapi", ] [[package]] name = "globset" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", "log", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "iana-time-zone" version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "ignore" version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", ] [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "indoc" version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "insta" version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" dependencies = [ "console", "lazy_static", "linked-hash-map", "similar", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.9", "libc", "windows-sys 0.48.0", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.6.0", "libc", "redox_syscall", ] [[package]] name = "linked-hash-map" version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ "regex-automata 0.1.10", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mutants" version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "nom_locate" version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e3c83c053b0713da60c5b8de47fe8e494fe3ece5267b2f23090a07a053ba8f3" dependencies = [ "bytecount", "memchr", "nom", ] [[package]] name = "normalize-line-endings" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" [[package]] name = "nu-ansi-term" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", "winapi", ] [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi 0.3.9", "libc", ] [[package]] name = "nutmeg" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210b363fa6901c372f264fa32ef3710c0e86328901deaed31294fecfd51e848b" dependencies = [ "atty", "parking_lot", "terminal_size 0.2.6", "yansi 0.5.1", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets 0.52.6", ] [[package]] name = "patch" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c07fdcdd8b05bdcf2a25bc195b6c34cbd52762ada9dba88bf81e7686d14e7a" dependencies = [ "chrono", "nom", "nom_locate", ] [[package]] name = "path-slash" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" [[package]] name = "pin-project-lite" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "predicates" version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "difflib", "float-cmp", "normalize-line-endings", "predicates-core", "regex", ] [[package]] name = "predicates-core" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" [[package]] name = "predicates-tree" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" dependencies = [ "predicates-core", "termtree", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi 1.0.1", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.9", "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" dependencies = [ "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags 1.3.2", "errno", "io-lifetimes", "libc", "linux-raw-sys 0.3.8", "windows-sys 0.48.0", ] [[package]] name = "rustix" version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] [[package]] name = "rustversion" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "similar" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn", ] [[package]] name = "syn" version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix 0.38.41", "windows-sys 0.59.0", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix 0.37.27", "windows-sys 0.48.0", ] [[package]] name = "terminal_size" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ "rustix 0.38.41", "windows-sys 0.59.0", ] [[package]] name = "termtree" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "test-log" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dffced63c2b5c7be278154d76b479f9f9920ed34e7574201407f0b14e2bbb93" dependencies = [ "env_logger", "test-log-macros", "tracing-subscriber", ] [[package]] name = "test-log-macros" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f49a1853cf82743e3b7950f77e0f4d622ca36cf4317cba00c767838bac8d490" dependencies = [ "thiserror-impl 2.0.4", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8381894bb3efe0c4acac3ded651301ceee58a15d47c2e34885ed1908ad667061" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", ] [[package]] name = "time" version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde", "time-core", "time-macros", ] [[package]] name = "time-core" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] [[package]] name = "toml" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-appender" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", "thiserror 1.0.69", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "regex", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "wait-timeout" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" dependencies = [ "libc", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "whoami" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ "redox_syscall", "wasite", "web-sys", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" cargo-mutants-25.0.0/Cargo.toml0000644000000107670000000000100117150ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.78" name = "cargo-mutants" version = "25.0.0" authors = ["Martin Pool"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Inject bugs and see if your tests catch them" homepage = "https://mutants.rs/" readme = "README.md" keywords = [ "testing", "mutants", "cargo", "mutation-testing", "coverage", ] categories = ["development-tools::testing"] license = "MIT" repository = "https://github.com/sourcefrog/cargo-mutants" [package.metadata.wix] eula = false license = false path-guid = "457C7F8E-0F02-42CC-80D9-FE55FBED23F8" upgrade-guid = "CA7BFE8D-F3A7-4D1D-AE43-B7749110FA90" [profile.dist] lto = "thin" inherits = "release" [profile.mutants] debug = 0 inherits = "test" [[bin]] name = "cargo-mutants" path = "src/main.rs" [[test]] name = "build_dir" path = "tests/build_dir.rs" [[test]] name = "check_build" path = "tests/check_build.rs" [[test]] name = "colors" path = "tests/colors.rs" [[test]] name = "config" path = "tests/config.rs" [[test]] name = "error_value" path = "tests/error_value.rs" [[test]] name = "in_diff" path = "tests/in_diff.rs" [[test]] name = "in_place" path = "tests/in_place.rs" [[test]] name = "insta" path = "tests/insta.rs" [[test]] name = "iterate" path = "tests/iterate.rs" [[test]] name = "jobs" path = "tests/jobs.rs" [[test]] name = "list" path = "tests/list.rs" [[test]] name = "main" path = "tests/main.rs" [[test]] name = "nextest" path = "tests/nextest.rs" [[test]] name = "shard" path = "tests/shard.rs" [[test]] name = "trace" path = "tests/trace.rs" [[test]] name = "unix" path = "tests/unix.rs" [[test]] name = "windows" path = "tests/windows.rs" [[test]] name = "workspace" path = "tests/workspace.rs" [dependencies.anyhow] version = "1.0.86" [dependencies.camino] version = "1.1.6" [dependencies.cargo_metadata] version = "0.19" [dependencies.clap] version = "4.4.1" features = [ "deprecated", "derive", "env", "wrap_help", ] [dependencies.clap_complete] version = "4" [dependencies.color-print] version = "0.3" [dependencies.console] version = "0.15" [dependencies.ctrlc] version = "3.2.1" features = ["termination"] [dependencies.fastrand] version = "2" [dependencies.fs2] version = "0.4" [dependencies.globset] version = "0.4.10" [dependencies.humantime] version = "2.1.0" [dependencies.ignore] version = "0.4.20" [dependencies.indoc] version = "2.0.0" [dependencies.itertools] version = "0.13" [dependencies.jobserver] version = "0.1" [dependencies.mutants] version = "0.0.3" [dependencies.num_cpus] version = "1.16" [dependencies.nutmeg] version = "0.1.4" [dependencies.patch] version = "0.7" [dependencies.path-slash] version = "0.2" [dependencies.proc-macro2] version = "1.0.74" features = ["span-locations"] [dependencies.quote] version = "1.0.35" [dependencies.regex] version = "1.10" [dependencies.serde] version = "1.0.194" features = ["derive"] [dependencies.serde_json] version = "1.0.118" [dependencies.similar] version = "2.1" [dependencies.strum] version = "0.26" features = ["derive"] [dependencies.syn] version = "2.0.46" features = [ "full", "extra-traits", "visit", ] [dependencies.tempfile] version = "3.8" [dependencies.test-log] version = "0.2.16" features = ["trace"] [dependencies.time] version = "0.3" [dependencies.toml] version = "0.8" [dependencies.tracing] version = "0.1.40" [dependencies.tracing-appender] version = "0.2" [dependencies.tracing-subscriber] version = "0.3.18" [dependencies.whoami] version = "1.5" [dev-dependencies.assert_cmd] version = "2.0" [dev-dependencies.assert_matches] version = "1.5" [dev-dependencies.cp_r] version = "0.5.2" [dev-dependencies.insta] version = "1.12" [dev-dependencies.lazy_static] version = "1.4" [dev-dependencies.predicates] version = "3" [dev-dependencies.pretty_assertions] version = "1" [dev-dependencies.rusty-fork] version = "0.3" [dev-dependencies.walkdir] version = "2.5" [target."cfg(unix)".dependencies.nix] version = "0.29" features = [ "process", "signal", ] cargo-mutants-25.0.0/Cargo.toml.orig000064400000000000000000000062541046102023000153720ustar 00000000000000[package] name = "cargo-mutants" version = "25.0.0" edition = "2021" authors = ["Martin Pool"] license = "MIT" description = "Inject bugs and see if your tests catch them" repository = "https://github.com/sourcefrog/cargo-mutants" homepage = "https://mutants.rs/" categories = ["development-tools::testing"] keywords = ["testing", "mutants", "cargo", "mutation-testing", "coverage"] rust-version = "1.78" [package.metadata.wix] upgrade-guid = "CA7BFE8D-F3A7-4D1D-AE43-B7749110FA90" path-guid = "457C7F8E-0F02-42CC-80D9-FE55FBED23F8" license = false eula = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] anyhow = "1.0.86" camino = "1.1.6" cargo_metadata = "0.19" clap = { version = "4.4.1", features = [ "deprecated", "derive", "env", "wrap_help", ] } clap_complete = "4" color-print = "0.3" console = "0.15" ctrlc = { version = "3.2.1", features = ["termination"] } fastrand = "2" fs2 = "0.4" globset = "0.4.10" humantime = "2.1.0" ignore = "0.4.20" indoc = "2.0.0" itertools = "0.13" jobserver = "0.1" mutants = "0.0.3" num_cpus = "1.16" patch = "0.7" path-slash = "0.2" quote = "1.0.35" regex = "1.10" serde_json = "1.0.118" similar = "2.1" strum = { version = "0.26", features = ["derive"] } tempfile = "3.8" test-log = { version = "0.2.16", features = [ "trace" ] } time = "0.3" toml = "0.8" tracing = "0.1.40" tracing-appender = "0.2" tracing-subscriber = "0.3.18" whoami = "1.5" [dependencies.nutmeg] version = "0.1.4" # git = "https://github.com/sourcefrog/nutmeg.git" [dependencies.proc-macro2] features = ["span-locations"] version = "1.0.74" [dependencies.serde] version = "1.0.194" features = ["derive"] [dependencies.syn] version = "2.0.46" features = ["full", "extra-traits", "visit"] [target.'cfg(unix)'.dependencies] nix = { version = "0.29", features = ["process", "signal"] } [dev-dependencies] assert_cmd = "2.0" assert_matches = "1.5" cp_r = { version = "0.5.2" } # git = "https://github.com/sourcefrog/cp_r" insta = "1.12" lazy_static = "1.4" predicates = "3" pretty_assertions = "1" rusty-fork = "0.3" walkdir = "2.5" [workspace] members = ["mutants_attrs"] resolver = "2" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) cargo-dist-version = "0.14.1" # CI backends to support ci = "github" # The installers to generate for each app installers = [] # Target platforms to build apps for (Rust target-triple syntax) targets = [ "aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", ] # Publish jobs to run in CI pr-run-mode = "plan" # # Publish jobs to run in CI # pr-run-mode = "upload" # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" lto = "thin" [profile.mutants] inherits = "test" debug = "none" # Config for [workspace.metadata.release] pre-release-replacements = [ { "file" = "NEWS.md", search = "## Unreleased", replace = "## {{version}}", exactly = 1 }, { "file" = "CITATION.cff", search = "^version: .*", replace = "version: {{version}}", exactly = 1 }, ] cargo-mutants-25.0.0/DESIGN.md000064400000000000000000000402061046102023000137710ustar 00000000000000# cargo-mutants design See [README.md](README.md) for an overview of how the project works. See also [CONTRIBUTING.md](CONTRIBUTING.md) for more advice on style, approach, etc. ## Physical structure / source tree overview `main.rs` -- the `cargo mutants` entry point and command-line parsing. `cargo.rs` -- Knows how to compose Cargo commands. Actually running subprocesses is delegated to `process.rs`, so that we can later potentially run different build tools to Cargo. `build_dir.rs` -- Manage temporary build directories. `console.rs` -- colored output to the console including drawing progress bars. The interface to the `console` and `indicatif` crates is localized here. `copy_tree.rs` -- Copy a source file tree into a build dir, with gitignore and other exclusions. `interrupt.rs` -- Handle Ctrl-C signals by setting a global atomic flag, which is checked during long-running operations. `lab.rs` -- A mutants "lab": manages generating and testing mutants. Contains effectively the main loop of the program: build and test every mutant. `log_file.rs` -- Manage one log file per mutant scenario, within the output dir. `mutate.rs` -- Different types of mutations we can apply, based on the AST from `visit.rs`, including generating a diff for the mutation and generating a tree with the mutation applied. `options.rs` -- Global options for timeouts, etc. `main.rs` has the command line flags; this has an internal version of the options that have a pervasive effect through the program. `outcome.rs` -- The result of running a single test or build, including distinguishing which type of command was run (check/build/test), where the log file is, what happened (success/failure/timeout/etc), and whether a mutation was applied. `output.rs` -- Manages the `mutants.out` directory. `scenario.rs` -- Each of the build/test cycles is a "scenario": either building the source tree, testing the baseline, or testing a mutant. `source.rs` -- A source tree and files within it, including visiting each source file to find mutations. `span.rs` -- A (line, column) addressing within a source file, a range between two positions, and edits to the content based on those addresses. `visit.rs` -- Walk a source file's AST, and guess at likely-legal-but-wrong replacements. The interface to the `syn` parser is localized here, and also the core of cargo-mutants logic to guess at valid replacements. ## Major processing stages 1. Find the workspace enclosing the start directory. 1. Find packages within the workspace, and determine which packages to mutate. 1. Find targets within selected packages. 1. Generate mutants by walking down from the top source file of each target. 1. Copy the source tree. 1. Run baseline tests. 1. Test each mutant in parallel. ### Finding the workspace cargo-mutants is invoked from within, or given with `-d`, a single directory, called the _start directory_. To find mutants and run tests we first need to find the enclosing workspace and the packages within it. This is done basically by parsing the output of `cargo locate-project` and `cargo metadata`. ### Finding packages Within the workspace there can be multiple packages. The implementation of this tries to keep the path open to later supporting build systems other than Cargo, such as perhaps Bazel. In Bazel, most likely, package names will be Bazel package names (or possibly target names.) Packages are used in several ways in cargo-mutants: 1. The user can select which packages to mutate, with `--package` or `--workspace`. If neither is specified we try to give the same heuristic behavior as other cargo commands, based on the start directory. 2. Packages are the unit at which we can ask cargo to build code or run tests. #### Package ids It is rare but legal in Cargo to have multiple packages with the same base name but different versions within a tree: this can happen when a package recursively depends on a different version of itself. (`itertools` is a good example because its package depends on `criterion` which depends on an older version of `itertools`.) In that case, `cargo build -p itertools` will fail because the name is ambiguous: even though there is only one package of that name in the working tree, the `cargo -p` option can also match against dependencies. To avoid these problems we invoke cargo with the ["package id"](https://doc.rust-lang.org/cargo/reference/pkgid-spec.html) including the version, which should be ambiguous? ### Package targets For each package, cargo tells us the build targets. Each target is a _crate_, the rustc translation unit. Targets may have various types: tests, binaries, libraries, and potentially others. Test targets are not considered for mutation, so this leaves us with some targets of interest, and for each of them cargo tells us one _top source file_, typically something like `src/lib.rs` or `src/main.rs`. ### Discovering mutants After discovering packages and before running any tests, we discover all the potential mutants. Starting from the top files for each package, we parse each source file using `syn` and then walk its AST. In the course of that walk we can find three broad categories of patterns: * A `mod` statement (without a block), which tells us of another source file we must remember to walk. * A source pattern that cargo-mutants knows how to mutate, such as a function returning a value. * A pattern that tells cargo-mutants not to look further into this branch of the tree, such as `#[test]` or `#[mutants::skip]`. For baseline builds and tests, we test all the packages that will later be mutated. For mutant builds and tests, we pass `--package` to build and test only the package containing the mutant, on the assumption that each mutant should be caught by its own package's tests. ### Copying the source tree Mutations are tested in copies of the source tree. (An option could be added to test in-place, which would be nice for CI.) Initially, one copy is made to run baseline tests; if they succeed then additional copies are made as necessary for each parallel job. After copying the tree, cargo-mutants scans the top-level `Cargo.toml` and any `.cargo/config.toml` for relative dependencies. If there are any, the paths are rewritten to be absolute, so that they still work when cargo is run in the scratch directory. Currently, the whole workspace tree is copied. In future, possibly only the package to be mutated could be copied: this would require changes to the code that fixes up dependencies. Copies by default respect gitignore, but this can be turned off. Each parallel build dir is copied from the original source so that it sees any gitignore files in parent directories. (This current approach assumes that all the packages are under the workspace directory, which is common but not actually required.) ## Handling timeouts Mutations can cause a program to go into an infinite (or just very long) loop: for example we might mutate a function in `if should_terminate() { break }` to return false. It's also possible that the un-mutated program has a bug that makes its test suite loop forever sometimes. Obviously this is a bug but we want cargo-mutants to be safe and easy to use on arbitrary trees that might have bugs. We want to handle timeouts internally for a few reasons, including: * If one mutation hangs we still want to go on and try others. (So it's not so good if the `cargo mutants` process is killed by the user or a CI timeout.) * The fact that the mutation hung is a potentially interesting signal about the program to report. (Possibly the user will just have to mark `should_terminate` as skipped, but at least they can do that once and then have other builds go faster.) * For either CI or interactive use it's better if `cargo mutants` finishes in a bounded time. (We are primarily concerned here with timeouts on tests; let's assume that `cargo build` will never get stuck; if it does then the whole environment probably has problems that need user investigation.) The timeout for running tests is controlled by `Options::timeout`. The timeout can be set by the user with `--timeout`, in which case it's simply used as is. If it's not specified, it is auto-set from the time to run the baseline tests, with a multiplier and a floor. Detecting that a program has run too long is simple: we just watch the clock while waiting for it to finish. Terminating it, however, is more complicated: The immediate child process spawned by `cargo-mutants` is `cargo test ...`. This in turn spawns its own children running the various test binaries. It is these grandchild processes that are most likely stuck in a loop. (It's also possible, and not unlikely, that the test binaries themselves start children: the cargo-mutants CLI tests do this. And those great-grand-children might get stuck. But the same logic applies.) cargo mutants .... cargo test ... target/debug/someprog_api_test target/debug/someprog_cli_test target/debug/someprog ... When we decide to stop the long-running test, we need to terminate the whole tree of processes. Unix provides a "process group" concept for doing this: we put the immediate child in a new process group, and then all its descendents will also be in that process group. We can stop the whole lot using `killpg`. However, the test processes are then _not_ in cargo-mutants's process group. So if the user hits ctrl-c on `cargo mutants`, that signal will not get to the test processes: cargo mutants would stop but the test process that's actually chewing up the CPU will continue. Therefore we need to also intercept the signal to cargo-mutants and manually pass it on to the subprocess group. ## Output directory handling Various output files, including the text output from all the cargo commands are written into `mutants.out` within the directory specified by `--output`, or by default the source directory. ## Cargo quirks To build the tests, we actually run `cargo test --no-test` (or similarly for Nextest) so that the build uses the same profile as the tests. This is because the tests are built with the `test` profile, which can have different settings from the `dev` profile. ## Handling strict lints Some trees are configured so that any unused variable is an error. This is a reasonable choice to keep the tree very clean in CI, but if unhandled it would be a problem for cargo mutants. Many mutants -- in fact at the time of writing all generated mutants -- ignore function parameters and return a static value. Rejecting them due to the lint failure is a missed opportunity to consider a similar but more subtle potential bug. Therefore when running `rustc` we configure all warnings off, with `--cap-lints`. ## Testing Testing cargo-mutants is of course important: both so that it works reliably and also so that it's a good example of a well-tested project that should have few missed mutants. ### integration and unit tests The tests have a combination of: 1. Rust "unit" tests run in-process, declared in `mod test{}` within various files in `src`. 2. Rust "integration" tests in the `tests` directory that actually run `cargo-mutants` as a subprocess and that test its overall behavior. The integration tests are in some sense more realistic because they drive the same interface as real users, but they will also typically be slower because they spawn subprocesses. As a result, my general idea is to have at least basic coverage of each feature with an integration test, and then to fill in the other cases with unit tests. 1. Make a copy of a `testdata` tree, so that it's not accidentally modified. 2. Run a `cargo-mutants` command on it. 3. Inspect the stdout, return code, or `mutants.out`. `cargo-mutants` runs as a subprocess of the test process so that we get the most realistic view of its behavior. In some cases it is run via the `cargo` command to test that this level of indirection works properly. ### Test performance Since cargo-mutants itself runs the test suite of the program under test many times there is a risk that the test suite can get slow. Aside from slowing down developers and CI, this has a multiplicative effect on the time to run `cargo mutants` on itself. To manage test time: * Although key behaviour should be tested through integration tests that run the CLI, it's OK to handle additional cases with unit tests that run much faster. * Whenever reasonable, CLI tests can only list mutants with `--list` rather than actually testing all of them: we have just a small set of tests that check that the mutants that are listed are actually run. * Use relatively small testdata trees that are sufficient to test the right behavior. ### `testdata` trees Many tests run against trees under `testdata`. These have been "disarmed" by renaming `Cargo.toml` to `Cargo_test.toml`, so `cargo` won't normally run them, or see them as part of the main workspace. (See for context.) Tests should always run against a copy of these trees using `copy_of_testdata`, to make sure their work has no side effects on the main tree. Please describe the purpose of the testdata tree inside the tree, either in `Cargo.toml` or a `README.md` file. To make a new tree you can copy an existing tree, but make sure to change the package name in its `Cargo.toml`. ### `--list` tests There is a general test that runs `cargo mutants --list` on each tree and compares the result to an Insta snapshot, for both the text and json output formats. Many features can be tested adequately by only looking at the list of mutants produced, with no need to actually test the mutants. In this case the generic list tests might be enough. ### Nextest tests cargo-mutants tests require `nextest` to be installed so that we can test the integration. ## UI Style Always print paths with forward slashes, even on Windows. Use `path_slash`. ## Logging cargo-mutants writes two types of log files into the `mutants.out` directory: Within the `log` directory, there is a file for each mutant scenario. This mostly contains the output from the Cargo commands, interspersed with some lines showing what command was run and the outcome. This is opened in append mode so that both cargo-mutants and cargo can write to it. This log is watched and the last line is shown in the progress bar to indicate what's going on. Also, a file `mutants.out/debug.log` is written using [tracing](https://docs.rs/tracing) and written using the tracing macros. This contains more detailed information about what's happening in cargo-mutants itself. ## Parallelism cargo-mutants supports running multiple tests in parallel. Although both Cargo builds and Rust tests can parallelize internally, they can also both end up waiting for a single compilation unit, link, or straggling test. Running multiple tests in parallel makes better use of machine resources when each individual test becomes serialized. The initial baseline build is done in a single job, with no parallelism at the cargo-mutants layer. If the baseline test completes successfully, its build directory is copied to make a total of one per parallel job. Unlike the initial copy from the source directory, this includes the `target` directory, since it should now be up to date for the compiler options that cargo-mutants will use. We then launch the appropriate number of threads, each of which has its own build directory. They each build and test new mutants until everything is done, or until there's an error that stops all processing. ## `RUSTFLAGS` Cargo has a somewhat complex system for controlling flags passed to `rustc`. cargo-mutants uses this to pass `--cap-lints` to avoid failures due to strict lints. However, for other settings, we want to keep using whatever flags the user has configured in their environment, in the source tree, or in their per-user configuration. Unfortunately the Cargo `RUSTFLAGS` or `CARGO_ENCODED_RUSTFLAGS` variables entirely replace, instead of appending to, the user's flags. Fully handling this seems to require us to reimplement some of Cargo's logic from to find the right target and config, in the right files, determine the user's flags, and then append our own. For now, cargo-mutants appends to the environment variables but does not attempt to read the config files. cargo-mutants-25.0.0/LICENSE000064400000000000000000000020541046102023000135020ustar 00000000000000MIT License Copyright (c) 2021 Martin Pool Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cargo-mutants-25.0.0/NEWS.md000064400000000000000000000757261046102023000136130ustar 00000000000000# cargo-mutants changelog ## 25.0.0 - Better estimation of time remaining, based on the time taken to test mutants so far, excluding the time for the baseline. - New: `--copy-vcs` option and config option will copy `.git` and other VCS directories, to accommodate trees whose tests depend on the contents or presence of the VCS directory. - New: Add `.jj` to the list of known VCS directories. These are excluded by default when copying the tree, but can be included using `--copy-vcs=true`. - Fixed: `cargo --package` options now include the version number of the targeted package, like `foo@0.1.2`. This avoids cargo failing with an error that the package name is ambiguous in trees whose dependencies include multiple versions of one of the packages being tested. ## 24.11.2 - Changed: `.gitignore` (and other git ignore files) are only consulted when copying the tree if it is contained within a directory with a `.git` directory. - Fixed: `.gitignore` files above the git root directory are no longer read. In particular this fixes the problem where `.gitignore *` in the home directory would prevent copying any source trees. ## 24.11.1 - Changed: The arguments of calls to functions or methods named `with_capacity` are not mutated by default. This can be turned off with `--skip-calls-defaults=false` on the command line or `skip_calls_defaults = false` in `.cargo/mutants.toml`. - New: `--skip-calls=NAME,NAME` on the command line or `skip_calls = [NAMES..]` in `.cargo/mutants.toml` allows configuring other functions whose calls should never be mutated. ## 24.11.0 - New: `--test-workspace` and `--test-package` arguments and config options support projects whose tests live in a different package. - New: Mutate `proc_macro` targets and functions. - New: Write diffs to dedicated files under `mutants.out/diff/`. The filename is included in the mutant json output. - New: The package tarball on `crates.io` now includes all the test data, so that the tests can be run on the unpacked tarball. This may be helpful for people packaging `cargo-mutants` for distributions, and keeps an accurate record of the whole tree separate from the git history. ## 24.9.0 - Fixed: Avoid generating empty string elements in `ENCODED_RUSTFLAGS` when `--cap-lints` is set. In some situations these could cause a compiler error complaining about the empty argument. - New: `--profile` option allows selecting a Cargo profile. In particular, it's recommended that you can use `--profile=mutants` and configure a custom profile in your `Cargo.toml` to optimize the build for mutants, by turning off debug symbols. - New: `--iterate` option skips mutants that were previously caught or unviable. - New: cargo-mutants starts a GNU jobserver, shared across all children, so that running multiple `--jobs` does not spawn an excessive number of compiler processes. The jobserver is on by default and can be turned off with `--jobserver false`. - Fixed: Don't error on diffs containing a "Binary files differ" message. ## 24.7.1 - Changed: No build timeouts by default. Previously, cargo-mutants set a default build timeout based on the baseline build, but experience showed that this would sometimes make builds flaky, because build times can be quite variable. If mutants cause builds to hang, then you can still set a timeout using `--build-timeout` or `--build-timeout-multiplier`. - Fixed: Don't error if the `--in-diff` file is empty. - Changed: cargo-mutants no longer passes `--cap-lints=allow` to rustc. This was previously done so that mutants would not unnecessarily be unviable due to triggering compiler warnings in trees configured to deny some lints, but it had the undesirable effect of disabling rustc's detection of long running const evaluations. If your tree treats some lints as errors then the previous behavior can be restored with `--cap-lints=true` (or the equivalent config option), or you can use `cfg_attr` and a feature flag to accept those warnings when testing under cargo-mutants. ## 24.7.0 - Fixed: The auto-set timeout for building mutants is now 2 times the baseline build time times the number of jobs, with a minimum of 20 seconds. This was changed because builds of mutants contend with each other for access to CPUs and may be slower than the baseline build. ## 24.5.0 - Fixed: Follow `path` attributes on `mod` statements. - New: `--build-timeout` and `--build-timeout-multiplier` options for setting timeouts for the `build` and `check` cargo phases. - Changed: `--timeout-multiplier` now overrides `timeout_multiplier` from `.cargo/mutants.toml`. - Changed: `--timeout` and `--timeout-multiplier` are now conflicting options. ## 24.4.0 - Changes: Baselines and mutants are now built with `cargo test --no-run` rather than `cargo build --tests` as previously. This avoids wasted build effort if the `dev` and `test` Cargo profiles are not the same, and may better distinguish build failures from test failures. With `--test-tool=nextest`, the corresponding `cargo nextest run --no-run` is used. - Fixed: `.ignore` files can no longer affect source tree copying, so test files listed in a `.ignore` (e.g. `*.snap` for Insta snapshots) are now correctly copied into temporary build directories. - Fixed: Don't visit files marked with `#![cfg(test)]` (or other inner attributes that generally cause code to be skipped.) - Fixed: Paths to module files nested within `mod` blocks are now correctly resolved. - Added: Document stability policy in the manual. - New: Generate mutations that delete the `!` and `-` unary operators. ## 24.3.0 - Fixed: `cargo install cargo-mutants` without `--locked` was failing due to breaking API changes in some unstable dependencies. - Changed: In globs, `*` no longer matches path separators, only parts of a filename. For example, `src/*.rs` will now only match files directly in `src/`, not in subdirectories. To include subdirectories, use `**` as in `src/**/*.rs`. And, patterns that do not contain a path separator match directories at any level, and all files within them. For example, `-f db` will match `src/db.rs` and `src/db/mod.rs` and all files in `src/db/` or in `other/db`. This may break existing configurations but is considered a bug fix because it brings the behavior in line with other tools and allows more precise expressions. - Changed: Minimum Rust version (to build cargo-mutants, not to use it) increased to 1.74. - Changed: Removed the count of `failure` from `mutants.out/outcomes.json`: it was already the case that every outcome received some other classification, so the count was always zero. ## 24.2.1 - New: `--features`, `--no-default-features` and `--all-features` options are passed through to Cargo. - Changed: Minimum Rust version (to build cargo-mutants, not to use it) increased to 1.73. - New: Warn if nextest returns an exit code indicating some failure other than test failure, such as an internal error in nextest. - New: json output includes the exit code of subprocesses, and the signal if it was killed by a signal. - Changed: Set `INSTA_FORCE_PASS=0` (in addition to previously `INSTA_UPDATE=no`) when running tests, so that tests that use the [Insta](https://insta.rs/) library don't write updates back into the source directory, and so don't falsely pass. - New: `--timeout-multiplier` option allows setting the timeout for mutants to be a multiple of the baseline timeout, rather than a fixed time. ## 24.2.0 - New: Colored output can be enabled in CI or other noninteractive situations by passing `--colors=always`, or setting `CARGO_TERM_COLOR=always`, or `CLICOLOR_FORCE=1`. Colors can similarly be forced off with `--colors=never`, `CARGO_TERM_COLOR=never`, or `NO_COLOR=1`. ## 24.1.2 - New: `--in-place` option tests mutations in the original source tree, without copying the tree. This is faster and uses less disk space, but it's incompatible with `--jobs`, and you must be careful not to edit or commit the source tree while tests are running. ## 24.1.1 - New: Mutate `+, -, *, /, %, &, ^, |, <<, >>` binary ops, and their corresponding assignment ops like `+=`. - New: `--baseline=skip` option to skip running tests in an unmutated tree, when they're already been checked externally. - Changed: Stop generating mutations of `||` and `&&` to `!=` and `||`, because it seems to raise too many low-value false positives that may be hard to test. - Fixed: Colors in command-line help and error messages. ## 24.1.0 - New! `cargo mutants --test-tool nextest`, or `test_tool = "nextest"` in `.cargo/mutants.toml` runs tests under [Nextest](https://nexte.st/). Some trees have tests that only work under Nextest, and this allows them to be tested. In other cases Nextest may be significantly faster, because it will exit soon after the first test failure. - Fixed: Fixed spurious "Patch input contains repeated filenames" error when `--in-diff` is given a patch that deletes multiple files. ## 23.12.2 - New: A `--shard k/n` allows you to split the work across n independent parallel `cargo mutants` invocations running on separate machines to get a faster overall solution on large suites. You, or your CI system, are responsible for launching all the shards and checking whether any of them failed. - Improved: Better documentation about `-j`, with stronger recommendations not to set it too high. - New: Binary releases on GitHub through cargo-dist. ## 23.12.1 - Improved progress bars and console output, including putting the outcome of each mutant on the left, and the overall progress bar at the bottom. Improved display of estimated remaining time, and other times. - Fixed: Correctly traverse `mod` statements within package top source files that are not named `lib.rs` or `main.rs`, by following the `path` setting of each target within the manifest. - Improved: Don't generate function mutants that have the same AST as the code they're replacing. ## 23.12.0 An exciting step forward: cargo-mutants can now generate mutations smaller than a whole function. To start with, several binary operators are mutated. - New: Mutate `==` to `!=` and vice versa. - New: Mutate `&&` to `||` and vice versa, and mutate both of them to `==` and `!=`. - New: Mutate `<`, `<=`, `>`, `>=`. - Changed: If no mutants are generated then `cargo mutants` now exits successfully, showing a warning. (Previously it would exit with an error.) This works better with `--in-diff` in CI, where it's normal that some changes may not have any mutants. - Changed: Include column numbers in text listings of mutants and output to disambiguate smaller-than-function mutants, for example if there are several operators that can be changed on one line. This also applies to the names used for regex matching, so may break some regexps that match the entire line (sorry). The new option `--line-col=false` turns them both off in `--list` output. - Changed: In the mutants.json format, replaced the `function`, `line`, and `return_type` fields with a `function` submessage (including the name and return type) and a `span` indicating the entire replaced region, to better handle smaller-than-function mutants. Also, the `function` includes the line-column span of the entire function. ## 23.11.2 - Changed: If `--file` or `--exclude` are set on the command line, then they replace the corresponding config file options. Similarly, if `--re` is given then the `examine_re` config key is ignored, and if `--exclude-re` is given then `exclude_regex` is ignored. (Previously the values were combined.) This makes it easier to use the command line to test files or mutants that are normally not tested. - Improved: By default, files matching gitignore patterns (including in parent directories, per-user configuration, and `info/exclude`) are excluded from copying to temporary build directories. This should improve performance in some large trees with many files that are not part of the build. This behavior can be turned off with `--gitignore=false`. - Improved: Run `cargo metadata` with `--no-deps`, so that it doesn't download and compute dependency information, which can save time in some situations. - Added: Alternative aliases for command line options, so you don't need to remember if it's "regex" or "re": `--regex`, `--examine-re`, `--examine-regex` (all for names to include) and `--exclude-regex`. - Added: Accept `--manifest-path` as an alternative to `-d`, for consistency with other cargo commands. ## 23.11.1 - New `--in-diff FILE` option tests only mutants that are in the diff from the given file. This is useful to avoid testing mutants from code that has not changed, either locally or in CI. ## 23.11.0 - Changed: `cargo mutants` now tries to match the behavior of `cargo test` when run within a workspace. If run in a package directory, it tests only that package. If run in a workspace that is not a package (a "virtual workspace"), it tests the configured default packages, or otherwise all packages. This can all be overridden with the `--package` or `--workspace` options. - New: generate key-value map values from types like `BTreeMap>`. - Changed: Send trace messages to stderr rather stdout, in part so that it won't pollute json output. ## 23.10.0 - The baseline test (with no mutants) now tests only the packages in which mutants will be generated, subject to any file or regex filters. This should both make baseline tests faster, and allow testing workspaces in which some packages have non-hermetic tests. ## 23.9.1 - Mutate the known collection types `BinaryHeap`, `BTreeSet`, `HashSet`, `LinkedList`, and `VecDeque` to generate empty and one-element collections using `T::new()` and `T::from_iter(..)`. - Mutate known container types like `Arc`, `Box`, `Cell`, `Mutex`, `Rc`, `RefCell` into `T::new(a)`. - Mutate unknown types that look like containers or collections `T` or `T<'a, A>'` and try to construct them from an `A` with `T::from_iter`, `T::new`, and `T::from`. - Minimum Rust version updated to 1.70. - Mutate `Cow<'_, T>` into `Owned` and `Borrowed` variants. - Mutate functions returning `&[T]` and `&mut [T]` to return leaked vecs of values. - Mutate `(A, B, C, ...)` into the product of all replacements for `a, b, c, ...` - The combination of options `--list --diff --json` is now supported, and emits a `diff` key in the JSON. - Mutate `-> impl Iterator` to produce empty and one-element iterators of the item type. ## 23.9.0 - Fixed a bug causing an assertion failure when cargo-mutants was run from a subdirectory of a workspace. Thanks to Adam Chalmers! - Generate `HttpResponse::Ok().finish()` as a mutation of an Actix `HttpResponse`. ## 23.6.0 - Generate `Box::leak(Box::new(...))` as a mutation of functions returning `&mut`. - Add a concept of mutant "genre", which is included in the json listing of mutants. The only genre today is `FnValue`, in which a function body is replaced by a value. This will in future allow filtering by genre. - Recurse into return types, so that for example `Result` can generate `Ok(true)` and `Ok(false)`, and `Some` generates `None` and every generated value of `T`. Similarly for `Box`, `Vec`, `Rc`, `Arc`. - Generate specific values for integers: `[0, 1]` for unsigned integers, `[0, 1, -1]` for signed integers; `[1]` for NonZero unsigned integers and `[1, -1]` for NonZero signed integers. - Generate specific values for floats: `[0.0, 1.0, -1.0]`. - Generate (fixed-length) array values, like `[0; 256], [1; 256]` using every recursively generated value for the element type. ## 23.5.0 _"Pickled crab"_ Released 2023-05-27 - `cargo mutants` can now successfully test packages that transitively depend on a different version of themselves, such as `itertools`. Previously, cargo-mutants used the cargo `--package` option, which is ambiguous in this case, and now it uses `--manifest-path` instead. - Mutate functions returning `&'_ str` (whether a lifetime is named or not) to return `"xyzzy"` and `""`. - Switch to CalVer numbering. ## 1.2.3 Released 2023-05-05 - Mutate functions returning `String` to `String::new()` rather than `"".into()`: same result but a bit more idiomatic. - New `--leak-dirs` option, for debugging cargo-mutants. - Update to [syn 2.0](https://github.com/dtolnay/syn/releases/tag/2.0.0), adding support for new Rust syntax. - Minimum supported Rust version increased to 1.65 due to changes in dependencies. - New `--error` option, to cause functions returning `Result` to be mutated to return the specified error. - New `--no-config` option, to disable reading `.cargo/mutants.toml`. ## 1.2.2 Released 2023-04-01 - Don't mutate `unsafe` fns. - Don't mutate functions that never return (i.e. `-> !`). - Minimum supported Rust version increased to 1.64 due to changes in dependencies. - Some command-line options can now also be configured through environment variables: `CARGO_MUTANTS_JOBS`, `CARGO_MUTANTS_TRACE_LEVEL`. - New command line option `--minimum-test-timeout` and config file variable `minimum_test_timeout` join existing environment variable `CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT`, to allow boosting the minimum, especially for test environments with poor or uneven throughput. - Changed: Renamed fields in `outcomes.json` from `cargo_result` to `process_status` and from `command` to `argv`. - Warn if no mutants were generated or if all mutants were unviable. ## 1.2.1 Released 2023-01-05 - Converted most of the docs to a book available at . - Fixed: Correctly find submodules that don't use mmod.rs`naming, e.g. when descending from`src/foo.rs`to`src/foo/bar.rs`. Also handle module names that are raw identifiers using`r#`. (Thanks to @kpreid for the report.) ## 1.2.0 _Thankful mutants!_ - Fixed: Files that are excluded by filters are also excluded from `--list-files`. - Fixed: `--exclude-re` and `--re` can match against the return type as shown in `--list`. - New: A `.cargo/mutants.toml` file can be used to configure standard filters and cargo args for a project. ## 1.1.1 Released 2022-10-31 _Spooky mutants!_ - Fixed support for the Mold linker, or for other options passed via `RUSTFLAGS` or `CARGO_ENCODED_RUSTFLAGS`. (See the instructions in README.md). - Source trees are walked by following `mod` statements rather than globbing the directory. This is more correct if there are files that are not referenced by `mod` statements. Once attributes on modules are stable in Rust () this opens a path to skip mods using attributes. ## 1.1.0 Released 2022-10-30 _Fearless concurrency!_ - cargo-mutants can now run multiple cargo build and test tasks in parallel, to make better use of machine resources and find mutants faster, controlled by `--jobs`. - The minimum Rust version to build cargo-mutants is now 1.63.0. It can still be used to test code under older toolchains. ## 1.0.3 Released 2022-09-29 - cargo-mutants is now finds no uncaught mutants in itself! Various tests were added and improved, particularly around handling timeouts. - New: `--re` and `--exclude-re` options to filter by mutant name, including the path. The regexps match against the strings printed by `--list`. ## 1.0.2 Released 2022-09-24 - New: `cargo mutants --completions SHELL` to generate shell completions using `clap_complete`. - Changed: `cargo-mutants` no longer builds in the source directory, and no longer copies the `target/` directory to the scratch directory. Since `cargo-mutants` now sets `RUSTFLAGS` to avoid false failures from warnings, it is unlikely to match the existing build products in the source directory `target/`, and in fact building there is just likely to cause rebuilds in the source. The behavior now is as if `--no-copy-target` was always passed. That option is still accepted, but it has no effect. - Changed: `cargo-mutants` finds all possible mutations before doing the baseline test, so that you can see earlier how many there will be. - New: Set `INSTA_UPDATE=no` so that tests that use the [Insta](https://insta.rs/) library don't write updates back into the source directory, and so don't falsely pass. ## 1.0.1 Released 2022-09-12 - Fixed: Don't try to mutate functions within test targets, e.g. within `tests/**/*.rs`. - New: `missed.txt`, `caught.txt`, `timeout.txt` and `unviable.txt` files are written in to the output directory to make results easier to review later. - New: `--output` creates the specified directory if it does not exist. - Internal: Switched from Argh to Clap for command-line parsing. There may be some small changes in CLI behavior and help formatting. ## 1.0.0 Released 2022-08-21 A 1.0 release to celebrate that with the addition of workspace handling, cargo-mutants gives useful results on many Rust projects. - New: Supports workspaces containing multiple packages. Mutants are generated for all relevant targets in all packages, and mutants are subject to the tests of their own package. `cargo mutants --list-files --json` and `cargo mutants --list --json` now includes package names for each file or mutant. - Improved: Generate mutations in `cdylib`, `rlib`, and ever other `*lib` target. For example, this correctly exercises Wasm projects. - Improved: Write `mutants.out/outcomes.json` after the source-tree build and baseline tests so that it can be observed earlier on. - Improved: `mutants.out/outcomes.json` includes the commands run. ## 0.2.11 Released 2022-08-20 - New `--exclude` command line option to exclude source files from mutants generation, matching a glob. - New: `CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT` sets a minimum timeout for cargo tests, in seconds. This can be used to allow more time on slow CI builders. If unset the default is still 20s. - Added: A new `mutants.out/debug.log` with internal debugging information. - Improved: The time for check, build, and test is now shown separately in progress bars and output, to give a better indication of which is taking more time in the tree under test. Also, times are show in seconds with one decimal place, and they are styled more consistently. - Improved: More consistent use of 'unviable' and other terms for outcomes in the UI. ## 0.2.10 Released 2022-08-07 cargo-mutants 0.2.10 comes with improved docs, and the new `-C` option can be used to pass options like `--release` or `--all-features` to `cargo`. - Added: `--cargo-arg` (or `-C` for short) allows passing arguments to cargo commands (check, build, and test), for example to set `--release` or `--features`. - Improved: Works properly if run from a subdirectory of a crate, or if `-d` points to a subdirectory of a crate. - Improved: Various docs. - Improved: Relative dependencies within the source tree are left as relative paths, and will be built within the scratch directory. Relative dependencies outside the source tree are still rewritten as absolute paths. ## 0.2.9 Released 2022-07-30 - Faster: `cargo mutants` no longer runs `cargo check` before building, in cases where the build products are wanted or tests will be run. This saves a significant amount of work in build phases; in some trees `cargo mutants` is now 30% faster. (In trees where most of the time is spent running tests the effect will be less.) - Fixed: Open log files in append mode to fix messages from other processes occasionally being partly overwritten. - Improved: `cargo mutants` should now give useful results in packages that use `#![deny(unused)]` or other mechanisms to reject warnings. Mutated functions often ignore some parameters, which would previously be rejected by this configuration without proving anything interesting about test coverage. Now, `--cap-lints=allow` is passed in `RUSTFLAGS` while building mutants, so that they're not falsely rejected and the tests can be exercised. - Improved: The build dir name includes the root package name. - Improved: The progress bar shows more information. - Improved: The final message shows how many mutants were tested and how long it took. ## 0.2.8 Released 2022-07-18 - New: Summarize the overall number of mutants generated, caught, missed, etc, at the end. - Fixed: Works properly with crates that have relative `path` dependencies in `Cargo.toml` or `.cargo/config.toml`, by rewriting them to absolute paths in the scratch directory. ## 0.2.7 Released 2022-07-11 - New: You can skip functions by adding `#[cfg_attr(test, mutants::skip)`, in which case the `mutants` crate can be only a `dev-dependency`. - Improved: Don't generate pointless mutations of functions with an empty body (ignoring comments.) - Improved: Remove extra whitespace from the display of function names and return types: the new formatting is closer to the spacing used in idiomatic Rust. - Improved: Show the last line of compiler/test output while running builds, so that it's more clear where time is being spent. - Docs: Instructions on how to check for missed mutants from CI. ## 0.2.6 Released 2022-04-17 - Improved: Find source files by looking at `cargo metadata` output, rather than assuming they're in `src/**/*.rs`. This makes `cargo mutants` work properly on trees where it previously failed to find the source. - New `--version` option. - New: Write a `lock.json` into the `mutants.out` directory including the start timestamp, cargo-mutants version, hostname and username. Take a lock on this file while `cargo mutants` is running, so that it doesn't crash or get confused if two tasks try to write to the same directory at the same time. - New: Restored a `--list-files` option. - Changed: Error if no mutants are generated, which probably indicates a bug or configuration error(?) ## 0.2.5 Released 2022-04-14 - New `--file` command line option to mutate only functions in source files matching a glob. - Improved: Don't attempt to mutate functions called `new` or implementations of `Default`. cargo-mutants can not yet generate good mutations for these so they are generally false positives. - Improved: Better display of `::foo` and similar type paths. - New: `--output` directory to write `mutants.out` somewhere other than the source directory. ## 0.2.4 Released 2022-03-26 - Fix: Ignore errors setting file mtimes during copies, which can cause failures on Windows if some files are readonly. - Fix: Log file names now include only the source file relative path, the line number, and a counter, so they are shorter, and shouldn't cause problems on filesystems with length limits. - Change: version-control directories like `.git` are not copied with the source tree: they should have no effect on the build, so copying them is just a waste. - Changed/improved json logs in `mutants.out`: - Show durations as fractional seconds. - Outcomes include a "summary" field. ## 0.2.3 Released 2022-03-23 - Switch from Indicatif to [Nutmeg](https://github.com/sourcefrog/nutmeg) to draw progress bars and output. This fixes a bug where terminal output line-wraps badly, and adds a projection for the total estimated time to completion. - Change: Mutants are now tested in random order by default, so that repeated runs are more likely to surface interesting new findings early, rather than repeating previous results. The previous behavior of testing mutants in the deterministic order they're encountered in the tree can be restored with `--no-shuffle`. ## 0.2.2 Released 2022-02-16 - The progress bar now shows which mutant is being tested out of how many total. - The automatic timeout is now set to the minimum of 20 seconds, or 5x the time of the tests in a baseline tree, to reduce the incidence of false timeouts on machines with variable throughput. - Ctrl-c (or `SIGINT`) interrupts the program during copying the tree. Previously it was not handled until the copy was complete. - New `--no-copy-target` option. ## 0.2.1 Released 2022-02-10 - Arguments to `cargo test` can be passed on the command line after `--`. This allows, for example, skipping doctests or setting the number of test threads. ## 0.2.0 Released 2022-02-06 - A new `--timeout SECS` option to limit the runtime of any `cargo test` invocation, so that mutations that cause tests to hang don't cause `cargo mutants` to hang. A default timeout is set based on the time to run tests in an unmutated tree. There is no timeout by default on the unmutated tree. On Unix, the `cargo` subprocesses run in a new process group. As a consequence ctrl-c is explicitly caught and propagated to the child processes. - Show a progress bar while looking for mutation opportunities, and show the total number found. - Show how many mutation opportunities were found, before testing begins. - New `--shuffle` option tests mutants in random order. - By default, the output now only lists mutants that were missed or that timed out. Mutants that were caught, and mutants that did not build, can be printed with `--caught` and `--unviable` respectively. ## 0.1.0 Released 2021-11-30 - Logs and other information are written into `mutants.out` in the source directory, rather than `target/mutants`. - New `--all-logs` option prints all Cargo output to stdout, which is verbose but useful for example in CI, by making all the output directly available in captured stdout. - The output distinguishes check or build failures (probably due to an unviable mutant) from test failures (probably due to lacking coverage.) - A new file `mutants.out/mutants.json` lists all the generated mutants. - Show function return types in some places, to make it easier to understand whether the mutants were useful or viable. - Run `cargo check --tests` and `cargo build --tests` in the source directory to freshen the build and download any dependencies, before copying it to a scratch directory. - New `--check` option runs `cargo check` on generated mutants to see if they are viable, without actually running the tests. This is useful in tuning cargo-mutants to generate better mutants. - New `--no-times` output hides times (and tree sizes) from stdout, mostly to make the output deterministic and easier to match in tests. - Mutate methods too! ## 0.0.4 Released 2021-11-10 - Fixed `cargo install cargo-mutants` (sometimes?) failing due to the `derive` feature not getting set on the `serde` dependency. - Show progress while copying the tree. - Respect the `$CARGO` environment variable so that the same toolchain is used to run tests as was used to invoke `cargo mutants`. Concretely, `cargo +nightly mutants` should work correctly. ## 0.0.3 Released 2021-11-06 - Skip functions or modules marked `#[test]`, `#[cfg(test)]` or `#[mutants::skip]`. - Early steps towards type-guided mutations: - Generate mutations of `true` and `false` for functions that return `bool` - Empty and arbitrary strings for functions returning `String`. - Return `Ok(Default::default())` for functions that return `Result<_, _>`. - Rename `--list-mutants` to just `--list`. - New `--list --json`. - Colored output makes test names and mutations easier to read (for me at least.) - Return distinct exit codes for different situations including that uncaught mutations were found. ## 0.0.2 - Functions that should not be mutated can be marked with `#[mutants::skip]` from the [`mutants`](https://crates.io/crates/mutants) helper crate. ## 0.0.1 First release. cargo-mutants-25.0.0/README.md000064400000000000000000000062021046102023000137530ustar 00000000000000# cargo-mutants [![Tests](https://github.com/sourcefrog/cargo-mutants/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/sourcefrog/cargo-mutants/actions/workflows/tests.yml?query=branch%3Amain) [![crates.io](https://img.shields.io/crates/v/cargo-mutants.svg)](https://crates.io/crates/cargo-mutants) [![libs.rs](https://img.shields.io/badge/libs.rs-cargo--mutants-blue)](https://lib.rs/crates/cargo-mutants) [![GitHub Sponsors](https://img.shields.io/badge/Sponsor-%E2%9D%A4-%23db61a2.svg?&logo=github&logoColor=white&labelColor=181717&style=flat-square)](https://github.com/sponsors/sourcefrog) cargo-mutants helps you improve your program's quality by finding places where bugs could be inserted without causing any tests to fail. Coverage measurements can be helpful, but they really tell you what code is _reached_ by a test, and not whether the test really _checks_ anything about the behavior of the code. Mutation tests give different information, about whether the tests really check the code's behavior. The goal of cargo-mutants is to be _easy_ to run on any Rust source tree, and to tell you something _interesting_ about areas where bugs might be lurking or the tests might be insufficient. **The main documentation is the user guide at .** ## Prerequisites cargo-mutants can help on trees with non-flaky tests that run under `cargo test` or [`cargo nextest run`](https://nexte.st/). ## Install ```sh cargo install --locked cargo-mutants ``` You can also install using [cargo-binstall](https://github.com/cargo-bins/cargo-binstall) or from binaries attached to GitHub releases. ## Quick start From within a Rust source directory, just run ```sh cargo mutants ``` To generate mutants in only one file: ```sh cargo mutants -f src/something.rs ``` ## Integration with CI The [manual includes instructions and examples for automatically testing mutants in CI](https://mutants.rs/ci.html), including incremental testing of pull requests and full testing of the development branch. ## Help advance cargo-mutants If you use cargo-mutants or just like the idea you can help it get better: * [Post an experience report in GitHub discussions](https://github.com/sourcefrog/cargo-mutants/discussions), saying whether it worked, failed, found interesting results, etc. * [Sponsor development](https://github.com/sponsors/sourcefrog) ## Project status As of January 2024 this is an actively-maintained spare time project. I expect to make [releases](https://github.com/sourcefrog/cargo-mutants/releases) about every one or two months. It's very usable at it is and there's room for lots more future improvement, especially in adding new types of mutation. This software is provided as-is with no warranty of any kind. ## Further reading See also: * [cargo-mutants manual](https://mutants.rs/) * [How cargo-mutants compares to other techniques and tools](https://github.com/sourcefrog/cargo-mutants/wiki/Compared). * [Design notes](DESIGN.md) * [Contributing](CONTRIBUTING.md) * [Release notes](NEWS.md) * [Discussions](https://github.com/sourcefrog/cargo-mutants/discussions) cargo-mutants-25.0.0/SECURITY.md000064400000000000000000000022501046102023000142640ustar 00000000000000# Security Policy ## Supported Versions Only the latest release is supported. ## Reporting a Vulnerability If you are aware of a security vulnerability or risk in cargo-mutants, please contact me directly by mail at , rather than filing a public bug. I expect to normally respond within one week but this is not guaranteed. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Security Model cargo-mutants runs `cargo build` and `cargo test` on the specified source tree, and on generated mutations of that tree. Rust builds (through `build.rs`) and tests necessarily provide a means for generic code execution. Malicious code under test will have control of the test environment. If the code is not trusted it should be tested within a strong sandbox. cargo-mutants-25.0.0/book/book.toml000064400000000000000000000006061046102023000152570ustar 00000000000000[book] authors = ["Martin Pool"] language = "en" multilingual = false src = "src" title = "cargo-mutants" [output.html] git-repository-url = "https://github.com/sourcefrog/cargo-mutants" edit-url-template = "https://github.com/sourcefrog/cargo-mutants/edit/main/book/{path}" [output.linkcheck] follow-web-links = true [output.linkcheck.http-headers] 'crates\.io' = ["Accept: text/html"] cargo-mutants-25.0.0/book/src/SUMMARY.md000064400000000000000000000031021046102023000156700ustar 00000000000000# Summary - [Welcome](welcome.md) - [Installation](installation.md) - [Getting started](getting-started.md) - [Using the results](using-results.md) - [Hangs and timeouts](timeouts.md) - [Exit codes](exit-codes.md) - [The `mutants.out` directory](mutants-out.md) - [Skipping untestable code](skip.md) - [Skipping functions with an attribute](attrs.md) - [Skipping function calls](skip_calls.md) - [Filtering files](skip_files.md) - [Filtering functions and mutants](filter_mutants.md) - [Controlling cargo-mutants](controlling.md) - [Display and output](output.md) - [Listing and previewing mutations](list.md) - [Workspaces and packages](workspaces.md) - [Passing options to Cargo](cargo-args.md) - [Copying the tree](build-dirs.md) - [Using nextest](nextest.md) - [Baseline tests](baseline.md) - [Testing in-place](in-place.md) - [Iterating on missed mutants](iterate.md) - [Strict lints](lints.md) - [Generating mutants](mutants.md) - [Error values](error-values.md) - [Macros](macros.md) - [Improving performance](performance.md) - [Parallelism](parallelism.md) - [Jobserver](jobserver.md) - [Sharding](shards.md) - [Testing code changed in a diff](in-diff.md) - [Integrations](integrations.md) - [Continuous integration](ci.md) - [Incremental tests of pull requests](pr-diff.md) - [How it works](how-it-works.md) - [Goals](goals.md) - [Mutations vs coverage](vs-coverage.md) - [Differences from fuzzing](vs-fuzzing.md) - [Limitations](limitations.md) - [How to help](how-to-help.md) - [Stability](stability.md) - [Changelog](changelog.md) cargo-mutants-25.0.0/book/src/attrs.md000064400000000000000000000027571046102023000157070ustar 00000000000000# Skipping functions with an attribute To mark functions as skipped, so they are not mutated: 1. Add a Cargo dependency on the [mutants](https://crates.io/crates/mutants) crate, version "0.0.3" or later. (This must be a regular `dependency` not a `dev-dependency`, because the annotation will be on non-test code.) 2. Mark functions with `#[mutants::skip]` or other attributes containing `mutants::skip` (e.g. `#[cfg_attr(test, mutants::skip)]`). The `mutants` crate is tiny and the attribute has no effect on the compiled code. It only flags the function for cargo-mutants. However, you can avoid the dependency by using the slightly longer `#[cfg_attr(test, mutants::skip)]` form. **Note:** Currently, `cargo-mutants` does not (yet) evaluate attributes like `cfg_attr`, it only looks for the sequence `mutants::skip` in the attribute. You may want to also add a comment explaining why the function is skipped. For example: ```rust use std::time::{Duration, Instant}; /// Returns true if the program should stop #[cfg_attr(test, mutants::skip)] // Returning false would cause a hang fn should_stop() -> bool { true } pub fn controlled_loop() { let start = Instant::now(); for i in 0.. { println!("{}", i); if should_stop() { break; } if start.elapsed() > Duration::from_secs(60 * 5) { panic!("timed out"); } } } mod test { #[test] fn controlled_loop_terminates() { super::controlled_loop() } } ``` cargo-mutants-25.0.0/book/src/baseline.md000064400000000000000000000041161046102023000163230ustar 00000000000000# Baseline tests Normally, cargo-mutants builds and runs your tree in a temporary directory before applying any mutations. This makes sure that your tests are in fact all passing, including in the copy of the tree that cargo-mutants will mutate. Baseline tests can be skipped by passing the `--baseline=skip` command line option. (There is no config option for this.)
If you use --baseline=skip, you must make sure that the tests are actually passing, otherwise the results of cargo-mutants will be meaningless. cargo-mutants will probably report that all or most mutations were caught, but the test failures were not because of the mutations.
## Performance effects The performance gain from skipping the baseline is one run of the full test suite, plus one incremental build. When the baseline is run, its build is typically slow because it must do the initial build of the tree, but when it is skipped, the first mutant will have to do a full (rather than incremental) build instead. This means that, in a run that tests many mutants, the relative performance gain from skipping the baseline will be relatively small. However, it may still be useful to skip baseline tests in some specific situations. ## Timeouts Normally, cargo-mutants uses the baseline test to establish an appropriate `timeout` for the test suite. If you skip the baseline, you should set `--timeout` manually. ## Use cases for skipping baseline tests `--baseline=skip` might be useful in these situations: 1. You are running cargo-mutants in a CI or build system that separately runs the tests before cargo-mutants. In this case, you can be confident that the tests are passing, and you can save time by skipping the baseline. In particular, if you are [sharding](shards.md) work in CI, this avoids running the baseline on each shard. 2. You're repeatedly running `cargo-mutants` with different options, without changing the source code, perhaps with different `--file` or `--exclude` options. 3. You're developing `cargo-mutants` itself, and running it repeatedly on a tree that doesn't change. cargo-mutants-25.0.0/book/src/build-dirs.md000064400000000000000000000032731046102023000166020ustar 00000000000000# Copying the tree By default, cargo-mutants copies your tree to a temporary directory before mutating and building it. This behavior is turned of by the [`--in-place`](in-place.md) option, which builds mutated code in the original source directory. When the [`--jobs`](parallelism.md) option is used, one build directory is created per job. Some filters are applied while copying the tree, which can be configured by options. ## Troubleshooting tree copies If the baseline tests fail in the copied directory it is a good first debugging step to try building with `--in-place`. ## `.git` and other version control directories By default, files or directories matching these patterns are not copied, because they can be large and typically are not needed to build the source: .git .hg .jj .bzr .svn _darcs .pijul If your tree's build or tests require the VCS directory then it can be copied with `--copy-vcs=true` or by setting `copy_vcs = true` in `.cargo/mutants.toml`. ## `.gitignore` From 23.11.2, by default, cargo-mutants will not copy files that are excluded by gitignore patterns, to make copying faster in large trees. gitignore filtering is only used within trees containing a `.git` directory. The filter, based on the [`ignore` crate](https://docs.rs/ignore/), also respects global git ignore configuration in the home directory, as well as `.gitignore` files within the tree. This behavior can be turned off with `--gitignore=false`, causing ignored files to be copied. Rust projects typically configure gitignore to exclude the `target/` directory. ## `mutants.out` `mutants.out` and `mutants.out.old` are never copied, even if they're not covered by `.gitignore`. cargo-mutants-25.0.0/book/src/cargo-args.md000064400000000000000000000036701046102023000165720ustar 00000000000000# Passing options to Cargo cargo-mutants runs `cargo test` to build and run tests. (With `--check`, it runs `cargo check`.) Additional options can be passed in three different ways: to all `cargo` commands; to `cargo test` only; and to the test binaries run by `cargo test`. There is not yet a way to pass options only to `cargo build` but not to `cargo test`. ## Feature flags The `--features`, `--all-features`, and `--no-default-features` flags can be given to cargo-mutants and they will be passed down to cargo invocations. For example, this can be useful if you have tests that are only enabled with a feature flag: ```shell cargo mutants -- --features=fail/failpoints ``` ## Arguments to all `cargo` commands To pass more arguments to every Cargo invocation, use `--cargo-arg`, or the `additional_cargo_args` configuration key. `--cargo-arg` can be repeated. For example ```shell cargo mutants -- --cargo-arg=--release ``` or in `.cargo/mutants.toml`: ```toml additional_cargo_args = ["--all-features"] ``` ## Arguments to `cargo test` Command-line options following a `--` delimiter are passed through to `cargo test` (or to [nextest](nextest.md), if you're using that). For example, this can be used to pass `--all-targets` which (unobviously) excludes doctests. (If the doctests are numerous and slow, and not relied upon to catch bugs, this can improve performance.) ```shell cargo mutants -- --all-targets ``` These options can also be configured statically with the `additional_cargo_test_args` key in `.cargo/mutants.toml`: ```toml additional_cargo_test_args = ["--jobs=1"] ``` ## Arguments to test binaries You can use a second double-dash to pass options through to the test targets: ```sh cargo mutants -- -- --test-threads 1 --nocapture ``` (However, this may interact poorly with using `additional_cargo_test_args` in the configuration file, as the argument lists are currently appended without specially handling the `--` separator.) cargo-mutants-25.0.0/book/src/changelog.md000064400000000000000000000000331046102023000164620ustar 00000000000000{{#include ../../NEWS.md}} cargo-mutants-25.0.0/book/src/ci.md000064400000000000000000000025221046102023000151330ustar 00000000000000# Continuous integration You might want to use cargo-mutants in your continuous integration (CI) system, to ensure that no uncaught mutants are merged into your codebase. There are at least two complementary ways to use cargo-mutants in CI: 1. [Check for mutants produced in the code changed in a pull request](pr-diff.md). This is typically _much_ faster than testing all mutants, and is a good way to ensure that newly merged code is well tested, and to facilitate conversations about how to test the PR. 2. Checking that all mutants are caught, on PRs or on the development branch. ## Recommendations for CI * Use the [`--in-place`](in-place.md) option to avoid copying the tree. ## Installing into CI The recommended way to install cargo-mutants is using [install-action](https://github.com/taiki-e/install-action), which will fetch a binary from cargo-mutants most recent GitHub release, which is faster than building from source. You could alternatively use [baptiste0928/cargo-install](https://github.com/baptiste0928/cargo-install) which will build it from source in your worker and cache the result. ## Example workflow Here is an example of a GitHub Actions workflow that runs mutation tests and uploads the results as an artifact. This will fail if it finds any uncaught mutants. ```yml {{#include ../../examples/workflows/basic.yml}} ``` cargo-mutants-25.0.0/book/src/controlling.md000064400000000000000000000024611046102023000170740ustar 00000000000000# Controlling cargo-mutants `cargo mutants` takes various options to control how it runs. These options, can, in general, be passed on the command line, set in a `.cargo/mutants.toml` file in the source tree, or passed in `CARGO_MUTANTS_` environment variables. Not every method of setting an option is available for every option, however, as some would not make sense or be useful. For options that take a list of values, values from the configuration file are appended to values from the command line. For options that take a single value, the value from the command line takes precedence. `--no-config` can be used to disable reading the configuration file. ## Execution order By default, mutants are run in a randomized order, so as to surface results from different parts of the codebase earlier. This can be disabled with `--no-shuffle`, in which case mutants will run in order by file name and within each file in the order they appear in the source. ## Source directory location `-d`, `--dir`: Test the Rust tree in the given directory, rather than the source tree enclosing the working directory where cargo-mutants is launched. `--manifest-path`: Also selects the tree to test, but takes a path to a Cargo.toml file rather than a directory. (This is less convenient but compatible with other Cargo commands.) cargo-mutants-25.0.0/book/src/error-values.md000064400000000000000000000030061046102023000171640ustar 00000000000000# Generating error values cargo-mutants can be configured to generate mutants that return an error value from functions that return a Result. This will flag cases where no test fails if the function returns an error: that might happen if there are _only_ tests for the error cases and not for the Ok case. Since crates can choose to use any type for their error values, cargo-mutants must be told how to construct an appropriate error. The `--error` command line option and the `error_value` configuration option specify an error value to use. These options can be repeated or combined, which might be useful if there are multiple error types in the crate. On any one mutation site, probably only one of the error values will be viable, and cargo-mutants will discover that and use it. The error value can be any Rust expression that evaluates to a value of the error type. It should not include the `Err` wrapper, because cargo-mutants will add that. For example, if your crate uses `anyhow::Error` as its error type, you might use `--error '::anyhow::anyhow!("error")'`. If you have your own error type, you might use `--error 'crate::MyError::Generic'`. Since the correct error type is a property of the source tree, the configuration should typically go into `.cargo/mutants.toml` rather than being specified on the command line: ```toml error_values = ["::anyhow::anyhow!(\"mutated\")"] ``` To see only the mutants generated by this configuration, you can use a command like this: ```sh cargo r mutants -F anyhow -vV -j4 ``` cargo-mutants-25.0.0/book/src/exit-codes.md000064400000000000000000000011321046102023000166000ustar 00000000000000# Exit codes cargo-mutants returns an exit code that can be used by scripts or CI. * **0**: Success! Every viable mutant that was tested was caught by a test. * **1**: Usage error: bad command-line arguments etc. * **2**: Found some mutants that were not covered by tests. * **3**: Some tests timed out: possibly the mutations caused an infinite loop, or the timeout is too low. * **4**: The tests are already failing or hanging before any mutations are applied, so no mutations were tested. For more detailed machine-readable information, use the [`mutants.out` directory](mutants-out.md). cargo-mutants-25.0.0/book/src/filter_mutants.md000064400000000000000000000037131046102023000176030ustar 00000000000000# Filtering functions and mutants You can filter mutants by name, using the `--re` and `--exclude-re` command line options and the corresponding `examine_re` and `exclude_re` config file options. These options are useful if you want to run cargo-mutants just once, focusing on a subset of functions or mutants. These options filter mutants by the full name of the mutant, which includes the function name, file name, and a description of the change, as shown in the output of `cargo mutants --list`. For example, one mutant name might be: ```text src/outcome.rs:157: replace ::serialize -> Result with Ok(Default::default()) ``` Within this name, your regex can match any substring, including for example: - The filename - The trait, `impl Serialize` - The struct name, `ScenarioOutcome` - The function name, `serialize` - The mutated return value, `with Ok(Default::default())`, or any part of it. The regex matches a substring, but can be anchored with `^` and `$` to require that it match the whole name. The regex syntax is defined by the [`regex`](https://docs.rs/regex/latest/regex/) crate. These filters are applied after [filtering by filename](skip_files.md), and `--re` is applied before `--exclude-re`. Examples: - `-E 'impl Debug'` -- don't test `impl Debug` methods, because coverage of them might be considered unimportant. - `-F 'impl Serialize' -F 'impl Deserialize'` -- test implementations of these two traits. ## Configuring filters by name Mutants can be filtered by name in the `.cargo/mutants.toml` file. The `exclude_re` and `examine_re` keys are each a list of strings. This can be helpful if you want to systematically skip testing implementations of certain traits, or functions with certain names. From cargo-mutants 23.11.2 onwards, if the command line options are given then the corresponding config file option is ignored. For example: ```toml exclude_re = ["impl Debug"] # same as -E ``` cargo-mutants-25.0.0/book/src/getting-started.md000064400000000000000000000025251046102023000176500ustar 00000000000000# Getting started Just run `cargo mutants` in a Rust source directory, and it will point out functions that may be inadequately tested. ## Prerequisites For cargo-mutants to give useful results, your tree must already have reliable non-flaky tests that run under either `cargo test` or `cargo nextest`. If the tests are flaky, meaning that they can pass or fail depending on factors other than the source tree, then the cargo-mutants results will be meaningless. Cross-compilation is not currently supported, so the tree must be buildable for the host platform. Build tools other than Cargo are not supported today, but could in principle be added. ## Example ```none ; cargo mutants Found 14 mutants to test Copy source to scratch directory ... 0 MB in 0.0s Unmutated baseline ... ok in 1.6s build + 0.3s test Auto-set test timeout to 20.0s src/lib.rs:386: replace ::source -> Option<&(dyn std::error::Error + 'static)> with Default::default() ... NOT CAUGHT in 0.6s build + 0.3s test src/lib.rs:485: replace copy_symlink -> Result<()> with Ok(Default::default()) ... NOT CAUGHT in 0.5s build + 0.3s test 14 mutants tested in 0:08: 2 missed, 9 caught, 3 unviable ``` In v0.5.1 of the `cp_r` crate, the `copy_symlink` function was reached by a test but not adequately tested, and the `Error::source` function was not tested at all. cargo-mutants-25.0.0/book/src/goals.md000064400000000000000000000104721046102023000156500ustar 00000000000000# Goals **The goal of cargo-mutants is to be _easy_ to run on any Rust source tree, and to tell you something _interesting_ about areas where bugs might be lurking or the tests might be insufficient.** The detailed goals in this section are intended to generally guide development priorities and tradeoffs. For example, the goal of _ease_ means that we will generally prefer to automatically infer reasonable default behavior rather than requiring the user to configure anything at first. The goal of being _interesting_ means that we will generally only enable by default features that seem reasonably likely to say something important about test quality to at least some users. ## Ease Being _easy_ to use means: - cargo-mutants requires no changes to the source tree or other setup: just install and run. So, if it does not find anything interesting to say about a well-tested tree, it didn't cost you much. (This worked out really well: `cargo install cargo-mutants && cargo mutants` will do it.) - There is no chance that running cargo-mutants will change the released behavior of your program (other than by helping you to fix bugs!), because you don't need to change the source to use it. - cargo-mutants should be reasonably fast even on large Rust trees. The overall run time is, roughly, the product of the number of viable mutations multiplied by the time to run the test suite for each mutation. Typically, one `cargo mutants` run will give you all the information it can find about missing test coverage in the tree, and you don't need to run it again as you iterate on tests, so it's relatively OK if it takes a while. (There is currently very little overhead beyond the cost to do an incremental build and run the tests for each mutant, but that can still take a while for large trees that produce many mutants especially if their test suite takes a while.) - cargo-mutants should run correctly on any Rust source trees that are built and tested by Cargo, that will build and run their tests in a copy of the tree, and that have hermetic tests. - cargo-mutants shouldn't crash or hang, even if it generates mutants that cause the software under test to crash or hang. - The results should be reproducible, assuming the build and test suite is deterministic. - cargo-mutants should avoid generating unviable mutants that don't compile, because that wastes time. However, when it's uncertain whether the mutant will build, it's worth trying things that _might_ find interesting results even if they might fail to build. (It does currently generate _some_ unviable mutants, but typically not too many, and they don't have a large effect on runtime in most trees.) - Realistically, cargo-mutants may generate some mutants that aren't caught by tests but also aren't interesting, or aren't feasible to test. In those cases it should be easy to permanently dismiss them (e.g. by adding a `#[mutants::skip]` attribute or a config file.) ## Interestingness Showing _interesting results_ mean: - cargo-mutants should tell you about places where the code could be wrong and the test suite wouldn't catch it. If it doesn't find any interesting results on typical trees, there's no point. Aspirationally, it will even find useful results in code with high line coverage, when there is code that is reached by a test, but no test depends on its behavior. - In superbly-tested projects cargo-mutants may find nothing to say, but hey, at least it was easy to run, and hopefully the assurance that the tests really do seem to be good is useful data. - _Most_, ideally all, findings should indicate something that really should be tested more, or that may already be buggy, or that's at least worth looking at. - It should be easy to understand what the output is telling you about a potential bug that wouldn't be caught. (This seems true today.) It might take some thought to work out _why_ the existing tests don't cover it, or how to check it, but at least you know where to begin. - As much as possible cargo-mutants should avoid generating trivial mutants, where the mutated code is effectively equivalent to the original code, and so it's not interesting that the test suite doesn't catch the change. - For trees that are thoroughly tested, you can use `cargo mutants` in CI to check that they remain so. cargo-mutants-25.0.0/book/src/how-it-works.md000064400000000000000000000045361046102023000171210ustar 00000000000000# How cargo-mutants works The basic approach is: - Build a list of mutations: - Run `cargo metadata` to find directories containing Rust source files. - Walk all source files and parse each one looking for functions. - Skip functions that should not be mutated for any of several reasons: because they're tests, because they have a `#[mutants::skip]` attribute, etc. - For each function, depending on its return type, generate every mutation pattern that produces a result of that type. - Make a copy of the source tree into a scratch directory, excluding version-control directories like `.git` and the `/target` directory. The same directory is reused across all the mutations to benefit from incremental builds. - After copying the tree, cargo-mutants scans the top-level `Cargo.toml` and any `.cargo/config.toml` for relative dependencies. If there are any, the paths are rewritten to be absolute, so that they still work when cargo is run in the scratch directory. - Before applying any mutations, check that `cargo test` succeeds in the scratch directory: perhaps a test is already broken, or perhaps the tree doesn't build when copied because it relies on relative paths to find dependencies, etc. This is called the "baseline" test. - If running more than one parallel job, make the appropriate number of additional scratch directories. - For each mutation: - Apply the mutation to the scratch tree by patching the affected file. - Run `cargo test --no-run`: if this fails, the mutant is unviable, and that's ok. - Run `cargo test` in the tree, saving output to a log file. - If the the tests fail, that's good: the mutation was somehow caught. - If the tests succeed, that might mean test coverage was inadequate, or it might mean we accidentally generated a no-op mutation. - Revert the mutation to return the tree to its clean state. The file is parsed using the [`syn`](https://docs.rs/syn) crate, but mutations are applied textually, rather than to the token stream, so that unmutated code retains its prior formatting, comments, line numbers, etc. This makes it possible to show a text diff of the mutation and should make it easier to understand any error messages from the build of the mutated code. For more details, see [DESIGN.md](https://github.com/sourcefrog/cargo-mutants/blob/main/DESIGN.md). cargo-mutants-25.0.0/book/src/how-to-help.md000064400000000000000000000011731046102023000167040ustar 00000000000000# How to help Experience reports in [GitHub Discussions](https://github.com/sourcefrog/cargo-mutants/discussions) or issues are very welcome: - Did it find a bug or important coverage gap? - Did it fail to build and test your tree? (Some cases that aren't supported yet are already listed in this doc or the bug tracker.) It's especially helpful if you can either point to an open source tree that will reproduce the problem (or success) or at least describe how to reproduce it. If you are interested in contributing a patch, please read [CONTRIBUTING.md](https://github.com/sourcefrog/cargo-mutants/blob/main/CONTRIBUTING.md). cargo-mutants-25.0.0/book/src/in-diff.md000064400000000000000000000043301046102023000160530ustar 00000000000000# Testing code changed in a diff If you're working on a large project or one with a long test suite, you may not want to test the entire codebase every time you make a change. You can use `cargo-mutants --in-diff` to test only mutants generated from recently changed code. The `--in-diff DIFF_FILE` option tests only mutants that overlap with regions changed in the diff. The diff is expected to either have a prefix of `b/` on the new filename, which is the format produced by `git diff`, or no prefix. Some ways you could use `--in-diff`: 1. Before submitting code, check your uncommitted changes with `git diff`. 2. In CI, or locally, check the diff between the current branch and the base branch of the pull request. Changes to non-Rust files, or files from which no mutants are produced, are ignored. `--in-diff` is applied on the output of other filters including `--package` and `--regex`. For example, `cargo mutants --in-diff --package foo` will only test mutants in the `foo` package that overlap with the diff. ## Caution `--in-diff` makes tests faster by covering the mutants that are most likely to be missed in the changed code. However, it's certainly possible that edits in one region cause code in a different region or a different file to no longer be well tested. Incremental tests are helpful for giving faster feedback, but they're not a substitute for a full test run. The diff is only matched against the code under test, not the test code. So, a diff that only deletes or changes test code won't cause any mutants to run, even though it may have a very material effect on test coverage. ## Example In this diff, we've added a new function `two` to `src/lib.rs`, and the existing code is unaltered. With `--in-diff`, `cargo-mutants` will only test mutants that affect the function `two`. ```diff ```diff --- a/src/lib.rs 2023-11-12 13:05:25.774658230 -0800 +++ b/src/lib.rs 2023-11-12 12:54:04.373806696 -0800 @@ -2,6 +2,10 @@ "one".to_owned() } +pub fn two() -> String { + format!("{}", 2) +} + #[cfg(test)] mod test_super { use super::*; @@ -10,4 +14,9 @@ fn test_one() { assert_eq!(one(), "one"); } + + #[test] + fn test_two() { + assert_eq!(two(), "2"); + } } ``` cargo-mutants-25.0.0/book/src/in-place.md000064400000000000000000000033201046102023000162250ustar 00000000000000# Testing in place By default, cargo-mutants copies your code to a temporary directory, where it applies mutations and then runs tests there. With the `--in-place` option, it will instead mutate and test your code in the original source directory. `--in-place` is currently incompatible with the [`--jobs` option](parallelism.md), because running multiple jobs requires making multiple copies of the tree. ## Cautions If you use `--in-place` then you shouldn't edit the code, commit, or run your own tests while tests are running, because cargo-mutants will be modifying the code at the same time. It's not the default because of the risk that users might accidentally do this. cargo-mutants will try to restore the code to its original state after testing each mutant, but it's possible that it might fail to do so if it's interrupted or panics. ## Why test in place? Some situations where `--in-place` might be useful are: * You're running cargo-mutants in CI with a source checkout that exists solely for testing, so it would be a waste of time and space to copy it. * You've previously built the tree into `target` and want to avoid rebuilding it: the Rust toolchain currently doesn't reuse build products after cargo-mutants copies the tree, but it will reuse them with `--in-place`. * The source tree is extremely large, and making a copy would use too much disk space, or take time that you don't want to spend. (In most cases copying the tree takes negligible time compared to running the tests, but if it contains many binary assets it might be significant.) * You're investigating or debugging a problem where the tests don't pass in a copy of the tree. (Please report this as a bug if you can describe how to reproduce it.) cargo-mutants-25.0.0/book/src/installation.md000064400000000000000000000012461046102023000172430ustar 00000000000000# Installation Install cargo-mutants from source: ```sh cargo install --locked cargo-mutants ``` You can also use `cargo binstall` from [cargo-binstall](https://github.com/cargo-bins/cargo-binstall), or install binaries from GitHub releases. ## Supported Rust versions Building cargo-mutants requires a reasonably recent stable (or nightly or beta) Rust toolchain. The supported version is visible in . After installing cargo-mutants, you should be able to use it to run tests under any toolchain, even toolchains that are far too old to build cargo-mutants, using the standard `+` option to `cargo`: ```sh cargo +1.48 mutants ``` cargo-mutants-25.0.0/book/src/integrations.md000064400000000000000000000012311046102023000172420ustar 00000000000000# Integrations ## Shell completion The `--completions SHELL` emits completion scripts for the given shell. The right place to install these depends on your shell and operating system. For example, for Fish[^fishconf]: ```sh cargo mutants --completions fish >~/.config/fish/conf.d/cargo-mutants-completions.fish ``` [^fishconf]: This command installs them to `conf.d` instead of `completions` because you may have completions for several `cargo` plugins. ## vim-cargomutants [`vim-cargomutants`](https://github.com/yining/vim-cargomutants) provides commands view cargo-mutants results, see the diff of mutations, and to launch cargo-mutants from within vim. cargo-mutants-25.0.0/book/src/iterate.md000064400000000000000000000035041046102023000161760ustar 00000000000000# Iterating on missed mutants When you're working to improve test coverage in a tree, you might use a process like this: 1. Run `cargo-mutants` to find code that's untested, possibly filtering to some selected files. 2. Think about why some mutants are missed, and then write tests that will catch them. 3. Run cargo-mutants again to learn whether your tests caught all the mutants, or if any remain. 4. Repeat until everything is caught. You can speed up this process by using the `--iterate` option. This tells cargo-mutants to skip mutants that were either caught or unviable in a previous run, and to accumulate the results. You can run repeatedly with `--iterate`, adding tests each time, until all the missed mutants are caught (or skipped.) ## How it works When `--iterate` is given, cargo-mutants reads `mutants.out/caught.txt`, `previously_caught.txt`, and `unviable.txt` before renaming that directory to `mutants.out.old`. If those files don't exist, the lists are assumed to be empty. Mutants are then tested as usual, but excluding all the mutants named in those files. `--list --iterate` also applies this exclusion and shows you which mutants will be tested. Mutants are matched based on their file name, line, column, and description, just as shown in `--list` and in those files. As a result, if you insert or move text in a source file, some mutants may be re-tested. After testing, all the previously caught, caught, and unviable are written into `previously_caught.txt` so that they'll be excluded on future runs. `previously_caught.txt` is only written when `--iterate` is given. ## Caution `--iterate` is a heuristic, and makes the assumption that any new changes you make won't reduce coverage, which might not be true. After you think you've caught all the mutants, you should run again without `--iterate` to make sure. cargo-mutants-25.0.0/book/src/jobserver.md000064400000000000000000000015021046102023000165360ustar 00000000000000# Jobserver The GNU Jobserver protocol enables a build system to limit the total number of concurrent jobs at any point in time. By default, cargo-mutants starts a jobserver configured to allow one job per CPU. This limit applies across all the subprocesses spawned by cargo-mutants, including all parallel jobs. This allows you to use `--jobs` to run multiple test suites in parallel, without causing excessive load on the system from running too many compiler tasks in parallel. `--jobserver=false` disables running the jobserver. `--jobserver-tasks=N` sets the number of tasks that the jobserver will allow to run concurrently. The Rust test framework does not currently use the jobserver protocol, so it won't affect tests, only builds. However, the jobserver can be observed by tests and build scripts in `CARGO_MAKEFLAGS`. cargo-mutants-25.0.0/book/src/limitations.md000064400000000000000000000073011046102023000170740ustar 00000000000000# Limitations, caveats, known bugs, and future enhancements ## Cases where cargo-mutants _can't_ help cargo-mutants can only help if the test suite is hermetic: if the tests are flaky or non-deterministic, or depend on external state, it will draw the wrong conclusions about whether the tests caught a bug. If you rely on testing the program's behavior by manual testing, or by an integration test not run by `cargo test`, then cargo-mutants can't know this, and will only tell you about gaps in the in-tree tests. It may still be helpful to run mutation tests on only some selected modules that do have in-tree tests. Running cargo-mutants on your code won't, by itself, make your code better. It only helps suggest places you might want to improve your tests, and that might indirectly find bugs, or prevent future bugs. Sometimes the results will point out real current bugs. But it's on you to follow up. (However, it's really easy to run, so you might as well look!) cargo-mutants typically can't do much to help with crates that primarily generate code using macros or build scripts, because it can't "see" the code that's generated. (You can still run it, but it's may generate very few mutants.) ## Stability cargo-mutants behavior, output formats, command-line syntax, json output formats, etc, may change from one release to the next. ## Limitations and known bugs cargo-mutants currently only supports mutation testing of Rust code that builds using `cargo` and where the tests are run using `cargo test`. Support for other tools such as Bazel or Nextest could in principle be added. cargo-mutants sees the AST of the tree but doesn't fully "understand" the types, so sometimes generates unviable mutants or misses some opportunities to generate interesting mutants. cargo-mutants reads `CARGO_ENCODED_RUSTFLAGS` and `RUSTFLAGS` environment variables, and sets `CARGO_ENCODED_RUSTFLAGS`. It does not read `.cargo/config.toml` files, and so any rust flags set there will be ignored. cargo-mutants does not yet understand conditional compilation, such as `#[cfg(target_os = "linux")]`. It will report functions for other platforms as missed, when it should know to skip them. ### Support for other build tools cargo-mutants currently only works with Cargo, but could in principle be extended to work with other build tools such as Bazel. cargo-mutants contains two main categories of code, which are mostly independent: 1. Code for reading Rust source code, parsing it, and mutating it: this is not specific to Cargo. 2. Code for finding the modules to mutate and their source files, finding the tree to copy, adjusting paths after it is copied, and finally running builds and tests. This is very Cargo-specific, but should not be too hard to generalize. The main precondition for supporting Bazel is a realistic test case: preferably an open source Rust tree built with Bazel, or at least a contributor with a Bazel-based Rust tree who is willing to help test and debug and to produce some test cases. (See for more discussion.) ## Caution on side effects cargo-mutants builds and runs code with machine-generated modifications. This is generally fine, but if the code under test has side effects such as writing or deleting files, running it with mutations might conceivably have unexpected effects, such as deleting the wrong files, in the same way that a bug might. If you're concerned about this, run cargo-mutants in a container or virtual machine. cargo-mutants never modifies the original source tree, other than writing a `mutants.out` directory, and that can be sent elsewhere with the `--output` option. All mutations are applied and tested in a copy of the source tree. cargo-mutants-25.0.0/book/src/lints.md000064400000000000000000000016161046102023000156740ustar 00000000000000# Strict lints Because cargo-mutants builds versions of your tree with many heuristically injected errors, it may not work well in trees that are configured to treat warnings as errors. For example, mutants that delete code are likely to cause some parameters to be seen as unused, which will cause problems with trees that configure `#[deny(unused)]`. This will manifest as an excessive number of mutants being reported as "unviable". There are a few possible solutions: 1. Define a feature flag for mutation testing, and use `cfg_attr` to enable strict warnings only when not testing mutants. 2. Use the `cargo mutants --cap-lints=true` command line option, or the `cap_lints = true` config option. `--cap_lints=true` also disables rustc's detection of long-running const expression evaluation, so may cause some builds to fail. If that happens in your tree, you can set a [build timeout](timeouts.md). cargo-mutants-25.0.0/book/src/list.md000064400000000000000000000010241046102023000155070ustar 00000000000000# Listing generated mutants `--list`: Show what mutants could be generated, without running them. `--diff`: With `--list`, also include a diff of the source change for each mutant. `--json`: With `--list`, show the list in json for easier processing by other programs. (The same format is written to `mutants.out/mutants.json` when running tests.) `--check`: Run `cargo check` on all generated mutants to find out which ones are viable, but don't actually run the tests. (This is primarily useful when debugging cargo-mutants.) cargo-mutants-25.0.0/book/src/macros.md000064400000000000000000000006561046102023000160320ustar 00000000000000# Mutating code using macros cargo-mutants will mutate the contents of `#[proc_macro]` functions defined in the current crate, and run tests to see if those mutations are caught. cargo-mutants does not currently mutate calls to macros, or the expansion of a macro, or the definition of declarative `macro_rules` macros. As a result on code that is mostly produced by macro expansion it may not find many mutation opportunities. cargo-mutants-25.0.0/book/src/mutants-out.md000064400000000000000000000040501046102023000170360ustar 00000000000000# The `mutants.out` directory A `mutants.out` directory is created in the original source directory. You can put the output directory elsewhere with the `--output` option or using `CARGO_MUTANTS_OUTPUT` environment variable or via `output` directive in the config file. On each run, any existing `mutants.out` is renamed to `mutants.out.old`, and any existing `mutants.out.old` is deleted. The output directory contains: * A `lock.json`, on which an [fs2 lock](https://docs.rs/fs2) is held while cargo-mutants is running, to avoid two tasks trying to write to the same directory at the same time. `lock.json` contains the start time, cargo-mutants version, username, and hostname. `lock.json` is left in `mutants.out` when the run completes, but the lock on it is released. * A `mutants.json` file describing all the generated mutants. This file is completely written before testing begins. * An `outcomes.json` file describing the results of all tests, and summary counts of each outcome. * A `diff/` directory, containing a diff file for each mutation, relative to the unmutated baseline. `mutants.json` includes for each mutant the name of the diff file. * A `logs/` directory, with one log file for each mutation plus the baseline unmutated case. The log contains the diff of the mutation plus the output from cargo. `outcomes.json` includes for each mutant the name of the log file. * `caught.txt`, `missed.txt`, `timeout.txt`, `unviable.txt`, each listing mutants with the corresponding outcome. * `previously_caught.txt` accumulates a list of mutants caught in previous runs with [`--iterate`](iterate.md). The contents of the directory and the format of these files is subject to change in future versions. These files are incrementally updated while cargo-mutants runs, so other programs can read them to follow progress. There is generally no reason to include this directory in version control, so it is recommended that you add `/mutants.out*` to your `.gitignore` file or equivalent. This will exclude both `mutants.out` and `mutants.out.old`. cargo-mutants-25.0.0/book/src/mutants.md000064400000000000000000000102321046102023000162300ustar 00000000000000# Generating mutants cargo mutants generates mutants by inspecting the existing source code and applying a set of rules to generate new code that is likely to compile but have different behavior. Mutants each have a "genre", each of which is described below. ## Replace function body with value The `FnValue` genre of mutants replaces a function's body with a value that is guessed to be of the right type. This checks that the tests: 1. Observe any side effects of the original function. 2. Distinguish return values. More mutation genres and patterns will be added in future releases. | Return type | Mutation pattern | | ----------------- | ---------------- | | `()` | `()` (return unit, with no side effects) | | signed integers | `0, 1, -1` | | unsigned integers | `0, 1` | | floats | `0.0, 1.0, -1.0` | | `NonZeroI*` | `1, -1` | | `NonZeroU*` | `1` | | `bool` | `true`, `false` | | `String` | `String::new()`, `"xyzzy".into()` | | `&'_ str` . | `""`, `"xyzzy"` | | `&mut ...` | `Box::leak(Box::new(...))` | | `Result` | `Ok(...)` , [and an error if configured](error-values.md) | | `Option` | `Some(...)`, `None` | | `Box` | `Box::new(...)` | | `Vec` | `vec![]`, `vec![...]` | | `Arc` | `Arc::new(...)` | | `Rc` | `Rc::new(...)` | | `BinaryHeap`, `BTreeSet`, `HashSet`, `LinkedList`, `VecDeque` | empty and one-element collections | | `BTreeMap`, `HashMap` | empty map and the product of all key and value replacements | | `Cow<'_, T>` | `Cow::Borrowed(t)`, `Cow::Owned(t.to_owned())` | | `[T; L]` | `[r; L]` for all replacements of T | | `&[T]`, `&mut [T]`| Leaked empty and one-element vecs | | `&T` | `&...` (all replacements for T) | | `HttpResponse` | `HttpResponse::Ok().finish` | | `(A, B, ...)` | `(a, b, ...)` for the product of all replacements of A, B, ... | | `impl Iterator` | Empty and one-element iterators of the inner type | | (any other) | `Default::default()` | `...` in the mutation patterns indicates that the type is recursively mutated. For example, `Result` can generate `Ok(true)` and `Ok(false)`. The recursion can nest for types like `Result>`. Some of these values may not be valid for all types: for example, returning `Default::default()` will work for many types, but not all. In this case the mutant is said to be "unviable": by default these are counted but not printed, although they can be shown with `--unviable`. ## Binary operators Binary operators are replaced with other binary operators in expressions like `a == 0`. | Operator | Replacements | | -------- | ------------------ | | `==` | `!=` | | `!=` | `==` | | `&&` | `\|\|` | | `\|\|` | `&&`, | | `<` | `==`, `>` | | `>` | `==`, `<` | | `<=` | `>` | | `>=` | `<` | | `+` | `-`, `*` | | `-` | `+`, `/` | | `*` | `+`, `/` | | `/` | `%`, `*` | | `%` | `/`, `+` | | `<<` | `>>` | | `>>` | `<<` | | `&` | `\|`,`^` | | `\|` | `&`, `^` | | `^` | `&`, `\|` | | `+=` and similar assignments | assignment corresponding to the line above | Equality operators are not currently replaced with comparisons like `<` or `<=` because they are too prone to generate false positives, for example when unsigned integers are compared to 0. ## Unary operators Unary operators are deleted in expressions like `-a` and `!a`. They are not currently replaced with other unary operators because they are too prone to generate unviable cases (e.g. `!1.0`, `-false`). cargo-mutants-25.0.0/book/src/nextest.md000064400000000000000000000043121046102023000162310ustar 00000000000000# cargo-mutants with nextest [nextest](https://nexte.st) is a tool for running Rust tests, as a replacement for `cargo test`. You can use nextest to run your tests with cargo-mutants, instead of `cargo test`, by either passing the `--test-tool=nextest` option, or setting `test_tool = "nextest"` in `.cargo/mutants.toml`. ## How nextest works In the context of cargo-mutants the most important difference between cargo-test and nextest is that nextest runs each test in a separate process, and it can run tests from multiple test targets in parallel. (Nextest also has some nice UI improvements and other features, but they're not relevant here.) This means that nextest can stop faster if a single test fails, whereas cargo test will continue running all the tests within the test binary. This is beneficial for cargo-mutants, because it only needs to know whether at least one test caught the mutation, and so exiting as soon as there's a failure is better. However, running each test individually also means there is more per-test startup cost, and so on some trees nextest may be slower. In general, nextest will do relatively poorly on trees that have tests that are individually very fast, or on trees that establish shared or cached state across tests. ## When to use nextest There are at least two reasons why you might want to use nextest: 1. Some Rust source trees only support testing under nextest, and their tests fail under `cargo test`: in that case, you have to use this option! In particular, nextest's behavior of running each test in a separate process gives better isolation between tests. 2. Some trees might be faster under nextest than under `cargo test`, because they have a lot of tests that fail quickly, and the startup time is a small fraction of the time for the average test. This may or may not be true for your tree, so you can try it and see. Some trees, including cargo-mutants itself, are slower under nextest. ## nextest and doctests **Caution:** [nextest currently does not run doctests](https://github.com/nextest-rs/nextest/issues/16), so behaviors that are only caught by doctests will show as missed when using nextest. (cargo-mutants could separately run the doctests, but currently does not.) cargo-mutants-25.0.0/book/src/output.md000064400000000000000000000026321046102023000161020ustar 00000000000000# Display and output cargo-mutants writes a list of missed or timed-out mutants to stderr, and optionally mutants that were caught (with `--caught`) or failed to build (with `--unviable`) to stdout. It writes error or debug messages to stderr. The following options control what is printed to stdout and stderr. `-v`, `--caught`: Also print mutants that were caught by tests. `-V`, `--unviable`: Also print mutants that failed `cargo build`. `--no-times`: Don't print elapsed times. (This is intended mostly to make the output more stable for testing.) ## Colors `--colors=always|never|auto`: Control whether to use colors in output. The default is `auto`, which will write colors if the output is a terminal that supports colors. Color support is detected independently for stdout and stderr, so you should still see colors on stderr if stdout is redirected. The same values can be set with the `CARGO_TERM_COLOR` environment variable, which is respected by many Cargo commands. cargo-mutants also respects the [`NO_COLOR`](https://no-color.org/) and [`CLICOLOR_FORCE`](https://bixense.com/clicolors/) environment variables. If they are set to a value other than `0` then colors will be disabled or enabled regardless of any other settings. ## Debug trace `-L`, `--level`, and `$CARGO_MUTANTS_TRACE_LEVEL`: set the verbosity of trace output to stderr. The default is `info`, and it can be increased to `debug` or `trace`. cargo-mutants-25.0.0/book/src/parallelism.md000064400000000000000000000065011046102023000170460ustar 00000000000000# Parallelism After the initial test of the unmutated tree, cargo-mutants can run multiple builds and tests of the tree in parallel on a single machine. Separately, you can [shard](shards.md) work across multiple machines. **Caution:** `cargo build` and `cargo test` internally spawn many threads and processes and can be very resource hungry. Don't set `--jobs` too high, or your machine may thrash, run out of memory, or overheat. ## Background Even though cargo builds, rustc, and Rust's test framework launch multiple processes or threads, they typically spend some time waiting for straggler tasks, during which time some CPU cores are idle. For example, a cargo build commonly ends up waiting for a single-threaded linker for several seconds. Running one or more build or test tasks in parallel can use up this otherwise wasted capacity. This can give significant performance improvements, depending on the tree under test and the hardware resources available. ## Timeouts Because tests may be slower with high parallelism, or may exhibit more variability in execution time, you may see some spurious timeouts, and you may need to set `--timeout` manually to allow enough safety margin. (User feedback on this is welcome.) ## Non-hermetic tests If your test suite is non-hermetic -- for example, if it talks to an external database -- then running multiple jobs in parallel may cause test flakes. `cargo-mutants` is just running multiple copies of `cargo test` simultaneously: if that doesn't work in your tree, then you can't use this option. ## Choosing a job count You should set the number of jobs very conservatively, starting at `-j2` or `-j3`. Higher settings are only likely to be helpful on very large machines, perhaps with >100 cores and >256GB RAM. Unlike with `make`, setting `-j` proportionally to the number of cores is unlikely to work out well, because so the Rust build and test tools already parallelize very aggressively. The best setting will depend on many factors including the behavior of your program's test suite, the amount of memory on your system, and your system's behavior under high load. Ultimately you'll need to experiment to find the best setting. To tune the number of jobs, you can watch `htop` or some similar program while the tests are running, to see whether cores are fully utilized or whether the system is running out of memory. On laptop or desktop machines you might also want to watch the temperature of the CPU. As well as using more CPU and RAM, higher `-j` settings will also use more disk space in your temporary directory: Rust `target` directories can commonly be 2GB or more, and there will be one per parallel job, plus whatever temp files your test suite might create. ## Interaction with `--test-threads` The Rust test framework exposes a `--test-threads` option controlling how many threads run inside a test binary. cargo-mutants doesn't set this, but you can set it from the command line, along with other parameters to the test binary. You might need to set this if your test suite is non-hermetic with regard to global process state. Limiting the number of threads inside a single test binary would tend to make that binary less resource-hungry, and so _might_ allow you to set a higher `-j` option. Reducing the number of test threads to increase `-j` seems unlikely to help performance in most trees. cargo-mutants-25.0.0/book/src/performance.md000064400000000000000000000061161046102023000170440ustar 00000000000000# Improving performance Most of the runtime for cargo-mutants is spent in running the program test suite and in running incremental builds: both are done once per viable mutant. So, anything you can do to make the `cargo build` and `cargo test` suite faster will have a multiplicative effect on `cargo mutants` run time, and of course will also make normal development more pleasant. has good general advice on making Rust builds and tests faster. ## Avoid doctests Rust doctests are pretty slow, because every doctest example becomes a separate test binary. If you're using doctests only as testable documentation and not to assert correctness of the code, you can skip them with `cargo mutants -- --all-targets`. ## Choosing a cargo profile [Cargo profiles](https://doc.rust-lang.org/cargo/reference/profiles.html) provide a way to configure compiler settings including several that influence build and runtime performance. By default, cargo-mutants will use the default profile selected for `cargo test`, which is also called `test`. This includes debug symbols but disables optimization. You can select a different profile using the `--profile` option or the `profile` configuration key. You may wish to define a `mutants` profile in `Cargo.toml`, such as: ```toml [profile.mutants] inherits = "test" debug = "none" ``` and then configure this as the default in `.cargo/mutants.toml`: ```toml profile = "mutants" ``` Turning off debug symbols will make the builds faster, at the expense of possibly giving less useful output when a test fails. In general, since mutants are expected to cause tests to fail, debug symbols may not be worth cost. If your project's tests take a long time to run then it may be worth experimenting with increasing the `opt` level or other optimization parameters in the profile, to trade off longer builds for faster test runs. cargo-mutants now shows the breakdown of build versus test time which may help you work out if this will help: if the tests are much slower than the build it's worth trying more more compiler optimizations. ## Ramdisks cargo-mutants causes the Rust toolchain (and, often, the program under test) to read and write _many_ temporary files. Setting the temporary directory onto a ramdisk can improve performance significantly. This is particularly important with parallel builds, which might otherwise hit disk bandwidth limits. For example on Linux: ```shell sudo mkdir /ram sudo mount -t tmpfs /ram /ram # or put this in fstab, or just change /tmp sudo chmod 1777 /ram env TMPDIR=/ram cargo mutants ``` ## Using the Mold linker Using the [Mold linker](https://github.com/rui314/mold) on Unix can give a 20% performance improvement, depending on the tree. Because cargo-mutants does many incremental builds, link time is important, especially if the test suite is relatively fast. Because of limitations in the way cargo-mutants runs Cargo, the standard way of configuring Mold for Rust in `~/.cargo/config.toml` won't work. Instead, set the `RUSTFLAGS` environment variable to `-Clink-arg=-fuse-ld=mold`. cargo-mutants-25.0.0/book/src/pr-diff.md000064400000000000000000000006411046102023000160670ustar 00000000000000# Incremental tests of pull requests You can use `--in-diff` to test only the code that has changed in a pull request. This can be useful for incremental testing in CI, where you want to test only the code that has changed since the last commit. For example, you can use the following workflow to test only the code that has changed in a pull request: ```yaml {{#include ../../examples/workflows/in-diff.yml}} ``` cargo-mutants-25.0.0/book/src/shards.md000064400000000000000000000103421046102023000160230ustar 00000000000000# Sharding In addition to [running multiple jobs locally](parallelism.md), cargo-mutants can also run jobs on multiple machines, to get an overall result faster by using more CPU cores. Each job tests a subset of mutants, selected by a shard. Shards are described as `k/n`, where `n` is the number of shards and `k` is the index of the shard, from 0 to `n-1`. There is no runtime coordination between shards: they each independently discover the available mutants and then select a subset based on the `--shard` option. If any shard fails then that would indicate that some mutants were missed, or there was some other problem. ## Consistency across shards **CAUTION:** All shards must be run with the same arguments, and the same sharding denominator `n`, or the results will be meaningless, as they won't agree on how to divide the work. Sharding can be combined with filters or shuffling, as long as the filters are set consistently in all shards. Sharding can also combine with `--in-diff`, again as long as all shards see the same diff. ## Setting up sharding Your CI system or other tooling is responsible for launching multiple shards, and for collecting the results. You're responsible for choosing the number of shards (see below). For example, in GitHub Actions, you could use a matrix job to run multiple shards: ```yaml {{#include ../../examples/workflows/sharded.yml}} ``` Note that the number of shards is set to match the `/8` in the `--shard` argument. ## Skipping the baseline [Sharding works with `--baseline=skip`](baseline.md), to avoid the cost of running the baseline on every shard. But, if you do this, then you must ensure that the tests suite is passing in the baseline, for example by checking it in a previous CI step. ## Performance of sharding Each mutant does some constant upfront work: * Any CI setup including starting the machine, getting a checkout, installing a Rust toolchain, and installing cargo-mutants * An initial clean build of the code under test * A baseline run of the unmutated code (unless this is skipped) Then, for each mutant in its shard, it does an incremental build and runs all the tests. Each shard runs the same number of mutants, +/-1. Typically this will mean they each take roughly the same amount of time, although it's possible that some shards are unlucky in drawing mutants that happen to take longer to test. A rough model for the overall execution time for all of the shards, allowing for this work occurring in parallel, is ```raw SHARD_STARTUP + (CLEAN_BUILD + TEST) + (N_TOTAL_MUTANTS / N_SHARDS) * (INCREMENTAL_BUILD + TEST) ``` The total cost in CPU seconds can be modelled as: ```raw N_SHARDS * (SHARD_STARTUP + CLEAN_BUILD + TEST) + N_MUTANTS * (INCREMENTAL_BUILD + TEST) ``` As a result, if you use many shards the cost of the initial build will dominate, and the overall time will converge towards the time for a clean build, a baseline test, and the test of one mutant. ## Choosing a number of shards Because there's some constant overhead for every shard there will be diminishing returns and increasing ineffiency if you use too many shards. (In the extreme cases where there are more shards than mutants, some of them will find they have nothing to do and immediately exit.) As a rule of thumb, you should probably choose `n` such that each worker runs at least 10 mutants, and possibly much more. 8 to 32 shards might be a good place to start. The optimal setting probably depends on how long your tree takes to build from zero and incrementally, how long the tests take to run, and the performance of your CI system. If your CI system offers a choice of VM sizes you might experiment with using smaller or larger VMs and more or less shards: the optimal setting probably also depends on your tree's ability to exploit larger machines. You should also think about cost and capacity constraints in your CI system, and the risk of starving out other users. cargo-mutants has no internal scaling constraints to prevent you from setting `n` very large, if cost, efficiency and CI capacity are not a concern. ## Sampling mutants An option like `--shard 1/100` can be used to run 1% of all the generated mutants for testing cargo-mutants, to get a sense of whether it works or to see how it performs on some tree. cargo-mutants-25.0.0/book/src/skip.md000064400000000000000000000025401046102023000155060ustar 00000000000000# Skipping untestable code Some functions may be inherently hard to cover with tests, for example if: * Generated mutants cause tests to hang. * You've chosen to test the functionality by human inspection or some higher-level integration tests. * The function has side effects or performance characteristics that are hard to test. * You've decided the function is not important to test. There are three ways to skip mutating some code: 1. [Marking the function with an attribute](attrs.md) within the source file. 2. [Filtering by path](skip_files.md) in the config file or command line. 3. [Filtering by function and mutant name](filter_mutants.md) in the config file or command line. The results of all these filters can be previewed using the `--list` option. ## Which filtering method to use? * If some particular functions are hard to test with cargo-mutants, use an attribute, so that the skip is visible in the code. * If a whole module is untestable, use a filter by path in the config file, so that the filter's stored in the source tree and covers any new code in that module. * If you want to permanently ignore a class of functions, such as `Debug` implementations, use a regex filter in the config file. * If you want to run cargo-mutants just once, focusing on a subset of files, functions, or mutants, use command line options to filter by name or path. cargo-mutants-25.0.0/book/src/skip_calls.md000064400000000000000000000033351046102023000166670ustar 00000000000000# Skipping function calls Using the `--skip-calls` argument and config key you can tell cargo-mutants not to mutate the arguments to calls to specific named functions and methods. For example: ```sh cargo mutants --skip-calls=skip_this,and_this ``` or in `.cargo/mutants.toml` ```toml skip_calls = ["skip_this", "and_this"] ``` The command line arguments are added to the values specified in the configuration. The names given in the option and argument are matched against the final component of the path in each call, disregarding any type parameters. For example, the default value of `with_capacity` will match `std::vec::Vec::::with_capacity(10)`. This is separate from [skipping mutation of the body of a function](attrs.md), and only affects the generation of mutants within the call expression, typically in its arguments. By default, calls to functions called `with_capacity` are not mutated. The defaults can be turned off using `--skip-calls-defaults=false`. ## `with_capacity` The motivating example for this feature is Rust's `with_capacity` function on `Vec` and other collections, which preallocates capacity for a slight performance gain. ```rust let mut v = Vec::with_capacity(4 * n); ``` cargo-mutants normally mutates expressions in function calls, and in this case it will try mutating the capacity expression to `4 / n` etc. These mutations would change the program behavior. Assuming the original calculation is correct the mutation then the mutation will likely be wrong. However, many authors may feel that preallocating the estimated memory needs is worth doing but not worth specifically writing tests or assertions for, and so they would like to skip generating mutants in any calls to these functions. cargo-mutants-25.0.0/book/src/skip_files.md000064400000000000000000000063571046102023000167020ustar 00000000000000# Filtering files Two options (each with short and long names) control which files are mutated: - `-f GLOB`, `--file GLOB`: Mutate only functions in files matching the glob. - `-e GLOB`, `--exclude GLOB`: Exclude files that match the glob. These options may be repeated. If any `-f` options are given, only source files that match are considered; otherwise all files are considered. This list is then further reduced by exclusions. Globs are treated differently depending on whether they contain a path separator or not. `/` matches the path separator on both Unix and Windows. `\\` matches the path separator on Windows and is an escape character on Unix. If the glob contains a path separator then it matches against the path from the root of the source tree. For example, `src/*/*.rs` will match (and exclude or exclude) all files in subdirectories of `src`. Matches on paths can use `**` to match zero or more directory components: `src/**/*.rs` will match all `.rs` files in `src` and its subdirectories. If the glob does not contain a path separator, it matches against file and directory names, in any directory. For example, `t*.rs` will match all files whose name start with `t` and ends with `.rs`, in any directory. in any directory. `--exclude console` excludes all files within directories called "console", but not files called "console.rs". Note that the glob must contain `.rs` (or a matching wildcard) to match source files with that suffix. For example, `-f network` will match `src/network/mod.rs` but it will _not_ match `src/network.rs`. Files that are excluded are still parsed (and so must be syntactically valid), and `mod` statements in them are followed to discover other source files. So, for example, you can exclude `src/main.rs` but still test mutants in other files referenced by `mod` statements in `main.rs`. Since Rust does not currently allow attributes such as `#[mutants::skip]` on `mod` statements or at module scope filtering by filename is the only way to skip an entire module. The results of filters can be previewed with the `--list-files` and `--list` options. Examples: - `cargo mutants -f visit.rs -f change.rs` -- test mutants only in files called `visit.rs` or `change.rs` (in any directory). - `cargo mutants -e console.rs` -- test mutants in any file except `console.rs`. - `cargo mutants -f src/db/*.rs` -- test mutants in any file in this directory. This could also be written as `-f src/db`, or (if all the source is in `src`) as `-f db`. ## Configuring filters by filename Files may also be filtered with the `exclude_globs` and `examine_globs` options in `.cargo/mutants.toml`. Exclusions in the config file may be particularly useful when there are modules that are inherently hard to automatically test, and the project has made a decision to accept lower test coverage for them. From cargo-mutants 23.11.2 onwards, if the command line options are given then the corresponding config file option is ignored. This allows you to use the config file to test files that are normally expected to pass, and then to use the command line to test files that are not yet passing. For example: ```toml exclude_globs = ["src/main.rs", "src/cache/*.rs"] # like -e examine_globs = ["src/important/*.rs"] # like -f: test *only* these files ``` cargo-mutants-25.0.0/book/src/stability.md000064400000000000000000000032001046102023000165360ustar 00000000000000# Stability ## Reproducibility within a single version The results of running `cargo mutants` should be deterministic and reproducible assuming that the build and test process for the code under test is also deterministic. Any nondeterminism is a bug. By default, the order in which mutants are tested is randomized. If the tests are hermetic, this should make no difference other than the order in which the output is presented. This can be disabled with `--no-shuffle`. If multiple parallel jobs are run, the results should be the same as running the same number of serial jobs, except for the order of the output. ## Reproducibility across versions cargo-mutants behavior may change between versions, although we will attempt to minimize disruption and to document any changes in the [changelog](changelog.md). In particular the following changes can be expected: - Addition of new mutation patterns, so that later versions generate new mutants. - Removal or changes of existing mutation patterns if they turn out to generate too many unviable mutants or too few interesting mutants. - Changes to the built-in heuristics controlling what code is skipped or mutated. For example, an earlier version failed to skip functions marked with `#![cfg(test)]` and this was fixed in a later version. - Addition of new information to the JSON output files. Removal of existing files or fields will be avoided where possible. - Changes to the presentation of mutant names in the console and in JSON. - Changes to console output and progress. As a result of all these, a tree that passes all mutants in one version may fail some in a later version, and vice versa. cargo-mutants-25.0.0/book/src/timeouts.md000064400000000000000000000051551046102023000164160ustar 00000000000000# Hangs and timeouts Some mutations to the tree can cause the test suite to hang. For example, in this code, cargo-mutants might try changing `should_stop` to always return `false`, but this will cause the program to hang: ```rust while !should_stop() { // something } ``` In general you will want to skip functions which cause a hang when mutated, either by [marking them with an attribute](skip.md) or in the [configuration file](filter_mutants.md). ## Timeouts To avoid hangs, cargo-mutants will kill the build or test after a timeout and continue to the next mutant. By default, the timeouts are set automatically, relative to the times taken to build and test the unmodified tree (baseline). The default test timeout is 5 times the baseline test time, with a minimum of 20 seconds. The minimum of 20 seconds for the test timeout can be overridden by the `--minimum-test-timeout` option or the `CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT` environment variable, measured in seconds. You can set an explicit timeouts with the `--timeout` option, also measured in seconds. You can also set the test timeout as a multiple of the duration of the baseline test, with the `--timeout-multiplier` option and the `timeout_multiplier` configuration key. The multiplier only has an effect if the baseline is not skipped and if `--timeout` is not specified. ## Build timeouts `const` expressions may be evaluated at compile time. In the same way that mutations can cause tests to hang, mutations to const code may potentially cause the compiler to enter an infinite loop. rustc imposes a time limit on evaluation of const expressions. This is controlled by the `long_running_const_eval` lint, which by default will interrupt compilation: as a result the mutants will be seen as unviable. If this lint is configured off in your program, or if you use the `--cap-lints=true` option to turn off all lints, then the compiler may hang when constant expressions are mutated. In this case you can use the `--build-timeout` or `--build-timeout-multiplier` options, or their corresponding configuration keys, to impose a limit on overall build time. However, because build time can be quite variable there's some risk of this causing builds to be flaky, and so it's off by default. You might also choose to skip mutants that can cause long-running const evaluation. ## Exceptions The multiplier timeout options cannot be used when the baseline is skipped (`--baseline=skip`), or when the build is in-place (`--in-place`). If no explicit timeouts is provided in these cases, then there is no build timeout and the test timeout default of 300 seconds will be used. cargo-mutants-25.0.0/book/src/using-results.md000064400000000000000000000124721046102023000173710ustar 00000000000000# Using the results ## Tests fail in an clean tree? If tests fail in a clean copy of the tree, there might be an (intermittent) failure in the source directory, or there might be some problem that stops them passing when run from a different location. Fix this first: cargo-mutants can't do anything until you have a tree where `cargo test` passes reliably when copied to a temporary directory. ## Mutant outcomes Assuming tests pass in a clean copy of the tree, cargo-mutants proceeds to generate every mutant it can, subject to any configured filters, and then runs `cargo build` and `cargo test` on each of them. Each mutant results in one of the following outcomes: * **caught** — A test failed with this mutant applied. This is a good sign about test coverage. * **missed** — No test failed with this mutation applied, which seems to indicate a gap in test coverage. Or, it may be that the mutant is undistinguishable from the correct code. You may wish to add a better test, or mark that the function should be skipped. * **unviable** — The attempted mutation doesn't compile. This is inconclusive about test coverage and no action is needed, but indicates an opportunity for cargo-mutants to either generate better mutants, or at least not generate unviable mutants. * **timeout** — The mutation caused the test suite to run for a long time, until it was eventually killed. You might want to investigate the cause and potentially mark the function to be skipped. By default only missed mutants and timeouts are printed to stdout, because they're the most actionable. Others can be shown with the `--caught` and `--unviable` options. ## What to do about missed mutants? Each missed mutant is a sign that there _might_ be a gap in test coverage. What to do about them is up to you, bearing in mind your goals and priorities for your project, but here are some suggestions: First, look at the overall list of missed mutants: there might be patterns such as a cluster of related functions all having missed mutants. Probably some will stand out as potentially more important to the correct function of your program. You should first look for any mutations where it's very _surprising_ that they were not caught by any tests, given what you know about the codebase. For example, if cargo-mutants reports that replacing an important function with `Ok(())` is not caught then that seems important to investigate. You should then look at the tests that you would think _would_ catch the mutant: that might be unit tests within the relevant module, or some higher-level public-API or integration test, depending on how your project's tests are structured. If you can't find any tests that you think should have caught the mutant, then perhaps you should add some. The right thing here is _not_ necessarily to directly assert that the mutated behavior doesn't happen. For example, if the mutant changed a private function, you don't necessarily want to add a test for that private function, but instead ask yourself what public-API behavior would break if the private function was buggy, and then add a test for that. Try to avoid writing tests that are too tightly targeted to the mutant, which is really just an _example_ of something that could be wrong, and instead write tests that assert the _correct_ behavior at the right level of abstraction, preferably through a public interface. If it's not clear why the tests aren't already failing, it may help to manually inject the same mutation into your working tree and then run the tests under a debugger, or add trace statements to the test. (The `--diff` option or looking in the `mutants.out` directory will show you exactly what change cargo-mutants made.) You may notice some messages about missed mutants in functions that you feel are not very important to test, such as `Debug` implementations. You can use the `--exclude-re` options to filter out these mutants, or mark them as skipped with `#[mutants::skip]`. (Or, you might decide that you do want to add unit tests for the `Debug` representation, but perhaps as a lower priority than investigating mutants in more important code.) In some cases cargo-mutants will generate a mutant that is effectively the same as the original code, and so not really incorrect. cargo-mutants tries to avoid doing this, but if it does happen then you can mark the function as skipped. ## Iterating on mutant coverage After you've changed your program to address some of the missed mutants, you can run `cargo mutants` again with the [`--file` option](skip_files.md) to re-test only functions from the changed files. ## Hard-to-test cases Some functions don't cause a test suite failure if emptied, but also cannot be removed. For example, functions to do with managing caches or that have other performance side effects. Ideally, these should be tested, but doing so in a way that's not flaky can be difficult. cargo-mutants can help in a few ways: * It helps to at least highlight to the developer that the function is not covered by tests, and so should perhaps be treated with extra care, or tested manually. * A [`#[mutants::skip]` annotation](skip.md) can be added to suppress warnings and explain the decision. * Sometimes these effects can be tested by making the side-effect observable with, for example, a counter of the number of memory allocations or cache misses/hits. cargo-mutants-25.0.0/book/src/vs-coverage.md000064400000000000000000000030471046102023000167640ustar 00000000000000# How is mutation testing different to coverage measurement? Coverage measurements tell you which lines of code (or other units) are reached while running a test. They don't tell you whether the test really _checks_ anything about the behavior of the code. For example, a function that writes a file and returns a `Result` might be covered by a test that checks the return value, but not by a test that checks that the file was actually written. cargo-mutants will try mutating the function to simply return `Ok(())` and report that this was not caught by any tests. Historically, rust coverage measurements have required manual setup of several OS and toolchain-dependent tools, although this is improving. Because `cargo-mutants` just runs `cargo` it has no OS-specific or tight toolchain integrations, and so is simple to install and run on any Rust source tree. cargo-mutants also needs no special tools to view or interpret the results. Coverage tools also in some cases produce output that is hard to interpret, with lines sometimes shown as covered or not due to toolchain quirks that aren't easy to map to direct changes to the test suite. cargo-mutants produces a direct list of changes that are not caught by the test suite, which can be quickly reviewed and prioritized. One drawback of mutation testing is that it runs the whole test suite once per generated mutant, so it can be slow on large trees with slow test suites. There are [some techniques to speed up cargo-mutants](performance.md), including [running multiple tests in parallel](parallelism.md). cargo-mutants-25.0.0/book/src/vs-fuzzing.md000064400000000000000000000012531046102023000166620ustar 00000000000000# How is mutation testing different to fuzzing? Fuzzing is a technique for finding bugs by feeding pseudo-random inputs to a program, and is particularly useful on programs that parse complex or untrusted inputs such as binary file formats or network protocols. Mutation testing makes algorithmically-generated changes to a copy of the program source, and measures whether the test suite catches the change. The two techniques are complementary. Although some bugs might be found by either technique, fuzzing will tend to find bugs that are triggered by complex or unusual inputs, whereas mutation testing will tend to point out logic that might be correct but that's not tested. cargo-mutants-25.0.0/book/src/welcome.md000064400000000000000000000012231046102023000161700ustar 00000000000000# Welcome to cargo-mutants cargo-mutants is a mutation testing tool for Rust. It helps you improve your program's quality by finding places where bugs can be inserted without causing any tests to fail. **The goal of cargo-mutants is to be _easy_ to run on any Rust source tree, and to tell you something _interesting_ about areas where bugs might be lurking or the tests might be insufficient.** ([More about these goals](goals.md).) To get started: 1. [Install cargo-mutants](installation.md). 2. [Run `cargo mutants`](getting-started.md) in your Rust source tree. For more resources see the repository at . cargo-mutants-25.0.0/book/src/workspaces.md000064400000000000000000000060141046102023000167210ustar 00000000000000# Workspace and package support cargo-mutants supports testing Cargo workspaces that contain multiple packages. The entire workspace tree is copied to the temporary directory (unless `--in-place` is used). In workspaces with multiple packages, there are two considerations: 1. Which packages to generate mutants in, and 2. Which tests to run on those mutants. ## Selecting packages to mutate By default, cargo-mutants selects packages to mutate using [similar heuristics to other Cargo commands](https://doc.rust-lang.org/cargo/reference/workspaces.html). These rules work from the "starting directory", which is the directory selected by `--dir` or the current working directory. * If `--workspace` is given, all packages in the workspace are mutated. * If `--package` is given, the named packages are mutated. * If the starting directory is in a package, that package is mutated. Concretely, this means: if the starting directory or its parents contain a `Cargo.toml` containing a `[package]` section. * If the starting directory's parents contain a `Cargo.toml` with a `[workspace]` section but no `[package]` section, then the directory is said to be in a "virtual workspace". If the `[workspace]` section has a `default-members` key then these packages are mutated. Otherwise, all packages are mutated. Selection of packages can be combined with [`--file`](skip_files.md) and other filters. You can also use the `--file` options to restrict cargo-mutants to testing only files from some subdirectory, e.g. with `-f "utils/**/*.rs"`. (Remember to quote globs on the command line, so that the shell doesn't expand them.) You can use `--list` or `--list-files` to preview the effect of filters. ## Selecting tests to run For each baseline and mutant scenario, cargo-mutants selects some tests to see if the mutant is caught. These selections turn into `--package` or `--workspace` arguments to `cargo test`. There are different behaviors for the baseline tests (before mutation), which run once for all packages, and then for the tests applied to each mutant. These behaviors can be controlled by the `--test-workspace` and `--test-package` command line options and the corresponding configuration options. By default, the baseline runs the tests from all and only the packages for which mutants will be generated. That is, if the whole workspace is being tested, then it runs `cargo test --workspace`, and otherwise runs tests for each selected package. By default, each mutant runs only the tests from the package that's being mutated. If the `--test-workspace=true` argument or `test_workspace` configuration key is set, then all tests from the workspace are run for the baseline and against each mutant. If the `--test-package` argument or `test_package` configuration key is set then the specified packages are tested for the baseline and all mutants. As for other options, the command line arguments have priority over the configuration file. Like `--package`, the argument to `--test-package` can be a comma-separated list, or the option can be repeated. cargo-mutants-25.0.0/examples/workflows/basic.yml000064400000000000000000000017761046102023000201660ustar 00000000000000# Example of how to configure a GitHub Actions workflow to run `cargo mutants` # on every push to main and every pull request that changes the code. # You could run this standalone or merge it into a workflow that runs other tests. name: cargo-mutants env: CARGO_TERM_COLOR: always on: push: branches: - main pull_request: # Only test PR if it changes something that's likely to affect the results, because # mutant tests can take a long time. Adjust these paths to suit your project. paths: - ".cargo/mutants.toml" - ".github/workflows/tests.yml" - "Cargo.*" - "src/**" - "testdata/**" - "tests/**" jobs: cargo-mutants: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@v2 with: tool: cargo-mutants - run: cargo mutants -vV --in-place - uses: actions/upload-artifact@v4 if: always() with: name: mutants-out path: mutants.out cargo-mutants-25.0.0/examples/workflows/in-diff.yml000064400000000000000000000022721046102023000204110ustar 00000000000000# An example of how to run cargo-mutants on only the sections of code that have changed in a pull request, # using the `--in-diff` feature of cargo-mutants. # # This can give much faster feedback on pull requests, but can miss some problems that # would be found by running mutants on the whole codebase. name: Tests permissions: contents: read env: CARGO_TERM_COLOR: always on: push: branches: - main pull_request: jobs: incremental-mutants: runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Relative diff run: | git branch -av git diff origin/${{ github.base_ref }}.. | tee git.diff - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 name: Install cargo-mutants using install-action with: tool: cargo-mutants - name: Mutants run: | cargo mutants --no-shuffle -vV --in-diff git.diff - name: Archive mutants.out uses: actions/upload-artifact@v4 if: always() with: name: mutants-incremental.out path: mutants.out cargo-mutants-25.0.0/examples/workflows/sharded.yml000064400000000000000000000052121046102023000205040ustar 00000000000000# Example of using a GitHub Actions matrix to shard the mutants run into 8 parts. # See https://github.com/sourcefrog/cargo-mutants/blob/main/.github/workflows/tests.yml for a full example. # Only run this on PRs or main branch commits that could affect the results, # so we don't waste time on doc-only changes. Adjust these paths and branch names # to suit your project. on: pull_request: paths: - ".cargo/*.toml" - ".github/workflows/tests.yml" - "Cargo.*" - "mutants_attrs/**" - "src/**" - "testdata/**" - "tests/**" push: branches: - main # Actions doesn't support YAML references, so it's repeated here paths: - ".cargo/*.toml" - ".github/workflows/tests.yml" - "Cargo.*" - "mutants_attrs/**" - "src/**" - "testdata/**" - "tests/**" jobs: # Before testing mutants, run the build and tests on all platforms. # You probably already have CI configuration like this, so don't duplicate it, # merge cargo-mutants into your existing workflow. test: strategy: matrix: os: [macOS-latest, ubuntu-latest, windows-latest] version: [stable, nightly] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.version }} components: rustfmt - uses: swatinem/rust-cache@v2 - name: rustfmt run: cargo fmt --all -- --check - name: Build run: cargo build --all-targets - name: Test run: cargo test --workspace cargo-mutants: runs-on: ubuntu-latest # Often you'll want to only run this after the build is known to pass its basic tests, # to avoid wasting time, and to allow using --baseline=skip. needs: [test] strategy: fail-fast: false # Collect all mutants even if some are missed matrix: shard: [0, 1, 2, 3, 4, 5, 6, 7] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@v2 name: Install cargo-mutants using install-action with: tool: cargo-mutants # Set an appropriate timeout for your tree here. # The denominator of the shard count must be the number of shards. - name: Mutants run: | cargo mutants --no-shuffle -vV --shard ${{ matrix.shard }}/8 --baseline=skip --timeout 300 --in-place - name: Archive mutants.out uses: actions/upload-artifact@v4 if: always() with: path: mutants.out name: mutants-shard${{matrix.shard}}.out cargo-mutants-25.0.0/src/build_dir.rs000064400000000000000000000134171046102023000155740ustar 00000000000000// Copyright 2021-2024 Martin Pool //! A directory containing mutated source to run cargo builds and tests. #![warn(clippy::pedantic)] use std::fs::write; use anyhow::{ensure, Context}; use camino::{Utf8Path, Utf8PathBuf}; use tempfile::TempDir; use tracing::info; use crate::{ console::Console, copy_tree::copy_tree, manifest::{fix_cargo_config, fix_manifest}, options::Options, workspace::Workspace, Result, }; /// A directory containing source, that can be mutated, built, and tested. /// /// Depending on how its constructed, this might be a copy in a tempdir /// or the original source directory. #[derive(Debug)] pub struct BuildDir { /// The path of the root of the build directory. path: Utf8PathBuf, /// Holds a reference to the temporary directory, so that it will be deleted when this /// object is dropped. If None, there's nothing to clean up. #[allow(dead_code)] temp_dir: Option, } impl BuildDir { /// Make the build dir for the baseline. /// /// Depending on the options, this might be either a copy of the source directory /// or in-place. pub fn for_baseline( workspace: &Workspace, options: &Options, console: &Console, ) -> Result { if options.in_place { BuildDir::in_place(workspace.root()) } else { BuildDir::copy_from(workspace.root(), options, console) } } /// Make a new build dir, copying from a source directory, subject to exclusions. pub fn copy_from(source: &Utf8Path, options: &Options, console: &Console) -> Result { let name_base = format!("cargo-mutants-{}-", source.file_name().unwrap_or("unnamed")); let source_abs = source .canonicalize_utf8() .context("canonicalize source path")?; let temp_dir = copy_tree(source, &name_base, options, console)?; let path: Utf8PathBuf = temp_dir .path() .to_owned() .try_into() .context("tempdir path to UTF-8")?; fix_manifest(&path.join("Cargo.toml"), &source_abs)?; fix_cargo_config(&path, &source_abs)?; let temp_dir = if options.leak_dirs { let _ = temp_dir.into_path(); info!(?path, "Build directory will be leaked for inspection"); None } else { Some(temp_dir) }; let build_dir = BuildDir { path, temp_dir }; Ok(build_dir) } /// Make a build dir that works in-place on the source directory. pub fn in_place(source_path: &Utf8Path) -> Result { Ok(BuildDir { temp_dir: None, path: source_path .canonicalize_utf8() .context("canonicalize source path")?, }) } pub fn path(&self) -> &Utf8Path { self.path.as_path() } pub fn overwrite_file(&self, relative_path: &Utf8Path, code: &str) -> Result<()> { let full_path = self.path.join(relative_path); // for safety, don't follow symlinks ensure!(full_path.is_file(), "{full_path:?} is not a file"); write(&full_path, code.as_bytes()) .with_context(|| format!("failed to write code to {full_path:?}")) } } #[cfg(test)] mod test { use crate::test_util::copy_of_testdata; use super::*; #[test] fn build_dir_copy_from() { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(tmp.path()).unwrap(); let options = Options { in_place: false, gitignore: true, leak_dirs: false, ..Default::default() }; let build_dir = BuildDir::copy_from(workspace.root(), &options, &Console::new()).unwrap(); let debug_form = format!("{build_dir:?}"); println!("debug form is {debug_form:?}"); assert!(debug_form.starts_with("BuildDir { path: ")); assert!(build_dir.path().is_dir()); assert!(build_dir.path().join("Cargo.toml").is_file()); assert!(build_dir.path().join("src").is_dir()); } #[test] fn for_baseline_in_place() -> Result<()> { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(tmp.path())?; let options = Options { in_place: true, ..Default::default() }; let build_dir = BuildDir::for_baseline(&workspace, &options, &Console::new())?; assert_eq!( build_dir.path().canonicalize_utf8()?, workspace.root().canonicalize_utf8()? ); assert!(build_dir.temp_dir.is_none()); Ok(()) } #[test] fn for_baseline_copied() -> Result<()> { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(tmp.path())?; let options = Options { in_place: false, ..Default::default() }; let build_dir = BuildDir::for_baseline(&workspace, &options, &Console::new())?; assert!(build_dir.path().is_dir()); assert!(build_dir.path().join("Cargo.toml").is_file()); assert!(build_dir.path().join("src").is_dir()); assert!(build_dir.temp_dir.is_some()); assert_ne!( build_dir.path().canonicalize_utf8()?, workspace.root().canonicalize_utf8()? ); Ok(()) } #[test] fn build_dir_in_place() -> Result<()> { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(tmp.path())?; let build_dir = BuildDir::in_place(workspace.root())?; // On Windows e.g. the paths might not have the same form, but they // should point to the same place. assert_eq!( build_dir.path().canonicalize_utf8()?, workspace.root().canonicalize_utf8()? ); Ok(()) } } cargo-mutants-25.0.0/src/cargo.rs000064400000000000000000000353051046102023000147320ustar 00000000000000// Copyright 2021-2025 Martin Pool //! Run Cargo as a subprocess, including timeouts and propagating signals. #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] use std::env; use std::iter::once; use std::time::{Duration, Instant}; use tracing::{debug, debug_span, warn}; use crate::build_dir::BuildDir; use crate::console::Console; use crate::interrupt::check_interrupted; use crate::options::{Options, TestTool}; use crate::outcome::{Phase, PhaseResult}; use crate::output::ScenarioOutput; use crate::package::PackageSelection; use crate::process::{Exit, Process}; use crate::Result; /// Run cargo build, check, or test. #[allow(clippy::too_many_arguments)] // I agree it's a lot but I'm not sure wrapping in a struct would be better. pub fn run_cargo( build_dir: &BuildDir, jobserver: Option<&jobserver::Client>, packages: &PackageSelection, phase: Phase, timeout: Option, scenario_output: &mut ScenarioOutput, options: &Options, console: &Console, ) -> Result { let _span = debug_span!("run", ?phase).entered(); let start = Instant::now(); let argv = cargo_argv(packages, phase, options); let mut env = vec![ // The tests might use Insta , and we don't want it to write // updates to the source tree, and we *certainly* don't want it to write // updates and then let the test pass. ("INSTA_UPDATE".to_owned(), "no".to_owned()), ("INSTA_FORCE_PASS".to_owned(), "0".to_owned()), ]; if let Some(encoded_rustflags) = encoded_rustflags(options) { debug!(?encoded_rustflags); env.push(("CARGO_ENCODED_RUSTFLAGS".to_owned(), encoded_rustflags)); } let process_status = Process::run( &argv, &env, build_dir.path(), timeout, jobserver, scenario_output, console, )?; check_interrupted()?; debug!(?process_status, elapsed = ?start.elapsed()); if let Exit::Failure(code) = process_status { // 100 "one or more tests failed" from ; // I'm not addind a dependency to just get one integer. if argv[1] == "nextest" && code != 100 { // Nextest returns detailed exit codes. I think we should still treat any non-zero result as just an // error, but we can at least warn if it's unexpected. warn!(%code, "nextest process exited with unexpected code (not TEST_RUN_FAILED)"); } } Ok(PhaseResult { phase, duration: start.elapsed(), process_status, argv, }) } /// Return the name of the cargo binary. pub fn cargo_bin() -> String { // When run as a Cargo subcommand, which is the usual/intended case, // $CARGO tells us the right way to call back into it, so that we get // the matching toolchain etc. env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()) } /// Make up the argv for a cargo check/build/test invocation, including argv[0] as the /// cargo binary itself. // (This is split out so it's easier to test.) fn cargo_argv(packages: &PackageSelection, phase: Phase, options: &Options) -> Vec { let mut cargo_args = vec![cargo_bin()]; match phase { Phase::Test => match &options.test_tool { TestTool::Cargo => cargo_args.push("test".to_string()), TestTool::Nextest => { cargo_args.push("nextest".to_string()); cargo_args.push("run".to_string()); } }, Phase::Build => { match &options.test_tool { TestTool::Cargo => { // These invocations default to the test profile, and might // have other differences? Generally we want to do everything // to make the tests build, but not actually run them. // See . cargo_args.push("test".to_string()); cargo_args.push("--no-run".to_string()); } TestTool::Nextest => { cargo_args.push("nextest".to_string()); cargo_args.push("run".to_string()); cargo_args.push("--no-run".to_string()); } } } Phase::Check => { cargo_args.push("check".to_string()); cargo_args.push("--tests".to_string()); } } if let Some(profile) = &options.profile { match options.test_tool { TestTool::Cargo => { cargo_args.push(format!("--profile={profile}")); } TestTool::Nextest => { cargo_args.push(format!("--cargo-profile={profile}")); } } } cargo_args.push("--verbose".to_string()); match packages { PackageSelection::All => { cargo_args.push("--workspace".to_string()); } PackageSelection::Explicit(packages) => { cargo_args.extend( packages .iter() .map(|p| format!("--package={}", p.version_qualified_name())), ); } } let features = &options.features; if features.no_default_features { cargo_args.push("--no-default-features".to_owned()); } if features.all_features { cargo_args.push("--all-features".to_owned()); } // N.B. it can make sense to have --all-features and also explicit features from non-default packages. cargo_args.extend(features.features.iter().map(|f| format!("--features={f}"))); cargo_args.extend(options.additional_cargo_args.iter().cloned()); if phase == Phase::Test { cargo_args.extend(options.additional_cargo_test_args.iter().cloned()); } cargo_args } /// Return adjusted `CARGO_ENCODED_RUSTFLAGS`, including any changes to cap-lints. /// /// It seems we have to set this in the environment because Cargo doesn't expose /// a way to pass it in as an option from all commands? /// /// This does not currently read config files; it's too complicated. /// /// See /// fn encoded_rustflags(options: &Options) -> Option { let cap_lints_arg = "--cap-lints=warn"; let separator = "\x1f"; if !options.cap_lints { None } else if let Ok(encoded) = env::var("CARGO_ENCODED_RUSTFLAGS") { if encoded.is_empty() { Some(cap_lints_arg.to_owned()) } else { Some(encoded + separator + cap_lints_arg) } } else if let Ok(rustflags) = env::var("RUSTFLAGS") { if rustflags.is_empty() { Some(cap_lints_arg.to_owned()) } else { Some( rustflags .split(' ') .filter(|s| !s.is_empty()) .chain(once("--cap-lints=warn")) .collect::>() .join(separator), ) } } else { Some(cap_lints_arg.to_owned()) } } #[cfg(test)] mod test { use clap::Parser; use pretty_assertions::assert_eq; use rusty_fork::rusty_fork_test; use crate::Args; use super::*; #[test] fn generate_cargo_args_for_baseline_with_default_options() { let options = Options::default(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], ["check", "--tests", "--verbose", "--workspace"] ); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..], ["test", "--no-run", "--verbose", "--workspace"] ); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Test, &options)[1..], ["test", "--verbose", "--workspace"] ); } #[test] fn generate_cargo_args_with_additional_cargo_test_args_and_package() { let mut options = Options::default(); options .additional_cargo_test_args .extend(["--lib", "--no-fail-fast"].iter().map(ToString::to_string)); assert_eq!( cargo_argv( &PackageSelection::one( "cargo-mutants-testdata-something", "0.1.0", "", "src/lib.rs" ), Phase::Check, &options )[1..], [ "check", "--tests", "--verbose", "--package=cargo-mutants-testdata-something@0.1.0", ] ); } #[test] fn generate_cargo_args_with_additional_cargo_args_and_test_args() { let mut options = Options::default(); options .additional_cargo_test_args .extend(["--lib", "--no-fail-fast"].iter().map(|&s| s.to_string())); options .additional_cargo_args .extend(["--release".to_owned()]); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], ["check", "--tests", "--verbose", "--workspace", "--release"] ); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..], ["test", "--no-run", "--verbose", "--workspace", "--release"] ); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Test, &options)[1..], [ "test", "--verbose", "--workspace", "--release", "--lib", "--no-fail-fast" ] ); } #[test] fn no_default_features_args_passed_to_cargo() { let args = Args::try_parse_from(["mutants", "--no-default-features"].as_slice()).unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], [ "check", "--tests", "--verbose", "--workspace", "--no-default-features" ] ); } #[test] fn all_features_args_passed_to_cargo() { let args = Args::try_parse_from(["mutants", "--all-features"].as_slice()).unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], [ "check", "--tests", "--verbose", "--workspace", "--all-features" ] ); } #[test] fn cap_lints_passed_to_cargo() { let args = Args::try_parse_from(["mutants", "--cap-lints=true"].as_slice()).unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], ["check", "--tests", "--verbose", "--workspace",] ); } #[test] fn feature_args_passed_to_cargo() { let args = Args::try_parse_from( ["mutants", "--features", "foo", "--features", "bar,baz"].as_slice(), ) .unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], [ "check", "--tests", "--verbose", "--workspace", "--features=foo", "--features=bar,baz" ] ); } #[test] fn profile_arg_passed_to_cargo() { let args = Args::try_parse_from(["mutants", "--profile", "mutants"].as_slice()).unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Check, &options)[1..], [ "check", "--tests", "--profile=mutants", "--verbose", "--workspace", ] ); } #[test] fn nextest_gets_special_cargo_profile_option() { let args = Args::try_parse_from( ["mutants", "--test-tool=nextest", "--profile", "mutants"].as_slice(), ) .unwrap(); let options = Options::from_args(&args).unwrap(); assert_eq!( cargo_argv(&PackageSelection::All, Phase::Build, &options)[1..], [ "nextest", "run", "--no-run", "--cargo-profile=mutants", "--verbose", "--workspace", ] ); } rusty_fork_test! { #[test] fn rustflags_without_cap_lints_and_no_environment_variables() { env::remove_var("RUSTFLAGS"); env::remove_var("CARGO_ENCODED_RUSTFLAGS"); assert_eq!( encoded_rustflags(&Options { ..Default::default() }), None ); } #[test] fn rustflags_with_cap_lints_and_no_environment_variables() { env::remove_var("RUSTFLAGS"); env::remove_var("CARGO_ENCODED_RUSTFLAGS"); assert_eq!( encoded_rustflags(&Options { cap_lints: true, ..Default::default() }), Some("--cap-lints=warn".into()) ); } // Don't generate an empty argument if the encoded rustflags is empty. #[test] fn rustflags_with_empty_encoded_rustflags() { env::set_var("CARGO_ENCODED_RUSTFLAGS", ""); assert_eq!( encoded_rustflags(&Options { cap_lints: true, ..Default::default() }).unwrap(), "--cap-lints=warn" ); } #[test] fn rustflags_added_to_existing_encoded_rustflags() { env::set_var("RUSTFLAGS", "--something\x1f--else"); env::remove_var("CARGO_ENCODED_RUSTFLAGS"); let options = Options { cap_lints: true, ..Default::default() }; assert_eq!(encoded_rustflags(&options).unwrap(), "--something\x1f--else\x1f--cap-lints=warn"); } #[test] fn rustflags_added_to_existing_rustflags() { env::set_var("RUSTFLAGS", "-Dwarnings"); env::remove_var("CARGO_ENCODED_RUSTFLAGS"); assert_eq!(encoded_rustflags(&Options { cap_lints: true, ..Default::default() }).unwrap(), "-Dwarnings\x1f--cap-lints=warn"); } } } cargo-mutants-25.0.0/src/config.rs000064400000000000000000000067761046102023000151160ustar 00000000000000// Copyright 2022-2024 Martin Pool. //! `.cargo/mutants.toml` configuration file. //! //! The config file is read after parsing command line arguments, //! and after finding the source tree, because these together //! determine its location. //! //! The config file is then merged in to the [Options]. use std::default::Default; use std::fs::read_to_string; use std::path::Path; use std::str::FromStr; use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use serde::Deserialize; use crate::options::TestTool; use crate::Result; /// Configuration read from a config file. /// /// This is similar to [Options], and eventually merged into it, but separate because it /// can be deserialized. #[derive(Debug, Default, Clone, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Config { /// Pass `--cap-lints` to rustc. pub cap_lints: bool, /// Copy `.git` and other VCS directories to the build directory. pub copy_vcs: Option, /// Generate these error values from functions returning Result. pub error_values: Vec, /// Generate mutants from source files matching these globs. pub examine_globs: Vec, /// Exclude mutants from source files matching these globs. pub exclude_globs: Vec, /// Exclude mutants from source files matches these regexps. pub exclude_re: Vec, /// Examine only mutants matching these regexps. pub examine_re: Vec, /// Pass extra args to every cargo invocation. pub additional_cargo_args: Vec, /// Pass extra args to cargo test. pub additional_cargo_test_args: Vec, /// Minimum test timeout, in seconds, as a floor on the autoset value. pub minimum_test_timeout: Option, /// Output directory. pub output: Option, /// Cargo profile. pub profile: Option, /// Skip calls to functions or methods with these names. /// /// This is combined with values from the --skip-calls argument. pub skip_calls: Vec, /// Use built-in defaults for `skip_calls` in addition to any explicit values. pub skip_calls_defaults: Option, /// Run tests from these packages for all mutants. pub test_package: Vec, /// Choice of test tool: cargo or nextest. pub test_tool: Option, /// Timeout multiplier, relative to the baseline 'cargo test'. pub timeout_multiplier: Option, /// Build timeout multiplier, relative to the baseline 'cargo build'. pub build_timeout_multiplier: Option, /// Run tests from all packages in the workspace, not just the mutated package. /// /// Overrides `test_package`. pub test_workspace: Option, } impl Config { pub fn read_file(path: &Path) -> Result { let toml = read_to_string(path).with_context(|| format!("read config {path:?}"))?; Config::from_str(&toml).with_context(|| format!("parse toml from {path:?}")) } /// Read the config from a tree's `.cargo/mutants.toml`, and return a default (empty) /// Config is the file does not exist. pub fn read_tree_config(workspace_dir: &Utf8Path) -> Result { let path = workspace_dir.join(".cargo").join("mutants.toml"); if path.exists() { Config::read_file(path.as_ref()) } else { Ok(Config::default()) } } } impl FromStr for Config { type Err = anyhow::Error; fn from_str(s: &str) -> Result { toml::de::from_str(s).with_context(|| "parse toml") } } cargo-mutants-25.0.0/src/console.rs000064400000000000000000000464061046102023000153050ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Print messages and progress bars on the terminal. use std::borrow::Cow; use std::fs::File; use std::io; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use camino::{Utf8Path, Utf8PathBuf}; use console::{style, StyledObject}; use humantime::format_duration; use nutmeg::Destination; use tracing::Level; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::prelude::*; use crate::options::Colors; use crate::outcome::{LabOutcome, ScenarioOutcome, SummaryOutcome}; use crate::scenario::Scenario; use crate::tail_file::TailFile; use crate::{Mutant, Options, Phase}; /// An interface to the console for the rest of cargo-mutants. /// /// This wraps the Nutmeg view and model. pub struct Console { /// The inner view through which progress bars and messages are drawn. view: Arc>, /// The `mutants.out/debug.log` file, if it's open yet. debug_log: Arc>>, } impl Console { pub fn new() -> Console { Console { view: Arc::new(nutmeg::View::new(LabModel::default(), nutmeg_options())), debug_log: Arc::new(Mutex::new(None)), } } pub fn walk_tree_start(&self) { self.view .update(|model| model.walk_tree = Some(WalkModel::default())); } pub fn walk_tree_update(&self, files_done: usize, mutants_found: usize) { self.view.update(|model| { *model.walk_tree.as_mut().expect("walk tree started") = WalkModel { files_done, mutants_found, } }); } pub fn walk_tree_done(&self) { self.view.update(|model| model.walk_tree = None); } /// Update that a cargo task is starting. pub fn scenario_started(&self, dir: &Utf8Path, scenario: &Scenario, log_file: File) { let start = Instant::now(); let scenario_model = ScenarioModel::new(dir, scenario, start, log_file); self.view.update(|model| { model.scenario_models.push(scenario_model); }); } /// Update that cargo finished. pub fn scenario_finished( &self, dir: &Utf8Path, scenario: &Scenario, outcome: &ScenarioOutcome, options: &Options, ) { self.view.update(|model| { if scenario.is_mutant() { model.mutants_done += 1; } match outcome.summary() { SummaryOutcome::CaughtMutant => model.mutants_caught += 1, SummaryOutcome::MissedMutant => model.mutants_missed += 1, SummaryOutcome::Timeout => model.timeouts += 1, SummaryOutcome::Unviable => model.unviable += 1, SummaryOutcome::Success => model.successes += 1, SummaryOutcome::Failure => model.failures += 1, } model.remove_scenario(dir); }); if (outcome.mutant_caught() && !options.print_caught) || (outcome.scenario.is_mutant() && outcome.check_or_build_failed() && !options.print_unviable) { return; } let mut s = format!( "{:8} {}", style_outcome(outcome), style_scenario(scenario, true), ); if options.show_times { let prs: Vec = outcome .phase_results() .iter() .map(|pr| { format!( "{secs} {phase}", secs = style_secs(pr.duration), phase = style(pr.phase.to_string()).dim() ) }) .collect(); s.push_str(" in "); s.push_str(&prs.join(" + ")); } if outcome.should_show_logs() || options.show_all_logs { s.push('\n'); s.push_str( outcome .get_log_content() .expect("read log content") .as_str(), ); } s.push('\n'); self.message(&s); } pub fn start_copy(&self, dir: &Utf8Path) { self.view.update(|model| { model.copy_models.push(CopyModel::new(dir.to_owned())); }); } pub fn finish_copy(&self, dir: &Utf8Path) { self.view.update(|model| { let idx = model .copy_models .iter() .position(|m| m.dest == dir) .expect("copy model not found"); model.copy_models.swap_remove(idx); }); } pub fn copy_progress(&self, dest: &Utf8Path, total_bytes: u64) { self.view.update(|model| { model .copy_models .iter_mut() .find(|m| m.dest == dest) .expect("copy in progress") .bytes_copied(total_bytes); }); } /// Update that we discovered some mutants to test. pub fn discovered_mutants(&self, mutants: &[Mutant]) { self.message(&format!( "Found {} to test\n", plural(mutants.len(), "mutant") )); let n_mutants = mutants.len(); self.view.update(|model| { model.n_mutants = n_mutants; model.lab_start_time = Some(Instant::now()); }); } /// Update that work is starting on testing a given number of mutants. pub fn start_testing_mutants(&self, _n_mutants: usize) { self.view .update(|model| model.mutants_start_time = Some(Instant::now())); } /// A new phase of this scenario started. pub fn scenario_phase_started(&self, dir: &Utf8Path, phase: Phase) { self.view.update(|model| { model.find_scenario_mut(dir).phase_started(phase); }); } pub fn scenario_phase_finished(&self, dir: &Utf8Path, phase: Phase) { self.view.update(|model| { model.find_scenario_mut(dir).phase_finished(phase); }); } pub fn lab_finished(&self, lab_outcome: &LabOutcome, start_time: Instant, options: &Options) { self.view.update(|model| { model.scenario_models.clear(); }); self.message(&format!( "{}\n", lab_outcome.summary_string(start_time, options) )); } pub fn clear(&self) { self.view.clear(); } pub fn message(&self, message: &str) { // A workaround for nutmeg not being able to coordinate writes to both stdout and // stderr... // self.view.clear(); print!("{message}"); } pub fn tick(&self) { self.view.update(|_| ()); } /// Return a tracing `MakeWriter` that will send messages via nutmeg to the console. pub fn make_terminal_writer(&self) -> TerminalWriter { TerminalWriter { view: Arc::clone(&self.view), } } /// Return a tracing `MakeWriter` that will send messages to the debug log file if /// it's open. pub fn make_debug_log_writer(&self) -> DebugLogWriter { DebugLogWriter(Arc::clone(&self.debug_log)) } /// Set the debug log file. pub fn set_debug_log(&self, file: File) { *self.debug_log.lock().unwrap() = Some(file); } /// Configure tracing to send messages to the console and debug log. /// /// The debug log is opened later and provided by [`Console::set_debug_log`]. pub fn setup_global_trace(&self, console_trace_level: Level, colors: Colors) { // Show time relative to the start of the program. let uptime = tracing_subscriber::fmt::time::uptime(); let stderr_colors = colors .forced_value() .unwrap_or_else(::console::colors_enabled_stderr); let debug_log_layer = tracing_subscriber::fmt::layer() .with_ansi(false) .with_file(true) // source file name .with_line_number(true) .with_timer(uptime) .with_writer(self.make_debug_log_writer()); let level_filter = tracing_subscriber::filter::LevelFilter::from_level(console_trace_level); let console_layer = tracing_subscriber::fmt::layer() .with_ansi(stderr_colors) .with_writer(self.make_terminal_writer()) .with_target(false) .without_time() .with_filter(level_filter); tracing_subscriber::registry() .with(debug_log_layer) .with(console_layer) .init(); } } pub fn enable_console_colors(colors: Colors) { if let Some(colors) = colors.forced_value() { ::console::set_colors_enabled(colors); ::console::set_colors_enabled_stderr(colors); } // Otherwise, let the console crate decide, based on isatty, etc. } impl Default for Console { fn default() -> Self { Self::new() } } /// Write trace output to the terminal via the console. pub struct TerminalWriter { view: Arc>, } impl MakeWriter<'_> for TerminalWriter { type Writer = Self; fn make_writer(&self) -> Self::Writer { TerminalWriter { view: Arc::clone(&self.view), } } } impl std::io::Write for TerminalWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { // This calls `message` rather than `View::write` because the latter // only requires a &View and it handles locking internally, without // requiring exclusive use of the Arc. self.view.message(std::str::from_utf8(buf).unwrap()); Ok(buf.len()) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } /// Write trace output to the debug log file if it's open. pub struct DebugLogWriter(Arc>>); impl MakeWriter<'_> for DebugLogWriter { type Writer = Self; fn make_writer(&self) -> Self::Writer { DebugLogWriter(self.0.clone()) } } impl io::Write for DebugLogWriter { fn write(&mut self, buf: &[u8]) -> io::Result { if let Some(file) = self.0.lock().unwrap().as_mut() { file.write(buf) } else { Ok(buf.len()) } } fn flush(&mut self) -> io::Result<()> { if let Some(file) = self.0.lock().unwrap().as_mut() { file.flush() } else { Ok(()) } } } /// Description of all current activities in the lab. /// /// At the moment there is either a copy, cargo runs, or nothing. Later, there /// might be concurrent activities. #[derive(Default)] struct LabModel { walk_tree: Option, /// Copy jobs in progress copy_models: Vec, scenario_models: Vec, lab_start_time: Option, /// The instant when we started trying mutation scenarios, after running the baseline. mutants_start_time: Option, mutants_done: usize, n_mutants: usize, mutants_caught: usize, mutants_missed: usize, unviable: usize, timeouts: usize, successes: usize, failures: usize, } impl nutmeg::Model for LabModel { #[allow(clippy::cast_precision_loss)] fn render(&mut self, width: usize) -> String { let mut s = String::with_capacity(1024); if let Some(walk_tree) = &mut self.walk_tree { s += &walk_tree.render(width); } for copy_model in &mut self.copy_models { if !s.is_empty() { s.push('\n'); } s.push_str(©_model.render(width)); } for sm in &mut self.scenario_models { if !s.is_empty() { s.push('\n'); } s.push_str(&sm.render(width)); } if let Some(lab_start_time) = self.lab_start_time { if !s.is_empty() { s.push('\n'); } let elapsed = lab_start_time.elapsed(); s += &format!( "{}/{} mutants tested", style(self.mutants_done).cyan(), style(self.n_mutants).cyan(), ); if self.mutants_missed > 0 { s += &format!( ", {} {}", style(self.mutants_missed).cyan(), style("MISSED").red() ); } if self.timeouts > 0 { s += &format!( ", {} {}", style(self.timeouts).cyan(), style("timeout").red() ); } if self.mutants_caught > 0 { s.push_str(&format!(", {} caught", style(self.mutants_caught).cyan())); } if self.unviable > 0 { s.push_str(&format!(", {} unviable", style(self.unviable).cyan())); } // Maybe don't report these, because they're uninteresting? // if self.successes > 0 { // write!(s, ", {} successes", self.successes).unwrap(); // } // if self.failures > 0 { // write!(s, ", {} failures", self.failures).unwrap(); // } s.push_str(&format!(", {} elapsed", style_duration(elapsed))); if self.mutants_done > 2 { let done = self.mutants_done as f64; let remain = self.n_mutants as f64 - done; let mut remaining_secs = self.mutants_start_time.unwrap().elapsed().as_secs_f64() * remain / done; if remaining_secs > 300.0 { // Round up to minutes remaining_secs = ((remaining_secs + 30.0) / 60.0).ceil() * 60.0; } s += &format!( ", about {} remaining", style_duration(Duration::from_secs_f64(remaining_secs.ceil())) ); } } s } } impl LabModel { fn find_scenario_mut(&mut self, dir: &Utf8Path) -> &mut ScenarioModel { self.scenario_models .iter_mut() .find(|sm| sm.dir == *dir) .expect("scenario directory not found") } fn remove_scenario(&mut self, dir: &Utf8Path) { self.scenario_models.retain(|sm| sm.dir != *dir); } } /// A Nutmeg progress model for walking the tree. #[derive(Default)] struct WalkModel { files_done: usize, mutants_found: usize, } impl nutmeg::Model for WalkModel { fn render(&mut self, _width: usize) -> String { if self.files_done == 0 { "Scanning tree metadata...\n".to_owned() } else { format!( "Finding mutation opportunities: {} files done, {} mutants found\n", self.files_done, self.mutants_found ) } } } /// A Nutmeg progress model for running a single scenario. /// /// It draws the command and some description of what scenario is being tested. struct ScenarioModel { /// The directory where this is being built: unique across all models. dir: Utf8PathBuf, name: Cow<'static, str>, phase_start: Instant, phase: Option, /// Previously-executed phases and durations. previous_phase_durations: Vec<(Phase, Duration)>, log_tail: TailFile, } impl ScenarioModel { fn new(dir: &Utf8Path, scenario: &Scenario, start: Instant, log_file: File) -> ScenarioModel { ScenarioModel { dir: dir.to_owned(), name: style_scenario(scenario, true), phase: None, phase_start: start, log_tail: TailFile::new(log_file), previous_phase_durations: Vec::new(), } } fn phase_started(&mut self, phase: Phase) { self.phase = Some(phase); self.phase_start = Instant::now(); } fn phase_finished(&mut self, phase: Phase) { debug_assert_eq!(self.phase, Some(phase)); self.previous_phase_durations .push((phase, self.phase_start.elapsed())); self.phase = None; } } impl nutmeg::Model for ScenarioModel { fn render(&mut self, _width: usize) -> String { let mut parts = Vec::new(); if let Some(phase) = self.phase { parts.push(style(format!("{phase:8}")).bold().cyan().to_string()); } parts.push(self.name.to_string()); parts.push("...".to_string()); parts.push(style_secs(self.phase_start.elapsed()).to_string()); // let mut prs = self // .previous_phase_durations // .iter() // .map(|(phase, duration)| format!("{} {}", style_secs(*duration), style(phase).dim())) // .collect::>(); // if prs.len() > 1 { // prs.insert(0, String::new()) // } // parts.push(prs.join(" + ")); let mut s = parts.join(" "); if let Ok(last_line) = self.log_tail.last_line() { s.push_str(&format!( "\n{:8} {}", style("â””").cyan(), style(last_line).dim() )); } s } } /// A Nutmeg model for progress in copying a tree. struct CopyModel { dest: Utf8PathBuf, bytes_copied: u64, start: Instant, } impl CopyModel { fn new(dest: Utf8PathBuf) -> CopyModel { CopyModel { dest, start: Instant::now(), bytes_copied: 0, } } /// Update that some bytes have been copied. /// /// `bytes_copied` is the total bytes copied so far. fn bytes_copied(&mut self, bytes_copied: u64) { self.bytes_copied = bytes_copied; } } impl nutmeg::Model for CopyModel { fn render(&mut self, _width: usize) -> String { format!( "{:8} {} in {}", style("copy").cyan(), style_mb(self.bytes_copied), style_secs(self.start.elapsed()), ) } } fn nutmeg_options() -> nutmeg::Options { nutmeg::Options::default() .print_holdoff(Duration::from_millis(50)) .destination(Destination::Stderr) } /// Return a styled string reflecting the moral value of this outcome. pub fn style_outcome(outcome: &ScenarioOutcome) -> StyledObject<&'static str> { match outcome.summary() { SummaryOutcome::CaughtMutant => style("caught").green(), SummaryOutcome::MissedMutant => style("MISSED").red().bold(), SummaryOutcome::Failure => style("FAILED").red().bold(), SummaryOutcome::Success => style("ok").green(), SummaryOutcome::Unviable => style("unviable").blue(), SummaryOutcome::Timeout => style("TIMEOUT").red().bold(), } } fn style_secs(duration: Duration) -> String { style(format!("{:.1}s", duration.as_secs_f32())) .cyan() .to_string() } fn style_duration(duration: Duration) -> String { // We don't want silly precision. let duration = Duration::from_secs(duration.as_secs()); style(format_duration(duration).to_string()) .cyan() .to_string() } fn format_mb(bytes: u64) -> String { format!("{} MB", bytes / 1_000_000) } fn style_mb(bytes: u64) -> StyledObject { style(format_mb(bytes)).cyan() } pub fn style_scenario(scenario: &Scenario, line_col: bool) -> Cow<'static, str> { match scenario { Scenario::Baseline => Cow::Borrowed("Unmutated baseline"), Scenario::Mutant(mutant) => Cow::Owned(mutant.to_styled_string(line_col)), } } pub fn plural(n: usize, noun: &str) -> String { if n == 1 { format!("{n} {noun}") } else { format!("{n} {noun}s") } } cargo-mutants-25.0.0/src/copy_tree/unix.rs000064400000000000000000000007131046102023000166060ustar 00000000000000use std::fs::FileType; use anyhow::Context; use camino::Utf8Path; use crate::Result; pub(super) fn copy_symlink(_ft: FileType, src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> { let link_target = std::fs::read_link(src_path) .with_context(|| format!("Failed to read link {src_path:?}"))?; std::os::unix::fs::symlink(link_target, dest_path) .with_context(|| format!("Failed to create symlink {dest_path:?}",))?; Ok(()) } cargo-mutants-25.0.0/src/copy_tree/windows.rs000064400000000000000000000014601046102023000173150ustar 00000000000000use std::fs::FileType; use std::os::windows::fs::FileTypeExt; use anyhow::Context; use camino::Utf8Path; use crate::Result; #[mutants::skip] // Mutant tests run on Linux pub(super) fn copy_symlink(ft: FileType, src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> { let link_target = std::fs::read_link(src_path).with_context(|| format!("read link {src_path:?}"))?; if ft.is_symlink_dir() { std::os::windows::fs::symlink_dir(link_target, dest_path) .with_context(|| format!("create symlink {dest_path:?}"))?; } else if ft.is_symlink_file() { std::os::windows::fs::symlink_file(link_target, dest_path) .with_context(|| format!("create symlink {dest_path:?}"))?; } else { anyhow::bail!("Unknown symlink type: {:?}", ft); } Ok(()) } cargo-mutants-25.0.0/src/copy_tree.rs000064400000000000000000000230721046102023000156260ustar 00000000000000// Copyright 2023 - 2025 Martin Pool //! Copy a source tree, with some exclusions, to a new temporary directory. use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use ignore::WalkBuilder; use path_slash::PathExt; use tempfile::TempDir; use tracing::{debug, warn}; use crate::options::Options; use crate::{check_interrupted, Console, Result}; #[cfg(unix)] mod unix; #[cfg(unix)] use unix::copy_symlink; #[cfg(windows)] mod windows; #[cfg(windows)] use windows::copy_symlink; static VCS_DIRS: &[&str] = &[".git", ".hg", ".bzr", ".svn", "_darcs", ".jj", ".pijul"]; /// Copy a source tree, with some exclusions, to a new temporary directory. pub fn copy_tree( from_path: &Utf8Path, name_base: &str, options: &Options, console: &Console, ) -> Result { let mut total_bytes = 0; let mut total_files = 0; let temp_dir = tempfile::Builder::new() .prefix(name_base) .suffix(".tmp") .tempdir() .context("create temp dir")?; let dest = temp_dir .path() .try_into() .context("Convert path to UTF-8")?; console.start_copy(dest); let mut walk_builder = WalkBuilder::new(from_path); let copy_vcs = options.copy_vcs; // for lifetime walk_builder .git_ignore(options.gitignore) .git_exclude(options.gitignore) .git_global(options.gitignore) .hidden(false) // copy hidden files .ignore(false) // don't use .ignore .require_git(true) // stop at git root; only read gitignore files inside git trees .filter_entry(move |entry| { let name = entry.file_name().to_string_lossy(); name != "mutants.out" && name != "mutants.out.old" && (copy_vcs || !VCS_DIRS.contains(&name.as_ref())) }); debug!(?walk_builder); for entry in walk_builder.build() { check_interrupted()?; let entry = entry?; let relative_path = entry .path() .strip_prefix(from_path) .expect("entry path is in from_path"); let dest_path: Utf8PathBuf = temp_dir .path() .join(relative_path) .try_into() .context("Convert path to UTF-8")?; let ft = entry .file_type() .with_context(|| format!("Expected file to have a file type: {:?}", entry.path()))?; if ft.is_file() { let bytes_copied = std::fs::copy(entry.path(), &dest_path).with_context(|| { format!( "Failed to copy {:?} to {dest_path:?}", entry.path().to_slash_lossy(), ) })?; total_bytes += bytes_copied; total_files += 1; console.copy_progress(dest, total_bytes); } else if ft.is_dir() { std::fs::create_dir_all(&dest_path) .with_context(|| format!("Failed to create directory {dest_path:?}"))?; } else if ft.is_symlink() { copy_symlink( ft, entry .path() .try_into() .context("Convert filename to UTF-8")?, &dest_path, )?; } else { warn!("Unexpected file type: {:?}", entry.path()); } } console.finish_copy(dest); debug!(?total_bytes, ?total_files, temp_dir = ?temp_dir.path(), "Copied source tree"); Ok(temp_dir) } #[cfg(test)] mod test { // TODO: Maybe run these with $HOME set to a temp dir so that global git config has no effect? use std::fs::{create_dir, write}; use camino::Utf8PathBuf; use tempfile::TempDir; use crate::console::Console; use crate::options::Options; use crate::Result; use super::copy_tree; /// Test for regression of #[test] fn copy_tree_with_parent_ignoring_star() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = tmp_dir.path(); write(tmp.join(".gitignore"), "*\n")?; let a = Utf8PathBuf::try_from(tmp.join("a")).unwrap(); create_dir(&a)?; write(a.join("Cargo.toml"), "[package]\nname = a")?; let src = a.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; let options = Options::from_arg_strs(["--gitignore=true"]); let dest_tmpdir = copy_tree(&a, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(dest.join("Cargo.toml").is_file()); assert!(dest.join("src").is_dir()); assert!(dest.join("src/main.rs").is_file()); Ok(()) } /// With `gitignore` set to `true`, but no `.git`, don't exclude anything. #[test] fn copy_with_gitignore_but_without_git_dir() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); write(tmp.join(".gitignore"), "foo\n")?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; write(tmp.join("foo"), "bar")?; let options = Options::from_arg_strs(["--gitignore=true"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!( dest.join("foo").is_file(), "foo should be copied because gitignore is not used without .git" ); Ok(()) } /// With `gitignore` set to `true`, in a tree with `.git`, `.gitignore` is respected. #[test] fn copy_with_gitignore_and_git_dir() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); write(tmp.join(".gitignore"), "foo\n")?; create_dir(tmp.join(".git"))?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; write(tmp.join("foo"), "bar")?; let options = Options::from_arg_strs(["mutants", "--gitignore=true"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!( !dest.join("foo").is_file(), "foo should have been excluded by gitignore" ); Ok(()) } /// With `gitignore` set to `false`, patterns in that file have no effect. #[test] fn copy_without_gitignore() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); write(tmp.join(".gitignore"), "foo\n")?; create_dir(tmp.join(".git"))?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; write(tmp.join("foo"), "bar")?; let options = Options::from_arg_strs(["mutants", "--gitignore=false"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); // gitignore didn't exclude `foo` assert!(dest.join("foo").is_file()); Ok(()) } #[test] fn dont_copy_git_dir_or_mutants_out_by_default() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); create_dir(tmp.join(".git"))?; write(tmp.join(".git/foo"), "bar")?; create_dir(tmp.join("mutants.out"))?; write(tmp.join("mutants.out/foo"), "bar")?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; let options = Options::from_arg_strs(["mutants"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(!dest.join(".git").is_dir(), ".git should not be copied"); assert!( !dest.join(".git/foo").is_file(), ".git/foo should not be copied" ); assert!( !dest.join("mutants.out").exists(), "mutants.out should not be copied" ); assert!( dest.join("Cargo.toml").is_file(), "Cargo.toml should be copied" ); Ok(()) } #[test] fn copy_git_dir_when_requested() -> Result<()> { let tmp_dir = TempDir::new().unwrap(); let tmp = Utf8PathBuf::try_from(tmp_dir.path().to_owned()).unwrap(); create_dir(tmp.join(".git"))?; write(tmp.join(".git/foo"), "bar")?; create_dir(tmp.join("mutants.out"))?; write(tmp.join("mutants.out/foo"), "bar")?; write(tmp.join("Cargo.toml"), "[package]\nname = a")?; let src = tmp.join("src"); create_dir(&src)?; write(src.join("main.rs"), "fn main() {}")?; let options = Options::from_arg_strs(["mutants", "--copy-vcs=true"]); let dest_tmpdir = copy_tree(&tmp, "a", &options, &Console::new())?; let dest = dest_tmpdir.path(); assert!(dest.join(".git").is_dir(), ".git should be copied"); assert!(dest.join(".git/foo").is_file(), ".git/foo should be copied"); assert!( !dest.join("mutants.out").exists(), "mutants.out should not be copied" ); assert!( dest.join("Cargo.toml").is_file(), "Cargo.toml should be copied" ); Ok(()) } } cargo-mutants-25.0.0/src/exit_code.rs000064400000000000000000000016521046102023000156000ustar 00000000000000// Copyright 2021 Martin Pool //! Exit codes from cargo-mutants. //! //! These are assigned so that different cases that CI or other automation (or //! cargo-mutants' own test suite) might want to distinguish are distinct. //! //! These are also described in README.md. // TODO: Maybe merge this with outcome::Status, and maybe merge with sysexit. /// Everything worked and all the mutants were caught. pub const SUCCESS: i32 = 0; /// The wrong arguments, etc. /// /// (1 is also the value returned by Clap.) pub const USAGE: i32 = 1; /// Found one or mutants that were not caught by tests. pub const FOUND_PROBLEMS: i32 = 2; /// One or more tests timed out: probably the mutant caused an infinite loop, or the timeout is too low. pub const TIMEOUT: i32 = 3; /// The tests are already failing in an unmutated tree. pub const CLEAN_TESTS_FAILED: i32 = 4; /// An internal software error, from sysexit. pub const SOFTWARE: i32 = 70; cargo-mutants-25.0.0/src/fnvalue.rs000064400000000000000000000665201046102023000153020ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Mutations of replacing a function body with a value of a (hopefully) appropriate type. #![warn(clippy::pedantic)] use std::iter; use itertools::Itertools; use proc_macro2::TokenStream; use quote::quote; use syn::{ AngleBracketedGenericArguments, AssocType, Expr, GenericArgument, Ident, Path, PathArguments, ReturnType, TraitBound, Type, TypeArray, TypeImplTrait, TypeParamBound, TypeSlice, TypeTuple, }; use tracing::trace; use crate::pretty::ToPrettyString; /// Generate replacement text for a function based on its return type. pub(crate) fn return_type_replacements( return_type: &ReturnType, error_exprs: &[Expr], ) -> Vec { match return_type { ReturnType::Default => vec![quote! { () }], ReturnType::Type(_rarrow, type_) => type_replacements(type_, error_exprs).collect_vec(), } } /// Generate some values that we hope are reasonable replacements for a type. #[allow(clippy::too_many_lines)] fn type_replacements(type_: &Type, error_exprs: &[Expr]) -> impl Iterator { // This could probably change to run from some configuration rather than // hardcoding various types, which would make it easier to support tree-specific // mutation values, and perhaps reduce duplication. However, it seems better // to support all the core cases with direct code first to learn what generalizations // are needed. match type_ { Type::Path(syn::TypePath { path, .. }) => { // dbg!(&path); if path.is_ident("bool") { vec![quote! { true }, quote! { false }] } else if path.is_ident("String") { vec![quote! { String::new() }, quote! { "xyzzy".into() }] } else if path.is_ident("str") { vec![quote! { "" }, quote! { "xyzzy" }] } else if path_is_unsigned(path) { vec![quote! { 0 }, quote! { 1 }] } else if path_is_signed(path) { vec![quote! { 0 }, quote! { 1 }, quote! { -1 }] } else if path_is_nonzero_signed(path) { vec![quote! { 1 }, quote! { -1 }] } else if path_is_nonzero_unsigned(path) { vec![quote! { 1 }] } else if path_is_float(path) { vec![quote! { 0.0 }, quote! { 1.0 }, quote! { -1.0 }] } else if path_ends_with(path, "Result") { if let Some(ok_type) = match_first_type_arg(path, "Result") { type_replacements(ok_type, error_exprs) .map(|rep| { quote! { Ok(#rep) } }) .collect_vec() } else { // A result with no type arguments, like `fmt::Result`; hopefully // the Ok value can be constructed with Default. vec![quote! { Ok(Default::default()) }] } .into_iter() .chain(error_exprs.iter().map(|error_expr| { quote! { Err(#error_expr) } })) .collect_vec() } else if path_ends_with(path, "HttpResponse") { vec![quote! { HttpResponse::Ok().finish() }] } else if let Some(some_type) = match_first_type_arg(path, "Option") { iter::once(quote! { None }) .chain(type_replacements(some_type, error_exprs).map(|rep| { quote! { Some(#rep) } })) .collect_vec() } else if let Some(element_type) = match_first_type_arg(path, "Vec") { // Generate an empty Vec, and then a one-element vec for every recursive // value. iter::once(quote! { vec![] }) .chain(type_replacements(element_type, error_exprs).map(|rep| { quote! { vec![#rep] } })) .collect_vec() } else if let Some(borrowed_type) = match_first_type_arg(path, "Cow") { // TODO: We could specialize Cows for cases like Vec and Box where // we would have to leak to make the reference; perhaps it would only // look better... type_replacements(borrowed_type, error_exprs) .flat_map(|rep| { [ quote! { Cow::Borrowed(#rep) }, quote! { Cow::Owned(#rep.to_owned()) }, ] }) .collect_vec() } else if let Some((container_type, inner_type)) = known_container(path) { // Something like Arc, Mutex, etc. // TODO: Ideally we should use the path without relying on it being // imported, but we must strip or rewrite the arguments, so that // `std::sync::Arc` becomes either `std::sync::Arc::::new` // or at least `std::sync::Arc::new`. Similarly for other types. type_replacements(inner_type, error_exprs) .map(|rep| { quote! { #container_type::new(#rep) } }) .collect_vec() } else if let Some((collection_type, inner_type)) = known_collection(path) { iter::once(quote! { #collection_type::new() }) .chain(type_replacements(inner_type, error_exprs).map(|rep| { quote! { #collection_type::from_iter([#rep]) } })) .collect_vec() } else if let Some((collection_type, key_type, value_type)) = known_map(path) { let key_reps = type_replacements(key_type, error_exprs).collect_vec(); let val_reps = type_replacements(value_type, error_exprs).collect_vec(); iter::once(quote! { #collection_type::new() }) .chain( key_reps .iter() .cartesian_product(val_reps) .map(|(k, v)| quote! { #collection_type::from_iter([(#k, #v)]) }), ) .collect_vec() } else if let Some((collection_type, inner_type)) = maybe_collection_or_container(path) { // Something like `T
` or `T<'a, A>`, when we don't know exactly how // to call it, but we strongly suspect that you could construct it from // an `A`. iter::once(quote! { #collection_type::new() }) .chain(type_replacements(inner_type, error_exprs).flat_map(|rep| { [ quote! { #collection_type::from_iter([#rep]) }, quote! { #collection_type::new(#rep) }, quote! { #collection_type::from(#rep) }, ] })) .collect_vec() } else { trace!( type_ = type_.to_pretty_string(), "Return type is not recognized, trying Default" ); vec![quote! { Default::default() }] } } Type::Array(TypeArray { elem, len, .. }) => // Generate arrays that repeat each replacement value however many times. // In principle we could generate combinations, but that might get very // large, and values like "all zeros" and "all ones" seem likely to catch // lots of things. { type_replacements(elem, error_exprs) .map(|r| quote! { [ #r; #len ] }) .collect_vec() } Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) }) .chain(type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) })) .collect_vec(), Type::Reference(syn::TypeReference { mutability: None, elem, .. }) => match &**elem { // You can't currently match box patterns in Rust Type::Path(path) if path.path.is_ident("str") => { vec![quote! { "" }, quote! { "xyzzy" }] } Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) }) .chain( type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), ) .collect_vec(), _ => type_replacements(elem, error_exprs) .map(|rep| { quote! { &#rep } }) .collect_vec(), }, Type::Reference(syn::TypeReference { mutability: Some(_), elem, .. }) => match &**elem { Type::Slice(TypeSlice { elem, .. }) => iter::once(quote! { Vec::leak(Vec::new()) }) .chain( type_replacements(elem, error_exprs).map(|r| quote! { Vec::leak(vec![ #r ]) }), ) .collect_vec(), _ => { // Make &mut with static lifetime by leaking them on the heap. type_replacements(elem, error_exprs) .map(|rep| { quote! { Box::leak(Box::new(#rep)) } }) .collect_vec() } }, Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => { vec![quote! { () }] } Type::Tuple(TypeTuple { elems, .. }) => { // Generate the cartesian product of replacements of every type within the tuple. elems .iter() .map(|elem| type_replacements(elem, error_exprs).collect_vec()) .multi_cartesian_product() .map(|reps| { quote! { ( #( #reps ),* ) } }) .collect_vec() } // -> impl Iterator Type::ImplTrait(impl_trait) => { if let Some(item_type) = match_impl_iterator(impl_trait) { iter::once(quote! { ::std::iter::empty() }) .chain( type_replacements(item_type, error_exprs) .map(|r| quote! { ::std::iter::once(#r) }), ) .collect_vec() } else { // TODO: Can we do anything with other impl traits? vec![] } } Type::Never(_) => { vec![] } _ => { trace!(?type_, "Return type is not recognized, trying Default"); vec![quote! { Default::default() }] } } .into_iter() } fn path_ends_with(path: &Path, ident: &str) -> bool { path.segments.last().is_some_and(|s| s.ident == ident) } fn match_impl_iterator(TypeImplTrait { bounds, .. }: &TypeImplTrait) -> Option<&Type> { for bound in bounds { if let TypeParamBound::Trait(TraitBound { path, .. }) = bound { if let Some(last_segment) = path.segments.last() { if last_segment.ident == "Iterator" { if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last_segment.arguments { if let Some(GenericArgument::AssocType(AssocType { ident, ty, .. })) = args.first() { if ident == "Item" { return Some(ty); } } } } } } } None } /// If the type has a single type argument then, perhaps it's a simple container /// like Box, Cell, Mutex, etc, that can be constructed with `T::new(inner_val)`. /// /// If so, return the short name (like "Box") and the inner type. fn known_container(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; if ["Box", "Cell", "RefCell", "Arc", "Rc", "Mutex"] .iter() .any(|v| last.ident == v) { if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { // TODO: Skip lifetime args. // TODO: Return the path with args stripped out. if args.len() == 1 { if let Some(GenericArgument::Type(inner_type)) = args.first() { return Some((&last.ident, inner_type)); } } } } None } /// Match known simple collections that can be empty or constructed from an /// iterator. /// /// Returns the short name (like `"VecDeque"`) and the inner type. fn known_collection(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; if ![ "BinaryHeap", "BTreeSet", "HashSet", "LinkedList", "VecDeque", ] .iter() .any(|v| last.ident == v) { return None; } if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { // TODO: Skip lifetime args. // TODO: Return the path with args stripped out. if args.len() == 1 { if let Some(GenericArgument::Type(inner_type)) = args.first() { return Some((&last.ident, inner_type)); } } } None } /// Match known key-value maps that can be empty or constructed from pair of /// recursively-generated values. fn known_map(path: &Path) -> Option<(&Ident, &Type, &Type)> { let last = path.segments.last()?; if !["BTreeMap", "HashMap"].iter().any(|v| last.ident == v) { return None; } if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { // TODO: Skip lifetime args. // TODO: Return the path with args stripped out. if let Some((GenericArgument::Type(ref key_type), GenericArgument::Type(ref value_type))) = args.iter().collect_tuple() { return Some((&last.ident, key_type, value_type)); } } None } /// Match a type with one type argument, which might be a container or collection. fn maybe_collection_or_container(path: &Path) -> Option<(&Ident, &Type)> { let last = path.segments.last()?; if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { let type_args: Vec<_> = args .iter() .filter_map(|a| match a { GenericArgument::Type(t) => Some(t), _ => None, }) .collect(); // TODO: Return the path with args stripped out. if type_args.len() == 1 { return Some((&last.ident, type_args.first().unwrap())); } } None } fn path_is_float(path: &Path) -> bool { ["f32", "f64"].iter().any(|s| path.is_ident(s)) } fn path_is_unsigned(path: &Path) -> bool { ["u8", "u16", "u32", "u64", "u128", "usize"] .iter() .any(|s| path.is_ident(s)) } fn path_is_signed(path: &Path) -> bool { ["i8", "i16", "i32", "i64", "i128", "isize"] .iter() .any(|s| path.is_ident(s)) } fn path_is_nonzero_signed(path: &Path) -> bool { if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { matches!( l.as_str(), "NonZeroIsize" | "NonZeroI8" | "NonZeroI16" | "NonZeroI32" | "NonZeroI64" | "NonZeroI128", ) } else { false } } fn path_is_nonzero_unsigned(path: &Path) -> bool { if let Some(l) = path.segments.last().map(|p| p.ident.to_string()) { matches!( l.as_str(), "NonZeroUsize" | "NonZeroU8" | "NonZeroU16" | "NonZeroU32" | "NonZeroU64" | "NonZeroU128", ) } else { false } } /// If this is a path ending in `expected_ident`, return the first type argument, ignoring /// lifetimes. fn match_first_type_arg<'p>(path: &'p Path, expected_ident: &str) -> Option<&'p Type> { // TODO: Maybe match only things with one arg? let last = path.segments.last()?; if last.ident == expected_ident { if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = &last.arguments { for arg in args { match arg { GenericArgument::Type(arg_type) => return Some(arg_type), GenericArgument::Lifetime(_) => (), _ => return None, } } } } None } #[cfg(test)] mod test { use itertools::Itertools; use pretty_assertions::assert_eq; use syn::{parse_quote, Expr, ReturnType}; use crate::fnvalue::match_impl_iterator; use crate::pretty::ToPrettyString; use super::{known_map, return_type_replacements}; #[test] fn recurse_into_result_bool() { check_replacements( &parse_quote! {-> std::result::Result }, &[], &["Ok(true)", "Ok(false)"], ); } #[test] fn recurse_into_result_result_bool_with_error_values() { check_replacements( &parse_quote! {-> std::result::Result> }, &[parse_quote! { anyhow!("mutated") }], &[ "Ok(Ok(true))", "Ok(Ok(false))", r#"Ok(Err(anyhow!("mutated")))"#, r#"Err(anyhow!("mutated"))"#, ], ); } #[test] fn u16_replacements() { check_replacements(&parse_quote! { -> u16 }, &[], &["0", "1"]); } #[test] fn isize_replacements() { check_replacements(&parse_quote! { -> isize }, &[], &["0", "1", "-1"]); } #[test] fn nonzero_integer_replacements() { check_replacements( &parse_quote! { -> std::num::NonZeroIsize }, &[], &["1", "-1"], ); check_replacements(&parse_quote! { -> std::num::NonZeroUsize }, &[], &["1"]); check_replacements(&parse_quote! { -> std::num::NonZeroU32 }, &[], &["1"]); } #[test] fn unit_replacement() { check_replacements(&parse_quote! { -> () }, &[], &["()"]); } #[test] fn result_unit_replacement() { check_replacements(&parse_quote! { -> Result<(), Error> }, &[], &["Ok(())"]); check_replacements(&parse_quote! { -> Result<()> }, &[], &["Ok(())"]); } #[test] fn http_response_replacement() { check_replacements( &parse_quote! { -> HttpResponse }, &[], &["HttpResponse::Ok().finish()"], ); } #[test] fn option_usize_replacement() { check_replacements( &parse_quote! { -> Option }, &[], &["None", "Some(0)", "Some(1)"], ); } #[test] fn box_usize_replacement() { check_replacements( &parse_quote! { -> Box }, &[], &["Box::new(0)", "Box::new(1)"], ); } #[test] fn box_unrecognized_type_replacement() { check_replacements( &parse_quote! { -> Box }, &[], &["Box::new(Default::default())"], ); } #[test] fn vec_string_replacement() { check_replacements( &parse_quote! { -> std::vec::Vec }, &[], &["vec![]", "vec![String::new()]", r#"vec!["xyzzy".into()]"#], ); } #[test] fn float_replacement() { check_replacements(&parse_quote! { -> f32 }, &[], &["0.0", "1.0", "-1.0"]); } #[test] fn ref_replacement_recurses() { check_replacements(&parse_quote! { -> &bool }, &[], &["&true", "&false"]); } #[test] fn array_replacement() { check_replacements( &parse_quote! { -> [u8; 256] }, &[], &["[0; 256]", "[1; 256]"], ); } #[test] fn arc_replacement() { // Also checks that it matches the path, even using an atypical path. // TODO: Ideally this would be fully qualified like `alloc::sync::Arc::new(String::new())`. check_replacements( &parse_quote! { -> alloc::sync::Arc }, &[], &["Arc::new(String::new())", r#"Arc::new("xyzzy".into())"#], ); } #[test] fn rc_replacement() { // Also checks that it matches the path, even using an atypical path. // TODO: Ideally this would be fully qualified like `alloc::sync::Rc::new(String::new())`. check_replacements( &parse_quote! { -> alloc::sync::Rc }, &[], &["Rc::new(String::new())", r#"Rc::new("xyzzy".into())"#], ); } #[test] fn match_known_collection() { assert_eq!( super::known_collection(&parse_quote! { std::collections::VecDeque }), Some((&parse_quote! { VecDeque }, &parse_quote! { String })) ); assert_eq!( super::known_collection(&parse_quote! { std::collections::BinaryHeap<(u32, u32)> }), Some((&parse_quote! { BinaryHeap }, &parse_quote! { (u32, u32) })) ); assert_eq!( super::known_collection(&parse_quote! { LinkedList<[u8; 256]> }), Some((&parse_quote! { LinkedList }, &parse_quote! { [u8; 256] })) ); assert_eq!(super::known_collection(&parse_quote! { Arc }), None); // This might be a collection, and is handled generically, but it's not a specifically known // collection type. (Maybe we shouldn't bother knowing specific types?) assert_eq!( super::known_collection(&parse_quote! { Wibble<&str> }), None ); } #[test] fn match_known_map() { assert_eq!( super::known_map(&parse_quote! { std::collections::BTreeMap }), Some(( &parse_quote! { BTreeMap }, &parse_quote! { String }, &parse_quote! { usize } )) ); assert_eq!( super::known_map(&parse_quote! { std::collections::HashMap<(usize, usize), bool> }), Some(( &parse_quote! { HashMap }, &parse_quote! { (usize, usize) }, &parse_quote! { bool } )) ); assert_eq!( super::known_map(&parse_quote! { Option<(usize, usize)> }), None ); assert_eq!( super::known_map(&parse_quote! { MyMap }), None, ); assert_eq!( super::known_map(&parse_quote! { Pair }), None, ); } #[test] fn btreeset_replacement() { check_replacements( &parse_quote! { -> std::collections::BTreeSet }, &[], &[ "BTreeSet::new()", "BTreeSet::from_iter([String::new()])", r#"BTreeSet::from_iter(["xyzzy".into()])"#, ], ); } #[test] fn cow_generates_borrowed_and_owned() { check_replacements( &parse_quote! { -> Cow<'static, str> }, &[], &[ r#"Cow::Borrowed("")"#, r#"Cow::Owned("".to_owned())"#, r#"Cow::Borrowed("xyzzy")"#, r#"Cow::Owned("xyzzy".to_owned())"#, ], ); } #[test] fn unknown_container_replacement() { // This looks like something that holds a &str, and maybe can be constructed // from a &str, but we don't know anything else about it, so we just guess. check_replacements( &parse_quote! { -> UnknownContainer<'static, str> }, &[], &[ "UnknownContainer::new()", r#"UnknownContainer::from_iter([""])"#, r#"UnknownContainer::new("")"#, r#"UnknownContainer::from("")"#, r#"UnknownContainer::from_iter(["xyzzy"])"#, r#"UnknownContainer::new("xyzzy")"#, r#"UnknownContainer::from("xyzzy")"#, ], ); } #[test] fn tuple_combinations() { check_replacements( &parse_quote! { -> (bool, usize) }, &[], &["(true, 0)", "(true, 1)", "(false, 0)", "(false, 1)"], ); } #[test] fn tuple_combination_longer() { check_replacements( &parse_quote! { -> (bool, Option) }, &[], &[ "(true, None)", "(true, Some(String::new()))", r#"(true, Some("xyzzy".into()))"#, "(false, None)", "(false, Some(String::new()))", r#"(false, Some("xyzzy".into()))"#, ], ); } #[test] fn iter_replacement() { check_replacements( &parse_quote! { -> impl Iterator }, &[], &[ "::std::iter::empty()", "::std::iter::once(String::new())", r#"::std::iter::once("xyzzy".into())"#, ], ); } #[test] fn impl_matches_iterator() { assert_eq!( match_impl_iterator(&parse_quote! { impl std::iter::Iterator }), Some(&parse_quote! { String }) ); assert_eq!( match_impl_iterator(&parse_quote! { impl Iterator }), Some(&parse_quote! { String }) ); // Strange, maybe it's a type defined in this crate, but we don't know what to // do with it. assert_eq!(match_impl_iterator(&parse_quote! { impl Iterator }), None); assert_eq!( match_impl_iterator(&parse_quote! { impl Borrow }), None ); } #[test] fn slice_replacement() { check_replacements( &parse_quote! { -> [u8] }, &[], &[ "Vec::leak(Vec::new())", "Vec::leak(vec![0])", "Vec::leak(vec![1])", ], ); } #[test] fn btreemap_replacement() { check_replacements( &parse_quote! { -> BTreeMap }, &[], &[ "BTreeMap::new()", "BTreeMap::from_iter([(String::new(), true)])", "BTreeMap::from_iter([(String::new(), false)])", "BTreeMap::from_iter([(\"xyzzy\".into(), true)])", "BTreeMap::from_iter([(\"xyzzy\".into(), false)])", ], ); } fn check_replacements(return_type: &ReturnType, error_exprs: &[Expr], expected: &[&str]) { assert_eq!( return_type_replacements(return_type, error_exprs) .into_iter() .map(|t| t.to_pretty_string()) .collect_vec(), expected ); } #[test] fn match_map() { assert!(known_map(&parse_quote! { BTreeMap }).is_some()); assert!(known_map(&parse_quote! { HashMap<(usize, usize), bool> }).is_some()); assert!(known_map(&parse_quote! { Option<(usize, usize)> }).is_none()); } } cargo-mutants-25.0.0/src/glob.rs000064400000000000000000000134771046102023000145700ustar 00000000000000// Copyright 2024 Martin Pool //! Build globsets. use std::borrow::Cow; use anyhow::Context; use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; use crate::Result; pub fn build_glob_set(globs: &[S]) -> Result> where S: AsRef, { if globs.is_empty() { return Ok(None); } let mut builder = GlobSetBuilder::new(); for glob_str in globs { let glob_str = glob_str.as_ref(); let match_whole_path = if cfg!(windows) { glob_str.contains(['/', '\\']) } else { glob_str.contains('/') }; let adjusted = if match_whole_path { vec![Cow::Borrowed(glob_str)] } else { vec![ Cow::Owned(format!("**/{glob_str}")), Cow::Owned(format!("**/{glob_str}/**")), ] }; for g in adjusted { builder.add( GlobBuilder::new(&g) .literal_separator(true) // * does not match / .build() .with_context(|| format!("Failed to build glob from {glob_str:?}"))?, ); } } Ok(Some(builder.build().context("Failed to build glob set")?)) } #[cfg(test)] mod test { use super::*; #[test] fn empty_globs() { assert!(build_glob_set(&[] as &[&str]) .expect("build GlobSet") .is_none()); } #[test] fn literal_filename_matches_anywhere() { let set = build_glob_set(&["foo.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("foo.rs")); assert!(set.is_match("src/foo.rs")); assert!(set.is_match("src/bar/foo.rs")); assert!(!set.is_match("src/bar/foo.rs~")); assert!(!set.is_match("src/bar/bar.rs")); } #[test] fn filename_matches_directories_and_their_contents() { let set = build_glob_set(&["console"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("console")); assert!(set.is_match("src/console")); assert!(set.is_match("src/bar/console")); assert!(set.is_match("src/bar/console/mod.rs")); assert!(set.is_match("src/console/ansi.rs")); } #[test] fn glob_without_slash_matches_filename_anywhere() { let set = build_glob_set(&["*.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("foo.rs")); assert!(set.is_match("src/foo.rs")); assert!(set.is_match("src/bar/foo.rs")); assert!(!set.is_match("src/bar/foo.rs~")); assert!(set.is_match("src/bar/bar.rs")); } #[test] fn set_with_multiple_filenames() { let set = build_glob_set(&["foo.rs", "bar.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("foo.rs")); assert!(set.is_match("bar.rs")); assert!(!set.is_match("baz.rs")); } #[test] fn glob_with_slashes_matches_whole_path() { let set = build_glob_set(&["src/*.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(!set.is_match("foo.rs")); assert!(set.is_match("src/foo.rs")); assert!(!set.is_match("src/bar/foo.rs")); assert!(!set.is_match("src/foo.rs~")); assert!( !set.is_match("other/src/bar.rs"), "Glob with slashes anchors to whole path" ); } #[test] fn starstar_at_start_of_path_matches_anywhere() { let set = build_glob_set(&["**/foo.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("foo.rs")); assert!(set.is_match("src/foo.rs")); assert!(set.is_match("src/bar/foo.rs")); assert!(set.is_match("some/other/src/bar/foo.rs")); assert!(!set.is_match("src/bar/foo.rs~")); assert!(!set.is_match("src/bar/bar.rs")); assert!(!set.is_match("foo.rs/bar/bar.rs")); } #[test] fn starstar_within_path_matches_zero_or_more_directories() { let set = build_glob_set(&["src/**/f*.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(!set.is_match("foo.rs"), "path must start with src"); assert!( set.is_match("src/foo.rs"), "starstar can match zero directories" ); assert!(set.is_match("src/bar/foo.rs")); assert!(set.is_match("src/bar/freq.rs")); assert!( !set.is_match("some/other/src/bar/foo.rs"), "path must start with src" ); assert!(!set.is_match("src/bar/foo.rs~")); assert!(!set.is_match("src/bar/bar.rs")); assert!(!set.is_match("foo.rs/bar/bar.rs")); } #[test] #[cfg(unix)] fn on_unix_backslash_is_escape() { // weird glob but ok let set = build_glob_set(&["src\\*.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!( !set.is_match("src/foo.rs"), "backslash is not a path separator on Unix" ); assert!( set.is_match("src*.rs"), "backslash escapes star (and is removed itself)" ); } #[test] #[cfg(windows)] fn on_windows_backslash_is_path_separator() { let set = build_glob_set(&["src\\*.rs"]) .expect("build GlobSet") .expect("GlobSet should not be empty"); assert!(set.is_match("src\\foo.rs")); assert!(!set.is_match("src\\bar\\foo.rs")); assert!(!set.is_match("src\\foo.rs~")); assert!( !set.is_match("other\\src\\bar.rs"), "Glob with slashes anchors to whole path" ); } } cargo-mutants-25.0.0/src/in_diff.rs000064400000000000000000000324601046102023000152340ustar 00000000000000// Copyright 2023 Martin Pool //! Filter mutants to those intersecting a diff on the file tree, //! for example from uncommitted or unmerged changes. use std::collections::HashMap; use std::iter::once; use anyhow::{anyhow, bail}; use camino::Utf8Path; use indoc::formatdoc; use itertools::Itertools; use patch::{Line, Patch}; use tracing::{info, trace, warn}; use crate::mutate::Mutant; use crate::source::SourceFile; use crate::Result; /// Return only mutants to functions whose source was touched by this diff. pub fn diff_filter(mutants: Vec, diff_text: &str) -> Result> { // Strip any "Binary files .. differ" lines because `patch` doesn't understand them at // the moment; this could be removed if it's fixed in that crate. let fixed_diff = diff_text .lines() .filter(|line| !(line.starts_with("Binary files "))) .chain(once("")) .join("\n"); // Flatten the error to a string because otherwise it references the diff, and can't be returned. if fixed_diff.trim().is_empty() { info!("diff file is empty; no mutants will match"); return Ok(Vec::new()); } let patches = Patch::from_multiple(&fixed_diff).map_err(|err| anyhow!("Failed to parse diff: {err}"))?; check_diff_new_text_matches(&patches, &mutants)?; let mut lines_changed_by_path: HashMap<&Utf8Path, Vec> = HashMap::new(); for patch in &patches { let path = strip_patch_path(&patch.new.path); if path == "/dev/null" { // The file was deleted; we can't possibly match anything in it. continue; } if lines_changed_by_path .insert(path, affected_lines(patch)) .is_some() { bail!("Patch input contains repeated filename: {path:?}"); } } let mut matched: Vec = Vec::with_capacity(mutants.len()); 'mutant: for mutant in mutants { let path = mutant.source_file.path(); if let Some(lines_changed) = lines_changed_by_path.get(path) { // We could do be smarter about searching for an intersection of ranges, rather // than probing one line at a time... But, the numbers are likely to be small // enough that this is tolerable... // // We could also search for each unique span in each file, and then include // every mutant that intersects any of those spans, since commonly there will // be multiple mutants in the same function. for line in mutant.span.start.line..=mutant.span.end.line { if lines_changed.binary_search(&line).is_ok() { trace!( ?path, line, mutant = mutant.name(true), "diff matched mutant" ); matched.push(mutant); continue 'mutant; } } } } Ok(matched) } /// Error if the new text from the diffs doesn't match the source files. fn check_diff_new_text_matches(patches: &[Patch], mutants: &[Mutant]) -> Result<()> { let mut source_by_name: HashMap<&Utf8Path, &SourceFile> = HashMap::new(); for mutant in mutants { source_by_name .entry(mutant.source_file.path()) .or_insert_with(|| &mutant.source_file); } for patch in patches { let path = strip_patch_path(&patch.new.path); if let Some(source_file) = source_by_name.get(&path) { let reconstructed = partial_new_file(patch); let lines = source_file.code().lines().collect_vec(); for (lineno, diff_content) in reconstructed { let source_content = lines.get(lineno - 1).unwrap_or(&""); if diff_content != *source_content { warn!( ?path, lineno, ?diff_content, ?source_content, "Diff content doesn't match source file" ); bail!(formatdoc! { "\ Diff content doesn't match source file: {path} line {lineno} diff has: {diff_content:?} source has: {source_content:?} The diff might be out of date with this source tree. "}); } } } } Ok(()) } /// Remove the `b/` prefix commonly found in paths within diffs. fn strip_patch_path(path: &str) -> &Utf8Path { let path = Utf8Path::new(path); path.strip_prefix("b").unwrap_or(path) } /// Given a diff, return the ranges of actually-changed lines, ignoring context lines. /// /// Code that's only included as context doesn't need to be tested. /// /// This returns a list of line numbers that are either added to the new file, or /// adjacent to deletions. /// /// (A list of ranges would be more concise but this is easier for a first version.) /// /// If a line is deleted then the range will span from the line before to the line after. fn affected_lines(patch: &Patch) -> Vec { let mut affected_lines = Vec::new(); for hunk in &patch.hunks { let mut lineno: usize = hunk.new_range.start.try_into().unwrap(); // True if the previous line was deleted. If set, then the next line that exists in the // new file, if there is one, will be marked as affected. let mut prev_removed = false; for line in &hunk.lines { match line { Line::Remove(_) => { prev_removed = true; } Line::Add(_) | Line::Context(_) => { if prev_removed { debug_assert!( affected_lines.last().map_or(true, |last| *last < lineno), "{lineno} {affected_lines:?}" ); debug_assert!(lineno >= 1, "{lineno}"); affected_lines.push(lineno); prev_removed = false; } } } match line { Line::Context(_) => { lineno += 1; } Line::Add(_) => { if affected_lines.last().map_or(true, |last| *last != lineno) { affected_lines.push(lineno); } lineno += 1; } Line::Remove(_) => { if lineno > 1 && affected_lines .last() .map_or(true, |last| *last != (lineno - 1)) { affected_lines.push(lineno - 1); } } } } } debug_assert!( affected_lines.iter().tuple_windows().all(|(a, b)| a < b), "remove_context: line numbers not sorted and unique: {affected_lines:?}" ); affected_lines } /// Recreate a partial view of the new file from a Patch. /// /// This contains lines present as adedd or context. Typically not all context /// will be covered, so the output is a list of line numbers and their text. fn partial_new_file<'d>(patch: &Patch<'d>) -> Vec<(usize, &'d str)> { let mut r: Vec<(usize, &'d str)> = Vec::new(); for hunk in &patch.hunks { let mut lineno: usize = hunk.new_range.start.try_into().unwrap(); for line in &hunk.lines { match line { Line::Context(text) | Line::Add(text) => { debug_assert!(lineno >= 1, "{lineno}"); debug_assert!( r.last().map_or(true, |last| last.0 < lineno), "{lineno} {r:?}" ); r.push((lineno, text)); lineno += 1; } Line::Remove(_) => {} } } debug_assert_eq!( Ok(lineno), (hunk.new_range.start + hunk.new_range.count).try_into(), "Wrong number of resulting lines?" ); } r } #[cfg(test)] mod test_super { use std::fs::read_to_string; use pretty_assertions::assert_eq; use similar::TextDiff; use super::*; #[test] fn patch_parse_error() { let diff = "not really a diff\n"; let err = diff_filter(Vec::new(), diff).unwrap_err(); assert_eq!( err.to_string(), "Failed to parse diff: Line 1: Error while parsing: not really a diff\n" ); } #[test] fn read_diff_with_empty_mutants() { let diff = "\ diff --git a/src/mutate.rs b/src/mutate.rs index eb42779..a0091b7 100644 --- a/src/mutate.rs +++ b/src/mutate.rs @@ -6,9 +6,7 @@ use std::fmt; use std::fs; use std::sync::Arc; use std::foo; -use anyhow::ensure; -use anyhow::Context; -use anyhow::Result; +use anyhow::{ensure, Context, Result}; use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; use similar::TextDiff; "; let filtered: Vec = diff_filter(Vec::new(), diff).expect("diff filtered"); assert_eq!(filtered.len(), 0); } fn make_diff(old: &str, new: &str) -> String { TextDiff::from_lines(old, new) .unified_diff() .context_radius(2) .header("a/file.rs", "b/file.rs") .to_string() } #[test] fn strip_patch_path_prefix() { assert_eq!(strip_patch_path("b/src/mutate.rs"), "src/mutate.rs"); } #[test] fn affected_lines_from_single_insertion() { let orig_lines = (1..=4).map(|i| format!("line {i}\n")).collect_vec(); for i in 1..=5 { let mut new = orig_lines.clone(); let new_value = "new line\n".to_owned(); if i < 5 { new.insert(i - 1, new_value); } else { new.push(new_value); } let diff = make_diff(&orig_lines.join(""), &new.join("")); println!("{diff}"); let patch = Patch::from_single(&diff).unwrap(); let affected = affected_lines(&patch); // When we insert a line then only that one line is affected. assert_eq!(affected, &[i]); } } #[test] fn affected_lines_from_single_deletion() { let orig_lines = (1..=5).map(|i| format!("line {i}\n")).collect_vec(); for i in 1..=5 { let mut new = orig_lines.clone(); new.remove(i - 1); let diff = make_diff(&orig_lines.join(""), &new.join("")); println!("{diff}"); let patch = Patch::from_single(&diff).unwrap(); let affected = affected_lines(&patch); // If line 1 is removed we should see line 1 as affected. If line 2 is removed // then 1 and 2 are affected, etc. If line 5 is removed, then only 4, the last // remaining line is affected. match i { 1 => assert_eq!(affected, &[1]), 5 => assert_eq!(affected, &[4]), i => assert_eq!(affected, &[i - 1, i]), } } } #[test] fn affected_lines_from_double_deletion() { let orig_lines = (1..=5).map(|i| format!("line {i}\n")).collect_vec(); for i in 1..=4 { let mut new = orig_lines.clone(); new.remove(i - 1); new.remove(i - 1); let diff = make_diff(&orig_lines.join(""), &new.join("")); println!("{diff}"); let patch = Patch::from_single(&diff).unwrap(); let affected = affected_lines(&patch); match i { 1 => assert_eq!(affected, &[1]), 4 => assert_eq!(affected, &[3]), 2 | 3 => assert_eq!(affected, &[i - 1, i]), _ => unreachable!(), } } } #[test] fn affected_lines_from_replacement() { let orig_lines = (1..=5).map(|i| format!("line {i}\n")).collect_vec(); for i in 1..=5 { let insertion = ["new 1\n".to_owned(), "new 2\n".to_owned()]; let new = orig_lines[..(i - 1)] .iter() .cloned() .chain(insertion) .chain(orig_lines[i..].iter().cloned()) .collect_vec(); let diff = make_diff(&orig_lines.join(""), &new.join("")); println!("{diff}"); let patch = Patch::from_single(&diff).unwrap(); let affected = affected_lines(&patch); if i > 1 { // The line before the deletion also counts as affected. assert_eq!(affected, &[i - 1, i, i + 1]); } else { assert_eq!(affected, &[i, i + 1]); } } } #[test] fn reconstruct_partial_new_file() { let old = read_to_string("testdata/diff0/src/lib.rs").unwrap(); let new = read_to_string("testdata/diff1/src/lib.rs").unwrap(); let diff = make_diff(&old, &new); let patch = Patch::from_single(&diff).unwrap(); let reconstructed = partial_new_file(&patch); println!("{reconstructed:#?}"); assert_eq!(reconstructed.len(), 16); let new_lines = new.lines().collect_vec(); for (lineno, text) in reconstructed { assert_eq!(text, new_lines[lineno - 1]); } } } cargo-mutants-25.0.0/src/interrupt.rs000064400000000000000000000013751046102023000156730ustar 00000000000000// Copyright 2022 Martin Pool //! Handle ctrl-c by setting a global atomic and checking it from long-running //! operations. use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::anyhow; use tracing::error; use crate::Result; static INTERRUPTED: AtomicBool = AtomicBool::new(false); pub fn install_handler() { ctrlc::set_handler(|| INTERRUPTED.store(true, Ordering::SeqCst)) .expect("install ctrl-c handler"); } /// Return an error if the program was interrupted and should exit. #[mutants::skip] // With this mutated too many of the tests will hang. pub fn check_interrupted() -> Result<()> { if INTERRUPTED.load(Ordering::SeqCst) { error!("interrupted"); Err(anyhow!("interrupted")) } else { Ok(()) } } cargo-mutants-25.0.0/src/lab.rs000064400000000000000000000312731046102023000143750ustar 00000000000000// Copyright 2021-2025 Martin Pool //! Successively apply mutations to the source code and run cargo to check, //! build, and test them. #![warn(clippy::pedantic)] use std::cmp::{max, min}; use std::panic::resume_unwind; use std::sync::{Arc, Mutex}; use std::time::Instant; use std::{thread, vec}; use itertools::Itertools; use tracing::{debug, debug_span, error, trace, warn}; use crate::{ cargo::run_cargo, options::TestPackages, outcome::LabOutcome, output::OutputDir, package::Package, package::PackageSelection, timeouts::Timeouts, workspace::Workspace, BaselineStrategy, BuildDir, Console, Context, Mutant, Options, Phase, Result, Scenario, ScenarioOutcome, }; /// Run all possible mutation experiments. /// /// This is called after all filtering is complete, so all the mutants here will be tested /// or checked. /// /// Before testing the mutants, the lab checks that the source tree passes its tests with no /// mutations applied. pub fn test_mutants( mut mutants: Vec, workspace: &Workspace, output_dir: OutputDir, options: &Options, console: &Console, ) -> Result { let start_time = Instant::now(); console.set_debug_log(output_dir.open_debug_log()?); if options.shuffle { fastrand::shuffle(&mut mutants); } output_dir.write_mutants_list(&mutants)?; console.discovered_mutants(&mutants); if mutants.is_empty() { warn!("No mutants found under the active filters"); return Ok(LabOutcome::default()); } let output_mutex = Mutex::new(output_dir); let baseline_build_dir = BuildDir::for_baseline(workspace, options, console)?; let jobserver = options .jobserver .then(|| { let n_tasks = options.jobserver_tasks.unwrap_or_else(num_cpus::get); debug!(n_tasks, "starting jobserver"); jobserver::Client::new(n_tasks) }) .transpose() .context("Start jobserver")?; let tests_for_mutant = TestsForMutant::new(options, workspace); let lab = Lab { output_mutex, jobserver, tests_for_mutant, options, console, }; let timeouts = match options.baseline { BaselineStrategy::Run => { let outcome = lab.run_baseline(&baseline_build_dir, &mutants)?; if outcome.success() { Timeouts::from_baseline(&outcome, options) } else { error!( "cargo {phase} failed in an unmutated tree, so no mutants were tested", phase = outcome.last_phase(), ); return Ok(lab .output_mutex .into_inner() .expect("lock output_dir") .take_lab_outcome()); } } BaselineStrategy::Skip => Timeouts::without_baseline(options), }; debug!(?timeouts); let build_dir_0 = Mutex::new(Some(baseline_build_dir)); // Create n threads, each dedicated to one build directory. Each of them tries to take a // scenario to test off the queue, and then exits when there are no more left. console.start_testing_mutants(mutants.len()); let n_threads = max(1, min(options.jobs.unwrap_or(1), mutants.len())); let work_queue = &Mutex::new(mutants.into_iter()); thread::scope(|scope| -> crate::Result<()> { let mut threads = Vec::new(); for _i_thread in 0..n_threads { threads.push(scope.spawn(|| -> crate::Result<()> { trace!(thread_id = ?thread::current().id(), "start thread"); // First thread to start can use the baseline's build dir; // others need to copy a new one let build_dir_0 = build_dir_0.lock().expect("lock build dir 0").take(); // separate for lock let build_dir = &if let Some(d) = build_dir_0 { d } else { BuildDir::copy_from(workspace.root(), options, console)? }; lab.run_queue(build_dir, timeouts, work_queue) })); } join_threads(threads) })?; let output_dir = lab .output_mutex .into_inner() .expect("final unlock mutants queue"); console.lab_finished(&output_dir.lab_outcome, start_time, options); let lab_outcome = output_dir.take_lab_outcome(); if lab_outcome.total_mutants == 0 { // This should be unreachable as we also bail out before copying // the tree if no mutants are generated. warn!("No mutants were generated"); } else if lab_outcome.unviable == lab_outcome.total_mutants { warn!("No mutants were viable: perhaps there is a problem with building in a scratch directory. Look in mutants.out/log/* for more information."); } Ok(lab_outcome) } #[mutants::skip] // it's a little hard to observe that the threads were collected? fn join_threads(threads: Vec>>) -> Result<()> { // The errors potentially returned from `join` are a special `std::thread::Result` // that does not implement error, indicating that the thread panicked. // Probably the most useful thing is to `resume_unwind` it. // Inside that, there's an actual Mutants error indicating a non-panic error. // Most likely, this would be "interrupted" but it might be some IO error // etc. In that case, print them all and return the first. let errors = threads .into_iter() .filter_map(|thread| match thread.join() { Err(panic) => resume_unwind(panic), Ok(Ok(())) => None, Ok(Err(err)) => { // To avoid console spam don't print "interrupted" errors for each thread, // since that should have been printed by check_interrupted but do return them. if err.to_string() != "interrupted" { error!("Worker thread failed: {:?}", err); } Some(err) } }) .collect_vec(); if let Some(first_err) = errors.into_iter().next() { Err(first_err) } else { Ok(()) } } /// Common context across all scenarios, threads, and build dirs. struct Lab<'a> { output_mutex: Mutex, jobserver: Option, tests_for_mutant: TestsForMutant, options: &'a Options, console: &'a Console, } impl Lab<'_> { /// Run the baseline scenario, which is the same as running `cargo test` on the unmutated /// tree. /// /// If it fails, return None, indicating that no further testing should be done. /// /// If it succeeds, return the timeouts to be used for the other scenarios. fn run_baseline(&self, build_dir: &BuildDir, mutants: &[Mutant]) -> Result { let all_mutated_packages: Vec> = mutants .iter() .map(|m| Arc::clone(&m.source_file.package)) .sorted_by_key(|p| p.name.clone()) .unique() .collect_vec(); self.make_worker(build_dir).run_one_scenario( &Scenario::Baseline, &PackageSelection::Explicit(all_mutated_packages), Timeouts::for_baseline(self.options), ) } /// Run until the input queue is empty. /// /// The queue, inside a mutex, can be consumed by multiple threads. fn run_queue( &self, build_dir: &BuildDir, timeouts: Timeouts, work_queue: &Mutex>, ) -> Result<()> { self.make_worker(build_dir).run_queue(work_queue, timeouts) } fn make_worker<'a>(&'a self, build_dir: &'a BuildDir) -> Worker<'a> { Worker { build_dir, output_mutex: &self.output_mutex, jobserver: self.jobserver.as_ref(), tests_for_mutant: &self.tests_for_mutant, options: self.options, console: self.console, } } } /// A worker owns one build directory and runs a single thread of testing. /// /// It consumes jobs from an input queue and runs them until the queue is empty, /// appending output to the output directory. struct Worker<'a> { build_dir: &'a BuildDir, output_mutex: &'a Mutex, jobserver: Option<&'a jobserver::Client>, tests_for_mutant: &'a TestsForMutant, options: &'a Options, console: &'a Console, } impl Worker<'_> { /// Run until the input queue is empty. fn run_queue( mut self, work_queue: &Mutex>, timeouts: Timeouts, ) -> Result<()> { let _span = debug_span!("worker thread", build_dir = ?self.build_dir.path()).entered(); loop { // Not a `for` statement so that we don't hold the lock // for the whole iteration. let Some(mutant) = work_queue.lock().expect("Lock pending work queue").next() else { return Ok(()); }; let _span = debug_span!("mutant", name = mutant.name(false)).entered(); let test_packages = match self.tests_for_mutant { TestsForMutant::Workspace => PackageSelection::All, TestsForMutant::Mutated => { PackageSelection::Explicit(vec![mutant.source_file.package.clone()]) } TestsForMutant::Explicit(packages) => PackageSelection::Explicit(packages.clone()), }; self.run_one_scenario(&Scenario::Mutant(mutant), &test_packages, timeouts)?; } } fn run_one_scenario( &mut self, scenario: &Scenario, test_packages: &PackageSelection, timeouts: Timeouts, ) -> Result { let mut scenario_output = self .output_mutex .lock() .expect("lock output_dir to start scenario") .start_scenario(scenario)?; let dir = self.build_dir.path(); self.console .scenario_started(dir, scenario, scenario_output.open_log_read()?); debug!(?test_packages); if let Some(mutant) = scenario.mutant() { let mutated_code = mutant.mutated_code(); let diff = scenario.mutant().unwrap().diff(&mutated_code); scenario_output.write_diff(&diff)?; mutant.apply(self.build_dir, &mutated_code)?; } let mut outcome = ScenarioOutcome::new(&scenario_output, scenario.clone()); for &phase in self.options.phases() { self.console.scenario_phase_started(dir, phase); let timeout = match phase { Phase::Test => timeouts.test, Phase::Build | Phase::Check => timeouts.build, }; match run_cargo( self.build_dir, self.jobserver, test_packages, phase, timeout, &mut scenario_output, self.options, self.console, ) { Ok(phase_result) => { let success = phase_result.is_success(); // so we can move it away outcome.add_phase_result(phase_result); self.console.scenario_phase_finished(dir, phase); if !success { break; } } Err(err) => { error!(?err, ?phase, "scenario execution internal error"); // Some unexpected internal error that stops the program. if let Some(mutant) = scenario.mutant() { mutant.revert(self.build_dir)?; } return Err(err); } } } if let Some(mutant) = scenario.mutant() { mutant.revert(self.build_dir)?; } self.output_mutex .lock() .expect("lock output dir to add outcome") .add_scenario_outcome(&outcome)?; debug!(outcome = ?outcome.summary()); self.console .scenario_finished(dir, scenario, &outcome, self.options); Ok(outcome) } } /// Which packages to test #[derive(Debug, Clone, PartialEq, Eq)] pub enum TestsForMutant { /// Test all packages in the workspace Workspace, /// Test only the package that was mutated Mutated, /// Test specific packages Explicit(Vec>), } impl TestsForMutant { fn new(options: &Options, workspace: &Workspace) -> Self { match options.test_package { TestPackages::Workspace => TestsForMutant::Workspace, TestPackages::Mutated => TestsForMutant::Mutated, TestPackages::Named(ref package_names) => { TestsForMutant::Explicit(workspace.packages_by_name(package_names)) } } } } cargo-mutants-25.0.0/src/list.rs000064400000000000000000000050751046102023000146130ustar 00000000000000// Copyright 2023-2024 Martin Pool //! List mutants and files as text or json. #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] use itertools::Itertools; use serde_json::{json, Value}; use crate::mutate::Mutant; use crate::path::Utf8PathSlashes; use crate::source::SourceFile; use crate::Options; /// Return a string representation of a list of mutants. /// /// The format is controlled by the `emit_json`, `emit_diffs`, `show_line_col`, and `colors` options. pub fn list_mutants(mutants: &[Mutant], options: &Options) -> String { if options.emit_json { // Panic: only if we created illegal json, which would be a bug. let mut list: Vec = Vec::new(); for mutant in mutants { let mut obj = serde_json::to_value(mutant).expect("Serialize mutant"); if options.emit_diffs { obj.as_object_mut().unwrap().insert( "diff".to_owned(), json!(mutant.diff(&mutant.mutated_code())), ); } list.push(obj); } serde_json::to_string_pretty(&list).expect("Serialize mutants") } else { // TODO: Do we need to check this? Could the console library strip them if they're not // supported? let colors = options.colors.active_stdout(); let mut out = String::with_capacity(200 * mutants.len()); for mutant in mutants { if colors { out.push_str(&mutant.to_styled_string(options.show_line_col)); } else { out.push_str(&mutant.name(options.show_line_col)); } out.push('\n'); if options.emit_diffs { out.push_str(&mutant.diff(&mutant.mutated_code())); out.push('\n'); } } out } } /// List the source files as json or text. pub fn list_files(source_files: &[SourceFile], options: &Options) -> String { if options.emit_json { let json_list = Value::Array( source_files .iter() .map(|source_file| { json!({ "path": source_file.tree_relative_path.to_slash_path(), "package": source_file.package.name, }) }) .collect(), ); serde_json::to_string_pretty(&json_list).expect("Serialize source files") } else { source_files .iter() .map(|file| file.tree_relative_path.to_slash_path() + "\n") .join("") } } cargo-mutants-25.0.0/src/main.rs000064400000000000000000000433721046102023000145660ustar 00000000000000// Copyright 2021-2024 Martin Pool //! `cargo-mutants`: Find test gaps by inserting bugs. //! //! See for the manual and more information. #![warn(clippy::pedantic)] #![allow(clippy::module_name_repetitions, clippy::needless_raw_string_hashes)] mod build_dir; mod cargo; mod config; mod console; mod copy_tree; mod exit_code; mod fnvalue; mod glob; mod in_diff; mod interrupt; mod lab; mod list; mod manifest; mod mutate; mod options; mod outcome; mod output; mod package; mod path; mod pretty; mod process; mod scenario; mod shard; mod source; mod span; mod tail_file; #[cfg(test)] #[path = "../tests/util/mod.rs"] mod test_util; mod timeouts; mod visit; mod workspace; use std::env; use std::fs::read_to_string; use std::io; use std::process::exit; use anyhow::{anyhow, ensure, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use clap::builder::styling::{self}; use clap::builder::Styles; use clap::{ArgAction, CommandFactory, Parser, ValueEnum}; use clap_complete::{generate, Shell}; use color_print::cstr; use console::enable_console_colors; use output::{load_previously_caught, OutputDir}; use tracing::{debug, info}; use crate::build_dir::BuildDir; use crate::console::Console; use crate::in_diff::diff_filter; use crate::interrupt::check_interrupted; use crate::lab::test_mutants; use crate::list::{list_files, list_mutants}; use crate::mutate::{Genre, Mutant}; use crate::options::{Colors, Options, TestTool}; use crate::outcome::{Phase, ScenarioOutcome}; use crate::scenario::Scenario; use crate::shard::Shard; use crate::workspace::{PackageFilter, Workspace}; const VERSION: &str = env!("CARGO_PKG_VERSION"); const NAME: &str = env!("CARGO_PKG_NAME"); /// A comment marker inserted next to changes, so they can be easily found. static MUTATION_MARKER_COMMENT: &str = "/* ~ changed by cargo-mutants ~ */"; static SPONSOR_MESSAGE: &str = cstr!("Support and accelerate cargo-mutants at <>"); #[mutants::skip] // only visual effects, not worth testing fn clap_styles() -> Styles { styling::Styles::styled() .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD) .literal(styling::AnsiColor::Blue.on_default() | styling::Effects::BOLD) .placeholder(styling::AnsiColor::Cyan.on_default()) } #[derive(Parser)] #[command(name = "cargo", bin_name = "cargo", styles(clap_styles()))] enum Cargo { #[command(name = "mutants", styles(clap_styles()))] Mutants(Args), } #[derive(Debug, Default, ValueEnum, Clone, Copy, Eq, PartialEq)] pub enum BaselineStrategy { /// Run tests in an unmutated tree before testing mutants. #[default] Run, /// Don't run tests in an unmutated tree: assume that they pass. Skip, } /// Find inadequately-tested code that can be removed without any tests failing. /// /// See for more information. #[allow(clippy::struct_excessive_bools)] #[derive(Parser, PartialEq, Debug)] #[command( author, about, after_help = SPONSOR_MESSAGE, )] pub struct Args { /// Show cargo output for all invocations (very verbose). #[arg(long, help_heading = "Output")] all_logs: bool, /// Baseline strategy: check that tests pass in an unmutated tree before testing mutants. #[arg(long, value_enum, default_value_t = BaselineStrategy::Run, help_heading = "Execution")] baseline: BaselineStrategy, /// Turn off all rustc lints, so that denied warnings won't make mutants unviable. #[arg(long, action = ArgAction::Set, help_heading = "Build")] cap_lints: Option, /// Print mutants that were caught by tests. #[arg(long, short = 'v', help_heading = "Output")] caught: bool, /// Cargo check generated mutants, but don't run tests. #[arg(long, help_heading = "Execution")] check: bool, /// Draw colors in output. #[arg( long, value_enum, help_heading = "Output", default_value_t, env = "CARGO_TERM_COLOR" )] colors: Colors, /// Copy `.git` and other VCS directories to the build directory. /// /// This is useful if you have tests that depend on the presence of these directories. /// /// Known VCS directories are /// `.git`, `.hg`, `.bzr`, `.svn`, `_darcs`, `.pijul`. #[arg(long, help_heading = "Copying", visible_alias = "copy_git")] copy_vcs: Option, /// Show the mutation diffs. #[arg(long, help_heading = "Filters")] diff: bool, /// Rust crate directory to examine. #[arg( long, short = 'd', conflicts_with = "manifest_path", help_heading = "Input" )] dir: Option, /// Generate autocompletions for the given shell. #[arg(long)] completions: Option, /// Return this error values from functions returning Result: /// for example, `::anyhow::anyhow!("mutated")`. #[arg(long, help_heading = "Generate")] error: Vec, /// Regex for mutations to examine, matched against the names shown by `--list`. #[arg( long = "re", short = 'F', alias = "regex", alias = "examine-regex", alias = "examine-re", help_heading = "Filters" )] examine_re: Vec, /// Glob for files to exclude; with no glob, all files are included; globs containing /// slash match the entire path. If used together with `--file` argument, then the files to be examined are matched before the files to be excluded. #[arg(long, short = 'e', help_heading = "Filters")] exclude: Vec, /// Regex for mutations to exclude, matched against the names shown by `--list`. #[arg(long, short = 'E', alias = "exclude-regex", help_heading = "Filters")] exclude_re: Vec, /// Glob for files to examine; with no glob, all files are examined; globs containing /// slash match the entire path. If used together with `--exclude` argument, then the files to be examined are matched before the files to be excluded. #[arg(long, short = 'f', help_heading = "Filters")] file: Vec, /// Don't copy files matching gitignore patterns. #[arg(long, action = ArgAction::Set, default_value = "true", help_heading = "Copying", group = "copy_opts")] gitignore: bool, /// Test mutations in the source tree, rather than in a copy. #[arg( long, help_heading = "Copying", conflicts_with = "jobs", conflicts_with = "copy_opts" )] in_place: bool, /// Skip mutants that were caught in previous runs. #[arg(long, help_heading = "Filters")] iterate: bool, /// Run this many cargo build/test jobs in parallel. #[arg( long, short = 'j', env = "CARGO_MUTANTS_JOBS", help_heading = "Execution" )] jobs: Option, /// Use a GNU Jobserver to cap concurrency between child processes. #[arg(long, action = ArgAction::Set, help_heading = "Execution", default_value_t = true)] jobserver: bool, /// Allow this many jobserver tasks in parallel, across all child processes. /// /// By default, NCPUS. #[arg(long, help_heading = "Execution")] jobserver_tasks: Option, /// Output json (only for --list). #[arg(long, help_heading = "Output")] json: bool, /// Don't delete the scratch directories, for debugging. #[arg(long, help_heading = "Debug")] leak_dirs: bool, /// Log level for stdout (trace, debug, info, warn, error). #[arg( long, short = 'L', default_value = "info", env = "CARGO_MUTANTS_TRACE_LEVEL", help_heading = "Debug" )] level: tracing::Level, /// Just list possible mutants, don't run them. #[arg(long, help_heading = "Execution")] list: bool, /// List source files, don't run anything. #[arg(long, help_heading = "Execution")] list_files: bool, /// Path to Cargo.toml for the package to mutate. #[arg(long, help_heading = "Input")] manifest_path: Option, /// Don't read .cargo/mutants.toml. #[arg(long, help_heading = "Input")] no_config: bool, /// Don't copy the /target directory, and don't build the source tree first. #[arg(long, help_heading = "Copying", group = "copy_opts")] no_copy_target: bool, /// Don't print times or tree sizes, to make output deterministic. #[arg(long, help_heading = "Output")] no_times: bool, /// Include line & column numbers in the mutation list. #[arg(long, action = ArgAction::Set, default_value = "true", help_heading = "Output")] line_col: bool, /// Create mutants.out within this directory. #[arg( long, short = 'o', env = "CARGO_MUTANTS_OUTPUT", help_heading = "Output" )] output: Option, /// Include only mutants in code touched by this diff. #[arg(long, short = 'D', help_heading = "Filters")] in_diff: Option, /// Minimum timeout for tests, in seconds, as a lower bound on the auto-set time. #[arg( long, env = "CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT", help_heading = "Execution" )] minimum_test_timeout: Option, /// Only test mutants from these packages. #[arg(id = "package", long, short = 'p', help_heading = "Filters")] mutate_packages: Vec, /// Run mutants in random order. #[arg(long, help_heading = "Execution")] shuffle: bool, /// Run mutants in the fixed order they occur in the source tree. #[arg(long, help_heading = "Execution")] no_shuffle: bool, /// Build with this cargo profile. #[arg(long, help_heading = "Build")] profile: Option, /// Run only one shard of all generated mutants: specify as e.g. 1/4. #[arg(long, help_heading = "Execution")] shard: Option, /// Skip calls to functions and methods named in this list. /// /// The list may contain comma-separated names and may be repeated. /// /// If a qualified path is given in the source then this matches only the final component, /// and it ignores type parameters. /// /// This value is combined with the names from the config `skip_calls` key. #[arg(long, help_heading = "Filters")] skip_calls: Vec, /// Use built-in defaults for `skip_calls`, in addition to any explicit values. /// /// The default is `with_capacity`. #[arg(long)] skip_calls_defaults: Option, /// Run tests from these packages for all mutants. #[arg(long, help_heading = "Tests")] test_package: Vec, /// Tool used to run test suites: cargo or nextest. #[arg(long, help_heading = "Execution")] test_tool: Option, /// Run all tests in the workspace. /// /// If false, only the tests in the mutated package are run. /// /// Overrides `--test_package`. #[arg(long, help_heading = "Tests")] test_workspace: Option, /// Maximum run time for all cargo commands, in seconds. #[arg(long, short = 't', help_heading = "Execution")] timeout: Option, /// Test timeout multiplier (relative to base test time). #[arg(long, help_heading = "Execution", conflicts_with = "timeout")] timeout_multiplier: Option, /// Maximum run time for cargo build command, in seconds. #[arg(long, help_heading = "Execution")] build_timeout: Option, /// Build timeout multiplier (relative to base build time). #[arg(long, help_heading = "Execution", conflicts_with = "build_timeout")] build_timeout_multiplier: Option, /// Print mutations that failed to check or build. #[arg(long, short = 'V', help_heading = "Output")] unviable: bool, /// Show version and quit. #[arg(long, action = clap::ArgAction::SetTrue)] version: bool, /// Generate mutations in every package in the workspace. #[arg(long, help_heading = "Filters")] workspace: bool, /// Additional args for all cargo invocations. #[arg( long, short = 'C', allow_hyphen_values = true, help_heading = "Execution" )] cargo_arg: Vec, /// Pass remaining arguments to cargo test after all options and after `--`. #[arg(last = true, help_heading = "Execution")] cargo_test_args: Vec, #[command(flatten)] features: Features, } #[derive(clap::Args, PartialEq, Eq, Debug, Default, Clone)] pub struct Features { //--- features /// Space or comma separated list of features to activate. // (The features are not split or parsed, just passed through to Cargo.) #[arg(long, help_heading = "Feature Selection")] pub features: Vec, /// Do not activate the `default` feature. #[arg(long, help_heading = "Feature Selection")] pub no_default_features: bool, /// Activate all features. // (This does not conflict because this only turns on features in the top level package, // and you might use --features to turn on features in dependencies.) #[arg(long, help_heading = "Feature Selection")] pub all_features: bool, } fn main() -> Result<()> { let args = match Cargo::try_parse() { Ok(Cargo::Mutants(args)) => args, Err(e) => { e.print().expect("Failed to show clap error message"); // Clap by default exits with code 2. let code = match e.exit_code() { 2 => exit_code::USAGE, 0 => 0, _ => exit_code::SOFTWARE, }; exit(code); } }; if args.version { println!("{NAME} {VERSION}"); return Ok(()); } else if let Some(shell) = args.completions { generate(shell, &mut Cargo::command(), "cargo", &mut io::stdout()); return Ok(()); } let console = Console::new(); console.setup_global_trace(args.level, args.colors); // We don't have Options yet. enable_console_colors(args.colors); interrupt::install_handler(); let start_dir: &Utf8Path = if let Some(manifest_path) = &args.manifest_path { ensure!(manifest_path.is_file(), "Manifest path is not a file"); manifest_path .parent() .ok_or(anyhow!("Manifest path has no parent"))? } else if let Some(dir) = &args.dir { dir } else { Utf8Path::new(".") }; let workspace = Workspace::open(start_dir)?; let config = if args.no_config { config::Config::default() } else { config::Config::read_tree_config(workspace.root())? }; debug!(?config); debug!(?args.features); let options = Options::new(&args, &config)?; debug!(?options); let package_filter = if !args.mutate_packages.is_empty() { PackageFilter::explicit(&args.mutate_packages) } else if args.workspace { PackageFilter::All } else { PackageFilter::Auto(start_dir.to_owned()) }; let output_parent_dir = options .output_in_dir .clone() .unwrap_or_else(|| workspace.root().to_owned()); let mut discovered = workspace.discover(&package_filter, &options, &console)?; let previously_caught = if args.iterate { let previously_caught = load_previously_caught(&output_parent_dir)?; info!( "Iteration excludes {} previously caught or unviable mutants", previously_caught.len() ); discovered.remove_previously_caught(&previously_caught); Some(previously_caught) } else { None }; console.clear(); if args.list_files { print!("{}", list_files(&discovered.files, &options)); return Ok(()); } let mut mutants = discovered.mutants; if let Some(in_diff) = &args.in_diff { mutants = diff_filter( mutants, &read_to_string(in_diff).context("Failed to read filter diff")?, )?; } if let Some(shard) = &args.shard { mutants = shard.select(mutants); } if args.list { print!("{}", list_mutants(&mutants, &options)); } else { let output_dir = OutputDir::new(&output_parent_dir)?; if let Some(previously_caught) = previously_caught { output_dir.write_previously_caught(&previously_caught)?; } console.set_debug_log(output_dir.open_debug_log()?); let lab_outcome = test_mutants(mutants, &workspace, output_dir, &options, &console)?; exit(lab_outcome.exit_code()); } Ok(()) } #[cfg(test)] mod test { use clap::CommandFactory; #[test] fn option_help_sentence_case_without_period() { let args = super::Args::command(); let mut problems = Vec::new(); for arg in args.get_arguments() { if let Some(help) = arg.get_help().map(ToString::to_string) { if !help.starts_with(char::is_uppercase) { problems.push(format!( "Help for {:?} does not start with a capital letter: {:?}", arg.get_id(), help )); } // Clap seems to automatically strip periods from the end of help text in docstrings, // but let's leave this here just in case. if help.ends_with('.') { problems.push(format!( "Help for {:?} ends with a period: {:?}", arg.get_id(), help )); } if help.is_empty() { problems.push(format!("Help for {:?} is empty", arg.get_id())); } } else { problems.push(format!("No help for {:?}", arg.get_id())); } } for problem in &problems { eprintln!("{problem}"); } assert!(problems.is_empty(), "Problems with help text"); } } cargo-mutants-25.0.0/src/manifest.rs000064400000000000000000000277561046102023000154600ustar 00000000000000// Copyright 2022-2024 Martin Pool. //! Manipulate Cargo manifest and config files. //! //! In particular, when the tree is copied we have to fix up relative paths, so //! that they still work from the new location of the scratch directory. use std::fs::{read_to_string, write}; use anyhow::Context; use camino::Utf8Path; use tracing::debug; use crate::path::ascent; use crate::Result; /// Rewrite the scratch copy of a manifest to have absolute paths. /// /// `manifest_source_dir` is the directory originally containing the manifest, from /// which the absolute paths are calculated. #[allow(clippy::module_name_repetitions)] pub fn fix_manifest(manifest_scratch_path: &Utf8Path, source_dir: &Utf8Path) -> Result<()> { let toml_str = read_to_string(manifest_scratch_path).with_context(|| { format!("failed to read manifest from build directory: {manifest_scratch_path}") })?; if let Some(changed_toml) = fix_manifest_toml(&toml_str, source_dir)? { let toml_str = toml::to_string_pretty(&changed_toml).context("serialize changed manifest")?; write(manifest_scratch_path, toml_str.as_bytes()).with_context(|| { format!("Failed to write fixed manifest to {manifest_scratch_path}") })?; } Ok(()) } /// Fix any relative paths within a Cargo.toml manifest. /// /// Returns the new manifest, or None if no changes were made. fn fix_manifest_toml( manifest_toml: &str, manifest_source_dir: &Utf8Path, ) -> Result> { let mut value: toml::Value = manifest_toml.parse().context("parse manifest")?; let orig_value = value.clone(); if let Some(top_table) = value.as_table_mut() { if let Some(dependencies) = top_table.get_mut("dependencies") { fix_dependency_table(dependencies, manifest_source_dir); } if let Some(replace) = top_table.get_mut("replace") { // The replace section is a table from package name/version to a // table which might include a `path` key. (The keys are not exactly // package names but it doesn't matter.) // fix_dependency_table(replace, manifest_source_dir); } if let Some(patch_table) = top_table.get_mut("patch").and_then(|p| p.as_table_mut()) { // The keys of the patch table are registry names or source URLs; // the values are like dependency tables. // for (_name, dependencies) in patch_table { fix_dependency_table(dependencies, manifest_source_dir); } } } if value == orig_value { Ok(None) } else { Ok(Some(value)) } } /// Fix up paths in a manifest "dependency table". /// /// This is a pattern that can occur at various places in the manifest. It's a /// map from a string (such as a package name) to a table which may contain a "path" field. /// /// For example: /// /// ```yaml /// mutants = { version = "1.0", path = "../mutants" } /// ``` /// /// The table is mutated if necessary. /// /// `dependencies` is a TOML Value that should normally be a table; /// other values are left unchanged. /// /// Entries that have no `path` are left unchanged too. fn fix_dependency_table(dependencies: &mut toml::Value, manifest_source_dir: &Utf8Path) { if let Some(dependencies_table) = dependencies.as_table_mut() { for (_, value) in dependencies_table.iter_mut() { if let Some(dependency_table) = value.as_table_mut() { if let Some(path_value) = dependency_table.get_mut("path") { if let Some(path_str) = path_value.as_str() { if let Some(new_path) = fix_path(path_str, manifest_source_dir) { *path_value = toml::Value::String(new_path); } } } } } } } /// Rewrite relative paths within `.cargo/config.toml` to be absolute paths. pub fn fix_cargo_config(build_path: &Utf8Path, source_path: &Utf8Path) -> Result<()> { let config_path = build_path.join(".cargo/config.toml"); if config_path.exists() { let toml_str = read_to_string(&config_path).context("read .cargo/config.toml")?; if let Some(changed_toml) = fix_cargo_config_toml(&toml_str, source_path)? { write(build_path.join(&config_path), changed_toml.as_bytes()) .context("write .cargo/config.toml")?; } } Ok(()) } /// Replace any relative paths in a config file with absolute paths. /// /// Returns None if no changes are needed. /// /// See . fn fix_cargo_config_toml(config_toml: &str, source_dir: &Utf8Path) -> Result> { let mut value: toml::Value = config_toml.parse().context("parse config.toml")?; let mut changed = false; if let Some(paths) = value.get_mut("paths").and_then(|p| p.as_array_mut()) { for path_value in paths { if let Some(path_str) = path_value.as_str() { if let Some(new_path) = fix_path(path_str, source_dir) { *path_value = toml::Value::String(new_path); changed = true; } } } } if changed { Ok(Some(toml::to_string_pretty(&value)?)) } else { Ok(None) } } /// Fix one path, from inside a scratch tree, to be absolute as interpreted /// relative to the source tree. /// /// Paths pointing into a subdirectory of the source tree are left unchanged. /// /// Returns None if the path does not need to be changed. fn fix_path(path_str: &str, source_dir: &Utf8Path) -> Option { let path = Utf8Path::new(path_str); if path.is_absolute() || ascent(path) == 0 { None } else { let mut new_path = source_dir.to_owned(); new_path.push(path); let new_path_str = new_path.to_string(); debug!("fix path {path_str} -> {new_path_str}"); Some(new_path_str) } } #[cfg(test)] mod test { use camino::{Utf8Path, Utf8PathBuf}; use indoc::indoc; use pretty_assertions::assert_eq; use toml::Table; use super::{fix_cargo_config_toml, fix_manifest_toml}; #[test] fn fix_path_absolute_unchanged() { let dependency_abspath = Utf8Path::new("testdata/dependency") .canonicalize_utf8() .unwrap(); assert_eq!( super::fix_path( dependency_abspath.as_str(), Utf8Path::new("/home/user/src/foo") ), None ); } #[test] fn fix_path_relative() { let fixed_path: Utf8PathBuf = super::fix_path( "../dependency", Utf8Path::new("testdata/relative_dependency"), ) .expect("path was adjusted") .into(); assert_eq!( &fixed_path, Utf8Path::new("testdata/relative_dependency/../dependency"), ); } #[test] fn fix_relative_path_in_manifest() { let manifest_toml = indoc! { r#" # A comment, which will be dropped. author = "A Smithee" [dependencies] wibble = { path = "../wibble" } # Use the relative path to the dependency. "# }; let orig_path = Utf8Path::new("/home/user/src/foo"); let fixed = fix_manifest_toml(manifest_toml, orig_path) .unwrap() .expect("toml was modified"); println!("{fixed:#?}"); assert_eq!(fixed["author"].as_str().unwrap(), "A Smithee"); assert_eq!( fixed["dependencies"]["wibble"]["path"].as_str().unwrap(), Utf8Path::new("/home/user/src/foo/../wibble") ); } #[test] fn fix_replace_section() { let manifest_toml = indoc! { r#" [dependencies] wibble = "1.2.3" [replace] "wibble:1.2.3" = { path = "../wibble" } # Use the relative path to the dependency. "# }; let orig_path = Utf8Path::new("/home/user/src/foo"); let fixed = fix_manifest_toml(manifest_toml, orig_path) .unwrap() .expect("toml was modified"); println!("fixed toml:\n{}", toml::to_string_pretty(&fixed).unwrap()); assert_eq!(fixed["dependencies"]["wibble"].as_str().unwrap(), "1.2.3"); assert_eq!( fixed["replace"]["wibble:1.2.3"]["path"].as_str().unwrap(), orig_path.join("../wibble") ); } #[test] fn absolute_path_in_manifest_is_unchanged() { #[cfg(unix)] let manifest_toml = indoc! { r#" [dependencies] wibble = { path = "/home/asmithee/src/wibble" } "# }; #[cfg(windows)] let manifest_toml = indoc! { r#" [dependencies] wibble = { path = "c:/home/asmithee/src/wibble" } "# }; let orig_path = Utf8Path::new("/home/user/src/foo"); let fixed_toml = fix_manifest_toml(manifest_toml, orig_path).unwrap(); assert_eq!( fixed_toml, None, "manifest containing only an absolute path should not be modified" ); } #[test] fn subdir_path_in_manifest_is_unchanged() { let manifest_toml = indoc! { r#" [dependencies] wibble = { path = "wibble" } "# }; let orig_path = Utf8Path::new("/home/user/src/foo"); let fixed_toml = fix_manifest_toml(manifest_toml, orig_path).unwrap(); assert_eq!( fixed_toml, None, "manifest with a relative path to a subdirectory should not be modified", ); } #[test] fn fix_patch_section() { let manifest_toml = indoc! { r#" [dependencies] wibble = "1.2.3" [patch.crates-io] wibble = { path = "../wibble" } # Use the relative path to the dependency. "# }; let orig_path = Utf8Path::new("/home/user/src/foo"); let fixed = fix_manifest_toml(manifest_toml, orig_path) .unwrap() .expect("toml was modified"); println!("{fixed:#?}"); assert_eq!(fixed["dependencies"]["wibble"].as_str(), Some("1.2.3")); assert_eq!( fixed["patch"]["crates-io"]["wibble"]["path"] .as_str() .unwrap(), orig_path.join("../wibble") ); } #[test] fn cargo_config_toml_paths_outside_tree_are_made_absolute() { // To avoid test flakiness due to TOML stylistic changes, we compare the // TOML values. // // And, to avoid headaches about forward and backslashes on Windows, // compare path objects. let cargo_config_toml = indoc! { r#" paths = [ "sub_dependency", "../sibling_dependency", "../../parent_dependency", "/Users/jane/src/absolute_dependency", "/src/other", ]"# }; let source_dir = Utf8Path::new("/Users/jane/src/foo"); let fixed_toml = fix_cargo_config_toml(cargo_config_toml, source_dir) .unwrap() .expect("toml was modified"); println!("fixed toml:\n{fixed_toml}"); // TODO: Maybe fix_cargo_config_toml should return the Value. let fixed_table: Table = fixed_toml.parse::().unwrap(); let fixed_paths = fixed_table["paths"] .as_array() .unwrap() .iter() .map(|val| val.as_str().unwrap().into()) .collect::>(); assert_eq!( fixed_paths, [ Utf8Path::new("sub_dependency"), &source_dir.join("../sibling_dependency"), &source_dir.join("../../parent_dependency"), &source_dir.parent().unwrap().join("absolute_dependency"), Utf8Path::new("/src/other"), ] ); } } cargo-mutants-25.0.0/src/mutate.rs000064400000000000000000000336361046102023000151430ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Mutations to source files, and inference of interesting mutations to apply. use std::fmt; use std::sync::Arc; use anyhow::Result; use console::{style, StyledObject}; use serde::ser::{SerializeStruct, Serializer}; use serde::Serialize; use similar::TextDiff; use tracing::trace; use crate::build_dir::BuildDir; use crate::output::clean_filename; use crate::source::SourceFile; use crate::span::Span; use crate::MUTATION_MARKER_COMMENT; /// Various broad categories of mutants. #[derive(Clone, Eq, PartialEq, Debug, Serialize)] pub enum Genre { /// Replace the body of a function with a fixed value. FnValue, /// Replace `==` with `!=` and so on. BinaryOperator, UnaryOperator, } /// A mutation applied to source code. #[derive(Clone, Eq, PartialEq)] pub struct Mutant { /// Which file is being mutated. pub source_file: SourceFile, /// The function that's being mutated: the nearest enclosing function, if they are nested. /// /// There may be none for mutants in e.g. top-level const expressions. pub function: Option>, /// The mutated textual region. /// /// This is deleted and replaced with the replacement text. pub span: Span, /// The replacement text. pub replacement: String, /// What general category of mutant this is. pub genre: Genre, } /// The function containing a mutant. /// /// This is used for both mutations of the whole function, and smaller mutations within it. #[derive(Eq, PartialEq, Debug, Serialize)] pub struct Function { /// The function that's being mutated, including any containing namespaces. #[allow(clippy::struct_field_names)] pub function_name: String, /// The return type of the function, including a leading "-> ", as a fragment of Rust syntax. /// /// Empty if the function has no return type (i.e. returns `()`). pub return_type: String, /// The span (line/column range) of the entire function. pub span: Span, } impl Mutant { /// Return text of the whole file with the mutation applied. pub fn mutated_code(&self) -> String { self.span.replace( self.source_file.code(), &format!("{} {}", &self.replacement, MUTATION_MARKER_COMMENT), ) } /// Describe the mutant briefly, not including the location. /// /// The result is like `replace factorial -> u32 with Default::default()`. pub fn describe_change(&self) -> String { self.styled_parts() .into_iter() .map(|x| x.force_styling(false).to_string()) .collect::() } pub fn name(&self, show_line_col: bool) -> String { let mut v = Vec::new(); v.push(self.source_file.tree_relative_slashes()); if show_line_col { v.push(format!( ":{}:{}: ", self.span.start.line, self.span.start.column )); } else { v.push(": ".to_owned()); } v.extend( self.styled_parts() .into_iter() .map(|x| x.force_styling(false).to_string()), ); v.join("") } /// Return a one-line description of this mutant, with coloring, including the file names /// and optionally the line and column. pub fn to_styled_string(&self, show_line_col: bool) -> String { let mut v = Vec::new(); v.push(self.source_file.tree_relative_slashes()); if show_line_col { v.push(format!( ":{}:{}", self.span.start.line, self.span.start.column )); } v.push(": ".to_owned()); v.extend(self.styled_parts().into_iter().map(|x| x.to_string())); v.join("") } fn styled_parts(&self) -> Vec> { // This is like `impl Display for Mutant`, but with colors. // The text content should be the same. #[allow(clippy::needless_pass_by_value)] // actually is needed for String vs &str? fn s(s: S) -> StyledObject { style(s.to_string()) } let mut v: Vec> = Vec::new(); if self.genre == Genre::FnValue { v.push(s("replace ")); let function = self .function .as_ref() .expect("FnValue mutant should have a function"); v.push(s(&function.function_name).bright().magenta()); if !function.return_type.is_empty() { v.push(s(" ")); v.push(s(&function.return_type).magenta()); } v.push(s(" with ")); v.push(s(self.replacement_text()).yellow()); } else { if self.replacement.is_empty() { v.push(s("delete ")); } else { v.push(s("replace ")); } v.push(s(self.original_text()).yellow()); if !self.replacement.is_empty() { v.push(s(" with ")); v.push(s(&self.replacement).bright().yellow()); } if let Some(function) = &self.function { v.push(s(" in ")); v.push(s(&function.function_name).bright().magenta()); } } v } pub fn original_text(&self) -> String { self.span.extract(self.source_file.code()) } /// Return the text inserted for this mutation. pub fn replacement_text(&self) -> &str { self.replacement.as_str() } /// Return a unified diff for the mutant. /// /// The mutated text must be passed in because we should have already computed /// it, and don't want to pointlessly recompute it here. pub fn diff(&self, mutated_code: &str) -> String { let old_label = self.source_file.tree_relative_slashes(); // There shouldn't be any newlines, but just in case... let new_label = self.describe_change().replace('\n', " "); TextDiff::from_lines(self.source_file.code(), mutated_code) .unified_diff() .context_radius(8) .header(&old_label, &new_label) .to_string() } /// Apply this mutant to the relevant file within a `BuildDir`. pub fn apply(&self, build_dir: &BuildDir, mutated_code: &str) -> Result<()> { trace!(?self, "Apply mutant"); build_dir.overwrite_file(&self.source_file.tree_relative_path, mutated_code) } pub fn revert(&self, build_dir: &BuildDir) -> Result<()> { trace!(?self, "Revert mutant"); build_dir.overwrite_file( &self.source_file.tree_relative_path, self.source_file.code(), ) } /// Return a string describing this mutant that's suitable for building a log file name, /// but can contain slashes. pub fn log_file_name_base(&self) -> String { // TODO: Also include a unique number so that they can't collide, even // with similar mutants on the same line? format!( "{filename}_line_{line}_col_{col}", filename = clean_filename(&self.source_file.tree_relative_slashes()), line = self.span.start.line, col = self.span.start.column, ) } } impl fmt::Debug for Mutant { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Custom implementation to show spans more concisely f.debug_struct("Mutant") .field("function", &self.function) .field("replacement", &self.replacement) .field("genre", &self.genre) .field("span", &self.span) .field("package_name", &self.source_file.package.name) .finish() } } impl Serialize for Mutant { fn serialize(&self, serializer: S) -> Result where S: Serializer, { // custom serialize to omit inessential info let mut ss = serializer.serialize_struct("Mutant", 7)?; ss.serialize_field("package", &self.source_file.package.name)?; ss.serialize_field("file", &self.source_file.tree_relative_slashes())?; ss.serialize_field("function", &self.function.as_ref().map(Arc::as_ref))?; ss.serialize_field("span", &self.span)?; ss.serialize_field("replacement", &self.replacement)?; ss.serialize_field("genre", &self.genre)?; ss.end() } } #[cfg(test)] mod test { use indoc::indoc; use itertools::Itertools; use pretty_assertions::assert_eq; use crate::test_util::copy_of_testdata; use crate::visit::mutate_source_str; use crate::*; #[test] fn discover_factorial_mutants() { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(tmp.path()).unwrap(); let options = Options::default(); let mutants = workspace .discover(&PackageFilter::All, &options, &Console::new()) .unwrap() .mutants; assert_eq!(mutants.len(), 5); assert_eq!( format!("{:#?}", mutants[0]), indoc! { r#"Mutant { function: Some( Function { function_name: "main", return_type: "", span: Span(1, 1, 5, 2), }, ), replacement: "()", genre: FnValue, span: Span(2, 5, 4, 6), package_name: "cargo-mutants-testdata-factorial", }"# } ); assert_eq!( mutants[0].name(true), "src/bin/factorial.rs:2:5: replace main with ()" ); assert_eq!( format!("{:#?}", mutants[1]), indoc! { r#" Mutant { function: Some( Function { function_name: "factorial", return_type: "-> u32", span: Span(7, 1, 13, 2), }, ), replacement: "0", genre: FnValue, span: Span(8, 5, 12, 6), package_name: "cargo-mutants-testdata-factorial", }"# } ); assert_eq!( mutants[1].name(false), "src/bin/factorial.rs: replace factorial -> u32 with 0" ); assert_eq!( mutants[1].name(true), "src/bin/factorial.rs:8:5: replace factorial -> u32 with 0" ); assert_eq!( mutants[2].name(true), "src/bin/factorial.rs:8:5: replace factorial -> u32 with 1" ); } #[test] fn filter_by_attributes() { let tmp = copy_of_testdata("hang_avoided_by_attr"); let mutants = Workspace::open(tmp.path()) .unwrap() .discover(&PackageFilter::All, &Options::default(), &Console::new()) .unwrap() .mutants; let descriptions = mutants.iter().map(Mutant::describe_change).collect_vec(); assert_eq!( descriptions, [ "replace controlled_loop with ()", "replace > with == in controlled_loop", "replace > with < in controlled_loop", "replace * with + in controlled_loop", "replace * with / in controlled_loop", ] ); } #[test] fn always_skip_constructors_called_new() { let code = indoc! { r" struct S { x: i32, } impl S { fn new(x: i32) -> Self { Self { x } } } " }; let mutants = mutate_source_str(code, &Options::default()).unwrap(); assert_eq!(mutants, []); } #[test] fn mutate_factorial() -> Result<()> { let temp = copy_of_testdata("factorial"); let tree_path = temp.path(); let mutants = Workspace::open(tree_path)? .discover(&PackageFilter::All, &Options::default(), &Console::new())? .mutants; assert_eq!(mutants.len(), 5); let mutated_code = mutants[0].mutated_code(); assert_eq!(mutants[0].function.as_ref().unwrap().function_name, "main"); assert_eq!( strip_trailing_space(&mutated_code), indoc! { r#" fn main() { () /* ~ changed by cargo-mutants ~ */ } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } "# } ); let mutated_code = mutants[1].mutated_code(); assert_eq!( mutants[1].function.as_ref().unwrap().function_name, "factorial" ); assert_eq!( strip_trailing_space(&mutated_code), indoc! { r#" fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { 0 /* ~ changed by cargo-mutants ~ */ } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } "# } ); Ok(()) } fn strip_trailing_space(s: &str) -> String { // Split on \n so that we retain empty lines etc s.split('\n').map(str::trim_end).join("\n") } } cargo-mutants-25.0.0/src/options.rs000064400000000000000000000754761046102023000153470ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Global in-process options for experimenting on mutants. //! //! The [`Options`] structure is built by combining, in priority order: //! //! 1. Command line options //! 2. Config options (read from `.cargo/mutants.toml`) //! 3. Built-in defaults #![warn(clippy::pedantic)] use std::env; #[cfg(test)] use std::ffi::OsString; use std::time::Duration; use camino::{Utf8Path, Utf8PathBuf}; use globset::GlobSet; use regex::RegexSet; use serde::Deserialize; use strum::{Display, EnumString}; use syn::Expr; use tracing::warn; use crate::config::Config; use crate::glob::build_glob_set; use crate::mutate::Mutant; use crate::{Args, BaselineStrategy, Context, Phase, Result, ValueEnum}; /// Options for mutation testing, based on both command-line arguments and the /// config file. #[derive(Default, Debug, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct Options { /// Run tests in an unmutated tree? pub baseline: BaselineStrategy, /// Turn off all lints. pub cap_lints: bool, /// Don't run the tests, just see if each mutant builds. pub check_only: bool, /// Copy `.git` and other VCS directories to build directories. pub copy_vcs: bool, /// Don't copy files matching gitignore patterns to build directories. pub gitignore: bool, /// Don't copy at all; run tests in the source directory. pub in_place: bool, /// Run a jobserver to limit concurrency between child processes. pub jobserver: bool, /// Allow this many concurrent jobs, across all child processes. None means NCPU. pub jobserver_tasks: Option, /// Don't delete scratch directories. pub leak_dirs: bool, /// The time limit for test tasks, if set. /// /// If this is not set by the user it's None, in which case there is no time limit /// on the baseline test, and then the mutated tests get a multiple of the time /// taken by the baseline test. pub test_timeout: Option, /// The time multiplier for test tasks, if set (relative to baseline test duration). pub test_timeout_multiplier: Option, /// Which packages to test for a given mutant. /// /// Comes from `--test-workspace` etc. pub test_package: TestPackages, /// The time limit for build tasks, if set. /// /// If this is not set by the user it's None, in which case there is no time limit /// on the baseline build, and then the mutated builds get a multiple of the time /// taken by the baseline build. pub build_timeout: Option, /// The time multiplier for build tasks, if set (relative to baseline build duration). pub build_timeout_multiplier: Option, /// The minimum test timeout, as a floor on the autoset value. pub minimum_test_timeout: Duration, pub print_caught: bool, pub print_unviable: bool, pub show_times: bool, /// Show logs even from mutants that were caught, or source/unmutated builds. pub show_all_logs: bool, /// List mutants with line and column numbers. pub show_line_col: bool, /// Test mutants in random order. /// /// This is now the default, so that repeated partial runs are more likely to find /// interesting results. pub shuffle: bool, /// Don't mutate arguments to functions or methods matching any of these name. /// /// This matches as a string against the last component of the path, so should not include /// `::`. pub skip_calls: Vec, /// Cargo profile. pub profile: Option, /// Additional arguments for every cargo invocation. pub additional_cargo_args: Vec, /// Additional arguments to `cargo test`. pub additional_cargo_test_args: Vec, /// Selection of features for cargo. pub features: super::Features, /// Files to examine. pub examine_globset: Option, /// Files to exclude. pub exclude_globset: Option, /// Mutants to examine, as a regexp matched against the full name. pub examine_names: RegexSet, /// Mutants to skip, as a regexp matched against the full name. pub exclude_names: RegexSet, /// Create `mutants.out` within this directory (by default, the source directory). pub output_in_dir: Option, /// Run this many `cargo build` or `cargo test` tasks in parallel. pub jobs: Option, /// Insert these values as errors from functions returning `Result`. pub error_values: Vec, /// Show ANSI colors. pub colors: Colors, /// List mutants in json, etc. pub emit_json: bool, /// Emit diffs showing just what changed. pub emit_diffs: bool, /// The tool to use to run tests. pub test_tool: TestTool, } /// Which packages should be tested for a given mutant? #[derive(Debug, Default, Clone, PartialEq, Eq, EnumString, Display, Deserialize)] pub enum TestPackages { /// Only the package containing the mutated file. #[default] Mutated, /// All packages in the workspace. Workspace, /// Certain packages, specified by name. Named(Vec), } /// Choice of tool to use to run tests. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumString, Display, Deserialize)] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum TestTool { /// Use `cargo test`, the default. #[default] Cargo, /// Use `cargo nextest`. Nextest, } /// Join two slices into a new vector. fn join_slices(a: &[String], b: &[String]) -> Vec { a.iter().chain(b).cloned().collect() } /// Should ANSI colors be drawn? #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Display, Deserialize, ValueEnum)] #[strum(serialize_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum Colors { #[default] Auto, Always, Never, } impl Colors { /// If colors were forced on or off by the user through an option or /// environment variable, return that value. /// /// Otherwise, return None, meaning we should decide based on the /// detected terminal characteristics. pub fn forced_value(self) -> Option { // From https://bixense.com/clicolors/ if env::var("NO_COLOR").is_ok_and(|x| x != "0") { Some(false) } else if env::var("CLICOLOR_FORCE").is_ok_and(|x| x != "0") { Some(true) } else { match self { Colors::Always => Some(true), Colors::Never => Some(false), Colors::Auto => None, // library should decide } } } #[mutants::skip] // depends on a real tty etc, hard to test pub fn active_stdout(self) -> bool { self.forced_value() .unwrap_or_else(::console::colors_enabled) } } impl Options { /// Build options by merging command-line args and config file. pub(crate) fn new(args: &Args, config: &Config) -> Result { if args.no_copy_target { warn!("--no-copy-target is deprecated and has no effect; target/ is never copied"); } let minimum_test_timeout = Duration::from_secs_f64( args.minimum_test_timeout .or(config.minimum_test_timeout) .unwrap_or(20f64), ); // If either command line argument is set, it overrides the config. let test_package = if args.test_workspace == Some(true) { TestPackages::Workspace } else if !args.test_package.is_empty() { TestPackages::Named( args.test_package .iter() .flat_map(|s| s.split(',')) .map(ToString::to_string) .collect(), ) } else if args.test_workspace.is_none() && config.test_workspace == Some(true) { TestPackages::Workspace } else if !config.test_package.is_empty() { TestPackages::Named(config.test_package.clone()) } else { TestPackages::Mutated }; let mut skip_calls: Vec = args .skip_calls .iter() .flat_map(|s| s.split(',')) .map(ToString::to_string) .chain(config.skip_calls.iter().cloned()) .collect(); if args .skip_calls_defaults .or(config.skip_calls_defaults) .unwrap_or(true) { skip_calls.push("with_capacity".to_owned()); } let options = Options { additional_cargo_args: join_slices(&args.cargo_arg, &config.additional_cargo_args), additional_cargo_test_args: join_slices( &args.cargo_test_args, &config.additional_cargo_test_args, ), baseline: args.baseline, build_timeout: args.build_timeout.map(Duration::from_secs_f64), build_timeout_multiplier: args .build_timeout_multiplier .or(config.build_timeout_multiplier), cap_lints: args.cap_lints.unwrap_or(config.cap_lints), check_only: args.check, colors: args.colors, copy_vcs: args.copy_vcs.or(config.copy_vcs).unwrap_or(false), emit_json: args.json, emit_diffs: args.diff, error_values: join_slices(&args.error, &config.error_values), examine_names: RegexSet::new(or_slices(&args.examine_re, &config.examine_re)) .context("Failed to compile examine_re regex")?, exclude_names: RegexSet::new(or_slices(&args.exclude_re, &config.exclude_re)) .context("Failed to compile exclude_re regex")?, examine_globset: build_glob_set(or_slices(&args.file, &config.examine_globs))?, exclude_globset: build_glob_set(or_slices(&args.exclude, &config.exclude_globs))?, features: args.features.clone(), gitignore: args.gitignore, in_place: args.in_place, jobs: args.jobs, jobserver: args.jobserver, jobserver_tasks: args.jobserver_tasks, leak_dirs: args.leak_dirs, minimum_test_timeout, output_in_dir: args.output.clone().or(config.output.clone()), print_caught: args.caught, print_unviable: args.unviable, profile: args.profile.as_ref().or(config.profile.as_ref()).cloned(), shuffle: !args.no_shuffle, show_line_col: args.line_col, show_times: !args.no_times, show_all_logs: args.all_logs, skip_calls, test_package, test_timeout: args.timeout.map(Duration::from_secs_f64), test_timeout_multiplier: args.timeout_multiplier.or(config.timeout_multiplier), test_tool: args.test_tool.or(config.test_tool).unwrap_or_default(), }; options.error_values.iter().for_each(|e| { if e.starts_with("Err(") { warn!( "error_value option gives the value of the error, and probably should not start with Err(: got {}", e ); } }); Ok(options) } #[cfg(test)] pub fn from_args(args: &Args) -> Result { Options::new(args, &Config::default()) } /// Parse options from command-line arguments, using the default config. /// /// # Panics /// /// If the arguments are invalid. #[cfg(test)] pub fn from_arg_strs, S: Into + Clone>(args: I) -> Options { use crate::Args; use clap::Parser; let args = Args::try_parse_from(args).expect("Failed to parse args"); Options::from_args(&args).expect("Build options from args") } /// Which phases to run for each mutant. pub fn phases(&self) -> &[Phase] { if self.check_only { &[Phase::Check] } else { &[Phase::Build, Phase::Test] } } /// Return the syn ASTs for the error values, which should be inserted as return values /// from functions returning `Result`. pub(crate) fn parsed_error_exprs(&self) -> Result> { self.error_values .iter() .map(|e| { syn::parse_str(e).with_context(|| format!("Failed to parse error value {e:?}")) }) .collect() } /// True if the options allow mutants to be generated from the given path. /// /// That is: it matches the examine globset (if specified) and does not match the exclude globset /// (if specified). pub fn allows_source_file_path(&self, path: &Utf8Path) -> bool { // TODO: Use Option::is_none_or when MSRV>1.80 self.examine_globset .as_ref() .map_or(true, |g| g.is_match(path)) && !self .exclude_globset .as_ref() .is_some_and(|g| g.is_match(path)) } /// True if the options allow this mutant to be tested. pub fn allows_mutant(&self, mutant: &Mutant) -> bool { let name = mutant.name(true); (self.examine_names.is_empty() || self.examine_names.is_match(&name)) && (self.exclude_names.is_empty() || !self.exclude_names.is_match(&name)) } } /// If the first slices is non-empty, return that, otherwise the second. fn or_slices<'a: 'c, 'b: 'c, 'c, T>(a: &'a [T], b: &'b [T]) -> &'c [T] { if a.is_empty() { b } else { a } } #[cfg(test)] mod test { use std::io::Write; use std::str::FromStr; use clap::Parser; use indoc::indoc; use rusty_fork::rusty_fork_test; use tempfile::NamedTempFile; use super::*; use crate::Args; #[test] fn default_options() { let args = Args::parse_from(["mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(!options.check_only); assert_eq!(options.test_tool, TestTool::Cargo); assert!(!options.cap_lints); } #[test] fn options_from_test_tool_arg() { let args = Args::parse_from(["mutants", "--test-tool", "nextest"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_tool, TestTool::Nextest); } #[test] fn options_from_baseline_arg() { let args = Args::parse_from(["mutants", "--baseline", "skip"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.baseline, BaselineStrategy::Skip); let args = Args::parse_from(["mutants", "--baseline", "run"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.baseline, BaselineStrategy::Run); let args = Args::parse_from(["mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.baseline, BaselineStrategy::Run); } #[test] fn options_from_timeout_args() { let args = Args::parse_from(["mutants", "--timeout=2.0"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_timeout, Some(Duration::from_secs(2))); let args = Args::parse_from(["mutants", "--timeout-multiplier=2.5"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_timeout_multiplier, Some(2.5)); let args = Args::parse_from(["mutants", "--minimum-test-timeout=60.0"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.minimum_test_timeout, Duration::from_secs(60)); let args = Args::parse_from(["mutants", "--build-timeout=3.0"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.build_timeout, Some(Duration::from_secs(3))); let args = Args::parse_from(["mutants", "--build-timeout-multiplier=3.5"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.build_timeout_multiplier, Some(3.5)); } #[test] fn cli_timeout_multiplier_overrides_config() { let config = indoc! { r" timeout_multiplier = 1.0 build_timeout_multiplier = 2.0 "}; let mut config_file = NamedTempFile::new().unwrap(); config_file.write_all(config.as_bytes()).unwrap(); let args = Args::parse_from([ "mutants", "--timeout-multiplier=2.0", "--build-timeout-multiplier=1.0", ]); let config = Config::read_file(config_file.path()).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_timeout_multiplier, Some(2.0)); assert_eq!(options.build_timeout_multiplier, Some(1.0)); } #[test] fn conflicting_timeout_options() { let args = Args::try_parse_from(["mutants", "--timeout=1", "--timeout-multiplier=1"]) .expect_err("--timeout and --timeout-multiplier should conflict"); let rendered = format!("{}", args.render()); assert!(rendered.contains("error: the argument '--timeout ' cannot be used with '--timeout-multiplier '")); } #[test] fn conflicting_build_timeout_options() { let args = Args::try_parse_from([ "mutants", "--build-timeout=1", "--build-timeout-multiplier=1", ]) .expect_err("--build-timeout and --build-timeout-multiplier should conflict"); let rendered = format!("{}", args.render()); assert!(rendered.contains("error: the argument '--build-timeout ' cannot be used with '--build-timeout-multiplier '")); } #[test] fn from_config() { let config = indoc! { r#" test_tool = "nextest" cap_lints = true "#}; let mut config_file = NamedTempFile::new().unwrap(); config_file.write_all(config.as_bytes()).unwrap(); let args = Args::try_parse_from(["mutants"]).unwrap(); let config = Config::read_file(config_file.path()).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_tool, TestTool::Nextest); assert!(options.cap_lints); } #[test] fn features_arg() { let args = Args::try_parse_from(["mutants", "--features", "nice,shiny features"]).unwrap(); assert_eq!( args.features.features.iter().as_ref(), ["nice,shiny features"] ); assert!(!args.features.no_default_features); assert!(!args.features.all_features); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!( options.features.features.iter().as_ref(), ["nice,shiny features"] ); assert!(!options.features.no_default_features); assert!(!options.features.all_features); } #[test] fn no_default_features_arg() { let args = Args::try_parse_from([ "mutants", "--no-default-features", "--features", "nice,shiny features", ]) .unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!( options.features.features.iter().as_ref(), ["nice,shiny features"] ); assert!(options.features.no_default_features); assert!(!options.features.all_features); } #[test] fn default_jobserver_settings() { let args = Args::parse_from(["mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(options.jobserver); assert_eq!(options.jobserver_tasks, None); } #[test] fn disable_jobserver() { let args = Args::parse_from(["mutants", "--jobserver=false"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(!options.jobserver); assert_eq!(options.jobserver_tasks, None); } #[test] fn jobserver_tasks() { let args = Args::parse_from(["mutants", "--jobserver-tasks=13"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(options.jobserver); assert_eq!(options.jobserver_tasks, Some(13)); } #[test] fn all_features_arg() { let args = Args::try_parse_from([ "mutants", "--all-features", "--features", "nice,shiny features", ]) .unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!( options.features.features.iter().as_ref(), ["nice,shiny features"] ); assert!(!options.features.no_default_features); assert!(options.features.all_features); } rusty_fork_test! { #[test] fn color_control_from_cargo_env() { use std::env::{set_var,remove_var}; set_var("CARGO_TERM_COLOR", "always"); remove_var("CLICOLOR_FORCE"); remove_var("NO_COLOR"); let args = Args::parse_from(["mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.colors.forced_value(), Some(true)); set_var("CARGO_TERM_COLOR", "never"); let args = Args::parse_from(["mutants"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.colors.forced_value(), Some(false)); set_var("CARGO_TERM_COLOR", "auto"); let args = Args::parse_from(["mutants"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.colors.forced_value(), None); remove_var("CARGO_TERM_COLOR"); let args = Args::parse_from(["mutants"]); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.colors.forced_value(), None); } #[test] fn color_control_from_env() { use std::env::{set_var,remove_var}; remove_var("CARGO_TERM_COLOR"); remove_var("CLICOLOR_FORCE"); remove_var("NO_COLOR"); let args = Args::parse_from(["mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.colors.forced_value(), None); remove_var("CLICOLOR_FORCE"); set_var("NO_COLOR", "1"); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.colors.forced_value(), Some(false)); remove_var("NO_COLOR"); set_var("CLICOLOR_FORCE", "1"); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.colors.forced_value(), Some(true)); remove_var("CLICOLOR_FORCE"); remove_var("NO_COLOR"); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.colors.forced_value(), None); } } #[test] fn profile_option_from_args() { let args = Args::parse_from(["mutants", "--profile=mutants"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.profile.unwrap(), "mutants"); } #[test] fn profile_from_config() { let args = Args::try_parse_from(["mutants", "-j3"]).unwrap(); let config = indoc! { r#" profile = "mutants" timeout_multiplier = 1.0 build_timeout_multiplier = 2.0 "#}; let mut config_file = NamedTempFile::new().unwrap(); config_file.write_all(config.as_bytes()).unwrap(); let config = Config::read_file(config_file.path()).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.profile.unwrap(), "mutants"); } #[test] fn test_workspace_arg_true() { let args = Args::parse_from(["mutants", "--test-workspace=true"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Workspace); } #[test] fn test_workspace_arg_false() { let args = Args::parse_from(["mutants", "--test-workspace=false"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Mutated); } #[test] fn test_workspace_config_true() { let args = Args::try_parse_from(["mutants"]).unwrap(); let config = indoc! { r" test_workspace = true "}; let config = Config::from_str(config).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Workspace); } #[test] fn test_workspace_config_false() { let args = Args::try_parse_from(["mutants"]).unwrap(); let config = indoc! { r" test_workspace = false "}; let config = Config::from_str(config).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Mutated); } #[test] fn test_workspace_args_override_config_true() { let args = Args::try_parse_from(["mutants", "--test-workspace=true"]).unwrap(); let config = indoc! { r" test_workspace = false "}; let config = Config::from_str(config).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Workspace); } #[test] fn test_workspace_args_override_config_false() { let args = Args::try_parse_from(["mutants", "--test-workspace=false"]).unwrap(); let config = indoc! { r" test_workspace = true "}; let config = Config::from_str(config).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_package, TestPackages::Mutated); } #[test] fn test_workspace_arg_false_allows_packages_from_config() { let args = Args::try_parse_from(["mutants", "--test-workspace=false"]).unwrap(); let config = indoc! { r#" # Normally the packages would be ignored, but --test-workspace=false. test_workspace = true test_package = ["foo", "bar"] "#}; let config = Config::from_str(config).unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!( options.test_package, TestPackages::Named(vec!["foo".to_string(), "bar".to_string()]) ); } #[test] fn test_package_arg_with_commas() { let args = Args::parse_from(["mutants", "--test-package=foo,bar"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!( options.test_package, TestPackages::Named(vec!["foo".to_string(), "bar".to_string()]) ); } #[test] fn default_skip_calls_includes_with_capacity() { let args = Args::try_parse_from(["mutants"]).unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.skip_calls, ["with_capacity"]); } #[test] fn arg_configure_skip_calls_default_off() { let args = Args::try_parse_from(["mutants", "--skip-calls-defaults=false"]).unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.skip_calls, [""; 0]); } #[test] fn arg_redundantly_configure_skip_calls_default_on() { let args = Args::try_parse_from(["mutants", "--skip-calls-defaults=true"]).unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.skip_calls, ["with_capacity"]); } #[test] fn skip_calls_from_args() { let args = Args::try_parse_from(["mutants", "--skip-calls=a", "--skip-calls=b,c"]).unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.skip_calls, ["a", "b", "c", "with_capacity"]); } #[test] fn skip_calls_from_args_and_options() { let args = Args::try_parse_from(["mutants", "--skip-calls=a", "--skip-calls=b,c"]).unwrap(); let config = Config::from_str( r#" skip_calls = ["d", "e"] "#, ) .unwrap(); let options = Options::new(&args, &config).unwrap(); // In this case the default is not used assert_eq!( options.skip_calls, ["a", "b", "c", "d", "e", "with_capacity"] ); } #[test] fn config_skip_calls_default_off() { // You can configure off the default `with_capacity` skip_calls let args = Args::try_parse_from(["mutants"]).unwrap(); let config = Config::from_str( r" skip_calls_defaults = false skip_calls = [] ", ) .unwrap(); let options = Options::new(&args, &config).unwrap(); // In this case the default is not used assert_eq!(options.skip_calls, [""; 0]); } #[test] fn arg_overrides_config_skip_calls_defaults() { // You can configure off the default `with_capacity` skip_calls let args = Args::try_parse_from(["mutants", "--skip-calls-defaults=true", "--skip-calls=x"]) .unwrap(); let config = Config::from_str( r#" skip_calls_defaults = false skip_calls = ["y"] "#, ) .unwrap(); let options = Options::new(&args, &config).unwrap(); // In this case the default is not used assert_eq!(options.skip_calls, ["x", "y", "with_capacity"]); } #[test] fn copy_vcs() { let args = Args::parse_from(["mutants", "--copy-vcs=true"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(options.copy_vcs); let args = Args::parse_from(["mutants", "--copy-vcs=false"]); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert!(!options.copy_vcs); let args = Args::parse_from(["mutants"]); let config = Config::from_str("copy_vcs = true").unwrap(); let options = Options::new(&args, &config).unwrap(); assert!(options.copy_vcs); let args = Args::parse_from(["mutants"]); let config = Config::from_str("").unwrap(); let options = Options::new(&args, &config).unwrap(); assert!(!options.copy_vcs); } } cargo-mutants-25.0.0/src/outcome.rs000064400000000000000000000302071046102023000153060ustar 00000000000000// Copyright 2022-2024 Martin Pool //! The outcome of running a single mutation scenario, or a whole lab. use std::fmt; use std::fs::read_to_string; use std::time::{Duration, Instant}; use anyhow::Context; use camino::Utf8PathBuf; use humantime::format_duration; use output::ScenarioOutput; use serde::ser::SerializeStruct; use serde::Serialize; use serde::Serializer; use tracing::warn; use crate::console::plural; use crate::process::Exit; use crate::{exit_code, output, Options, Result, Scenario}; /// What phase of running a scenario. /// /// Every scenario proceed through up to three phases in order. They are: /// /// 1. `cargo check` -- is the tree basically buildable? This is skipped /// during normal testing, but used with `--check`, in which case the /// other phases are skipped. /// 2. `cargo build` -- actually build it. /// 3. `cargo tests` -- do the tests pass? /// /// Some scenarios such as freshening the tree don't run the tests. #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)] pub enum Phase { Check, Build, Test, } impl Phase { pub fn name(self) -> &'static str { match self { Phase::Check => "check", Phase::Build => "build", Phase::Test => "test", } } } impl fmt::Display for Phase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.pad(self.name()) } } /// The outcome from a whole lab run containing multiple mutants. #[derive(Debug, Default, Serialize)] #[allow(clippy::module_name_repetitions)] pub struct LabOutcome { /// All the scenario outcomes, including baseline builds. pub outcomes: Vec, pub total_mutants: usize, pub missed: usize, pub caught: usize, pub timeout: usize, pub unviable: usize, pub success: usize, } impl LabOutcome { pub fn new() -> LabOutcome { LabOutcome::default() } /// Record the event of one test. pub fn add(&mut self, outcome: ScenarioOutcome) { if outcome.scenario.is_mutant() { self.total_mutants += 1; match outcome.summary() { SummaryOutcome::CaughtMutant => self.caught += 1, SummaryOutcome::MissedMutant => self.missed += 1, SummaryOutcome::Timeout => self.timeout += 1, SummaryOutcome::Unviable => self.unviable += 1, SummaryOutcome::Success => self.success += 1, SummaryOutcome::Failure => { // We don't expect to see failures that don't fit into the other categories. warn!("Unclassified failure for mutant {:?}", outcome.scenario); } } } self.outcomes.push(outcome); } /// Return the overall program exit code reflecting this outcome. pub fn exit_code(&self) -> i32 { // TODO: Maybe move this into an error returned from experiment()? if self .outcomes .iter() .any(|o| !o.scenario.is_mutant() && !o.success()) { exit_code::CLEAN_TESTS_FAILED } else if self.timeout > 0 { exit_code::TIMEOUT } else if self.missed > 0 { exit_code::FOUND_PROBLEMS } else { exit_code::SUCCESS } } /// Return an overall summary, to show at the end of the program. pub fn summary_string(&self, start_time: Instant, options: &Options) -> String { let mut s = Vec::new(); s.push(format!("{} tested", plural(self.total_mutants, "mutant"))); if options.show_times { s.push(format!( " in {}", format_duration(Duration::from_secs(start_time.elapsed().as_secs())) )); } s.push(": ".into()); let mut by_outcome: Vec = Vec::new(); if self.missed != 0 { by_outcome.push(format!("{} missed", self.missed)); } if self.caught != 0 { by_outcome.push(format!("{} caught", self.caught)); } if self.unviable != 0 { by_outcome.push(format!("{} unviable", self.unviable)); } if self.timeout != 0 { by_outcome.push(format!("{} timeouts", self.timeout)); } if self.success != 0 { by_outcome.push(format!("{} succeeded", self.success)); } s.push(by_outcome.join(", ")); s.join("") } } /// The result of running one mutation scenario. #[derive(Debug, Clone, Eq, PartialEq)] #[allow(clippy::module_name_repetitions)] pub struct ScenarioOutcome { /// A file holding the text output from running this test. // TODO: Maybe this should be a log object? output_dir: Utf8PathBuf, log_path: Utf8PathBuf, /// The path relative to `mutants.out` for a file showing the diff between the unmutated /// and mutated source. Only present for mutant scenarios. diff_path: Option, /// What kind of scenario was being built? pub scenario: Scenario, /// For each phase, the duration and the cargo result. phase_results: Vec, } impl Serialize for ScenarioOutcome { fn serialize(&self, serializer: S) -> Result where S: Serializer, { // custom serialize to omit inessential info and to inline a summary. let mut ss = serializer.serialize_struct("Outcome", 5)?; ss.serialize_field("scenario", &self.scenario)?; ss.serialize_field("summary", &self.summary())?; ss.serialize_field("log_path", &self.log_path)?; ss.serialize_field("diff_path", &self.diff_path)?; ss.serialize_field("phase_results", &self.phase_results)?; ss.end() } } impl ScenarioOutcome { pub fn new(scenario_output: &ScenarioOutput, scenario: Scenario) -> ScenarioOutcome { ScenarioOutcome { output_dir: scenario_output.output_dir.clone(), log_path: scenario_output.log_path().to_owned(), diff_path: scenario_output.diff_path.clone(), scenario, phase_results: Vec::new(), } } pub fn add_phase_result(&mut self, phase_result: PhaseResult) { self.phase_results.push(phase_result); } pub fn get_log_content(&self) -> Result { read_to_string(self.output_dir.join(&self.log_path)).context("read log file") } pub fn last_phase(&self) -> Phase { self.phase_results.last().unwrap().phase } pub fn last_phase_result(&self) -> Exit { self.phase_results.last().unwrap().process_status } /// Return the results of all phases. pub fn phase_results(&self) -> &[PhaseResult] { &self.phase_results } /// Return the result of the given phase, if it was run. pub fn phase_result(&self, phase: Phase) -> Option<&PhaseResult> { self.phase_results.iter().find(|pr| pr.phase == phase) } /// True if this status indicates the user definitely needs to see the logs, because a task /// failed that should not have failed. pub fn should_show_logs(&self) -> bool { !self.scenario.is_mutant() && !self.success() } pub fn success(&self) -> bool { self.last_phase_result().is_success() } pub fn has_timeout(&self) -> bool { self.phase_results .iter() .any(|pr| pr.process_status.is_timeout()) } pub fn check_or_build_failed(&self) -> bool { self.phase_results .iter() .any(|pr| pr.phase != Phase::Test && pr.process_status.is_failure()) } /// True if this outcome is a caught mutant: it's a mutant and the tests failed. pub fn mutant_caught(&self) -> bool { self.scenario.is_mutant() && self.last_phase() == Phase::Test && self.last_phase_result().is_failure() } /// True if this outcome is a missed mutant: it's a mutant and the tests succeeded. pub fn mutant_missed(&self) -> bool { self.scenario.is_mutant() && self.last_phase() == Phase::Test && self.last_phase_result().is_success() } pub fn summary(&self) -> SummaryOutcome { // Caution: this function is called when rendering progress // and so should not log; see https://github.com/sourcefrog/nutmeg/issues/16. match self.scenario { Scenario::Baseline => { if self.has_timeout() { SummaryOutcome::Timeout } else if self.success() { SummaryOutcome::Success } else { SummaryOutcome::Failure } } Scenario::Mutant(_) => { if self.check_or_build_failed() { SummaryOutcome::Unviable } else if self.has_timeout() { SummaryOutcome::Timeout } else if self.mutant_caught() { SummaryOutcome::CaughtMutant } else if self.mutant_missed() { SummaryOutcome::MissedMutant } else if self.success() { SummaryOutcome::Success } else { // Some unattributed failure; should be rare or impossible? SummaryOutcome::Failure } } } } } /// The result of running one phase of a mutation scenario, i.e. a single cargo check/build/test command. #[derive(Debug, Clone, Eq, PartialEq)] pub struct PhaseResult { /// What phase was this? pub phase: Phase, /// How long did it take? pub duration: Duration, /// Did it succeed? pub process_status: Exit, /// What command was run, as an argv list. pub argv: Vec, } impl PhaseResult { pub fn is_success(&self) -> bool { self.process_status.is_success() } } impl Serialize for PhaseResult { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut ss = serializer.serialize_struct("PhaseResult", 4)?; ss.serialize_field("phase", &self.phase)?; ss.serialize_field("duration", &self.duration.as_secs_f64())?; ss.serialize_field("process_status", &self.process_status)?; ss.serialize_field("argv", &self.argv)?; ss.end() } } /// Overall summary outcome for one mutant. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Hash)] #[allow(clippy::module_name_repetitions)] pub enum SummaryOutcome { Success, CaughtMutant, MissedMutant, Unviable, Failure, Timeout, } #[cfg(test)] mod test { use std::time::Duration; use crate::process::Exit; use super::{Phase, PhaseResult, Scenario, ScenarioOutcome}; #[test] fn find_phase_result() { let outcome = ScenarioOutcome { output_dir: "output".into(), log_path: "log".into(), diff_path: Some("mutant.diff".into()), scenario: Scenario::Baseline, phase_results: vec![ PhaseResult { phase: Phase::Build, duration: Duration::from_secs(2), process_status: Exit::Success, argv: vec!["cargo".into(), "build".into()], }, PhaseResult { phase: Phase::Test, duration: Duration::from_secs(3), process_status: Exit::Success, argv: vec!["cargo".into(), "test".into()], }, ], }; assert_eq!( outcome.phase_result(Phase::Build), Some(&PhaseResult { phase: Phase::Build, duration: Duration::from_secs(2), process_status: Exit::Success, argv: vec!["cargo".into(), "build".into()], }) ); assert_eq!( outcome .phase_result(Phase::Build) .unwrap() .duration .as_secs(), 2 ); assert_eq!( outcome .phase_result(Phase::Test) .unwrap() .duration .as_secs(), 3 ); assert_eq!(outcome.phase_result(Phase::Check), None); } } cargo-mutants-25.0.0/src/output.rs000064400000000000000000000446311046102023000152010ustar 00000000000000// Copyright 2021-2024 Martin Pool //! A `mutants.out` directory holding logs and other output. use std::collections::{hash_map::Entry, HashMap}; use std::fs::{create_dir, read_to_string, remove_dir_all, rename, write, File, OpenOptions}; use std::io::{BufWriter, Write}; use std::path::Path; use std::thread::sleep; use std::time::Duration; use camino::{Utf8Path, Utf8PathBuf}; use fs2::FileExt; use path_slash::PathExt; use serde::Serialize; use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use tracing::{info, trace}; use crate::outcome::{LabOutcome, SummaryOutcome}; use crate::{check_interrupted, Context, Mutant, Result, Scenario, ScenarioOutcome}; const OUTDIR_NAME: &str = "mutants.out"; const ROTATED_NAME: &str = "mutants.out.old"; const LOCK_JSON: &str = "lock.json"; const LOCK_POLL: Duration = Duration::from_millis(100); static CAUGHT_TXT: &str = "caught.txt"; static PREVIOUSLY_CAUGHT_TXT: &str = "previously_caught.txt"; static UNVIABLE_TXT: &str = "unviable.txt"; /// The contents of a `lock.json` written into the output directory and used as /// a lock file to ensure that two cargo-mutants invocations don't try to write /// to the same `mutants.out` simultneously. #[derive(Debug, Serialize)] struct LockFile { cargo_mutants_version: String, start_time: String, hostname: String, username: String, } impl LockFile { fn new() -> LockFile { let start_time = OffsetDateTime::now_utc() .format(&Rfc3339) .expect("format current time"); LockFile { cargo_mutants_version: crate::VERSION.to_string(), start_time, hostname: whoami::fallible::hostname().unwrap_or_default(), username: whoami::username(), } } /// Block until acquiring a file lock on `lock.json` in the given `mutants.out` /// directory. /// /// Return the `File` whose lifetime controls the file lock. pub fn acquire_lock(output_dir: &Path) -> Result { let lock_path = output_dir.join(LOCK_JSON); let mut lock_file = File::options() .create(true) .truncate(false) .write(true) .open(&lock_path) .context("open or create lock.json in existing directory")?; let mut first = true; while let Err(err) = lock_file.try_lock_exclusive() { if first { info!( "Waiting for lock on {} ...: {err}", lock_path.to_slash_lossy() ); first = false; } check_interrupted()?; sleep(LOCK_POLL); } lock_file.set_len(0)?; lock_file .write_all(serde_json::to_string_pretty(&LockFile::new())?.as_bytes()) .context("write lock.json")?; Ok(lock_file) } } /// A `mutants.out` directory holding logs and other output information. #[derive(Debug)] #[allow(clippy::module_name_repetitions)] pub struct OutputDir { path: Utf8PathBuf, #[allow(unused)] // Lifetime controls the file lock lock_file: File, /// A file holding a list of missed mutants as text, one per line. missed_list: File, /// A file holding a list of caught mutants as text, one per line. caught_list: File, /// A file holding a list of mutants where testing timed out, as text, one per line. timeout_list: File, unviable_list: File, /// The accumulated overall lab outcome. pub lab_outcome: LabOutcome, /// Log filenames which have already been used, and the number of times that each /// basename has been used. used_log_names: HashMap, } impl OutputDir { /// Create a new `mutants.out` output directory, within the given directory. /// /// If `in_dir` does not exist, it's created too, so that users can name a new directory /// with `--output`. /// /// If the directory already exists, it's rotated to `mutants.out.old`. If that directory /// exists, it's deleted. /// /// If the directory already exists and `lock.json` exists and is locked, this waits for /// the lock to be released. The returned `OutputDir` holds a lock for its lifetime. pub fn new(in_dir: &Utf8Path) -> Result { if !in_dir.exists() { create_dir(in_dir).context("create output parent directory {in_dir:?}")?; } let output_dir = in_dir.join(OUTDIR_NAME); if output_dir.exists() { LockFile::acquire_lock(output_dir.as_ref())?; // Now release the lock for a bit while we move the directory. This might be // slightly racy. // TODO: Move the lock outside the directory, . let rotated = in_dir.join(ROTATED_NAME); if rotated.exists() { remove_dir_all(&rotated).with_context(|| format!("remove {:?}", &rotated))?; } rename(&output_dir, &rotated) .with_context(|| format!("move {:?} to {:?}", &output_dir, &rotated))?; } create_dir(&output_dir) .with_context(|| format!("create output directory {:?}", &output_dir))?; let lock_file = LockFile::acquire_lock(output_dir.as_std_path()) .context("create lock.json lock file")?; let log_dir = output_dir.join("log"); create_dir(&log_dir).with_context(|| format!("create log directory {:?}", &log_dir))?; let diff_dir = output_dir.join("diff"); create_dir(diff_dir).context("create diff dir")?; // Create text list files. let mut list_file_options = OpenOptions::new(); list_file_options.create(true).append(true); let missed_list = list_file_options .open(output_dir.join("missed.txt")) .context("create missed.txt")?; let caught_list = list_file_options .open(output_dir.join(CAUGHT_TXT)) .context("create caught.txt")?; let unviable_list = list_file_options .open(output_dir.join(UNVIABLE_TXT)) .context("create unviable.txt")?; let timeout_list = list_file_options .open(output_dir.join("timeout.txt")) .context("create timeout.txt")?; Ok(OutputDir { path: output_dir, lab_outcome: LabOutcome::new(), lock_file, missed_list, caught_list, timeout_list, unviable_list, used_log_names: HashMap::new(), }) } /// Allocate a sequence number and the output files for a scenario. pub fn start_scenario(&mut self, scenario: &Scenario) -> Result { let scenario_name = match scenario { Scenario::Baseline => "baseline".into(), Scenario::Mutant(mutant) => mutant.log_file_name_base(), }; let basename = match self.used_log_names.entry(scenario_name.clone()) { Entry::Occupied(mut e) => { let index = e.get_mut(); *index += 1; format!("{scenario_name}_{index:03}") } Entry::Vacant(e) => { e.insert(0); scenario_name } }; ScenarioOutput::new(&self.path, scenario, &basename) } /// Return the path of the `mutants.out` directory. #[allow(unused)] pub fn path(&self) -> &Utf8Path { &self.path } /// Update the state of the overall lab. /// /// Called multiple times as the lab runs. fn write_lab_outcome(&self) -> Result<()> { serde_json::to_writer_pretty( BufWriter::new(File::create(self.path.join("outcomes.json"))?), &self.lab_outcome, ) .context("write outcomes.json") } /// Add the result of testing one scenario. pub fn add_scenario_outcome(&mut self, scenario_outcome: &ScenarioOutcome) -> Result<()> { self.lab_outcome.add(scenario_outcome.to_owned()); self.write_lab_outcome()?; let scenario = &scenario_outcome.scenario; if let Scenario::Mutant(mutant) = scenario { let file = match scenario_outcome.summary() { SummaryOutcome::MissedMutant => &mut self.missed_list, SummaryOutcome::CaughtMutant => &mut self.caught_list, SummaryOutcome::Timeout => &mut self.timeout_list, SummaryOutcome::Unviable => &mut self.unviable_list, _ => return Ok(()), }; writeln!(file, "{}", mutant.name(true)).context("write to list file")?; } Ok(()) } pub fn open_debug_log(&self) -> Result { let debug_log_path = self.path.join("debug.log"); OpenOptions::new() .create(true) .append(true) .open(&debug_log_path) .with_context(|| format!("open {debug_log_path}")) } pub fn write_mutants_list(&self, mutants: &[Mutant]) -> Result<()> { serde_json::to_writer_pretty( BufWriter::new(File::create(self.path.join("mutants.json"))?), mutants, ) .context("write mutants.json") } pub fn take_lab_outcome(self) -> LabOutcome { self.lab_outcome } pub fn write_previously_caught(&self, caught: &[String]) -> Result<()> { let path = self.path.join(PREVIOUSLY_CAUGHT_TXT); let mut b = String::with_capacity(caught.iter().map(|l| l.len() + 1).sum()); for l in caught { b.push_str(l); b.push('\n'); } File::options() .create_new(true) .write(true) .open(&path) .and_then(|mut f| f.write_all(b.as_bytes())) .with_context(|| format!("Write {path:?}")) } } /// Return the string names of mutants previously caught in this output directory, including /// unviable mutants. /// /// Returns an empty vec if there are none. pub fn load_previously_caught(output_parent_dir: &Utf8Path) -> Result> { let mut r = Vec::new(); for filename in [CAUGHT_TXT, UNVIABLE_TXT, PREVIOUSLY_CAUGHT_TXT] { let p = output_parent_dir.join(OUTDIR_NAME).join(filename); trace!(?p, "read previously caught"); if p.is_file() { r.extend( read_to_string(&p) .with_context(|| format!("Read previously caught mutants from {p:?}"))? .lines() .map(str::to_string), ); } } Ok(r) } /// Where to write output about a particular Scenario. #[allow(clippy::module_name_repetitions)] pub struct ScenarioOutput { pub output_dir: Utf8PathBuf, log_path: Utf8PathBuf, pub log_file: File, /// File holding the diff of the mutated file, only if it's a mutation. pub diff_path: Option, } impl ScenarioOutput { fn new(output_dir: &Utf8Path, scenario: &Scenario, basename: &str) -> Result { let log_path = Utf8PathBuf::from(format!("log/{basename}.log")); let log_file = File::options() .append(true) .create_new(true) .read(true) .open(output_dir.join(&log_path))?; let diff_path = if scenario.is_mutant() { Some(Utf8PathBuf::from(format!("diff/{basename}.diff"))) } else { None }; let mut scenario_output = Self { output_dir: output_dir.to_owned(), log_path, log_file, diff_path, }; scenario_output.message(&scenario.to_string())?; Ok(scenario_output) } pub fn log_path(&self) -> &Utf8Path { &self.log_path } pub fn write_diff(&mut self, diff: &str) -> Result<()> { self.message(&format!("mutation diff:\n{diff}"))?; let diff_path = self.diff_path.as_ref().expect("should know the diff path"); write(self.output_dir.join(diff_path), diff.as_bytes()) .with_context(|| format!("write diff to {diff_path}")) } /// Open a new handle reading from the start of the log file. pub fn open_log_read(&self) -> Result { let path = self.output_dir.join(&self.log_path); OpenOptions::new() .read(true) .open(&path) .with_context(|| format!("reopen {path} for read")) } /// Open a new handle that appends to the log file, so that it can be passed to a subprocess. pub fn open_log_append(&self) -> Result { let path = self.output_dir.join(&self.log_path); OpenOptions::new() .append(true) .open(&path) .with_context(|| format!("reopen {path} for append")) } /// Write a message, with a marker. pub fn message(&mut self, message: &str) -> Result<()> { write!(self.log_file, "\n*** {message}\n").context("write message to log") } } pub fn clean_filename(s: &str) -> String { s.replace('/', "__") .chars() .map(|c| match c { '\\' | ' ' | ':' | '<' | '>' | '?' | '*' | '|' | '"' => '_', c => c, }) .collect::() } #[cfg(test)] mod test { use std::fs::write; use indoc::indoc; use itertools::Itertools; use pretty_assertions::assert_eq; use tempfile::{tempdir, TempDir}; use super::*; use crate::workspace::Workspace; fn minimal_source_tree() -> TempDir { let tmp = tempdir().unwrap(); let path = tmp.path(); write( path.join("Cargo.toml"), indoc! { br#" # enough for a test [package] name = "cargo-mutants-minimal-test-tree" version = "0.0.0" "# }, ) .unwrap(); create_dir(path.join("src")).unwrap(); write(path.join("src/lib.rs"), b"fn foo() {}").unwrap(); tmp } fn list_recursive(path: &Path) -> Vec { walkdir::WalkDir::new(path) .sort_by_file_name() .into_iter() .map(|entry| { entry .unwrap() .path() .strip_prefix(path) .unwrap() .to_slash_lossy() .to_string() }) .collect_vec() } #[test] fn clean_filename_removes_special_characters() { assert_eq!( clean_filename("1/2\\3:4<5>6?7*8|9\"0"), "1__2_3_4_5_6_7_8_9_0" ); } #[test] fn create_output_dir() { let tmp = minimal_source_tree(); let tmp_path: &Utf8Path = tmp.path().try_into().unwrap(); let workspace = Workspace::open(tmp_path).unwrap(); let output_dir = OutputDir::new(workspace.root()).unwrap(); assert_eq!( list_recursive(tmp.path()), &[ "", "Cargo.toml", "mutants.out", "mutants.out/caught.txt", "mutants.out/diff", "mutants.out/lock.json", "mutants.out/log", "mutants.out/missed.txt", "mutants.out/timeout.txt", "mutants.out/unviable.txt", "src", "src/lib.rs", ] ); assert_eq!(output_dir.path(), workspace.root().join("mutants.out")); assert!(output_dir.path().join("lock.json").is_file()); } #[test] fn rotate() { let temp_dir = TempDir::new().unwrap(); let temp_dir_path = Utf8Path::from_path(temp_dir.path()).unwrap(); // Create an initial output dir with one log. let mut output_dir = OutputDir::new(temp_dir_path).unwrap(); let scenario_output = output_dir.start_scenario(&Scenario::Baseline).unwrap(); assert!(temp_dir_path.join("mutants.out/log/baseline.log").is_file()); drop(output_dir); // release the lock. drop(scenario_output); // The second time we create it in the same directory, the old one is moved away. let mut output_dir = OutputDir::new(temp_dir_path).unwrap(); output_dir.start_scenario(&Scenario::Baseline).unwrap(); assert!(temp_dir .path() .join("mutants.out.old/log/baseline.log") .is_file()); assert!(temp_dir .path() .join("mutants.out/log/baseline.log") .is_file()); drop(output_dir); // The third time (and later), the .old directory is removed. let mut output_dir = OutputDir::new(temp_dir_path).unwrap(); output_dir.start_scenario(&Scenario::Baseline).unwrap(); assert!(temp_dir .path() .join("mutants.out/log/baseline.log") .is_file()); assert!(temp_dir .path() .join("mutants.out.old/log/baseline.log") .is_file()); assert!(temp_dir .path() .join("mutants.out.old/log/baseline.log") .is_file()); } #[test] fn track_previously_caught() { let temp_dir = TempDir::new().unwrap(); let parent = Utf8Path::from_path(temp_dir.path()).unwrap(); let example = "src/process.rs:213:9: replace ProcessStatus::is_success -> bool with true src/process.rs:248:5: replace get_command_output -> Result with Ok(String::new()) "; // Read from an empty dir: succeeds. assert!(load_previously_caught(parent) .expect("load succeeds") .is_empty()); let output_dir = OutputDir::new(parent).unwrap(); assert!(load_previously_caught(parent) .expect("load succeeds") .is_empty()); write(parent.join("mutants.out/caught.txt"), example.as_bytes()).unwrap(); let previously_caught = load_previously_caught(parent).expect("load succeeds"); assert_eq!( previously_caught.iter().collect_vec(), example.lines().collect_vec() ); // make a new output dir, moving away the old one, and write this drop(output_dir); let output_dir = OutputDir::new(parent).unwrap(); output_dir .write_previously_caught(&previously_caught) .unwrap(); assert_eq!( read_to_string(parent.join("mutants.out/caught.txt")).expect("read caught.txt"), "" ); assert!(parent.join("mutants.out/previously_caught.txt").is_file()); let now = load_previously_caught(parent).expect("load succeeds"); assert_eq!(now.iter().collect_vec(), example.lines().collect_vec()); } } cargo-mutants-25.0.0/src/package.rs000064400000000000000000000115041046102023000152250ustar 00000000000000// Copyright 2023-2025 Martin Pool //! Discover and represent cargo packages within a workspace. use std::sync::Arc; use camino::{Utf8Path, Utf8PathBuf}; use cargo_metadata::TargetKind; use itertools::Itertools; use serde::Serialize; use tracing::{debug, debug_span, warn}; /// A package built and tested as a unit. /// /// This is an internal representation derived from and similar to a `cargo_metadata::Package`, /// in a more digested form and as an extension point for later supporting tools other than Cargo. #[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize)] pub struct Package { /// The short name of the package, like "mutants". pub name: String, /// The version of the package, like `"0.1.0"`. pub version: String, /// The directory for this package relative to the workspace. /// /// For a package in the root, this is `""`. pub relative_dir: Utf8PathBuf, /// The top source files for this package, relative to the workspace root, /// like `["src/lib.rs"]`. pub top_sources: Vec, } /// Read `cargo-metadata` parsed output, and produce our package representation. pub fn packages_from_metadata(metadata: &cargo_metadata::Metadata) -> Vec> { metadata .workspace_packages() .into_iter() .sorted_by_key(|p| &p.name) .filter_map(|p| Package::from_cargo_metadata(p, &metadata.workspace_root)) .map(Arc::new) .collect() } impl Package { pub fn from_cargo_metadata( package_metadata: &cargo_metadata::Package, workspace_root: &Utf8Path, ) -> Option { let name = package_metadata.name.clone(); let _span = debug_span!("package", %name).entered(); let manifest_path = &package_metadata.manifest_path; debug!(%manifest_path, "walk package"); let Some(relative_dir) = manifest_path .strip_prefix(workspace_root) .ok() .and_then(|p| p.parent()) .map(ToOwned::to_owned) else { warn!( "manifest path {manifest_path:?} for package {name:?} is not within \ the detected source root path {workspace_root:?} or has no parent" ); return None; }; Some(Package { name, top_sources: package_top_sources(workspace_root, package_metadata), version: package_metadata.version.to_string(), relative_dir, }) } pub fn version_qualified_name(&self) -> String { format!("{}@{}", self.name, self.version) } } /// Find all the files that are named in the `path` of targets in a /// Cargo manifest, if the kind of the target is one that we should mutate. /// /// These are the starting points for discovering source files. fn package_top_sources( workspace_root: &Utf8Path, package_metadata: &cargo_metadata::Package, ) -> Vec { let mut found = Vec::new(); let pkg_dir = package_metadata.manifest_path.parent().unwrap(); for target in &package_metadata.targets { if should_mutate_target(target) { if let Ok(relpath) = target .src_path .strip_prefix(workspace_root) .map(ToOwned::to_owned) { debug!( "found mutation target {relpath} of kind {kind:?}", kind = target.kind ); found.push(relpath); } else { warn!("{:?} is not in {:?}", target.src_path, pkg_dir); } } else { debug!( "skipping target {:?} of kinds {:?}", target.name, target.kind ); } } found.sort(); found.dedup(); found } fn should_mutate_target(target: &cargo_metadata::Target) -> bool { target.kind.iter().any(|kind| { matches!( kind, TargetKind::Bin | TargetKind::ProcMacro | TargetKind::CDyLib | TargetKind::DyLib | TargetKind::Lib | TargetKind::RLib | TargetKind::StaticLib ) }) } /// Selection of which specific packages to mutate or test. #[derive(Debug, Clone)] #[allow(clippy::module_name_repetitions)] pub enum PackageSelection { /// All packages in the workspace. All, /// Explicitly selected packages. Explicit(Vec>), } impl PackageSelection { #[cfg(test)] pub fn one>( name: &str, version: &str, relative_dir: P, top_source: &str, ) -> Self { Self::Explicit(vec![Arc::new(Package { name: name.to_string(), version: version.to_string(), relative_dir: relative_dir.into(), top_sources: vec![top_source.into()], })]) } } cargo-mutants-25.0.0/src/path.rs000064400000000000000000000043071046102023000145710ustar 00000000000000// Copyright 2022 Martin Pool. //! Utilities for file paths. use camino::Utf8Path; /// Measures how far above its starting point a path ascends. /// /// If in following this path you would ever ascend above the starting point, /// this returns a positive number indicating the number of steps above the /// starting point. /// /// This only considers the textual content of the path, and does not look at /// symlinks or whether the directories actually exist. pub fn ascent(path: &Utf8Path) -> isize { let mut max_ascent: isize = 0; let mut ascent = 0; for component in path.components().map(|c| c.as_str()) { if component == ".." { ascent += 1; } else if component != "." { ascent -= 1; } if ascent > max_ascent { max_ascent = ascent; } } max_ascent } /// An extension trait that helps `Utf8Path` print with forward slashes, /// even on Windows. /// /// This makes the output more consistent across platforms and so easier /// to test. pub trait Utf8PathSlashes { fn to_slash_path(&self) -> String; } impl Utf8PathSlashes for Utf8Path { fn to_slash_path(&self) -> String { self.components() .map(|c| c.as_str()) .filter(|c| !c.is_empty()) .map(|c| if c == "/" || c == "\\" { "" } else { c }) .collect::>() .join("/") } } #[cfg(test)] mod test { use camino::{Utf8Path, Utf8PathBuf}; use super::{ascent, Utf8PathSlashes}; #[test] fn path_slashes_drops_empty_parts() { let mut path = Utf8PathBuf::from("/a/b/c/"); path.push("d/e/f"); assert_eq!(path.to_slash_path(), "/a/b/c/d/e/f"); } #[test] fn path_ascent() { assert_eq!(ascent(Utf8Path::new(".")), 0); assert_eq!(ascent(Utf8Path::new("..")), 1); assert_eq!(ascent(Utf8Path::new("sub/dir")), 0); assert_eq!(ascent(Utf8Path::new("sub/dir/../..")), 0); assert_eq!(ascent(Utf8Path::new("sub/../sub/./..")), 0); assert_eq!(ascent(Utf8Path::new("../back")), 1); assert_eq!(ascent(Utf8Path::new("../back/../back")), 1); assert_eq!(ascent(Utf8Path::new("../back/../../back/down")), 2); } } cargo-mutants-25.0.0/src/pretty.rs000064400000000000000000000077631046102023000151750ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Convert a token stream back to (reasonably) pretty Rust code in a string. use proc_macro2::{Delimiter, TokenTree}; use quote::ToTokens; /// Convert something to a pretty-printed string. pub(crate) trait ToPrettyString { fn to_pretty_string(&self) -> String; } /// Convert a `TokenStream` representing some code to a reasonably formatted /// string of Rust code. /// /// `TokenStream` has a `to_string`, but it adds spaces in places that don't /// look idiomatic, so this reimplements it in a way that looks better. /// /// This is probably not correctly formatted for all Rust syntax, and only tries /// to cover cases that can emerge from the code we generate. impl ToPrettyString for T where T: ToTokens, { fn to_pretty_string(&self) -> String { use TokenTree::{Group, Ident, Literal, Punct}; let mut b = String::with_capacity(200); let mut ts = self.to_token_stream().into_iter().peekable(); while let Some(tt) = ts.next() { match tt { Punct(p) => { let pc = p.as_char(); b.push(pc); if ts.peek().is_some() && (b.ends_with("->") || pc == ',' || pc == ';') { b.push(' '); } } Ident(_) | Literal(_) => { if b.ends_with('=') || b.ends_with("=>") { b.push(' '); } match tt { Literal(l) => b.push_str(&l.to_string()), Ident(i) => b.push_str(&i.to_string()), _ => unreachable!(), }; if let Some(next) = ts.peek() { match next { Ident(_) | Literal(_) => b.push(' '), Punct(p) => match p.as_char() { ',' | ';' | '<' | '>' | ':' | '.' | '!' => (), _ => b.push(' '), }, Group(_) => (), } } } Group(g) => { match g.delimiter() { Delimiter::Brace => b.push('{'), Delimiter::Bracket => b.push('['), Delimiter::Parenthesis => b.push('('), Delimiter::None => (), } b += &g.stream().to_pretty_string(); match g.delimiter() { Delimiter::Brace => b.push('}'), Delimiter::Bracket => b.push(']'), Delimiter::Parenthesis => b.push(')'), Delimiter::None => (), } } } } debug_assert!( !b.ends_with(' '), "generated a trailing space: ts={ts:?}, b={b:?}", ts = self.to_token_stream(), ); b } } #[cfg(test)] mod test { use pretty_assertions::assert_eq; use quote::quote; use super::ToPrettyString; #[test] fn pretty_format_examples() { assert_eq!( quote! { // Nonsense rust but a big salad of syntax > :: next -> Option < Self :: Item > } .to_pretty_string(), ">::next -> Option" ); assert_eq!( quote! { Lex < 'buf >::take }.to_pretty_string(), "Lex<'buf>::take" ); } #[test] fn format_trait_with_assoc_type() { assert_eq!( quote! { impl Iterator < Item = String > }.to_pretty_string(), "impl Iterator" ); } #[test] fn format_thick_arrow() { assert_eq!(quote! { a => b }.to_pretty_string(), "a => b"); } } cargo-mutants-25.0.0/src/process/unix.rs000064400000000000000000000030461046102023000162750ustar 00000000000000use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::process::{Child, Command, ExitStatus}; use anyhow::bail; use nix::errno::Errno; use nix::sys::signal::{killpg, Signal}; use nix::unistd::Pid; use tracing::warn; use crate::Result; use super::Exit; #[allow(unknown_lints, clippy::needless_pass_by_ref_mut)] // To match Windows #[mutants::skip] // hard to exercise the ESRCH edge case pub(super) fn terminate_child(child: &mut Child) -> Result<()> { let pid = Pid::from_raw(child.id().try_into().unwrap()); match killpg(pid, Signal::SIGTERM) { Ok(()) => Ok(()), Err(Errno::ESRCH) => { Ok(()) // Probably already gone } Err(Errno::EPERM) if cfg!(target_os = "macos") => { Ok(()) // If the process no longer exists then macos can return EPERM (maybe?) } Err(errno) => { // TODO: Maybe strerror? let message = format!("failed to terminate child: error {errno}"); warn!("{}", message); bail!(message); } } } #[mutants::skip] pub(super) fn configure_command(command: &mut Command) { command.process_group(0); } impl From for Exit { fn from(status: ExitStatus) -> Self { if let Some(code) = status.code() { if code == 0 { Exit::Success } else { Exit::Failure(code) } } else if let Some(signal) = status.signal() { return Exit::Signalled(signal); } else { Exit::Other } } } cargo-mutants-25.0.0/src/process/windows.rs000064400000000000000000000012201046102023000167740ustar 00000000000000use std::process::{Child, Command, ExitStatus}; use anyhow::Context; use crate::Result; use super::Exit; #[mutants::skip] // hard to exercise the ESRCH edge case pub(super) fn terminate_child(child: &mut Child) -> Result<()> { child.kill().context("Kill child") } #[mutants::skip] pub(super) fn configure_command(_command: &mut Command) {} impl From for Exit { fn from(status: ExitStatus) -> Self { if let Some(code) = status.code() { if code == 0 { Exit::Success } else { Exit::Failure(code) } } else { Exit::Other } } } cargo-mutants-25.0.0/src/process.rs000064400000000000000000000145661046102023000153230ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Manage a subprocess, with polling, timeouts, termination, and so on. //! //! On Unix, the subprocess runs as its own process group, so that any //! grandchild processes are also signalled if it's interrupted. #![warn(clippy::pedantic)] #![allow(clippy::redundant_else)] use std::ffi::OsStr; use std::process::{Child, Command, Stdio}; use std::thread::sleep; use std::time::{Duration, Instant}; use anyhow::Context; use camino::Utf8Path; use serde::Serialize; use tracing::{debug, span, trace, Level}; use crate::console::Console; use crate::interrupt::check_interrupted; use crate::output::ScenarioOutput; use crate::Result; /// How frequently to check if a subprocess finished. const WAIT_POLL_INTERVAL: Duration = Duration::from_millis(50); #[cfg(windows)] mod windows; #[cfg(windows)] use windows::{configure_command, terminate_child}; #[cfg(unix)] mod unix; #[cfg(unix)] use unix::{configure_command, terminate_child}; pub struct Process { child: Child, start: Instant, timeout: Option, } impl Process { /// Run a subprocess to completion, watching for interrupts, with a timeout, while /// ticking the progress bar. pub fn run( argv: &[String], env: &[(String, String)], cwd: &Utf8Path, timeout: Option, jobserver: Option<&jobserver::Client>, scenario_output: &mut ScenarioOutput, console: &Console, ) -> Result { let mut child = Process::start(argv, env, cwd, timeout, jobserver, scenario_output)?; let process_status = loop { if let Some(exit_status) = child.poll()? { break exit_status; } console.tick(); sleep(WAIT_POLL_INTERVAL); }; scenario_output.message(&format!("result: {process_status:?}"))?; Ok(process_status) } /// Launch a process, and return an object representing the child. pub fn start( argv: &[String], env: &[(String, String)], cwd: &Utf8Path, timeout: Option, jobserver: Option<&jobserver::Client>, scenario_output: &mut ScenarioOutput, ) -> Result { let start = Instant::now(); let quoted_argv = quote_argv(argv); scenario_output.message("ed_argv)?; debug!(%quoted_argv, "start process"); let os_env = env.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v))); let mut command = Command::new(&argv[0]); command .args(&argv[1..]) .envs(os_env) .stdin(Stdio::null()) .stdout(scenario_output.open_log_append()?) .stderr(scenario_output.open_log_append()?) .current_dir(cwd); if let Some(js) = jobserver { js.configure(&mut command); } configure_command(&mut command); let child = command .spawn() .with_context(|| format!("failed to spawn {}", argv.join(" ")))?; Ok(Process { child, start, timeout, }) } /// Check if the child process has finished; if so, return its status. #[mutants::skip] // It's hard to avoid timeouts if this never works... pub fn poll(&mut self) -> Result> { if self.timeout.is_some_and(|t| self.start.elapsed() > t) { debug!("timeout, terminating child process...",); self.terminate()?; Ok(Some(Exit::Timeout)) } else if let Err(e) = check_interrupted() { debug!("interrupted, terminating child process..."); self.terminate()?; Err(e) } else if let Some(status) = self.child.try_wait()? { Ok(Some(status.into())) } else { Ok(None) } } /// Terminate the subprocess, initially gently and then harshly. /// /// Blocks until the subprocess is terminated and then returns the exit status. /// /// The status might not be `Timeout` if this raced with a normal exit. #[mutants::skip] // would leak processes from tests if skipped fn terminate(&mut self) -> Result<()> { let _span = span!(Level::DEBUG, "terminate_child", pid = self.child.id()).entered(); debug!("terminating child process"); terminate_child(&mut self.child)?; trace!("wait for child after termination"); match self.child.wait() { Err(err) => debug!(?err, "Failed to wait for child after termination"), Ok(exit) => debug!("terminated child exit status {exit:?}"), } Ok(()) } } /// The result of running a single child process. #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)] pub enum Exit { /// Exited with status 0. Success, /// Exited with status non-0. Failure(i32), /// Exceeded its timeout, and killed. Timeout, /// Killed by some signal. #[cfg(unix)] Signalled(i32), /// Unknown or unexpected situation. Other, } impl Exit { pub fn is_success(self) -> bool { self == Exit::Success } pub fn is_timeout(self) -> bool { self == Exit::Timeout } pub fn is_failure(self) -> bool { matches!(self, Exit::Failure(_)) } } /// Quote an argv slice in Unix shell style. /// /// This isn't guaranteed to match the interpretation of a shell or to be safe. /// It's just for debug logs. fn quote_argv, I: IntoIterator>(argv: I) -> String { let mut r = String::new(); for s in argv { if !r.is_empty() { r.push(' '); } for c in s.as_ref().chars() { match c { '\t' => r.push_str(r"\t"), '\n' => r.push_str(r"\n"), '\r' => r.push_str(r"\r"), ' ' | '\\' | '\'' | '"' => { r.push('\\'); r.push(c); } _ => r.push(c), } } } r } #[cfg(test)] mod test { use super::quote_argv; #[test] fn shell_quoting() { assert_eq!(quote_argv(["foo".to_string()]), "foo"); assert_eq!( quote_argv(["foo bar", r"\blah\x", r#""quoted""#]), r#"foo\ bar \\blah\\x \"quoted\""# ); assert_eq!(quote_argv([""]), ""); assert_eq!( quote_argv(["with whitespace", "\r\n\t\t"]), r"with\ whitespace \r\n\t\t" ); } } cargo-mutants-25.0.0/src/scenario.rs000064400000000000000000000020271046102023000154350ustar 00000000000000// Copyright 2021-2025 Martin Pool use serde::Serialize; use std::fmt; use crate::Mutant; /// A scenario is either a freshening build in the source tree, a baseline test with no mutations, or a mutation test. #[derive(Clone, Eq, PartialEq, Debug, Serialize)] pub enum Scenario { /// Build in a copy of the source tree but with no mutations applied. Baseline, /// Build with a mutation applied. Mutant(Mutant), } impl fmt::Display for Scenario { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Scenario::Baseline => f.write_str("baseline"), Scenario::Mutant(mutant) => f.write_str(&mutant.name(true)), } } } impl Scenario { pub fn is_mutant(&self) -> bool { matches!(self, Scenario::Mutant { .. }) } /// Return a reference to the mutant, if there is one. pub fn mutant(&self) -> Option<&Mutant> { match self { Scenario::Baseline => None, Scenario::Mutant(mutant) => Some(mutant), } } } cargo-mutants-25.0.0/src/shard.rs000064400000000000000000000040371046102023000147360ustar 00000000000000// Copyright 2023 Martin Pool //! Sharding parameters. use std::str::FromStr; use anyhow::{anyhow, ensure, Context, Error}; /// Select mutants for a particular shard of the total list. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct Shard { /// Index modulo n. pub k: usize, /// Modulus of sharding. pub n: usize, } impl Shard { /// Select the mutants that should be run for this shard. pub fn select>(&self, mutants: I) -> Vec { mutants .into_iter() .enumerate() .filter_map(|(i, m)| if i % self.n == self.k { Some(m) } else { None }) .collect() } } impl FromStr for Shard { type Err = Error; fn from_str(s: &str) -> Result { let parts = s.split_once('/').ok_or(anyhow!("shard must be k/n"))?; let k = parts.0.parse().context("shard k")?; let n = parts.1.parse().context("shard n")?; ensure!(k < n, "shard k must be less than n"); // implies n>0 Ok(Shard { k, n }) } } #[cfg(test)] mod tests { use super::*; #[test] fn shard_from_str_valid_input() { let shard = Shard::from_str("2/5").unwrap(); assert_eq!(shard.k, 2); assert_eq!(shard.n, 5); assert_eq!(shard, Shard { k: 2, n: 5 }); } #[test] fn shard_from_str_invalid_input() { assert_eq!( Shard::from_str("").unwrap_err().to_string(), "shard must be k/n" ); assert_eq!( Shard::from_str("2").unwrap_err().to_string(), "shard must be k/n" ); assert_eq!( Shard::from_str("2/0").unwrap_err().to_string(), "shard k must be less than n" ); assert_eq!( Shard::from_str("5/2").unwrap_err().to_string(), "shard k must be less than n" ); } #[test] fn shard_select() { assert_eq!( Shard::from_str("1/4").unwrap().select(0..10).as_slice(), &[1, 5, 9] ); } } cargo-mutants-25.0.0/src/source.rs000064400000000000000000000124701046102023000151350ustar 00000000000000// Copyright 2021-2025 Martin Pool //! Access to a Rust source tree and files. use std::fs::read_to_string; use std::sync::Arc; use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; #[allow(unused_imports)] use tracing::{debug, info, warn}; use crate::package::Package; use crate::path::{ascent, Utf8PathSlashes}; use crate::span::LineColumn; /// A Rust source file within a source tree. /// /// It can be viewed either relative to the source tree (for display) /// or as a path that can be opened (relative to cwd or absolute.) /// /// Code is normalized to Unix line endings as it's read in, and modified /// files are written with Unix line endings. #[derive(Clone, Debug, Eq, PartialEq)] #[allow(clippy::module_name_repetitions)] pub struct SourceFile { /// What package includes this file? pub package: Arc, /// Path of this source file relative to workspace. pub tree_relative_path: Utf8PathBuf, /// Full copy of the unmodified source. /// /// This is held in an [Arc] so that `SourceFile`s can be cloned without using excessive /// amounts of memory. pub code: Arc, /// True if this is the top source file for its target: typically but /// not always `lib.rs` or `main.rs`. pub is_top: bool, } impl SourceFile { /// Construct a `SourceFile` representing a file within a tree. /// /// This eagerly loads the text of the file. /// /// This also skip files outside of the tree, returning `Ok(None)`. pub fn load( tree_path: &Utf8Path, tree_relative_path: &Utf8Path, package: &Package, is_top: bool, ) -> Result> { // TODO: Perhaps the caller should be responsible for checking this? if ascent(tree_relative_path) > 0 { warn!( "skipping source outside of tree: {:?}", tree_relative_path.to_slash_path() ); return Ok(None); } let full_path = tree_path.join(tree_relative_path); let code = Arc::new( read_to_string(&full_path) .with_context(|| format!("failed to read source of {full_path:?}"))? .replace("\r\n", "\n"), ); Ok(Some(SourceFile { tree_relative_path: tree_relative_path.to_owned(), code, package: Arc::new(package.clone()), is_top, })) } /// Construct from in-memory text. #[cfg(test)] pub fn for_tests>( tree_relative_path: P, code: &str, package_name: &str, is_top: bool, ) -> SourceFile { let tree_relative_path = tree_relative_path.into(); let top_sources = if is_top { vec![tree_relative_path.clone()] } else { vec!["src/lib.rs".into()] }; SourceFile { tree_relative_path, code: Arc::new(code.to_owned()), package: Arc::new(Package { name: package_name.to_owned(), relative_dir: Utf8PathBuf::new(), top_sources, version: "0.1.0".to_owned(), }), is_top, } } /// Return the path of this file relative to the tree root, with forward slashes. pub fn tree_relative_slashes(&self) -> String { self.tree_relative_path.to_slash_path() } pub fn path(&self) -> &Utf8Path { self.tree_relative_path.as_path() } pub fn code(&self) -> &str { self.code.as_str() } /// Format a location within this source file for display to the user pub fn format_source_location(&self, location: LineColumn) -> String { let source_file = self.tree_relative_slashes(); let LineColumn { line, column } = location; format!("{source_file}:{line}:{column}") } } #[cfg(test)] mod test { use std::fs::File; use std::io::Write; use pretty_assertions::assert_eq; use super::*; #[test] fn source_file_normalizes_crlf() { let temp_dir = tempfile::tempdir().unwrap(); let temp_dir_path = Utf8Path::from_path(temp_dir.path()).unwrap(); let file_name = "lib.rs"; File::create(temp_dir.path().join(file_name)) .unwrap() .write_all(b"fn main() {\r\n 640 << 10;\r\n}\r\n") .unwrap(); let package = Package { name: "imaginary-package".to_owned(), relative_dir: Utf8PathBuf::from(""), top_sources: vec!["src/lib.rs".into()], version: "0.1.0".to_owned(), }; let source_file = SourceFile::load(temp_dir_path, Utf8Path::new(file_name), &package, true) .unwrap() .unwrap(); assert_eq!(source_file.code(), "fn main() {\n 640 << 10;\n}\n"); } #[test] fn skips_files_outside_of_workspace() { let package = Arc::new(Package { name: "imaginary-package".to_owned(), relative_dir: Utf8PathBuf::from(""), top_sources: vec!["src/lib.rs".into()], version: "0.1.0".to_owned(), }); let source_file = SourceFile::load( Utf8Path::new("unimportant"), Utf8Path::new("../outside_workspace.rs"), &package, true, ) .unwrap(); assert_eq!(source_file, None); } } cargo-mutants-25.0.0/src/span.rs000064400000000000000000000165711046102023000146040ustar 00000000000000// Copyright 2021-2023 Martin Pool //! Locations (line/column) and spans between them in source code. //! //! This is similar to, and can be automatically derived from, //! `proc_macro2::Span` and `proc_macro2::LineColumn`, but is //! a bit more convenient for our purposes. use std::fmt; use serde::Serialize; /// A (line, column) position in a source file. #[derive(Clone, Copy, Eq, PartialEq, Serialize)] pub struct LineColumn { /// 1-based line number. pub line: usize, /// 1-based column, measured in chars. pub column: usize, } impl From for LineColumn { fn from(l: proc_macro2::LineColumn) -> Self { LineColumn { line: l.line, column: l.column + 1, } } } impl fmt::Debug for LineColumn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "LineColumn({}, {})", self.line, self.column) } } /// A contiguous text span in a file. #[derive(Clone, Copy, Eq, PartialEq, Serialize)] pub struct Span { /// The *inclusive* position where the span starts. pub start: LineColumn, /// The *exclusive* position where the span ends. pub end: LineColumn, } impl Span { #[allow(dead_code)] pub fn quad( start_line: usize, start_column: usize, end_line: usize, end_column: usize, ) -> Self { Span { start: LineColumn { line: start_line, column: start_column, }, end: LineColumn { line: end_line, column: end_column, }, } } /// Return the region of a multi-line string that this span covers. pub fn extract(&self, s: &str) -> String { let mut r = String::new(); let mut line_no = 1; let mut col_no = 1; let start = self.start; let end = self.end; for c in s.chars() { if ((line_no == start.line && col_no >= start.column) || line_no > start.line) && (line_no < end.line || (line_no == end.line && col_no < end.column)) { r.push(c); } if c == '\n' { line_no += 1; if line_no > end.line { break; } col_no = 1; } else if c == '\r' { // counts as part of the last column, not a separate column } else { col_no += 1; } if line_no == end.line && col_no >= end.column { break; } } r } /// Replace a subregion of text. /// /// Returns a copy of `s` with the region identified by this span replaced by /// `replacement`. pub fn replace(&self, s: &str, replacement: &str) -> String { let mut r = String::with_capacity(s.len() + replacement.len()); let mut line_no = 1; let mut col_no = 1; let start = self.start; let end = self.end; for c in s.chars() { if line_no == start.line && col_no == start.column { r.push_str(replacement); } if line_no < start.line || line_no > end.line || (line_no == start.line && col_no < start.column) || (line_no == end.line && col_no >= end.column) { r.push(c); } if c == '\n' { line_no += 1; col_no = 1; } else if c == '\r' { // counts as part of the last column, not a separate column } else { col_no += 1; } } if line_no == start.line && col_no == start.column { r.push_str(replacement); } r } } impl From for Span { fn from(s: proc_macro2::Span) -> Self { Span { start: s.start().into(), end: s.end().into(), } } } impl From<&proc_macro2::Span> for Span { fn from(s: &proc_macro2::Span) -> Self { Span { start: s.start().into(), end: s.end().into(), } } } impl From for Span { fn from(s: proc_macro2::extra::DelimSpan) -> Self { // Get the span for the whole block from the start delimiter // to the end. let joined = s.join(); Span { start: joined.start().into(), end: joined.end().into(), } } } impl fmt::Debug for Span { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // A concise form, similar to ::quad write!( f, "Span({}, {}, {}, {})", self.start.line, self.start.column, self.end.line, self.end.column ) } } #[cfg(test)] mod test { use indoc::indoc; // use pretty_assertions::assert_eq; use super::*; #[test] fn linecolumn_debug_form() { let lc = LineColumn { line: 1, column: 2 }; assert_eq!(format!("{lc:?}"), "LineColumn(1, 2)"); } #[test] fn span_debug_form() { let span = Span::quad(1, 2, 3, 4); assert_eq!(format!("{span:?}"), "Span(1, 2, 3, 4)"); } #[test] fn cut_before_crlf() { let source = "fn foo() {\r\n wibble();\r\n}\r\n//hey!\r\n"; let span = Span::quad(1, 10, 3, 2); assert_eq!(span.extract(source), "{\r\n wibble();\r\n}"); assert_eq!(span.replace(source, "{}"), "fn foo() {}\r\n//hey!\r\n"); } #[test] fn empty_span_in_empty_string() { let span = Span::quad(1, 1, 1, 1); assert_eq!(span.extract(""), ""); assert_eq!(span.replace("", "x"), "x"); } #[test] fn empty_span_at_start_of_string() { let span = Span::quad(1, 1, 1, 1); assert_eq!(span.extract("hello"), ""); assert_eq!(span.replace("hello", "x"), "xhello"); } #[test] fn empty_span_at_end_of_string() { let span = Span::quad(1, 6, 1, 6); assert_eq!(span.extract("hello"), ""); assert_eq!(span.replace("hello", "x"), "hellox"); } #[test] fn cut_including_crlf() { let source = "fn foo() {\r\n wibble();\r\n}\r\n//hey!\r\n"; let span = Span::quad(1, 10, 3, 3); assert_eq!(span.extract(source), "{\r\n wibble();\r\n}\r\n"); assert_eq!(span.replace(source, "{}\r\n"), "fn foo() {}\r\n//hey!\r\n"); } #[test] fn span_ops() { let source = indoc! { r" fn foo() { some(); stuff(); } const BAR: u32 = 32; " }; // typical multi-line case let span = Span::quad(2, 10, 5, 2); assert_eq!(span.extract(source), "{\n some();\n stuff();\n}"); let replaced = span.replace(source, "{ /* body deleted */ }"); assert_eq!( replaced, indoc! { r" fn foo() { /* body deleted */ } const BAR: u32 = 32; " } ); // single-line case let span = Span::quad(7, 18, 7, 20); assert_eq!(span.extract(source), "32"); let replaced = span.replace(source, "69"); assert_eq!( replaced, indoc! { r" fn foo() { some(); stuff(); } const BAR: u32 = 69; " } ); } } cargo-mutants-25.0.0/src/tail_file.rs000064400000000000000000000073521046102023000155700ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Tail a log file: watch for new writes and return the last line. use std::fs::File; use std::io::Read; use anyhow::Context; use crate::Result; /// Tail a log file, and return the last non-empty line seen. /// /// This assumes that the log file always receives whole lines as atomic writes, which /// is typical. If the file is being written by a process that writes partial lines, /// this won't panic or error but it may not return whole correct lines. pub struct TailFile { file: File, /// The last non-empty line we've seen in the file so far. last_line_seen: String, read_buf: Vec, } impl TailFile { /// Watch lines appended to the given file, which should be open for reading. pub fn new(file: File) -> TailFile { TailFile { file, last_line_seen: String::new(), read_buf: Vec::new(), } } /// Return the last non-empty, non-whitespace line from this file, or an empty string /// if none have been seen yet. /// /// Non-UTF8 content is lost. pub fn last_line(&mut self) -> Result<&str> { self.read_buf.clear(); let n_read = self .file .read_to_end(&mut self.read_buf) .context("Read tail of log file")?; if n_read > 0 { if let Some(new_last) = String::from_utf8_lossy(&self.read_buf) .lines() .filter(|l| !l.trim().is_empty()) .last() { new_last.clone_into(&mut self.last_line_seen); } } Ok(self.last_line_seen.as_str()) } } #[cfg(test)] mod test { use camino::Utf8PathBuf; use std::io::Write; use super::*; #[test] fn last_line_of_file() { let mut tempfile = tempfile::NamedTempFile::new().unwrap(); let path: Utf8PathBuf = tempfile.path().to_owned().try_into().unwrap(); let reopened = File::open(&path).unwrap(); let mut tailer = TailFile::new(reopened); assert_eq!( tailer.last_line().unwrap(), "", "empty file has an empty last line" ); tempfile.write_all(b"hello").unwrap(); assert_eq!( tailer.last_line().unwrap(), "hello", "single line file with no terminator has that line as last line" ); tempfile.write_all(b"\n\n\n").unwrap(); assert_eq!( tailer.last_line().unwrap(), "hello", "trailing blank lines are ignored" ); tempfile.write_all(b"that's all folks!\n").unwrap(); assert_eq!( tailer.last_line().unwrap(), "that's all folks!", "newline terminated last line is returned" ); tempfile.write_all(b"").unwrap(); assert_eq!( tailer.last_line().unwrap(), "that's all folks!", "touched but unchanged file returns the same last line" ); // These cases of partial writes aren't supported, because they don't seem to occur in // cargo/rustc output. // tempfile.write_all(b"word ").unwrap(); // assert_eq!( // tailer.last_line().unwrap(), // "word ", // "see one word from an incomplete line" // ); // tempfile.write_all(b"word2 ").unwrap(); // assert_eq!( // tailer.last_line().unwrap(), // "word word2 ", // "see two words from an incomplete line" // ); // tempfile.write_all(b"word3\n").unwrap(); // assert_eq!( // tailer.last_line().unwrap(), // "word word2 word3", // "the same line is continued and finished" // ); } } cargo-mutants-25.0.0/src/timeouts.rs000064400000000000000000000173371046102023000155150ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Calculation of timeouts for the build and test phases. use std::{cmp::max, time::Duration}; use tracing::{info, warn}; use crate::{ options::Options, outcome::{Phase, ScenarioOutcome}, }; #[derive(Debug, Copy, Clone)] pub struct Timeouts { pub build: Option, pub test: Option, } impl Timeouts { pub fn for_baseline(options: &Options) -> Timeouts { Timeouts { test: options.test_timeout, build: None, } } pub fn from_baseline(baseline: &ScenarioOutcome, options: &Options) -> Timeouts { Timeouts { build: build_timeout( baseline.phase_result(Phase::Build).map(|pr| pr.duration), options, ), test: Some(test_timeout( baseline.phase_result(Phase::Test).map(|pr| pr.duration), options, )), } } pub fn without_baseline(options: &Options) -> Timeouts { Timeouts { build: build_timeout(None, options), test: Some(test_timeout(None, options)), } } } const FALLBACK_TIMEOUT_SECS: u64 = 300; fn warn_fallback_timeout(phase_name: &str, option: &str) { warn!("An explicit {phase_name} timeout is recommended when using {option}; using {FALLBACK_TIMEOUT_SECS} seconds by default"); } fn test_timeout(baseline_duration: Option, options: &Options) -> Duration { if let Some(explicit) = options.test_timeout { explicit } else if let Some(baseline_duration) = baseline_duration { let timeout = max( options.minimum_test_timeout, Duration::from_secs_f64( (baseline_duration.as_secs_f64() * options.test_timeout_multiplier.unwrap_or(5.0)) .ceil(), ), ); if options.show_times { info!( "Auto-set test timeout to {}", humantime::format_duration(timeout) ); } timeout } else if options.check_only { // We won't have run baseline tests, and we won't run any other tests either. Duration::from_secs(0) } else { warn_fallback_timeout("test", "--baseline=skip"); Duration::from_secs(FALLBACK_TIMEOUT_SECS) } } fn build_timeout(baseline_duration: Option, options: &Options) -> Option { if let Some(t) = options.build_timeout { Some(t) } else if let Some(baseline) = baseline_duration { if let Some(multiplier) = options.build_timeout_multiplier { let timeout = Duration::from_secs_f64(baseline.as_secs_f64() * multiplier); if options.show_times { info!( "Auto-set build timeout to {}", humantime::format_duration(timeout) ); } Some(timeout) } else { None } } else { None } } #[cfg(test)] mod test { use std::str::FromStr; use clap::Parser; use indoc::indoc; use super::*; use crate::{config::Config, Args}; #[test] fn timeout_multiplier_from_option() { let options = Options::from_arg_strs(["mutants", "--timeout-multiplier", "1.5"]); assert_eq!(options.test_timeout_multiplier, Some(1.5)); assert_eq!( test_timeout(Some(Duration::from_secs(40)), &options), Duration::from_secs(60), ); } #[test] fn test_timeout_unaffected_by_in_place_build() { let options = Options::from_arg_strs(["mutants", "--timeout-multiplier", "1.5", "--in-place"]); assert_eq!( test_timeout(Some(Duration::from_secs(40)), &options), Duration::from_secs(60), ); } #[test] fn build_timeout_multiplier_from_option() { let args = Args::try_parse_from(["mutants", "--build-timeout-multiplier", "1.5"]).unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.build_timeout_multiplier, Some(1.5)); assert_eq!( build_timeout(Some(Duration::from_secs(40)), &options), Some(Duration::from_secs(60)), ); } #[test] fn build_timeout_is_affected_by_in_place_build() { let args = Args::try_parse_from(["mutants", "--build-timeout-multiplier", "5", "--in-place"]) .unwrap(); let config = Config::default(); let options = Options::new(&args, &config).unwrap(); assert_eq!( build_timeout(Some(Duration::from_secs(40)), &options), Some(Duration::from_secs(40 * 5)) ); } #[test] fn timeout_multiplier_from_config() { let args = Args::try_parse_from(["mutants"]).unwrap(); let config = Config::from_str(indoc! {r#" timeout_multiplier = 2.0 "#}) .unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.test_timeout_multiplier, Some(2.0)); assert_eq!( test_timeout(Some(Duration::from_secs(42)), &options), Duration::from_secs(42 * 2), ); } #[test] fn build_timeout_multiplier_from_config() { let args = Args::try_parse_from(["mutants"]).unwrap(); let config = Config::from_str(indoc! {r#" build_timeout_multiplier = 2.0 "#}) .unwrap(); let options = Options::new(&args, &config).unwrap(); assert_eq!(options.build_timeout_multiplier, Some(2.0)); assert_eq!( build_timeout(Some(Duration::from_secs(42)), &options), Some(Duration::from_secs(42 * 2)), ); } #[test] fn timeout_multiplier_default() { let args = Args::try_parse_from(["mutants"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.test_timeout_multiplier, None); assert_eq!( test_timeout(Some(Duration::from_secs(42)), &options), Duration::from_secs(42 * 5), ); } #[test] fn build_timeout_multiplier_default() { let args = Args::try_parse_from(["mutants"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.build_timeout_multiplier, None); assert_eq!(build_timeout(Some(Duration::from_secs(42)), &options), None,); } #[test] fn timeout_from_option() { let args = Args::try_parse_from(["mutants", "--timeout=8"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.test_timeout, Some(Duration::from_secs(8))); } #[test] fn build_timeout_from_option() { let args = Args::try_parse_from(["mutants", "--build-timeout=4"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.build_timeout, Some(Duration::from_secs(4))); } #[test] fn no_default_build_timeout() { let args = Args::try_parse_from(["mutants"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.build_timeout, None); } #[test] fn timeout_multiplier_default_with_baseline_skip() { // The --baseline option is not used to set the timeout but it's // indicative of the realistic situation. let args = Args::try_parse_from(["mutants", "--baseline", "skip"]).unwrap(); let options = Options::new(&args, &Config::default()).unwrap(); assert_eq!(options.test_timeout_multiplier, None); assert_eq!(test_timeout(None, &options), Duration::from_secs(300)); assert_eq!(build_timeout(None, &options), None); } } cargo-mutants-25.0.0/src/visit.rs000064400000000000000000001117661046102023000150030ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Visit all the files in a source tree, and then the AST of each file, //! to discover mutation opportunities. //! //! Walking the tree starts with some root files known to the build tool: //! e.g. for cargo they are identified from the targets. The tree walker then //! follows `mod` statements to recursively visit other referenced files. #![warn(clippy::pedantic)] use std::collections::VecDeque; use std::sync::Arc; use std::vec; use camino::{Utf8Path, Utf8PathBuf}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, ToTokens}; use syn::ext::IdentExt; use syn::spanned::Spanned; use syn::visit::Visit; use syn::{Attribute, BinOp, Block, Expr, ExprPath, File, ItemFn, ReturnType, Signature, UnOp}; use tracing::{debug, debug_span, error, info, trace, trace_span, warn}; use crate::fnvalue::return_type_replacements; use crate::mutate::Function; use crate::package::Package; use crate::pretty::ToPrettyString; use crate::source::SourceFile; use crate::span::Span; use crate::{check_interrupted, Console, Context, Genre, Mutant, Options, Result}; /// Mutants and files discovered in a source tree. /// /// Files are listed separately so that we can represent files that /// were visited but that produced no mutants. pub struct Discovered { pub mutants: Vec, pub files: Vec, } impl Discovered { pub(crate) fn remove_previously_caught(&mut self, previously_caught: &[String]) { self.mutants.retain(|m| { let name = m.name(true); let c = previously_caught.contains(&name); if c { trace!(?name, "skip previously caught mutant"); } !c }); } } /// Discover all mutants and all source files. /// /// The returned `Discovered` struct contains all mutants found in the /// source tree, and also a list of all source files visited (whether /// they generated mutants or not). pub fn walk_tree( workspace_dir: &Utf8Path, packages: &[Arc], options: &Options, console: &Console, ) -> Result { let mut mutants = Vec::new(); let mut files = Vec::new(); let error_exprs = options.parsed_error_exprs()?; console.walk_tree_start(); for package in packages { walk_package( workspace_dir, package, &error_exprs, &mut mutants, &mut files, options, console, )?; } console.walk_tree_done(); Ok(Discovered { mutants, files }) } /// Walk one package, starting from its top files, discovering files /// and mutants. #[allow(clippy::from_iter_instead_of_collect)] fn walk_package( workspace_dir: &Utf8Path, package: &Package, error_exprs: &[Expr], mutants: &mut Vec, files: &mut Vec, options: &Options, console: &Console, ) -> Result<()> { let mut filename_queue = VecDeque::from_iter(package.top_sources.iter().map(|p| (p.to_owned(), true))); while let Some((path, package_top)) = filename_queue.pop_front() { let Some(source_file) = SourceFile::load(workspace_dir, &path, package, package_top)? else { info!("Skipping source file outside of tree: {path:?}"); continue; }; console.walk_tree_update(files.len(), mutants.len()); check_interrupted()?; let (mut file_mutants, external_mods) = walk_file(&source_file, error_exprs, options)?; // TODO: It would be better not to spend time generating mutants from // files that are not going to be visited later. However, we probably do // still want to walk them to find modules that are referenced by them. // since otherwise it could be pretty confusing that lower files are not // visited. // // We'll still walk down through files that don't match globs, so that // we have a chance to find modules underneath them. However, we won't // collect any mutants from them, and they don't count as "seen" for // `--list-files`. for mod_namespace in &external_mods { if let Some(mod_path) = find_mod_source(workspace_dir, &source_file, mod_namespace) { filename_queue.push_back((mod_path, false)); } } if !options.allows_source_file_path(&source_file.tree_relative_path) { continue; } file_mutants.retain(|m| options.allows_mutant(m)); mutants.append(&mut file_mutants); files.push(source_file); } Ok(()) } /// Find all possible mutants in a source file. /// /// Returns the mutants found, and the names of modules referenced by `mod` statements /// that should be visited later. fn walk_file( source_file: &SourceFile, error_exprs: &[Expr], options: &Options, ) -> Result<(Vec, Vec)> { let _span = debug_span!("source_file", path = source_file.tree_relative_slashes()).entered(); debug!("visit source file"); let syn_file = syn::parse_str::(source_file.code()) .with_context(|| format!("failed to parse {}", source_file.tree_relative_slashes()))?; let mut visitor = DiscoveryVisitor { error_exprs, external_mods: Vec::new(), mutants: Vec::new(), mod_namespace_stack: Vec::new(), namespace_stack: Vec::new(), fn_stack: Vec::new(), source_file: source_file.clone(), options, }; visitor.visit_file(&syn_file); Ok((visitor.mutants, visitor.external_mods)) } /// For testing: parse and generate mutants from one single file provided as a string. /// /// The source code is assumed to be named `src/main.rs` with a fixed package name. #[cfg(test)] pub fn mutate_source_str(code: &str, options: &Options) -> Result> { let source_file = SourceFile::for_tests( Utf8Path::new("src/main.rs"), code, "cargo-mutants-testdata-internal", true, ); let (mutants, _) = walk_file(&source_file, &options.parsed_error_exprs()?, options)?; Ok(mutants) } /// Reference to an external module from a source file. /// /// This is approximately a list of namespace components like `["foo", "bar"]` for /// `foo::bar`, but each may also be decorated with a `#[path="..."]` attribute, /// and they're attributed to a location in the source. #[derive(Clone, Debug, PartialEq, Eq)] struct ExternalModRef { /// Namespace components of the module path parts: Vec, } /// Namespace for a module defined in a `mod foo { ... }` block or `mod foo;` statement /// /// In the context of resolving modules, a module "path" (and to some extent "name") is ambiguous: /// paths may describe a sequence of identifiers in code (e.g. `crate::foo::bar`) or sequence of /// folder and file names on the filesystem (e.g. `src/foo/bar.rs`). /// /// The field and method names in this struct distinguish between the uses of path elements. #[derive(Clone, Debug, PartialEq, Eq)] struct ModNamespace { /// Identifier of the module (e.g. `foo` for `mod foo;`) name: String, /// File location override for the module, if specified by `#[path="file"]` /// /// Note that `mod foo { ... }` blocks can also have a file location specified, /// which affects the filesystem location of all child `mod bar;` statements. path_attribute: Option, /// Location of the module definition in the source file source_location: Span, } impl ModNamespace { /// Returns the name of the module for filesystem purposes fn get_filesystem_name(&self) -> &Utf8Path { self.path_attribute .as_ref() .map_or(Utf8Path::new(&self.name), Utf8PathBuf::as_path) } } /// `syn` visitor that recursively traverses the syntax tree, accumulating places /// that could be mutated. /// /// As it walks the tree, it accumulates within itself a list of mutation opportunities, /// and other files referenced by `mod` statements that should be visited later. struct DiscoveryVisitor<'o> { /// All the mutants generated by visiting the file. mutants: Vec, /// The file being visited. source_file: SourceFile, /// The stack of modules namespaces that we're currently inside, from /// visiting `mod foo { ... }` statements. /// /// This is a subsequence of `namespace_stack` (with `#[path="..."]` information), /// containing only elements that form a module path. mod_namespace_stack: Vec, /// The stack of namespaces, loosely defined, that we're inside. /// /// Basically these are names or strings that can be concatenated with `::` /// to form a name that meaningfully describes where we are; it might not /// exactly be valid Rust. /// /// For example, this includes mods, fns, impls, etc. namespace_stack: Vec, /// The functions we're inside. /// /// Empty at the top level, often has one element, but potentially more if /// there are nested functions. fn_stack: Vec>, /// The names from `mod foo;` statements that should be visited later, /// namespaced relative to the source file external_mods: Vec, /// Parsed error expressions, from the config file or command line. error_exprs: &'o [Expr], options: &'o Options, } impl DiscoveryVisitor<'_> { fn enter_function( &mut self, function_name: &Ident, return_type: &ReturnType, span: proc_macro2::Span, ) -> Arc { self.namespace_stack.push(function_name.to_string()); let full_function_name = self.namespace_stack.join("::"); let function = Arc::new(Function { function_name: full_function_name, return_type: return_type.to_pretty_string(), span: span.into(), }); self.fn_stack.push(Arc::clone(&function)); function } fn leave_function(&mut self, function: Arc) { self.namespace_stack .pop() .expect("Namespace stack should not be empty"); assert_eq!( self.fn_stack.pop(), Some(function), "Function stack mismatch" ); } /// Record that we generated some mutants. fn collect_mutant(&mut self, span: Span, replacement: &TokenStream, genre: Genre) { self.mutants.push(Mutant { source_file: self.source_file.clone(), function: self.fn_stack.last().cloned(), span, replacement: replacement.to_pretty_string(), genre, }); } fn collect_fn_mutants(&mut self, sig: &Signature, block: &Block) { if let Some(function) = self.fn_stack.last().cloned() { let body_span = function_body_span(block).expect("Empty function body"); let repls = return_type_replacements(&sig.output, self.error_exprs); if repls.is_empty() { debug!( function_name = function.function_name, return_type = function.return_type, "No mutants generated for this return type" ); } else { let orig_block = block.to_token_stream().to_pretty_string(); for rep in repls { // Comparing strings is a kludge for proc_macro2 not (yet) apparently // exposing any way to compare token streams... // // TODO: Maybe this should move into collect_mutant, but at the moment // FnValue is the only genre that seems able to generate no-ops. // // The original block has braces and the replacements don't, so put // them back for the comparison... let new_block = quote!( { #rep } ).to_token_stream().to_pretty_string(); // dbg!(&orig_block, &new_block); if orig_block == new_block { debug!("Replacement is the same as the function body; skipping"); } else { self.collect_mutant(body_span, &rep, Genre::FnValue); } } } } else { warn!("collect_fn_mutants called while not in a function?"); } } /// Call a function with a namespace pushed onto the stack. /// /// This is used when recursively descending into a namespace. fn in_namespace(&mut self, name: &str, f: F) -> T where F: FnOnce(&mut Self) -> T, { self.namespace_stack.push(name.to_owned()); let r = f(self); assert_eq!(self.namespace_stack.pop().unwrap(), name); r } } impl<'ast> Visit<'ast> for DiscoveryVisitor<'_> { fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) { let _span = trace_span!("expr_call", line = i.span().start().line).entered(); if attrs_excluded(&i.attrs) { return; } if let Expr::Path(ExprPath { path, .. }) = &*i.func { debug!(path = path.to_pretty_string(), "visit call"); if let Some(hit) = self .options .skip_calls .iter() .find(|s| path_ends_with(path, s)) { trace!("skip call to {hit}"); return; } } syn::visit::visit_expr_call(self, i); } fn visit_expr_method_call(&mut self, i: &'ast syn::ExprMethodCall) { let _span = trace_span!("expr_method_call", line = i.span().start().line).entered(); if attrs_excluded(&i.attrs) { return; } if let Some(hit) = self.options.skip_calls.iter().find(|s| i.method == s) { trace!("skip method call to {hit}"); return; } syn::visit::visit_expr_method_call(self, i); } /// Visit a source file. fn visit_file(&mut self, i: &'ast File) { // No trace here; it's created per file for the whole visitor if attrs_excluded(&i.attrs) { trace!("file excluded by attrs"); return; } syn::visit::visit_file(self, i); } /// Visit top-level `fn foo()`. fn visit_item_fn(&mut self, i: &'ast ItemFn) { let function_name = i.sig.ident.to_pretty_string(); let _span = trace_span!( "fn", line = i.sig.fn_token.span.start().line, name = function_name ) .entered(); trace!("visit fn"); if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || block_is_empty(&i.block) { return; } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, &i.block); syn::visit::visit_item_fn(self, i); self.leave_function(function); } /// Visit `fn foo()` within an `impl`. fn visit_impl_item_fn(&mut self, i: &'ast syn::ImplItemFn) { // Don't look inside constructors (called "new") because there's often no good // alternative. let function_name = i.sig.ident.to_pretty_string(); let _span = trace_span!( "fn", line = i.sig.fn_token.span.start().line, name = function_name ) .entered(); if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || i.sig.ident == "new" || block_is_empty(&i.block) { return; } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, &i.block); syn::visit::visit_impl_item_fn(self, i); self.leave_function(function); } /// Visit `fn foo() { ... }` within a trait, i.e. a default implementation of a function. fn visit_trait_item_fn(&mut self, i: &'ast syn::TraitItemFn) { let function_name = i.sig.ident.to_pretty_string(); let _span = trace_span!( "fn", line = i.sig.fn_token.span.start().line, name = function_name ) .entered(); if fn_sig_excluded(&i.sig) || attrs_excluded(&i.attrs) || i.sig.ident == "new" { return; } if let Some(block) = &i.default { if block_is_empty(block) { return; } let function = self.enter_function(&i.sig.ident, &i.sig.output, i.span()); self.collect_fn_mutants(&i.sig, block); syn::visit::visit_trait_item_fn(self, i); self.leave_function(function); } } /// Visit `impl Foo { ...}` or `impl Debug for Foo { ... }`. fn visit_item_impl(&mut self, i: &'ast syn::ItemImpl) { if attrs_excluded(&i.attrs) { return; } let type_name = i.self_ty.to_pretty_string(); let name = if let Some((_, trait_path, _)) = &i.trait_ { if path_ends_with(trait_path, "Default") { // Can't think of how to generate a viable different default. return; } format!("", trait = trait_path.segments.last().unwrap().ident) // TODO: trait = trait_path.to_pretty_string()) and update tests to match } else { type_name }; self.in_namespace(&name, |v| syn::visit::visit_item_impl(v, i)); } /// Visit `trait Foo { ... }` fn visit_item_trait(&mut self, i: &'ast syn::ItemTrait) { let name = i.ident.to_pretty_string(); let _span = trace_span!("trait", line = i.span().start().line, name).entered(); if attrs_excluded(&i.attrs) { return; } self.in_namespace(&name, |v| syn::visit::visit_item_trait(v, i)); } /// Visit `mod foo { ... }` or `mod foo;`. fn visit_item_mod(&mut self, node: &'ast syn::ItemMod) { let mod_name = node.ident.unraw().to_string(); let _span = trace_span!("mod", line = node.mod_token.span.start().line, mod_name).entered(); if attrs_excluded(&node.attrs) { trace!("mod excluded by attrs"); return; } let source_location = Span::from(node.span()); // Extract path attribute value, if any (e.g. `#[path="..."]`) let path_attribute = match find_path_attribute(&node.attrs) { Ok(path) => path, Err(path_attribute) => { let definition_site = self .source_file .format_source_location(source_location.start); error!(?path_attribute, ?definition_site, %mod_name, "invalid filesystem traversal in mod path attribute"); return; } }; let mod_namespace = ModNamespace { name: mod_name, path_attribute, source_location, }; self.mod_namespace_stack.push(mod_namespace.clone()); // If there's no content in braces, then this is a `mod foo;` // statement referring to an external file. We remember the module // name and then later look for the file. if node.content.is_none() { // If we're already inside `mod a { ... }` and see `mod b;` then // remember [a, b] as an external module to visit later. self.external_mods.push(ExternalModRef { parts: self.mod_namespace_stack.clone(), }); } self.in_namespace(&mod_namespace.name, |v| syn::visit::visit_item_mod(v, node)); assert_eq!(self.mod_namespace_stack.pop(), Some(mod_namespace)); } /// Visit `a op b` expressions. fn visit_expr_binary(&mut self, i: &'ast syn::ExprBinary) { let _span = trace_span!("binary", line = i.op.span().start().line).entered(); trace!("visit binary operator"); if attrs_excluded(&i.attrs) { return; } let replacements = match i.op { // We don't generate `<=` from `==` because it can too easily go // wrong with unsigned types compared to 0. // We try replacing logical ops with == and !=, which are effectively // XNOR and XOR when applied to booleans. However, they're often unviable // because they require parenthesis for disambiguation in many expressions. BinOp::Eq(_) => vec![quote! { != }], BinOp::Ne(_) => vec![quote! { == }], BinOp::And(_) => vec![quote! { || }], BinOp::Or(_) => vec![quote! { && }], BinOp::Lt(_) => vec![quote! { == }, quote! {>}], BinOp::Gt(_) => vec![quote! { == }, quote! {<}], BinOp::Le(_) => vec![quote! {>}], BinOp::Ge(_) => vec![quote! {<}], BinOp::Add(_) => vec![quote! {-}, quote! {*}], BinOp::AddAssign(_) => vec![quote! {-=}, quote! {*=}], BinOp::Sub(_) | BinOp::Mul(_) => vec![quote! {+}, quote! {/}], BinOp::SubAssign(_) | BinOp::MulAssign(_) => vec![quote! {+=}, quote! {/=}], BinOp::Div(_) => vec![quote! {%}, quote! {*}], BinOp::DivAssign(_) => vec![quote! {%=}, quote! {*=}], BinOp::Rem(_) => vec![quote! {/}, quote! {+}], BinOp::RemAssign(_) => vec![quote! {/=}, quote! {+=}], BinOp::Shl(_) => vec![quote! {>>}], BinOp::ShlAssign(_) => vec![quote! {>>=}], BinOp::Shr(_) => vec![quote! {<<}], BinOp::ShrAssign(_) => vec![quote! {<<=}], BinOp::BitAnd(_) => vec![quote! {|}, quote! {^}], BinOp::BitAndAssign(_) => vec![quote! {|=}, quote! {^=}], BinOp::BitOr(_) => vec![quote! {&}, quote! {^}], BinOp::BitOrAssign(_) => vec![quote! {&=}, quote! {^=}], BinOp::BitXor(_) => vec![quote! {|}, quote! {&}], BinOp::BitXorAssign(_) => vec![quote! {|=}, quote! {&=}], _ => { trace!( op = i.op.to_pretty_string(), "No mutants generated for this binary operator" ); Vec::new() } }; replacements .into_iter() .for_each(|rep| self.collect_mutant(i.op.span().into(), &rep, Genre::BinaryOperator)); syn::visit::visit_expr_binary(self, i); } fn visit_expr_unary(&mut self, i: &'ast syn::ExprUnary) { let _span = trace_span!("unary", line = i.op.span().start().line).entered(); trace!("visit unary operator"); if attrs_excluded(&i.attrs) { return; } match i.op { UnOp::Not(_) | UnOp::Neg(_) => { self.collect_mutant(i.op.span().into(), "e! {}, Genre::UnaryOperator); } _ => { trace!( op = i.op.to_pretty_string(), "No mutants generated for this unary operator" ); } }; syn::visit::visit_expr_unary(self, i); } } // Get the span of the block excluding the braces, or None if it is empty. fn function_body_span(block: &Block) -> Option { Some(Span { start: block.stmts.first()?.span().start().into(), end: block.stmts.last()?.span().end().into(), }) } /// Find a new source file referenced by a `mod` statement. /// /// Possibly, our heuristics just won't be able to find which file it is, /// in which case we return `Ok(None)`. fn find_mod_source( tree_root: &Utf8Path, parent: &SourceFile, mod_namespace: &ExternalModRef, ) -> Option { // First, work out whether the mod will be a sibling in the same directory, or // in a child directory. // // 1. The parent is "src/foo.rs" and `mod bar` means "src/foo/bar.rs". // // 2. The parent is "src/lib.rs" (a target top file) and `mod bar` means "src/bar.rs". // // 3. The parent is "src/foo/mod.rs" and so `mod bar` means "src/foo/bar.rs". // // 4. A path attribute on a mod statement when there is no enclosing mod block // E.g. for parent file "src/a/parent_file.rs", // ``` // // `path` is relative to the directory where the source file is located // #[path="foo_file.rs"] // resolves to: src/a/foo_file.rs // mod foo; // // mod bar { // // `path` is relative to the directory of the enclosing module block // #[path="baz_file.rs"] // resolves to: src/a/parent_file/bar/baz_file.rs // mod baz; // } // ``` // // Having determined the right directory then we can follow the path attribute, or // if no path is specified, then look for either `foo.rs` or `foo/mod.rs`. let (mod_child, mod_parents) = mod_namespace .parts .split_last() .expect("mod namespace is empty"); // TODO: Beyond #115, we should probably remove all special handling of // `mod.rs` here by remembering how we found this file, and whether it // is above or inside the directory corresponding to its module? let parent_path = &parent.tree_relative_path; let mut search_dir = if parent.is_top || parent_path.ends_with("mod.rs") // NOTE: Path attribute on a top-level `mod foo;` (no enclosing block) // ignores the parent module path || (mod_child.path_attribute.is_some() && mod_parents.is_empty()) { parent_path .parent() .expect("mod path has no parent") .to_owned() // src/lib.rs -> src/ } else { parent_path.with_extension("") // foo.rs -> foo/ }; search_dir.extend(mod_parents.iter().map(ModNamespace::get_filesystem_name)); let mod_child_candidates = if let Some(filesystem_name) = &mod_child.path_attribute { vec![search_dir.join(filesystem_name)] } else { [".rs", "/mod.rs"] .iter() .map(|tail| search_dir.join(mod_child.name.clone() + tail)) .collect() }; let mut tried_paths = Vec::new(); for relative_path in mod_child_candidates { let full_path = tree_root.join(&relative_path); if full_path.is_file() { trace!("found submodule in {full_path}"); return Some(relative_path); } tried_paths.push(full_path); } let mod_name = &mod_child.name; let definition_site = parent.format_source_location(mod_child.source_location.start); warn!(?definition_site, %mod_name, ?tried_paths, "referent of mod not found"); None } /// True if the signature of a function is such that it should be excluded. fn fn_sig_excluded(sig: &syn::Signature) -> bool { if sig.unsafety.is_some() { trace!("Skip unsafe fn"); true } else { false } } /// True if any of the attrs indicate that we should skip this node and everything inside it. /// /// This checks for `#[cfg(test)]`, `#[test]`, and `#[mutants::skip]`. fn attrs_excluded(attrs: &[Attribute]) -> bool { attrs .iter() .any(|attr| attr_is_cfg_test(attr) || attr_is_test(attr) || attr_is_mutants_skip(attr)) } /// True if the block (e.g. the contents of a function) is empty. fn block_is_empty(block: &syn::Block) -> bool { block.stmts.is_empty() } /// True if the attribute looks like `#[cfg(test)]`, or has "test" /// anywhere in it. fn attr_is_cfg_test(attr: &Attribute) -> bool { if !path_is(attr.path(), &["cfg"]) { return false; } let mut contains_test = false; if let Err(err) = attr.parse_nested_meta(|meta| { if meta.path.is_ident("test") { contains_test = true; } Ok(()) }) { debug!( ?err, attr = attr.to_pretty_string(), "Attribute is in an unrecognized form so skipped", ); return false; } contains_test } /// True if the attribute is `#[test]`. fn attr_is_test(attr: &Attribute) -> bool { attr.path().is_ident("test") } fn path_is(path: &syn::Path, idents: &[&str]) -> bool { path.segments.iter().map(|ps| &ps.ident).eq(idents.iter()) } /// True if the path ends with this identifier. /// /// This is used as a heuristic to match types without being sensitive to which /// module they are in, or to match functions without being sensitive to which /// type they might be associated with. /// /// This does not check type arguments. fn path_ends_with(path: &syn::Path, ident: &str) -> bool { path.segments.last().is_some_and(|s| s.ident == ident) } /// True if the attribute contains `mutants::skip`. /// /// This for example returns true for `#[mutants::skip]` or `#[cfg_attr(test, mutants::skip)]`. fn attr_is_mutants_skip(attr: &Attribute) -> bool { if path_is(attr.path(), &["mutants", "skip"]) { return true; } if !path_is(attr.path(), &["cfg_attr"]) { return false; } let mut skip = false; if let Err(err) = attr.parse_nested_meta(|meta| { if path_is(&meta.path, &["mutants", "skip"]) { skip = true; } Ok(()) }) { debug!( ?attr, ?err, "Attribute is not a path with attributes; skipping" ); return false; } skip } /// Finds the first path attribute (`#[path = "..."]`) /// /// # Errors /// Returns an error if the path attribute contains a dubious path (leading `/`) fn find_path_attribute(attrs: &[Attribute]) -> std::result::Result, String> { attrs .iter() .find_map(|attr| match &attr.meta { syn::Meta::NameValue(meta) if meta.path.is_ident("path") => { let syn::Expr::Lit(expr_lit) = &meta.value else { return None; }; let syn::Lit::Str(lit_str) = &expr_lit.lit else { return None; }; let path = lit_str.value(); // refuse to follow absolute paths if path.starts_with('/') { Some(Err(path)) } else { Some(Ok(Utf8PathBuf::from(path))) } } _ => None, }) .transpose() } #[cfg(test)] mod test { use indoc::indoc; use itertools::Itertools; use test_log::test; use crate::test_util::copy_of_testdata; use crate::workspace::{PackageFilter, Workspace}; use super::*; #[test] fn path_ends_with() { use super::path_ends_with; use syn::parse_quote; let path = parse_quote! { foo::bar::baz }; assert!(path_ends_with(&path, "baz")); assert!(!path_ends_with(&path, "bar")); assert!(!path_ends_with(&path, "foo")); let path = parse_quote! { baz }; assert!(path_ends_with(&path, "baz")); assert!(!path_ends_with(&path, "bar")); let path = parse_quote! { BTreeMap }; assert!(path_ends_with(&path, "BTreeMap")); assert!(!path_ends_with(&path, "V")); assert!(!path_ends_with(&path, "K")); } /// We should not generate mutants that produce the same tokens as the /// source. #[test] fn no_mutants_equivalent_to_source() { let code = indoc! { " fn always_true() -> bool { true } "}; let source_file = SourceFile::for_tests("src/lib.rs", code, "unimportant", true); let (mutants, _files) = walk_file(&source_file, &[], &Options::default()).expect("walk_file"); let mutant_names = mutants.iter().map(|m| m.name(false)).collect_vec(); // It would be good to suggest replacing this with 'false', breaking a key behavior, // but bad to replace it with 'true', changing nothing. assert_eq!( mutant_names, ["src/lib.rs: replace always_true -> bool with false"] ); } /// We don't visit functions inside files marked with `#![cfg(test)]`. #[test] fn no_mutants_in_files_with_inner_cfg_test_attribute() { let options = Options::default(); let console = Console::new(); let tmp = copy_of_testdata("cfg_test_inner"); let workspace = Workspace::open(tmp.path()).unwrap(); let discovered = workspace .discover(&PackageFilter::All, &options, &console) .unwrap(); assert_eq!(discovered.mutants.as_slice(), &[]); } /// Helper function for `find_path_attribute` tests fn run_find_path_attribute( token_stream: &TokenStream, ) -> std::result::Result, String> { let token_string = token_stream.to_string(); let item_mod = syn::parse_str::(&token_string).unwrap_or_else(|err| { panic!("Failed to parse test case token stream: {token_string}\n{err}") }); find_path_attribute(&item_mod.attrs) } #[test] fn find_path_attribute_on_module_item() { let outer = run_find_path_attribute("e! { #[path = "foo_file.rs"] mod foo; }); assert_eq!(outer, Ok(Some(Utf8PathBuf::from("foo_file.rs")))); let inner = run_find_path_attribute("e! { mod foo { #![path = "foo_folder"] #[path = "file_for_bar.rs"] mod bar; } }); assert_eq!(inner, Ok(Some(Utf8PathBuf::from("foo_folder")))); } #[test] fn reject_module_path_absolute() { // dots are valid let dots = run_find_path_attribute("e! { #[path = "contains/../dots.rs"] mod dots; }); assert_eq!(dots, Ok(Some(Utf8PathBuf::from("contains/../dots.rs")))); let dots_inner = run_find_path_attribute("e! { mod dots_in_path { #![path = "contains/../dots"] } }); assert_eq!(dots_inner, Ok(Some(Utf8PathBuf::from("contains/../dots")))); let leading_slash = run_find_path_attribute("e! { #[path = "/leading_slash.rs"] mod dots; }); assert_eq!(leading_slash, Err("/leading_slash.rs".to_owned())); let allow_other_slashes = run_find_path_attribute("e! { #[path = "foo/other/slashes/are/allowed.rs"] mod dots; }); assert_eq!( allow_other_slashes, Ok(Some(Utf8PathBuf::from("foo/other/slashes/are/allowed.rs"))) ); let leading_slash2 = run_find_path_attribute("e! { #[path = "/leading_slash/../and_dots.rs"] mod dots; }); assert_eq!( leading_slash2, Err("/leading_slash/../and_dots.rs".to_owned()) ); } /// Demonstrate that we can generate mutants from a string, without needing a whole tree. #[test] fn mutants_from_test_str() { let options = Options::default(); let mutants = mutate_source_str( indoc! {" fn always_true() -> bool { true } "}, &options, ) .expect("walk_file_string"); assert_eq!( mutants.iter().map(|m| m.name(false)).collect_vec(), ["src/main.rs: replace always_true -> bool with false"] ); } /// Skip mutating arguments to a particular named function. #[test] fn skip_named_fn() { let options = Options { skip_calls: vec!["dont_touch_this".to_owned()], ..Default::default() }; let mut mutants = mutate_source_str( indoc! {" fn main() { dont_touch_this(2 + 3); } "}, &options, ) .expect("walk_file_string"); // Ignore the main function itself mutants.retain(|m| m.genre != Genre::FnValue); assert_eq!(mutants, []); } #[test] fn skip_with_capacity_by_default() { let options = Options::from_arg_strs(["mutants"]); let mut mutants = mutate_source_str( indoc! {" fn main() { let mut v = Vec::with_capacity(2 * 100); } "}, &options, ) .expect("walk_file_string"); // Ignore the main function itself mutants.retain(|m| m.genre != Genre::FnValue); assert_eq!(mutants, []); } #[test] fn mutate_vec_with_capacity_when_default_skips_are_turned_off() { let options = Options::from_arg_strs(["mutants", "--skip-calls-defaults", "false"]); let mutants = mutate_source_str( indoc! {" fn main() { let mut _v = std::vec::Vec::::with_capacity(2 * 100); } "}, &options, ) .expect("walk_file_string"); dbg!(&mutants); // The main fn plus two mutations of the `*` expression. assert_eq!(mutants.len(), 3); } #[test] fn skip_method_calls_by_name() { let options = Options::from_arg_strs(["mutants", "--skip-calls", "dont_touch_this"]); let mutants = mutate_source_str( indoc! {" fn main() { let mut v = V::new(); v.dont_touch_this(2 + 3); } "}, &options, ) .unwrap(); dbg!(&mutants); assert_eq!( mutants .iter() .filter(|mutant| mutant.genre != Genre::FnValue) .count(), 0 ); } } cargo-mutants-25.0.0/src/workspace.rs000064400000000000000000000377201046102023000156400ustar 00000000000000// Copyright 2023-2025 Martin Pool //! Understand cargo workspaces, which can contain multiple packages. //! //! In cargo-mutants there are a few important connections to workspaces: //! //! 1. We copy the whole workspace to scratch directories, so need to find the root. //! //! 2. We can select to mutate, or run tests from, all packages in the workspace, //! or just some, so we need to find the packages. Also, mutants are marked with the //! package they come from. //! //! 3. In particular when selecting packages, we attempt to match cargo's own heuristics //! when invoked inside a workspace. #![warn(clippy::pedantic)] use std::fmt; use std::panic::catch_unwind; use std::path::Path; use std::process::Command; use std::sync::Arc; use anyhow::{anyhow, bail, ensure, Context}; use camino::{Utf8Path, Utf8PathBuf}; use itertools::Itertools; use serde_json::Value; use tracing::{debug, error, warn}; use crate::cargo::cargo_bin; use crate::console::Console; use crate::interrupt::check_interrupted; use crate::options::Options; use crate::package::{packages_from_metadata, Package, PackageSelection}; use crate::visit::{walk_tree, Discovered}; use crate::Result; /// Which packages to mutate in a workspace? /// /// This expresses the user's _intention_ for what to mutate, in general. It's later resolved /// to a specific list of packages based on the workspace structure. #[derive(Debug, Clone)] pub enum PackageFilter { /// Include every package in the workspace. All, /// Packages with given names, from `--package`. Explicit(Vec), /// Automatic behavior when invoked from a subdirectory. /// /// This tries to match /// . /// /// If the directory is within a package directory, select that package. /// /// Otherwise, this is a "virtual workspace" directory, containing members but no /// primary package. In this case, if there is a `default-members` field in the workspace, /// use that list. Otherwise, apply to all members of the workspace. Auto(Utf8PathBuf), } impl PackageFilter { /// Convenience constructor for `PackageFilter::Explicit`. pub fn explicit>(names: I) -> PackageFilter { PackageFilter::Explicit(names.into_iter().map(|s| s.to_string()).collect_vec()) } } /// A cargo workspace. pub struct Workspace { metadata: cargo_metadata::Metadata, packages: Vec>, } impl fmt::Debug for Workspace { #[mutants::skip] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // The `metadata` value is very large so is omitted here; // just the root is enough. f.debug_struct("Workspace") .field("root", &self.root().to_string()) .finish_non_exhaustive() } } impl Workspace { /// The root directory of the workspace. pub fn root(&self) -> &Utf8Path { &self.metadata.workspace_root } /// Open the workspace containing a given directory. pub fn open>(start_dir: P) -> Result { let start_dir = start_dir.as_ref(); let dir = locate_project(start_dir.try_into().expect("start_dir is UTF-8"), true)?; assert!( dir.is_absolute(), "project location {dir:?} is not absolute" ); let manifest_path = dir.join("Cargo.toml"); debug!(?manifest_path, "Find root files"); check_interrupted()?; let metadata = cargo_metadata::MetadataCommand::new() .no_deps() .manifest_path(&manifest_path) .verbose(false) .exec() .with_context(|| format!("Failed to run cargo metadata on {manifest_path}"))?; debug!(workspace_root = ?metadata.workspace_root, "Found workspace root"); let packages = packages_from_metadata(&metadata); debug!(?packages, "Found packages"); Ok(Workspace { metadata, packages }) } pub fn packages_by_name>(&self, names: &[S]) -> Vec> { names .iter() .map(AsRef::as_ref) .sorted() .filter_map(|name| { if let Some(p) = self.packages.iter().find(|p| p.name == name) { Some(Arc::clone(p)) } else { warn!("Package {name:?} not found in source tree"); None } }) .collect() } /// Match a `PackageFilter` to the actual packages in this workspace, returning a list of packages. fn filter_packages(&self, filter: &PackageFilter) -> Result { match filter { PackageFilter::Auto(dir) => { let root = self.root(); // Find the closest package directory (with a cargo manifest) to the current directory. let package_dir = locate_project(dir, false)?; assert!(package_dir.is_absolute()); // It's not required that the members be inside the workspace directory: see // for package in &self.packages { // If this package is one of the workspace members, then select this package. if root.join(&package.relative_dir) == package_dir { debug!( package = package.name, ?package_dir, "Resolved auto package filter based on enclosing directory" ); return Ok(PackageSelection::Explicit(vec![package.clone()])); } } // Otherwise, we're in a virtual workspace directory, and not inside any package. // Use configured defaults if there are any, otherwise test all packages. ensure!( package_dir == root, "package {package_dir:?} doesn't match any child and doesn't match the workspace root {root:?}?", ); let default_packages = self.default_packages(); debug!( ?default_packages, "Resolved auto package filter to workspace default packages" ); Ok(default_packages) } PackageFilter::All => Ok(PackageSelection::All), PackageFilter::Explicit(names) => { Ok(PackageSelection::Explicit(self.packages_by_name(names))) } } } fn expand_selection(&self, selection: PackageSelection) -> Vec> { match selection { PackageSelection::All => self.packages.clone(), PackageSelection::Explicit(packages) => packages, } } /// Make all the mutants from the filtered packages in this workspace. pub fn discover( &self, package_filter: &PackageFilter, options: &Options, console: &Console, ) -> Result { walk_tree( self.root(), &self.expand_selection(self.filter_packages(package_filter)?), options, console, ) } /// Return the default workspace packages. /// /// Default packages can be specified in the workspace's `Cargo.toml` file; /// if not, all packages are included. fn default_packages(&self) -> PackageSelection { let metadata = &self.metadata; // `cargo_metadata::workspace_default_packages` will panic when calling Cargo older than 1.71; // in that case we'll just fall back to everything, for lack of a better option. // TODO: Use the new cargo_metadata API that doesn't panic? match catch_unwind(|| metadata.workspace_default_packages()) { Ok(default_packages) if default_packages.is_empty() => { debug!("manifest has no explicit default packages"); PackageSelection::All } Ok(default_packages) => { let default_package_names: Vec<&str> = default_packages .iter() .map(|pmeta| pmeta.name.as_str()) .sorted() // for reproducibility .collect(); debug!( ?default_package_names, "Manifest defines explicit default packages" ); PackageSelection::Explicit(self.packages_by_name(&default_package_names)) } Err(err) => { warn!( cargo_metadata_error = err.downcast::().unwrap_or_default(), "workspace_default_packages is not supported; testing all packages", ); PackageSelection::All } } } } /// Return the path of the workspace or package directory enclosing a given directory. fn locate_project(path: &Utf8Path, workspace: bool) -> Result { ensure!(path.is_dir(), "{path:?} is not a directory"); let mut args: Vec<&str> = vec!["locate-project"]; if workspace { args.push("--workspace"); } let output = Command::new(cargo_bin()) .args(&args) .current_dir(path) .output() .with_context(|| format!("failed to spawn {args:?}"))?; let exit = output.status; if !exit.success() { error!( ?exit, "cargo locate-project failed: {}", String::from_utf8_lossy(&output.stderr) ); bail!("cargo locate-project failed"); } let stdout = String::from_utf8(output.stdout).context("cargo locate-project output is not UTF-8")?; debug!("output: {}", stdout.trim()); let val: Value = serde_json::from_str(&stdout).context("parse cargo locate-project output")?; let cargo_toml_path: Utf8PathBuf = val["root"] .as_str() .with_context(|| format!("cargo locate-project output has no root: {stdout:?}"))? .to_owned() .into(); debug!(?cargo_toml_path, "Found workspace root manifest"); ensure!( cargo_toml_path.is_file(), "cargo locate-project root {cargo_toml_path:?} is not a file" ); let root = cargo_toml_path .parent() .ok_or_else(|| anyhow!("cargo locate-project root {cargo_toml_path:?} has no parent"))? .to_owned(); ensure!( root.is_dir(), "apparent project root directory {root:?} is not a directory" ); Ok(root) } #[cfg(test)] mod test { use assert_matches::assert_matches; use camino::Utf8PathBuf; use itertools::Itertools; use crate::console::Console; use crate::options::Options; use crate::package::PackageSelection; use crate::test_util::copy_of_testdata; use crate::workspace::PackageFilter; use super::Workspace; #[test] fn error_opening_outside_of_crate() { Workspace::open("/").unwrap_err(); } #[test] fn open_subdirectory_of_crate_opens_the_crate() { let tmp = copy_of_testdata("factorial"); let workspace = Workspace::open(&tmp).expect("open source tree from subdirectory"); let root = workspace.root(); assert!(root.is_dir()); assert!(root.join("Cargo.toml").is_file()); assert!(root.join("src/bin/factorial.rs").is_file()); } #[test] fn find_root_from_subdirectory_of_workspace_finds_the_workspace_root() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path()).expect("Find root from within workspace/main"); let root = workspace.root(); assert_eq!( root.canonicalize().unwrap(), tmp.path().canonicalize().unwrap() ); } #[test] fn find_top_source_files_from_subdirectory_of_workspace() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path()).expect("Find workspace root"); let packages = workspace.filter_packages(&PackageFilter::All).unwrap(); assert_matches!(packages, super::PackageSelection::All); let packages = workspace.expand_selection(packages); assert_eq!(packages[0].name, "cargo_mutants_testdata_workspace_utils"); assert_eq!(packages[0].top_sources, ["utils/src/lib.rs"]); assert_eq!(packages[1].name, "main"); assert_eq!(packages[1].top_sources, ["main/src/main.rs"]); assert_eq!(packages[2].name, "main2"); assert_eq!(packages[2].top_sources, ["main2/src/main.rs"]); } #[test] fn package_filter_all_from_subdir_gets_everything() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path().join("main")).expect("Find workspace root"); let packages = workspace.filter_packages(&PackageFilter::All).unwrap(); assert_matches!(packages, super::PackageSelection::All); let packages = workspace.expand_selection(packages); assert_eq!( packages.iter().map(|p| &p.name).collect_vec(), ["cargo_mutants_testdata_workspace_utils", "main", "main2"] ); } #[test] fn auto_packages_in_workspace_subdir_finds_single_package() { let tmp = copy_of_testdata("workspace"); let subdir_path = Utf8PathBuf::try_from(tmp.path().join("main")).unwrap(); let workspace = Workspace::open(&subdir_path).expect("Find workspace root"); let packages = workspace .filter_packages(&PackageFilter::Auto(subdir_path.clone())) .unwrap(); let PackageSelection::Explicit(packages) = packages else { panic!("Expected PackageSelection::Explicit, got {packages:?}"); }; assert_eq!(packages.len(), 1); assert_eq!(packages[0].name, "main"); } #[test] fn auto_packages_in_virtual_workspace_gets_everything() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path()).expect("Find workspace root"); let packages = workspace .filter_packages(&PackageFilter::Auto( tmp.path().to_owned().try_into().unwrap(), )) .unwrap(); let PackageSelection::Explicit(packages) = packages else { panic!("Expected PackageSelection::Explicit, got {packages:?}"); }; assert_eq!( packages.iter().map(|p| &p.name).sorted().collect_vec(), ["cargo_mutants_testdata_workspace_utils", "main", "main2"] ); } #[test] fn filter_by_single_package() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path().join("main")).expect("Find workspace root"); let root_dir = workspace.root(); assert_eq!( root_dir.canonicalize().unwrap(), tmp.path().canonicalize().unwrap() ); let filter = PackageFilter::explicit(["main"]); let packages = workspace.filter_packages(&filter).unwrap(); println!("{packages:#?}"); let PackageSelection::Explicit(packages) = packages else { panic!("Expected PackageSelection::Explicit, got {packages:?}"); }; assert_eq!(packages.len(), 1); assert_eq!(packages[0].name, "main"); assert_eq!(packages[0].top_sources, ["main/src/main.rs"]); } #[test] fn filter_by_multiple_packages() { let tmp = copy_of_testdata("workspace"); let workspace = Workspace::open(tmp.path().join("main")).expect("Find workspace root"); assert_eq!( workspace.root().canonicalize().unwrap(), tmp.path().canonicalize().unwrap() ); let selection = PackageFilter::explicit(["main", "main2"]); let discovered = workspace .discover(&selection, &Options::default(), &Console::new()) .unwrap(); assert_eq!( discovered .files .iter() .map(|sf| sf.tree_relative_path.clone()) .collect_vec(), ["main/src/main.rs", "main2/src/main.rs"] ); } } cargo-mutants-25.0.0/testdata/.gitignore000064400000000000000000000000231046102023000162700ustar 00000000000000target/ Cargo.lock cargo-mutants-25.0.0/testdata/README.md000064400000000000000000000020621046102023000155640ustar 00000000000000# cargo-mutants testdata Each directory below here is a Rust tree used by the cargo-mutants tests. In these trees, the manifest file is called `Cargo_test.toml` (rather than `Cargo.toml`) for a couple of reasons: 1. `cargo publish` excludes directories containing `Cargo.toml`, on the grounds that each crate should be published separately, but we want to include these in the published tarball so that the tests can run and succeed in an unpacked tarball. (See https://github.com/sourcefrog/cargo-mutants/issues/355.) 2. We don't want cargo to look at these crates when building or resolving dependencies for cargo-mutants itself. Since the `--manifest-path` of Cargo commands expects the manifest to be named `Cargo.toml` we have to always copy these trees before using them. The `copy_of_testdata` helper function copies them and fixes the manifest name. Copying the tree also avoids any conflicts between concurrent or consecutive tests. Similarly, the testdata here has `.cargo_test` instead of `.cargo` directories, but they're fixed up as the trees are copied. cargo-mutants-25.0.0/testdata/already_failing_doctests/Cargo_test.toml000064400000000000000000000005001046102023000243110ustar 00000000000000[package] name = "mutants-testdata-already-failing-doctests" edition = "2018" version = "0.0.0" authors = ["Martin Pool"] publish = false [lib] doctest = true # They exist, but we don't want cargo mutants to run them. [workspace] # Don't include this in the overall cargo-mutants workspace, because it fails to build. cargo-mutants-25.0.0/testdata/already_failing_doctests/src/lib.rs000064400000000000000000000006621046102023000232360ustar 00000000000000/// Here is a function. It has a doctest, but the doctest does not even build. /// /// cargo-mutants tests use this to check that doctests can be skipped. /// /// ``` /// # use mutants_testdata_already_failing_doctests::takes_one_arg; /// takes_one_arg(123,123,123); /// ``` pub fn takes_one_arg(a: usize) -> usize { a + 1 } mod test { #[test] fn takes_one_arg() { assert_eq!(super::takes_one_arg(1), 2); } } cargo-mutants-25.0.0/testdata/already_failing_tests/Cargo_test.toml000064400000000000000000000004031046102023000236250ustar 00000000000000[package] name = "mutants-testdata-already-failing-tests" edition = "2018" version = "0.0.0" authors = ["Martin Pool"] publish = false [lib] doctest = false [workspace] # Don't include this in the overall cargo-mutants workspace, because it fails to build. cargo-mutants-25.0.0/testdata/already_failing_tests/src/lib.rs000064400000000000000000000002531046102023000225440ustar 00000000000000pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { assert_eq!(factorial(6), 72); } cargo-mutants-25.0.0/testdata/already_hangs/Cargo_test.toml000064400000000000000000000002761046102023000221020ustar 00000000000000[package] name = "cargo-mutants-testdata-already-hangs" version = "0.1.0" edition = "2018" [dependencies.mutants] version = "0.0.3" # path = "../../../mutants_attrs" [lib] doctest = false cargo-mutants-25.0.0/testdata/already_hangs/src/lib.rs000064400000000000000000000011241046102023000210070ustar 00000000000000//! These tests hang, even in a clean tree. //! //! This lets us test we impose a reasonable timeout on clean tree tests. use std::thread::sleep; use std::time::Duration; pub fn infinite_loop() { // Not really infinite, so that orphaned processes don't hang around forever. // They shouldn't normally happen, but they might when cargo-mutants is itself // being mutation tested, or has a bug, etc. for i in 0..600 { println!("{}", i); sleep(Duration::from_secs(1)); } } mod test { #[test] fn infinite_loop() { super::infinite_loop() } } cargo-mutants-25.0.0/testdata/cdylib/Cargo_test.toml000064400000000000000000000002631046102023000205430ustar 00000000000000[package] name = "cargo-mutants-testdata-cdylib" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [lib] crate-type = ["cdylib"] path = "src/entry.rs" cargo-mutants-25.0.0/testdata/cdylib/README.md000064400000000000000000000003031046102023000170260ustar 00000000000000# `cdylib` testdata A tree that looks like a WASM application: it builds a `cdylib` with a specifically named top-level source file. See cargo-mutants-25.0.0/testdata/cdylib/src/entry.rs000064400000000000000000000004421046102023000200510ustar 00000000000000pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[cfg(test)] mod test { use super::*; #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); assert_eq!(factorial(6), 720); } } cargo-mutants-25.0.0/testdata/cfg_attr_mutants_skip/Cargo_test.toml000064400000000000000000000002451046102023000236670ustar 00000000000000[package] name = "cargo-mutants-testdata-cfg-attr-mutants-skip" version = "0.0.0" edition = "2018" authors = ["Tim Diekmann"] publish = false [lib] doctest = false cargo-mutants-25.0.0/testdata/cfg_attr_mutants_skip/src/lib.rs000064400000000000000000000005641046102023000226070ustar 00000000000000//! Mutations can be skipped with `cfg_attr` attributes. #[cfg_attr(mutants, mutants::skip)] pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } cargo-mutants-25.0.0/testdata/cfg_attr_test_skip/Cargo_test.toml000064400000000000000000000003041046102023000231470ustar 00000000000000[package] name = "cargo-mutants-testdata-cfg-attr-test-skip" version = "0.0.0" edition = "2018" authors = ["Tim Diekmann"] publish = false [dev-dependencies] mutants = "0" [lib] doctest = false cargo-mutants-25.0.0/testdata/cfg_attr_test_skip/src/lib.rs000064400000000000000000000012701046102023000220660ustar 00000000000000//! Mutants can be skipped with `cfg_attr` attributes. //! //! (We don't currently examine the attributes, we just look for anything mentioning //! `mutants::skip`.) #[cfg_attr(test, mutants::skip)] pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } // This has a cfg_attr, but not one that we should match #[cfg_attr(feature = "magic", sparkles, crackles)] pub fn double(x: usize) -> usize { 2 * x } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } #[test] fn test_double() { assert_eq!(double(21), 42); } cargo-mutants-25.0.0/testdata/cfg_test_inner/Cargo_test.toml000064400000000000000000000001721046102023000222650ustar 00000000000000[package] name = "cargo-mutants-cfg-test-inner" version = "0.0.0" edition = "2021" publish = false [lib] doctest = false cargo-mutants-25.0.0/testdata/cfg_test_inner/README.md000064400000000000000000000002121046102023000205500ustar 00000000000000# cfg_test_inner Functions in a file with a `#![cfg(test)]` are skipped. From . cargo-mutants-25.0.0/testdata/cfg_test_inner/src/lib.rs000064400000000000000000000000121046102023000211720ustar 00000000000000mod test; cargo-mutants-25.0.0/testdata/cfg_test_inner/src/test.rs000064400000000000000000000000561046102023000214130ustar 00000000000000#![cfg(test)] fn foo() -> bool { false } cargo-mutants-25.0.0/testdata/cross_package_tests/Cargo_test.toml000064400000000000000000000003231046102023000233200ustar 00000000000000# This workspace has no root package, which is an interesting edge case to test: # older cargo-mutants assumed there was a root package and only tested it. [workspace] members = ["lib", "tests"] resolver = "2" cargo-mutants-25.0.0/testdata/cross_package_tests/README.md000064400000000000000000000003571046102023000216170ustar 00000000000000# cross-package tests This is an example of a workspace where code in one package is tested by another package. To measure the right coverage, we need to run tests either for the whole workspace or at least for some other named packages. cargo-mutants-25.0.0/testdata/cross_package_tests/lib/Cargo_test.toml000064400000000000000000000003271046102023000240720ustar 00000000000000[package] name = "cargo-mutants-testdata-cross-package-tests-lib" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html cargo-mutants-25.0.0/testdata/cross_package_tests/lib/src/lib.rs000064400000000000000000000000601046102023000230000ustar 00000000000000pub fn add(a: u32, b: u32) -> u32 { a + b } cargo-mutants-25.0.0/testdata/cross_package_tests/tests/Cargo_test.toml000064400000000000000000000004561046102023000244710ustar 00000000000000[package] name = "cargo-mutants-testdata-cross-package-tests-tests" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo-mutants-testdata-cross-package-tests-lib = { path = "../lib" } cargo-mutants-25.0.0/testdata/cross_package_tests/tests/tests/lib.rs000064400000000000000000000001621046102023000237520ustar 00000000000000use cargo_mutants_testdata_cross_package_tests_lib::add; #[test] fn test_add() { assert_eq!(add(1, 2), 3); } cargo-mutants-25.0.0/testdata/custom_top_file/Cargo_test.toml000064400000000000000000000004051046102023000224660ustar 00000000000000[package] name = "cargo-mutants-testdata-custom-top-file" description = "A package that overrides the default file name for a target" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [lib] doctest = false path = "src/custom_top.rs" cargo-mutants-25.0.0/testdata/custom_top_file/src/custom_top.rs000064400000000000000000000003051046102023000230270ustar 00000000000000pub fn is_even(n: u32) -> bool { n % 2 == 0 } #[cfg(test)] mod test { use super::*; #[test] fn test_is_even() { assert!(is_even(2)); assert!(!is_even(3)); } } cargo-mutants-25.0.0/testdata/dangling_mod/Cargo_test.toml000064400000000000000000000001721046102023000217160ustar 00000000000000[package] name = "cargo-mutants-testdata-dangling-mod" publish = false version = "0.0.0" edition = "2021" [dependencies] cargo-mutants-25.0.0/testdata/dangling_mod/README.md000064400000000000000000000011421046102023000202040ustar 00000000000000# `dangling_mod` testdata tree This tree intentionally references invalid module files to verify the discovery system continues gracefully in the face of module file warnings. 1. `nonexistent` - the source file does not exist 2. `outside_of_workspace` - the source file exists outside of the workspace (and is accepted by rustc) but is outside the scope of `cargo-mutants` to apply modifications Notes: - `#[cfg(not(test))]` allows tests to run despite the intentionally missing module file - `lib.rs` is omitted, as the `#[cfg(not(test))]` trick does not work for the `cargo build` step in `cargo-mutants` cargo-mutants-25.0.0/testdata/dangling_mod/src/main.rs000064400000000000000000000007231046102023000210120ustar 00000000000000fn main() {} /// Source file intentionally does not exist #[cfg(not(test))] // allow tests to run successfully mod nonexistent; #[path = "../../nested_mod/src/paths_in_main/a/foo.rs"] #[cfg(not(test))] // allow tests to run successfully pub mod outside_workspace; mod verify_continue { pub fn always_true() -> bool { true } } #[cfg(test)] mod tests { #[test] fn it_works() { assert!(super::verify_continue::always_true()); } } cargo-mutants-25.0.0/testdata/dependency/Cargo_test.toml000064400000000000000000000002601046102023000214100ustar 00000000000000[package] authors = ["Martin Pool"] description = "A crate that is depended upon" edition = "2018" name = "cargo-mutants-testdata-dependency" publish = false version = "0.0.0" cargo-mutants-25.0.0/testdata/dependency/src/lib.rs000064400000000000000000000004261046102023000203300ustar 00000000000000pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } cargo-mutants-25.0.0/testdata/diff0/Cargo_test.toml000064400000000000000000000002761046102023000202710ustar 00000000000000[package] name = "diff0" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo-mutants-25.0.0/testdata/diff0/src/lib.rs000064400000000000000000000002541046102023000172010ustar 00000000000000pub fn one() -> String { "one".to_owned() } #[cfg(test)] mod test_super { use super::*; #[test] fn test_one() { assert_eq!(one(), "one"); } } cargo-mutants-25.0.0/testdata/diff1/Cargo_test.toml000064400000000000000000000002761046102023000202720ustar 00000000000000[package] name = "diff1" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo-mutants-25.0.0/testdata/diff1/src/lib.rs000064400000000000000000000004441046102023000172030ustar 00000000000000pub fn one() -> String { "one".to_owned() } pub fn two() -> String { format!("{}", 2) } #[cfg(test)] mod test_super { use super::*; #[test] fn test_one() { assert_eq!(one(), "one"); } #[test] fn test_two() { assert_eq!(two(), "2"); } } cargo-mutants-25.0.0/testdata/error_value/.cargo_test/mutants.toml000064400000000000000000000001271046102023000234320ustar 00000000000000# This crate uses just `&'static str` as an error type. error_values = ['"injected"'] cargo-mutants-25.0.0/testdata/error_value/Cargo_test.toml000064400000000000000000000002321046102023000216160ustar 00000000000000[package] name = "cargo-mutants-testdata-error-value" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false [lib] doctest = false cargo-mutants-25.0.0/testdata/error_value/README.md000064400000000000000000000006611046102023000201140ustar 00000000000000# `error_value` tree This tree contains a function that can return an error. The tests ignore the error case, so would fail to detect a bug that causes an error to be returned. (It's probably pretty likely that many Rust tests will `unwrap` or `?` on the error and so implicitly catch it, but it's still possible.) With cargo-mutants `--error` option, we generate a mutant that returns an error and so catch the missing coverage. cargo-mutants-25.0.0/testdata/error_value/src/lib.rs000064400000000000000000000015201046102023000205330ustar 00000000000000use std::result::Result; pub fn zero_is_ok(n: u32) -> Result { if n == 0 { Ok(n) } else { Err("not zero") } } #[cfg(test)] mod test { use super::*; // These would be better tests but for the sake of the test let's // assume nobody wrote them yet. // #[test] // fn test_even_is_ok() { // assert_eq!(even_is_ok(2), Ok(2)); // assert_eq!(even_is_ok(3), Err("number is odd")); // } // #[test] // fn test_even_with_unwrap() { // assert_eq!(even_is_ok(2).unwrap(), 2); // } #[test] fn bad_test_ignores_error_results() { // A bit contrived but does the job: never checks that // the code passes on values that it should accept. assert!(zero_is_ok(1).is_err()); assert!(zero_is_ok(3).is_err()); } } cargo-mutants-25.0.0/testdata/everything_skipped/Cargo_test.toml000064400000000000000000000003351046102023000232000ustar 00000000000000[package] name = "cargo-mutants-testdata-everything-skipped" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [[bin]] name = "everything-skipped" doctest = false [dependencies] mutants = "0" cargo-mutants-25.0.0/testdata/everything_skipped/README.md000064400000000000000000000003201046102023000214620ustar 00000000000000# `everything_skipped` This tree generates no mutants, either because things are explicitly skipped with annotations, or because they match built-in patterns for things that cannot or should not be mutated. cargo-mutants-25.0.0/testdata/everything_skipped/src/bin/everything-skipped.rs000064400000000000000000000023541046102023000257420ustar 00000000000000//! Everything is skipped, so this tests the case where no mutants are found. #[mutants::skip] fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } #[mutants::skip] fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } struct Thing(u32); impl From for Thing { #[mutants::skip] fn from(x: u32) -> Self { Thing(x) } } impl Thing { #[mutants::skip] fn value(&self) -> u32 { self.0 } // impl fns called "new" are implicitly skipped, because it seems unlikely that // we can create the type without them, and it might cause infinite recursions // between `default` and `new`. fn new() -> Thing { Thing(42) } fn nothing() { // Has an empty body (and therefore returns unit), so it's skipped } } trait Pet { #[mutants::skip] fn name(&self) -> String { "Pixel".to_string() } fn new() -> Self { // This is skipped because it's an impl fn called "new" unimplemented!() } } cargo-mutants-25.0.0/testdata/factorial/Cargo_test.toml000064400000000000000000000002551046102023000212420ustar 00000000000000[package] name = "cargo-mutants-testdata-factorial" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [[bin]] name = "factorial" doctest = false cargo-mutants-25.0.0/testdata/factorial/src/bin/factorial.rs000064400000000000000000000005531046102023000221250ustar 00000000000000fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } cargo-mutants-25.0.0/testdata/fails_without_feature/Cargo_test.toml000064400000000000000000000003731046102023000236730ustar 00000000000000[package] name = "cargo-mutants-testdata-fails-without-feature" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false [dependencies.mutants] version = "0.0.3" [features] needed = [] [[bin]] name = "factorial" doctest = false cargo-mutants-25.0.0/testdata/fails_without_feature/README.md000064400000000000000000000002671046102023000221650ustar 00000000000000# `fails_without_feature` The crates for this test fail unless a feature is turned on. (Not a good style perhaps, but a good way to test that Cargo features can be passed through.) cargo-mutants-25.0.0/testdata/fails_without_feature/src/bin/factorial.rs000064400000000000000000000010271046102023000245520ustar 00000000000000#[mutants::skip] fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } #[cfg(feature = "needed")] fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[cfg(not(feature = "needed"))] #[mutants::skip] fn factorial(_n: u32) -> u32 { panic!("needed feature is not enabled"); } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } cargo-mutants-25.0.0/testdata/hang_avoided_by_attr/Cargo_test.toml000064400000000000000000000003051046102023000234260ustar 00000000000000[package] name = "cargo-mutants-testdata-hang-avoided-by-attr" version = "0.1.0" edition = "2018" [dependencies.mutants] version = "0.0.3" # path = "../../../mutants_attrs" [lib] doctest = false cargo-mutants-25.0.0/testdata/hang_avoided_by_attr/src/lib.rs000064400000000000000000000013621046102023000223460ustar 00000000000000//! An example of a function we should not mutate because it will hang. use std::time::{Duration, Instant}; /// If mutated to return false, the program will spin forever. /// /// Ideally and eventually, cargo-mutants should stop it after a timeout, /// but that still takes some time, so you can also choose to skip this. #[mutants::skip] fn should_stop() -> bool { true } pub fn controlled_loop() { let start = Instant::now(); for i in 0.. { println!("{}", i); if should_stop() { break; } if start.elapsed() > Duration::from_secs(60 * 5) { panic!("timed out"); } } } mod test { #[test] fn controlled_loop_terminates() { super::controlled_loop() } } cargo-mutants-25.0.0/testdata/hang_when_mutated/Cargo_test.toml000064400000000000000000000003021046102023000227500ustar 00000000000000[package] name = "cargo-mutants-testdata-hang-when-mutated" version = "0.1.0" edition = "2018" [dependencies.mutants] version = "0.0.3" # path = "../../../mutants_attrs" [lib] doctest = false cargo-mutants-25.0.0/testdata/hang_when_mutated/src/lib.rs000064400000000000000000000033101046102023000216660ustar 00000000000000//! An example of a function that will hang when mutated. //! //! An attribute could be added to avoid mutating it, but this tree //! lets us test the case where that has not yet been fixed. use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; static TRIGGER: AtomicBool = AtomicBool::new(false); const fn should_stop_const() -> bool { true } /// If `should_stop_const` is mutated to return false, then this const block /// will hang and block compilation. pub const VAL: i32 = loop { if should_stop_const() { break 1; } }; /// If mutated to return false, the program will spin forever. fn should_stop() -> bool { if TRIGGER.load(Ordering::Relaxed) { return true; } TRIGGER.store(true, Ordering::Relaxed); false } /// Runs until `should_stop` returns true, and then returns the number /// of iterations. /// /// Also stops after a few minutes anyhow, so that if the timeouts are not /// properly implemented, the child process doesn't hang around forever. pub fn controlled_loop() -> usize { let start = Instant::now(); for i in 1.. { println!("{}", i); if should_stop() { return i; } std::thread::sleep(std::time::Duration::from_millis(100)); if start.elapsed() > Duration::from_secs(60) { panic!("timed out"); } } unreachable!(); } mod test { #[test] fn controlled_loop_terminates() { // Should do two passes: first the trigger is false but gets set, // then the trigger is true and the loop terminates. assert_eq!(super::controlled_loop(), 2); } #[test] fn val_is_correct() { assert_eq!(super::VAL, 1); } } cargo-mutants-25.0.0/testdata/insta/Cargo_test.toml000064400000000000000000000002771046102023000204200ustar 00000000000000[package] name = "cargo-mutants-testdata-insta" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [dependencies.insta] version = "1.19.1" default-features = false cargo-mutants-25.0.0/testdata/insta/README.md000064400000000000000000000004411046102023000167010ustar 00000000000000# cargo-mutants `insta` test tree An example of a crate that uses the [Insta](https://insta.rs) test framework. Insta in some modes will either write `.snap.new` files into the source directory, or update existing snapshots. We don't want either of those to happen when testing mutants. cargo-mutants-25.0.0/testdata/insta/src/lib.rs000064400000000000000000000001111046102023000173170ustar 00000000000000pub fn say_hello(name: &str) -> String { format!("Hello, {name}!") } cargo-mutants-25.0.0/testdata/insta/tests/insta.rs000064400000000000000000000002411046102023000202460ustar 00000000000000use cargo_mutants_testdata_insta::say_hello; #[test] fn say_hello_vs_insta_snapshot() { let name = "Robin"; insta::assert_snapshot!(say_hello(name)); } cargo-mutants-25.0.0/testdata/insta/tests/snapshots/insta__say_hello_vs_insta_snapshot.snap000064400000000000000000000001111046102023000306240ustar 00000000000000--- source: tests/insta.rs expression: say_hello(name) --- Hello, Robin! cargo-mutants-25.0.0/testdata/integration_tests/Cargo_test.toml000064400000000000000000000002111046102023000230330ustar 00000000000000[package] name = "cargo-mutants-testdata-integration-tests" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false cargo-mutants-25.0.0/testdata/integration_tests/README.md000064400000000000000000000001471046102023000213330ustar 00000000000000# `integration_tests` testdata Demonstrates handling of a crate that has `tests/` for its public API. cargo-mutants-25.0.0/testdata/integration_tests/src/lib.rs000064400000000000000000000000531046102023000217530ustar 00000000000000pub fn double(n: u32) -> u32 { 2 * n } cargo-mutants-25.0.0/testdata/integration_tests/tests/api.rs000064400000000000000000000007401046102023000223340ustar 00000000000000use cargo_mutants_testdata_integration_tests::double; #[test] fn double_zero() { assert_eq!(0, double(0)); } #[test] fn double_one() { assert_eq!(2, double(1)); } #[test] fn double_a_number() { let n = a_number(); assert_eq!(double(n), n + n); } /// Example of a non-test function within the test module. /// /// If mutated to return 0 this will make tests falsely pass. /// /// Functions in test modules should not be mutated. fn a_number() -> u32 { 42 } cargo-mutants-25.0.0/testdata/many_patterns/Cargo_test.toml000064400000000000000000000002141046102023000221550ustar 00000000000000[package] name = "cargo-mutants-testdata-many-patterns" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false [lib] cargo-mutants-25.0.0/testdata/many_patterns/README.md000064400000000000000000000004411046102023000204470ustar 00000000000000# `many_patterns` testdata tree This tree contains code that generates many different mutants, to exercise code that matches these patterns. This tree is not tested from the test suite, because it may eventually generate many mutants and get slow, and there is in fact no test coverage. cargo-mutants-25.0.0/testdata/many_patterns/src/binops.rs000064400000000000000000000004441046102023000216220ustar 00000000000000pub fn binops() { let _ = 1 + 2 * 3 / 4 % 5; let _ = 1 & 2 | 3 ^ 4 << 5 >> 6; let mut a = 0isize; a += 1; a -= 2; a *= 3; a /= 2; } pub fn bin_assign() -> i32 { let mut a = 0; a |= 0xfff7; a ^= 0xffff; a &= 0x0f; a >>= 4; a <<= 1; a } cargo-mutants-25.0.0/testdata/many_patterns/src/lib.rs000064400000000000000000000000201046102023000210640ustar 00000000000000pub mod binops; cargo-mutants-25.0.0/testdata/missing_test/Cargo_test.toml000064400000000000000000000002641046102023000220060ustar 00000000000000[package] name = "cargo-mutants-testdata-missing-test" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false resolver = "2" [dependencies] tempfile = "3.4" cargo-mutants-25.0.0/testdata/missing_test/src/lib.rs000064400000000000000000000010411046102023000207140ustar 00000000000000pub fn is_symlink(unix_permissions: u32) -> bool { unix_permissions & 0o140000 != 0 } #[test] fn test_symlink_from_known_unix_permissions() { assert!(is_symlink(0o147777)); } #[cfg(unix)] #[test] fn test_symlink_on_real_symlink_permissions() { use std::os::unix::fs::PermissionsExt; let td = tempfile::TempDir::new().unwrap(); let p = td.path().join("link"); std::os::unix::fs::symlink("target", &p).unwrap(); let meta = std::fs::symlink_metadata(&p).unwrap(); assert!(is_symlink(meta.permissions().mode())); } cargo-mutants-25.0.0/testdata/missing_test_fixed/Cargo_test.toml000064400000000000000000000002721046102023000231640ustar 00000000000000[package] name = "cargo-mutants-testdata-missing-test-fixed" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false resolver = "2" [dependencies] tempfile = "3.4" cargo-mutants-25.0.0/testdata/missing_test_fixed/README.md000064400000000000000000000001031046102023000214450ustar 00000000000000A fixed and better-tested version of the code from `missing_test`. cargo-mutants-25.0.0/testdata/missing_test_fixed/src/lib.rs000064400000000000000000000015531046102023000221030ustar 00000000000000pub fn is_symlink(unix_permissions: u32) -> bool { unix_permissions & 0o170_000 == 0o120_000 } #[test] fn test_symlink_from_known_unix_permissions() { assert!(is_symlink(0o120777)); } #[cfg(unix)] #[test] fn test_symlink_on_real_symlink_permissions() { use std::fs::symlink_metadata; use std::os::unix::fs::PermissionsExt; let td = tempfile::TempDir::new().unwrap(); let p = td.path().join("link"); std::os::unix::fs::symlink("target", &p).unwrap(); let meta = symlink_metadata(&p).unwrap(); assert!(is_symlink(meta.permissions().mode())); assert!(!is_symlink( symlink_metadata(td.path()).unwrap().permissions().mode() )); let file_path = td.path().join("file"); std::fs::File::create(&file_path).unwrap(); assert!(!is_symlink( symlink_metadata(&file_path).unwrap().permissions().mode() )); } cargo-mutants-25.0.0/testdata/mut_ref/Cargo_test.toml000064400000000000000000000001771046102023000207420ustar 00000000000000[package] name = "cargo-mutants-testdata-mut-ref" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false cargo-mutants-25.0.0/testdata/mut_ref/README.md000064400000000000000000000004171046102023000172270ustar 00000000000000# `mut_ref` test case An example of a function that returns a mut reference into a mut argument. cargo-mutants generates a mutant that changes the return value to a mut reference into a new value on the heap. As a result, tests that check the return value should fail. cargo-mutants-25.0.0/testdata/mut_ref/src/lib.rs000064400000000000000000000003241046102023000176500ustar 00000000000000pub fn returns_mut_ref(a: &mut Vec) -> &mut u32 { a.get_mut(0).unwrap() } #[test] fn test_mut_ref() { let mut a = vec![1, 2, 3]; *returns_mut_ref(&mut a) += 10; assert_eq!(a, [11, 2, 3]); } cargo-mutants-25.0.0/testdata/mutants_attrs/.gitignore000064400000000000000000000000231046102023000212000ustar 00000000000000target/ Cargo.lock cargo-mutants-25.0.0/testdata/mutants_attrs/Cargo_test.toml000064400000000000000000000004211046102023000222010ustar 00000000000000[package] name = "mutants" version = "0.0.3" edition = "2018" license = "MIT" description = "Decorator attributes to be used with cargo-mutants" repository = "https://github.com/sourcefrog/cargo-mutants" categories = ["development-tools::testing"] [lib] proc-macro = true cargo-mutants-25.0.0/testdata/mutants_attrs/README.md000064400000000000000000000005131046102023000204730ustar 00000000000000# cargo-mutants `#[mutants]` attrs [![crates.io](https://img.shields.io/crates/v/mutants.svg)](https://crates.io/crates/mutants) This small crate defines attributes that can be attached into Rust code to guide `cargo-mutants`. The primary documentation is in the [`cargo-mutants` crate](https://crates.io/crates/cargo-mutants). cargo-mutants-25.0.0/testdata/mutants_attrs/src/lib.rs000064400000000000000000000015611046102023000211230ustar 00000000000000// Copyright 2021 Martin Pool //! Attribute macros to control how [cargo-mutants](https://crates.io/crates/cargo-mutants) mutates code. //! //! For example, a function that is difficult to test, or has disruptive effects when mutated, can //! be marked with [macro@skip]. //! //! # Changelog //! //! ## 0.0.3 //! //! * Reset edition to 2018 for broader compatibility. use proc_macro::TokenStream; /// `cargo mutants` should not mutate functions marked with this attribute. /// /// This can currently only be applied to functions, not modules or other syntactic constructs. /// /// ``` /// #[mutants::skip] /// pub fn some_difficult_function() { /// // ... /// } /// ``` /// /// This is a no-op during compilation, but is seen by cargo-mutants as it processes the source. #[proc_macro_attribute] pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream { item } cargo-mutants-25.0.0/testdata/nested_mod/Cargo_test.toml000064400000000000000000000001701046102023000214130ustar 00000000000000[package] name = "cargo-mutants-testdata-nested-mod" publish = false version = "0.0.0" edition = "2021" [dependencies] cargo-mutants-25.0.0/testdata/nested_mod/README.md000064400000000000000000000004731046102023000177110ustar 00000000000000# `nested_mod` testdata tree This tree contains many nested modules from both toplevel files (lib.rs and main.rs) to exercise the module file discovery mechanism. The module names are descriptive where necessary (e.g. `c_file` is a file) and short where details are not needed (e.g. `a`, `b` for simple nesting). cargo-mutants-25.0.0/testdata/nested_mod/src/block_in_lib/a/b/c_file/d/e/f_file.rs000064400000000000000000000000521046102023000257430ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/block_in_lib/a/b/c_file.rs000064400000000000000000000000741046102023000240540ustar 00000000000000pub mod d { pub mod e { pub mod f_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/block_in_main/a/b/c_file/d/e/f_file.rs000064400000000000000000000000521046102023000261210ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/block_in_main/a/b/c_file.rs000064400000000000000000000000741046102023000242320ustar 00000000000000pub mod d { pub mod e { pub mod f_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_lib/a/b/c_file/d/e/f_file.rs000064400000000000000000000000521046102023000255700ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_lib/a/b/c_file.rs000064400000000000000000000000741046102023000237010ustar 00000000000000pub mod d { pub mod e { pub mod f_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_lib.rs000064400000000000000000000000741046102023000220170ustar 00000000000000pub mod a { pub mod b { pub mod c_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_main/a/b/c_file/d/e/f_file.rs000064400000000000000000000000521046102023000257460ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_main/a/b/c_file.rs000064400000000000000000000000741046102023000240570ustar 00000000000000pub mod d { pub mod e { pub mod f_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/file_in_main.rs000064400000000000000000000000741046102023000221750ustar 00000000000000pub mod a { pub mod b { pub mod c_file; } } cargo-mutants-25.0.0/testdata/nested_mod/src/lib.rs000064400000000000000000000030741046102023000203350ustar 00000000000000pub mod block_in_lib { pub mod a { pub mod b { pub mod c_file; } } } pub mod file_in_lib; pub mod paths_in_lib { //! Loosely follows naming from examples in the reference //! pub mod a { pub mod b; } pub mod a_mod_file; #[path = "thread_files"] pub mod thread { #[path = "tls.rs"] pub mod local_data; } pub mod thread_inner_attr { //! `path` can also be an inner attribute on `mod foo { ... }` blocks #![path = "thread_files_inner_attr"] #[path = "tls.rs"] pub mod local_data; } pub mod upward_traversal; } #[path = "toplevel_file_in_lib.rs"] pub mod toplevel_in_lib; #[cfg(test)] mod tests { #[test] fn it_works() { assert!(crate::file_in_lib::a::b::c_file::d::e::f_file::always_true()); assert!(crate::block_in_lib::a::b::c_file::d::e::f_file::always_true()); assert!(crate::paths_in_lib::a::b::c::always_true()); assert!(crate::paths_in_lib::a_mod_file::c::always_true()); assert!(crate::paths_in_lib::a::b::inline::inner::always_true()); assert!(crate::paths_in_lib::a_mod_file::inline::inner::always_true()); assert!(crate::paths_in_lib::thread::local_data::always_true()); assert!(crate::paths_in_lib::thread_inner_attr::local_data::always_true()); assert!(crate::paths_in_lib::upward_traversal::target::always_true()); assert!(crate::toplevel_in_lib::always_true()); } } cargo-mutants-25.0.0/testdata/nested_mod/src/main.rs000064400000000000000000000027511046102023000205140ustar 00000000000000fn main() {} pub mod block_in_main { pub mod a { pub mod b { pub mod c_file; } } } pub mod file_in_main; pub mod paths_in_main { //! Loosely follows naming from examples in the reference //! pub mod a { pub mod b; } pub mod a_mod_file; #[path = "thread_files"] pub mod thread { #[path = "tls.rs"] pub mod local_data; } pub mod thread_inner_attr { //! `path` can also be an inner attribute on `mod foo { ... }` blocks #![path = "thread_files_inner_attr"] #[path = "tls.rs"] pub mod local_data; } } #[path = "toplevel_file_in_main.rs"] pub mod toplevel_in_main; #[cfg(test)] mod tests { #[test] fn it_works() { assert!(crate::file_in_main::a::b::c_file::d::e::f_file::always_true()); assert!(crate::block_in_main::a::b::c_file::d::e::f_file::always_true()); assert!(crate::paths_in_main::a::b::c::always_true()); assert!(crate::paths_in_main::a_mod_file::c::always_true()); assert!(crate::paths_in_main::a::b::inline::inner::always_true()); assert!(crate::paths_in_main::a_mod_file::inline::inner::always_true()); assert!(crate::paths_in_main::thread::local_data::always_true()); assert!(crate::paths_in_main::thread_inner_attr::local_data::always_true()); assert!(crate::toplevel_in_main::always_true()); } } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a/b/inline/other.rs000064400000000000000000000000521046102023000252530ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a/b.rs000064400000000000000000000001361046102023000226570ustar 00000000000000#[path = "foo.rs"] pub mod c; pub mod inline { #[path = "other.rs"] pub mod inner; } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a/foo.rs000064400000000000000000000000521046102023000232160ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a_mod_file/foo.rs000064400000000000000000000000521046102023000250540ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a_mod_file/inline/other.rs000064400000000000000000000000521046102023000266700ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/a_mod_file/mod.rs000064400000000000000000000001361046102023000250530ustar 00000000000000#[path = "foo.rs"] pub mod c; pub mod inline { #[path = "other.rs"] pub mod inner; } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/thread_files/tls.rs000064400000000000000000000000521046102023000254460ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/thread_files_inner_attr/tls.rs000064400000000000000000000000521046102023000276730ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_lib/upward_traversal.rs000064400000000000000000000001001046102023000255720ustar 00000000000000#[path = "../upward_traversal_file_for_lib.rs"] pub mod target; cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a/b/inline/other.rs000064400000000000000000000000521046102023000254310ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a/b.rs000064400000000000000000000001361046102023000230350ustar 00000000000000#[path = "foo.rs"] pub mod c; pub mod inline { #[path = "other.rs"] pub mod inner; } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a/foo.rs000064400000000000000000000000521046102023000233740ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a_mod_file/foo.rs000064400000000000000000000000521046102023000252320ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a_mod_file/inline/other.rs000064400000000000000000000000521046102023000270460ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/a_mod_file/mod.rs000064400000000000000000000001361046102023000252310ustar 00000000000000#[path = "foo.rs"] pub mod c; pub mod inline { #[path = "other.rs"] pub mod inner; } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/thread_files/tls.rs000064400000000000000000000000521046102023000256240ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/paths_in_main/thread_files_inner_attr/tls.rs000064400000000000000000000000521046102023000300510ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/toplevel_file_in_lib.rs000064400000000000000000000000521046102023000237250ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/toplevel_file_in_main.rs000064400000000000000000000000521046102023000241030ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/nested_mod/src/upward_traversal_file_for_lib.rs000064400000000000000000000000521046102023000256400ustar 00000000000000pub fn always_true() -> bool { true } cargo-mutants-25.0.0/testdata/never_type/Cargo_test.toml000064400000000000000000000002021046102023000214460ustar 00000000000000[package] name = "cargo-mutants-testdata-never-type" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false cargo-mutants-25.0.0/testdata/never_type/src/lib.rs000064400000000000000000000000541046102023000203670ustar 00000000000000pub fn never_returns() -> ! { loop {} } cargo-mutants-25.0.0/testdata/nightly_only/Cargo_test.toml000064400000000000000000000004561046102023000220200ustar 00000000000000[package] name = "nightly_only" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [workspace] # This isn't included in cargo-mutants overall workspace, because it would break builds on other toolchains cargo-mutants-25.0.0/testdata/nightly_only/README.md000064400000000000000000000007201046102023000203020ustar 00000000000000# `nightly_only` test tree This tree only builds on nightly Rust, and can be used to check that `cargo mutants` uses the corresponding `cargo` and `rustc` when building candidates. For example this should fail: cargo +stable mutants -d ./testdata/nightly_only/ and this should succeed: cargo +nightly mutants -d ./testdata/nightly_only/ This isn't covered by an integration test because there's no guarantee the user has both toolchains installed... cargo-mutants-25.0.0/testdata/nightly_only/src/lib.rs000064400000000000000000000002411046102023000207240ustar 00000000000000#![feature(box_patterns)] fn box_an_int() -> Box { Box::new(5) } #[test] fn unbox_by_pattern() { let box a = box_an_int(); assert_eq!(a, 5); } cargo-mutants-25.0.0/testdata/override_dependency/.cargo_test/config.toml000064400000000000000000000000401046102023000246660ustar 00000000000000paths = ["../../mutants_attrs"] cargo-mutants-25.0.0/testdata/override_dependency/Cargo_test.toml000064400000000000000000000010511046102023000233060ustar 00000000000000# Tests that paths in .cargo/config.toml are fixed up to be relative to the # source tree. # # This crate depends on the 'mutants' crate, but pulls it in from the # tree of the cargo-mutants repo, rather than crates.io. If the path # below wasn't fixed when copied to a scratch tree, the build would # fail. [package] name = "cargo-mutants-testdata-override-dependency" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false # This has to be a dependency that's published to crates.io. [dependencies] mutants = { version = "0" } cargo-mutants-25.0.0/testdata/override_dependency/src/lib.rs000064400000000000000000000001471046102023000222270ustar 00000000000000#[mutants::skip] pub fn hang() -> ! { loop {} } pub fn is_even(n: i32) -> bool { n % 2 == 0 } cargo-mutants-25.0.0/testdata/override_dependency/tests/api.rs000064400000000000000000000003621046102023000226040ustar 00000000000000use cargo_mutants_testdata_override_dependency::*; #[test] fn zero_is_even() { assert_eq!(is_even(0), true); } #[test] fn three_is_not_even() { assert_eq!(is_even(3), false); } #[test] fn two_is_even() { assert!(is_even(2)); } cargo-mutants-25.0.0/testdata/package_fails/Cargo_test.toml000064400000000000000000000003311046102023000220420ustar 00000000000000# This workspace has no root package, which is an interesting edge case to test: # older cargo-mutants assumed there was a root package and only tested it. [workspace] members = ["passing", "failing"] resolver = "2" cargo-mutants-25.0.0/testdata/package_fails/README.md000064400000000000000000000004061046102023000203350ustar 00000000000000# package-fails A tree with a workspace and two packages, one of which has already-failing tests. (Let's suppose they have a non-hermetic dependency.) It should still be possible to test the other. See . cargo-mutants-25.0.0/testdata/package_fails/failing/Cargo_test.toml000064400000000000000000000001631046102023000234560ustar 00000000000000[package] name = "cargo-mutants-testdata-package-fails-failing" version = "0.1.0" edition = "2021" publish = false cargo-mutants-25.0.0/testdata/package_fails/failing/src/lib.rs000064400000000000000000000002541046102023000223730ustar 00000000000000pub fn triple(a: usize) -> usize { 3 * a } #[cfg(test)] mod test { use super::*; #[test] fn triple_3_is_10() { assert_eq!(triple(3), 10); } } cargo-mutants-25.0.0/testdata/package_fails/passing/Cargo_test.toml000064400000000000000000000003251046102023000235110ustar 00000000000000[package] name = "cargo-mutants-testdata-package-fails-passing" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html cargo-mutants-25.0.0/testdata/package_fails/passing/src/lib.rs000064400000000000000000000001501046102023000224210ustar 00000000000000pub fn triple(a: usize) -> usize { a * 3 } #[test] fn triple_2() { assert_eq!(triple(2), 6); } cargo-mutants-25.0.0/testdata/parse_fails/Cargo_test.toml000064400000000000000000000003711046102023000215650ustar 00000000000000[package] name = "mutants-testdata-parse-fails" edition = "2018" version = "0.0.0" authors = ["Martin Pool"] publish = false [lib] doctest = false [workspace] # Don't include this in the overall cargo-mutants workspace, because it fails to build. cargo-mutants-25.0.0/testdata/parse_fails/src/lib.rs000064400000000000000000000001261046102023000204770ustar 00000000000000//! Test how cargo-mutants handles a tree that doesn't even parse This isn't Rust... cargo-mutants-25.0.0/testdata/patch_dependency/Cargo_test.toml000064400000000000000000000011041046102023000225650ustar 00000000000000# Tests that paths in the 'patch' section are rewritten. # # This crate depends on the 'mutants' crate, but pulls it in from the # tree of the cargo-mutants repo, rather than crates.io. If the path # below wasn't fixed when copied to a scratch tree, the build would # fail. [package] name = "cargo-mutants-testdata-patch-dependency" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false # This has to be a dependency that's published to crates.io. [dependencies] mutants = { version = "0" } [patch.crates-io] "mutants" = { path = "../../mutants_attrs" } cargo-mutants-25.0.0/testdata/patch_dependency/src/lib.rs000064400000000000000000000001471046102023000215070ustar 00000000000000#[mutants::skip] pub fn hang() -> ! { loop {} } pub fn is_even(n: i32) -> bool { n % 2 == 0 } cargo-mutants-25.0.0/testdata/patch_dependency/tests/api.rs000064400000000000000000000003571046102023000220700ustar 00000000000000use cargo_mutants_testdata_patch_dependency::*; #[test] fn zero_is_even() { assert_eq!(is_even(0), true); } #[test] fn three_is_not_even() { assert_eq!(is_even(3), false); } #[test] fn two_is_even() { assert!(is_even(2)); } cargo-mutants-25.0.0/testdata/proc_macro/Cargo_test.toml000064400000000000000000000002571046102023000214240ustar 00000000000000[package] name = "cargo-mutants-testdata-proc-macro" version = "0.0.0" edition = "2021" license = "MIT" publish = false [lib] proc-macro = true [dependencies] quote = "1.0" cargo-mutants-25.0.0/testdata/proc_macro/README.md000064400000000000000000000001471046102023000177120ustar 00000000000000# `testdata/proc_macro` An example of mutating a proc macro, and catching those mutations from tests. cargo-mutants-25.0.0/testdata/proc_macro/src/lib.rs000064400000000000000000000006251046102023000203370ustar 00000000000000use std::iter::once; use proc_macro::{Literal, TokenStream, TokenTree}; /// Count the number of items in a static array. #[proc_macro] pub fn static_len(item: TokenStream) -> TokenStream { let count = item .into_iter() .filter(|tt| !matches!(tt, TokenTree::Punct(p) if p.as_char() == ',')) .count(); once(TokenTree::Literal(Literal::usize_unsuffixed(count))).collect() } cargo-mutants-25.0.0/testdata/proc_macro/tests/macro.rs000064400000000000000000000003011046102023000212340ustar 00000000000000use cargo_mutants_testdata_proc_macro::static_len; #[test] fn static_len() { assert_eq!(static_len!(2, 3, 4, 5), 4); } #[test] fn static_len_empty() { assert_eq!(static_len!(), 0); } cargo-mutants-25.0.0/testdata/relative_dependency/Cargo_test.toml000064400000000000000000000003511046102023000233040ustar 00000000000000[package] name = "cargo-mutants-testdata-relative-dependency" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false [dependencies] cargo-mutants-testdata-dependency = { version = "0", path = "../dependency" } cargo-mutants-25.0.0/testdata/relative_dependency/src/lib.rs000064400000000000000000000002561046102023000222240ustar 00000000000000use cargo_mutants_testdata_dependency::factorial; pub fn double_factorial(n: i32) -> u32 { if n < 0 { return 0; } 2 * factorial(n.try_into().unwrap()) } cargo-mutants-25.0.0/testdata/relative_dependency/tests/api.rs000064400000000000000000000004771046102023000226070ustar 00000000000000use cargo_mutants_testdata_relative_dependency::double_factorial; #[test] fn double_factorial_zero_is_2() { assert_eq!(double_factorial(0), 2); } #[test] fn double_factorial_one_is_2() { assert_eq!(double_factorial(1), 2); } #[test] fn double_factorial_two_is_4() { assert_eq!(double_factorial(2), 4); } cargo-mutants-25.0.0/testdata/replace_dependency/Cargo_test.toml000064400000000000000000000011061046102023000231030ustar 00000000000000# Tests that paths in the 'replace' section are rewritten. # # This crate depends on the 'mutants' crate, but pulls it in from the # tree of the cargo-mutants repo, rather than crates.io. If the path # below wasn't fixed when copied to a scratch tree, the build would # fail. [package] name = "cargo-mutants-testdata-replace-dependency" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false # This has to be a dependency that's published to crates.io. [dependencies] mutants = { version = "0" } [replace] "mutants:0.0.3" = { path = "../../mutants_attrs" } cargo-mutants-25.0.0/testdata/replace_dependency/src/lib.rs000064400000000000000000000001471046102023000220230ustar 00000000000000#[mutants::skip] pub fn hang() -> ! { loop {} } pub fn is_even(n: i32) -> bool { n % 2 == 0 } cargo-mutants-25.0.0/testdata/replace_dependency/tests/api.rs000064400000000000000000000003611046102023000223770ustar 00000000000000use cargo_mutants_testdata_replace_dependency::*; #[test] fn zero_is_even() { assert_eq!(is_even(0), true); } #[test] fn three_is_not_even() { assert_eq!(is_even(3), false); } #[test] fn two_is_even() { assert!(is_even(2)); } cargo-mutants-25.0.0/testdata/small_well_tested/Cargo_test.toml000064400000000000000000000003421046102023000227760ustar 00000000000000[package] name = "cargo-mutants-testdata-small-well-tested" description = "A small tree with one function with good coverage" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false [lib] doctest = false cargo-mutants-25.0.0/testdata/small_well_tested/src/lib.rs000064400000000000000000000007041046102023000217140ustar 00000000000000//! A small tree with one function with good coverage: a fast-to-run successful //! case for cargo-mutants. pub fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[cfg(test)] mod test { use super::*; #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } } cargo-mutants-25.0.0/testdata/strict_warnings/Cargo_test.toml000064400000000000000000000003001046102023000225050ustar 00000000000000[package] name = "cargo-mutants-testdata-strict-warnings" version = "0.1.0" edition = "2018" [dependencies.mutants] version = "0.0.3" # path = "../../../mutants_attrs" [lib] doctest = false cargo-mutants-25.0.0/testdata/strict_warnings/src/lib.rs000064400000000000000000000003731046102023000214330ustar 00000000000000//! Example of code with strict warnings that might fail to compile. #![forbid(unused)] pub fn some_fn(a: usize) -> usize { a + 2 } #[cfg(test)] mod test { #[test] fn test_some_fn() { assert_eq!(super::some_fn(10), 12); } } cargo-mutants-25.0.0/testdata/struct_with_no_default/Cargo_test.toml000064400000000000000000000002161046102023000240520ustar 00000000000000[package] name = "cargo-mutants-testdata-struct-with-no-default" version = "0.0.0" edition = "2018" authors = ["Martin Pool"] publish = false cargo-mutants-25.0.0/testdata/struct_with_no_default/src/lib.rs000064400000000000000000000006401046102023000227670ustar 00000000000000//! Example of a struct with no Default that generates unviable mutants. #![allow(dead_code)] pub struct S { a: &'static str, b: usize, } // This can't be called "new" because that name is specifically excluded. pub fn make_an_s() -> S { S { a: "on the beach", b: 99, } } #[test] fn test_new_s() { let s = make_an_s(); assert!(!s.a.is_empty()); assert_eq!(s.b, 99); } cargo-mutants-25.0.0/testdata/symlink/Cargo_test.toml000064400000000000000000000001741046102023000207640ustar 00000000000000[package] name = "cargo-mutants-testdata-symlink" version = "0.0.0" edition = "2021" publish = false [lib] doctest = false cargo-mutants-25.0.0/testdata/symlink/README.md000064400000000000000000000005501046102023000172520ustar 00000000000000# testdata/symlink This is a source tree which when built will contain a symlink in its testdata. The symlink must exist for the tests to pass. This is used to test that cargo-mutants copies the symlinks correctly, especially on Windows. Because `cargo publish` doesn't include symlinks, the symlink is created when the directory is copied to run the tests. cargo-mutants-25.0.0/testdata/symlink/src/lib.rs000064400000000000000000000002731046102023000177000ustar 00000000000000use std::path::Path; pub fn read_through_symlink() -> String { let path = Path::new("testdata/symlink"); assert!(path.is_symlink()); std::fs::read_to_string(path).unwrap() } cargo-mutants-25.0.0/testdata/symlink/testdata/target000064400000000000000000000000151046102023000210110ustar 00000000000000Hello, world!cargo-mutants-25.0.0/testdata/symlink/tests/main.rs000064400000000000000000000013351046102023000204310ustar 00000000000000use std::fs::{read_link, read_to_string}; use std::path::Path; use cargo_mutants_testdata_symlink::read_through_symlink; #[test] fn read_through_symlink_test() { assert_eq!(read_through_symlink().trim(), "Hello, world!"); } /// This should fail from the baseline test if the symlink is somehow /// missing. #[test] fn symlink_testdata_exists() { let target = Path::new("testdata/target"); let symlink = Path::new("testdata/symlink"); assert!(symlink.is_symlink()); assert!(target.is_file()); assert_eq!(read_link(&symlink).unwrap(), Path::new("target")); assert_eq!(read_to_string(&target).unwrap().trim(), "Hello, world!"); assert_eq!(read_to_string(&symlink).unwrap().trim(), "Hello, world!"); } cargo-mutants-25.0.0/testdata/typecheck_fails/Cargo_test.toml000064400000000000000000000003751046102023000224360ustar 00000000000000[package] name = "mutants-testdata-typecheck-fails" edition = "2018" version = "0.0.0" authors = ["Martin Pool"] publish = false [lib] doctest = false [workspace] # Don't include this in the overall cargo-mutants workspace, because it fails to build. cargo-mutants-25.0.0/testdata/typecheck_fails/src/lib.rs000064400000000000000000000005531046102023000213500ustar 00000000000000//! Test how cargo-mutants handles a tree that doesn't build. //! //! This version is at least parseable as a Rust AST (so that we can list mutants) but it won't typecheck. fn try_value_coercion() -> String { "1" + 2 // Doesn't work in Rust: just as well! } #[test] fn add_string_and_integer() { assert_eq!(try_value_coercion(), "3"); // probably not } cargo-mutants-25.0.0/testdata/unapply/Cargo_test.toml000064400000000000000000000003551046102023000207670ustar 00000000000000[package] name = "cargo-mutants-testdata-unapply" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [lib] doctest = false cargo-mutants-25.0.0/testdata/unapply/README.md000064400000000000000000000003541046102023000172560ustar 00000000000000# `cargo-mutants-testdata-unapply` A test tree with several source files and several functions, some of which will be caught and some not. This tests that the tree is correctly restored after a mutation is applied and then unapplied. cargo-mutants-25.0.0/testdata/unapply/src/a.rs000064400000000000000000000001231046102023000173460ustar 00000000000000pub fn one() -> i32 { 1 } #[test] fn test_one() { assert_eq!(one(), 1); } cargo-mutants-25.0.0/testdata/unapply/src/b.rs000064400000000000000000000000471046102023000173540ustar 00000000000000pub fn one_untested() -> i32 { 1 } cargo-mutants-25.0.0/testdata/unapply/src/c.rs000064400000000000000000000001231046102023000173500ustar 00000000000000pub fn one() -> i32 { 1 } #[test] fn test_one() { assert_eq!(one(), 1); } cargo-mutants-25.0.0/testdata/unapply/src/lib.rs000064400000000000000000000000411046102023000176730ustar 00000000000000pub mod a; pub mod b; pub mod c; cargo-mutants-25.0.0/testdata/unsafe/Cargo_test.toml000064400000000000000000000001761046102023000205610ustar 00000000000000[package] name = "cargo-mutants-testdata-unsafe" version = "0.0.0" edition = "2021" authors = ["Martin Pool"] publish = false cargo-mutants-25.0.0/testdata/unsafe/src/lib.rs000064400000000000000000000002071046102023000174700ustar 00000000000000pub unsafe fn unsafe_fn() -> usize { 42 } #[test] fn test_unsafe_fn() { unsafe { assert_eq!(unsafe_fn(), 42); } } cargo-mutants-25.0.0/testdata/well_tested/Cargo_test.toml000064400000000000000000000002231046102023000216040ustar 00000000000000[package] name = "cargo-mutants-testdata-well-tested" version = "0.1.0" edition = "2021" publish = false [dependencies.mutants] version = "0.0.3" cargo-mutants-25.0.0/testdata/well_tested/README.md000064400000000000000000000003571046102023000201040ustar 00000000000000An example tree for `cargo-mutants` with examples of sites where mutants could be, or shouldn't be, applied. Every example in this tree should be adequately tested so that: 1. All tests pass in a clean tree. 2. Every mutation is caught. cargo-mutants-25.0.0/testdata/well_tested/src/arc.rs000064400000000000000000000002561046102023000205250ustar 00000000000000use std::sync::Arc; fn return_arc() -> Arc { Arc::new(String::from("hello!")) } #[test] fn returns_hello() { assert_eq!(return_arc().as_ref(), "hello!"); } cargo-mutants-25.0.0/testdata/well_tested/src/booleans.rs000064400000000000000000000017261046102023000215650ustar 00000000000000fn and(a: bool, b: bool) -> bool { a && b } fn or(a: bool, b: bool) -> bool { a || b } fn xor(a: bool, b: bool) -> bool { a ^ b } fn not(a: bool) -> bool { !a } #[cfg(test)] mod test { use super::*; #[test] fn all_and() { assert_eq!(and(false, false), false); assert_eq!(and(true, false), false); assert_eq!(and(false, true), false); assert_eq!(and(true, true), true); } #[test] fn all_or() { assert_eq!(or(false, false), false); assert_eq!(or(true, false), true); assert_eq!(or(false, true), true); assert_eq!(or(true, true), true); } #[test] fn all_xor() { assert_eq!(xor(false, false), false); assert_eq!(xor(true, false), true); assert_eq!(xor(false, true), true); assert_eq!(xor(true, true), false); } #[test] fn all_not() { assert_eq!(not(false), true); assert_eq!(not(true), false); } } cargo-mutants-25.0.0/testdata/well_tested/src/empty_fns.rs000064400000000000000000000006251046102023000217640ustar 00000000000000//! These functions have empty bodies; we cannot usefully mutate them. fn just_empty() {} fn only_a_comment() { /* it's still basically empty */ } struct Foo(); impl Foo { fn empty_in_foo(&self) { /* also caught */ } } #[cfg(test)] mod test { use super::*; #[test] fn empty_fns_do_nothing() { just_empty(); only_a_comment(); Foo().empty_in_foo(); } } cargo-mutants-25.0.0/testdata/well_tested/src/inside_mod.rs000064400000000000000000000003031046102023000220630ustar 00000000000000mod outer { mod inner { pub fn name() -> &'static str { "Bob" } #[test] fn test_name() { assert_eq!(name(), "Bob"); } } } cargo-mutants-25.0.0/testdata/well_tested/src/item_mod.rs000064400000000000000000000002151046102023000215500ustar 00000000000000/// A module that's not only for tests, but should be excluded anyhow. #[mutants::skip] mod skip_this_mod { fn inside_skipped_mod() {} } cargo-mutants-25.0.0/testdata/well_tested/src/lib.rs000064400000000000000000000007531046102023000205300ustar 00000000000000//! An example tree for `cargo-mutants` with examples of sites where mutants could be, or //! shouldn't be, applied. //! //! In this well-tested tree: //! //! 1. The tests should all pass in a clean tree. //! 2. Every mutant is caught. #![allow(unused, dead_code)] mod arc; mod booleans; mod empty_fns; mod inside_mod; mod item_mod; mod methods; mod nested_function; mod numbers; mod result; mod sets; pub mod simple_fns; mod slices; mod static_item; mod struct_with_lifetime; mod traits; cargo-mutants-25.0.0/testdata/well_tested/src/methods.rs000064400000000000000000000021661046102023000214250ustar 00000000000000//! Demonstrate mutation of impl methods. #![allow(clippy::disallowed_names)] // "Foo" is just an example name. use std::fmt; struct Foo { i: u32, } impl Foo { pub fn new() -> Foo { Foo { i: 32 } } pub fn double(&mut self) { self.i *= 2; } } impl fmt::Display for Foo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Foo {}", self.i) } } impl fmt::Debug for &Foo { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "&Foo {}", self.i) } } impl Default for Foo { fn default() -> Self { Foo::new() } } #[test] fn double() { let mut foo = Foo::new(); assert_eq!(foo.i, 32); foo.double(); assert_eq!(foo.i, 64); foo.double(); assert_eq!(foo.i, 128); } #[test] fn default() { let foo = Foo::default(); assert_eq!(foo.i, 32); } #[test] fn new_foo() { let foo = Foo::new(); assert_eq!(foo.i, 32); } #[test] fn display_foo() { assert_eq!(format!("{}", Foo { i: 123 }), "Foo 123"); } #[test] fn debug_ref_foo() { assert_eq!(format!("{:?}", &Foo { i: 123 }), "&Foo 123"); } cargo-mutants-25.0.0/testdata/well_tested/src/nested_function.rs000064400000000000000000000003131046102023000231410ustar 00000000000000fn has_nested() -> u32 { fn inner() -> u32 { 12 } inner() * inner() } #[cfg(test)] mod test { #[test] fn has_nested() { assert_eq!(super::has_nested(), 144); } } cargo-mutants-25.0.0/testdata/well_tested/src/numbers.rs000064400000000000000000000023511046102023000214310ustar 00000000000000fn double_float(a: f32) -> f32 { 2.0 * a } fn is_double(a: u32, b: u32) -> bool { a == 2 * b } fn negate_i32(a: i32) -> i32 { -a } fn negate_f32(a: f32) -> f32 { -a } fn bitwise_not_i32(a: i32) -> i32 { !a } fn bitwise_not_u32(a: u32) -> u32 { !a } #[cfg(test)] mod test { use super::*; #[test] fn double_zero() { assert_eq!(double_float(0.0), 0.0); } #[test] fn double_three() { assert_eq!(double_float(3.0), 6.0); } #[test] fn is_double_zero() { assert!(is_double(0, 0)); } #[test] fn is_double_one() { assert!(is_double(2, 1)); assert!(!is_double(1, 1)); assert!(!is_double(5, 1)); } #[test] fn negate_one() { assert_eq!(negate_i32(1), -1); assert_eq!(negate_f32(1.0), -1.0); } #[test] fn negate_two() { assert_eq!(negate_i32(2), -2); assert_eq!(negate_f32(2.0), -2.0); } #[test] fn bitwise_not_one() { assert_eq!(bitwise_not_i32(1), -2); assert_eq!(bitwise_not_u32(1), u32::MAX - 1); } #[test] fn bitwise_not_two() { assert_eq!(bitwise_not_i32(2), -3); assert_eq!(bitwise_not_u32(2), u32::MAX - 2); } } cargo-mutants-25.0.0/testdata/well_tested/src/result.rs000064400000000000000000000016531046102023000213000ustar 00000000000000// /// Function returning a Result. // fn io_result() -> std::io::Result< /// Simple easily-recognizable Result. fn simple_result() -> Result<&'static str, ()> { Ok("success") } fn error_if_negative(a: i32) -> Result<(), ()> { if a < 0 { Err(()) } else { Ok(()) } } fn result_with_no_apparent_type_args() -> std::fmt::Result { Err(Default::default()) } mod test { use super::*; #[test] fn simple_result_success() { assert_eq!(simple_result(), Ok("success")); } #[test] fn error_if_negative() { use super::error_if_negative; assert_eq!(error_if_negative(0), Ok(())); assert_eq!(error_if_negative(-1), Err(())); assert_eq!(error_if_negative(1), Ok(())); } #[test] fn fmt_result_fails() { let r = super::result_with_no_apparent_type_args(); assert!(r.is_err(), "Result should be an error: {r:?}"); } } cargo-mutants-25.0.0/testdata/well_tested/src/sets.rs000064400000000000000000000003661046102023000207400ustar 00000000000000use std::collections::BTreeSet; fn make_a_set() -> BTreeSet { let mut s = BTreeSet::new(); s.insert("one".into()); s.insert("two".into()); s } #[test] fn set_has_two_elements() { assert_eq!(make_a_set().len(), 2); } cargo-mutants-25.0.0/testdata/well_tested/src/simple_fns.rs000064400000000000000000000022621046102023000221160ustar 00000000000000/// This function is only built for tests so shouldn't be mutated. #[cfg(test)] fn outer_test_helper() { panic!() } fn returns_unit(a: &mut u32) { *a += 1; } /// Can be mutated to return default (0). fn returns_42u32() -> u32 { 42 } /// Can be mutated to return bool::default. fn divisible_by_three(a: u32) -> bool { a % 3 == 0 } /// Return `s` repeated twice. /// /// ``` /// assert_eq!(cargo_mutants_testdata_well_tested::simple_fns::double_string("cat"), "catcat"); /// ``` pub fn double_string(s: &str) -> String { let mut r = s.to_owned(); r.push_str(s); r } #[cfg(test)] mod tests { use super::*; /// A test helper function: it shouldn't be mutated because it's inside a /// `#[cfg(tests)]` mod. fn test_helper() -> usize { 42 } #[test] fn use_test_helper() { let result = 2 + test_helper(); assert_eq!(result, 44); } #[test] fn main_tests() { assert_eq!(returns_42u32(), 42); assert!(divisible_by_three(0)); assert!(divisible_by_three(9)); assert!(!divisible_by_three(20)); let mut a = 0; returns_unit(&mut a); assert_eq!(a, 1); } } cargo-mutants-25.0.0/testdata/well_tested/src/slices.rs000064400000000000000000000011641046102023000212410ustar 00000000000000use std::borrow::Cow; fn pad<'a>(aa: &'a mut [Cow<'static, str>]) -> &'a [Cow<'static, str>] { for a in aa.iter_mut() { if a.len() < 3 { a.to_mut().push_str("___"); } } aa } fn return_mut_slice(a: &mut [usize]) -> &mut [usize] { for x in a.iter_mut() { *x *= 2 } a } #[cfg(test)] mod test { #[test] fn test_pad() { assert_eq!( super::pad(&mut ["hello".into(), "ok".into()]), ["hello", "ok___"] ); } #[test] fn mut_slice() { assert_eq!(super::return_mut_slice(&mut [1, 2, 3]), [2, 4, 6]); } } cargo-mutants-25.0.0/testdata/well_tested/src/static_item.rs000064400000000000000000000002331046102023000222600ustar 00000000000000static SHOULD_BE_TRUE: bool = 3 == (2 + 1); mod test { #[test] fn static_expression_evaluated() { assert!(super::SHOULD_BE_TRUE); } } cargo-mutants-25.0.0/testdata/well_tested/src/struct_with_lifetime.rs000064400000000000000000000007061046102023000242150ustar 00000000000000//! Show how to handle a struct with lifetime. pub(crate) struct Lex<'buf> { buf: &'buf [u8], /// Position of the cursor within `buf`. pos: usize, } impl<'buf> Lex<'buf> { pub fn new(buf: &'buf [u8]) -> Lex<'buf> { Lex { buf, pos: 0 } } pub fn buf_len(&self) -> usize { self.buf.len() } } #[test] fn get_as_slice() { let buf = b"hello"; let lex = Lex::new(buf); assert_eq!(lex.buf_len(), 5); } cargo-mutants-25.0.0/testdata/well_tested/src/traits.rs000064400000000000000000000005311046102023000212620ustar 00000000000000//! Test mutation of a default fn in a trait. trait Something { fn is_three(&self, a: usize) -> bool { a == 3 } } #[cfg(test)] mod test { use super::*; struct Three; impl Something for Three {} #[test] fn test_is_three() { assert!(Three.is_three(3)); assert!(!Three.is_three(4)); } } cargo-mutants-25.0.0/testdata/with_child_directories/Cargo_test.toml000064400000000000000000000002361046102023000240070ustar 00000000000000[package] name = "cargo-mutants-testdata-with-child-directories" version = "0.1.0" edition = "2018" publish = false [dependencies.mutants] version = "0.0.3" cargo-mutants-25.0.0/testdata/with_child_directories/README.md000064400000000000000000000002621046102023000222760ustar 00000000000000# `with_child_directories` An example tree for `cargo-mutants` with various subdirectories, to test matching filenames. All tests should pass and everything should be covered. cargo-mutants-25.0.0/testdata/with_child_directories/src/lib.rs000064400000000000000000000002311046102023000227160ustar 00000000000000//! An example tree for `cargo-mutants` with various subdirectories, to test matching filenames. #![allow(unused, dead_code)] mod methods; mod module; cargo-mutants-25.0.0/testdata/with_child_directories/src/methods.rs000064400000000000000000000003061046102023000236160ustar 00000000000000pub fn double(x: usize) -> usize { x * 2 } #[cfg(test)] mod test { #[test] fn test_double() { assert_eq!(super::double(2), 4); assert_eq!(super::double(8), 16); } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/module_methods.rs000064400000000000000000000003521046102023000264510ustar 00000000000000pub fn double(x: usize) -> usize { x * 2 } #[cfg(test)] mod test { #[test] fn double() { assert_eq!(super::double(2), 4); assert_eq!(super::double(0), 0); assert_eq!(super::double(6), 12); } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/unreferenced_mod.rs000064400000000000000000000001751046102023000267500ustar 00000000000000//! This source file is not referenced by any `mod` statement, so it should not be visited. fn untested() -> i32 { 32 } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/inside_mod.rs000064400000000000000000000003031046102023000267070ustar 00000000000000mod outer { mod inner { pub fn name() -> &'static str { "Bob" } #[test] fn test_name() { assert_eq!(name(), "Bob"); } } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/inside_utils_empty_fns.rs000064400000000000000000000006251046102023000313630ustar 00000000000000//! These functions have empty bodies; we cannot usefully mutate them. fn just_empty() {} fn only_a_comment() { /* it's still basically empty */ } struct Foo(); impl Foo { fn empty_in_foo(&self) { /* also caught */ } } #[cfg(test)] mod test { use super::*; #[test] fn empty_fns_do_nothing() { just_empty(); only_a_comment(); Foo().empty_in_foo(); } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/mod.rs000064400000000000000000000001201046102023000253510ustar 00000000000000mod inside_mod; mod inside_utils_empty_fns; mod nested_function; mod sub_utils; cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/nested_function.rs000064400000000000000000000003131046102023000277650ustar 00000000000000fn has_nested() -> u32 { fn inner() -> u32 { 12 } inner() * inner() } #[cfg(test)] mod test { #[test] fn has_nested() { assert_eq!(super::has_nested(), 144); } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/sub_utils/mod.rs000064400000000000000000000000361046102023000273700ustar 00000000000000mod subutils_nested_function; ././@LongLink00006440000000000000000000000154000000000000007773Lustar cargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/sub_utils/subutils_nested_function.rscargo-mutants-25.0.0/testdata/with_child_directories/src/module/utils/sub_utils/subutils_nested_func000064400000000000000000000003131046102023000324130ustar 00000000000000fn has_nested() -> u32 { fn inner() -> u32 { 12 } inner() * inner() } #[cfg(test)] mod test { #[test] fn has_nested() { assert_eq!(super::has_nested(), 144); } } cargo-mutants-25.0.0/testdata/with_child_directories/src/module.rs000064400000000000000000000001151046102023000234360ustar 00000000000000mod module_methods; // Also an example of handling raw symbols: mod r#utils; cargo-mutants-25.0.0/testdata/workspace/Cargo_test.toml000064400000000000000000000003351046102023000212730ustar 00000000000000# This workspace has no root package, which is an interesting edge case to test: # older cargo-mutants assumed there was a root package and only tested it. [workspace] members = ["main", "main2", "utils"] resolver = "2" cargo-mutants-25.0.0/testdata/workspace/README.md000064400000000000000000000000471046102023000175630ustar 00000000000000# Test project for cargo mutants issue cargo-mutants-25.0.0/testdata/workspace/main/Cargo_test.toml000064400000000000000000000004501046102023000222150ustar 00000000000000[package] name = "main" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo_mutants_testdata_workspace_utils = { path = "../utils", default-features = false } mutants = "0.0.3" cargo-mutants-25.0.0/testdata/workspace/main/src/main.rs000064400000000000000000000005531046102023000213130ustar 00000000000000use cargo_mutants_testdata_workspace_utils::triple; #[mutants::skip] fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } println!("3 * 3 = {}", triple(3)); } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a } #[test] fn factorial_5() { assert_eq!(factorial(5), 120); } cargo-mutants-25.0.0/testdata/workspace/main2/Cargo_test.toml000064400000000000000000000004511046102023000223000ustar 00000000000000[package] name = "main2" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo_mutants_testdata_workspace_utils = { path = "../utils", default-features = false } mutants = "0.0.3" cargo-mutants-25.0.0/testdata/workspace/main2/src/main.rs000064400000000000000000000005021046102023000213670ustar 00000000000000use cargo_mutants_testdata_workspace_utils::triple; #[mutants::skip] fn main() { println!("Print works from main2 binary"); println!("3 * 3 = {}", triple_3()); } fn triple_3() -> i32 { triple(3) } mod test { use super::*; #[test] fn triple_3_test() { assert_eq!(triple_3(), 9); } } cargo-mutants-25.0.0/testdata/workspace/utils/Cargo_test.toml000064400000000000000000000003371046102023000224350ustar 00000000000000[package] name = "cargo_mutants_testdata_workspace_utils" version = "0.1.0" edition = "2021" publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cargo-mutants-25.0.0/testdata/workspace/utils/src/lib.rs000064400000000000000000000001471046102023000213500ustar 00000000000000pub fn triple(a: i32) -> i32 { a * 3 } #[test] fn test_triple() { assert_eq!(triple(3), 9); } cargo-mutants-25.0.0/tests/.gitattributes000064400000000000000000000000231046102023000165240ustar 00000000000000*.snap -whitespace cargo-mutants-25.0.0/tests/build_dir.rs000064400000000000000000000035041046102023000161430ustar 00000000000000// Copyright 2023-2024 Martin Pool use std::fs::{create_dir, write}; mod util; use util::{copy_of_testdata, run}; #[test] fn gitignore_respected_in_copy_by_default() { // Make a tree with a (dumb) gitignore that excludes the source file; when you copy it // to a build directory, the source file should not be there and so the check will fail. let tmp = copy_of_testdata("factorial"); // There must be something that looks like a `.git` dir, otherwise we don't read // `.gitignore` files. create_dir(tmp.path().join(".git")).unwrap(); write(tmp.path().join(".gitignore"), b"src\n").unwrap(); run() .args(["mutants", "--check", "-d"]) .arg(tmp.path()) .assert() .stdout(predicates::str::contains("can't find `factorial` bin")) .code(4); } #[test] fn gitignore_can_be_turned_off() { // Make a tree with a (dumb) gitignore that excludes the source file; when you copy it // to a build directory, with gitignore off, it succeeds. let tmp = copy_of_testdata("factorial"); write(tmp.path().join(".gitignore"), b"src\n").unwrap(); run() .args(["mutants", "--check", "--gitignore=false", "-d"]) .arg(tmp.path()) .assert() .success(); } /// A tree containing a symlink that must exist for the tests to pass works properly. #[test] fn symlink_in_source_tree_is_copied() { let tmp = copy_of_testdata("symlink"); let testdata = tmp.path().join("testdata"); #[cfg(unix)] std::os::unix::fs::symlink("target", testdata.join("symlink")).unwrap(); #[cfg(windows)] std::os::windows::fs::symlink_file("target", testdata.join("symlink")).unwrap(); assert!(tmp.path().join("testdata").join("symlink").is_symlink()); run() .args(["mutants", "-d"]) .arg(tmp.path()) .assert() .success(); } cargo-mutants-25.0.0/tests/check_build.rs000064400000000000000000000144561046102023000164520ustar 00000000000000// Copyright 2024 Martin Pool //! Tests for `--check` use indoc::indoc; use predicates::prelude::*; use pretty_assertions::assert_eq; mod util; use util::{copy_of_testdata, outcome_json_counts, run}; #[test] fn small_well_tested_tree_check_only() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .args(["mutants", "--check", "--no-shuffle", "--no-times"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(indoc! {r" Found 4 mutants to test ok Unmutated baseline ok src/lib.rs:5:5: replace factorial -> u32 with 0 ok src/lib.rs:5:5: replace factorial -> u32 with 1 ok src/lib.rs:7:11: replace *= with += in factorial ok src/lib.rs:7:11: replace *= with /= in factorial 4 mutants tested: 4 succeeded "}) .stderr(""); let outcomes = outcome_json_counts(&tmp_src_dir); assert_eq!( outcomes, serde_json::json!({ "success": 4, // They did all build "caught": 0, // They weren't actually tested "unviable": 0, "missed": 0, "timeout": 0, "total_mutants": 4, }) ); } #[test] fn small_well_tested_tree_check_only_shuffled() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .args(["mutants", "--check", "--no-times", "--shuffle"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::str::contains("4 mutants tested: 4 succeeded")); assert_eq!( outcome_json_counts(&tmp_src_dir), serde_json::json!({ "success": 4, // They did all build "caught": 0, // They weren't actually tested "unviable": 0, "missed": 0, "timeout": 0, "total_mutants": 4, }) ); } #[test] fn warning_when_no_mutants_found() { let tmp_src_dir = copy_of_testdata("everything_skipped"); run() .args(["mutants", "--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .assert() .stderr(predicate::str::contains( "No mutants found under the active filters", )) .stdout(predicate::str::contains("Found 0 mutants to test")) .success(); // It's arguable, but better if CI doesn't fail in this case. // There is no outcomes.json? Arguably a bug. } #[test] fn check_succeeds_in_tree_that_builds_but_fails_tests() { // --check doesn't actually run the tests so won't discover that they fail. let tmp_src_dir = copy_of_testdata("already_failing_tests"); run() .args(["mutants", "--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout, @r###" Found 4 mutants to test ok Unmutated baseline ok src/lib.rs:2:5: replace factorial -> u32 with 0 ok src/lib.rs:2:5: replace factorial -> u32 with 1 ok src/lib.rs:4:11: replace *= with += in factorial ok src/lib.rs:4:11: replace *= with /= in factorial 4 mutants tested: 4 succeeded "###); true })); assert_eq!( outcome_json_counts(&tmp_src_dir), serde_json::json!({ "caught": 0, "missed": 0, "success": 4, "timeout": 0, "unviable": 0, "total_mutants": 4, }) ); } #[test] fn check_tree_with_mutants_skip() { let tmp_src_dir = copy_of_testdata("hang_avoided_by_attr"); run() .arg("mutants") .args(["--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success() .stdout(indoc! { r" Found 5 mutants to test ok Unmutated baseline ok src/lib.rs:15:5: replace controlled_loop with () ok src/lib.rs:21:28: replace > with == in controlled_loop ok src/lib.rs:21:28: replace > with < in controlled_loop ok src/lib.rs:21:53: replace * with + in controlled_loop ok src/lib.rs:21:53: replace * with / in controlled_loop 5 mutants tested: 5 succeeded "}) .stderr(""); assert_eq!( outcome_json_counts(&tmp_src_dir), serde_json::json!({ "caught": 0, "missed": 0, "success": 5, "timeout": 0, "unviable": 0, "total_mutants": 5, }) ); } #[test] fn check_tree_where_build_fails() { let tmp_src_dir = copy_of_testdata("typecheck_fails"); run() .arg("mutants") .args(["--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .code(4) // clean tests failed .stdout(predicate::str::contains("FAILED Unmutated baseline")); assert_eq!( outcome_json_counts(&tmp_src_dir), serde_json::json!({ "caught": 0, "missed": 0, "success": 0, "timeout": 0, "unviable": 0, "total_mutants": 0, }) ); } #[test] fn unviable_mutation_of_struct_with_no_default() { let tmp_src_dir = copy_of_testdata("struct_with_no_default"); run() .args([ "mutants", "--line-col=false", "--check", "--no-times", "--no-shuffle", "-v", "-V", ]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success() .stdout( predicate::str::is_match( r"unviable *src/lib.rs:\d+:\d+: replace make_an_s -> S with Default::default\(\)", ) .unwrap(), ); assert_eq!( outcome_json_counts(&tmp_src_dir), serde_json::json!({ "success": 0, "caught": 0, "unviable": 1, "missed": 0, "timeout": 0, "total_mutants": 1, }) ); } cargo-mutants-25.0.0/tests/colors.rs000064400000000000000000000051651046102023000155140ustar 00000000000000// Copyright 2024 Martin Pool //! Tests for color output. // Testing autodetection seems hard because we'd have to make a tty, so we'll rely on humans noticing // for now. use predicates::prelude::*; mod util; use util::run; fn has_color_listing() -> impl Predicate { predicates::str::contains("with \x1b[33m0\x1b[0m") } fn has_ansi_escape() -> impl Predicate { predicates::str::contains("\x1b[") } fn has_color_debug() -> impl Predicate { predicates::str::contains("\x1b[34mDEBUG\x1b[0m") } /// The test fixtures force off colors, even if something else tries to turn it on. #[test] fn no_color_in_test_subprocesses_by_default() { run() .args(["mutants", "-d", "testdata/small_well_tested", "--list"]) .assert() .success() .stdout(has_ansi_escape().not()) .stderr(has_ansi_escape().not()); } /// Colors can be turned on with `--color` and they show up in the listing and /// in trace output. #[test] fn colors_always_shows_in_stdout_and_trace() { run() .args([ "mutants", "-d", "testdata/small_well_tested", "--list", "--colors=always", "-Ltrace", ]) .assert() .success() .stdout(has_color_listing()) .stderr(has_color_debug()); } #[test] fn cargo_term_color_env_shows_colors() { run() .env("CARGO_TERM_COLOR", "always") .args([ "mutants", "-d", "testdata/small_well_tested", "--list", "-Ltrace", ]) .assert() .success() .stdout(has_color_listing()) .stderr(has_color_debug()); } #[test] fn invalid_cargo_term_color_rejected_with_message() { run() .env("CARGO_TERM_COLOR", "invalid") .args([ "mutants", "-d", "testdata/small_well_tested", "--list", "-Ltrace", ]) .assert() .stderr(predicate::str::contains( // The message does not currently name the variable due to . "invalid value 'invalid'", )) .code(1); } /// Colors can be turned on with `CLICOLOR_FORCE`. #[test] fn clicolor_force_shows_in_stdout_and_trace() { run() .env("CLICOLOR_FORCE", "1") .args([ "mutants", "-d", "testdata/small_well_tested", "--list", "--colors=never", "-Ltrace", ]) .assert() .success() .stdout(has_color_listing()) .stderr(has_color_debug()); } cargo-mutants-25.0.0/tests/config.rs000064400000000000000000000207341046102023000154570ustar 00000000000000// Copyright 2022, 2023 Martin Pool. //! Test handling of `mutants.toml` configuration. use std::fs::{create_dir, write}; use indoc::indoc; use insta::assert_snapshot; use predicates::prelude::*; use tempfile::TempDir; mod util; use util::{copy_of_testdata, run}; fn write_config_file(tempdir: &TempDir, config: &str) { let path = tempdir.path(); // This will error if it exists, which today it never will, // but perhaps later we should ignore that. create_dir(path.join(".cargo")).unwrap(); write(path.join(".cargo/mutants.toml"), config.as_bytes()).unwrap(); } #[test] fn invalid_toml_rejected() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#"what even is this? "#, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .assert() .failure() .stderr(predicates::str::contains("Error: parse toml from ")); } #[test] fn invalid_field_rejected() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#"wobble = false "#, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .assert() .failure() .stderr( predicates::str::contains("Error: parse toml from ") .and(predicates::str::contains("unknown field `wobble`")), ); } #[test] fn list_with_config_file_exclusion() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#"exclude_globs = ["src/*_mod.rs"] "#, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .assert() .success() .stdout(predicates::str::contains("_mod.rs").not()); run() .args(["mutants", "--list", "-d"]) .arg(testdata.path()) .assert() .success() .stdout(predicates::str::contains("_mod.rs").not()); } #[test] fn list_with_config_file_inclusion() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#"examine_globs = ["src/*_mod.rs"] "#, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .assert() .success() .stdout(predicates::str::diff(indoc! { "\ src/inside_mod.rs src/item_mod.rs " })); run() .args(["mutants", "--list", "-d"]) .arg(testdata.path()) .assert() .success() .stdout(predicates::str::contains("simple_fns.rs").not()); } #[test] fn file_argument_overrides_config_examine_globs_key() { let testdata = copy_of_testdata("well_tested"); // This config key has no effect because the command line argument // takes precedence. write_config_file( &testdata, r#"examine_globs = ["src/*_mod.rs"] "#, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .args(["--file", "src/simple_fns.rs"]) .assert() .success() .stdout(predicates::str::diff(indoc! { "\ src/simple_fns.rs " })); } #[test] fn exclude_file_argument_overrides_config() { let testdata = copy_of_testdata("well_tested"); // This config key has no effect because the command line argument // takes precedence. write_config_file( &testdata, indoc! { r#" examine_globs = ["src/*_mod.rs"] exclude_globs = ["src/inside_mod.rs"] "#}, ); run() .args(["mutants", "--list-files", "-d"]) .arg(testdata.path()) .args(["--file", "src/*.rs"]) .args(["--exclude", "src/*_mod.rs"]) .args(["--exclude", "src/s*.rs"]) .args(["--exclude", "src/n*.rs"]) .args(["--exclude", "src/b*.rs"]) .assert() .success() .stdout(predicates::str::diff(indoc! { "\ src/lib.rs src/arc.rs src/empty_fns.rs src/methods.rs src/result.rs src/traits.rs " })); } #[test] fn list_with_config_file_regexps() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#" # comments are ok examine_re = ["divisible"] exclude_re = ["-> bool with true"] "#, ); let cmd = run() .args(["mutants", "--list", "--line-col=false", "-d"]) .arg(testdata.path()) .assert() .success(); assert_snapshot!( String::from_utf8_lossy(&cmd.get_output().stdout), @r###" src/simple_fns.rs: replace divisible_by_three -> bool with false src/simple_fns.rs: replace == with != in divisible_by_three src/simple_fns.rs: replace % with / in divisible_by_three src/simple_fns.rs: replace % with + in divisible_by_three "### ); } #[test] fn exclude_re_overrides_config() { let testdata = copy_of_testdata("well_tested"); write_config_file( &testdata, r#" exclude_re = [".*"] # would exclude everything "#, ); run() .args(["mutants", "--list", "-d"]) .arg(testdata.path()) .assert() .success() .stdout(predicates::str::is_empty()); // Also tests that the alias --exclude-regex is accepted let cmd = run() .args(["mutants", "--list", "--line-col=false", "-d"]) .arg(testdata.path()) .args(["--exclude-regex", " -> "]) .args(["-f", "src/simple_fns.rs"]) .assert() .success(); assert_snapshot!( String::from_utf8_lossy(&cmd.get_output().stdout), @r###" src/simple_fns.rs: replace returns_unit with () src/simple_fns.rs: replace += with -= in returns_unit src/simple_fns.rs: replace += with *= in returns_unit src/simple_fns.rs: replace == with != in divisible_by_three src/simple_fns.rs: replace % with / in divisible_by_three src/simple_fns.rs: replace % with + in divisible_by_three "###); } #[test] fn tree_fails_without_needed_feature() { // The point of this tree is to check that Cargo features can be turned on, // but let's make sure it does fail as intended if they're not. let testdata = copy_of_testdata("fails_without_feature"); run() .args(["mutants", "-d"]) .arg(testdata.path()) .assert() .failure() .stderr(predicates::str::contains( "test failed in an unmutated tree", )); } #[test] fn additional_cargo_args() { let testdata = copy_of_testdata("fails_without_feature"); write_config_file( &testdata, r#" additional_cargo_args = ["--features", "needed"] "#, ); run() .args(["mutants", "-d"]) .arg(testdata.path()) .assert() .success(); } #[test] fn additional_cargo_test_args() { let testdata = copy_of_testdata("fails_without_feature"); write_config_file( &testdata, r#" additional_cargo_test_args = ["--all-features", ] "#, ); run() .args(["mutants", "-d"]) .arg(testdata.path()) .assert() .success(); } #[test] /// Set the `--output` directory via `output` config directive. fn output_option_use_config() { let output_tmpdir = TempDir::new().unwrap(); let output_via_config = output_tmpdir.path().join("output_via_config"); let testdata = copy_of_testdata("factorial"); let out_path_str = output_via_config .to_string_lossy() .escape_default() .to_string(); write_config_file(&testdata, &format!("output = \"{out_path_str}\"")); assert!( !testdata.path().join("mutants.out").exists(), "mutants.out should not be in a clean copy of the test data" ); run() .arg("mutants") .args(["--check", "--no-times"]) .arg("-d") .arg(testdata.path()) .assert() .success(); assert!( !testdata.path().join("mutants.out").exists(), "mutants.out should not be in the source directory" ); let mutants_out = output_via_config.join("mutants.out"); assert!( mutants_out.exists(), "mutants.out is in changed `output` directory" ); for name in [ "mutants.json", "debug.log", "outcomes.json", "missed.txt", "caught.txt", "timeout.txt", "unviable.txt", ] { assert!(mutants_out.join(name).is_file(), "{name} is in mutants.out",); } } cargo-mutants-25.0.0/tests/error_value.rs000064400000000000000000000125001046102023000165270ustar 00000000000000// Copyright 2023 Martin Pool //! Tests for error value mutations, from `--error-value` etc. use std::{ env, fs::{create_dir_all, rename}, }; use indoc::indoc; use predicates::prelude::*; mod util; use tempfile::TempDir; use util::{copy_of_testdata, run}; #[test] fn error_value_catches_untested_ok_case() { // By default this tree should fail because it's configured to // generate an error value, and the tests forgot to check that // the code under test does return Ok. let tmp = copy_of_testdata("error_value"); run() .arg("mutants") .args(["-v", "-V", "--no-times", "--no-shuffle"]) .arg("-d") .arg(tmp.path()) .assert() .code(2) .stderr(""); } #[test] fn no_config_option_disables_config_file_so_error_value_is_not_generated() { // In this case, the config file is not loaded. Error values are not // generated by default (because we don't know what a good value for // this tree would be), so no mutants are caught. let tmp_src_dir = copy_of_testdata("error_value"); run() .arg("mutants") .args(["-v", "-V", "--no-times", "--no-shuffle", "--no-config"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(0) .stderr("") .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn list_mutants_with_error_value_from_command_line_list() { // This is not a good error mutant for this tree, which uses // anyhow, but it's a good test of the command line option. let tmp_src_dir = copy_of_testdata("error_value"); run() .arg("mutants") .args([ "--no-times", "--no-shuffle", "--no-config", "--list", "--error=::eyre::eyre!(\"mutant\")", ]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(0) .stderr("") .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn warn_if_error_value_starts_with_err() { // Users might misunderstand what should be passed to --error, // so give a warning. let tmp_src_dir = copy_of_testdata("error_value"); run() .arg("mutants") .args([ "--no-times", "--no-shuffle", "--no-config", "--list", "--error=Err(anyhow!(\"mutant\"))", ]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(0) .stderr(predicate::str::contains( "error_value option gives the value of the error, and probably should not start with Err(: got Err(anyhow!(\"mutant\"))" )); } #[test] fn warn_unresolved_module() { let tmp_src_dir = copy_of_testdata("dangling_mod"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "--no-config", "--list"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(0) .stderr(predicate::str::contains( r#"referent of mod not found definition_site="src/main.rs:3:1" mod_name=nonexistent"#, )); } #[test] fn warn_module_outside_of_tree() { // manually copy tree, so that external path still resolves correctly for `cargo` // // [TEMP]/dangling_mod/* // [TEMP]/nested_mod/src/paths_in_main/a/foo.rs // let tree_name = "dangling_mod"; let tmp_src_dir_parent = TempDir::with_prefix("warn_module_outside_of_tree").unwrap(); let tmp_src_dir = tmp_src_dir_parent.path().join("dangling_mod"); cp_r::CopyOptions::new() .filter(|path, _stat| { Ok(["target", "mutants.out", "mutants.out.old"] .iter() .all(|p| !path.starts_with(p))) }) .copy_tree( std::path::Path::new("testdata").join(tree_name), &tmp_src_dir, ) .unwrap(); rename( tmp_src_dir.join("Cargo_test.toml"), tmp_src_dir.join("Cargo.toml"), ) .unwrap(); let external_file_path = tmp_src_dir_parent .path() .join("nested_mod/src/paths_in_main/a"); create_dir_all(&external_file_path).unwrap(); std::fs::copy( std::path::Path::new("testdata/nested_mod/src/paths_in_main/a/foo.rs"), external_file_path.join("foo.rs"), ) .unwrap(); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "--no-config", "--list"]) .arg("-d") .arg(tmp_src_dir) .assert() .code(0) .stderr(predicate::str::contains( r#"skipping source outside of tree: "src/../../nested_mod/src/paths_in_main/a/foo.rs""#, )); } #[test] fn fail_when_error_value_does_not_parse() { let tmp_src_dir = copy_of_testdata("error_value"); run() .arg("mutants") .args([ "--no-times", "--no-shuffle", "--no-config", "--list", "--error=shouldn't work", ]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(1) .stderr(predicate::str::contains(indoc! { " Error: Failed to parse error value \"shouldn\'t work\" Caused by: unexpected token "})) .stdout(predicate::str::is_empty()); } cargo-mutants-25.0.0/tests/in_diff.rs000064400000000000000000000120401046102023000155770ustar 00000000000000// Copyright 2023 Martin Pool use std::fs::read_to_string; use std::io::Write; use indoc::indoc; use predicates::prelude::predicate; use similar::TextDiff; use tempfile::NamedTempFile; mod util; use util::{copy_of_testdata, run}; #[test] fn diff_trees_well_tested() { for name in &["diff0", "diff1"] { let tmp = copy_of_testdata(name); run() .args(["mutants", "-d"]) .arg(tmp.path()) .assert() .success(); } } #[test] fn list_mutants_changed_in_diff1() { let src0 = read_to_string("testdata/diff0/src/lib.rs").unwrap(); let src1 = read_to_string("testdata/diff1/src/lib.rs").unwrap(); let diff = TextDiff::from_lines(&src0, &src1) .unified_diff() .context_radius(2) .header("a/src/lib.rs", "b/src/lib.rs") .to_string(); println!("{diff}"); let mut diff_file = NamedTempFile::new().unwrap(); diff_file.write_all(diff.as_bytes()).unwrap(); let tmp = copy_of_testdata("diff1"); run() .args(["mutants", "--no-shuffle", "-d"]) .arg(tmp.path()) .arg("--in-diff") .arg(diff_file.path()) .assert() .success(); // Between these trees we just added one function; the existing unchanged // function does not need to be tested. assert_eq!( read_to_string(tmp.path().join("mutants.out/caught.txt")).unwrap(), indoc! { "\ src/lib.rs:6:5: replace two -> String with String::new() src/lib.rs:6:5: replace two -> String with \"xyzzy\".into() "} ); let mutants_json: serde_json::Value = serde_json::from_str(&read_to_string(tmp.path().join("mutants.out/mutants.json")).unwrap()) .unwrap(); assert_eq!( mutants_json .as_array() .expect("mutants.json contains an array") .len(), 2 ); } #[test] fn binary_diff_is_not_an_error_and_matches_nothing() { // From https://github.com/sourcefrog/cargo-mutants/issues/391 let mut diff_file = NamedTempFile::new().unwrap(); diff_file.write_all(b"Binary files a/test-renderers/expected/renderers/fog-None-wgpu.png and b/test-renderers/expected/renderers/fog-None-wgpu.png differ\n").unwrap(); let tmp = copy_of_testdata("diff1"); run() .args(["mutants", "--no-shuffle", "-d"]) .arg(tmp.path()) .arg("--in-diff") .arg(diff_file.path()) .arg("--list") .assert() .success() .stdout("") .stderr(predicate::str::contains("diff file is empty")); } #[test] fn empty_diff_is_not_an_error_and_matches_nothing() { let diff_file = NamedTempFile::new().unwrap(); let tmp = copy_of_testdata("diff1"); run() .args(["mutants", "--no-shuffle", "-d"]) .arg(tmp.path()) .arg("--in-diff") .arg(diff_file.path()) .arg("--list") .assert() .success() .stdout("") .stderr(predicate::str::contains("diff file is empty")); } /// If the text in the diff doesn't look like the tree then error out. #[test] fn mismatched_diff_causes_error() { let src0 = read_to_string("testdata/diff0/src/lib.rs").unwrap(); let src1 = read_to_string("testdata/diff1/src/lib.rs").unwrap(); let diff = TextDiff::from_lines(&src0, &src1) .unified_diff() .context_radius(2) .header("a/src/lib.rs", "b/src/lib.rs") .to_string(); let diff = diff.replace("fn", "FUNCTION"); println!("{diff}"); let mut diff_file = NamedTempFile::new().unwrap(); diff_file.write_all(diff.as_bytes()).unwrap(); let tmp = copy_of_testdata("diff1"); run() .args(["mutants", "--no-shuffle", "-d"]) .arg(tmp.path()) .arg("--in-diff") .arg(diff_file.path()) .assert() .failure() .stderr(predicates::str::contains( "Diff content doesn't match source file: src/lib.rs", )); } /// If the diff contains multiple deletions (with a new filename of /dev/null), /// don't fail. /// /// #[test] fn diff_with_multiple_deletions_is_ok() { let diff = indoc! {r#" diff --git a/src/monitor/collect.rs b/src/monitor/collect.rs deleted file mode 100644 index d842cf9..0000000 --- a/src/monitor/collect.rs +++ /dev/null @@ -1,1 +0,0 @@ -// Some stuff diff --git a/src/monitor/another.rs b/src/monitor/another.rs deleted file mode 100644 index d842cf9..0000000 --- a/src/monitor/collect.rs +++ /dev/null @@ -1,1 +0,0 @@ -// More stuff "#}; let mut diff_file = NamedTempFile::new().unwrap(); diff_file.write_all(diff.as_bytes()).unwrap(); let tmp = copy_of_testdata("diff1"); run() .args(["mutants", "--no-shuffle", "-d"]) .arg(tmp.path()) .arg("--in-diff") .arg(diff_file.path()) .assert() .stderr(predicates::str::contains( "No mutants found under the active filters", )) .success(); } cargo-mutants-25.0.0/tests/in_place.rs000064400000000000000000000025371046102023000157650ustar 00000000000000// Copyright 2024 Martin Pool //! Test `--in-place` behavior. use std::{fs::read_to_string, path::Path}; use anyhow::Result; use tempfile::TempDir; mod util; use util::{copy_of_testdata, run}; #[test] fn in_place_check_leaves_no_changes() -> Result<()> { let tmp = copy_of_testdata("small_well_tested"); let tmp_path = tmp.path(); let output_tmp = TempDir::with_prefix("in_place_check_leaves_no_changes").unwrap(); let cmd = run() .args(["mutants", "--in-place", "--check", "-d"]) .arg(tmp.path()) .arg("-o") .arg(output_tmp.path()) .assert() .success(); println!( "stdout:\n{}", String::from_utf8_lossy(&cmd.get_output().stdout) ); println!( "stderr:\n{}", String::from_utf8_lossy(&cmd.get_output().stderr) ); check_file(tmp_path, "Cargo.toml", "Cargo_test.toml")?; check_file(tmp_path, "src/lib.rs", "src/lib.rs")?; Ok(()) } fn check_file(tmp: &Path, new_name: &str, old_name: &str) -> Result<()> { let orig_path = Path::new("testdata/small_well_tested"); println!("comparing {new_name} and {old_name}"); assert_eq!( read_to_string(tmp.join(new_name))?.replace("\r\n", "\n"), read_to_string(orig_path.join(old_name))?.replace("\r\n", "\n"), "{old_name} should be the same as {new_name}" ); Ok(()) } cargo-mutants-25.0.0/tests/insta.rs000064400000000000000000000015051046102023000153230ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Test special handling of cargo-insta. mod util; use util::{copy_of_testdata, run}; /// `INSTA_UPDATE=always` in the environment will cause Insta to update /// the snaphots, so the tests will pass, so mutants will not be caught. /// This test checks that cargo-mutants sets the environment variable /// so that mutants are caught properly. #[test] fn insta_test_failures_are_detected() { for insta_update in ["auto", "always"] { println!("INSTA_UPDATE={insta_update}"); let tmp_src_dir = copy_of_testdata("insta"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "--caught", "-Ltrace"]) .env("INSTA_UPDATE", insta_update) .current_dir(tmp_src_dir.path()) .assert() .success(); } } cargo-mutants-25.0.0/tests/iterate.rs000064400000000000000000000164441046102023000156520ustar 00000000000000// Copyright 2024 Martin Pool //! Tests for `--iterate` mod util; use std::fs::{create_dir, read_to_string, write, File}; use std::io::Write; use indoc::indoc; use predicates::prelude::*; use pretty_assertions::assert_eq; use tempfile::tempdir; use self::util::run; #[test] #[allow(clippy::too_many_lines)] // long but pretty straightforward fn iterate() { let temp = tempdir().unwrap(); write( temp.path().join("Cargo.toml"), indoc! { r#" [package] name = "cargo_mutants_iterate" edition = "2021" version = "0.0.0" publish = false "# }, ) .unwrap(); create_dir(temp.path().join("src")).unwrap(); create_dir(temp.path().join("tests")).unwrap(); // First, write some untested code, and expect that the mutant is missed. write( temp.path().join("src/lib.rs"), indoc! { r#" pub fn is_two(a: usize) -> bool { a == 2 } "#}, ) .unwrap(); run() .arg("mutants") .arg("-d") .arg(temp.path()) .arg("--list") .assert() .success() .stderr(predicate::str::is_empty()) .stdout(indoc! { r#" src/lib.rs:1:35: replace is_two -> bool with true src/lib.rs:1:35: replace is_two -> bool with false src/lib.rs:1:37: replace == with != in is_two "# }); run() .arg("mutants") .arg("--no-shuffle") .arg("-d") .arg(temp.path()) .assert() .code(2); // missed mutants assert_eq!( read_to_string(temp.path().join("mutants.out/missed.txt")).unwrap(), indoc! { r#" src/lib.rs:1:35: replace is_two -> bool with true src/lib.rs:1:35: replace is_two -> bool with false src/lib.rs:1:37: replace == with != in is_two "# } ); assert_eq!( read_to_string(temp.path().join("mutants.out/caught.txt")).unwrap(), "" ); assert!(!temp .path() .join("mutants.out/previously_caught.txt") .is_file()); // Now add a test that should catch this. write( temp.path().join("tests/main.rs"), indoc! { r#" use cargo_mutants_iterate::*; #[test] fn some_test() { assert!(is_two(2)); assert!(!is_two(4)); } "#}, ) .unwrap(); run() .arg("mutants") .arg("--no-shuffle") .arg("-d") .arg(temp.path()) .assert() .code(0); // caught it assert_eq!( read_to_string(temp.path().join("mutants.out/missed.txt")).unwrap(), "" ); assert_eq!( read_to_string(temp.path().join("mutants.out/caught.txt")).unwrap(), indoc! { r#" src/lib.rs:1:35: replace is_two -> bool with true src/lib.rs:1:35: replace is_two -> bool with false src/lib.rs:1:37: replace == with != in is_two "# } ); // Now that everything's caught, run tests again and there should be nothing to test, // on both the first and second run with --iterate for _ in 0..2 { run() .arg("mutants") .args(["--list", "--iterate"]) .arg("-d") .arg(temp.path()) .assert() .success() .stdout(""); run() .arg("mutants") .args(["--no-shuffle", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .success() .stderr(predicate::str::contains( "No mutants found under the active filters", )) .stdout(predicate::str::contains("Found 0 mutants to test")); assert_eq!( read_to_string(temp.path().join("mutants.out/caught.txt")).unwrap(), "" ); assert_eq!( read_to_string(temp.path().join("mutants.out/previously_caught.txt")) .unwrap() .lines() .count(), 3 ); } // Add some more code and it should be seen as untested. let mut src = File::options() .append(true) .open(temp.path().join("src/lib.rs")) .unwrap(); src.write_all("pub fn not_two(a: usize) -> bool { !is_two(a) }\n".as_bytes()) .unwrap(); drop(src); // We should see only the new function as untested let added_mutants = indoc! { r#" src/lib.rs:2:36: replace not_two -> bool with true src/lib.rs:2:36: replace not_two -> bool with false src/lib.rs:2:36: delete ! in not_two "# }; run() .arg("mutants") .args(["--list", "--iterate"]) .arg("-d") .arg(temp.path()) .assert() .success() .stdout(added_mutants); // These are missed by a new incremental run run() .arg("mutants") .args(["--no-shuffle", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .code(2); assert_eq!( read_to_string(temp.path().join("mutants.out/missed.txt")).unwrap(), added_mutants ); // Add a new test that catches some but not all mutants File::options() .append(true) .open(temp.path().join("tests/main.rs")) .unwrap() .write_all("#[test] fn three_is_not_two() { assert!(not_two(3)); }\n".as_bytes()) .unwrap(); run() .arg("mutants") .args(["--no-shuffle", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .code(2); assert_eq!( read_to_string(temp.path().join("mutants.out/missed.txt")).unwrap(), "src/lib.rs:2:36: replace not_two -> bool with true\n" ); // There should only be one more mutant to test run() .arg("mutants") .args(["--list", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .success() .stdout("src/lib.rs:2:36: replace not_two -> bool with true\n"); // Add another test File::options() .append(true) .open(temp.path().join("tests/main.rs")) .unwrap() .write_all("#[test] fn two_is_not_not_two() { assert!(!not_two(2)); }\n".as_bytes()) .unwrap(); run() .arg("mutants") .args(["--list", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .success() .stdout("src/lib.rs:2:36: replace not_two -> bool with true\n"); run() .arg("mutants") .args(["--no-shuffle", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .success(); assert_eq!( read_to_string(temp.path().join("mutants.out/missed.txt")).unwrap(), "" ); assert_eq!( read_to_string(temp.path().join("mutants.out/caught.txt")).unwrap(), "src/lib.rs:2:36: replace not_two -> bool with true\n" ); assert_eq!( read_to_string(temp.path().join("mutants.out/previously_caught.txt")) .unwrap() .lines() .count(), 5 ); // nothing more is missed run() .arg("mutants") .args(["--list", "--iterate", "--in-place"]) .arg("-d") .arg(temp.path()) .assert() .success() .stdout(""); } cargo-mutants-25.0.0/tests/jobs.rs000064400000000000000000000027431046102023000151470ustar 00000000000000// Copyright 2022-2023 Martin Pool. //! Test handling of `--jobs` concurrency option. use std::fs::read_to_string; use itertools::Itertools; use regex::Regex; mod util; use util::{copy_of_testdata, run}; /// It's a bit hard to assess that multiple jobs really ran in parallel, /// but we can at least check that the option is accepted, and see that the /// debug log looks like it's using multiple threads. #[test] fn jobs_option_accepted_and_causes_multiple_threads() { let testdata = copy_of_testdata("small_well_tested"); run() .arg("mutants") .arg("-d") .arg(testdata.path()) .arg("-j2") .arg("--minimum-test-timeout=120") // to avoid flakes on slow CI .assert() .success(); let debug_log = read_to_string(testdata.path().join("mutants.out/debug.log")).expect("read debug log"); println!("debug log:\n{debug_log}"); // This might be brittle, as the ThreadId debug form is not specified, and // also _possibly_ everything will complete on one thread before the next // gets going, though that seems unlikely. let re = Regex::new(r#"start thread thread_id=ThreadId\(\d+\)"#).expect("compile regex"); let matches = re .find_iter(&debug_log) .map(|m| m.as_str()) .unique() .collect::>(); println!("threadid matches: {matches:?}"); assert!( matches.len() > 1, "expected more than {} thread ids in debug log", matches.len() ); } cargo-mutants-25.0.0/tests/list.rs000064400000000000000000000226701046102023000151660ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Tests for cargo-mutants `--list` and filtering of the list. use std::env; use std::fmt::Write; use predicates::prelude::*; use pretty_assertions::assert_eq; mod util; use util::{all_testdata_tree_names, copy_of_testdata, run, CommandInstaExt, OUTER_TIMEOUT}; #[test] fn list_diff_json_contains_diffs() { let tmp = copy_of_testdata("factorial"); let cmd = run() .args(["mutants", "--list", "--json", "--diff", "-d"]) .arg(tmp.path()) .assert() .success(); // needed for lifetime let out = cmd.get_output(); assert_eq!(String::from_utf8_lossy(&out.stderr), ""); println!("{}", String::from_utf8_lossy(&out.stdout)); let out_json = serde_json::from_slice::(&out.stdout).unwrap(); let mutants_json = out_json.as_array().expect("json output is array"); assert_eq!(mutants_json.len(), 5); assert!(mutants_json.iter().all(|e| e.as_object().unwrap()["diff"] .as_str() .unwrap() .contains("--- src/bin/factorial.rs"))); } #[test] fn list_mutants_in_all_trees_as_json() { // The snapshot accumulated here is actually a big text file // containing JSON fragments. This might seem a bit weird for easier // review I want just a single snapshot, and json-inside-json has quoting // that makes it harder to review. let mut buf = String::new(); for tree_name in all_testdata_tree_names() { writeln!(buf, "## testdata/{tree_name}\n").unwrap(); let tmp = copy_of_testdata(&tree_name); let cmd_assert = run() .arg("mutants") .arg("--list") .arg("--json") .current_dir(tmp.path()) .timeout(OUTER_TIMEOUT) .assert() .success(); let json_str = String::from_utf8_lossy(&cmd_assert.get_output().stdout); writeln!(buf, "```json\n{json_str}\n```\n").unwrap(); } insta::assert_snapshot!(buf); } #[test] fn list_mutants_in_all_trees_as_text() { let mut buf = String::new(); for tree_name in all_testdata_tree_names() { writeln!(buf, "## testdata/{tree_name}\n\n```").unwrap(); let tmp = copy_of_testdata(&tree_name); let cmd_assert = run() .arg("mutants") .arg("--list") .current_dir(tmp.path()) .timeout(OUTER_TIMEOUT) .assert() .success(); buf.push_str(&String::from_utf8_lossy(&cmd_assert.get_output().stdout)); buf.push_str("```\n\n"); } insta::assert_snapshot!(buf); } #[test] fn list_mutants_in_factorial() { let tmp = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--list") .current_dir(&tmp) .assert_insta("list_mutants_in_factorial"); } #[test] fn list_mutants_in_factorial_json() { let tmp = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--list") .arg("--json") .current_dir(tmp.path()) .assert_insta("list_mutants_in_factorial_json"); } #[test] fn list_mutants_in_cfg_attr_mutants_skip() { let tmp_src_dir = copy_of_testdata("cfg_attr_mutants_skip"); run() .arg("mutants") .arg("--list") .current_dir(tmp_src_dir.path()) .assert_insta("list_mutants_in_cfg_attr_mutants_skip"); } #[test] fn list_mutants_in_cfg_attr_mutants_skip_json() { let tmp_src_dir = copy_of_testdata("cfg_attr_mutants_skip"); run() .arg("mutants") .arg("--list") .arg("--json") .current_dir(tmp_src_dir.path()) .assert_insta("list_mutants_in_cfg_attr_mutants_skip_json"); } #[test] fn list_mutants_in_cfg_attr_test_skip() { let tmp_src_dir = copy_of_testdata("cfg_attr_test_skip"); run() .arg("mutants") .arg("--list") .current_dir(tmp_src_dir.path()) .assert_insta("list_mutants_in_cfg_attr_test_skip"); } #[test] fn list_mutants_in_cfg_attr_test_skip_json() { let tmp_src_dir = copy_of_testdata("cfg_attr_test_skip"); run() .arg("mutants") .arg("--list") .arg("--json") .current_dir(tmp_src_dir.path()) .assert_insta("list_mutants_in_cfg_attr_test_skip_json"); } #[test] fn list_mutants_with_dir_option() { let temp = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--list") .arg("--dir") .arg(temp.path()) .assert_insta("list_mutants_with_dir_option"); } #[test] fn list_mutants_with_diffs_in_factorial() { let tmp = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--list") .arg("--diff") .current_dir(&tmp) .assert_insta("list_mutants_with_diffs_in_factorial"); } #[test] fn list_mutants_well_tested() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .arg("--list") .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested"); } #[test] fn list_mutants_well_tested_examine_name_filter() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args(["--list", "--file", "nested_function.rs"]) .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested_examine_name_filter"); } #[test] fn list_mutants_well_tested_exclude_name_filter() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args(["--list", "--exclude", "simple_fns.rs"]) .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested_exclude_name_filter"); } #[test] fn list_mutants_well_tested_exclude_folder_filter() { let tmp = copy_of_testdata("with_child_directories"); run() .arg("mutants") .args(["--list", "--exclude", "module"]) .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested_exclude_folder_filter"); } #[test] fn list_mutants_well_tested_examine_and_exclude_name_filter_combined() { let tmp = copy_of_testdata("with_child_directories"); run() .arg("mutants") .args([ "--list", "--file", "src/module/utils/**/*.rs", "--exclude", "nested_function.rs", ]) .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested_examine_and_exclude_name_filter_combined"); } #[test] fn list_mutants_regex_filters() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args(["--list", "--re", "divisible"]) .arg("-d") .arg(tmp.path()) .assert_insta("list_mutants_regex_filters"); } #[test] fn list_mutants_regex_anchored_matches_full_line() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args([ "--list", "--re", r"^src/simple_fns.rs:\d+:\d+: replace returns_unit with \(\)$", ]) .arg("-d") .arg(tmp.path()) .assert() .success() .stdout("src/simple_fns.rs:8:5: replace returns_unit with ()\n"); } #[test] fn list_mutants_regex_filters_json() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args([ "--list", "--re", "divisible", "--exclude-re", "false", "--json", ]) .arg("-d") .arg(tmp.path()) .assert_insta("list_mutants_regex_filters_json"); } #[test] fn list_mutants_well_tested_multiple_examine_and_exclude_name_filter_with_files_and_folders() { let tmp = copy_of_testdata("with_child_directories"); run() .arg("mutants") .args(["--list", "--file", "module_methods.rs", "--file", "utils", "--exclude", "**/sub_utils/**", "--exclude", "nested_function.rs"]) .current_dir(tmp.path()) .assert_insta("list_mutants_well_tested_multiple_examine_and_exclude_name_filter_with_files_and_folders"); } #[test] fn list_mutants_json_well_tested() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .arg("--list") .arg("--json") .current_dir(tmp.path()) .assert_insta("list_mutants_json_well_tested"); } #[test] fn list_files_text_well_tested() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .arg("--list-files") .current_dir(tmp.path()) .assert_insta("list_files_text_well_tested"); } #[test] fn list_files_respects_file_filters() { // Files matching excludes *are* visited to find references to other modules, // but they're not included in --list-files. let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .args(["--list-files", "--exclude", "lib.rs"]) .current_dir(tmp.path()) .assert() .success() .stdout(predicate::str::contains("methods.rs")) .stdout(predicate::str::contains("lib.rs").not()); } #[test] fn list_files_json_well_tested() { let tmp = copy_of_testdata("well_tested"); run() .arg("mutants") .arg("--list-files") .arg("--json") .current_dir(tmp.path()) .assert_insta("list_files_json_well_tested"); } #[test] fn no_mutants_in_tree_everything_skipped() { let tmp_src_dir = copy_of_testdata("everything_skipped"); run() .args(["mutants", "--list"]) .arg("--dir") .arg(tmp_src_dir.path()) .assert() .stderr(predicate::str::is_empty()) // not an error or warning .stdout(predicate::str::is_empty()) .success(); } cargo-mutants-25.0.0/tests/main.rs000064400000000000000000001007741046102023000151410ustar 00000000000000// Copyright 2021-2024 Martin Pool //! Tests for cargo-mutants CLI layer. use std::collections::HashSet; use std::env; use std::fs::{self, create_dir, read_dir, read_to_string}; use std::path::Path; use indoc::indoc; use itertools::Itertools; use predicate::str::{contains, is_match}; use predicates::prelude::*; use pretty_assertions::assert_eq; use tempfile::TempDir; mod util; use util::{copy_of_testdata, copy_testdata_to, run, OUTER_TIMEOUT}; #[test] fn incorrect_cargo_subcommand() { // argv[1] "mutants" is missing here. run().arg("wibble").assert().code(1); } #[test] fn missing_cargo_subcommand() { // argv[1] "mutants" is missing here. run().assert().code(1); } #[test] fn option_in_place_of_cargo_subcommand() { // argv[1] "mutants" is missing here. run().args(["--list"]).assert().code(1); } #[test] fn show_version() { run() .args(["mutants", "--version"]) .assert() .success() .stdout(predicates::str::is_match(r"^cargo-mutants \d+\.\d+\.\d+(-.*)?\n$").unwrap()); } #[test] fn uses_cargo_env_var_to_run_cargo_so_invalid_value_fails() { let tmp_src_dir = copy_of_testdata("well_tested"); let bogus_cargo = "NOTHING_NONEXISTENT_VOID"; run() .env("CARGO", bogus_cargo) .args(["mutants", "-d"]) .arg(tmp_src_dir.path()) .assert() .stderr( predicates::str::contains("No such file or directory") .or(predicates::str::contains( "The system cannot find the file specified", )) .or( predicates::str::contains("program not found"), /* Windows */ ), ) .code(1); // TODO: Preferably there would be a more specific exit code for the // clean build failing. } #[test] fn tree_with_child_directories_is_well_tested() { let tmp_src_dir = copy_of_testdata("with_child_directories"); run() .arg("mutants") .arg("-d") .arg(tmp_src_dir.path()) .arg("-Ldebug") .assert() .success() .stderr( predicate::str::is_match( r#"DEBUG Copied source tree total_bytes=\d{3,} total_files=1[34]"#, ) .unwrap(), ); // The outcomes all have `diff_path` keys and they all identify files. let outcomes_json = read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")).unwrap(); let json: serde_json::Value = serde_json::from_str(&outcomes_json).unwrap(); let mut all_diffs = HashSet::new(); for outcome_json in json["outcomes"].as_array().unwrap() { dbg!(&outcome_json); if outcome_json["scenario"].as_str() == Some("Baseline") { assert!( outcome_json .get("diff_path") .expect("has a diff_path") .is_null(), "diff_path should be null" ); } else { let diff_path = outcome_json["diff_path"].as_str().unwrap(); let full_diff_path = tmp_src_dir.path().join("mutants.out").join(diff_path); assert!(full_diff_path.is_file(), "{diff_path:?} is not a file"); assert!(all_diffs.insert(diff_path)); let diff_content = read_to_string(&full_diff_path).expect("read diff file"); assert!( diff_content.starts_with("--- src/"), "diff content in {full_diff_path:?} doesn't look right:\n{diff_content}" ); } } } #[test] fn copy_testdata_doesnt_include_build_artifacts() { // If there is a target or mutants.out in the source directory, we don't want it in the copy, // so that the tests are (more) hermetic. let tmp_src_dir = copy_of_testdata("factorial"); assert!(!tmp_src_dir.path().join("mutants.out").exists()); assert!(!tmp_src_dir.path().join("target").exists()); assert!(!tmp_src_dir.path().join("mutants.out.old").exists()); assert!(tmp_src_dir.path().join("Cargo.toml").exists()); } #[test] fn small_well_tested_tree_is_clean() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "-v", "-V"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); // The log file should exist and include something that looks like a diff. let log_content = fs::read_to_string( tmp_src_dir .path() .join("mutants.out/log/src__lib.rs_line_5_col_5.log"), ) .unwrap() .replace('\r', ""); println!("log content:\n{log_content}"); assert!(log_content.contains("*** mutation diff")); assert!(log_content.contains(indoc! { r#" *** mutation diff: --- src/lib.rs +++ replace factorial -> u32 with 0 @@ -1,17 +1,13 @@ "# })); assert!(log_content.contains(indoc! { r#" pub fn factorial(n: u32) -> u32 { - let mut a = 1; - for i in 2..=n { - a *= i; - } - a + 0 /* ~ changed by cargo-mutants ~ */ } "# })); // Also, it should contain output from the failed tests with mutations applied. assert!(log_content.contains("test test::test_factorial ... FAILED")); assert!(log_content.contains("---- test::test_factorial stdout ----")); assert!(log_content.contains("factorial(6) = 0")); } #[test] fn test_small_well_tested_tree_with_baseline_skip() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "-v", "-V", "--baseline=skip"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })) .stderr( predicate::str::contains( "An explicit test timeout is recommended when using --baseline=skip", ) .and(predicate::str::contains("Unmutated baseline in").not()), ); assert!(!tmp_src_dir .path() .join("mutants.out/log/baseline.log") .exists()); } #[test] fn cdylib_tree_is_well_tested() { let tmp_src_dir = copy_of_testdata("cdylib"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "-v", "-V"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn proc_macro_tree_is_well_tested() { let tmp_src_dir = copy_of_testdata("proc_macro"); run() .arg("mutants") .args(["--no-times", "--no-shuffle", "-v", "-V"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::str::contains( "2 mutants tested: 1 caught, 1 unviable", )); } #[test] fn well_tested_tree_finds_no_problems() { let tmp_src_dir = copy_of_testdata("well_tested"); run() .arg("mutants") .args(["--no-times", "--caught", "--unviable", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); assert!(tmp_src_dir .path() .join("mutants.out/outcomes.json") .exists()); let outcomes_json = fs::read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")).unwrap(); let outcomes: serde_json::Value = outcomes_json.parse().unwrap(); let caught = outcomes["caught"] .as_i64() .expect("outcomes['caught'] is an integer"); assert!(caught > 40, "expected more outcomes caught than {caught}"); assert_eq!(outcomes["unviable"], 0); assert_eq!(outcomes["missed"], 0); assert_eq!(outcomes["timeout"], 0); assert_eq!(outcomes["total_mutants"], outcomes["caught"]); check_text_list_output(tmp_src_dir.path(), "well_tested_tree_finds_no_problems"); } #[test] fn well_tested_tree_check_only() { let tmp_src_dir = copy_of_testdata("well_tested"); run() .args(["mutants", "--check", "--no-shuffle", "--no-times"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn well_tested_tree_check_only_shuffled() { let tmp_src_dir = copy_of_testdata("well_tested"); run() .args(["mutants", "--check", "--no-times", "--shuffle"]) .current_dir(tmp_src_dir.path()) .assert() .success(); // Caution: No assertions about output here, we just check that it runs. } #[test] fn unviable_mutation_of_struct_with_no_default() { let tmp_src_dir = copy_of_testdata("struct_with_no_default"); run() .args([ "mutants", "--line-col=false", "--no-times", "--no-shuffle", "-v", "-V", ]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success() .stdout( predicate::str::is_match( r"unviable *src/lib.rs:\d+:\d+: replace make_an_s -> S with Default::default\(\)", ) .unwrap(), ); check_text_list_output( tmp_src_dir.path(), "unviable_mutation_of_struct_with_no_default", ); } #[test] fn integration_test_source_is_not_mutated() { let tmp_src_dir = copy_of_testdata("integration_tests"); run() .args(["mutants", "--no-times", "--no-shuffle", "--list-files"]) .current_dir(tmp_src_dir.path()) .assert() .success() .stdout("src/lib.rs\n"); run() .args(["mutants", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .assert() .success(); check_text_list_output(tmp_src_dir.path(), "integration_test_source_is_not_mutated"); } #[test] fn uncaught_mutant_in_factorial() { let tmp_src_dir = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--no-shuffle") .arg("--no-times") .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(2) .stderr("") .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); // Some log files should have been created let log_dir = tmp_src_dir.path().join("mutants.out/log"); assert!(log_dir.is_dir()); let mut names = fs::read_dir(log_dir) .unwrap() .map(Result::unwrap) .map(|e| e.file_name().into_string().unwrap()) .collect_vec(); names.sort_unstable(); insta::assert_debug_snapshot!("factorial__log_names", &names); // A mutants.json is in the mutants.out directory. let mutants_json = fs::read_to_string(tmp_src_dir.path().join("mutants.out/mutants.json")).unwrap(); insta::assert_snapshot!("mutants.json", mutants_json); check_text_list_output(tmp_src_dir.path(), "uncaught_mutant_in_factorial"); } #[test] fn factorial_mutants_with_all_logs() { // The log contains a lot of build output, which is hard to deal with, but let's check that // some key lines are there. let tmp_src_dir = copy_of_testdata("factorial"); run() .arg("mutants") .arg("--all-logs") .arg("-v") .arg("-V") .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(2) .stderr(predicate::str::contains("WARN").or(predicate::str::contains("ERR")).not()) .stdout(is_match( r"ok *Unmutated baseline in \d+\.\ds" ).unwrap()) .stdout(is_match( r"MISSED *src/bin/factorial\.rs:\d+:\d+: replace main with \(\) in \d+\.\ds" ).unwrap()) .stdout(is_match( r"caught *src/bin/factorial\.rs:\d+:\d+: replace factorial -> u32 with 0 in \d+\.\ds" ).unwrap()); } #[test] fn factorial_mutants_with_all_logs_and_nocapture() { let tmp_src_dir = copy_of_testdata("factorial"); run() .arg("mutants") .args(["--all-logs", "-v", "-V"]) .arg("-d") .arg(tmp_src_dir.path()) .args(["--", "--", "--nocapture"]) .assert() .code(2) .stdout(contains("factorial(6) = 720")) // println from the test .stdout(contains("factorial(6) = 0")) // The mutated result ; } #[test] fn factorial_mutants_no_copy_target() { let tmp_src_dir = copy_of_testdata("factorial"); run() .arg("mutants") .args(["--no-times"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(2) .stderr("") .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn small_well_tested_mutants_with_cargo_arg_release() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .arg("mutants") .args(["--no-times", "--cargo-arg", "--release"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success() .stderr("") .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); // Check that it was actually passed. let baseline_log_path = &tmp_src_dir.path().join("mutants.out/log/baseline.log"); println!("{}", baseline_log_path.display()); let log_content = fs::read_to_string(baseline_log_path).unwrap(); println!("{log_content}"); regex::Regex::new(r"cargo.* test --no-run --verbose .* --release") .unwrap() .captures(&log_content) .unwrap(); regex::Regex::new(r"cargo.* test --verbose .* --release") .unwrap() .captures(&log_content) .unwrap(); } #[test] /// The `--output` directory creates the named directory if necessary, and then /// creates `mutants.out` within it. `mutants.out` is not created in the /// source directory in this case. fn output_option() { let tmp_src_dir = copy_of_testdata("factorial"); let output_tmpdir = TempDir::new().unwrap(); let output_parent = output_tmpdir.path().join("output_parent"); assert!( !tmp_src_dir.path().join("mutants.out").exists(), "mutants.out should not be in a clean copy of the test data" ); run() .arg("mutants") .arg("--output") .arg(&output_parent) .args(["--check", "--no-times"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success(); assert!( !tmp_src_dir.path().join("mutants.out").exists(), "mutants.out should not be in the source directory after --output was given" ); let mutants_out = output_parent.join("mutants.out"); assert!(mutants_out.exists(), "mutants.out is in --output directory"); for name in [ "mutants.json", "debug.log", "outcomes.json", "missed.txt", "caught.txt", "timeout.txt", "unviable.txt", ] { assert!(mutants_out.join(name).is_file(), "{name} is in mutants.out",); } } #[test] /// Set the `--output` directory via environment variable `CARGO_MUTANTS_OUTPUT` fn output_option_use_env() { let tmp_src_dir = copy_of_testdata("factorial"); let output_tmpdir = TempDir::new().unwrap(); let output_via_env = output_tmpdir.path().join("output_via_env"); assert!( !tmp_src_dir.path().join("mutants.out").exists(), "mutants.out should not be in a clean copy of the test data" ); run() .env("CARGO_MUTANTS_OUTPUT", &output_via_env) .arg("mutants") .args(["--check", "--no-times"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .success(); assert!( !tmp_src_dir.path().join("mutants.out").exists(), "mutants.out should not be in the source directory" ); let mutants_out = output_via_env.join("mutants.out"); assert!( mutants_out.exists(), "mutants.out is in $CARGO_MUTANTS_OUTPUT directory" ); for name in [ "mutants.json", "debug.log", "outcomes.json", "missed.txt", "caught.txt", "timeout.txt", "unviable.txt", ] { assert!(mutants_out.join(name).is_file(), "{name} is in mutants.out",); } } #[test] fn check_succeeds_in_tree_that_builds_but_fails_tests() { // --check doesn't actually run the tests so won't discover that they fail. let tmp_src_dir = copy_of_testdata("already_failing_tests"); run() .args(["mutants", "--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn check_tree_with_mutants_skip() { let tmp_src_dir = copy_of_testdata("hang_avoided_by_attr"); run() .arg("mutants") .args(["--check", "--no-times", "--no-shuffle"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn already_failing_tests_are_detected_before_running_mutants() { let tmp_src_dir = copy_of_testdata("already_failing_tests"); run() .arg("mutants") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .code(4) .stdout( predicate::str::contains("running 1 test\ntest test_factorial ... FAILED") .normalize() .and(predicate::str::contains("thread 'test_factorial' panicked")) .and(predicate::str::contains("72")) // the failing value should be in the output .and(predicate::str::contains("lib.rs:11:5")) .and( predicate::str::contains("test result: FAILED. 0 passed; 1 failed;") .normalize(), ), ) .stderr(predicate::str::contains( "cargo test failed in an unmutated tree, so no mutants were tested", )); } #[test] fn already_failing_doctests_are_detected() { let tmp_src_dir = copy_of_testdata("already_failing_doctests"); run() .arg("mutants") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .code(4) // CLEAN_TESTS_FAILED .stdout(contains("lib.rs - takes_one_arg (line 5) ... FAILED")) .stdout(contains( "this function takes 1 argument but 3 arguments were supplied", )) .stderr(predicate::str::contains( "cargo test failed in an unmutated tree, so no mutants were tested", )); } #[test] fn already_failing_doctests_can_be_skipped_with_cargo_arg() { let tmp_src_dir = copy_of_testdata("already_failing_doctests"); run() .arg("mutants") .args(["--", "--all-targets"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .code(0); } #[test] fn source_tree_parse_fails() { let tmp_src_dir = copy_of_testdata("parse_fails"); run() .arg("mutants") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .failure() // TODO: This should be a distinct error code .stderr(contains("Error: failed to parse src/lib.rs")); } #[test] fn source_tree_typecheck_fails() { let tmp_src_dir = copy_of_testdata("typecheck_fails"); run() .arg("mutants") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .failure() // TODO: This should be a distinct error code .stdout(is_match(r"FAILED *Unmutated baseline in \d+\.\ds").unwrap()) .stdout( contains(r#""1" + 2 // Doesn't work in Rust: just as well!"#) .name("The problem source line"), ) .stdout(contains("*** baseline")) .stdout(contains("test --no-run")) .stdout(contains("lib.rs:6")) .stdout(contains("*** result: ")) .stderr(contains( "build failed in an unmutated tree, so no mutants were tested", )); } /// `CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT` overrides the detected minimum timeout. #[test] fn minimum_test_timeout_from_env() { let tmp_src_dir = copy_of_testdata("small_well_tested"); run() .arg("mutants") .env("CARGO_MUTANTS_MINIMUM_TEST_TIMEOUT", "1234") .current_dir(tmp_src_dir.path()) .timeout(OUTER_TIMEOUT) .assert() .success() .stderr(predicate::str::contains("Auto-set test timeout to 20m 34s")); } /// In this tree, as the name suggests, tests will hang in a clean tree. /// /// cargo-mutants should notice this when doing baseline tests and return a clean result. /// /// We run the cargo-mutants subprocess with an enclosing timeout, so that the outer test will /// fail rather than hang if cargo-mutants own timeout doesn't work as intended. /// /// All these timeouts are a little brittle if the test machine is very slow. #[test] fn timeout_when_unmutated_tree_test_hangs() { let tmp_src_dir = copy_of_testdata("already_hangs"); run() .arg("mutants") .args(["--timeout", "2.9"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .timeout(OUTER_TIMEOUT) .assert() .code(4) // exit_code::CLEAN_TESTS_FAILED .stdout(is_match(r"TIMEOUT *Unmutated baseline in \d+\.\ds").unwrap()) .stderr(contains( "cargo test failed in an unmutated tree, so no mutants were tested", )); } /// A tree that hangs when some functions are mutated does not hang cargo-mutants /// overall, because we impose a timeout. The timeout can be specified on the /// command line, with decimal seconds. /// /// This test is a bit at risk of being flaky, because it depends on the progress /// of real time and tests can be unexpectedly slow on CI. /// /// The `hang_when_mutated` tree generates three mutants: /// /// * `controlled_loop` could be replaced to return 0 and this will be /// detected, because it should normally return at least one. /// /// * `should_stop` could change to always return `true`, in which case /// the test will fail and the mutant will be caught because the loop /// does only one pass. /// /// * `should_stop` could change to always return `false`, in which case /// the loop will never stop, but the test should eventually be killed /// by a timeout. /// /// * `should_stop_const` could change to always return `false`, in which /// case the loop in the block for the const `VAL` will never stop, but /// the build should eventually be killed by a timeout. #[test] fn mutants_causing_tests_to_hang_are_stopped_by_manual_timeout() { let tmp_src_dir = copy_of_testdata("hang_when_mutated"); // Also test that it accepts decimal seconds let out = run() .arg("mutants") .args(["-t", "8.1", "--build-timeout=15.5"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .timeout(OUTER_TIMEOUT) .assert() .code(3); // exit_code::TIMEOUT println!( "output:\n{}", String::from_utf8_lossy(&out.get_output().stdout) ); let unviable_txt = read_to_string(tmp_src_dir.path().join("mutants.out/unviable.txt")) .expect("read timeout.txt"); let caught_txt = read_to_string(tmp_src_dir.path().join("mutants.out/caught.txt")) .expect("read timeout.txt"); let timeout_txt = read_to_string(tmp_src_dir.path().join("mutants.out/timeout.txt")) .expect("read timeout.txt"); assert!( timeout_txt.contains("replace should_stop -> bool with false"), "expected text not found in:\n{timeout_txt}" ); assert!( unviable_txt.contains("replace should_stop_const -> bool with false"), "expected text not found in:\n{unviable_txt}" ); assert!( caught_txt.contains("replace should_stop -> bool with true"), "expected text not found in:\n{caught_txt}" ); assert!( caught_txt.contains("replace controlled_loop -> usize with 0"), "expected text not found in:\n{caught_txt}" ); let outcomes_json: serde_json::Value = read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")) .expect("read outcomes.json") .parse() .expect("parse outcomes.json"); assert_eq!(outcomes_json["timeout"], 1); let phases_for_const_fn = outcomes_json["outcomes"] .as_array() .unwrap() .iter() .filter(|outcome| { outcome["scenario"]["Mutant"]["function"]["function_name"] == "should_stop_const" }) .flat_map(|outcome| outcome["phase_results"].as_array()) .next() .expect("Failed to find phase_results for 'should_stop_const' fn"); assert_eq!(phases_for_const_fn.len(), 1); assert_eq!(phases_for_const_fn[0]["phase"], "Build"); } #[test] fn hang_avoided_by_build_timeout_with_cap_lints() { let tmp_src_dir = copy_of_testdata("hang_when_mutated"); let out = run() .arg("mutants") .args(["--build-timeout=10", "--regex=const", "--cap-lints=true"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .timeout(OUTER_TIMEOUT) .assert(); println!( "debug log:\n===\n{}\n===", read_to_string(tmp_src_dir.path().join("mutants.out/debug.log")).unwrap_or_default() ); out.code(3); // exit_code::TIMEOUT let timeout_txt = read_to_string(tmp_src_dir.path().join("mutants.out/timeout.txt")) .expect("read timeout.txt"); assert!( timeout_txt.contains("replace should_stop_const -> bool with false"), "expected text not found in:\n{timeout_txt}" ); } #[test] fn constfn_mutation_passes_check() { let tmp_src_dir = copy_of_testdata("hang_when_mutated"); let cmd = run() .arg("mutants") .args(["--check", "--build-timeout=10"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .timeout(OUTER_TIMEOUT) .assert() .code(0); println!("{}", String::from_utf8_lossy(&cmd.get_output().stdout)); } #[test] fn log_file_names_are_short_and_dont_collide() { // The "well_tested" tree can generate multiple mutants from single lines. They get distinct file names. let tmp_src_dir = copy_of_testdata("well_tested"); let cmd_assert = run() .arg("mutants") .args(["--check", "-v", "-V"]) .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success(); println!( "{}", String::from_utf8_lossy(&cmd_assert.get_output().stdout) ); let log_dir = tmp_src_dir.path().join("mutants.out").join("log"); let all_log_names = read_dir(log_dir) .unwrap() .map(|e| e.unwrap().file_name().to_str().unwrap().to_string()) .inspect(|filename| println!("{filename}")) .collect::>(); assert!(all_log_names.len() > 10); assert!( all_log_names.iter().all(|filename| filename.len() < 80), "log file names are too long" ); assert!( all_log_names .iter() .any(|filename| filename.ends_with("_001.log")), "log file names are not disambiguated" ); } fn setup_relative_dependency(tree_name: &str) -> TempDir { let tmp = TempDir::new().unwrap(); let tmp_path = tmp.path(); let tmp_testdata = tmp_path.join("testdata"); create_dir(&tmp_testdata).unwrap(); copy_testdata_to(tree_name, tmp_testdata.join(tree_name)); // Make a tiny version of the 'mutants' crate so that it can be imported by a relative // dependency or otherwise. // // This is a bit annoying because // - the dependency must be published to crates.io to be a dependency that's overridden // - but, we have a copy of it in this tree so we can override the dependency, without // needing to download it during the tests copy_testdata_to("mutants_attrs", tmp_path.join("mutants_attrs")); tmp } #[test] fn cargo_mutants_in_override_dependency_tree_passes() { let tree_name = "override_dependency"; let tmp = setup_relative_dependency(tree_name); run() .arg("mutants") .arg("--no-times") .arg("--no-shuffle") .arg("-d") .arg(tmp.path().join("testdata").join(tree_name)) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn cargo_mutants_in_relative_dependency_tree_passes() { let tree_name = "relative_dependency"; let tmp = setup_relative_dependency(tree_name); copy_testdata_to("dependency", tmp.path().join("testdata").join("dependency")); run() .arg("mutants") .arg("--no-times") .arg("--no-shuffle") .arg("-d") .arg(tmp.path().join("testdata").join(tree_name)) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn cargo_mutants_in_replace_dependency_tree_passes() { let tree_name = "replace_dependency"; let tmp = setup_relative_dependency(tree_name); run() .arg("mutants") .arg("--no-times") .arg("--no-shuffle") .arg("-d") .arg(tmp.path().join("testdata").join(tree_name)) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn cargo_mutants_in_patch_dependency_tree_passes() { let tmp = setup_relative_dependency("patch_dependency"); run() .arg("mutants") .arg("--no-times") .arg("--no-shuffle") .arg("-d") .arg(tmp.path().join("testdata").join("patch_dependency")) .assert() .success() .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } /// This test would fail if mutants aren't correctly removed from the tree after /// testing, which would cause all later mutants to be incorrectly marked as /// caught. /// /// This was suggested by `Mutant::unapply` being marked as missed. #[test] fn mutants_are_unapplied_after_testing_so_later_missed_mutants_are_found() { // This needs --no-shuffle because failure to unapply will show up when the // uncaught mutant is not the first file tested. let tmp_src_dir = copy_of_testdata("unapply"); run() .args(["mutants", "--no-times", "--no-shuffle"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .code(2) // some were missed .stdout(predicate::function(|stdout: &str| { insta::assert_snapshot!(stdout); true })); } #[test] fn strict_warnings_about_unused_variables_are_disabled_so_mutants_compile() { let tmp_src_dir = copy_of_testdata("strict_warnings"); run() .arg("mutants") .arg("--check") .arg("--no-times") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success(); run() .arg("mutants") .arg("--no-times") .current_dir(tmp_src_dir.path()) .env_remove("RUST_BACKTRACE") .assert() .success(); } fn check_text_list_output(dir: &Path, test_name: &str) { // There is a `missed.txt` file with the right content, etc. for name in ["missed", "caught", "timeout", "unviable"] { let path = dir.join(format!("mutants.out/{name}.txt")); let content = fs::read_to_string(&path).unwrap(); insta::assert_snapshot!(format!("{test_name}__{name}.txt"), content); } } /// `cargo mutants --completions SHELL` produces a shell script for some /// well-known shells. /// /// We won't check the content but let's just make sure that it succeeds /// and produces some non-empty output. #[test] fn completions_option_generates_something() { for shell in ["bash", "fish", "zsh", "powershell"] { println!("completions for {shell}"); run() .arg("mutants") .arg("--completions") .arg(shell) .assert() .success() .stdout(predicate::str::is_empty().not()); } } cargo-mutants-25.0.0/tests/nextest.rs000064400000000000000000000015161046102023000157010ustar 00000000000000// Copyright 2024 Martin Pool //! Integration tests for cargo mutants calling nextest. use predicates::prelude::*; mod util; use util::{copy_of_testdata, run}; #[test] fn test_with_nextest_on_small_tree() { let tmp_src_dir = copy_of_testdata("small_well_tested"); let assert = run() .args(["mutants", "--test-tool", "nextest", "-vV", "--no-shuffle"]) .arg("-d") .arg(tmp_src_dir.path()) .assert() .stderr(predicates::str::contains("WARN").not()) .stdout( predicates::str::contains("4 mutants tested") .and(predicates::str::contains("Found 4 mutants to test")) .and(predicates::str::contains("4 caught")), ) .success(); println!( "stdout:\n{}", String::from_utf8_lossy(&assert.get_output().stdout) ); } cargo-mutants-25.0.0/tests/shard.rs000064400000000000000000000045721046102023000153150ustar 00000000000000// Copyright 2023-2024 Martin Pool //! Test `--shard` use itertools::Itertools; mod util; use util::{copy_of_testdata, run}; #[test] fn shard_divides_all_mutants() { // For speed, this only lists the mutants, trusting that the mutants // that are listed are the ones that are run. let tmp = copy_of_testdata("well_tested"); let common_args = ["mutants", "--list", "-d", tmp.path().to_str().unwrap()]; let full_list = String::from_utf8( run() .args(common_args) .assert() .success() .get_output() .stdout .clone(), ) .unwrap() .lines() .map(ToOwned::to_owned) .collect_vec(); let n_shards = 5; let shard_lists = (0..n_shards) .map(|k| { String::from_utf8( run() .args(common_args) .args(["--shard", &format!("{}/{}", k, n_shards)]) .assert() .success() .get_output() .stdout .clone(), ) .unwrap() .lines() .map(ToOwned::to_owned) .collect_vec() }) .collect_vec(); // If you combine all the mutants selected for each shard, you get the // full list, with nothing lost or duplicated, disregarding order. // // If we had a bug where we shuffled before sharding, then the shards would // see inconsistent lists and this test would fail in at least some attempts. assert_eq!( shard_lists.iter().flatten().sorted().collect_vec(), full_list.iter().sorted().collect_vec() ); // The shards should also be approximately the same size. let shard_lens = shard_lists.iter().map(|l| l.len()).collect_vec(); assert!(shard_lens.iter().max().unwrap() - shard_lens.iter().min().unwrap() <= 1); // And the shards are disjoint for i in 0..n_shards { for j in 0..n_shards { if i != j { assert!( shard_lists[i].iter().all(|m| !shard_lists[j].contains(m)), "shard {} contains {}", j, shard_lists[j] .iter() .filter(|m| shard_lists[i].contains(m)) .join(", ") ); } } } } ././@LongLink00006440000000000000000000000154000000000000007773Lustar cargo-mutants-25.0.0/tests/snapshots/error_value__list_mutants_with_error_value_from_command_line_list.snapcargo-mutants-25.0.0/tests/snapshots/error_value__list_mutants_with_error_value_from_command_line_li000064400000000000000000000005431046102023000327530ustar 00000000000000--- source: tests/error_value.rs expression: stdout --- src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(0) src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(1) src/lib.rs:4:5: replace zero_is_ok -> Result with Err(::eyre::eyre!("mutant")) src/lib.rs:4:10: replace == with != in zero_is_ok ././@LongLink00006440000000000000000000000175000000000000007776Lustar cargo-mutants-25.0.0/tests/snapshots/error_value__no_config_option_disables_config_file_so_error_value_is_not_generated.snapcargo-mutants-25.0.0/tests/snapshots/error_value__no_config_option_disables_config_file_so_error_val000064400000000000000000000005531046102023000326510ustar 00000000000000--- source: tests/error_value.rs expression: stdout --- Found 3 mutants to test ok Unmutated baseline caught src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(0) caught src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(1) caught src/lib.rs:4:10: replace == with != in zero_is_ok 3 mutants tested: 3 caught cargo-mutants-25.0.0/tests/snapshots/list__list_mutants_in_all_trees_as_json.snap000064400000000000000000005373001046102023000267270ustar 00000000000000--- source: tests/list.rs expression: buf --- ## testdata/already_failing_doctests ```json [ { "file": "src/lib.rs", "function": { "function_name": "takes_one_arg", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "mutants-testdata-already-failing-doctests", "replacement": "0", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/lib.rs", "function": { "function_name": "takes_one_arg", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "mutants-testdata-already-failing-doctests", "replacement": "1", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/lib.rs", "function": { "function_name": "takes_one_arg", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-already-failing-doctests", "replacement": "-", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } }, { "file": "src/lib.rs", "function": { "function_name": "takes_one_arg", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-already-failing-doctests", "replacement": "*", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } } ] ``` ## testdata/already_failing_tests ```json [ { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "mutants-testdata-already-failing-tests", "replacement": "0", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "mutants-testdata-already-failing-tests", "replacement": "1", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-already-failing-tests", "replacement": "+=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-already-failing-tests", "replacement": "/=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } } ] ``` ## testdata/already_hangs ```json [ { "file": "src/lib.rs", "function": { "function_name": "infinite_loop", "return_type": "", "span": { "end": { "column": 2, "line": 16 }, "start": { "column": 1, "line": 8 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-already-hangs", "replacement": "()", "span": { "end": { "column": 6, "line": 15 }, "start": { "column": 5, "line": 12 } } } ] ``` ## testdata/cdylib ```json [ { "file": "src/entry.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cdylib", "replacement": "0", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/entry.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cdylib", "replacement": "1", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/entry.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cdylib", "replacement": "+=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } }, { "file": "src/entry.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cdylib", "replacement": "/=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } } ] ``` ## testdata/cfg_attr_mutants_skip ```json [] ``` ## testdata/cfg_attr_test_skip ```json [ { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "0", "span": { "end": { "column": 10, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "1", "span": { "end": { "column": 10, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "+", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "/", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } } ] ``` ## testdata/cfg_test_inner ```json [] ``` ## testdata/cross_package_tests ```json [ { "file": "lib/src/lib.rs", "function": { "function_name": "add", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cross-package-tests-lib", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "lib/src/lib.rs", "function": { "function_name": "add", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cross-package-tests-lib", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "lib/src/lib.rs", "function": { "function_name": "add", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cross-package-tests-lib", "replacement": "-", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "lib/src/lib.rs", "function": { "function_name": "add", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cross-package-tests-lib", "replacement": "*", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } } ] ``` ## testdata/custom_top_file ```json [ { "file": "src/custom_top.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-custom-top-file", "replacement": "true", "span": { "end": { "column": 15, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/custom_top.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-custom-top-file", "replacement": "false", "span": { "end": { "column": 15, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/custom_top.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-custom-top-file", "replacement": "!=", "span": { "end": { "column": 13, "line": 2 }, "start": { "column": 11, "line": 2 } } }, { "file": "src/custom_top.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-custom-top-file", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/custom_top.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-custom-top-file", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } } ] ``` ## testdata/dangling_mod ```json [ { "file": "src/main.rs", "function": { "function_name": "verify_continue::always_true", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-dangling-mod", "replacement": "false", "span": { "end": { "column": 13, "line": 13 }, "start": { "column": 9, "line": 13 } } } ] ``` ## testdata/dependency ```json [ { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-dependency", "replacement": "0", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-dependency", "replacement": "1", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-dependency", "replacement": "+=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-dependency", "replacement": "/=", "span": { "end": { "column": 13, "line": 4 }, "start": { "column": 11, "line": 4 } } } ] ``` ## testdata/diff0 ```json [ { "file": "src/lib.rs", "function": { "function_name": "one", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "diff0", "replacement": "String::new()", "span": { "end": { "column": 21, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "one", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "diff0", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 21, "line": 2 }, "start": { "column": 5, "line": 2 } } } ] ``` ## testdata/diff1 ```json [ { "file": "src/lib.rs", "function": { "function_name": "one", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "diff1", "replacement": "String::new()", "span": { "end": { "column": 21, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "one", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "diff1", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 21, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "two", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "diff1", "replacement": "String::new()", "span": { "end": { "column": 21, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "two", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "diff1", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 21, "line": 6 }, "start": { "column": 5, "line": 6 } } } ] ``` ## testdata/error_value ```json [ { "file": "src/lib.rs", "function": { "function_name": "zero_is_ok", "return_type": "-> Result", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-error-value", "replacement": "Ok(0)", "span": { "end": { "column": 6, "line": 8 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "zero_is_ok", "return_type": "-> Result", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-error-value", "replacement": "Ok(1)", "span": { "end": { "column": 6, "line": 8 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "zero_is_ok", "return_type": "-> Result", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-error-value", "replacement": "Err(\"injected\")", "span": { "end": { "column": 6, "line": 8 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "zero_is_ok", "return_type": "-> Result", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-error-value", "replacement": "!=", "span": { "end": { "column": 12, "line": 4 }, "start": { "column": 10, "line": 4 } } } ] ``` ## testdata/everything_skipped ```json [] ``` ## testdata/factorial ```json [ { "file": "src/bin/factorial.rs", "function": { "function_name": "main", "return_type": "", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "()", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "0", "span": { "end": { "column": 6, "line": 12 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "1", "span": { "end": { "column": 6, "line": 12 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-factorial", "replacement": "+=", "span": { "end": { "column": 13, "line": 10 }, "start": { "column": 11, "line": 10 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-factorial", "replacement": "/=", "span": { "end": { "column": 13, "line": 10 }, "start": { "column": 11, "line": 10 } } } ] ``` ## testdata/fails_without_feature ```json [ { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 8 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-fails-without-feature", "replacement": "0", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 8 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-fails-without-feature", "replacement": "1", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 8 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-fails-without-feature", "replacement": "+=", "span": { "end": { "column": 13, "line": 12 }, "start": { "column": 11, "line": 12 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 8 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-fails-without-feature", "replacement": "/=", "span": { "end": { "column": 13, "line": 12 }, "start": { "column": 11, "line": 12 } } } ] ``` ## testdata/hang_avoided_by_attr ```json [ { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "", "span": { "end": { "column": 2, "line": 25 }, "start": { "column": 1, "line": 14 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-avoided-by-attr", "replacement": "()", "span": { "end": { "column": 6, "line": 24 }, "start": { "column": 5, "line": 15 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "", "span": { "end": { "column": 2, "line": 25 }, "start": { "column": 1, "line": 14 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-avoided-by-attr", "replacement": "==", "span": { "end": { "column": 29, "line": 21 }, "start": { "column": 28, "line": 21 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "", "span": { "end": { "column": 2, "line": 25 }, "start": { "column": 1, "line": 14 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-avoided-by-attr", "replacement": "<", "span": { "end": { "column": 29, "line": 21 }, "start": { "column": 28, "line": 21 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "", "span": { "end": { "column": 2, "line": 25 }, "start": { "column": 1, "line": 14 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-avoided-by-attr", "replacement": "+", "span": { "end": { "column": 54, "line": 21 }, "start": { "column": 53, "line": 21 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "", "span": { "end": { "column": 2, "line": 25 }, "start": { "column": 1, "line": 14 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-avoided-by-attr", "replacement": "/", "span": { "end": { "column": 54, "line": 21 }, "start": { "column": 53, "line": 21 } } } ] ``` ## testdata/hang_when_mutated ```json [ { "file": "src/lib.rs", "function": { "function_name": "should_stop_const", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "false", "span": { "end": { "column": 9, "line": 12 }, "start": { "column": 5, "line": 12 } } }, { "file": "src/lib.rs", "function": { "function_name": "should_stop", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 23 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "true", "span": { "end": { "column": 10, "line": 29 }, "start": { "column": 5, "line": 25 } } }, { "file": "src/lib.rs", "function": { "function_name": "should_stop", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 23 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "false", "span": { "end": { "column": 10, "line": 29 }, "start": { "column": 5, "line": 25 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 50 }, "start": { "column": 1, "line": 32 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "0", "span": { "end": { "column": 20, "line": 49 }, "start": { "column": 5, "line": 38 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 50 }, "start": { "column": 1, "line": 32 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "1", "span": { "end": { "column": 20, "line": 49 }, "start": { "column": 5, "line": 38 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 50 }, "start": { "column": 1, "line": 32 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "==", "span": { "end": { "column": 29, "line": 45 }, "start": { "column": 28, "line": 45 } } }, { "file": "src/lib.rs", "function": { "function_name": "controlled_loop", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 50 }, "start": { "column": 1, "line": 32 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-hang-when-mutated", "replacement": "<", "span": { "end": { "column": 29, "line": 45 }, "start": { "column": 28, "line": 45 } } } ] ``` ## testdata/insta ```json [ { "file": "src/lib.rs", "function": { "function_name": "say_hello", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-insta", "replacement": "String::new()", "span": { "end": { "column": 30, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "say_hello", "return_type": "-> String", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-insta", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 30, "line": 2 }, "start": { "column": 5, "line": 2 } } } ] ``` ## testdata/integration_tests ```json [ { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-integration-tests", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-integration-tests", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-integration-tests", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-integration-tests", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } } ] ``` ## testdata/many_patterns ```json [ { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-many-patterns", "replacement": "()", "span": { "end": { "column": 12, "line": 8 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "-", "span": { "end": { "column": 16, "line": 2 }, "start": { "column": 15, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "*", "span": { "end": { "column": 16, "line": 2 }, "start": { "column": 15, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "/", "span": { "end": { "column": 28, "line": 2 }, "start": { "column": 27, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "+", "span": { "end": { "column": 28, "line": 2 }, "start": { "column": 27, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "%", "span": { "end": { "column": 24, "line": 2 }, "start": { "column": 23, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "*", "span": { "end": { "column": 24, "line": 2 }, "start": { "column": 23, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "+", "span": { "end": { "column": 20, "line": 2 }, "start": { "column": 19, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "/", "span": { "end": { "column": 20, "line": 2 }, "start": { "column": 19, "line": 2 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "&", "span": { "end": { "column": 20, "line": 3 }, "start": { "column": 19, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "^", "span": { "end": { "column": 20, "line": 3 }, "start": { "column": 19, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "|", "span": { "end": { "column": 16, "line": 3 }, "start": { "column": 15, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "^", "span": { "end": { "column": 16, "line": 3 }, "start": { "column": 15, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "|", "span": { "end": { "column": 24, "line": 3 }, "start": { "column": 23, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "&", "span": { "end": { "column": 24, "line": 3 }, "start": { "column": 23, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "<<", "span": { "end": { "column": 34, "line": 3 }, "start": { "column": 32, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": ">>", "span": { "end": { "column": 29, "line": 3 }, "start": { "column": 27, "line": 3 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "-=", "span": { "end": { "column": 9, "line": 5 }, "start": { "column": 7, "line": 5 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "*=", "span": { "end": { "column": 9, "line": 5 }, "start": { "column": 7, "line": 5 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "+=", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "/=", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "+=", "span": { "end": { "column": 9, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "/=", "span": { "end": { "column": 9, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "%=", "span": { "end": { "column": 9, "line": 8 }, "start": { "column": 7, "line": 8 } } }, { "file": "src/binops.rs", "function": { "function_name": "binops", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "*=", "span": { "end": { "column": 9, "line": 8 }, "start": { "column": 7, "line": 8 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-many-patterns", "replacement": "0", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 12 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-many-patterns", "replacement": "1", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 12 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-many-patterns", "replacement": "-1", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 12 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "&=", "span": { "end": { "column": 9, "line": 13 }, "start": { "column": 7, "line": 13 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "^=", "span": { "end": { "column": 9, "line": 13 }, "start": { "column": 7, "line": 13 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "|=", "span": { "end": { "column": 9, "line": 14 }, "start": { "column": 7, "line": 14 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "&=", "span": { "end": { "column": 9, "line": 14 }, "start": { "column": 7, "line": 14 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "|=", "span": { "end": { "column": 9, "line": 15 }, "start": { "column": 7, "line": 15 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "^=", "span": { "end": { "column": 9, "line": 15 }, "start": { "column": 7, "line": 15 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": "<<=", "span": { "end": { "column": 10, "line": 16 }, "start": { "column": 7, "line": 16 } } }, { "file": "src/binops.rs", "function": { "function_name": "bin_assign", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-many-patterns", "replacement": ">>=", "span": { "end": { "column": 10, "line": 17 }, "start": { "column": 7, "line": 17 } } } ] ``` ## testdata/missing_test ```json [ { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-missing-test", "replacement": "true", "span": { "end": { "column": 37, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-missing-test", "replacement": "false", "span": { "end": { "column": 37, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test", "replacement": "==", "span": { "end": { "column": 35, "line": 2 }, "start": { "column": 33, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test", "replacement": "|", "span": { "end": { "column": 23, "line": 2 }, "start": { "column": 22, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test", "replacement": "^", "span": { "end": { "column": 23, "line": 2 }, "start": { "column": 22, "line": 2 } } } ] ``` ## testdata/missing_test_fixed ```json [ { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-missing-test-fixed", "replacement": "true", "span": { "end": { "column": 46, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-missing-test-fixed", "replacement": "false", "span": { "end": { "column": 46, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test-fixed", "replacement": "!=", "span": { "end": { "column": 36, "line": 2 }, "start": { "column": 34, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test-fixed", "replacement": "|", "span": { "end": { "column": 23, "line": 2 }, "start": { "column": 22, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_symlink", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-missing-test-fixed", "replacement": "^", "span": { "end": { "column": 23, "line": 2 }, "start": { "column": 22, "line": 2 } } } ] ``` ## testdata/mut_ref ```json [ { "file": "src/lib.rs", "function": { "function_name": "returns_mut_ref", "return_type": "-> &mut u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-mut-ref", "replacement": "Box::leak(Box::new(0))", "span": { "end": { "column": 26, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/lib.rs", "function": { "function_name": "returns_mut_ref", "return_type": "-> &mut u32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-mut-ref", "replacement": "Box::leak(Box::new(1))", "span": { "end": { "column": 26, "line": 2 }, "start": { "column": 5, "line": 2 } } } ] ``` ## testdata/mutants_attrs ```json [ { "file": "src/lib.rs", "function": { "function_name": "skip", "return_type": "-> TokenStream", "span": { "end": { "column": 2, "line": 31 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "mutants", "replacement": "Default::default()", "span": { "end": { "column": 9, "line": 30 }, "start": { "column": 5, "line": 30 } } } ] ``` ## testdata/nested_mod ```json [ { "file": "src/paths_in_lib/thread_files/tls.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/thread_files_inner_attr/tls.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/toplevel_file_in_lib.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/thread_files/tls.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/thread_files_inner_attr/tls.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/toplevel_file_in_main.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/block_in_lib/a/b/c_file/d/e/f_file.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/a/foo.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/a/b/inline/other.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/a_mod_file/foo.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/a_mod_file/inline/other.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_lib/../upward_traversal_file_for_lib.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/block_in_main/a/b/c_file/d/e/f_file.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/a/foo.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/a/b/inline/other.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/a_mod_file/foo.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/paths_in_main/a_mod_file/inline/other.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/file_in_lib/a/b/c_file/d/e/f_file.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/file_in_main/a/b/c_file/d/e/f_file.rs", "function": { "function_name": "always_true", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-nested-mod", "replacement": "false", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 5, "line": 2 } } } ] ``` ## testdata/never_type ```json [] ``` ## testdata/nightly_only ```json [ { "file": "src/lib.rs", "function": { "function_name": "box_an_int", "return_type": "-> Box", "span": { "end": { "column": 2, "line": 4 }, "start": { "column": 1, "line": 2 } } }, "genre": "FnValue", "package": "nightly_only", "replacement": "Box::new(0)", "span": { "end": { "column": 16, "line": 3 }, "start": { "column": 5, "line": 3 } } }, { "file": "src/lib.rs", "function": { "function_name": "box_an_int", "return_type": "-> Box", "span": { "end": { "column": 2, "line": 4 }, "start": { "column": 1, "line": 2 } } }, "genre": "FnValue", "package": "nightly_only", "replacement": "Box::new(1)", "span": { "end": { "column": 16, "line": 3 }, "start": { "column": 5, "line": 3 } } }, { "file": "src/lib.rs", "function": { "function_name": "box_an_int", "return_type": "-> Box", "span": { "end": { "column": 2, "line": 4 }, "start": { "column": 1, "line": 2 } } }, "genre": "FnValue", "package": "nightly_only", "replacement": "Box::new(-1)", "span": { "end": { "column": 16, "line": 3 }, "start": { "column": 5, "line": 3 } } } ] ``` ## testdata/override_dependency ```json [ { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-override-dependency", "replacement": "true", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-override-dependency", "replacement": "false", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-override-dependency", "replacement": "!=", "span": { "end": { "column": 13, "line": 7 }, "start": { "column": 11, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-override-dependency", "replacement": "/", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-override-dependency", "replacement": "+", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } } ] ``` ## testdata/package_fails ```json [ { "file": "failing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-package-fails-failing", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "failing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-package-fails-failing", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "failing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-package-fails-failing", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "failing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-package-fails-failing", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "passing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-package-fails-passing", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "passing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-package-fails-passing", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "passing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-package-fails-passing", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "passing/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-package-fails-passing", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } } ] ``` ## testdata/patch_dependency ```json [ { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-patch-dependency", "replacement": "true", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-patch-dependency", "replacement": "false", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-patch-dependency", "replacement": "!=", "span": { "end": { "column": 13, "line": 7 }, "start": { "column": 11, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-patch-dependency", "replacement": "/", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-patch-dependency", "replacement": "+", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } } ] ``` ## testdata/proc_macro ```json [ { "file": "src/lib.rs", "function": { "function_name": "static_len", "return_type": "-> TokenStream", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-proc-macro", "replacement": "Default::default()", "span": { "end": { "column": 73, "line": 12 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/lib.rs", "function": { "function_name": "static_len", "return_type": "-> TokenStream", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 5 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-proc-macro", "replacement": "", "span": { "end": { "column": 23, "line": 10 }, "start": { "column": 22, "line": 10 } } } ] ``` ## testdata/relative_dependency ```json [ { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-relative-dependency", "replacement": "0", "span": { "end": { "column": 41, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-relative-dependency", "replacement": "1", "span": { "end": { "column": 41, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-relative-dependency", "replacement": "==", "span": { "end": { "column": 11, "line": 4 }, "start": { "column": 10, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-relative-dependency", "replacement": ">", "span": { "end": { "column": 11, "line": 4 }, "start": { "column": 10, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-relative-dependency", "replacement": "+", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "double_factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-relative-dependency", "replacement": "/", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } } ] ``` ## testdata/replace_dependency ```json [ { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-replace-dependency", "replacement": "true", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-replace-dependency", "replacement": "false", "span": { "end": { "column": 15, "line": 7 }, "start": { "column": 5, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-replace-dependency", "replacement": "!=", "span": { "end": { "column": 13, "line": 7 }, "start": { "column": 11, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-replace-dependency", "replacement": "/", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "is_even", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 6 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-replace-dependency", "replacement": "+", "span": { "end": { "column": 8, "line": 7 }, "start": { "column": 7, "line": 7 } } } ] ``` ## testdata/small_well_tested ```json [ { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-small-well-tested", "replacement": "0", "span": { "end": { "column": 6, "line": 9 }, "start": { "column": 5, "line": 5 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-small-well-tested", "replacement": "1", "span": { "end": { "column": 6, "line": 9 }, "start": { "column": 5, "line": 5 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 4 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-small-well-tested", "replacement": "+=", "span": { "end": { "column": 13, "line": 7 }, "start": { "column": 11, "line": 7 } } }, { "file": "src/lib.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 4 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-small-well-tested", "replacement": "/=", "span": { "end": { "column": 13, "line": 7 }, "start": { "column": 11, "line": 7 } } } ] ``` ## testdata/strict_warnings ```json [ { "file": "src/lib.rs", "function": { "function_name": "some_fn", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-strict-warnings", "replacement": "0", "span": { "end": { "column": 10, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "some_fn", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-strict-warnings", "replacement": "1", "span": { "end": { "column": 10, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "some_fn", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-strict-warnings", "replacement": "-", "span": { "end": { "column": 8, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "some_fn", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-strict-warnings", "replacement": "*", "span": { "end": { "column": 8, "line": 6 }, "start": { "column": 7, "line": 6 } } } ] ``` ## testdata/struct_with_no_default ```json [ { "file": "src/lib.rs", "function": { "function_name": "make_an_s", "return_type": "-> S", "span": { "end": { "column": 2, "line": 16 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-struct-with-no-default", "replacement": "Default::default()", "span": { "end": { "column": 6, "line": 15 }, "start": { "column": 5, "line": 12 } } } ] ``` ## testdata/symlink ```json [ { "file": "src/lib.rs", "function": { "function_name": "read_through_symlink", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-symlink", "replacement": "String::new()", "span": { "end": { "column": 43, "line": 6 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/lib.rs", "function": { "function_name": "read_through_symlink", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-symlink", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 43, "line": 6 }, "start": { "column": 5, "line": 4 } } } ] ``` ## testdata/typecheck_fails ```json [ { "file": "src/lib.rs", "function": { "function_name": "try_value_coercion", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "mutants-testdata-typecheck-fails", "replacement": "String::new()", "span": { "end": { "column": 12, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "try_value_coercion", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "mutants-testdata-typecheck-fails", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 12, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "try_value_coercion", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-typecheck-fails", "replacement": "-", "span": { "end": { "column": 10, "line": 6 }, "start": { "column": 9, "line": 6 } } }, { "file": "src/lib.rs", "function": { "function_name": "try_value_coercion", "return_type": "-> String", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "mutants-testdata-typecheck-fails", "replacement": "*", "span": { "end": { "column": 10, "line": 6 }, "start": { "column": 9, "line": 6 } } } ] ``` ## testdata/unapply ```json [ { "file": "src/a.rs", "function": { "function_name": "one", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "0", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/a.rs", "function": { "function_name": "one", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "-1", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/b.rs", "function": { "function_name": "one_untested", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "0", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/b.rs", "function": { "function_name": "one_untested", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "-1", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/c.rs", "function": { "function_name": "one", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "0", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/c.rs", "function": { "function_name": "one", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-unapply", "replacement": "-1", "span": { "end": { "column": 6, "line": 2 }, "start": { "column": 5, "line": 2 } } } ] ``` ## testdata/unsafe ```json [] ``` ## testdata/well_tested ```json [ { "file": "src/arc.rs", "function": { "function_name": "return_arc", "return_type": "-> Arc", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Arc::new(String::new())", "span": { "end": { "column": 37, "line": 4 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/arc.rs", "function": { "function_name": "return_arc", "return_type": "-> Arc", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Arc::new(\"xyzzy\".into())", "span": { "end": { "column": 37, "line": 4 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 11, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 11, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "||", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 11, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 11, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "&&", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "|", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "&", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"xyzzy\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "()", "span": { "end": { "column": 21, "line": 17 }, "start": { "column": 9, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+=", "span": { "end": { "column": 18, "line": 17 }, "start": { "column": 16, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/=", "span": { "end": { "column": 18, "line": 17 }, "start": { "column": 16, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "::fmt", "return_type": "-> fmt::Result", "span": { "end": { "column": 6, "line": 24 }, "start": { "column": 5, "line": 22 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 36, "line": 23 }, "start": { "column": 9, "line": 23 } } }, { "file": "src/methods.rs", "function": { "function_name": "::fmt", "return_type": "-> fmt::Result", "span": { "end": { "column": 6, "line": 30 }, "start": { "column": 5, "line": 28 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 37, "line": 29 }, "start": { "column": 9, "line": 29 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 9, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 9, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 13, "line": 6 }, "start": { "column": 12, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 13, "line": 6 }, "start": { "column": 12, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/result.rs", "function": { "function_name": "simple_result", "return_type": "-> Result<&'static str, ()>", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(\"\")", "span": { "end": { "column": 18, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/result.rs", "function": { "function_name": "simple_result", "return_type": "-> Result<&'static str, ()>", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(\"xyzzy\")", "span": { "end": { "column": 18, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(())", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "==", "span": { "end": { "column": 11, "line": 10 }, "start": { "column": 10, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": ">", "span": { "end": { "column": 11, "line": 10 }, "start": { "column": 10, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "result_with_no_apparent_type_args", "return_type": "-> std::fmt::Result", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 28, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::new()", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::from_iter([String::new()])", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::from_iter([\"xyzzy\".into()])", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "()", "span": { "end": { "column": 13, "line": 8 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "-=", "span": { "end": { "column": 10, "line": 8 }, "start": { "column": 8, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "*=", "span": { "end": { "column": 10, "line": 8 }, "start": { "column": 8, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_42u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 14 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 13 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_42u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 14 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 13 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 13, "line": 18 }, "start": { "column": 11, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "double_string", "return_type": "-> String", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "String::new()", "span": { "end": { "column": 6, "line": 29 }, "start": { "column": 5, "line": 27 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "double_string", "return_type": "-> String", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 6, "line": 29 }, "start": { "column": 5, "line": 27 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(Vec::new())", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Borrowed(\"\")])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Owned(\"\".to_owned())])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Borrowed(\"xyzzy\")])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Owned(\"xyzzy\".to_owned())])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "==", "span": { "end": { "column": 21, "line": 5 }, "start": { "column": 20, "line": 5 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": ">", "span": { "end": { "column": 21, "line": 5 }, "start": { "column": 20, "line": 5 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(Vec::new())", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![0])", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![1])", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+=", "span": { "end": { "column": 14, "line": 14 }, "start": { "column": 12, "line": 14 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/=", "span": { "end": { "column": 14, "line": 14 }, "start": { "column": 12, "line": 14 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 35, "line": 1 }, "start": { "column": 33, "line": 1 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "-", "span": { "end": { "column": 40, "line": 1 }, "start": { "column": 39, "line": 1 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "*", "span": { "end": { "column": 40, "line": 1 }, "start": { "column": 39, "line": 1 } } }, { "file": "src/struct_with_lifetime.rs", "function": { "function_name": "Lex<'buf>::buf_len", "return_type": "-> usize", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 14 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 23, "line": 15 }, "start": { "column": 9, "line": 15 } } }, { "file": "src/struct_with_lifetime.rs", "function": { "function_name": "Lex<'buf>::buf_len", "return_type": "-> usize", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 14 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 23, "line": 15 }, "start": { "column": 9, "line": 15 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 5 }, "start": { "column": 9, "line": 5 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 5 }, "start": { "column": 9, "line": 5 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 13, "line": 5 }, "start": { "column": 11, "line": 5 } } } ] ``` ## testdata/with_child_directories ```json [ { "file": "src/methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/module/module_methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/module_methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/module_methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/module/module_methods.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/module/utils/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "\"\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/module/utils/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "\"xyzzy\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "+", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/module/utils/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "/", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "0", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "1", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "+", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/module/utils/sub_utils/subutils_nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-with-child-directories", "replacement": "/", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } } ] ``` ## testdata/workspace ```json [ { "file": "utils/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo_mutants_testdata_workspace_utils", "replacement": "0", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "utils/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo_mutants_testdata_workspace_utils", "replacement": "1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "utils/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo_mutants_testdata_workspace_utils", "replacement": "-1", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "utils/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo_mutants_testdata_workspace_utils", "replacement": "+", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "utils/src/lib.rs", "function": { "function_name": "triple", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo_mutants_testdata_workspace_utils", "replacement": "/", "span": { "end": { "column": 8, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "main/src/main.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "main", "replacement": "0", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 12 } } }, { "file": "main/src/main.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "main", "replacement": "1", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 12 } } }, { "file": "main/src/main.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "main", "replacement": "+=", "span": { "end": { "column": 13, "line": 14 }, "start": { "column": 11, "line": 14 } } }, { "file": "main/src/main.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 11 } } }, "genre": "BinaryOperator", "package": "main", "replacement": "/=", "span": { "end": { "column": 13, "line": 14 }, "start": { "column": 11, "line": 14 } } }, { "file": "main2/src/main.rs", "function": { "function_name": "triple_3", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "main2", "replacement": "0", "span": { "end": { "column": 14, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "main2/src/main.rs", "function": { "function_name": "triple_3", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "main2", "replacement": "1", "span": { "end": { "column": 14, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "main2/src/main.rs", "function": { "function_name": "triple_3", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "main2", "replacement": "-1", "span": { "end": { "column": 14, "line": 10 }, "start": { "column": 5, "line": 10 } } } ] ``` cargo-mutants-25.0.0/tests/snapshots/list__list_mutants_in_all_trees_as_text.snap000064400000000000000000000502211046102023000267320ustar 00000000000000--- source: tests/list.rs expression: buf --- ## testdata/already_failing_doctests ``` src/lib.rs:10:5: replace takes_one_arg -> usize with 0 src/lib.rs:10:5: replace takes_one_arg -> usize with 1 src/lib.rs:10:7: replace + with - in takes_one_arg src/lib.rs:10:7: replace + with * in takes_one_arg ``` ## testdata/already_failing_tests ``` src/lib.rs:2:5: replace factorial -> u32 with 0 src/lib.rs:2:5: replace factorial -> u32 with 1 src/lib.rs:4:11: replace *= with += in factorial src/lib.rs:4:11: replace *= with /= in factorial ``` ## testdata/already_hangs ``` src/lib.rs:12:5: replace infinite_loop with () ``` ## testdata/cdylib ``` src/entry.rs:2:5: replace factorial -> u32 with 0 src/entry.rs:2:5: replace factorial -> u32 with 1 src/entry.rs:4:11: replace *= with += in factorial src/entry.rs:4:11: replace *= with /= in factorial ``` ## testdata/cfg_attr_mutants_skip ``` ``` ## testdata/cfg_attr_test_skip ``` src/lib.rs:18:5: replace double -> usize with 0 src/lib.rs:18:5: replace double -> usize with 1 src/lib.rs:18:7: replace * with + in double src/lib.rs:18:7: replace * with / in double ``` ## testdata/cfg_test_inner ``` ``` ## testdata/cross_package_tests ``` lib/src/lib.rs:2:5: replace add -> u32 with 0 lib/src/lib.rs:2:5: replace add -> u32 with 1 lib/src/lib.rs:2:7: replace + with - in add lib/src/lib.rs:2:7: replace + with * in add ``` ## testdata/custom_top_file ``` src/custom_top.rs:2:5: replace is_even -> bool with true src/custom_top.rs:2:5: replace is_even -> bool with false src/custom_top.rs:2:11: replace == with != in is_even src/custom_top.rs:2:7: replace % with / in is_even src/custom_top.rs:2:7: replace % with + in is_even ``` ## testdata/dangling_mod ``` src/main.rs:13:9: replace verify_continue::always_true -> bool with false ``` ## testdata/dependency ``` src/lib.rs:2:5: replace factorial -> u32 with 0 src/lib.rs:2:5: replace factorial -> u32 with 1 src/lib.rs:4:11: replace *= with += in factorial src/lib.rs:4:11: replace *= with /= in factorial ``` ## testdata/diff0 ``` src/lib.rs:2:5: replace one -> String with String::new() src/lib.rs:2:5: replace one -> String with "xyzzy".into() ``` ## testdata/diff1 ``` src/lib.rs:2:5: replace one -> String with String::new() src/lib.rs:2:5: replace one -> String with "xyzzy".into() src/lib.rs:6:5: replace two -> String with String::new() src/lib.rs:6:5: replace two -> String with "xyzzy".into() ``` ## testdata/error_value ``` src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(0) src/lib.rs:4:5: replace zero_is_ok -> Result with Ok(1) src/lib.rs:4:5: replace zero_is_ok -> Result with Err("injected") src/lib.rs:4:10: replace == with != in zero_is_ok ``` ## testdata/everything_skipped ``` ``` ## testdata/factorial ``` src/bin/factorial.rs:2:5: replace main with () src/bin/factorial.rs:8:5: replace factorial -> u32 with 0 src/bin/factorial.rs:8:5: replace factorial -> u32 with 1 src/bin/factorial.rs:10:11: replace *= with += in factorial src/bin/factorial.rs:10:11: replace *= with /= in factorial ``` ## testdata/fails_without_feature ``` src/bin/factorial.rs:10:5: replace factorial -> u32 with 0 src/bin/factorial.rs:10:5: replace factorial -> u32 with 1 src/bin/factorial.rs:12:11: replace *= with += in factorial src/bin/factorial.rs:12:11: replace *= with /= in factorial ``` ## testdata/hang_avoided_by_attr ``` src/lib.rs:15:5: replace controlled_loop with () src/lib.rs:21:28: replace > with == in controlled_loop src/lib.rs:21:28: replace > with < in controlled_loop src/lib.rs:21:53: replace * with + in controlled_loop src/lib.rs:21:53: replace * with / in controlled_loop ``` ## testdata/hang_when_mutated ``` src/lib.rs:12:5: replace should_stop_const -> bool with false src/lib.rs:25:5: replace should_stop -> bool with true src/lib.rs:25:5: replace should_stop -> bool with false src/lib.rs:38:5: replace controlled_loop -> usize with 0 src/lib.rs:38:5: replace controlled_loop -> usize with 1 src/lib.rs:45:28: replace > with == in controlled_loop src/lib.rs:45:28: replace > with < in controlled_loop ``` ## testdata/insta ``` src/lib.rs:2:5: replace say_hello -> String with String::new() src/lib.rs:2:5: replace say_hello -> String with "xyzzy".into() ``` ## testdata/integration_tests ``` src/lib.rs:2:5: replace double -> u32 with 0 src/lib.rs:2:5: replace double -> u32 with 1 src/lib.rs:2:7: replace * with + in double src/lib.rs:2:7: replace * with / in double ``` ## testdata/many_patterns ``` src/binops.rs:2:5: replace binops with () src/binops.rs:2:15: replace + with - in binops src/binops.rs:2:15: replace + with * in binops src/binops.rs:2:27: replace % with / in binops src/binops.rs:2:27: replace % with + in binops src/binops.rs:2:23: replace / with % in binops src/binops.rs:2:23: replace / with * in binops src/binops.rs:2:19: replace * with + in binops src/binops.rs:2:19: replace * with / in binops src/binops.rs:3:19: replace | with & in binops src/binops.rs:3:19: replace | with ^ in binops src/binops.rs:3:15: replace & with | in binops src/binops.rs:3:15: replace & with ^ in binops src/binops.rs:3:23: replace ^ with | in binops src/binops.rs:3:23: replace ^ with & in binops src/binops.rs:3:32: replace >> with << in binops src/binops.rs:3:27: replace << with >> in binops src/binops.rs:5:7: replace += with -= in binops src/binops.rs:5:7: replace += with *= in binops src/binops.rs:6:7: replace -= with += in binops src/binops.rs:6:7: replace -= with /= in binops src/binops.rs:7:7: replace *= with += in binops src/binops.rs:7:7: replace *= with /= in binops src/binops.rs:8:7: replace /= with %= in binops src/binops.rs:8:7: replace /= with *= in binops src/binops.rs:12:5: replace bin_assign -> i32 with 0 src/binops.rs:12:5: replace bin_assign -> i32 with 1 src/binops.rs:12:5: replace bin_assign -> i32 with -1 src/binops.rs:13:7: replace |= with &= in bin_assign src/binops.rs:13:7: replace |= with ^= in bin_assign src/binops.rs:14:7: replace ^= with |= in bin_assign src/binops.rs:14:7: replace ^= with &= in bin_assign src/binops.rs:15:7: replace &= with |= in bin_assign src/binops.rs:15:7: replace &= with ^= in bin_assign src/binops.rs:16:7: replace >>= with <<= in bin_assign src/binops.rs:17:7: replace <<= with >>= in bin_assign ``` ## testdata/missing_test ``` src/lib.rs:2:5: replace is_symlink -> bool with true src/lib.rs:2:5: replace is_symlink -> bool with false src/lib.rs:2:33: replace != with == in is_symlink src/lib.rs:2:22: replace & with | in is_symlink src/lib.rs:2:22: replace & with ^ in is_symlink ``` ## testdata/missing_test_fixed ``` src/lib.rs:2:5: replace is_symlink -> bool with true src/lib.rs:2:5: replace is_symlink -> bool with false src/lib.rs:2:34: replace == with != in is_symlink src/lib.rs:2:22: replace & with | in is_symlink src/lib.rs:2:22: replace & with ^ in is_symlink ``` ## testdata/mut_ref ``` src/lib.rs:2:5: replace returns_mut_ref -> &mut u32 with Box::leak(Box::new(0)) src/lib.rs:2:5: replace returns_mut_ref -> &mut u32 with Box::leak(Box::new(1)) ``` ## testdata/mutants_attrs ``` src/lib.rs:30:5: replace skip -> TokenStream with Default::default() ``` ## testdata/nested_mod ``` src/paths_in_lib/thread_files/tls.rs:2:5: replace always_true -> bool with false src/paths_in_lib/thread_files_inner_attr/tls.rs:2:5: replace always_true -> bool with false src/toplevel_file_in_lib.rs:2:5: replace always_true -> bool with false src/paths_in_main/thread_files/tls.rs:2:5: replace always_true -> bool with false src/paths_in_main/thread_files_inner_attr/tls.rs:2:5: replace always_true -> bool with false src/toplevel_file_in_main.rs:2:5: replace always_true -> bool with false src/block_in_lib/a/b/c_file/d/e/f_file.rs:2:5: replace always_true -> bool with false src/paths_in_lib/a/foo.rs:2:5: replace always_true -> bool with false src/paths_in_lib/a/b/inline/other.rs:2:5: replace always_true -> bool with false src/paths_in_lib/a_mod_file/foo.rs:2:5: replace always_true -> bool with false src/paths_in_lib/a_mod_file/inline/other.rs:2:5: replace always_true -> bool with false src/paths_in_lib/../upward_traversal_file_for_lib.rs:2:5: replace always_true -> bool with false src/block_in_main/a/b/c_file/d/e/f_file.rs:2:5: replace always_true -> bool with false src/paths_in_main/a/foo.rs:2:5: replace always_true -> bool with false src/paths_in_main/a/b/inline/other.rs:2:5: replace always_true -> bool with false src/paths_in_main/a_mod_file/foo.rs:2:5: replace always_true -> bool with false src/paths_in_main/a_mod_file/inline/other.rs:2:5: replace always_true -> bool with false src/file_in_lib/a/b/c_file/d/e/f_file.rs:2:5: replace always_true -> bool with false src/file_in_main/a/b/c_file/d/e/f_file.rs:2:5: replace always_true -> bool with false ``` ## testdata/never_type ``` ``` ## testdata/nightly_only ``` src/lib.rs:3:5: replace box_an_int -> Box with Box::new(0) src/lib.rs:3:5: replace box_an_int -> Box with Box::new(1) src/lib.rs:3:5: replace box_an_int -> Box with Box::new(-1) ``` ## testdata/override_dependency ``` src/lib.rs:7:5: replace is_even -> bool with true src/lib.rs:7:5: replace is_even -> bool with false src/lib.rs:7:11: replace == with != in is_even src/lib.rs:7:7: replace % with / in is_even src/lib.rs:7:7: replace % with + in is_even ``` ## testdata/package_fails ``` failing/src/lib.rs:2:5: replace triple -> usize with 0 failing/src/lib.rs:2:5: replace triple -> usize with 1 failing/src/lib.rs:2:7: replace * with + in triple failing/src/lib.rs:2:7: replace * with / in triple passing/src/lib.rs:2:5: replace triple -> usize with 0 passing/src/lib.rs:2:5: replace triple -> usize with 1 passing/src/lib.rs:2:7: replace * with + in triple passing/src/lib.rs:2:7: replace * with / in triple ``` ## testdata/patch_dependency ``` src/lib.rs:7:5: replace is_even -> bool with true src/lib.rs:7:5: replace is_even -> bool with false src/lib.rs:7:11: replace == with != in is_even src/lib.rs:7:7: replace % with / in is_even src/lib.rs:7:7: replace % with + in is_even ``` ## testdata/proc_macro ``` src/lib.rs:8:5: replace static_len -> TokenStream with Default::default() src/lib.rs:10:22: delete ! in static_len ``` ## testdata/relative_dependency ``` src/lib.rs:4:5: replace double_factorial -> u32 with 0 src/lib.rs:4:5: replace double_factorial -> u32 with 1 src/lib.rs:4:10: replace < with == in double_factorial src/lib.rs:4:10: replace < with > in double_factorial src/lib.rs:7:7: replace * with + in double_factorial src/lib.rs:7:7: replace * with / in double_factorial ``` ## testdata/replace_dependency ``` src/lib.rs:7:5: replace is_even -> bool with true src/lib.rs:7:5: replace is_even -> bool with false src/lib.rs:7:11: replace == with != in is_even src/lib.rs:7:7: replace % with / in is_even src/lib.rs:7:7: replace % with + in is_even ``` ## testdata/small_well_tested ``` src/lib.rs:5:5: replace factorial -> u32 with 0 src/lib.rs:5:5: replace factorial -> u32 with 1 src/lib.rs:7:11: replace *= with += in factorial src/lib.rs:7:11: replace *= with /= in factorial ``` ## testdata/strict_warnings ``` src/lib.rs:6:5: replace some_fn -> usize with 0 src/lib.rs:6:5: replace some_fn -> usize with 1 src/lib.rs:6:7: replace + with - in some_fn src/lib.rs:6:7: replace + with * in some_fn ``` ## testdata/struct_with_no_default ``` src/lib.rs:12:5: replace make_an_s -> S with Default::default() ``` ## testdata/symlink ``` src/lib.rs:4:5: replace read_through_symlink -> String with String::new() src/lib.rs:4:5: replace read_through_symlink -> String with "xyzzy".into() ``` ## testdata/typecheck_fails ``` src/lib.rs:6:5: replace try_value_coercion -> String with String::new() src/lib.rs:6:5: replace try_value_coercion -> String with "xyzzy".into() src/lib.rs:6:9: replace + with - in try_value_coercion src/lib.rs:6:9: replace + with * in try_value_coercion ``` ## testdata/unapply ``` src/a.rs:2:5: replace one -> i32 with 0 src/a.rs:2:5: replace one -> i32 with -1 src/b.rs:2:5: replace one_untested -> i32 with 0 src/b.rs:2:5: replace one_untested -> i32 with -1 src/c.rs:2:5: replace one -> i32 with 0 src/c.rs:2:5: replace one -> i32 with -1 ``` ## testdata/unsafe ``` ``` ## testdata/well_tested ``` src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) src/booleans.rs:2:5: replace and -> bool with true src/booleans.rs:2:5: replace and -> bool with false src/booleans.rs:2:7: replace && with || in and src/booleans.rs:6:5: replace or -> bool with true src/booleans.rs:6:5: replace or -> bool with false src/booleans.rs:6:7: replace || with && in or src/booleans.rs:10:5: replace xor -> bool with true src/booleans.rs:10:5: replace xor -> bool with false src/booleans.rs:10:7: replace ^ with | in xor src/booleans.rs:10:7: replace ^ with & in xor src/booleans.rs:14:5: replace not -> bool with true src/booleans.rs:14:5: replace not -> bool with false src/booleans.rs:14:5: delete ! in not src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/methods.rs:17:9: replace Foo::double with () src/methods.rs:17:16: replace *= with += in Foo::double src/methods.rs:17:16: replace *= with /= in Foo::double src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/nested_function.rs:5:13: replace * with + in has_nested src/nested_function.rs:5:13: replace * with / in has_nested src/numbers.rs:2:5: replace double_float -> f32 with 0.0 src/numbers.rs:2:5: replace double_float -> f32 with 1.0 src/numbers.rs:2:5: replace double_float -> f32 with -1.0 src/numbers.rs:2:9: replace * with + in double_float src/numbers.rs:2:9: replace * with / in double_float src/numbers.rs:6:5: replace is_double -> bool with true src/numbers.rs:6:5: replace is_double -> bool with false src/numbers.rs:6:7: replace == with != in is_double src/numbers.rs:6:12: replace * with + in is_double src/numbers.rs:6:12: replace * with / in is_double src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 src/numbers.rs:10:5: delete - in negate_i32 src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 src/numbers.rs:14:5: delete - in negate_f32 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 src/numbers.rs:18:5: delete ! in bitwise_not_i32 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 src/numbers.rs:22:5: delete ! in bitwise_not_u32 src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:8:5: replace returns_unit with () src/simple_fns.rs:8:8: replace += with -= in returns_unit src/simple_fns.rs:8:8: replace += with *= in returns_unit src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 1 src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false src/simple_fns.rs:18:11: replace == with != in divisible_by_three src/simple_fns.rs:18:7: replace % with / in divisible_by_three src/simple_fns.rs:18:7: replace % with + in divisible_by_three src/simple_fns.rs:27:5: replace double_string -> String with String::new() src/simple_fns.rs:27:5: replace double_string -> String with "xyzzy".into() src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) src/slices.rs:14:12: replace *= with += in return_mut_slice src/slices.rs:14:12: replace *= with /= in return_mut_slice src/static_item.rs:1:33: replace == with != src/static_item.rs:1:39: replace + with - src/static_item.rs:1:39: replace + with * src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 src/traits.rs:5:9: replace Something::is_three -> bool with true src/traits.rs:5:9: replace Something::is_three -> bool with false src/traits.rs:5:11: replace == with != in Something::is_three ``` ## testdata/with_child_directories ``` src/methods.rs:2:5: replace double -> usize with 0 src/methods.rs:2:5: replace double -> usize with 1 src/methods.rs:2:7: replace * with + in double src/methods.rs:2:7: replace * with / in double src/module/module_methods.rs:2:5: replace double -> usize with 0 src/module/module_methods.rs:2:5: replace double -> usize with 1 src/module/module_methods.rs:2:7: replace * with + in double src/module/module_methods.rs:2:7: replace * with / in double src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/module/utils/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/module/utils/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/module/utils/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/module/utils/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/module/utils/nested_function.rs:5:13: replace * with + in has_nested src/module/utils/nested_function.rs:5:13: replace * with / in has_nested src/module/utils/sub_utils/subutils_nested_function.rs:2:5: replace has_nested -> u32 with 0 src/module/utils/sub_utils/subutils_nested_function.rs:2:5: replace has_nested -> u32 with 1 src/module/utils/sub_utils/subutils_nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/module/utils/sub_utils/subutils_nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/module/utils/sub_utils/subutils_nested_function.rs:5:13: replace * with + in has_nested src/module/utils/sub_utils/subutils_nested_function.rs:5:13: replace * with / in has_nested ``` ## testdata/workspace ``` utils/src/lib.rs:2:5: replace triple -> i32 with 0 utils/src/lib.rs:2:5: replace triple -> i32 with 1 utils/src/lib.rs:2:5: replace triple -> i32 with -1 utils/src/lib.rs:2:7: replace * with + in triple utils/src/lib.rs:2:7: replace * with / in triple main/src/main.rs:12:5: replace factorial -> u32 with 0 main/src/main.rs:12:5: replace factorial -> u32 with 1 main/src/main.rs:14:11: replace *= with += in factorial main/src/main.rs:14:11: replace *= with /= in factorial main2/src/main.rs:10:5: replace triple_3 -> i32 with 0 main2/src/main.rs:10:5: replace triple_3 -> i32 with 1 main2/src/main.rs:10:5: replace triple_3 -> i32 with -1 ``` cargo-mutants-25.0.0/tests/snapshots/main__cargo_mutants_in_override_dependency_tree_passes.snap000064400000000000000000000002011046102023000317250ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline 5 mutants tested: 5 caught cargo-mutants-25.0.0/tests/snapshots/main__cargo_mutants_in_patch_dependency_tree_passes.snap000064400000000000000000000002011046102023000312050ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline 5 mutants tested: 5 caught cargo-mutants-25.0.0/tests/snapshots/main__cargo_mutants_in_relative_dependency_tree_passes.snap000064400000000000000000000002011046102023000317210ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 6 mutants to test ok Unmutated baseline 6 mutants tested: 6 caught cargo-mutants-25.0.0/tests/snapshots/main__cargo_mutants_in_replace_dependency_tree_passes.snap000064400000000000000000000002011046102023000315210ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline 5 mutants tested: 5 caught cargo-mutants-25.0.0/tests/snapshots/main__cdylib_tree_is_well_tested.snap000064400000000000000000000005571046102023000252700ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 4 mutants to test ok Unmutated baseline caught src/entry.rs:2:5: replace factorial -> u32 with 0 caught src/entry.rs:2:5: replace factorial -> u32 with 1 caught src/entry.rs:4:11: replace *= with += in factorial caught src/entry.rs:4:11: replace *= with /= in factorial 4 mutants tested: 4 caught cargo-mutants-25.0.0/tests/snapshots/main__check_succeeds_in_tree_that_builds_but_fails_tests.snap000064400000000000000000000005521046102023000322040ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 4 mutants to test ok Unmutated baseline ok src/lib.rs:2:5: replace factorial -> u32 with 0 ok src/lib.rs:2:5: replace factorial -> u32 with 1 ok src/lib.rs:4:11: replace *= with += in factorial ok src/lib.rs:4:11: replace *= with /= in factorial 4 mutants tested: 4 succeeded cargo-mutants-25.0.0/tests/snapshots/main__check_tree_with_mutants_skip.snap000064400000000000000000000006731046102023000256440ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline ok src/lib.rs:15:5: replace controlled_loop with () ok src/lib.rs:21:28: replace > with == in controlled_loop ok src/lib.rs:21:28: replace > with < in controlled_loop ok src/lib.rs:21:53: replace * with + in controlled_loop ok src/lib.rs:21:53: replace * with / in controlled_loop 5 mutants tested: 5 succeeded cargo-mutants-25.0.0/tests/snapshots/main__factorial__log_names.snap000064400000000000000000000005021046102023000240320ustar 00000000000000--- source: tests/main.rs expression: "&names" --- [ "baseline.log", "src__bin__factorial.rs_line_10_col_11.log", "src__bin__factorial.rs_line_10_col_11_001.log", "src__bin__factorial.rs_line_2_col_5.log", "src__bin__factorial.rs_line_8_col_5.log", "src__bin__factorial.rs_line_8_col_5_001.log", ] cargo-mutants-25.0.0/tests/snapshots/main__factorial_mutants_no_copy_target.snap000064400000000000000000000003031046102023000265150ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline MISSED src/bin/factorial.rs:2:5: replace main with () 5 mutants tested: 1 missed, 4 caught cargo-mutants-25.0.0/tests/snapshots/main__integration_test_source_is_not_mutated__caught.txt.snap000064400000000000000000000003431046102023000322560ustar 00000000000000--- source: tests/main.rs expression: content --- src/lib.rs:2:5: replace double -> u32 with 0 src/lib.rs:2:5: replace double -> u32 with 1 src/lib.rs:2:7: replace * with + in double src/lib.rs:2:7: replace * with / in double cargo-mutants-25.0.0/tests/snapshots/main__integration_test_source_is_not_mutated__missed.txt.snap000064400000000000000000000000631046102023000322660ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__integration_test_source_is_not_mutated__timeout.txt.snap000064400000000000000000000000631046102023000324700ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__integration_test_source_is_not_mutated__unviable.txt.snap000064400000000000000000000000631046102023000326070ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__mutants.json.snap000064400000000000000000000054141046102023000223550ustar 00000000000000--- source: tests/main.rs expression: mutants_json --- [ { "package": "cargo-mutants-testdata-factorial", "file": "src/bin/factorial.rs", "function": { "function_name": "main", "return_type": "", "span": { "start": { "line": 1, "column": 1 }, "end": { "line": 5, "column": 2 } } }, "span": { "start": { "line": 2, "column": 5 }, "end": { "line": 4, "column": 6 } }, "replacement": "()", "genre": "FnValue" }, { "package": "cargo-mutants-testdata-factorial", "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "start": { "line": 7, "column": 1 }, "end": { "line": 13, "column": 2 } } }, "span": { "start": { "line": 8, "column": 5 }, "end": { "line": 12, "column": 6 } }, "replacement": "0", "genre": "FnValue" }, { "package": "cargo-mutants-testdata-factorial", "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "start": { "line": 7, "column": 1 }, "end": { "line": 13, "column": 2 } } }, "span": { "start": { "line": 8, "column": 5 }, "end": { "line": 12, "column": 6 } }, "replacement": "1", "genre": "FnValue" }, { "package": "cargo-mutants-testdata-factorial", "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "start": { "line": 7, "column": 1 }, "end": { "line": 13, "column": 2 } } }, "span": { "start": { "line": 10, "column": 11 }, "end": { "line": 10, "column": 13 } }, "replacement": "+=", "genre": "BinaryOperator" }, { "package": "cargo-mutants-testdata-factorial", "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "start": { "line": 7, "column": 1 }, "end": { "line": 13, "column": 2 } } }, "span": { "start": { "line": 10, "column": 11 }, "end": { "line": 10, "column": 13 } }, "replacement": "/=", "genre": "BinaryOperator" } ] ././@LongLink00006440000000000000000000000166000000000000007776Lustar cargo-mutants-25.0.0/tests/snapshots/main__mutants_are_unapplied_after_testing_so_later_missed_mutants_are_found.snapcargo-mutants-25.0.0/tests/snapshots/main__mutants_are_unapplied_after_testing_so_later_missed_mutan000064400000000000000000000004001046102023000327010ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 6 mutants to test ok Unmutated baseline MISSED src/b.rs:2:5: replace one_untested -> i32 with 0 MISSED src/b.rs:2:5: replace one_untested -> i32 with -1 6 mutants tested: 2 missed, 4 caught cargo-mutants-25.0.0/tests/snapshots/main__small_well_tested_mutants_with_cargo_arg_release.snap000064400000000000000000000002011046102023000317140ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 4 mutants to test ok Unmutated baseline 4 mutants tested: 4 caught cargo-mutants-25.0.0/tests/snapshots/main__small_well_tested_tree_is_clean.snap000064400000000000000000000005471046102023000262730ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 4 mutants to test ok Unmutated baseline caught src/lib.rs:5:5: replace factorial -> u32 with 0 caught src/lib.rs:5:5: replace factorial -> u32 with 1 caught src/lib.rs:7:11: replace *= with += in factorial caught src/lib.rs:7:11: replace *= with /= in factorial 4 mutants tested: 4 caught cargo-mutants-25.0.0/tests/snapshots/main__small_well_tested_tree_with_baseline_skip.snap000064400000000000000000000005131046102023000303520ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 4 mutants to test caught src/lib.rs:5:5: replace factorial -> u32 with 0 caught src/lib.rs:5:5: replace factorial -> u32 with 1 caught src/lib.rs:7:11: replace *= with += in factorial caught src/lib.rs:7:11: replace *= with /= in factorial 4 mutants tested: 4 caught cargo-mutants-25.0.0/tests/snapshots/main__uncaught_mutant_in_factorial.snap000064400000000000000000000003031046102023000256220ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 5 mutants to test ok Unmutated baseline MISSED src/bin/factorial.rs:2:5: replace main with () 5 mutants tested: 1 missed, 4 caught cargo-mutants-25.0.0/tests/snapshots/main__uncaught_mutant_in_factorial__caught.txt.snap000064400000000000000000000004371046102023000301420ustar 00000000000000--- source: tests/main.rs expression: content --- src/bin/factorial.rs:8:5: replace factorial -> u32 with 0 src/bin/factorial.rs:8:5: replace factorial -> u32 with 1 src/bin/factorial.rs:10:11: replace *= with += in factorial src/bin/factorial.rs:10:11: replace *= with /= in factorial cargo-mutants-25.0.0/tests/snapshots/main__uncaught_mutant_in_factorial__missed.txt.snap000064400000000000000000000001421046102023000301440ustar 00000000000000--- source: tests/main.rs expression: content --- src/bin/factorial.rs:2:5: replace main with () cargo-mutants-25.0.0/tests/snapshots/main__uncaught_mutant_in_factorial__timeout.txt.snap000064400000000000000000000000631046102023000303500ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__uncaught_mutant_in_factorial__unviable.txt.snap000064400000000000000000000000631046102023000304670ustar 00000000000000--- source: tests/main.rs expression: content --- ././@LongLink00006440000000000000000000000150000000000000007767Lustar cargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__caught.txt.snapcargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__caught.txt.s000064400000000000000000000000631046102023000325660ustar 00000000000000--- source: tests/main.rs expression: content --- ././@LongLink00006440000000000000000000000150000000000000007767Lustar cargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__missed.txt.snapcargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__missed.txt.s000064400000000000000000000000631046102023000325770ustar 00000000000000--- source: tests/main.rs expression: content --- ././@LongLink00006440000000000000000000000151000000000000007770Lustar cargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__timeout.txt.snapcargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__timeout.txt.000064400000000000000000000000631046102023000326160ustar 00000000000000--- source: tests/main.rs expression: content --- ././@LongLink00006440000000000000000000000152000000000000007771Lustar cargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__unviable.txt.snapcargo-mutants-25.0.0/tests/snapshots/main__unviable_mutation_of_struct_with_no_default__unviable.txt000064400000000000000000000001631046102023000326600ustar 00000000000000--- source: tests/main.rs expression: content --- src/lib.rs:12:5: replace make_an_s -> S with Default::default() cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_check_only.snap000064400000000000000000000155051046102023000254440ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 94 mutants to test ok Unmutated baseline ok src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) ok src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) ok src/booleans.rs:2:5: replace and -> bool with true ok src/booleans.rs:2:5: replace and -> bool with false ok src/booleans.rs:2:7: replace && with || in and ok src/booleans.rs:6:5: replace or -> bool with true ok src/booleans.rs:6:5: replace or -> bool with false ok src/booleans.rs:6:7: replace || with && in or ok src/booleans.rs:10:5: replace xor -> bool with true ok src/booleans.rs:10:5: replace xor -> bool with false ok src/booleans.rs:10:7: replace ^ with | in xor ok src/booleans.rs:10:7: replace ^ with & in xor ok src/booleans.rs:14:5: replace not -> bool with true ok src/booleans.rs:14:5: replace not -> bool with false ok src/booleans.rs:14:5: delete ! in not ok src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" ok src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" ok src/methods.rs:17:9: replace Foo::double with () ok src/methods.rs:17:16: replace *= with += in Foo::double ok src/methods.rs:17:16: replace *= with /= in Foo::double ok src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) ok src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) ok src/nested_function.rs:2:5: replace has_nested -> u32 with 0 ok src/nested_function.rs:2:5: replace has_nested -> u32 with 1 ok src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 ok src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 ok src/nested_function.rs:5:13: replace * with + in has_nested ok src/nested_function.rs:5:13: replace * with / in has_nested ok src/numbers.rs:2:5: replace double_float -> f32 with 0.0 ok src/numbers.rs:2:5: replace double_float -> f32 with 1.0 ok src/numbers.rs:2:5: replace double_float -> f32 with -1.0 ok src/numbers.rs:2:9: replace * with + in double_float ok src/numbers.rs:2:9: replace * with / in double_float ok src/numbers.rs:6:5: replace is_double -> bool with true ok src/numbers.rs:6:5: replace is_double -> bool with false ok src/numbers.rs:6:7: replace == with != in is_double ok src/numbers.rs:6:12: replace * with + in is_double ok src/numbers.rs:6:12: replace * with / in is_double ok src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 ok src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 ok src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 ok src/numbers.rs:10:5: delete - in negate_i32 ok src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 ok src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 ok src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 ok src/numbers.rs:14:5: delete - in negate_f32 ok src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 ok src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 ok src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 ok src/numbers.rs:18:5: delete ! in bitwise_not_i32 ok src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 ok src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 ok src/numbers.rs:22:5: delete ! in bitwise_not_u32 ok src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") ok src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") ok src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) ok src/result.rs:10:10: replace < with == in error_if_negative ok src/result.rs:10:10: replace < with > in error_if_negative ok src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) ok src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() ok src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) ok src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) ok src/simple_fns.rs:8:5: replace returns_unit with () ok src/simple_fns.rs:8:8: replace += with -= in returns_unit ok src/simple_fns.rs:8:8: replace += with *= in returns_unit ok src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 0 ok src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 1 ok src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true ok src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false ok src/simple_fns.rs:18:11: replace == with != in divisible_by_three ok src/simple_fns.rs:18:7: replace % with / in divisible_by_three ok src/simple_fns.rs:18:7: replace % with + in divisible_by_three ok src/simple_fns.rs:27:5: replace double_string -> String with String::new() ok src/simple_fns.rs:27:5: replace double_string -> String with "xyzzy".into() ok src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) ok src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) ok src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) ok src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) ok src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) ok src/slices.rs:5:20: replace < with == in pad ok src/slices.rs:5:20: replace < with > in pad ok src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) ok src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) ok src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) ok src/slices.rs:14:12: replace *= with += in return_mut_slice ok src/slices.rs:14:12: replace *= with /= in return_mut_slice ok src/static_item.rs:1:33: replace == with != ok src/static_item.rs:1:39: replace + with - ok src/static_item.rs:1:39: replace + with * ok src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 ok src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 ok src/traits.rs:5:9: replace Something::is_three -> bool with true ok src/traits.rs:5:9: replace Something::is_three -> bool with false ok src/traits.rs:5:11: replace == with != in Something::is_three 94 mutants tested: 94 succeeded cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_finds_no_problems.snap000064400000000000000000000155021046102023000270250ustar 00000000000000--- source: tests/main.rs expression: stdout --- Found 94 mutants to test ok Unmutated baseline caught src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) caught src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) caught src/booleans.rs:2:5: replace and -> bool with true caught src/booleans.rs:2:5: replace and -> bool with false caught src/booleans.rs:2:7: replace && with || in and caught src/booleans.rs:6:5: replace or -> bool with true caught src/booleans.rs:6:5: replace or -> bool with false caught src/booleans.rs:6:7: replace || with && in or caught src/booleans.rs:10:5: replace xor -> bool with true caught src/booleans.rs:10:5: replace xor -> bool with false caught src/booleans.rs:10:7: replace ^ with | in xor caught src/booleans.rs:10:7: replace ^ with & in xor caught src/booleans.rs:14:5: replace not -> bool with true caught src/booleans.rs:14:5: replace not -> bool with false caught src/booleans.rs:14:5: delete ! in not caught src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" caught src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" caught src/methods.rs:17:9: replace Foo::double with () caught src/methods.rs:17:16: replace *= with += in Foo::double caught src/methods.rs:17:16: replace *= with /= in Foo::double caught src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) caught src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) caught src/nested_function.rs:2:5: replace has_nested -> u32 with 0 caught src/nested_function.rs:2:5: replace has_nested -> u32 with 1 caught src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 caught src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 caught src/nested_function.rs:5:13: replace * with + in has_nested caught src/nested_function.rs:5:13: replace * with / in has_nested caught src/numbers.rs:2:5: replace double_float -> f32 with 0.0 caught src/numbers.rs:2:5: replace double_float -> f32 with 1.0 caught src/numbers.rs:2:5: replace double_float -> f32 with -1.0 caught src/numbers.rs:2:9: replace * with + in double_float caught src/numbers.rs:2:9: replace * with / in double_float caught src/numbers.rs:6:5: replace is_double -> bool with true caught src/numbers.rs:6:5: replace is_double -> bool with false caught src/numbers.rs:6:7: replace == with != in is_double caught src/numbers.rs:6:12: replace * with + in is_double caught src/numbers.rs:6:12: replace * with / in is_double caught src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 caught src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 caught src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 caught src/numbers.rs:10:5: delete - in negate_i32 caught src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 caught src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 caught src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 caught src/numbers.rs:14:5: delete - in negate_f32 caught src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 caught src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 caught src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 caught src/numbers.rs:18:5: delete ! in bitwise_not_i32 caught src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 caught src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 caught src/numbers.rs:22:5: delete ! in bitwise_not_u32 caught src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") caught src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") caught src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) caught src/result.rs:10:10: replace < with == in error_if_negative caught src/result.rs:10:10: replace < with > in error_if_negative caught src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) caught src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() caught src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) caught src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) caught src/simple_fns.rs:8:5: replace returns_unit with () caught src/simple_fns.rs:8:8: replace += with -= in returns_unit caught src/simple_fns.rs:8:8: replace += with *= in returns_unit caught src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 0 caught src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 1 caught src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true caught src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false caught src/simple_fns.rs:18:11: replace == with != in divisible_by_three caught src/simple_fns.rs:18:7: replace % with / in divisible_by_three caught src/simple_fns.rs:18:7: replace % with + in divisible_by_three caught src/simple_fns.rs:27:5: replace double_string -> String with String::new() caught src/simple_fns.rs:27:5: replace double_string -> String with "xyzzy".into() caught src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) caught src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) caught src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) caught src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) caught src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) caught src/slices.rs:5:20: replace < with == in pad caught src/slices.rs:5:20: replace < with > in pad caught src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) caught src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) caught src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) caught src/slices.rs:14:12: replace *= with += in return_mut_slice caught src/slices.rs:14:12: replace *= with /= in return_mut_slice caught src/static_item.rs:1:33: replace == with != caught src/static_item.rs:1:39: replace + with - caught src/static_item.rs:1:39: replace + with * caught src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 caught src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 caught src/traits.rs:5:9: replace Something::is_three -> bool with true caught src/traits.rs:5:9: replace Something::is_three -> bool with false caught src/traits.rs:5:11: replace == with != in Something::is_three 94 mutants tested: 94 caught cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_finds_no_problems__caught.txt.snap000064400000000000000000000136431046102023000313410ustar 00000000000000--- source: tests/main.rs expression: content --- src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) src/booleans.rs:2:5: replace and -> bool with true src/booleans.rs:2:5: replace and -> bool with false src/booleans.rs:2:7: replace && with || in and src/booleans.rs:6:5: replace or -> bool with true src/booleans.rs:6:5: replace or -> bool with false src/booleans.rs:6:7: replace || with && in or src/booleans.rs:10:5: replace xor -> bool with true src/booleans.rs:10:5: replace xor -> bool with false src/booleans.rs:10:7: replace ^ with | in xor src/booleans.rs:10:7: replace ^ with & in xor src/booleans.rs:14:5: replace not -> bool with true src/booleans.rs:14:5: replace not -> bool with false src/booleans.rs:14:5: delete ! in not src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/methods.rs:17:9: replace Foo::double with () src/methods.rs:17:16: replace *= with += in Foo::double src/methods.rs:17:16: replace *= with /= in Foo::double src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/nested_function.rs:5:13: replace * with + in has_nested src/nested_function.rs:5:13: replace * with / in has_nested src/numbers.rs:2:5: replace double_float -> f32 with 0.0 src/numbers.rs:2:5: replace double_float -> f32 with 1.0 src/numbers.rs:2:5: replace double_float -> f32 with -1.0 src/numbers.rs:2:9: replace * with + in double_float src/numbers.rs:2:9: replace * with / in double_float src/numbers.rs:6:5: replace is_double -> bool with true src/numbers.rs:6:5: replace is_double -> bool with false src/numbers.rs:6:7: replace == with != in is_double src/numbers.rs:6:12: replace * with + in is_double src/numbers.rs:6:12: replace * with / in is_double src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 src/numbers.rs:10:5: delete - in negate_i32 src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 src/numbers.rs:14:5: delete - in negate_f32 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 src/numbers.rs:18:5: delete ! in bitwise_not_i32 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 src/numbers.rs:22:5: delete ! in bitwise_not_u32 src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:8:5: replace returns_unit with () src/simple_fns.rs:8:8: replace += with -= in returns_unit src/simple_fns.rs:8:8: replace += with *= in returns_unit src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 1 src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false src/simple_fns.rs:18:11: replace == with != in divisible_by_three src/simple_fns.rs:18:7: replace % with / in divisible_by_three src/simple_fns.rs:18:7: replace % with + in divisible_by_three src/simple_fns.rs:27:5: replace double_string -> String with String::new() src/simple_fns.rs:27:5: replace double_string -> String with "xyzzy".into() src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) src/slices.rs:14:12: replace *= with += in return_mut_slice src/slices.rs:14:12: replace *= with /= in return_mut_slice src/static_item.rs:1:33: replace == with != src/static_item.rs:1:39: replace + with - src/static_item.rs:1:39: replace + with * src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 src/traits.rs:5:9: replace Something::is_three -> bool with true src/traits.rs:5:9: replace Something::is_three -> bool with false src/traits.rs:5:11: replace == with != in Something::is_three cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_finds_no_problems__missed.txt.snap000064400000000000000000000000631046102023000313420ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_finds_no_problems__timeout.txt.snap000064400000000000000000000000631046102023000315440ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/snapshots/main__well_tested_tree_finds_no_problems__unviable.txt.snap000064400000000000000000000000631046102023000316630ustar 00000000000000--- source: tests/main.rs expression: content --- cargo-mutants-25.0.0/tests/trace.rs000064400000000000000000000011711046102023000153020ustar 00000000000000// Copyright 2023 Martin Pool //! Tests for trace from the cargo-mutants CLI. use predicates::prelude::*; mod util; use util::{copy_of_testdata, run}; #[test] fn env_var_controls_trace() { let tmp = copy_of_testdata("never_type"); run() .env("CARGO_MUTANTS_TRACE_LEVEL", "trace") .args(["mutants", "--list"]) .arg("-d") .arg(tmp.path()) .assert() // This is a debug!() message; it should only be seen if the trace var // was wired correctly to stderr. .stderr(predicate::str::contains( "No mutants generated for this return type", )); } cargo-mutants-25.0.0/tests/unix.rs000064400000000000000000000065531046102023000152000ustar 00000000000000#![cfg(unix)] use std::thread::sleep; use std::time::Duration; mod util; use util::{copy_of_testdata, MAIN_BINARY}; /// If the test hangs and the user (in this case the test suite) interrupts it, then /// the `cargo test` child should be killed. /// /// This is a bit hard to directly observe: the property that we really most care /// about is that _all_ grandchild processes are also killed and nothing is left /// behind. (On Unix, this is accomplished by use of a pgroup.) However that's a bit /// hard to mechanically check without reading and interpreting the process tree, which /// seems likely to be a bit annoying to do portably and without flakes. /// (But maybe we still should?) /// /// An easier thing to test is that the cargo-mutants process _thinks_ it has killed /// the children, and we can observe this in the debug log. /// /// In this test cargo-mutants has a very long timeout, but the test driver has a /// short timeout, so it should kill cargo-mutants. // TODO: An equivalent test on Windows? #[test] fn interrupt_caught_and_kills_children() { // Test a tree that has enough tests that we'll probably kill it before it completes. use std::process::{Command, Stdio}; use nix::libc::pid_t; use nix::sys::signal::{kill, SIGTERM}; use nix::unistd::Pid; let tmp_src_dir = copy_of_testdata("well_tested"); // We can't use `assert_cmd` `timeout` here because that sends the child a `SIGKILL`, // which doesn't give it a chance to clean up. And, `std::process::Command` only // has an abrupt kill. // Drop RUST_BACKTRACE because the traceback mentions "panic" handler functions // and we want to check that the process does not panic. // Skip baseline because firstly it should already pass but more importantly // #333 exhibited only during non-baseline scenarios. let args = [ MAIN_BINARY.to_str().unwrap(), "mutants", "--timeout=300", "--baseline=skip", "--level=trace", ]; println!("Running: {args:?}"); let mut child = Command::new(args[0]) .args(&args[1..]) .current_dir(&tmp_src_dir) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .env_remove("RUST_BACKTRACE") .spawn() .expect("spawn child"); sleep(Duration::from_secs(2)); // Let it get started assert!( child.try_wait().expect("try to wait for child").is_none(), "child exited early" ); println!("Sending SIGTERM to cargo-mutants..."); kill(Pid::from_raw(child.id() as pid_t), SIGTERM).expect("send SIGTERM"); println!("Wait for cargo-mutants to exit..."); let output = child .wait_with_output() .expect("wait for child after SIGTERM"); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); println!("stdout:\n{stdout}"); println!("stderr:\n{stderr}"); assert!(stderr.contains("interrupted")); // We used to look here for some other trace messages about how it's interrupted, but // that seems to be racy: sometimes the parent sees the child interrupted before it // emits these messages? Anyhow, it's not essential. // This shouldn't cause a panic though (#333) assert!(!stderr.contains("panic")); // And we don't want duplicate messages about workers failing. assert!(!stderr.contains("Worker thread failed")); } cargo-mutants-25.0.0/tests/util/mod.rs000064400000000000000000000124421046102023000157430ustar 00000000000000// Copyright 2024 Martin Pool #![allow(dead_code)] // rustc doesn't understand they're used by multiple crates //! Reusable utilities for cargo-mutants tests. //! //! This is available both to integration tests (by `mod util`) and unit tests inside //! cargo-mutants (as `use crate::test_util`). use std::borrow::Borrow; use std::env; use std::fs::{read_dir, read_to_string, rename}; use std::path::{Path, PathBuf}; use std::time::Duration; use itertools::Itertools; use lazy_static::lazy_static; use tempfile::TempDir; /// A timeout for a `cargo mutants` invocation from the test suite. Needs to be /// long enough that even commands that do a lot of work can pass even on slow /// CI VMs and even on Windows, but short enough that the test does not hang /// forever. pub const OUTER_TIMEOUT: Duration = Duration::from_secs(60); lazy_static! { pub static ref MAIN_BINARY: PathBuf = assert_cmd::cargo::cargo_bin("cargo-mutants"); } pub fn run() -> assert_cmd::Command { let mut cmd = assert_cmd::Command::new(MAIN_BINARY.as_os_str()); // Strip any options configured in the environment running these tests, // so that they don't cause unexpected behavior in the code under test. // // For example, without this, // `env CARGO_MUTANTS_JOBS=4 cargo mutants` // // would end up with tests running 4 jobs by default, which would cause // the tests to fail. // // Even more generally than that example, we want the tests to be as hermetic // as reasonably possible. env::vars() .map(|(k, _v)| k) .filter(|k| { k.starts_with("CARGO_MUTANTS_") || k == "CLICOLOR_FORCE" || k == "NOCOLOR" || k == "CARGO_TERM_COLOR" }) .for_each(|k| { cmd.env_remove(k); }); cmd } pub trait CommandInstaExt { fn assert_insta(&mut self, snapshot_name: &str); } impl CommandInstaExt for assert_cmd::Command { fn assert_insta(&mut self, snapshot_name: &str) { let output = self.output().expect("command completes"); assert!(output.status.success()); insta::assert_snapshot!(snapshot_name, String::from_utf8_lossy(&output.stdout)); assert_eq!(&String::from_utf8_lossy(&output.stderr), ""); } } // Copy the source for one testdata tree. pub fn copy_of_testdata(tree_name: &str) -> TempDir { assert!( !tree_name.contains('/'), "testdata tree name {tree_name:?} should be just the directory name" ); let tmp = TempDir::with_prefix(format!("cargo-mutants-testdata-{tree_name}-")).unwrap(); copy_testdata_to(tree_name, tmp.path()); tmp } pub fn copy_testdata_to>(tree_name: &str, dest: P) { let dest = dest.as_ref(); let mut renames = Vec::new(); cp_r::CopyOptions::new() .filter(|path, _stat| { Ok(["target", "mutants.out", "mutants.out.old"] .iter() .all(|p| !path.starts_with(p))) }) .after_entry_copied(|path, _file_type, _stats| { if path.ends_with("Cargo_test.toml") || path.ends_with(".cargo_test") { renames.push(dest.join(path)); } Ok(()) }) .copy_tree(Path::new("testdata").join(tree_name), dest) .unwrap(); for path in &renames { let new_name = path .file_name() .unwrap() .to_str() .unwrap() .replace("_test", ""); if let Err(err) = rename(path, path.parent().unwrap().join(new_name)) { panic!("failed to rename {path:?}: {err:?}") } } } /// Assert that some bytes, when parsed as json, equal a json value. pub fn assert_bytes_eq_json>(actual: &[u8], expected: J) { // The Borrow is so that you can pass either a value or a reference, for easier // calling. let actual_json = std::str::from_utf8(actual) .expect("bytes are UTF-8") .parse::() .expect("bytes can be parsed as JSON"); assert_eq!(&actual_json, expected.borrow()); } /// Return paths to all testdata trees, in order, excluding leftover git /// detritus with no Cargo.toml. pub fn all_testdata_tree_names() -> Vec { read_dir("testdata") .expect("list testdata") .map(|r| r.expect("read testdata dir entry")) .filter(|dir_entry| dir_entry.file_type().unwrap().is_dir()) .filter(|dir_entry| dir_entry.file_name() != "parse_fails") .filter(|dir_entry| { let dir_path = dir_entry.path(); dir_path.join("Cargo.toml").exists() || dir_path.join("Cargo_test.toml").exists() }) .map(|dir_entry| { dir_entry .file_name() .into_string() .expect("dir name is UTF-8") }) .sorted() .collect() } pub fn outcome_json(tmp_src_dir: &TempDir) -> serde_json::Value { read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")) .expect("read outcomes.json") .parse() .expect("parse outcomes.json") } pub fn outcome_json_counts(tmp_src_dir: &TempDir) -> serde_json::Value { let mut outcomes = outcome_json(tmp_src_dir); // We don't want to compare the detailed outcomes outcomes.as_object_mut().unwrap().remove("outcomes"); outcomes } cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_files_json_well_tested.snap000064400000000000000000000030401046102023000276640ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [ { "package": "cargo-mutants-testdata-well-tested", "path": "src/lib.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/arc.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/booleans.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/empty_fns.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/inside_mod.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/item_mod.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/methods.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/nested_function.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/numbers.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/result.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/sets.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/simple_fns.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/slices.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/static_item.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/struct_with_lifetime.rs" }, { "package": "cargo-mutants-testdata-well-tested", "path": "src/traits.rs" } ] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_files_text_well_tested.snap000064400000000000000000000005351046102023000277050ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/lib.rs src/arc.rs src/booleans.rs src/empty_fns.rs src/inside_mod.rs src/item_mod.rs src/methods.rs src/nested_function.rs src/numbers.rs src/result.rs src/sets.rs src/simple_fns.rs src/slices.rs src/static_item.rs src/struct_with_lifetime.rs src/traits.rs cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_cfg_attr_mutants_skip.snap000064400000000000000000000001311046102023000317670ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- ././@LongLink00006440000000000000000000000146000000000000007774Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_cfg_attr_mutants_skip_json.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_cfg_attr_mutants_skip_json.sna000064400000000000000000000001331046102023000326420ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_cfg_attr_test_skip.snap000064400000000000000000000004201046102023000312540ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/lib.rs:18:5: replace double -> usize with 0 src/lib.rs:18:5: replace double -> usize with 1 src/lib.rs:18:7: replace * with + in double src/lib.rs:18:7: replace * with / in double cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_cfg_attr_test_skip_json.snap000064400000000000000000000044131046102023000323130ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [ { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "0", "span": { "end": { "column": 10, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "1", "span": { "end": { "column": 10, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "+", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/lib.rs", "function": { "function_name": "double", "return_type": "-> usize", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-cfg-attr-test-skip", "replacement": "/", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } } ] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_factorial.snap000064400000000000000000000005631046102023000273520ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/bin/factorial.rs:2:5: replace main with () src/bin/factorial.rs:8:5: replace factorial -> u32 with 0 src/bin/factorial.rs:8:5: replace factorial -> u32 with 1 src/bin/factorial.rs:10:11: replace *= with += in factorial src/bin/factorial.rs:10:11: replace *= with /= in factorial cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_in_factorial_json.snap000064400000000000000000000054551046102023000304100ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [ { "file": "src/bin/factorial.rs", "function": { "function_name": "main", "return_type": "", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "()", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "0", "span": { "end": { "column": 6, "line": 12 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-factorial", "replacement": "1", "span": { "end": { "column": 6, "line": 12 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-factorial", "replacement": "+=", "span": { "end": { "column": 13, "line": 10 }, "start": { "column": 11, "line": 10 } } }, { "file": "src/bin/factorial.rs", "function": { "function_name": "factorial", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 13 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-factorial", "replacement": "/=", "span": { "end": { "column": 13, "line": 10 }, "start": { "column": 11, "line": 10 } } } ] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_json_well_tested.snap000064400000000000000000001463431046102023000302730ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [ { "file": "src/arc.rs", "function": { "function_name": "return_arc", "return_type": "-> Arc", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Arc::new(String::new())", "span": { "end": { "column": 37, "line": 4 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/arc.rs", "function": { "function_name": "return_arc", "return_type": "-> Arc", "span": { "end": { "column": 2, "line": 5 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Arc::new(\"xyzzy\".into())", "span": { "end": { "column": 37, "line": 4 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 11, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 11, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "and", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "||", "span": { "end": { "column": 9, "line": 2 }, "start": { "column": 7, "line": 2 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 11, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 11, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "or", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "&&", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 10, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "|", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "xor", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "&", "span": { "end": { "column": 8, "line": 10 }, "start": { "column": 7, "line": 10 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/booleans.rs", "function": { "function_name": "not", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/inside_mod.rs", "function": { "function_name": "outer::inner::name", "return_type": "-> &'static str", "span": { "end": { "column": 10, "line": 5 }, "start": { "column": 9, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"xyzzy\"", "span": { "end": { "column": 18, "line": 4 }, "start": { "column": 13, "line": 4 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "()", "span": { "end": { "column": 21, "line": 17 }, "start": { "column": 9, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+=", "span": { "end": { "column": 18, "line": 17 }, "start": { "column": 16, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "Foo::double", "return_type": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/=", "span": { "end": { "column": 18, "line": 17 }, "start": { "column": 16, "line": 17 } } }, { "file": "src/methods.rs", "function": { "function_name": "::fmt", "return_type": "-> fmt::Result", "span": { "end": { "column": 6, "line": 24 }, "start": { "column": 5, "line": 22 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 36, "line": 23 }, "start": { "column": 9, "line": 23 } } }, { "file": "src/methods.rs", "function": { "function_name": "::fmt", "return_type": "-> fmt::Result", "span": { "end": { "column": 6, "line": 30 }, "start": { "column": 5, "line": 28 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 37, "line": 29 }, "start": { "column": 9, "line": 29 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 22, "line": 5 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested::inner", "return_type": "-> u32", "span": { "end": { "column": 6, "line": 4 }, "start": { "column": 5, "line": 2 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 11, "line": 3 }, "start": { "column": 9, "line": 3 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/nested_function.rs", "function": { "function_name": "has_nested", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 6 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 14, "line": 5 }, "start": { "column": 13, "line": 5 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1.0", "span": { "end": { "column": 12, "line": 2 }, "start": { "column": 5, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 9, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "double_float", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 3 }, "start": { "column": 1, "line": 1 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 10, "line": 2 }, "start": { "column": 9, "line": 2 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 9, "line": 6 }, "start": { "column": 7, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 13, "line": 6 }, "start": { "column": 12, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "is_double", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 5 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 13, "line": 6 }, "start": { "column": 12, "line": 6 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1", "span": { "end": { "column": 7, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 11 }, "start": { "column": 1, "line": 9 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 10 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1.0", "span": { "end": { "column": 7, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "negate_f32", "return_type": "-> f32", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 13 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 14 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "-1", "span": { "end": { "column": 7, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_i32", "return_type": "-> i32", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/numbers.rs", "function": { "function_name": "bitwise_not_u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 23 }, "start": { "column": 1, "line": 21 } } }, "genre": "UnaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "", "span": { "end": { "column": 6, "line": 22 }, "start": { "column": 5, "line": 22 } } }, { "file": "src/result.rs", "function": { "function_name": "simple_result", "return_type": "-> Result<&'static str, ()>", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(\"\")", "span": { "end": { "column": 18, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/result.rs", "function": { "function_name": "simple_result", "return_type": "-> Result<&'static str, ()>", "span": { "end": { "column": 2, "line": 7 }, "start": { "column": 1, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(\"xyzzy\")", "span": { "end": { "column": 18, "line": 6 }, "start": { "column": 5, "line": 6 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(())", "span": { "end": { "column": 6, "line": 14 }, "start": { "column": 5, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "==", "span": { "end": { "column": 11, "line": 10 }, "start": { "column": 10, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "error_if_negative", "return_type": "-> Result<(), ()>", "span": { "end": { "column": 2, "line": 15 }, "start": { "column": 1, "line": 9 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": ">", "span": { "end": { "column": 11, "line": 10 }, "start": { "column": 10, "line": 10 } } }, { "file": "src/result.rs", "function": { "function_name": "result_with_no_apparent_type_args", "return_type": "-> std::fmt::Result", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 17 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Ok(Default::default())", "span": { "end": { "column": 28, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::new()", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::from_iter([String::new()])", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/sets.rs", "function": { "function_name": "make_a_set", "return_type": "-> BTreeSet", "span": { "end": { "column": 2, "line": 8 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "BTreeSet::from_iter([\"xyzzy\".into()])", "span": { "end": { "column": 6, "line": 7 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "()", "span": { "end": { "column": 13, "line": 8 }, "start": { "column": 5, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "-=", "span": { "end": { "column": 10, "line": 8 }, "start": { "column": 8, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_unit", "return_type": "", "span": { "end": { "column": 2, "line": 9 }, "start": { "column": 1, "line": 7 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "*=", "span": { "end": { "column": 10, "line": 8 }, "start": { "column": 8, "line": 8 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_42u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 14 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 7, "line": 13 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "returns_42u32", "return_type": "-> u32", "span": { "end": { "column": 2, "line": 14 }, "start": { "column": 1, "line": 11 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 7, "line": 13 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 13, "line": 18 }, "start": { "column": 11, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "double_string", "return_type": "-> String", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "String::new()", "span": { "end": { "column": 6, "line": 29 }, "start": { "column": 5, "line": 27 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "double_string", "return_type": "-> String", "span": { "end": { "column": 2, "line": 30 }, "start": { "column": 1, "line": 21 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "\"xyzzy\".into()", "span": { "end": { "column": 6, "line": 29 }, "start": { "column": 5, "line": 27 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(Vec::new())", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Borrowed(\"\")])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Owned(\"\".to_owned())])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Borrowed(\"xyzzy\")])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![Cow::Owned(\"xyzzy\".to_owned())])", "span": { "end": { "column": 7, "line": 9 }, "start": { "column": 5, "line": 4 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "==", "span": { "end": { "column": 21, "line": 5 }, "start": { "column": 20, "line": 5 } } }, { "file": "src/slices.rs", "function": { "function_name": "pad", "return_type": "-> &'a[Cow<'static, str>]", "span": { "end": { "column": 2, "line": 10 }, "start": { "column": 1, "line": 3 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": ">", "span": { "end": { "column": 21, "line": 5 }, "start": { "column": 20, "line": 5 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(Vec::new())", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![0])", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "Vec::leak(vec![1])", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 13 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+=", "span": { "end": { "column": 14, "line": 14 }, "start": { "column": 12, "line": 14 } } }, { "file": "src/slices.rs", "function": { "function_name": "return_mut_slice", "return_type": "-> &mut[usize]", "span": { "end": { "column": 2, "line": 17 }, "start": { "column": 1, "line": 12 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/=", "span": { "end": { "column": 14, "line": 14 }, "start": { "column": 12, "line": 14 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 35, "line": 1 }, "start": { "column": 33, "line": 1 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "-", "span": { "end": { "column": 40, "line": 1 }, "start": { "column": 39, "line": 1 } } }, { "file": "src/static_item.rs", "function": null, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "*", "span": { "end": { "column": 40, "line": 1 }, "start": { "column": 39, "line": 1 } } }, { "file": "src/struct_with_lifetime.rs", "function": { "function_name": "Lex<'buf>::buf_len", "return_type": "-> usize", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 14 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "0", "span": { "end": { "column": 23, "line": 15 }, "start": { "column": 9, "line": 15 } } }, { "file": "src/struct_with_lifetime.rs", "function": { "function_name": "Lex<'buf>::buf_len", "return_type": "-> usize", "span": { "end": { "column": 6, "line": 16 }, "start": { "column": 5, "line": 14 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "1", "span": { "end": { "column": 23, "line": 15 }, "start": { "column": 9, "line": 15 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 5 }, "start": { "column": 9, "line": 5 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "false", "span": { "end": { "column": 15, "line": 5 }, "start": { "column": 9, "line": 5 } } }, { "file": "src/traits.rs", "function": { "function_name": "Something::is_three", "return_type": "-> bool", "span": { "end": { "column": 6, "line": 6 }, "start": { "column": 5, "line": 4 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 13, "line": 5 }, "start": { "column": 11, "line": 5 } } } ] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_regex_filters.snap000064400000000000000000000006431046102023000275610ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false src/simple_fns.rs:18:11: replace == with != in divisible_by_three src/simple_fns.rs:18:7: replace % with / in divisible_by_three src/simple_fns.rs:18:7: replace % with + in divisible_by_three cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_regex_filters_json.snap000064400000000000000000000045031046102023000306110ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- [ { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "FnValue", "package": "cargo-mutants-testdata-well-tested", "replacement": "true", "span": { "end": { "column": 15, "line": 18 }, "start": { "column": 5, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "!=", "span": { "end": { "column": 13, "line": 18 }, "start": { "column": 11, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "/", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } }, { "file": "src/simple_fns.rs", "function": { "function_name": "divisible_by_three", "return_type": "-> bool", "span": { "end": { "column": 2, "line": 19 }, "start": { "column": 1, "line": 16 } } }, "genre": "BinaryOperator", "package": "cargo-mutants-testdata-well-tested", "replacement": "+", "span": { "end": { "column": 8, "line": 18 }, "start": { "column": 7, "line": 18 } } } ] cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested.snap000064400000000000000000000137111046102023000272320ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) src/booleans.rs:2:5: replace and -> bool with true src/booleans.rs:2:5: replace and -> bool with false src/booleans.rs:2:7: replace && with || in and src/booleans.rs:6:5: replace or -> bool with true src/booleans.rs:6:5: replace or -> bool with false src/booleans.rs:6:7: replace || with && in or src/booleans.rs:10:5: replace xor -> bool with true src/booleans.rs:10:5: replace xor -> bool with false src/booleans.rs:10:7: replace ^ with | in xor src/booleans.rs:10:7: replace ^ with & in xor src/booleans.rs:14:5: replace not -> bool with true src/booleans.rs:14:5: replace not -> bool with false src/booleans.rs:14:5: delete ! in not src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/methods.rs:17:9: replace Foo::double with () src/methods.rs:17:16: replace *= with += in Foo::double src/methods.rs:17:16: replace *= with /= in Foo::double src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/nested_function.rs:5:13: replace * with + in has_nested src/nested_function.rs:5:13: replace * with / in has_nested src/numbers.rs:2:5: replace double_float -> f32 with 0.0 src/numbers.rs:2:5: replace double_float -> f32 with 1.0 src/numbers.rs:2:5: replace double_float -> f32 with -1.0 src/numbers.rs:2:9: replace * with + in double_float src/numbers.rs:2:9: replace * with / in double_float src/numbers.rs:6:5: replace is_double -> bool with true src/numbers.rs:6:5: replace is_double -> bool with false src/numbers.rs:6:7: replace == with != in is_double src/numbers.rs:6:12: replace * with + in is_double src/numbers.rs:6:12: replace * with / in is_double src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 src/numbers.rs:10:5: delete - in negate_i32 src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 src/numbers.rs:14:5: delete - in negate_f32 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 src/numbers.rs:18:5: delete ! in bitwise_not_i32 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 src/numbers.rs:22:5: delete ! in bitwise_not_u32 src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/simple_fns.rs:8:5: replace returns_unit with () src/simple_fns.rs:8:8: replace += with -= in returns_unit src/simple_fns.rs:8:8: replace += with *= in returns_unit src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 0 src/simple_fns.rs:13:5: replace returns_42u32 -> u32 with 1 src/simple_fns.rs:18:5: replace divisible_by_three -> bool with true src/simple_fns.rs:18:5: replace divisible_by_three -> bool with false src/simple_fns.rs:18:11: replace == with != in divisible_by_three src/simple_fns.rs:18:7: replace % with / in divisible_by_three src/simple_fns.rs:18:7: replace % with + in divisible_by_three src/simple_fns.rs:27:5: replace double_string -> String with String::new() src/simple_fns.rs:27:5: replace double_string -> String with "xyzzy".into() src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) src/slices.rs:14:12: replace *= with += in return_mut_slice src/slices.rs:14:12: replace *= with /= in return_mut_slice src/static_item.rs:1:33: replace == with != src/static_item.rs:1:39: replace + with - src/static_item.rs:1:39: replace + with * src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 src/traits.rs:5:9: replace Something::is_three -> bool with true src/traits.rs:5:9: replace Something::is_three -> bool with false src/traits.rs:5:11: replace == with != in Something::is_three ././@LongLink00006440000000000000000000000175000000000000007776Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_examine_and_exclude_name_filter_combined.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_examine_and_exclude_n000064400000000000000000000015071046102023000326500ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/module/utils/sub_utils/subutils_nested_function.rs:2:5: replace has_nested -> u32 with 0 src/module/utils/sub_utils/subutils_nested_function.rs:2:5: replace has_nested -> u32 with 1 src/module/utils/sub_utils/subutils_nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/module/utils/sub_utils/subutils_nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/module/utils/sub_utils/subutils_nested_function.rs:5:13: replace * with + in has_nested src/module/utils/sub_utils/subutils_nested_function.rs:5:13: replace * with / in has_nested ././@LongLink00006440000000000000000000000150000000000000007767Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_examine_name_filter.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_examine_name_filter.s000064400000000000000000000007221046102023000326040ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/nested_function.rs:5:13: replace * with + in has_nested src/nested_function.rs:5:13: replace * with / in has_nested ././@LongLink00006440000000000000000000000152000000000000007771Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_exclude_folder_filter.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_exclude_folder_filter000064400000000000000000000004341046102023000327010ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/methods.rs:2:5: replace double -> usize with 0 src/methods.rs:2:5: replace double -> usize with 1 src/methods.rs:2:7: replace * with + in double src/methods.rs:2:7: replace * with / in double ././@LongLink00006440000000000000000000000150000000000000007767Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_exclude_name_filter.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_exclude_name_filter.s000064400000000000000000000123071046102023000326110ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/arc.rs:4:5: replace return_arc -> Arc with Arc::new(String::new()) src/arc.rs:4:5: replace return_arc -> Arc with Arc::new("xyzzy".into()) src/booleans.rs:2:5: replace and -> bool with true src/booleans.rs:2:5: replace and -> bool with false src/booleans.rs:2:7: replace && with || in and src/booleans.rs:6:5: replace or -> bool with true src/booleans.rs:6:5: replace or -> bool with false src/booleans.rs:6:7: replace || with && in or src/booleans.rs:10:5: replace xor -> bool with true src/booleans.rs:10:5: replace xor -> bool with false src/booleans.rs:10:7: replace ^ with | in xor src/booleans.rs:10:7: replace ^ with & in xor src/booleans.rs:14:5: replace not -> bool with true src/booleans.rs:14:5: replace not -> bool with false src/booleans.rs:14:5: delete ! in not src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" src/methods.rs:17:9: replace Foo::double with () src/methods.rs:17:16: replace *= with += in Foo::double src/methods.rs:17:16: replace *= with /= in Foo::double src/methods.rs:23:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/methods.rs:29:9: replace ::fmt -> fmt::Result with Ok(Default::default()) src/nested_function.rs:2:5: replace has_nested -> u32 with 0 src/nested_function.rs:2:5: replace has_nested -> u32 with 1 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 0 src/nested_function.rs:3:9: replace has_nested::inner -> u32 with 1 src/nested_function.rs:5:13: replace * with + in has_nested src/nested_function.rs:5:13: replace * with / in has_nested src/numbers.rs:2:5: replace double_float -> f32 with 0.0 src/numbers.rs:2:5: replace double_float -> f32 with 1.0 src/numbers.rs:2:5: replace double_float -> f32 with -1.0 src/numbers.rs:2:9: replace * with + in double_float src/numbers.rs:2:9: replace * with / in double_float src/numbers.rs:6:5: replace is_double -> bool with true src/numbers.rs:6:5: replace is_double -> bool with false src/numbers.rs:6:7: replace == with != in is_double src/numbers.rs:6:12: replace * with + in is_double src/numbers.rs:6:12: replace * with / in is_double src/numbers.rs:10:5: replace negate_i32 -> i32 with 0 src/numbers.rs:10:5: replace negate_i32 -> i32 with 1 src/numbers.rs:10:5: replace negate_i32 -> i32 with -1 src/numbers.rs:10:5: delete - in negate_i32 src/numbers.rs:14:5: replace negate_f32 -> f32 with 0.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with 1.0 src/numbers.rs:14:5: replace negate_f32 -> f32 with -1.0 src/numbers.rs:14:5: delete - in negate_f32 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 0 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with 1 src/numbers.rs:18:5: replace bitwise_not_i32 -> i32 with -1 src/numbers.rs:18:5: delete ! in bitwise_not_i32 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 0 src/numbers.rs:22:5: replace bitwise_not_u32 -> u32 with 1 src/numbers.rs:22:5: delete ! in bitwise_not_u32 src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("") src/result.rs:6:5: replace simple_result -> Result<&'static str, ()> with Ok("xyzzy") src/result.rs:10:5: replace error_if_negative -> Result<(), ()> with Ok(()) src/result.rs:10:10: replace < with == in error_if_negative src/result.rs:10:10: replace < with > in error_if_negative src/result.rs:18:5: replace result_with_no_apparent_type_args -> std::fmt::Result with Ok(Default::default()) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::new() src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter([String::new()]) src/sets.rs:4:5: replace make_a_set -> BTreeSet with BTreeSet::from_iter(["xyzzy".into()]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(Vec::new()) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("".to_owned())]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Borrowed("xyzzy")]) src/slices.rs:4:5: replace pad -> &'a[Cow<'static, str>] with Vec::leak(vec![Cow::Owned("xyzzy".to_owned())]) src/slices.rs:5:20: replace < with == in pad src/slices.rs:5:20: replace < with > in pad src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(Vec::new()) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![0]) src/slices.rs:13:5: replace return_mut_slice -> &mut[usize] with Vec::leak(vec![1]) src/slices.rs:14:12: replace *= with += in return_mut_slice src/slices.rs:14:12: replace *= with /= in return_mut_slice src/static_item.rs:1:33: replace == with != src/static_item.rs:1:39: replace + with - src/static_item.rs:1:39: replace + with * src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 0 src/struct_with_lifetime.rs:15:9: replace Lex<'buf>::buf_len -> usize with 1 src/traits.rs:5:9: replace Something::is_three -> bool with true src/traits.rs:5:9: replace Something::is_three -> bool with false src/traits.rs:5:11: replace == with != in Something::is_three ././@LongLink00006440000000000000000000000224000000000000007771Lustar cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_multiple_examine_and_exclude_name_filter_with_files_and_folders.snapcargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_well_tested_multiple_examine_and_000064400000000000000000000010111046102023000326620ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/module/module_methods.rs:2:5: replace double -> usize with 0 src/module/module_methods.rs:2:5: replace double -> usize with 1 src/module/module_methods.rs:2:7: replace * with + in double src/module/module_methods.rs:2:7: replace * with / in double src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "" src/module/utils/inside_mod.rs:4:13: replace outer::inner::name -> &'static str with "xyzzy" cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_with_diffs_in_factorial.snap000064400000000000000000000051751046102023000315640ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/bin/factorial.rs:2:5: replace main with () --- src/bin/factorial.rs +++ replace main with () @@ -1,12 +1,10 @@ fn main() { - for i in 1..=6 { - println!("{}! = {}", i, factorial(i)); - } + () /* ~ changed by cargo-mutants ~ */ } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { a *= i; } a src/bin/factorial.rs:8:5: replace factorial -> u32 with 0 --- src/bin/factorial.rs +++ replace factorial -> u32 with 0 @@ -1,19 +1,15 @@ fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { - let mut a = 1; - for i in 2..=n { - a *= i; - } - a + 0 /* ~ changed by cargo-mutants ~ */ } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } src/bin/factorial.rs:8:5: replace factorial -> u32 with 1 --- src/bin/factorial.rs +++ replace factorial -> u32 with 1 @@ -1,19 +1,15 @@ fn main() { for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { - let mut a = 1; - for i in 2..=n { - a *= i; - } - a + 1 /* ~ changed by cargo-mutants ~ */ } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); } src/bin/factorial.rs:10:11: replace *= with += in factorial --- src/bin/factorial.rs +++ replace *= with += in factorial @@ -2,17 +2,17 @@ for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { - a *= i; + a += /* ~ changed by cargo-mutants ~ */ i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); src/bin/factorial.rs:10:11: replace *= with /= in factorial --- src/bin/factorial.rs +++ replace *= with /= in factorial @@ -2,17 +2,17 @@ for i in 1..=6 { println!("{}! = {}", i, factorial(i)); } } fn factorial(n: u32) -> u32 { let mut a = 1; for i in 2..=n { - a *= i; + a /= /* ~ changed by cargo-mutants ~ */ i; } a } #[test] fn test_factorial() { println!("factorial({}) = {}", 6, factorial(6)); // This line is here so we can see it in --nocapture assert_eq!(factorial(6), 720); cargo-mutants-25.0.0/tests/util/snapshots/list__util__list_mutants_with_dir_option.snap000064400000000000000000000005631046102023000301210ustar 00000000000000--- source: tests/util/mod.rs expression: "String::from_utf8_lossy(&output.stdout)" --- src/bin/factorial.rs:2:5: replace main with () src/bin/factorial.rs:8:5: replace factorial -> u32 with 0 src/bin/factorial.rs:8:5: replace factorial -> u32 with 1 src/bin/factorial.rs:10:11: replace *= with += in factorial src/bin/factorial.rs:10:11: replace *= with /= in factorial cargo-mutants-25.0.0/tests/windows.rs000064400000000000000000000014621046102023000157010ustar 00000000000000// Copyright 2021-2024 Martin Pool #![cfg(windows)] //! Windows-only CLI tests. use predicates::prelude::*; mod util; use util::{copy_of_testdata, run}; /// Only on Windows, backslash can be used as a path separator in filters. #[test] fn list_mutants_well_tested_exclude_folder_containing_backslash_on_windows() { // This could be written more simply as `--exclude module` but we want to // test that backslash is accepted. let tmp = copy_of_testdata("with_child_directories"); run() .arg("mutants") .args(["--list", "--exclude", "**\\module\\**\\*.rs"]) .current_dir(tmp.path()) .assert() .stdout( predicates::str::contains(r"src/module") .not() .and(predicates::str::contains(r"src/methods.rs")), ); } cargo-mutants-25.0.0/tests/workspace.rs000064400000000000000000000305221046102023000162040ustar 00000000000000// Copyright 2023-2025 Martin Pool //! Tests for cargo workspaces with multiple packages. use std::fs::{self, create_dir, read_to_string, write}; use insta::assert_snapshot; use itertools::Itertools; use predicates::prelude::predicate; use pretty_assertions::assert_eq; use serde_json::json; mod util; use util::{assert_bytes_eq_json, copy_of_testdata, run}; #[test] fn open_by_manifest_path() { let tmp = copy_of_testdata("factorial"); run() .args(["mutants", "--list", "--line-col=false", "--manifest-path"]) .arg(tmp.path().join("Cargo.toml")) .assert() .success() .stdout(predicates::str::contains( "src/bin/factorial.rs: replace main with ()", )); } #[test] fn list_warns_about_unmatched_packages() { run() .args([ "mutants", "--list", "-d", "testdata/workspace", "-p", "notapackage", ]) .assert() .stderr(predicates::str::contains( "Package \"notapackage\" not found in source tree", )) .code(0); } #[test] fn list_files_json_workspace() { // Demonstrates that we get package names in the json listing. let tmp = copy_of_testdata("workspace"); let cmd = run() .args(["mutants", "--list-files", "--json"]) .current_dir(tmp.path()) .assert() .success(); assert_bytes_eq_json( &cmd.get_output().stdout, json! { [ { "package": "cargo_mutants_testdata_workspace_utils", "path": "utils/src/lib.rs" }, { "package": "main", "path": "main/src/main.rs" }, { "package": "main2", "path": "main2/src/main.rs" } ] }, ); } #[test] fn list_files_as_json_in_workspace_subdir() { let tmp = copy_of_testdata("workspace"); let cmd = run() .args(["mutants", "--list-files", "--json", "--workspace"]) .current_dir(tmp.path().join("main2")) .assert() .success(); assert_bytes_eq_json( &cmd.get_output().stdout, json! { [ { "package": "cargo_mutants_testdata_workspace_utils", "path": "utils/src/lib.rs" }, { "package": "main", "path": "main/src/main.rs" }, { "package": "main2", "path": "main2/src/main.rs" } ] }, ); } #[test] fn workspace_tree_is_well_tested() { let tmp_src_dir = copy_of_testdata("workspace"); run() .args(["mutants", "-d"]) .arg(tmp_src_dir.path()) .assert() .success(); // The outcomes.json has some summary data let json_str = fs::read_to_string(tmp_src_dir.path().join("mutants.out/outcomes.json")).unwrap(); println!("outcomes.json:\n{json_str}"); let json: serde_json::Value = json_str.parse().unwrap(); let total = json["total_mutants"].as_u64().unwrap(); assert!(total > 8); assert_eq!(json["caught"].as_u64().unwrap(), total); assert_eq!(json["missed"].as_u64().unwrap(), 0); assert_eq!(json["timeout"].as_u64().unwrap(), 0); let outcomes = json["outcomes"].as_array().unwrap(); { let baseline = outcomes[0].as_object().unwrap(); assert_eq!(baseline["scenario"].as_str().unwrap(), "Baseline"); assert_eq!(baseline["summary"], "Success"); let baseline_phases = baseline["phase_results"].as_array().unwrap(); assert_eq!(baseline_phases.len(), 2); assert_eq!(baseline_phases[0]["process_status"], "Success"); assert_eq!( baseline_phases[0]["argv"] .as_array() .unwrap() .iter() .map(|v| v.as_str().unwrap()) .skip(1) .collect_vec(), [ "test", "--no-run", "--verbose", "--package=cargo_mutants_testdata_workspace_utils@0.1.0", "--package=main@0.1.0", "--package=main2@0.1.0" ] ); assert_eq!(baseline_phases[1]["process_status"], "Success"); assert_eq!( baseline_phases[1]["argv"] .as_array() .unwrap() .iter() .map(|v| v.as_str().unwrap()) .skip(1) .collect_vec(), [ "test", "--verbose", "--package=cargo_mutants_testdata_workspace_utils@0.1.0", "--package=main@0.1.0", "--package=main2@0.1.0" ], ); } assert!(outcomes.len() > 9); for outcome in &outcomes[1..] { let mutant = &outcome["scenario"]["Mutant"]; let package_name = mutant["package"].as_str().unwrap(); assert!(!package_name.is_empty()); assert_eq!(outcome["summary"], "CaughtMutant"); let mutant_phases = outcome["phase_results"].as_array().unwrap(); assert_eq!(mutant_phases.len(), 2); assert_eq!(mutant_phases[0]["process_status"], "Success"); assert_eq!( mutant_phases[0]["argv"].as_array().unwrap()[1..=2], ["test", "--no-run"] ); assert_eq!(mutant_phases[1]["process_status"], json!({"Failure": 101})); assert_eq!( mutant_phases[1]["argv"].as_array().unwrap()[1..=2], ["test", "--verbose"], ); } { let baseline = json["outcomes"][0].as_object().unwrap(); assert_eq!(baseline["scenario"].as_str().unwrap(), "Baseline"); assert_eq!(baseline["summary"], "Success"); let baseline_phases = baseline["phase_results"].as_array().unwrap(); assert_eq!(baseline_phases.len(), 2); assert_eq!(baseline_phases[0]["process_status"], "Success"); assert_eq!( baseline_phases[0]["argv"].as_array().unwrap()[1..] .iter() .map(|v| v.as_str().unwrap()) .collect_vec(), [ "test", "--no-run", "--verbose", "--package=cargo_mutants_testdata_workspace_utils@0.1.0", "--package=main@0.1.0", "--package=main2@0.1.0" ], ); assert_eq!(baseline_phases[1]["process_status"], "Success"); assert_eq!( baseline_phases[1]["argv"].as_array().unwrap()[1..] .iter() .map(|v| v.as_str().unwrap()) .collect_vec(), [ "test", "--verbose", "--package=cargo_mutants_testdata_workspace_utils@0.1.0", "--package=main@0.1.0", "--package=main2@0.1.0" ], ); } } #[test] /// Baseline tests in a workspace only test the packages that will later /// be mutated. /// See fn in_workspace_only_relevant_packages_included_in_baseline_tests_by_file_filter() { let tmp = copy_of_testdata("package_fails"); run() .args(["mutants", "-f", "passing/src/lib.rs", "--no-shuffle", "-d"]) .arg(tmp.path()) .assert() .success(); assert_snapshot!( read_to_string(tmp.path().join("mutants.out/caught.txt")).unwrap(), @r###" passing/src/lib.rs:2:5: replace triple -> usize with 0 passing/src/lib.rs:2:5: replace triple -> usize with 1 passing/src/lib.rs:2:7: replace * with + in triple passing/src/lib.rs:2:7: replace * with / in triple "###); assert_eq!( read_to_string(tmp.path().join("mutants.out/timeout.txt")).unwrap(), "" ); assert_eq!( read_to_string(tmp.path().join("mutants.out/missed.txt")).unwrap(), "" ); assert_eq!( read_to_string(tmp.path().join("mutants.out/unviable.txt")).unwrap(), "" ); } /// Even the baseline test only tests the explicitly selected packages, /// so it doesn't fail if some packages don't build. #[test] fn baseline_test_respects_package_options() { let tmp = copy_of_testdata("package_fails"); run() .args([ "mutants", "--package", "cargo-mutants-testdata-package-fails-passing", "--no-shuffle", "-d", ]) .arg(tmp.path()) .assert() .success(); assert_snapshot!( read_to_string(tmp.path().join("mutants.out/caught.txt")).unwrap(), @r###" passing/src/lib.rs:2:5: replace triple -> usize with 0 passing/src/lib.rs:2:5: replace triple -> usize with 1 passing/src/lib.rs:2:7: replace * with + in triple passing/src/lib.rs:2:7: replace * with / in triple "### ); assert_eq!( read_to_string(tmp.path().join("mutants.out/timeout.txt")).unwrap(), "" ); assert_eq!( read_to_string(tmp.path().join("mutants.out/missed.txt")).unwrap(), "" ); assert_eq!( read_to_string(tmp.path().join("mutants.out/unviable.txt")).unwrap(), "" ); } #[test] fn cross_package_tests() { // This workspace has two packages, one of which contains the tests. // Mutating the one with no tests will find test gaps, but // either testing the whole workspace, or naming the test package, // will show that it's actually all well tested. // // let tmp = copy_of_testdata("cross_package_tests"); let path = tmp.path(); // Testing only this one package will find gaps. run() .args(["mutants", "-v", "--shard=0/4"]) .arg("--no-times") .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 mutant tested: 1 missed")) .code(2); // missed mutants // Just asking to *mutate* the whole workspace will not cause us // to run the tests in "tests" against mutants in "lib". run() .args(["mutants", "--workspace", "--shard=0/4"]) .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 missed")) .code(2); // missed mutants // Similarly, starting in the workspace dir is not enough. run() .args(["mutants", "-v", "--shard=0/4"]) .arg("-d") .arg(path) .assert() .stdout(predicate::str::contains("1 missed")) .code(2); // missed mutants // Testing the whole workspace does catch everything. run() .args(["mutants", "--test-workspace=true", "--shard=0/4"]) .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 caught")) .code(0); // And naming the test package also catches everything. run() .args([ "mutants", "--test-package=cargo-mutants-testdata-cross-package-tests-tests", "--shard=0/4", ]) .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 caught")) .code(0); // Using the wrong package name gives a warning run() .args(["mutants", "--test-package=tests"]) .arg("-d") .arg(path.join("lib")) .env_remove("RUST_BACKTRACE") .assert() .stderr(predicate::str::contains( "WARN Package \"tests\" not found in source tree\n", )) .success(); // You can configure the test package in the workspace let cargo_dir = path.join(".cargo"); create_dir(&cargo_dir).unwrap(); let mutants_toml_path = cargo_dir.join("mutants.toml"); write(&mutants_toml_path, b"test_workspace = true").unwrap(); // Now the mutants are caught run() .args(["mutants"]) .arg("--shard=0/4") .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 caught")) .code(0); // It would also work to name the test package write( &mutants_toml_path, br#"test_package = ["cargo-mutants-testdata-cross-package-tests-tests"]"#, ) .unwrap(); run() .args(["mutants"]) .arg("--shard=0/4") .arg("-d") .arg(path.join("lib")) .assert() .stdout(predicate::str::contains("1 caught")) .code(0); }