Skip to main content

bcore_mutation/
commands.rs

1//! Project-specific build and test commands used by folder-based analysis.
2//!
3//! When the user does not pass `--command`, `analyze` has to build the project
4//! and derive a test command for the mutated file itself. Those commands differ
5//! per project (Bitcoin Core uses CMake + `test_bitcoin` + the functional
6//! `test_runner.py`; secp256k1 uses CMake + CTest), so each project provides a
7//! [`ProjectCommands`] implementation, selected by [`for_project`].
8//!
9//! This mirrors the [`crate::operators`] module: a trait per concern, one
10//! implementation per project, and a single `for_project` selector.
11
12use crate::error::{MutationError, Result};
13use crate::project::Project;
14use std::path::Path;
15
16/// Build and test commands for a single project.
17pub trait ProjectCommands {
18    /// Full clean build, run once before analyzing a folder when the user did
19    /// not supply an explicit `--command`.
20    fn build_command(&self) -> String;
21
22    /// Timeout, in seconds, allowed for [`ProjectCommands::build_command`].
23    fn build_timeout_secs(&self) -> u64 {
24        3600 // 1 hour
25    }
26
27    /// Incremental-build-and-test command for the mutated `target_file_path`.
28    /// `jobs` is the parallelism passed to the compiler (0 = system default).
29    fn test_command(&self, target_file_path: &str, jobs: u32) -> Result<String>;
30}
31
32/// Return the [`ProjectCommands`] for the given project.
33pub fn for_project(project: Project) -> Box<dyn ProjectCommands> {
34    match project {
35        Project::BitcoinCore => Box::new(BitcoinCore),
36        Project::Secp256k1 => Box::new(Secp256k1),
37    }
38}
39
40/// Append ` -j{jobs}` to `build` when `jobs > 0`.
41fn with_jobs(mut build: String, jobs: u32) -> String {
42    if jobs > 0 {
43        build.push_str(&format!(" -j{}", jobs));
44    }
45    build
46}
47
48pub struct BitcoinCore;
49
50impl ProjectCommands for BitcoinCore {
51    fn build_command(&self) -> String {
52        "rm -rf build && cmake -B build -DENABLE_IPC=OFF && cmake --build build -j $(nproc)"
53            .to_string()
54    }
55
56    fn test_command(&self, target_file_path: &str, jobs: u32) -> Result<String> {
57        let build_command = with_jobs("cmake --build build".to_string(), jobs);
58
59        let command = if target_file_path.contains("functional") {
60            format!("./build/{}", target_file_path)
61        } else if target_file_path.contains("test") {
62            let filename_with_extension = Path::new(target_file_path)
63                .file_name()
64                .and_then(|n| n.to_str())
65                .ok_or_else(|| MutationError::InvalidInput("Invalid file path".to_string()))?;
66
67            let test_to_run = filename_with_extension
68                .rsplit('.')
69                .nth(1)
70                .ok_or_else(|| {
71                    MutationError::InvalidInput("Cannot extract test name".to_string())
72                })?;
73
74            format!(
75                "{} && ./build/bin/test_bitcoin --run_test={}",
76                build_command, test_to_run
77            )
78        } else {
79            format!(
80                "{} && ctest --output-on-failure --stop-on-failure -C Release && CI_FAILFAST_TEST_LEAVE_DANGLING=1 ./build/test/functional/test_runner.py -F",
81                build_command
82            )
83        };
84
85        Ok(command)
86    }
87}
88
89pub struct Secp256k1;
90
91impl ProjectCommands for Secp256k1 {
92    fn build_command(&self) -> String {
93        // secp256k1 builds with CMake (no IPC option). Tests are enabled by
94        // default; this is a starting point and may need extra feature flags
95        // (e.g. -DSECP256K1_BUILD_EXHAUSTIVE_TESTS) as the tool is exercised.
96        "rm -rf build && cmake -B build && cmake --build build -j $(nproc)".to_string()
97    }
98
99    fn test_command(&self, _target_file_path: &str, jobs: u32) -> Result<String> {
100        // secp256k1 does not map source files to per-file test binaries the way
101        // Bitcoin Core does, so we rebuild incrementally and run the whole CTest
102        // suite regardless of which file was mutated.
103        let build_command = with_jobs("cmake --build build".to_string(), jobs);
104        Ok(format!(
105            "{} && ctest --test-dir build --output-on-failure",
106            build_command
107        ))
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_bitcoin_core_test_command() {
117        let cmds = BitcoinCore;
118
119        // Functional test
120        let cmd = cmds
121            .test_command("test/functional/test_example.py", 4)
122            .unwrap();
123        assert_eq!(cmd, "./build/test/functional/test_example.py");
124
125        // Unit test
126        let cmd = cmds.test_command("src/test/test_example.cpp", 0).unwrap();
127        assert_eq!(
128            cmd,
129            "cmake --build build && ./build/bin/test_bitcoin --run_test=test_example"
130        );
131
132        // General case
133        let cmd = cmds.test_command("src/wallet/wallet.cpp", 2).unwrap();
134        assert!(cmd.contains("cmake --build build -j2"));
135        assert!(cmd.contains("ctest"));
136        assert!(cmd.contains("test_runner.py"));
137    }
138
139    #[test]
140    fn test_secp256k1_test_command_runs_ctest() {
141        let cmds = Secp256k1;
142        let cmd = cmds.test_command("src/field_impl.h", 3).unwrap();
143        assert!(cmd.contains("cmake --build build -j3"));
144        assert!(cmd.contains("ctest --test-dir build"));
145        // No Bitcoin Core specifics leak in.
146        assert!(!cmd.contains("test_bitcoin"));
147        assert!(!cmd.contains("test_runner.py"));
148    }
149
150    #[test]
151    fn test_build_commands_differ() {
152        assert!(BitcoinCore.build_command().contains("-DENABLE_IPC=OFF"));
153        assert!(!Secp256k1.build_command().contains("-DENABLE_IPC=OFF"));
154    }
155}