diff --git a/stacks-core/README.md b/stacks-core/README.md
index e095e30..5539b35 100644
--- a/stacks-core/README.md
+++ b/stacks-core/README.md
@@ -5,3 +5,4 @@ Folder of composite actions for the [stacks blockchain](https://github.com/stack
- [cache](./cache) - Various cache operations
- [testenv](./testenv) - Configures a workflow with a testing environment
- [run-tests](./run-tests) - Run tests with [nextest](https://nexte.st)
+- [mutation-testing](./mutation-testing) - Run mutation testing
diff --git a/stacks-core/mutation-testing/README.md b/stacks-core/mutation-testing/README.md
new file mode 100644
index 0000000..ff63e22
--- /dev/null
+++ b/stacks-core/mutation-testing/README.md
@@ -0,0 +1,5 @@
+# Mutation Testing actions
+
+> - [Check Packages and Shards action](./check-packages-and-shards/README.md)
+> - [Output PR Mutants action](./output-pr-mutants/README.md)
+> - [PR Differences action](./pr-differences/README.md)
diff --git a/stacks-core/mutation-testing/check-packages-and-shards/README.md b/stacks-core/mutation-testing/check-packages-and-shards/README.md
new file mode 100644
index 0000000..ff30718
--- /dev/null
+++ b/stacks-core/mutation-testing/check-packages-and-shards/README.md
@@ -0,0 +1,28 @@
+# Check Packages and Shards action
+
+Checks whether to run mutants on big ([stackslib](https://github.com/stacks-network/stacks-core/tree/develop/stackslib)/[stacks-node](https://github.com/stacks-network/stacks-core/tree/develop/testnet/stacks-node)) or small packages (all others), and whether to run them using strategy matrix or not.
+
+## Documentation
+
+### Outputs
+| Output | Description |
+| ------------------------------- | ----------------------------------------------------- |
+| `run_big_packages` | True if there are mutants on `stackslib` or `stacks-node` packages, false otherwise.
+| `big_packages_with_shards` | True if there are more than 15 mutants on big packages.
+| `run_small_packages` | True if there are mutants on small packages, false otherwise.
+| `small_packages_with_shards` | True if there are more than 79 (119 if `big_packages_with_shards` is true) mutants on small packages.
+
+## Usage
+
+```yaml
+name: Action
+on: pull-request
+jobs:
+ check-packages-and-shards:
+ name: Check Packages and Shards
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check Packages and Shards
+ id: check_packages_and_shards
+ uses: stacks-network/actions/stacks-core/mutation-testing/check-packages-and-shards@main
+```
diff --git a/stacks-core/mutation-testing/check-packages-and-shards/action.yml b/stacks-core/mutation-testing/check-packages-and-shards/action.yml
new file mode 100644
index 0000000..342de18
--- /dev/null
+++ b/stacks-core/mutation-testing/check-packages-and-shards/action.yml
@@ -0,0 +1,168 @@
+name: Check Packages and Shards
+description: "Checks which types of packages are contained in the diffs and how many mutants there are"
+branding:
+ icon: "check"
+ color: "gray-dark"
+
+outputs:
+ run_big_packages:
+ description: "True if there are packages on `stackslib` or `stacks-node`."
+ value: ${{ steps.check_packages_and_shards.outputs.run_big_packages }}
+ big_packages_with_shards:
+ description: "True if there are more than 16 mutants on `stackslib` and `stacks-node`."
+ value: ${{ steps.check_packages_and_shards.outputs.big_packages_with_shards }}
+ run_small_packages:
+ description: "True if there are packages on other packages than `stackslib` or `stacks-node`."
+ value: ${{ steps.check_packages_and_shards.outputs.run_small_packages }}
+ small_packages_with_shards:
+ description: "True if there are more than 79 (119 if `big_packages_with_shards` is true) mutants on small packages."
+ value: ${{ steps.check_packages_and_shards.outputs.small_packages_with_shards }}
+
+runs:
+ using: "composite"
+
+ steps:
+ - name: Checkout stacks-core repo
+ id: checkout_stacks_core
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ fetch-depth: 0
+
+ - uses: taiki-e/install-action@ac89944b5b150d78567ab6c02badfbe48b0b55aa # v2.20.16
+ id: install_cargo_mutants
+ name: Install cargo-mutants
+ with:
+ tool: cargo-mutants@23.12.2 # v23.12.2
+
+ - name: Relative diff
+ id: relative_diff
+ shell: bash
+ run: |
+ git diff origin/${{ github.base_ref }}.. > git.diff
+
+ - name: Update git diff
+ id: update_git_diff
+ shell: bash
+ run: |
+ input_file="git.diff"
+ temp_file="temp_diff_file.diff"
+
+ # Check if the commands exist on the host
+ for cmd in tac awk sed; do
+ command -v "${cmd}" > /dev/null 2>&1 || { echo "Missing command: ${cmd}" && exit 1; }
+ done
+
+ # Check that the file exists before performing actions on it
+ if [ ! -s "$input_file" ]; then
+ echo "Diff file (git.diff) is missing or empty!"
+ exit 1
+ fi
+
+ # Reverse the file, remove 4 lines after '+++ /dev/null', then reverse it back (editors can't go backwards - to remove lines above)
+ tac "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+ sed '/+++ \/dev\/null/{n;N;N;N;d;}' "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+ tac "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+
+ # Remove the lines between '+++ /dev/null' (included) and 'diff --git a/'
+ awk '
+ BEGIN { in_block=0 }
+ /\+\+\+ \/dev\/null/ { in_block=1; next }
+ in_block && /diff --git a\// { in_block=0; print; next }
+ !in_block
+ ' "$input_file" > "$temp_file"
+
+ # Check that the file exists before performing actions on it
+ if [ -s "$temp_file" ]; then
+ mv "$temp_file" "$input_file"
+ exit 0
+ fi
+
+ echo "Temp diff file (temp_diff_file.diff) is missing or empty!"
+ exit 1
+
+ - name: Split diffs
+ id: split_diffs_by_packages
+ shell: bash
+ run: |
+ # Check that the file exists before performing actions on it
+ if [ ! -s git.diff ]; then
+ echo "Diff file (git.diff) is missing or empty!"
+ exit 1
+ fi
+
+ # Make a file containing all the mutants for the differences in the PR and a folder to split them into big and small packages
+ cargo mutants --in-diff git.diff --list > all_mutants.txt
+ if [ $? -ne 0 ]; then
+ echo "Error retrieving the list of mutants!"
+ exit $?
+ fi
+
+ mkdir -p mutants_by_packages
+
+ # Check that the file exists before performing actions on it
+ if [ ! -s all_mutants.txt ]; then
+ echo "The file containing mutants (all_mutants.txt) is missing or empty!"
+ exit 1
+ fi
+
+ # Split the differences from git into 2 parts, big packages ('stacks-node' and 'stackslib') and small packages (all others) and put them into separate files
+ while IFS= read -r line; do
+ package=$(echo "$line" | cut -d'/' -f1)
+ case $package in
+ "testnet" | "stackslib")
+ echo "$line" >> "mutants_by_packages/big_packages.txt"
+ ;;
+ *)
+ echo "$line" >> "mutants_by_packages/small_packages.txt"
+ ;;
+ esac
+ done < all_mutants.txt
+
+ exit 0
+
+ - name: Check packages and shards
+ id: check_packages_and_shards
+ shell: bash
+ run: |
+ number_of_big_mutants=0
+ number_of_small_mutants=0
+
+ # If big_packages file exists, count how many mutants there are
+ if [[ -s mutants_by_packages/big_packages.txt ]]; then
+ number_of_big_mutants=$(cat mutants_by_packages/big_packages.txt | awk 'END { print NR }' | tr -d '[:space:]')
+ fi
+
+ # If small_packages file exists, count how many mutants there are
+ if [[ -s mutants_by_packages/small_packages.txt ]]; then
+ number_of_small_mutants=$(cat mutants_by_packages/small_packages.txt | awk 'END { print NR }' | tr -d '[:space:]')
+ fi
+
+ # Set the mutants limit for when to run with shards on the small packages
+ # If there are mutants from big packages, check whether to run with or without shards, otherwise there's nothing to run
+ if [[ $number_of_big_mutants -gt 15 ]]; then
+ small_packages_shard_limit=119
+ echo "run_big_packages=true" >> "$GITHUB_OUTPUT"
+ echo "big_packages_with_shards=true" >> "$GITHUB_OUTPUT"
+ else
+ small_packages_shard_limit=79
+ if [[ $number_of_big_mutants -ne 0 ]]; then
+ echo "run_big_packages=true" >> "$GITHUB_OUTPUT"
+ echo "big_packages_with_shards=false" >> "$GITHUB_OUTPUT"
+ else
+ echo "run_big_packages=false" >> "$GITHUB_OUTPUT"
+ fi
+ fi
+
+ # If there are mutants from small packages, check whether to run with or without shards, otherwise there's nothing to run
+ if [[ $number_of_small_mutants -ne 0 ]]; then
+ echo "run_small_packages=true" >> "$GITHUB_OUTPUT"
+ if [[ $number_of_small_mutants -gt $small_packages_shard_limit ]]; then
+ echo "small_packages_with_shards=true" >> "$GITHUB_OUTPUT"
+ else
+ echo "small_packages_with_shards=false" >> "$GITHUB_OUTPUT"
+ fi
+ else
+ echo "run_small_packages=false" >> "$GITHUB_OUTPUT"
+ fi
+
+ exit 0
diff --git a/stacks-core/mutation-testing/output-pr-mutants/README.md b/stacks-core/mutation-testing/output-pr-mutants/README.md
new file mode 100644
index 0000000..7c8e85f
--- /dev/null
+++ b/stacks-core/mutation-testing/output-pr-mutants/README.md
@@ -0,0 +1,34 @@
+# Output PR Mutants action
+
+Write the mutants tested in the previous jobs of the same workflow to github step summary.
+
+## Documentation
+
+### Inputs
+
+| Input | Description | Required | Default |
+| ------------------------------- | ----------------------------------------------------- | ------------------------- | ------------------------- |
+| `big_packages` | True if there were big packages running | true | |
+| `shards_for_big_packages` | True if the big packages were running using matrix | true | |
+| `small_packages` | True if there were small packages running | true | |
+| `shards_for_small_packages` | True if the small packages were running using matrix | true | |
+
+## Usage
+
+```yaml
+name: Action
+on: push
+jobs:
+ check-packages-and-shards:
+ name: Output Mutants
+ runs-on: ubuntu-latest
+ steps:
+ - name: Output Mutants
+ id: output_pr_mutants
+ uses: stacks-network/actions/stacks-core/mutation-testing/output-pr-mutants@main
+ with:
+ big_packages: "true"
+ shards_for_big_packages: "false"
+ small_packages: "true"
+ shards_for_small_packages: "true"
+```
diff --git a/stacks-core/mutation-testing/output-pr-mutants/action.yml b/stacks-core/mutation-testing/output-pr-mutants/action.yml
new file mode 100644
index 0000000..0b68e51
--- /dev/null
+++ b/stacks-core/mutation-testing/output-pr-mutants/action.yml
@@ -0,0 +1,224 @@
+name: Output Mutants
+description: "Collects the outcomes of jobs running mutants from artifacts, combines them and outputs them to github step summary"
+branding:
+ icon: "share"
+ color: "gray-dark"
+
+inputs:
+ big_packages:
+ description: "True if there were big packages running."
+ required: true
+ shards_for_big_packages:
+ description: "True if the big packages were running using matrix."
+ required: true
+ small_packages:
+ description: "True if there were small packages running."
+ required: true
+ shards_for_small_packages:
+ description: "True if the small packages were running using matrix."
+ required: true
+
+runs:
+ using: "composite"
+
+ steps:
+ - name: Download artifacts
+ id: download_artifacts
+ uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
+
+ - name: Append output from shards
+ id: append_mutant_outcomes
+ shell: bash
+ run: |
+ # Check and append the folders that should be uploaded to artifacts for the big packages
+ if ${{ inputs.big_packages }} == "true"; then
+ if ${{ inputs.shards_for_big_packages }} == "true"; then
+ for i in {0..7}; do
+ folders+=("mutants-shard-big-$i")
+ done
+ else
+ folders+=("mutants-shard-big--1")
+ fi
+ fi
+
+ # Check and append the folders that should be uploaded to artifacts for the small packages
+ if ${{ inputs.small_packages }} == "true"; then
+ if ${{ inputs.shards_for_small_packages }} == "true"; then
+ for i in {0..3}; do
+ folders+=("mutants-shard-small-$i")
+ done
+ else
+ folders+=("mutants-shard-small--1")
+ fi
+ fi
+
+ files=("missed.txt" "caught.txt" "timeout.txt" "unviable.txt")
+ mkdir -p mutants-shards
+
+ # If the folder/file path exists, append the output to it's corresponding file name in a newly created folder that will contain all outputs combined
+ # If it doesn't exits, exit early because there was an error when uploading/downloading the artifact or the mutants were not run for that specific job
+ for file in "${files[@]}"; do
+ for folder in "${folders[@]}"; do
+ if [[ -f "$folder/$file" ]]; then
+ cat "$folder/$file" >> "mutants-shards/$file"
+ else
+ echo "Missing necessary file or folder: $folder/$file"
+ exit 1
+ fi
+ done
+ done
+
+ # If the folder/exit_code.txt path exists, check for the exit code and retain the most relevant one to a file
+ # - 4: unmutated build failed
+ # - 1: incorrect command line arguments
+ # - 2, 3: found timeout/missed/unviable mutants
+ # - 0: everything worked fine
+ # - *: unknown exit code
+ for folder in "${folders[@]}"; do
+ if [[ -s "$folder/exit_code.txt" ]]; then
+ exit_code=$(<"${folder}/exit_code.txt")
+ most_relevant_exit_code=0
+
+ case $exit_code in
+ 4)
+ most_relevant_exit_code=4
+ ;;
+ 1)
+ [ "$most_relevant_exit_code" -eq 0 ] && most_relevant_exit_code=1
+ ;;
+ 3)
+ [ "$most_relevant_exit_code" -eq 0 ] && most_relevant_exit_code=3
+ ;;
+ 2)
+ [ "$most_relevant_exit_code" -eq 0 ] && most_relevant_exit_code=2
+ ;;
+ 0)
+ ;;
+ *)
+ echo "Unknown exit code $exit_code"
+ most_relevant_exit_code=$exit_code
+ ;;
+ esac
+ else
+ # If the file containing the exit code doesn't exist, something happened to the job that should have saved it, so default to '4'
+ most_relevant_exit_code=4
+ fi
+ done
+
+ echo "$most_relevant_exit_code" > './mutants-shards/exit_code.txt'
+
+ exit 0
+
+ - name: Print mutants
+ id: print_tested_mutants
+ shell: bash
+ run: |
+ # Info for creating the link that paths to the specific mutation tested
+ server_url="${{ github.server_url }}"
+ organisation="${{ github.repository_owner }}"
+ repository="${{ github.event.repository.name }}"
+ commit="${{ github.sha }}"
+
+ # Function to write to github step summary with specific info depending on the mutation category
+ write_section() {
+ local section_title=$1
+ local file_name=$2
+
+ if [ -s "$file_name" ]; then
+ if [[ "$section_title" != "" ]]; then
+ echo "## $section_title" >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ if [[ "$section_title" == "Missed:" ]]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "What are missed mutants?
" >> "$GITHUB_STEP_SUMMARY"
+ echo "
" >> "$GITHUB_STEP_SUMMARY"
+ echo "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." >> "$GITHUB_STEP_SUMMARY"
+ echo " " >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$section_title" == "Timeout:" ]]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "What are timeout mutants?
" >> "$GITHUB_STEP_SUMMARY"
+ echo "
" >> "$GITHUB_STEP_SUMMARY"
+ echo "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." >> "$GITHUB_STEP_SUMMARY"
+ echo " " >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$section_title" == "Unviable:" ]]; then
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "What are unviable mutants?
" >> "$GITHUB_STEP_SUMMARY"
+ echo "
" >> "$GITHUB_STEP_SUMMARY"
+ echo "The attempted mutation doesn't compile. This is inconclusive about test coverage and no action is needed, unless you wish to test the specific function, in which case you may wish to add a 'Default::default()' implementation for the specific return type." >> "$GITHUB_STEP_SUMMARY"
+ echo " " >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ if [[ "$section_title" != "" ]]; then
+ awk -F':' '{printf "- [ ] " "[" $0 "]"; file_path=$1; line=$2; $1=""; $2=""; printf "(" "'"$server_url"'/'"$organisation"'/'"$repository"'/blob/'"$commit"'/" file_path "#L" line-1 ")\n\n"}' "$file_name" >> "$GITHUB_STEP_SUMMARY"
+ else
+ awk -F':' '{printf "- [x] " "[" $0 "]"; file_path=$1; line=$2; $1=""; $2=""; printf "(" "'"$server_url"'/'"$organisation"'/'"$repository"'/blob/'"$commit"'/" file_path "#L" line-1 ")\n\n"}' "$file_name" >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ if [[ "$section_title" == "Missed:" ]]; then
+ echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Modify or add tests including this function." >> "$GITHUB_STEP_SUMMARY"
+ echo "- If you are absolutely certain that this function should not undergo mutation testing, add '#[mutants::skip]' or '#[cfg_attr(test, mutants::skip)]' function header to skip it." >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$section_title" == "Timeout:" ]]; then
+ echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Modify the tests that include this funcion." >> "$GITHUB_STEP_SUMMARY"
+ echo "- Add '#[mutants::skip]' or '#[cfg_attr(test, mutants::skip)]' function header to skip it." >> "$GITHUB_STEP_SUMMARY"
+ elif [[ "$section_title" == "Unviable:" ]]; then
+ echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Create 'Default::default()' implementation for the specific structure." >> "$GITHUB_STEP_SUMMARY"
+ echo "- Add '#[mutants::skip]' or '#[cfg_attr(test, mutants::skip)]' function header to skip it." >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ echo >> "$GITHUB_STEP_SUMMARY"
+ fi
+ }
+
+ # Print uncaught (missed/timeout/unviable) mutants to summary
+ if [ -s ./mutants-shards/missed.txt -o -s ./mutants-shards/timeout.txt -o -s ./mutants-shards/unviable.txt ]; then
+ echo "# Uncaught Mutants" >> "$GITHUB_STEP_SUMMARY"
+ write_section "Missed:" "./mutants-shards/missed.txt"
+ write_section "Timeout:" "./mutants-shards/timeout.txt"
+ write_section "Unviable:" "./mutants-shards/unviable.txt"
+ fi
+
+ # Print caught mutants to summary
+ if [ -s ./mutants-shards/caught.txt ]; then
+ echo "# Caught Mutants" >> "$GITHUB_STEP_SUMMARY"
+ write_section "" "./mutants-shards/caught.txt"
+ fi
+
+ # Get most relevant exit code from the file and match it
+ exit_code=$(<"mutants-shards/exit_code.txt")
+
+ case $exit_code in
+ 0)
+ if [[ -f ./mutants-shards/unviable.txt ]]; then
+ echo "Found unviable mutants!"
+ exit 5
+ fi
+ echo "All new and updated functions are caught!"
+ ;;
+ 1)
+ echo "Invalid command line arguments!"
+ exit $exit_code
+ ;;
+ 2)
+ echo "Found missed mutants!"
+ exit $exit_code
+ ;;
+ 3)
+ echo "Found timeout mutants!"
+ exit $exit_code
+ ;;
+ 4)
+ echo "Building the packages failed without any mutations!"
+ exit $exit_code
+ ;;
+ *)
+ echo "Unknown exit code: $exit_code"
+ exit $exit_code
+ ;;
+ esac
diff --git a/stacks-core/mutation-testing/pr-differences/README.md b/stacks-core/mutation-testing/pr-differences/README.md
new file mode 100644
index 0000000..47df406
--- /dev/null
+++ b/stacks-core/mutation-testing/pr-differences/README.md
@@ -0,0 +1,29 @@
+# PR Differences Mutants action
+
+Run mutants for differences in pull requests, and upload the mutant outcomes and exit codes to artifacts.
+
+## Documentation
+
+### Inputs
+
+| Input | Description | Required | Default |
+| ------------------------------- | ----------------------------------------------------- | ------------------------- | ------------------------- |
+| `shard` | The number of the shard to run (`-1` if ran without shards) | false | -1 |
+| `package-dimension` | The dimension of the package. `big` for [stacks-node](https://github.com/stacks-network/stacks-core/tree/develop/testnet/stacks-node) and [stackslib](https://github.com/stacks-network/stacks-core/tree/develop/stackslib), `small` for others | true | |
+
+## Usage
+
+```yaml
+name: Action
+on: pull_request
+jobs:
+ build:
+ name: Job
+ runs-on: ubuntu-latest
+ steps:
+ - name: PR Differences Mutants
+ id: pr-differences-mutants
+ uses: stacks-network/actions/stacks-core/mutation-testing/pr-differences@main
+ with:
+ package-dimension: "big"
+```
diff --git a/stacks-core/mutation-testing/pr-differences/action.yml b/stacks-core/mutation-testing/pr-differences/action.yml
new file mode 100644
index 0000000..83dc12b
--- /dev/null
+++ b/stacks-core/mutation-testing/pr-differences/action.yml
@@ -0,0 +1,202 @@
+name: PR Differences Mutants
+description: "Runs mutants corresponding to its given inputs and uploads the outcomes to artifacts"
+branding:
+ icon: "users"
+ color: "gray-dark"
+
+inputs:
+ shard:
+ description: "The number of the shard to run (`-1` if ran without shards)"
+ required: false
+ default: -1
+ package-dimension:
+ description: "The dimension of the package. `big` for `stacks-node` and `stackslib`, `small` for others"
+ required: true
+
+runs:
+ using: "composite"
+
+ steps:
+ # Cleanup Runner
+ - name: Cleanup Runner
+ id: runner_cleanup
+ uses: stacks-network/actions/cleanup/disk@main
+
+ # Checkout the stacks-core code
+ - name: Checkout stacks-core repo
+ id: git_checkout_stacks_core
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ with:
+ fetch-depth: 0
+
+ - name: Relative diff
+ id: relative_diff
+ shell: bash
+ run: |
+ git diff origin/${{ github.base_ref }}.. > git.diff
+
+ - uses: taiki-e/install-action@ac89944b5b150d78567ab6c02badfbe48b0b55aa # v2.20.16
+ name: Install cargo-mutants
+ id: install_cargo_mutants
+ with:
+ tool: cargo-mutants@23.12.2 # v23.12.2
+
+ - name: Update git diff
+ id: update_git_diff
+ shell: bash
+ run: |
+ input_file="git.diff"
+ temp_file="temp_diff_file.diff"
+
+ # Check if the commands exist on the host
+ for cmd in tac awk sed; do
+ command -v "${cmd}" > /dev/null 2>&1 || { echo "Missing command: ${cmd}" && exit 1; }
+ done
+
+ # Check that the file exists before performing actions on it
+ if [ ! -s "$input_file" ]; then
+ echo "Diff file (git.diff) is missing or empty!"
+ exit 1
+ fi
+
+ # Reverse the file, remove 4 lines after '+++ /dev/null', then reverse it back (editors can't go backwards - to remove lines above)
+ tac "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+ sed '/+++ \/dev\/null/{n;N;N;N;d;}' "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+ tac "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
+
+ # Remove the lines between '+++ /dev/null' (included) and 'diff --git a/'
+ awk '
+ BEGIN { in_block=0 }
+ /\+\+\+ \/dev\/null/ { in_block=1; next }
+ in_block && /diff --git a\// { in_block=0; print; next }
+ !in_block
+ ' "$input_file" > "$temp_file"
+
+ # Check that the file exists before performing actions on it
+ if [ -s "$temp_file" ]; then
+ mv "$temp_file" "$input_file"
+ exit 0
+ fi
+
+ echo "Temp diff file (temp_diff_file.diff) is missing or empty!"
+ exit 1
+
+ - name: Split diffs
+ id: split_diffs
+ shell: bash
+ run: |
+ # Check that the file exists before performing actions on it
+ if [ ! -s git.diff ]; then
+ echo "Diff file (git.diff) is missing or empty!"
+ exit 1
+ fi
+
+ # Make a file containing all the mutants for the differences in the PR and a folder to split them into big and small packages
+ cargo mutants --in-diff git.diff --list > all_mutants.txt
+ if [ $? -ne 0 ]; then
+ echo "Error retrieving the list of mutants!"
+ exit $?
+ fi
+
+ mkdir -p mutants_by_packages
+
+ # Check that the file exists before performing actions on it
+ if [ ! -s all_mutants.txt ]; then
+ echo "The file containing mutants (all_mutants.txt) is missing or empty!"
+ exit 1
+ fi
+
+ # Split the differences from git into 2 parts, big packages ('stacks-node' and 'stackslib') and small packages (all others) and put them into separate files
+ while IFS= read -r line; do
+ package=$(echo "$line" | cut -d'/' -f1)
+
+ case $package in
+ "testnet" | "stackslib")
+ echo "$line" >> "mutants_by_packages/big_packages.txt"
+ ;;
+ *)
+ echo "$line" >> "mutants_by_packages/small_packages.txt"
+ ;;
+ esac
+ done < all_mutants.txt
+
+ # Create regex patterns of the mutants to be ran for `cargo mutants` -F flag and output them to files to be used later
+ for package in mutants_by_packages/*; do
+ regex_pattern=""
+
+ while IFS= read -r line; do
+ escaped_line=$(echo "$line" | sed 's/[][()\.^$*+?{}|]/\\&/g')
+ regex_pattern+="($escaped_line)|"
+ done < "$package"
+
+ regex_pattern="${regex_pattern%|}"
+
+ if [[ "$package" == "mutants_by_packages/big_packages.txt" ]]; then
+ echo "$regex_pattern" > mutants_by_packages/regex_big.txt
+ else
+ echo "$regex_pattern" > mutants_by_packages/regex_small.txt
+ fi
+ done
+
+ exit 0
+
+ - name: Run mutants
+ id: run_mutants
+ shell: bash
+ run: |
+ # Disable immediate exit on error
+ set +e
+
+ # Check which type of mutants to run: big, small, with or without shards, then capture the exit code
+ if [[ ${{ inputs.shard }} == -1 ]]; then
+ if [[ ${{ inputs.package-dimension }} == big ]]; then
+ if [[ -f mutants_by_packages/regex_big.txt ]]; then
+ echo "Running mutants without shards on big packages"
+ cargo mutants --no-shuffle -vV -F "$( ./mutants-shard-${{ inputs.package-dimension }}-${{ inputs.shard }}/exit_code.txt
+ mv ./mutants.out/*.txt mutants-shard-${{ inputs.package-dimension }}-${{ inputs.shard }}/
+
+ # Enable immediate exit on error again
+ set -e
+
+ - name: Upload artifact
+ id: upload_artifact
+ uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
+ with:
+ name: mutants-shard-${{ inputs.package-dimension }}-${{ inputs.shard }}
+ path: mutants-shard-${{ inputs.package-dimension }}-${{ inputs.shard }}