#![allow(non_camel_case_types)] #![feature(string_remove_matches)] extern crate codebiber; use codebiber::{ Indentation, Config, generate, }; extern crate proptest; use proptest::prelude::*; #[macro_use] extern crate lazy_regex; extern crate blake3; extern crate unwrap_display; use unwrap_display::UnwrapDisplay; #[derive(Clone, Debug)] enum Section { HANDWRITTEN(String), GENERATED{code: String, name: String, surround: Surround, generated_with_config: Config, action: Action}, } #[derive(Clone, Debug)] enum Action { SKIP, KEEP, REPLACE_WITH(String), } use Section::*; use Action::*; fn format_input(sections: &[Section]) -> String { let mut out = String::new(); for s in sections.iter() { match s { HANDWRITTEN(c) => out += c.as_str(), GENERATED{code, name, generated_with_config, surround, action: _} => format_generated_code(&mut out, code.as_str(), name.as_str(), surround, *generated_with_config).unwrap(), } } out } fn format_expected_output(sections: &[Section], cfg: Config) -> Option { let mut has_some_change = false; let mut out = String::new(); for s in sections.iter() { match s { HANDWRITTEN(c) => out += c.as_str(), GENERATED{code: old_code, name, generated_with_config, surround, action} => { has_some_change = has_some_change || generated_with_config!=&cfg; let code = match action { SKIP | KEEP => old_code, REPLACE_WITH(new_code) => {has_some_change = has_some_change || new_code!=old_code; new_code} }; format_generated_code(&mut out, code.as_str(), name.as_str(), surround, cfg).unwrap() } } } match has_some_change { true => Some(out), false => None, } } fn format_generated_code(out: &mut String, code: &str, name: &str, surround: &Surround, config: Config) -> std::fmt::Result { let mut code = code.to_owned(); ensure_newline(&mut code); let hash = blake3::hash(code.as_bytes()).to_hex(); let code = surround.indent.indent_str(code.as_str()); use std::fmt::Write; surround.begin(out, Some(name))?; write!(out, "{code}")?; let suffix = match config.checksum_bytes_to_store { 0 => None, n => Some(&hash[..2*n as usize]), }; surround.end::<&str>(out, suffix) } #[derive(Clone, Debug)] struct Surround { begin: Surround_Marker, end: Surround_Marker, indent: Indentation, } #[derive(Clone, Debug)] struct Surround_Marker { before: String, after: String, } impl Surround { fn begin(&self, out: &mut String, suffix: Option) -> std::fmt::Result { self.begin.write(out, false, self.indent, suffix) } fn end(&self, out: &mut String, suffix: Option) -> std::fmt::Result { self.end.write(out, true, self.indent, suffix) } } impl Surround_Marker { fn write(&self, out: &mut String, closing: bool, indent: Indentation, suffix: Option) -> std::fmt::Result { use std::fmt::Write; ensure_newline(out); write!(out, "{}{}<< {}codegen", indent, self.before, if closing {"/"} else {""})?; if let Some(suffix) = suffix { write!(out, " {}", suffix)?; } writeln!(out, " >>{}", self.after)?; Ok(()) } } proptest! { #[test] fn roundtrip(sections in many_sections(), cfg in config()) { let input = format_input(§ions[..]); let expected = format_expected_output(§ions[..], cfg); let mut codes : Vec> = sections.iter().filter_map(|s| match s { HANDWRITTEN(..) => None, GENERATED { action: SKIP, .. } => Some(None), GENERATED { action: KEEP, code, .. } | GENERATED { action: REPLACE_WITH(code), .. } => Some(Some(code.clone())), }).collect(); codes.reverse(); let actual = generate(input.as_str(), cfg, move |_| Ok(codes.pop().unwrap())).expect_display_code(input.as_str()); assert_eq!(actual, expected, " input: {:?}", input.as_str()); } } fn many_sections() -> impl Strategy> { let action = prop_oneof![ Just(SKIP), Just(KEEP), code().prop_map(|code| REPLACE_WITH(code)), ]; let surround = surround(); let section = prop_oneof![ (code(), prop::bool::ANY).prop_map(|(code, tailing_linebreak)| Section::HANDWRITTEN(set_tailing_linebreak(code, tailing_linebreak))), (code(), ident(), config(), surround, action).prop_map(|(code, name, generated_with_config, surround, action)| Section::GENERATED{code, name, generated_with_config, surround, action}), ]; prop::collection::vec(section, 0..16) } fn config() -> impl Strategy { prop_oneof![ (0..(blake3::KEY_LEN as u8)).prop_map(|checksum_bytes_to_store| Config{ checksum_bytes_to_store, }), ] } fn surround() -> impl Strategy { let indent = (..u8::MAX).prop_map(|i| Indentation(i.into())); let surround = (surround_marker(), surround_marker(), indent).prop_map(|(begin, end, indent)| Surround{begin, end, indent}); surround } fn surround_marker() -> impl Strategy { let surround = (before_marker(), after_marker()).prop_map(|(before, after)| Surround_Marker{before, after}); surround } fn code() -> impl Strategy { "(\n)?(.*\n)*.*(\n)?".prop_map(remove_carriage_return).prop_filter( "regular code is not allowed to contain `<< codegen`", no_marker) } fn before_marker() -> impl Strategy { "([^ \n]+[^\n]*)?" .prop_filter("regular code is not allowed to contain `<< codegen`", |code| no_marker(code) && !code.ends_with('<') && !code.contains("<<")) .prop_filter("surround_marker() /* before */", |code| no_marker(code) && !code.ends_with('<') && !code.contains("<<")) } fn after_marker() -> impl Strategy { "([^\n]*[^ \n][^\n]*)?".prop_filter( "regular code is not allowed to contain `<< codegen`", no_marker) } fn no_marker>(code: &S) -> bool {!regex_is_match!("<< *\\/?codegen", code.as_ref())} fn ident() -> impl Strategy { "[_a-zA-Z][_a-zA-Z0-9]*" } fn set_tailing_linebreak(mut code: String, expect_tailing_linebreak: bool) -> String { let len = code.len(); let ends_with_linebreak = code.as_bytes().last().cloned() == Some(b'\n'); match (expect_tailing_linebreak, ends_with_linebreak) { (false, false) | (true, true) => (), (false, true) => code.truncate(len-1), (true, false) => code.push('\n'), } code } fn remove_carriage_return(mut code: String) -> String { code.remove_matches('\r'); code } fn ensure_newline(code: &mut String) { if !code.is_empty() && !code.ends_with('\n') {code.push('\n');} }