Skip to content

Conversation

@benalleng
Copy link
Collaborator

@benalleng benalleng commented Jul 15, 2025

Closes #872, closes #836, closes #771, closes #890

This addressed #778 for now but I still would like to create a test instead of excluding it
I also added the basic test that should address the mutation from #847 (comment)

The exclusions are catogerized as follows.

  • timeouts
  • mutations that allow code to simply skip or follow a code path that does not actually cause or prevent an error
  • expiry time checks I found not worth creating a test for.
  • TODO exclusions

@benalleng benalleng mentioned this pull request Jul 20, 2025
@benalleng benalleng force-pushed the cargo-mutants-version-update branch from 4d00cdc to f41cbc8 Compare July 22, 2025 18:08
@coveralls
Copy link
Collaborator

coveralls commented Jul 22, 2025

Pull Request Test Coverage Report for Build 16575062311

Details

  • 51 of 54 (94.44%) changed or added relevant lines in 4 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.09%) to 85.873%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin/src/core/send/mod.rs 6 7 85.71%
payjoin/src/core/receive/v1/mod.rs 23 25 92.0%
Totals Coverage Status
Change from base Build 16543289714: 0.09%
Covered Lines: 7908
Relevant Lines: 9209

💛 - Coveralls

@benalleng benalleng force-pushed the cargo-mutants-version-update branch 3 times, most recently from b807b4c to 907a9ed Compare July 22, 2025 19:16
@benalleng benalleng marked this pull request as ready for review July 22, 2025 19:18
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

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

I slightly dislike the mutants exclusions by name because they seem vulnerable to regressions (e.g. if the skipped functions get renamed as #884 is likely to do for some of these) or false positives (e.g. what was once a trivial mutation becomes serious after several refactors). Trivial mutations seem like a code smell that the underlying code could be re-written to eliminate ambiguity. I don't have specific suggestions regarding those excluded mutants, but I want to ensure that we're very deliberate about what we choose to exclude.

assert_eq!(proposal.clone().psbt_fee_rate().unwrap(), min_fee_rate)
}
Err(_) => {
panic!("Broadcast suitability check should fail due to being below the min fee rate or unexpected error type")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
panic!("Broadcast suitability check should fail due to being below the min fee rate or unexpected error type")
panic!("Broadcast suitability check with appropriate min_fee_rate should succeed")

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, I'd probably re-write the whole match statement:

let min_fee_rate = proposal.psbt_fee_rate().expect("Feerate calculation should not fail");
let _ = proposal.clone().check_broadcast_suitability(Some(min_fee_rate), |_| Ok(true)).expect("Broadcast suitability check with appropriate min_fee_rate should succeed");
assert_eq!(proposal.clone().psbt_fee_rate().unwrap(), min_fee_rate);

Re-writing it like that, it's unclear what exactly this assert_eq is testing for? It seems tautological that min_fee_rate == proposal.psbt_fee_rate().unwrap()

assert_eq!(proposal_rate, proposal.clone().psbt_fee_rate().unwrap());
assert_eq!(min_rate, min_fee_rate);
},
_ => panic!("Broadcast suitability check should fail due to being below the min fee rate or unexpected error type"),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Probably this match could also be re-written as an expect_err() instead of match/panic.

wants_outputs.clone().replace_receiver_outputs(outputs, script_pubkey.as_script());
assert!(
increased_amount.is_ok(),
"Not changing the receiver output amount should always be allowed"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
"Not changing the receiver output amount should always be allowed"
"Increasing the receiver output amount is always allowed"

fee_contribution.err(),
Some(InternalBuildSenderError::FeeOutputValueLowerThanFeeContribution)
);
let fee_contribution = determine_fee_contribution(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Some explanatory comments on these new edge cases wouldn't hurt. I'm guessing this is testing the maximum allowed fee contribution based on the input value?


#[test]
fn process_response_invalid_utf8() {
// In UTF-8, 0xF0 represents the start of a 4-byte sequence, so 0xF0 by itself is invalid
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment looks obsolete now.

Comment on lines 22 to 28
# Async SystemTime comparison
"replace > with >= in Sender<WithReplyKey>::extract_v2", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace < with <= in Receiver<Initialized>::apply_unchecked_from_payload", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Receiver<Initialized>::extract_req", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in extract_err_req", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Receiver<Initialized>::create_poll_request", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Sender<WithReplyKey>::create_v2_post_request", # checking if the system time is equal to the expiry is difficult to reasonably test
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit:

Suggested change
# Async SystemTime comparison
"replace > with >= in Sender<WithReplyKey>::extract_v2", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace < with <= in Receiver<Initialized>::apply_unchecked_from_payload", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Receiver<Initialized>::extract_req", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in extract_err_req", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Receiver<Initialized>::create_poll_request", # checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Sender<WithReplyKey>::create_v2_post_request", # checking if the system time is equal to the expiry is difficult to reasonably test
# Async SystemTime comparison
# checking if the system time is equal to the expiry is difficult to reasonably test
"replace > with >= in Sender<WithReplyKey>::extract_v2",
"replace < with <= in Receiver<Initialized>::apply_unchecked_from_payload",
"replace > with >= in Receiver<Initialized>::extract_req",
"replace > with >= in extract_err_req",
"replace > with >= in Receiver<Initialized>::create_poll_request",
"replace > with >= in Sender<WithReplyKey>::create_v2_post_request",

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Tried to to a little further organization

"replace > with >= in PsbtContext::check_fees", # checking if the feerate is below the minimum when the minimum is allowed to be zero does nothing
"replace < with <= in SenderBuilder<'a>::build_recommended", # clamping the fee contribution when the fee equals to the recommended fee does not do anything

# Async SystemTime comparison
Copy link
Collaborator

Choose a reason for hiding this comment

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

We may want to figure out a way to mock SystemTime in tests. I've used freezegun in Python in the past, I wonder if a similar tool exists the Rust ecosystem. We may even be able to mock it with a Trait, per Claude:

use std::time::SystemTime;

trait TimeProvider {
    fn now(&self) -> SystemTime;
}

struct RealTimeProvider;

impl TimeProvider for RealTimeProvider {
    fn now(&self) -> SystemTime {
        SystemTime::now()
    }
}

struct MockTimeProvider {
    time: SystemTime,
}

impl TimeProvider for MockTimeProvider {
    fn now(&self) -> SystemTime {
        self.time
    }
}

// Your code uses the trait
fn process_with_timestamp<T: TimeProvider>(time_provider: &T) -> SystemTime {
    time_provider.now()
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{Duration, UNIX_EPOCH};

    #[test]
    fn test_with_mock_time() {
        let mock_time = UNIX_EPOCH + Duration::from_secs(1000);
        let provider = MockTimeProvider { time: mock_time };
        
        let result = process_with_timestamp(&provider);
        assert_eq!(result, mock_time);
    }
}

This seems reasonable to leave as a follow-up, maybe to be addressed alongside #893.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ahh interesting, I got hung up on trying to fit the tokio::test pause() method in but it just wasn't working.

@benalleng
Copy link
Collaborator Author

Thanks! as for those trivial mutants a good can of axe body spray by just making the code blocks follow the logic path in the inverted operand cases.

With the named exclusions I think we might want to consider using diff mutations https://github.com/sourcefrog/cargo-mutants/blob/1481918f377a2e0a86a66185cb536dd292157438/.github/workflows/tests.yml#L234-L277 perhaps we're not quite ready for it in our codebase but it would be nice to keep the mutant coverage more maintainable

@benalleng benalleng force-pushed the cargo-mutants-version-update branch from 907a9ed to e306e83 Compare July 28, 2025 15:34
This updates cargo mutants to 25.2.2 to allow us to catch more mutants
and add some more precise exclusions.
@benalleng benalleng force-pushed the cargo-mutants-version-update branch 3 times, most recently from d8a5edc to 0b0f758 Compare July 28, 2025 16:24
This commits adds the exclusions and mutant catches for the cargo
mutants upgrade to v25.2.2
@benalleng benalleng force-pushed the cargo-mutants-version-update branch from 0b0f758 to 7a21ab7 Compare July 28, 2025 16:49
@benalleng benalleng requested a review from spacebear21 July 28, 2025 16:50
Copy link
Collaborator

@spacebear21 spacebear21 left a comment

Choose a reason for hiding this comment

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

utACK 7a21ab7

It would be great to address the exclusions in later follow-ups and remove them from the exclusion list, but these seem reasonable enough for now, and merging this PR will reduce the noise from automated mutants issues.

@spacebear21 spacebear21 merged commit 1f39170 into payjoin:master Jul 29, 2025
9 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.

New Mutants Found New Mutants Found Consideration for using cargo mutants v25.2.0 New Mutants Found

3 participants