Skip to content

Commit abeb0c1

Browse files
committed
chore: merge
2 parents daf8a58 + fe842e2 commit abeb0c1

File tree

15 files changed

+1181
-372
lines changed

15 files changed

+1181
-372
lines changed

.github/CONTRIBUTING.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Contributing
2+
3+
Thank you for considering contributing to this product! We welcome any contributions, whether it's bug fixes, new features, or improvements to the existing codebase.
4+
5+
## Your First Pull Request
6+
7+
Working on your first Pull Request? You can learn how from this free video series:
8+
9+
[How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
10+
11+
To help you get familiar with our contribution process, we have a list of [good first issues](../../issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) that contain bugs that have a relatively limited scope. This is a great place to get started.
12+
13+
If you decide to fix an issue, please be sure to check the comment thread in case somebody is already working on a fix. If nobody is working on it at the moment, please leave a comment stating that you intend to work on it so other people don’t accidentally duplicate your effort.
14+
15+
If somebody claims an issue but doesn’t follow up for more than two weeks, it’s fine to take it over but you should still leave a comment. **Issues won't be assigned to anyone outside the core team**.
16+
17+
## How to run the repo test suite
18+
19+
```bash
20+
docker-compose -f dockerfiles/docker-compose.dev.postgres.yml up
21+
cargo test --workspace
22+
docker-compose -f dockerfiles/docker-compose.dev.postgres.yml down
23+
```
24+
25+
## How CI Mutants Output should be treated
26+
27+
1. **New Function Created in This PR:**
28+
- **Knowledgeable:**
29+
Ideally, write unit tests.
30+
This takes more time initially but is beneficial long-term.
31+
- **Not Knowledgeable:**
32+
Create an issue to highlight the gap, check examples, using the `mutation-testing` label on GitHub: [Mutation Testing Issues](https://github.com/hirosystems/runehook/issues?q=is%3Aissue%20state%3Aopen%20label%3Amutation-testing).
33+
34+
2. **Modified Function in This PR:**
35+
Review the commit history to identify the developer who is familiar with the context of the function, create a new issue and tag him.
36+
37+
### Types of Mutants
38+
39+
1. **Caught:**
40+
No action is needed as these represent well-tested functions.
41+
2. **Missed:**
42+
Add tests where coverage is lacking.
43+
3. **Timeout:**
44+
Use the skip flag for functions that include network requests/responses to avoid hang-ups due to alterations.
45+
4. **Unviable:**
46+
Implement defaults to enable running tests with these mutants.
47+
48+
49+
### How to treat different types of mutants
50+
51+
#### 1. Caught
52+
53+
Caught mutants indicate functions that are well-tested, where mutations break the unit tests.
54+
Aim to achieve this status.
55+
56+
#### 2. Timeout
57+
58+
Timeouts often occur in functions altered to include endless waits (e.g., altered HTTP requests/responses). Apply the `#[cfg_attr(test, mutants::skip)]` flag.
59+
Look into the function that has the mutation creating a timeout and if it has http requests/responses, or any one or multiple child levels have requests/responses, add this flag like showcased in the below example.
60+
```rust
61+
impl PeerNetwork {
62+
#[cfg_attr(test, mutants::skip)]
63+
/// Check that the sender is authenticated.
64+
/// Returns Some(remote sender address) if so
65+
/// Returns None otherwise
66+
fn check_peer_authenticated(&self, event_id: usize) -> Option<NeighborKey> {
67+
```
68+
69+
#### 3. Missed
70+
Missed mutants highlight that the function doesn’t have tests for specific cases.
71+
eg. if the function returns a `bool` and the mutant replaces the function’s body with `true`, then a missed mutant reflects that the function is not tested for the `false` case as it passes all test cases by having this default `true` value.
72+
73+
1. If you are the person creating the functions, most probably you are most adequate to create these tests.
74+
2. If you are the person modifying the function, if you are aware of how it works it would be best to be added by you as in the long run it would create less context switching for others that are aware of the function’s tests.
75+
3. If the context switching is worthy or you aren’t aware of the full context to add all the missing tests, than an issue should be created to highlight the problem and afterwards the tests be added or modified by someone else. [eg. issue format](https://github.com/stacks-network/stacks-core/issues/4872)
76+
77+
#### 4. Unviable
78+
79+
Unviable mutants show a need for a default value for the return type of the function.
80+
This is needed in order for the function’s body to be replaced with this default value and run the test suite.
81+
While this increases the chances of catching untested scenarios, it doesn’t mean it catches all of them.
82+
[eg. issue format](https://github.com/stacks-network/stacks-core/issues/4867)
83+
If a default implementation would not cover it, or it can’t be created for this structure for various reasons, it can be skipped in the same way as timeouts `#[cfg_attr(test, mutants::skip)]`
84+
85+
```rust
86+
// Define the Car struct with appropriate field types
87+
#[derive(Debug, Clone)]
88+
struct Car {
89+
color: String,
90+
model: String,
91+
year: i64,
92+
}
93+
94+
// Manually implement the Default trait for Car
95+
impl Default for Car {
96+
fn default() -> Self {
97+
Self {
98+
color: "Black".to_string(),
99+
model: "Generic Model".to_string(),
100+
year: 2020, // Specific default year
101+
}
102+
}
103+
}
104+
105+
impl Car {
106+
// Constructor to create a new Car instance with specific attributes
107+
fn new(color: &str, model: &str, year: i64) -> Self {
108+
Self {
109+
color: color.to_string(),
110+
model: model.to_string(),
111+
year,
112+
}
113+
}
114+
}
115+
116+
// Example usage of Car
117+
fn main() {
118+
// Create a default Car using the Default trait
119+
let default_car = Car::default();
120+
println!("Default car: {:?}", default_car);
121+
122+
// Create a custom Car using the new constructor
123+
let custom_car = Car::new("Red", "Ferrari", 2022);
124+
println!("Custom car: {:?}", custom_car);
125+
}
126+
127+
```
128+
129+
### Contribution Prerequisites
130+
131+
... 🚧 Work in progress 🚧 ...

.github/workflows/pr-mutants.yml

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
name: PR Differences Mutants
2+
3+
on:
4+
pull_request:
5+
types:
6+
- opened
7+
- reopened
8+
- synchronize
9+
- ready_for_review
10+
paths:
11+
- "**.rs"
12+
workflow_dispatch:
13+
14+
concurrency:
15+
group: pr-differences-${{ github.head_ref || github.ref || github.run_id }}
16+
# Always cancel duplicate jobs
17+
cancel-in-progress: true
18+
19+
jobs:
20+
mutants:
21+
name: Mutation Testing
22+
runs-on: ubuntu-latest
23+
steps:
24+
# Cleanup Runner
25+
- name: Cleanup Runner
26+
id: runner_cleanup
27+
uses: stacks-network/actions/cleanup/disk@main
28+
29+
- name: Checkout repo
30+
id: git_checkout
31+
uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
32+
with:
33+
fetch-depth: 0
34+
35+
- name: Relative diff
36+
id: relative_diff
37+
run: |
38+
git diff $(git merge-base origin/${{ github.base_ref || 'main' }} HEAD)..HEAD > git.diff
39+
40+
- name: Install cargo-mutants
41+
id: install_cargo_mutants
42+
run: |
43+
cargo install --version 24.11.2 cargo-mutants --locked # v24.11.2
44+
45+
- name: Install cargo-nextest
46+
id: install_cargo_nextest
47+
uses: taiki-e/install-action@2f990e9c484f0590cb76a07296e9677b417493e9 # v2.33.23
48+
with:
49+
tool: nextest # Latest version
50+
51+
- name: Update git diff
52+
id: update_git_diff
53+
run: |
54+
input_file="git.diff"
55+
temp_file="temp_diff_file.diff"
56+
57+
# Check if the file exists and is not empty
58+
if [ ! -s "$input_file" ]; then
59+
echo "Diff file ($input_file) is missing or empty!"
60+
exit 1
61+
fi
62+
63+
# Remove all lines related to deleted files including the first 'diff --git' line
64+
awk '
65+
/^diff --git/ {
66+
diff_line = $0
67+
getline
68+
if ($0 ~ /^deleted file mode/) {
69+
in_deleted_file_block = 1
70+
} else {
71+
if (diff_line != "") {
72+
print diff_line
73+
diff_line = ""
74+
}
75+
in_deleted_file_block = 0
76+
}
77+
}
78+
!in_deleted_file_block
79+
' "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
80+
81+
# Remove 'diff --git' lines only when followed by 'similarity index', 'rename from', and 'rename to'
82+
awk '
83+
/^diff --git/ {
84+
diff_line = $0
85+
getline
86+
if ($0 ~ /^similarity index/) {
87+
getline
88+
if ($0 ~ /^rename from/) {
89+
getline
90+
if ($0 ~ /^rename to/) {
91+
next
92+
}
93+
}
94+
}
95+
print diff_line
96+
}
97+
{ print }
98+
' "$input_file" > "$temp_file" && mv "$temp_file" "$input_file"
99+
100+
- name: Run docker-compose
101+
uses: hoverkraft-tech/compose-action@f1ca7fefe3627c2dab0ae1db43a106d82740245e # v2.0.2
102+
with:
103+
compose-file: "./dockerfiles/docker-compose.dev.postgres.yml"
104+
105+
- name: Run mutants
106+
id: run_mutants
107+
run: |
108+
# Disable immediate exit on error
109+
set +e
110+
111+
cargo mutants --workspace --timeout-multiplier 1.5 --no-shuffle -vV --in-diff git.diff --output ./ --test-tool=nextest -- --all-targets --test-threads 1
112+
exit_code=$?
113+
114+
# Create the folder only containing the outcomes (.txt files) and make a file containing the exit code of the command
115+
mkdir mutants
116+
echo "$exit_code" > ./mutants/exit_code.txt
117+
mv ./mutants.out/*.txt mutants/
118+
119+
# Enable immediate exit on error again
120+
set -e
121+
122+
- name: Print mutants
123+
id: print_tested_mutants
124+
shell: bash
125+
run: |
126+
# Info for creating the link that paths to the specific mutation tested
127+
server_url="${{ github.server_url }}"
128+
organisation="${{ github.repository_owner }}"
129+
repository="${{ github.event.repository.name }}"
130+
commit="${{ github.sha }}"
131+
132+
# Function to write to github step summary with specific info depending on the mutation category
133+
write_section() {
134+
local section_title=$1
135+
local file_name=$2
136+
137+
if [ -s "$file_name" ]; then
138+
if [[ "$section_title" != "" ]]; then
139+
echo "## $section_title" >> "$GITHUB_STEP_SUMMARY"
140+
fi
141+
142+
if [[ "$section_title" == "Missed:" ]]; then
143+
echo "<details>" >> "$GITHUB_STEP_SUMMARY"
144+
echo "<summary>What are missed mutants?</summary>" >> "$GITHUB_STEP_SUMMARY"
145+
echo "<br>" >> "$GITHUB_STEP_SUMMARY"
146+
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"
147+
echo "</details>" >> "$GITHUB_STEP_SUMMARY"
148+
echo "" >> "$GITHUB_STEP_SUMMARY"
149+
elif [[ "$section_title" == "Timeout:" ]]; then
150+
echo "<details>" >> "$GITHUB_STEP_SUMMARY"
151+
echo "<summary>What are timeout mutants?</summary>" >> "$GITHUB_STEP_SUMMARY"
152+
echo "<br>" >> "$GITHUB_STEP_SUMMARY"
153+
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"
154+
echo "</details>" >> "$GITHUB_STEP_SUMMARY"
155+
echo "" >> "$GITHUB_STEP_SUMMARY"
156+
elif [[ "$section_title" == "Unviable:" ]]; then
157+
echo "<details>" >> "$GITHUB_STEP_SUMMARY"
158+
echo "<summary>What are unviable mutants?</summary>" >> "$GITHUB_STEP_SUMMARY"
159+
echo "<br>" >> "$GITHUB_STEP_SUMMARY"
160+
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"
161+
echo "</details>" >> "$GITHUB_STEP_SUMMARY"
162+
echo "" >> "$GITHUB_STEP_SUMMARY"
163+
fi
164+
165+
if [[ "$section_title" != "" ]]; then
166+
awk -F':' '{gsub("%", "%%"); printf "- [ ] [%s](%s/%s/%s/blob/%s/%s#L%d)\n\n", $0, "'"$server_url"'", "'"$organisation"'", "'"$repository"'", "'"$commit"'", $1, $2-1}' "$file_name" >> "$GITHUB_STEP_SUMMARY"
167+
else
168+
awk -F':' '{gsub("%", "%%"); printf "- [x] [%s](%s/%s/%s/blob/%s/%s#L%d)\n\n", $0, "'"$server_url"'", "'"$organisation"'", "'"$repository"'", "'"$commit"'", $1, $2-1}' "$file_name" >> "$GITHUB_STEP_SUMMARY"
169+
fi
170+
171+
if [[ "$section_title" == "Missed:" ]]; then
172+
echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
173+
echo "- Modify or add tests including this function." >> "$GITHUB_STEP_SUMMARY"
174+
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"
175+
elif [[ "$section_title" == "Timeout:" ]]; then
176+
echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
177+
echo "- Modify the tests that include this funcion." >> "$GITHUB_STEP_SUMMARY"
178+
echo "- Add '#[mutants::skip]' or '#[cfg_attr(test, mutants::skip)]' function header to skip it." >> "$GITHUB_STEP_SUMMARY"
179+
elif [[ "$section_title" == "Unviable:" ]]; then
180+
echo "### To resolve this issue, consider one of the following options:" >> "$GITHUB_STEP_SUMMARY"
181+
echo "- Create 'Default::default()' implementation for the specific structure." >> "$GITHUB_STEP_SUMMARY"
182+
echo "- Add '#[mutants::skip]' or '#[cfg_attr(test, mutants::skip)]' function header to skip it." >> "$GITHUB_STEP_SUMMARY"
183+
fi
184+
185+
echo >> "$GITHUB_STEP_SUMMARY"
186+
fi
187+
}
188+
189+
# Print uncaught (missed/timeout/unviable) mutants to summary
190+
if [ -s ./mutants/missed.txt -o -s ./mutants/timeout.txt -o -s ./mutants/unviable.txt ]; then
191+
echo "# Uncaught Mutants" >> "$GITHUB_STEP_SUMMARY"
192+
echo "[Documentation - How to treat Mutants Output](https://github.com/stacks-network/actions/tree/main/stacks-core/mutation-testing#how-mutants-output-should-be-treated)" >> "$GITHUB_STEP_SUMMARY"
193+
write_section "Missed:" "./mutants/missed.txt"
194+
write_section "Timeout:" "./mutants/timeout.txt"
195+
write_section "Unviable:" "./mutants/unviable.txt"
196+
fi
197+
198+
# Print caught mutants to summary
199+
if [ -s ./mutants/caught.txt ]; then
200+
echo "# Caught Mutants" >> "$GITHUB_STEP_SUMMARY"
201+
write_section "" "./mutants/caught.txt"
202+
fi
203+
204+
# Get exit code from the file and match it
205+
exit_code=$(<"mutants/exit_code.txt")
206+
207+
yellow_bold="\033[1;33m"
208+
reset="\033[0m"
209+
summary_link_message="${yellow_bold}Click here for more information on how to fix:${reset} ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#:~:text=Output%20Mutants%20summary"
210+
211+
case $exit_code in
212+
0)
213+
if [[ ! -f ./mutants/caught.txt && ! -f ./mutants/missed.txt && ! -f ./mutants/timeout.txt && ! -f ./mutants/unviable.txt ]]; then
214+
echo "No mutants found to test!"
215+
elif [[ -s ./mutants/unviable.txt ]]; then
216+
echo -e "$summary_link_message"
217+
echo "Found unviable mutants!"
218+
exit 5
219+
else
220+
echo "All new and updated functions are caught!"
221+
fi
222+
;;
223+
1)
224+
echo -e "$summary_link_message"
225+
echo "Invalid command line arguments!"
226+
exit $exit_code
227+
;;
228+
2)
229+
echo -e "$summary_link_message"
230+
echo "Found missed mutants!"
231+
exit $exit_code
232+
;;
233+
3)
234+
echo -e "$summary_link_message"
235+
echo "Found timeout mutants!"
236+
exit $exit_code
237+
;;
238+
4)
239+
echo -e "$summary_link_message"
240+
echo "Building the packages failed without any mutations!"
241+
exit $exit_code
242+
;;
243+
*)
244+
echo -e "$summary_link_message"
245+
echo "Unknown exit code: $exit_code"
246+
exit $exit_code
247+
;;
248+
esac
249+
250+
exit 0

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,7 @@ Cargo.lock
222222
!.yarn/versions
223223

224224
*.node
225+
226+
# Mutation Testing
227+
mutants.out/
228+
mutants.out.old/

0 commit comments

Comments
 (0)