Skip to content

Add mutants for sender module#717

Merged
DanGould merged 2 commits intopayjoin:masterfrom
benalleng:sender-mutants
Jun 3, 2025
Merged

Add mutants for sender module#717
DanGould merged 2 commits intopayjoin:masterfrom
benalleng:sender-mutants

Conversation

@benalleng
Copy link
Collaborator

This adds the mutants for the mod file in the sender.

This is the first part of the sender to be covered by mutation testing. Once the the sender has complete mutation coverage we can look at finalizing #539

@benalleng
Copy link
Collaborator Author

No missed mutants as of 54c9110

Found 320 mutants to test
ok       Unmutated baseline in 29.5s build + 11.7s test
 INFO Auto-set test timeout to 59s
320 mutants tested in 9m 21s: 197 caught, 123 unviable

@coveralls
Copy link
Collaborator

coveralls commented May 26, 2025

Pull Request Test Coverage Report for Build 15424551466

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 220 of 220 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.5%) to 84.835%

Totals Coverage Status
Change from base Build 15400978156: 0.5%
Covered Lines: 6618
Relevant Lines: 7801

💛 - Coveralls

@DanGould DanGould self-requested a review May 26, 2025 23:44
@benalleng
Copy link
Collaborator Author

benalleng commented May 28, 2025

In reference to the fact that ensure!() is not caught by mutations as @spacebear21 pointed out first in #539 I added a commit that expands the mutation coverage by converting our ensure macros to plain function if statements. It's possible there is a cleaner way to add these into the mutation flow but with most of them being quite simple I thought just inverting the logic as an if statement made sense. Below is the new mutation coverage as of e484cd5.

Found 350 mutants to test
ok       Unmutated baseline in 28.4s build + 16.1s test
 INFO Auto-set test timeout to 1m 21s
350 mutants tested in 10m 36s: 222 caught, 128 unviable

I did have a mutation that was missed but I can no longer reproduce it and the logic indicates it shouldn't have happened but it did, I don't think we need to worry about this too much but something to point out if anyone else is able to reproduce it

MISSED   payjoin/src/send/mod.rs:275:24: replace match guard with true in 9.2s build + 21.9s test

Comment on lines 265 to 272
if self.output_substitution == OutputSubstitution::Disabled
|| (proposed_txout.script_pubkey == original_output.script_pubkey
&& proposed_txout.value < original_output.value)
{
return Err(InternalProposalError::DisallowedOutputSubstitution);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more strict than the original, it errors if output substitution is disabled, regardless of the output

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I think I have the logic cleaned up here now.

Comment on lines 73 to 80
macro_rules! ensure {
($cond:expr, $error:ident) => {
if !($cond) {
return Err(InternalProposalError::$error);
}
};
}

Copy link
Collaborator

@spacebear21 spacebear21 May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, could this be replaced with a simple function?

fn ensure<T>(condition: bool, error: T) -> Result<(), T> {
    if !condition {
        return Err(error);
    }
    Ok(())
}

ensure(original_fee <= proposed_fee, InternalProposalError::AbsoluteFeeDecreased)?;

In addition to keeping to DRY, it would make this diff easier to reason about and validate. The key thing to ensure with this approach is that mutants can still modify the operators in the condition.

Copy link
Collaborator Author

@benalleng benalleng May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I don't think it catches everything, we lose 8 possible mutants when going about this approach, I will check what gets missed. I am going to go out on a limb and assume that the .is_empty() or .is_some() by themselves as params are not mutated while they are in a traditional if clause as the clause can just be replaced with true or false. operands like || && <= etc are still mutated here I'm assuming.

I am checking now what can no-longer be mutated.

Copy link
Collaborator Author

@benalleng benalleng May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think actually these new mutants are just a byproduct of how the if clauses added a small amount of complexity into the catches, we can use this ensure function.

It was mostly the fact that >= has only one mutant; <
While the inverse > has two mutants; < and ==

Also having to check if (!is_empty()) adds a mutant with the negation vs the base ensure

@benalleng benalleng force-pushed the sender-mutants branch 4 times, most recently from 12ba639 to e3b048f Compare May 30, 2025 22:49
@benalleng
Copy link
Collaborator Author

Mutation coverage as of e3b048f

Found 342 mutants to test
ok       Unmutated baseline in 24.5s build + 11.2s test
 INFO Auto-set test timeout to 56s
342 mutants tested in 9m 32s: 214 caught, 128 unviable

@benalleng
Copy link
Collaborator Author

benalleng commented May 30, 2025

I wanted to mention that these example psbts in test_find_change_index() are not perfectly modified psbts, as such I am not sure if they would be valid if the test for each specific assert did not exist its possible the psbt would still fail at some other point.

I had a hard time grokking what key values to change on the psbt specifically to get to each error there but I do think targeting the key values instead of just creating new psbt strings is a better way to do this.

I tried to find an exact example but I couldn't before I stepped off but I'm fairly certain these psbts would fail on more than just each assert it is testing for.

Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK e3b048f

The function is so much better and it seems each instance is the same as before.

In the tests, did you try to make the PSBTs in rust rather than just with strings? It seems like a bit of a shortcut to read in strings, but if you're confident they contain what the comments state, and you did try to have semantic rust that was for some reason a PITA, I don't see a great reason to oppose letting this in as-is. How did you make the strings?

@benalleng
Copy link
Collaborator Author

I used bitcoin-cli createpsbt and https://bip174.org for some additional modification

@benalleng
Copy link
Collaborator Author

Sat down and managed to make them in rust after-all it was actually rather easy after realizing I had the psbt strings as reference to what should be changed 😅

@benalleng benalleng force-pushed the sender-mutants branch 4 times, most recently from e4b0b5a to 5e2bd4f Compare June 3, 2025 15:31
@benalleng
Copy link
Collaborator Author

Tested again and confirmed no new mutants on 5e2bd4f

Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a commit which should be squashed that's a shortcut for creating ScriptBuf from hex and a comment on string equalilty that we should replace with error matching (can squash/un-credit me). After those this should be good to go

Comment on lines 755 to 758
assert_eq!(
ctx.clone().process_proposal(proposal.clone()).unwrap_err().to_string(),
InternalProposalError::DisallowedOutputSubstitution.to_string()
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assert!(match!

Suggested change
assert_eq!(
ctx.clone().process_proposal(proposal.clone()).unwrap_err().to_string(),
InternalProposalError::DisallowedOutputSubstitution.to_string()
);
assert!(matches!(
ctx.clone().process_proposal(proposal.clone()).unwrap_err(),
InternalProposalError::DisallowedOutputSubstitution
));

let mut proposal: bitcoin::Psbt = PARSED_PAYJOIN_PROPOSAL.clone();

ctx.fee_contribution = None;
proposal.unsigned_tx.output.get_mut(0).unwrap().value =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, especially for tests, I think output[0] would work here and still panic. The only time to use get_mut(0) is if you wanted to handle it without a panic.

@DanGould
Copy link
Contributor

DanGould commented Jun 3, 2025

yeah if you use output[0] you don't even need the unwrap!

@benalleng
Copy link
Collaborator Author

I just need to bite the bullet and finally make a pre-commit hook 😅

@benalleng benalleng force-pushed the sender-mutants branch 2 times, most recently from 790aa6b to 4fe9c78 Compare June 3, 2025 18:04
This adds the mutants for the mod file in the sender.

I am breaking the mutation coverage up for the sender file by file so
there is not a huge PR touching everything as we can add coverage one
file at a time.
By using plain if statements we can test these logic blocks with mutants
ensuring some better coverage on the send mod.rs file.
Some((Amount::from_sat(1000), None)),
true,
);
assert_eq!(fee_contribution.err(), Some(InternalBuildSenderError::AmbiguousChangeOutput));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future I think this syntax is simpler:

Suggested change
assert_eq!(fee_contribution.err(), Some(InternalBuildSenderError::AmbiguousChangeOutput));
assert_eq!(fee_contribution, Err(InternalBuildSenderError::AmbiguousChangeOutput));

Copy link
Contributor

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK d8d7c1d

@DanGould DanGould merged commit 6975557 into payjoin:master Jun 3, 2025
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants