Skip to main content

bcore_mutation/operators/
mod.rs

1use regex::Regex;
2
3use crate::project::Project;
4
5mod bitcoin_core;
6mod common;
7mod secp256k1;
8
9pub use bitcoin_core::BitcoinCore;
10pub use secp256k1::Secp256k1;
11
12#[derive(Debug, Clone)]
13pub struct MutationOperator {
14    pub pattern: Regex,
15    pub replacement: String,
16}
17
18impl MutationOperator {
19    pub fn new(pattern: &str, replacement: &str) -> Result<Self, regex::Error> {
20        Ok(MutationOperator {
21            pattern: Regex::new(pattern)?,
22            replacement: replacement.to_string(),
23        })
24    }
25}
26
27/// Compile a list of `(pattern, replacement)` pairs into mutation operators,
28/// preserving order. Used by the per-project [`OperatorSet`] implementations.
29pub(crate) fn build(pairs: Vec<(&str, &str)>) -> Result<Vec<MutationOperator>, regex::Error> {
30    pairs
31        .into_iter()
32        .map(|(pattern, replacement)| MutationOperator::new(pattern, replacement))
33        .collect()
34}
35
36/// The mutation operators and skip rules for a single project.
37///
38/// Each project (Bitcoin Core, secp256k1, …) provides its own operators and
39/// "do not mutate" lists. Implementations typically compose the language-level
40/// operators in [`common`] with project-specific additions.
41pub trait OperatorSet {
42    /// Operators applied to general (non-test) source files.
43    fn regex_operators(&self) -> Result<Vec<MutationOperator>, regex::Error>;
44
45    /// Operators applied when `--only-security-mutations` is set.
46    fn security_operators(&self) -> Result<Vec<MutationOperator>, regex::Error>;
47
48    /// Operators applied to test files (unit and, where applicable, functional).
49    fn test_operators(&self) -> Result<Vec<MutationOperator>, regex::Error>;
50
51    /// Line prefixes that disable mutation of a line entirely.
52    fn do_not_mutate_patterns(&self) -> Vec<&'static str>;
53
54    /// Substrings that disable mutation of a Python test line.
55    fn do_not_mutate_py_patterns(&self) -> Vec<&'static str>;
56
57    /// Substrings that disable mutation of a (C/C++) unit-test line.
58    fn do_not_mutate_unit_patterns(&self) -> Vec<&'static str>;
59
60    /// Substrings that disable mutation of any line when contained.
61    fn skip_if_contain_patterns(&self) -> Vec<&'static str>;
62
63    /// Prefixes that mark a test line as not worth mutating (asserts, helpers).
64    /// Consumed by the default [`OperatorSet::should_mutate_test_line`].
65    fn test_line_skip_prefixes(&self) -> Vec<&'static str>;
66
67    /// Whether a test line should be mutated by [`OperatorSet::test_operators`].
68    ///
69    /// Default behaviour: skip lines starting with any
70    /// [`OperatorSet::test_line_skip_prefixes`], then only mutate lines that
71    /// look like a standalone function call.
72    fn should_mutate_test_line(&self, line: &str) -> bool {
73        let trimmed = line.trim();
74
75        for pattern in self.test_line_skip_prefixes() {
76            if trimmed.starts_with(pattern) {
77                return false;
78            }
79        }
80
81        // Only mutate if it looks like a function call.
82        let function_call_pattern =
83            Regex::new(r"^\s*(?:\w+(?:\.|->|::))*(\w+)\s*\([^)]*\)\s*;?\s*$").unwrap();
84        function_call_pattern.is_match(line)
85    }
86}
87
88/// Return the [`OperatorSet`] for the given project.
89pub fn for_project(project: Project) -> Box<dyn OperatorSet> {
90    match project {
91        Project::BitcoinCore => Box::new(BitcoinCore),
92        Project::Secp256k1 => Box::new(Secp256k1),
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn generic_call_deletion_op() -> MutationOperator {
101        MutationOperator::new(
102            r"^\s*[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*(?:(?:->|\.)[a-zA-Z_]\w*)*\s*\([^;]*\)\s*;$",
103            "",
104        )
105        .unwrap()
106    }
107
108    #[test]
109    fn test_generic_call_deletion_matches_free_function() {
110        let op = generic_call_deletion_op();
111        assert!(op.pattern.is_match("    Foo(arg1, arg2);"));
112        assert!(op.pattern.is_match("DoSomething();"));
113    }
114
115    #[test]
116    fn test_generic_call_deletion_matches_dot_member_call() {
117        let op = generic_call_deletion_op();
118        assert!(op.pattern.is_match("    obj.Method(arg);"));
119        assert!(op.pattern.is_match("obj.Method();"));
120    }
121
122    #[test]
123    fn test_generic_call_deletion_matches_arrow_member_call() {
124        let op = generic_call_deletion_op();
125        assert!(op.pattern.is_match("    ptr->Method(arg);"));
126        assert!(op.pattern.is_match("ptr->Method();"));
127    }
128
129    #[test]
130    fn test_generic_call_deletion_matches_namespaced_call() {
131        let op = generic_call_deletion_op();
132        assert!(op.pattern.is_match("    Namespace::Function(arg);"));
133        assert!(op.pattern.is_match("ns::Foo();"));
134    }
135
136    #[test]
137    fn test_generic_call_deletion_ignores_control_flow_and_keywords() {
138        let op = generic_call_deletion_op();
139        assert!(!op.pattern.is_match("    if (condition) {"));
140        assert!(!op.pattern.is_match("    while (x > 0) {"));
141        assert!(!op.pattern.is_match("    for (int i = 0; i < n; i++) {"));
142        assert!(!op.pattern.is_match("    switch (value) {"));
143        assert!(!op.pattern.is_match("    return Foo();"));
144        assert!(!op.pattern.is_match("    delete ptr;"));
145        assert!(!op
146            .pattern
147            .is_match("    throw std::runtime_error(\"err\");"));
148    }
149
150    #[test]
151    fn test_for_project_returns_distinct_sets() {
152        // secp256k1 has no Python functional tests, so its Python skip list is empty;
153        // Bitcoin Core's is not. This is a cheap proxy for "the sets differ".
154        let btc = for_project(Project::BitcoinCore);
155        let secp = for_project(Project::Secp256k1);
156        assert!(!btc.do_not_mutate_py_patterns().is_empty());
157        assert!(secp.do_not_mutate_py_patterns().is_empty());
158    }
159}