bitcoin-proptest 0.0.1-alpha.3

Proptest strategies for Bitcoin-related code
Documentation
use std::env;
use std::path::Path;

fn main() {
    let out_dir = env::var_os("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("strategy_list.rs");

    gen::strategy_list(&dest_path);
}

mod gen {
    use std::path::Path;

    use quote::quote;
    use tree_sitter::*;

    /// Generates list of strategies and functions generating sample data, all used by the crate's binary.
    pub fn strategy_list(path: &Path) {
        let mut parser = Parser::new();
        parser.set_language(&tree_sitter_rust::language()).unwrap();

        let generators = walk_file(
            Path::new("src"),
            "lib.rs",
            vec![String::from("bitcoin_proptest")],
            &mut parser,
        );

        let strategy_ids = generators.iter().map(|g| g.id());

        #[rustfmt::skip]
	let samples = generators
	    .iter()
	    .map(|g| {
		let id = g.id();
		let fun = syn::parse_str::<syn::Expr>(&g.function()).unwrap();
		quote! {
		    #id => {
			let s = #fun;
			println!("{}", s.new_tree(&mut runner).unwrap().current());
		    }
		}
	    })
	    .collect::<Vec<_>>();

        #[rustfmt::skip]
	let generated_module = quote! {
	    mod strategy_list {
		use proptest::strategy::ValueTree;
		use proptest::{strategy::Strategy, test_runner::TestRunner};

                // List of identifiers of usable strategies.
		pub const STRATEGIES: &[&str] = &[#(#strategy_ids),*];

                // Prints sample of given strategy
		pub fn sample(strategy_id: &str) {
		    let mut runner = TestRunner::new(Default::default());

		    match strategy_id {
			#(#samples)*
			_ => panic!("Unknown strategy, use -l to see list")
		    }
		}
	    }
	};

        std::fs::write(path, generated_module.to_string()).unwrap();
    }

    #[derive(Debug)]
    #[allow(dead_code)]
    struct Generator {
        pub path: Vec<String>,
        pub fn_name: String,
        pub comment: Option<String>,
    }

    impl Generator {
        pub fn function(&self) -> String {
            let path = self.path.join("::");
            format!("{path}::{}()", self.fn_name)
        }

        pub fn id(&self) -> String {
            let mut id = self.path.iter().skip(2).cloned().collect::<Vec<_>>();
            if &self.fn_name != "hex" && &self.fn_name != "string" {
                id.push(self.fn_name.clone());
            }
            id.join("/")
        }
    }

    fn walk_file(
        dir: &Path,
        file: &str,
        module: Vec<String>,
        parser: &mut Parser,
    ) -> Vec<Generator> {
        let code = std::fs::read_to_string(dir.join(file)).unwrap();
        let tree = parser.parse(code.clone(), None).unwrap();
        walk_node(dir, module, code.as_bytes(), &tree.root_node(), parser)
    }

    fn walk_mod(
        dir: &Path,
        code: &[u8],
        module: Vec<String>,
        name: &str,
        _comment: String,
        module_node: &Node,
        parser: &mut Parser,
    ) -> Vec<Generator> {
        if let Some(decl) = module_node
            .named_children(&mut module_node.walk())
            .find(|c| c.grammar_name() == "declaration_list")
        {
            walk_node(&dir.join(name), module, code, &decl, parser)
        } else {
            let mod_file_name = format!("{name}.rs");
            if dir.join(&mod_file_name).exists() {
                walk_file(dir, &mod_file_name, module, parser)
            } else {
                let mod_dir_path = dir.join(name);
                if mod_dir_path.join("mod.rs").exists() {
                    walk_file(&mod_dir_path, "mod.rs", module, parser)
                } else {
                    vec![]
                }
            }
        }
    }

    fn walk_node(
        dir: &Path,
        module: Vec<String>,
        code: &[u8],
        node: &Node,
        parser: &mut Parser,
    ) -> Vec<Generator> {
        node.named_children(&mut node.walk())
            .flat_map(|child| match child.grammar_name() {
                "mod_item" => {
                    let mod_name = child
                        .child_by_field_name("name")
                        .expect("module name")
                        .utf8_text(code)
                        .expect("valid name");
                    let comment = collect_comments(&child, code).join("\n");
                    if mod_name != "test" && mod_name != "tests" {
                        let mut module = module.clone();
                        module.push(mod_name.to_string());
                        walk_mod(dir, code, module, mod_name, comment, &child, parser)
                    } else {
                        vec![]
                    }
                }
                "function_item" => {
                    // At the moment only care about string strategies without parameters.
                    let is_string_strategy = child
                        .child_by_field_name("return_type")
                        .and_then(|n| n.utf8_text(code).ok())
                        == Some("impl Strategy<Value = String>");
                    let is_empty = child
                        .child_by_field_name("parameters")
                        .map(|n| n.named_child_count())
                        == Some(0);

                    if is_string_strategy && is_empty {
                        let comment = Some(collect_comments(&child, code).join("\n"))
                            .filter(|c| !c.is_empty());
                        let function_name = child.child_by_field_name("name").expect("xxx");
                        vec![Generator {
                            path: module.clone(),
                            fn_name: function_name.utf8_text(code).expect("yyy").to_string(),
                            comment,
                        }]
                    } else {
                        vec![]
                    }
                }
                _ => vec![],
            })
            .collect()
    }

    /// Collects comment lines above given node.
    fn collect_comments<'a>(node: &Node, code: &'a [u8]) -> Vec<&'a str> {
        node.prev_named_sibling()
            .filter(|prev| prev.grammar_name() == "line_comment")
            .map(|comment_node| {
                let mut lines_above = collect_comments(&comment_node, code);

                let current_line = comment_node
                    .utf8_text(code)
                    .ok()
                    .map(|c| c.trim_start_matches("///").trim());

                if let Some(line) = current_line {
                    lines_above.push(line);
                }

                lines_above
            })
            .unwrap_or_default()
    }
}