fn main() { generate_tests_from_spec() } // If the "gen-tests" feature is absent, // this function will be compiled down to nothing #[cfg(not(feature="gen-tests"))] fn generate_tests_from_spec() {} // If the feature is present, generate tests // from any .txt file present in the specs/ directory // // Test cases are present in the files in the // following format: // // ```````````````````````````````` example // markdown // . // expected html output // ```````````````````````````````` #[cfg(feature="gen-tests")] fn generate_tests_from_spec() { use std::path::{PathBuf}; use std::fs::{self, File}; use std::io::{Read, Write}; // This is a hardcoded path to the CommonMark spec because it is not situated in // the specs/ directory. It's in an array to easily chain it to the other iterator // and make it easy to eventually add other hardcoded paths in the future if needed let hardcoded = ["./third_party/CommonMark/spec.txt", "./third_party/GitHub/gfm_table.txt", "./third_party/GitHub/gfm_strikethrough.txt", "./third_party/GitHub/gfm_tasklist.txt"]; let hardcoded_iter = hardcoded.into_iter() .map(PathBuf::from); // Create an iterator over the files in the specs/ directory that have a .txt extension let spec_files = fs::read_dir("./specs") .expect("Could not find the 'specs' directory") .filter_map(Result::ok) .map(|d| d.path()) .filter(|p| p.extension().map(|e| e.to_owned()).is_some()) .chain(hardcoded_iter) .collect::>(); for file_path in &spec_files { let mut raw_spec = String::new(); File::open(&file_path) .and_then(|mut f| f.read_to_string(&mut raw_spec)) .expect("Could not read the spec file"); let rs_test_file = PathBuf::from("./tests/suite/") .join(file_path.file_name().expect("Invalid filename")) .with_extension("rs"); let mut spec_rs = File::create(&rs_test_file) .expect(&format!("Could not create {:?}", rs_test_file)); let spec_name = file_path.file_stem().unwrap().to_str().unwrap(); let spec = Spec::new(&raw_spec); let mut n_tests = 0; spec_rs.write(b"// This file is auto-generated by the build script\n").unwrap(); spec_rs.write(b"// Please, do not modify it manually\n").unwrap(); spec_rs.write(b"\nuse super::test_markdown_html;").unwrap(); for (i, testcase) in spec.enumerate() { spec_rs.write_fmt( format_args!( r###" #[test] fn {}_test_{i}() {{ let original = r##"{original}"##; let expected = r##"{expected}"##; test_markdown_html(original, expected); }}"###, spec_name, i=i+1, original=testcase.original, expected=testcase.expected ), ).unwrap(); n_tests += 1; } println!("cargo:warning=Generated {} tests in {:?}", n_tests, rs_test_file); } // write mods to suite/mod.rs let suite_mod_file = PathBuf::from("./tests/suite/mod").with_extension("rs"); let mut mod_rs = File::create(&suite_mod_file) .expect(&format!("Could not create {:?}", &suite_mod_file)); mod_rs.write(b"// This file is auto-generated by the build script\n").unwrap(); mod_rs.write(b"// Please, do not modify it manually\n").unwrap(); mod_rs.write(b"\npub use super::test_markdown_html;\n\n").unwrap(); for file_path in &spec_files { let mod_name = file_path.file_stem().unwrap().to_str().unwrap(); mod_rs.write(b"mod ").unwrap(); mod_rs.write(mod_name.as_bytes()).unwrap(); mod_rs.write(b";\n").unwrap(); } } #[cfg(feature="gen-tests")] pub struct Spec<'a> { spec: &'a str, } #[cfg(feature="gen-tests")] impl<'a> Spec<'a> { pub fn new(spec: &'a str) -> Self { Spec{ spec: spec } } } #[cfg(feature="gen-tests")] pub struct TestCase { pub original: String, pub expected: String, } #[cfg(feature="gen-tests")] impl<'a> Iterator for Spec<'a> { type Item = TestCase; fn next(&mut self) -> Option { let spec = self.spec; let i_start = match self.spec.find("```````````````````````````````` example\n").map(|pos| pos + 41) { Some(pos) => pos, None => return None, }; let i_end = match self.spec[i_start..].find("\n.\n").map(|pos| (pos + 1) + i_start){ Some(pos) => pos, None => return None, }; let e_end = match self.spec[i_end + 2..].find("````````````````````````````````\n").map(|pos| pos + i_end + 2){ Some(pos) => pos, None => return None, }; self.spec = &self.spec[e_end + 33 ..]; let test_case = TestCase { original: spec[i_start .. i_end].to_string().replace("→", "\t"), expected: spec[i_end + 2 .. e_end].to_string().replace("→", "\t") }; Some(test_case) } }