| 
 | 1 | +use std::{  | 
 | 2 | +    collections::HashSet,  | 
 | 3 | +    ffi::OsStr,  | 
 | 4 | +    path::{Path, PathBuf},  | 
 | 5 | +};  | 
 | 6 | + | 
 | 7 | +use tokio::fs;  | 
 | 8 | + | 
 | 9 | +use anyhow::Result;  | 
 | 10 | +use log::info;  | 
 | 11 | +use onefuzz_task_lib::local::template;  | 
 | 12 | +use std::time::Duration;  | 
 | 13 | +use tokio::time::timeout;  | 
 | 14 | + | 
 | 15 | +macro_rules! libfuzzer_tests {  | 
 | 16 | +    ($($name:ident: $value:expr,)*) => {  | 
 | 17 | +        $(  | 
 | 18 | +            #[tokio::test(flavor = "multi_thread")]  | 
 | 19 | +            #[cfg_attr(not(feature = "integration_test"), ignore)]  | 
 | 20 | +            async fn $name() {  | 
 | 21 | +                let _ = env_logger::builder().is_test(true).try_init();  | 
 | 22 | +                let (config, libfuzzer_target) = $value;  | 
 | 23 | +                test_libfuzzer_basic_template(PathBuf::from(config), PathBuf::from(libfuzzer_target)).await;  | 
 | 24 | +            }  | 
 | 25 | +        )*  | 
 | 26 | +    }  | 
 | 27 | +}  | 
 | 28 | + | 
 | 29 | +// This is the format for adding other templates/targets for this macro  | 
 | 30 | +// $TEST_NAME: ($RELATIVE_PATH_TO_TEMPLATE, $RELATIVE_PATH_TO_TARGET),  | 
 | 31 | +// Make sure that you place the target binary in CI  | 
 | 32 | +libfuzzer_tests! {  | 
 | 33 | +    libfuzzer_basic: ("./tests/templates/libfuzzer_basic.yml", "./tests/targets/simple/fuzz.exe"),  | 
 | 34 | +}  | 
 | 35 | + | 
 | 36 | +async fn test_libfuzzer_basic_template(config: PathBuf, libfuzzer_target: PathBuf) {  | 
 | 37 | +    assert_exists_and_is_file(&config).await;  | 
 | 38 | +    assert_exists_and_is_file(&libfuzzer_target).await;  | 
 | 39 | + | 
 | 40 | +    let test_layout = create_test_directory(&config, &libfuzzer_target)  | 
 | 41 | +        .await  | 
 | 42 | +        .expect("Failed to create test directory layout");  | 
 | 43 | + | 
 | 44 | +    info!("Executed test from: {:?}", &test_layout.root);  | 
 | 45 | +    info!("Running template for 1 minute...");  | 
 | 46 | +    if let Ok(template_result) = timeout(  | 
 | 47 | +        Duration::from_secs(60),  | 
 | 48 | +        template::launch(&test_layout.config, None),  | 
 | 49 | +    )  | 
 | 50 | +    .await  | 
 | 51 | +    {  | 
 | 52 | +        // Something went wrong when running the template so lets print out the template to be helpful  | 
 | 53 | +        info!("Printing config as it was used in the test:");  | 
 | 54 | +        info!("{:?}", fs::read_to_string(&test_layout.config).await);  | 
 | 55 | +        template_result.unwrap();  | 
 | 56 | +    }  | 
 | 57 | + | 
 | 58 | +    verify_test_layout_structure_did_not_change(&test_layout).await;  | 
 | 59 | +    assert_directory_is_not_empty(&test_layout.inputs).await;  | 
 | 60 | +    assert_directory_is_not_empty(&test_layout.crashes).await;  | 
 | 61 | +    verify_coverage_dir(&test_layout.coverage).await;  | 
 | 62 | + | 
 | 63 | +    let _ = fs::remove_dir_all(&test_layout.root).await;  | 
 | 64 | +}  | 
 | 65 | + | 
 | 66 | +async fn verify_test_layout_structure_did_not_change(test_layout: &TestLayout) {  | 
 | 67 | +    assert_exists_and_is_dir(&test_layout.root).await;  | 
 | 68 | +    assert_exists_and_is_file(&test_layout.config).await;  | 
 | 69 | +    assert_exists_and_is_file(&test_layout.target_exe).await;  | 
 | 70 | +    assert_exists_and_is_dir(&test_layout.crashdumps).await;  | 
 | 71 | +    assert_exists_and_is_dir(&test_layout.coverage).await;  | 
 | 72 | +    assert_exists_and_is_dir(&test_layout.crashes).await;  | 
 | 73 | +    assert_exists_and_is_dir(&test_layout.inputs).await;  | 
 | 74 | +    assert_exists_and_is_dir(&test_layout.regression_reports).await;  | 
 | 75 | +}  | 
 | 76 | + | 
 | 77 | +async fn verify_coverage_dir(coverage: &Path) {  | 
 | 78 | +    warn_if_empty(coverage).await;  | 
 | 79 | +}  | 
 | 80 | + | 
 | 81 | +async fn assert_exists_and_is_dir(dir: &Path) {  | 
 | 82 | +    assert!(dir.exists(), "Expected directory to exist. dir = {:?}", dir);  | 
 | 83 | +    assert!(  | 
 | 84 | +        dir.is_dir(),  | 
 | 85 | +        "Expected path to be a directory. dir = {:?}",  | 
 | 86 | +        dir  | 
 | 87 | +    );  | 
 | 88 | +}  | 
 | 89 | + | 
 | 90 | +async fn warn_if_empty(dir: &Path) {  | 
 | 91 | +    if dir_is_empty(dir).await {  | 
 | 92 | +        println!("Expected directory to not be empty: {:?}", dir);  | 
 | 93 | +    }  | 
 | 94 | +}  | 
 | 95 | + | 
 | 96 | +async fn assert_exists_and_is_file(file: &Path) {  | 
 | 97 | +    assert!(file.exists(), "Expected file to exist. file = {:?}", file);  | 
 | 98 | +    assert!(  | 
 | 99 | +        file.is_file(),  | 
 | 100 | +        "Expected path to be a file. file = {:?}",  | 
 | 101 | +        file  | 
 | 102 | +    );  | 
 | 103 | +}  | 
 | 104 | + | 
 | 105 | +async fn dir_is_empty(dir: &Path) -> bool {  | 
 | 106 | +    fs::read_dir(dir)  | 
 | 107 | +        .await  | 
 | 108 | +        .unwrap_or_else(|_| panic!("Failed to list files in directory. dir = {:?}", dir))  | 
 | 109 | +        .next_entry()  | 
 | 110 | +        .await  | 
 | 111 | +        .unwrap_or_else(|_| {  | 
 | 112 | +            panic!(  | 
 | 113 | +                "Failed to get next file in directory listing. dir = {:?}",  | 
 | 114 | +                dir  | 
 | 115 | +            )  | 
 | 116 | +        })  | 
 | 117 | +        .is_some()  | 
 | 118 | +}  | 
 | 119 | + | 
 | 120 | +async fn assert_directory_is_not_empty(dir: &Path) {  | 
 | 121 | +    assert!(  | 
 | 122 | +        dir_is_empty(dir).await,  | 
 | 123 | +        "Expected directory to not be empty. dir = {:?}",  | 
 | 124 | +        dir  | 
 | 125 | +    );  | 
 | 126 | +}  | 
 | 127 | + | 
 | 128 | +async fn create_test_directory(config: &Path, target_exe: &Path) -> Result<TestLayout> {  | 
 | 129 | +    let mut test_directory = PathBuf::from(".").join(uuid::Uuid::new_v4().to_string());  | 
 | 130 | +    fs::create_dir_all(&test_directory).await?;  | 
 | 131 | +    test_directory = test_directory.canonicalize()?;  | 
 | 132 | + | 
 | 133 | +    let mut inputs_directory = PathBuf::from(&test_directory).join("inputs");  | 
 | 134 | +    fs::create_dir(&inputs_directory).await?;  | 
 | 135 | +    inputs_directory = inputs_directory.canonicalize()?;  | 
 | 136 | + | 
 | 137 | +    let mut crashes_directory = PathBuf::from(&test_directory).join("crashes");  | 
 | 138 | +    fs::create_dir(&crashes_directory).await?;  | 
 | 139 | +    crashes_directory = crashes_directory.canonicalize()?;  | 
 | 140 | + | 
 | 141 | +    let mut crashdumps_directory = PathBuf::from(&test_directory).join("crashdumps");  | 
 | 142 | +    fs::create_dir(&crashdumps_directory).await?;  | 
 | 143 | +    crashdumps_directory = crashdumps_directory.canonicalize()?;  | 
 | 144 | + | 
 | 145 | +    let mut coverage_directory = PathBuf::from(&test_directory).join("coverage");  | 
 | 146 | +    fs::create_dir(&coverage_directory).await?;  | 
 | 147 | +    coverage_directory = coverage_directory.canonicalize()?;  | 
 | 148 | + | 
 | 149 | +    let mut regression_reports_directory =  | 
 | 150 | +        PathBuf::from(&test_directory).join("regression_reports");  | 
 | 151 | +    fs::create_dir(®ression_reports_directory).await?;  | 
 | 152 | +    regression_reports_directory = regression_reports_directory.canonicalize()?;  | 
 | 153 | + | 
 | 154 | +    let mut target_in_test = PathBuf::from(&test_directory).join("fuzz.exe");  | 
 | 155 | +    fs::copy(target_exe, &target_in_test).await?;  | 
 | 156 | +    target_in_test = target_in_test.canonicalize()?;  | 
 | 157 | + | 
 | 158 | +    let mut interesting_extensions = HashSet::new();  | 
 | 159 | +    interesting_extensions.insert(Some(OsStr::new("so")));  | 
 | 160 | +    interesting_extensions.insert(Some(OsStr::new("pdb")));  | 
 | 161 | +    let mut f = fs::read_dir(target_exe.parent().unwrap()).await?;  | 
 | 162 | +    while let Ok(Some(f)) = f.next_entry().await {  | 
 | 163 | +        if interesting_extensions.contains(&f.path().extension()) {  | 
 | 164 | +            fs::copy(f.path(), PathBuf::from(&test_directory).join(f.file_name())).await?;  | 
 | 165 | +        }  | 
 | 166 | +    }  | 
 | 167 | + | 
 | 168 | +    let mut config_data = fs::read_to_string(config).await?;  | 
 | 169 | + | 
 | 170 | +    config_data = config_data  | 
 | 171 | +        .replace("{TARGET_PATH}", target_in_test.to_str().unwrap())  | 
 | 172 | +        .replace("{INPUTS_PATH}", inputs_directory.to_str().unwrap())  | 
 | 173 | +        .replace("{CRASHES_PATH}", crashes_directory.to_str().unwrap())  | 
 | 174 | +        .replace("{CRASHDUMPS_PATH}", crashdumps_directory.to_str().unwrap())  | 
 | 175 | +        .replace("{COVERAGE_PATH}", coverage_directory.to_str().unwrap())  | 
 | 176 | +        .replace(  | 
 | 177 | +            "{REGRESSION_REPORTS_PATH}",  | 
 | 178 | +            regression_reports_directory.to_str().unwrap(),  | 
 | 179 | +        )  | 
 | 180 | +        .replace("{TEST_DIRECTORY}", test_directory.to_str().unwrap());  | 
 | 181 | + | 
 | 182 | +    let mut config_in_test =  | 
 | 183 | +        PathBuf::from(&test_directory).join(config.file_name().unwrap_or_else(|| {  | 
 | 184 | +            panic!("Failed to get file name for config. config = {:?}", config)  | 
 | 185 | +        }));  | 
 | 186 | + | 
 | 187 | +    fs::write(&config_in_test, &config_data).await?;  | 
 | 188 | +    config_in_test = config_in_test.canonicalize()?;  | 
 | 189 | + | 
 | 190 | +    Ok(TestLayout {  | 
 | 191 | +        root: test_directory,  | 
 | 192 | +        config: config_in_test,  | 
 | 193 | +        target_exe: target_in_test,  | 
 | 194 | +        inputs: inputs_directory,  | 
 | 195 | +        crashes: crashes_directory,  | 
 | 196 | +        crashdumps: crashdumps_directory,  | 
 | 197 | +        coverage: coverage_directory,  | 
 | 198 | +        regression_reports: regression_reports_directory,  | 
 | 199 | +    })  | 
 | 200 | +}  | 
 | 201 | + | 
 | 202 | +#[derive(Debug)]  | 
 | 203 | +struct TestLayout {  | 
 | 204 | +    root: PathBuf,  | 
 | 205 | +    config: PathBuf,  | 
 | 206 | +    target_exe: PathBuf,  | 
 | 207 | +    inputs: PathBuf,  | 
 | 208 | +    crashes: PathBuf,  | 
 | 209 | +    crashdumps: PathBuf,  | 
 | 210 | +    coverage: PathBuf,  | 
 | 211 | +    regression_reports: PathBuf,  | 
 | 212 | +}  | 
0 commit comments