| use googletest::matchers; |
| use googletest::prelude::*; |
| use std::process::Command; |
| use test_utils::EnvVars; |
| |
| #[gtest] |
| fn standalone_mode_invokes_centipede(fixture: &EnvVars) { |
| let target_binary_path = |
| fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin"); |
| let test_name = "__fuzztest_mod__standalone_validation_test::standalone_validation_test"; |
| |
| let process = Command::new(&target_binary_path) |
| .arg(test_name) |
| .env("FUZZTEST_FUZZ_FOR", "3s") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output = process.wait_with_output().expect("Should terminate"); |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| |
| // Assert that Centipede actually ran the test binary, and forwarded the worker log. |
| // We check for "LOG: STANDALONE_VALIDATION_WORKER_EXECUTED". The prefix "LOG: " indicates that |
| // this output is from centipede forwarding the output of the worker (due to --print_runner_log), |
| // whereas the rest is from the fuzztest itself. |
| expect_that!( |
| stderr, |
| matchers::contains_substring("LOG: STANDALONE_VALIDATION_WORKER_EXECUTED") |
| ); |
| } |
| |
| #[gtest] |
| fn standalone_mode_invokes_the_correct_fuzztest(fixture: &EnvVars) { |
| let target_binary_path = |
| fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin"); |
| let test_name = "__fuzztest_mod__standalone_validation_test::standalone_validation_test"; |
| |
| let process = Command::new(&target_binary_path) |
| .arg(test_name) |
| .env("FUZZTEST_FUZZ_FOR", "3s") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output = process.wait_with_output().expect("Should terminate"); |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| |
| // Check that the first test is executed |
| expect_that!( |
| stderr, |
| matchers::contains_substring("LOG: STANDALONE_VALIDATION_WORKER_EXECUTED") |
| ); |
| |
| // Check that the second test does not get executed |
| expect_that!(stderr, not(matchers::contains_substring("SECOND_VALIDATION_TEST_EXECUTED"))); |
| } |
| |
| #[gtest] |
| fn standalone_mode_handles_worker_crash(fixture: &EnvVars) { |
| let test_name = "__fuzztest_mod__find_bug_fuzz_test::find_bug_fuzz_test"; |
| |
| let process = Command::new(&fixture.target_binary_path) |
| .arg(test_name) |
| .env("FUZZTEST_FUZZ_FOR", "5s") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output = process.wait_with_output().expect("Should terminate"); |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| |
| expect_that!(stderr, matchers::contains_substring("Property function ran but crashed.")); |
| expect_that!(stderr, matchers::contains_regex("Signature[ \t]*: Unwinding panic")); |
| // Centipede prefixes logs from the crashing worker with "CRASH LOG: ". |
| expect_that!(stderr, matchers::contains_substring("CRASH LOG: Bug found!")); |
| } |
| |
| #[gtest] |
| fn standalone_mode_spawns_parallel_jobs(fixture: &EnvVars) { |
| let target_binary_path = |
| fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin"); |
| let test_name = "__fuzztest_mod__jobs_test::jobs_test"; |
| |
| let process = Command::new(&target_binary_path) |
| .arg(test_name) |
| .env("FUZZTEST_FUZZ_FOR", "5s") |
| .env("FUZZTEST_JOBS", "4") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output = process.wait_with_output().expect("Should terminate"); |
| let stderr = String::from_utf8_lossy(&output.stderr); |
| |
| // Extract all PIDs from logs. |
| // Expected format: "LOG: JOBS_TEST_PID: <pid>" |
| use std::collections::HashSet; |
| let mut pids = HashSet::new(); |
| for line in stderr.lines() { |
| if let Some(pos) = line.find("LOG: JOBS_TEST_PID: ") { |
| let pid_str = &line[pos + "LOG: JOBS_TEST_PID: ".len()..]; |
| if let Ok(pid) = pid_str.parse::<u32>() { |
| pids.insert(pid); |
| } |
| } |
| } |
| |
| // We expect 4 different PIDs |
| expect_that!(pids.len(), eq(4)); |
| } |
| |
| #[gtest] |
| fn standalone_mode_replay_corpus_per_test_budget(fixture: &EnvVars) { |
| let target_binary_path = |
| fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin"); |
| |
| let corpus_db = fixture.tmp_dir_path.join("corpus_db_per_test"); |
| let work_dir_root = fixture.tmp_dir_path.join("WD_root_per_test"); |
| |
| // Centipede appends the binary identifier (relative path) to the corpus database path. |
| let identifier = target_binary_path.to_str().unwrap(); |
| let identifier_relative = identifier.strip_prefix('/').unwrap_or(identifier); |
| |
| // Create corpus dirs for tests |
| let test1_dir = corpus_db |
| .join(identifier_relative) |
| .join("__fuzztest_mod__standalone_validation_test.standalone_validation_test") |
| .join("coverage"); |
| let test2_dir = corpus_db |
| .join(identifier_relative) |
| .join("__fuzztest_mod__second_validation_test.second_validation_test") |
| .join("coverage"); |
| |
| std::fs::create_dir_all(&test1_dir).unwrap(); |
| std::fs::create_dir_all(&test2_dir).unwrap(); |
| |
| // Write some corpus inputs. |
| // Assuming Arbitrary<i32> consumes 4 bytes. |
| use std::fs::File; |
| use std::io::Write; |
| |
| let mut file1 = File::create(test1_dir.join("input1")).unwrap(); |
| file1.write_all(&[1, 0, 0, 0]).unwrap(); |
| |
| let mut file2 = File::create(test1_dir.join("input2")).unwrap(); |
| file2.write_all(&[2, 0, 0, 0]).unwrap(); |
| |
| let mut file3 = File::create(test2_dir.join("input3")).unwrap(); |
| file3.write_all(&[3, 0, 0, 0]).unwrap(); |
| |
| let process_per_test = Command::new(&target_binary_path) |
| .arg("__fuzztest_mod__") |
| .env("FUZZTEST_REPLAY_CORPUS_FOR", "3s") |
| .env("FUZZTEST_TIME_BUDGET_TYPE", "per-test") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("FUZZTEST_CORPUS_DB", corpus_db.to_str().unwrap()) |
| .env("FUZZTEST_WORKDIR_ROOT", work_dir_root.to_str().unwrap()) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output_per_test = process_per_test.wait_with_output().expect("Should terminate"); |
| let stderr_per_test = String::from_utf8_lossy(&output_per_test.stderr); |
| |
| // Verify logs for per-test |
| expect_that!( |
| stderr_per_test, |
| matchers::contains_substring("Replaying __fuzztest_mod__standalone_validation_test.standalone_validation_test for 3s") |
| ); |
| expect_that!( |
| stderr_per_test, |
| matchers::contains_substring( |
| "Replaying __fuzztest_mod__second_validation_test.second_validation_test for 3s" |
| ) |
| ); |
| |
| // Count executions for per-test |
| let mut val1_count = 0; |
| let mut val2_count = 0; |
| for line in stderr_per_test.lines() { |
| if line.contains("STANDALONE_VALIDATION_INPUT:") { |
| val1_count += 1; |
| } |
| if line.contains("SECOND_VALIDATION_INPUT:") { |
| val2_count += 1; |
| } |
| } |
| // we had 2 inputs for first test |
| expect_that!(val1_count, eq(2)); |
| // and 1 input for second test |
| expect_that!(val2_count, eq(1)); |
| } |
| |
| #[gtest] |
| fn standalone_mode_replay_corpus_total_budget(fixture: &EnvVars) { |
| let target_binary_path = |
| fixture.target_binary_path.parent().unwrap().join("standalone_fuzz_tests_bin"); |
| |
| let corpus_db = fixture.tmp_dir_path.join("corpus_db_total"); |
| let work_dir_root = fixture.tmp_dir_path.join("WD_root_total"); |
| |
| // Centipede appends the binary identifier (relative path) to the corpus database path. |
| let identifier = target_binary_path.to_str().unwrap(); |
| let identifier_relative = identifier.strip_prefix('/').unwrap_or(identifier); |
| |
| // Create corpus dirs for tests |
| let test1_dir = corpus_db |
| .join(identifier_relative) |
| .join("__fuzztest_mod__standalone_validation_test.standalone_validation_test") |
| .join("coverage"); |
| let test2_dir = corpus_db |
| .join(identifier_relative) |
| .join("__fuzztest_mod__second_validation_test.second_validation_test") |
| .join("coverage"); |
| |
| std::fs::create_dir_all(&test1_dir).unwrap(); |
| std::fs::create_dir_all(&test2_dir).unwrap(); |
| |
| // Write some corpus inputs. |
| // Assuming Arbitrary<i32> consumes 4 bytes. |
| use std::fs::File; |
| use std::io::Write; |
| |
| let mut file1 = File::create(test1_dir.join("input1")).unwrap(); |
| file1.write_all(&[1, 0, 0, 0]).unwrap(); |
| |
| let mut file2 = File::create(test1_dir.join("input2")).unwrap(); |
| file2.write_all(&[2, 0, 0, 0]).unwrap(); |
| |
| let mut file3 = File::create(test2_dir.join("input3")).unwrap(); |
| file3.write_all(&[3, 0, 0, 0]).unwrap(); |
| |
| // We have 3 fuzz tests in the binary. Total 3s / 3 = 1s per test. |
| let process_total = Command::new(&target_binary_path) |
| .arg("__fuzztest_mod__") |
| .env("FUZZTEST_REPLAY_CORPUS_FOR", "3s") |
| .env("FUZZTEST_TIME_BUDGET_TYPE", "total") |
| .env("FUZZTEST_PRINT_SUBPROCESS_LOG", "true") |
| .env("FUZZTEST_CENTIPEDE_BINARY_PATH", &fixture.centipede_path) |
| .env("FUZZTEST_CORPUS_DB", corpus_db.to_str().unwrap()) |
| .env("FUZZTEST_WORKDIR_ROOT", work_dir_root.to_str().unwrap()) |
| .env("RUST_TEST_NOCAPTURE", "1") |
| .stdout(std::process::Stdio::piped()) |
| .stderr(std::process::Stdio::piped()) |
| .spawn() |
| .expect("Failed to spawn binary"); |
| |
| let output_total = process_total.wait_with_output().expect("Should terminate"); |
| let stderr_total = String::from_utf8_lossy(&output_total.stderr); |
| |
| // Verify logs for total -> should be 1s |
| expect_that!( |
| stderr_total, |
| matchers::contains_substring("Replaying __fuzztest_mod__standalone_validation_test.standalone_validation_test for 1s") |
| ); |
| expect_that!( |
| stderr_total, |
| matchers::contains_substring( |
| "Replaying __fuzztest_mod__second_validation_test.second_validation_test for 1s" |
| ) |
| ); |
| |
| // Counts should still match because it finishes early anyway |
| let mut val1_count_total = 0; |
| let mut val2_count_total = 0; |
| for line in stderr_total.lines() { |
| if line.contains("STANDALONE_VALIDATION_INPUT:") { |
| val1_count_total += 1; |
| } |
| if line.contains("SECOND_VALIDATION_INPUT:") { |
| val2_count_total += 1; |
| } |
| } |
| // we had 2 inputs for first test |
| expect_that!(val1_count_total, eq(2)); |
| // and 1 input for second test |
| expect_that!(val2_count_total, eq(1)); |
| } |