From 196339aaa9aad0cf9bdc4ef381f008c7c1651c54 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Jan 2019 14:22:35 -0800 Subject: [PATCH] Assert no memory leaks by stubbing malloc/free in the test suite --- Cargo.lock | 7 +++ cli/Cargo.toml | 3 + cli/src/tests/allocations.rs | 104 +++++++++++++++++++++++++++++++++++ cli/src/tests/corpuses.rs | 29 +++++----- cli/src/tests/mod.rs | 1 + lib/build.rs | 18 +++++- lib/src/subtree.c | 2 +- 7 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 cli/src/tests/allocations.rs diff --git a/Cargo.lock b/Cargo.lock index 003978c1..936c60ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,6 +502,11 @@ name = "smallbitvec" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "spin" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "strsim" version = "0.7.0" @@ -585,6 +590,7 @@ dependencies = [ "serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)", "smallbitvec 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "tree-sitter 0.3.5", ] @@ -702,6 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)" = "225de307c6302bec3898c51ca302fc94a7a1697ef0845fcee6448f33c032249c" "checksum serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)" = "c37ccd6be3ed1fdf419ee848f7c758eb31b054d7cd3ae3600e3bae0adf569811" "checksum smallbitvec 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1764fe2b30ee783bfe3b9b37b2649d8d590b3148bb12e0079715d4d5c673562e" +"checksum spin 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44363f6f51401c34e7be73db0db371c04705d35efbe9f7d6082e03a921a32c55" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" "checksum syn 0.15.22 (registry+https://github.com/rust-lang/crates.io-index)" = "ae8b29eb5210bc5cf63ed6149cbf9adfc82ac0be023d8735c176ee74a2db4da7" "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d8e50bbf..5eb92079 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -34,3 +34,6 @@ features = ["preserve_order"] [dependencies.log] version = "0.4.6" features = ["std"] + +[dev-dependencies] +spin = "0.5" diff --git a/cli/src/tests/allocations.rs b/cli/src/tests/allocations.rs new file mode 100644 index 00000000..c4a3dbac --- /dev/null +++ b/cli/src/tests/allocations.rs @@ -0,0 +1,104 @@ +#![cfg(test)] +#![allow(dead_code)] + +use spin::Mutex; +use std::collections::HashMap; +use std::os::raw::{c_ulong, c_void}; + +#[derive(Debug, PartialEq, Eq, Hash)] +struct Allocation(*const c_void); +unsafe impl Send for Allocation {} +unsafe impl Sync for Allocation {} + +#[derive(Default)] +struct AllocationRecorder { + enabled: bool, + allocation_count: u64, + outstanding_allocations: HashMap, +} + +lazy_static! { + static ref RECORDER: Mutex = Mutex::new(AllocationRecorder::default()); +} + +extern "C" { + fn malloc(size: c_ulong) -> *mut c_void; + fn calloc(count: c_ulong, size: c_ulong) -> *mut c_void; + fn realloc(ptr: *mut c_void, size: c_ulong) -> *mut c_void; + fn free(ptr: *mut c_void); +} + +pub fn start_recording() { + let mut recorder = RECORDER.lock(); + recorder.enabled = true; + recorder.allocation_count = 0; + recorder.outstanding_allocations.clear(); +} + +pub fn stop_recording() { + let mut recorder = RECORDER.lock(); + recorder.enabled = false; + + if !recorder.outstanding_allocations.is_empty() { + panic!( + "Leaked allocation indices: {:?}", + recorder + .outstanding_allocations + .iter() + .map(|e| e.1) + .collect::>() + ); + } +} + +fn record_alloc(ptr: *mut c_void) { + let mut recorder = RECORDER.lock(); + if recorder.enabled { + let count = recorder.allocation_count; + recorder.allocation_count += 1; + recorder + .outstanding_allocations + .insert(Allocation(ptr), count); + } +} + +fn record_dealloc(ptr: *mut c_void) { + let mut recorder = RECORDER.lock(); + if recorder.enabled { + recorder.outstanding_allocations.remove(&Allocation(ptr)); + } +} + +#[no_mangle] +extern "C" fn ts_record_malloc(size: c_ulong) -> *const c_void { + let result = unsafe { malloc(size) }; + record_alloc(result); + result +} + +#[no_mangle] +extern "C" fn ts_record_calloc(count: c_ulong, size: c_ulong) -> *const c_void { + let result = unsafe { calloc(count, size) }; + record_alloc(result); + result +} + +#[no_mangle] +extern "C" fn ts_record_realloc(ptr: *mut c_void, size: c_ulong) -> *const c_void { + record_dealloc(ptr); + let result = unsafe { realloc(ptr, size) }; + record_alloc(result); + result +} + +#[no_mangle] +extern "C" fn ts_record_free(ptr: *mut c_void) { + record_dealloc(ptr); + unsafe { free(ptr) }; +} + +#[no_mangle] +extern "C" fn ts_record_allocations_toggle() { + let mut recorder = RECORDER.lock(); + recorder.enabled = !recorder.enabled; +} diff --git a/cli/src/tests/corpuses.rs b/cli/src/tests/corpuses.rs index 6d46aacb..2c205d40 100644 --- a/cli/src/tests/corpuses.rs +++ b/cli/src/tests/corpuses.rs @@ -1,9 +1,10 @@ +use super::allocations; use super::fixtures::{fixtures_dir, get_language, get_test_language}; use crate::generate; use crate::test::{parse_tests, print_diff, print_diff_key, TestEntry}; use crate::util; use std::fs; -use tree_sitter::{LogType, Parser}; +use tree_sitter::{Language, LogType, Parser}; const LANGUAGES: &'static [&'static str] = &[ "bash", @@ -27,8 +28,6 @@ lazy_static! { #[test] fn test_real_language_corpus_files() { - let mut log_session = None; - let mut parser = get_parser(&mut log_session, "log1.html"); let grammars_dir = fixtures_dir().join("grammars"); let mut did_fail = false; @@ -44,8 +43,7 @@ fn test_real_language_corpus_files() { let language = get_language(language_name); let corpus_dir = grammars_dir.join(language_name).join("corpus"); let test = parse_tests(&corpus_dir).unwrap(); - parser.set_language(language).unwrap(); - did_fail |= run_mutation_tests(&mut parser, test); + did_fail |= run_mutation_tests(language, test); } if did_fail { @@ -55,8 +53,6 @@ fn test_real_language_corpus_files() { #[test] fn test_error_corpus_files() { - let mut log_session = None; - let mut parser = get_parser(&mut log_session, "log2.html"); let corpus_dir = fixtures_dir().join("error_corpus"); let mut did_fail = false; @@ -74,8 +70,7 @@ fn test_error_corpus_files() { let test = parse_tests(&entry.path()).unwrap(); let language = get_language(&language_name); - parser.set_language(language).unwrap(); - did_fail |= run_mutation_tests(&mut parser, test); + did_fail |= run_mutation_tests(language, test); } if did_fail { @@ -85,8 +80,6 @@ fn test_error_corpus_files() { #[test] fn test_feature_corpus_files() { - let mut log_session = None; - let mut parser = get_parser(&mut log_session, "log3.html"); let test_grammars_dir = fixtures_dir().join("test_grammars"); let mut did_fail = false; @@ -132,8 +125,7 @@ fn test_feature_corpus_files() { let c_code = generate_result.unwrap().1; let language = get_test_language(language_name, c_code, &test_path); let test = parse_tests(&corpus_path).unwrap(); - parser.set_language(language).unwrap(); - did_fail |= run_mutation_tests(&mut parser, test); + did_fail |= run_mutation_tests(language, test); } } @@ -142,7 +134,7 @@ fn test_feature_corpus_files() { } } -fn run_mutation_tests(parser: &mut Parser, test: TestEntry) -> bool { +fn run_mutation_tests(language: Language, test: TestEntry) -> bool { match test { TestEntry::Example { name, @@ -157,23 +149,30 @@ fn run_mutation_tests(parser: &mut Parser, test: TestEntry) -> bool { eprintln!(" example: {:?}", name); + allocations::start_recording(); + let mut log_session = None; + let mut parser = get_parser(&mut log_session, "log.html"); + parser.set_language(language).unwrap(); let tree = parser .parse_utf8(&mut |byte_offset, _| &input[byte_offset..], None) .unwrap(); let actual = tree.root_node().to_sexp(); + drop(tree); + drop(parser); if actual != output { print_diff_key(); print_diff(&actual, &output); println!(""); true } else { + allocations::stop_recording(); false } } TestEntry::Group { children, .. } => { let mut result = false; for child in children { - result |= run_mutation_tests(parser, child); + result |= run_mutation_tests(language, child); } result } diff --git a/cli/src/tests/mod.rs b/cli/src/tests/mod.rs index a874358a..174be67b 100644 --- a/cli/src/tests/mod.rs +++ b/cli/src/tests/mod.rs @@ -1,3 +1,4 @@ +mod allocations; mod corpuses; mod fixtures; mod parser_api; diff --git a/lib/build.rs b/lib/build.rs index 2a121001..df66ee7c 100644 --- a/lib/build.rs +++ b/lib/build.rs @@ -1,6 +1,6 @@ extern crate cc; -use std::env; +use std::{env, fs}; use std::path::{Path, PathBuf}; fn main() { @@ -20,13 +20,27 @@ fn main() { } let mut config = cc::Build::new(); + + println!("cargo:rerun-if-env-changed=TREE_SITTER_TEST"); + if env::var("TREE_SITTER_TEST").is_ok() { + config.define("TREE_SITTER_TEST", ""); + } + + let src_path = Path::new("src"); + + for entry in fs::read_dir(&src_path).unwrap() { + let entry = entry.unwrap(); + let path = src_path.join(entry.file_name()); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } + config .define("UTF8PROC_STATIC", "") .flag_if_supported("-std=c99") .flag_if_supported("-Wno-unused-parameter") .include("include") .include("utf8proc") - .file(Path::new("src").join("lib.c")) + .file(src_path.join("lib.c")) .compile("tree-sitter"); } diff --git a/lib/src/subtree.c b/lib/src/subtree.c index 48c8cff3..3e353f99 100644 --- a/lib/src/subtree.c +++ b/lib/src/subtree.c @@ -855,7 +855,7 @@ char *ts_subtree_string(Subtree self, const TSLanguage *language, bool include_a language, true, include_all, 0, false ) + 1; - char *result = ts_malloc(size * sizeof(char)); + char *result = malloc(size * sizeof(char)); ts_subtree__write_to_string(self, result, size, language, true, include_all, 0, false); return result; }