blob: 17c5bf055783a90cb9f35eb9ca2d8e05f0beeede [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
#![forbid(unsafe_code)]
use std::{
borrow::Cow,
fs::File,
io::Write,
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
};
use autocxx_engine::{get_clang_path, make_clang_args, preprocess};
use autocxx_parser::IncludeCppConfig;
use clap::{crate_authors, crate_version, Arg, ArgMatches, Command};
use indexmap::IndexSet;
use indoc::indoc;
use itertools::Itertools;
use quote::ToTokens;
use regex::Regex;
use tempfile::TempDir;
static LONG_HELP: &str = indoc! {"
Command line utility to minimize autocxx bug cases.
This is a wrapper for creduce.
Example command-line:
autocxx-reduce file -I my-inc-dir -h my-header -d 'generate!(\"MyClass\")' -k -- --n 64
"};
fn main() {
let matches = Command::new("autocxx-reduce")
.version(crate_version!())
.author(crate_authors!())
.about("Reduce a C++ test case")
.long_about(LONG_HELP)
.subcommand(Command::new("file")
.about("reduce a header file")
.arg(
Arg::new("inc")
.short('I')
.long("inc")
.multiple_occurrences(true)
.number_of_values(1)
.value_name("INCLUDE DIRS")
.help("include path")
.takes_value(true),
)
.arg(
Arg::new("define")
.short('D')
.long("define")
.multiple_occurrences(true)
.number_of_values(1)
.value_name("DEFINE")
.help("macro definition")
.takes_value(true),
)
.arg(
Arg::new("header")
.long("header")
.multiple_occurrences(true)
.number_of_values(1)
.required(true)
.value_name("HEADER")
.help("header file name")
.takes_value(true),
)
.arg(
Arg::new("directive")
.short('d')
.long("directive")
.multiple_occurrences(true)
.number_of_values(1)
.value_name("DIRECTIVE")
.help("directives to put within include_cpp!")
.takes_value(true),
)
)
.subcommand(Command::new("repro")
.about("reduce a repro case JSON file")
.arg(
Arg::new("repro")
.short('r')
.long("repro")
.required(true)
.value_name("REPRODUCTION CASE JSON")
.help("reproduction case JSON file name")
.takes_value(true),
)
.arg(
Arg::new("header")
.long("header")
.multiple_occurrences(true)
.number_of_values(1)
.value_name("HEADER")
.help("header file name; specify to resume a part-completed run")
.takes_value(true),
)
)
.arg(
Arg::new("problem")
.short('p')
.long("problem")
.required(true)
.value_name("PROBLEM")
.help("problem string we're looking for... may be in logs, or in generated C++, or generated .rs")
.takes_value(true),
)
.arg(
Arg::new("creduce")
.long("creduce")
.value_name("PATH")
.help("creduce binary location")
.default_value("creduce")
.takes_value(true),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("OUTPUT")
.help("where to write minimized output")
.takes_value(true),
)
.arg(
Arg::new("gen-cmd")
.short('g')
.long("gen-cmd")
.value_name("GEN-CMD")
.help("where to find autocxx-gen")
.takes_value(true),
)
.arg(
Arg::new("keep")
.short('k')
.long("keep-dir")
.help("keep the temporary directory for debugging purposes"),
)
.arg(
Arg::new("clang-args")
.short('c')
.long("clang-arg")
.multiple_occurrences(true)
.value_name("CLANG_ARG")
.help("Extra arguments to pass to Clang"),
)
.arg(
Arg::new("creduce-args")
.long("creduce-arg")
.multiple_occurrences(true)
.value_name("CREDUCE_ARG")
.help("Extra arguments to pass to Clang"),
)
.arg(
Arg::new("no-precompile")
.long("no-precompile")
.help("Do not precompile the C++ header before passing to autocxxgen"),
)
.arg(
Arg::new("no-postcompile")
.long("no-postcompile")
.help("Do not post-compile the C++ generated by autocxxgen"),
)
.arg(
Arg::new("suppress-cxx-inclusions")
.long("suppress-cxx-inclusions")
.takes_value(true)
.possible_value("yes")
.possible_value("no")
.possible_value("auto")
.default_value("auto")
.help("Whether the preprocessed header already includes cxx.h. If so, we'll try to suppress the natural behavior of cxx to include duplicate definitions of some of the types within gen0.cc.")
)
.arg_required_else_help(true)
.get_matches();
run(matches).unwrap();
}
fn run(matches: ArgMatches) -> Result<(), std::io::Error> {
let keep_tmp = matches.is_present("keep");
let tmp_dir = TempDir::new()?;
let r = do_run(matches, &tmp_dir);
if keep_tmp {
println!(
"Keeping temp dir created at: {}",
tmp_dir.into_path().to_str().unwrap()
);
}
r
}
#[derive(serde_derive::Deserialize)]
struct ReproCase {
config: String,
header: String,
}
fn do_run(matches: ArgMatches, tmp_dir: &TempDir) -> Result<(), std::io::Error> {
let rs_path = tmp_dir.path().join("input.rs");
let concat_path = tmp_dir.path().join("concat.h");
match matches.subcommand_matches("repro") {
None => {
let submatches = matches.subcommand_matches("file").unwrap();
let incs: Vec<_> = submatches
.values_of("inc")
.unwrap_or_default()
.map(PathBuf::from)
.collect();
let defs: Vec<_> = submatches.values_of("define").unwrap_or_default().collect();
let headers: Vec<_> = submatches.values_of("header").unwrap_or_default().collect();
assert!(!headers.is_empty());
let listing_path = tmp_dir.path().join("listing.h");
create_concatenated_header(&headers, &listing_path)?;
announce_progress(&format!(
"Preprocessing {:?} to {:?}",
listing_path, concat_path
));
preprocess(&listing_path, &concat_path, &incs, &defs)?;
let directives: Vec<_> = std::iter::once("#include \"concat.h\"\n".to_string())
.chain(
submatches
.values_of("directive")
.unwrap_or_default()
.map(|s| format!("{}\n", s)),
)
.collect();
create_rs_file(&rs_path, &directives)?;
}
Some(submatches) => {
let case: ReproCase = serde_json::from_reader(File::open(PathBuf::from(
submatches.value_of("repro").unwrap(),
))?)
.unwrap();
// Replace the headers in the config
let mut config: IncludeCppConfig = syn::parse_str(&case.config).unwrap();
config.replace_included_headers("concat.h");
create_file(
&rs_path,
&format!("autocxx::include_cpp!({});", config.to_token_stream()),
)?;
if let Some(header) = submatches.value_of("header") {
std::fs::copy(PathBuf::from(header), &concat_path)?;
} else {
create_file(&concat_path, &case.header)?
}
}
}
let suppress_cxx_classes = match matches.value_of("suppress-cxx-inclusions").unwrap() {
"yes" => true,
"no" => false,
"auto" => detect_cxx_h(&concat_path)?,
_ => panic!("unexpected value"),
};
let cxx_suppressions = if suppress_cxx_classes {
get_cxx_suppressions()
} else {
Vec::new()
};
let extra_clang_args: Vec<_> = matches
.values_of("clang-args")
.unwrap_or_default()
.map(Cow::Borrowed)
.chain(cxx_suppressions.into_iter().map(Cow::Owned))
.collect();
let extra_clang_args: Vec<&str> = extra_clang_args.iter().map(|s| s.as_ref()).collect_vec();
let default_gen_cmd = std::env::current_exe()?
.parent()
.unwrap()
.join("autocxx-gen")
.to_str()
.unwrap()
.to_string();
let gen_cmd = matches.value_of("gen-cmd").unwrap_or(&default_gen_cmd);
if !Path::new(gen_cmd).exists() {
panic!(
"autocxx-gen not found in {}. hint: autocxx-reduce --gen-cmd /path/to/autocxx-gen",
gen_cmd
);
}
run_sample_gen_cmd(gen_cmd, &rs_path, tmp_dir.path(), &extra_clang_args)?;
// Create and run an interestingness test which does not filter its output through grep.
let demo_interestingness_test_dir = tmp_dir.path().join("demo-interestingness-test");
std::fs::create_dir(&demo_interestingness_test_dir).unwrap();
let interestingness_test = demo_interestingness_test_dir.join("test-demo.sh");
create_interestingness_test(
gen_cmd,
&interestingness_test,
None,
&rs_path,
&extra_clang_args,
!matches.is_present("no-precompile"),
!matches.is_present("no-postcompile"),
)?;
let demo_dir_concat_path = demo_interestingness_test_dir.join("concat.h");
std::fs::copy(&concat_path, demo_dir_concat_path).unwrap();
run_demo_interestingness_test(&demo_interestingness_test_dir, &interestingness_test).unwrap();
// Now the main interestingness test
let interestingness_test = tmp_dir.path().join("test.sh");
create_interestingness_test(
gen_cmd,
&interestingness_test,
Some(matches.value_of("problem").unwrap()),
&rs_path,
&extra_clang_args,
!matches.is_present("no-precompile"),
!matches.is_present("no-postcompile"),
)?;
run_creduce(
matches.value_of("creduce").unwrap(),
&interestingness_test,
&concat_path,
matches.values_of("creduce-args").unwrap_or_default(),
);
announce_progress("creduce completed");
let output_path = matches.value_of("output");
match output_path {
None => print_minimized_case(&concat_path)?,
Some(output_path) => {
std::fs::copy(&concat_path, &PathBuf::from(output_path))?;
}
};
Ok(())
}
/// Try to detect whether the preprocessed source code already contains
/// a preprocessed version of cxx.h. This is hard because all the comments
/// and preprocessor symbols may have been removed, and in fact if we're
/// part way through reduction, parts of the code may have been removed too.
fn detect_cxx_h(concat_path: &Path) -> Result<bool, std::io::Error> {
let haystack = std::fs::read_to_string(concat_path)?;
Ok(["class Box", "class Vec", "class Slice"]
.iter()
.all(|needle| haystack.contains(needle)))
}
fn announce_progress(msg: &str) {
println!("=== {} ===", msg);
}
fn print_minimized_case(concat_path: &Path) -> Result<(), std::io::Error> {
announce_progress("Completed. Minimized test case:");
let contents = std::fs::read_to_string(concat_path)?;
println!("{}", contents);
Ok(())
}
/// Arguments we pass to creduce if supported. This pass always seems to cause a crash
/// as far as I can tell, so always exclude it. It may be environment-dependent,
/// of course, but as I'm the primary user of this tool I am ruthlessly removing it.
const REMOVE_PASS_LINE_MARKERS: &[&str] = &["--remove-pass", "pass_line_markers", "*"];
const SKIP_INITIAL_PASSES: &[&str] = &["--skip-initial-passes"];
fn creduce_supports_remove_pass(creduce_cmd: &str) -> bool {
let cmd = std::process::Command::new(creduce_cmd)
.arg("--help")
.output();
let msg = match cmd {
Err(error) => panic!("failed to run creduce. creduce_cmd = {}. hint: autocxx-reduce --creduce /path/to/creduce. error = {}", creduce_cmd, error),
Ok(result) => result.stdout
};
let msg = std::str::from_utf8(&msg).unwrap();
msg.contains("--remove-pass")
}
fn run_creduce<'a>(
creduce_cmd: &str,
interestingness_test: &'a Path,
concat_path: &'a Path,
creduce_args: impl Iterator<Item = &'a str>,
) {
announce_progress("creduce");
let args = std::iter::once(interestingness_test.to_str().unwrap())
.chain(std::iter::once(concat_path.to_str().unwrap()))
.chain(creduce_args)
.chain(
if creduce_supports_remove_pass(creduce_cmd) {
REMOVE_PASS_LINE_MARKERS
} else {
SKIP_INITIAL_PASSES
}
.iter()
.copied(),
)
.collect::<Vec<_>>();
println!("Command: {} {}", creduce_cmd, args.join(" "));
std::process::Command::new(creduce_cmd)
.args(args)
.status()
.expect("failed to creduce");
}
fn run_sample_gen_cmd(
gen_cmd: &str,
rs_file: &Path,
tmp_dir: &Path,
extra_clang_args: &[&str],
) -> Result<(), std::io::Error> {
let args = format_gen_cmd(rs_file, tmp_dir.to_str().unwrap(), extra_clang_args);
let args = args.collect::<Vec<_>>();
let args_str = args.join(" ");
announce_progress(&format!("Running sample gen cmd: {} {}", gen_cmd, args_str));
std::process::Command::new(gen_cmd).args(args).status()?;
Ok(())
}
fn run_demo_interestingness_test(demo_dir: &Path, test: &Path) -> Result<(), std::io::Error> {
announce_progress(&format!(
"Running demo interestingness test in {}",
demo_dir.to_string_lossy()
));
std::process::Command::new(test)
.current_dir(demo_dir)
.status()?;
Ok(())
}
fn format_gen_cmd<'a>(
rs_file: &Path,
dir: &str,
extra_clang_args: &'a [&str],
) -> impl Iterator<Item = String> + 'a {
let args = [
"-o".to_string(),
dir.to_string(),
"-I".to_string(),
dir.to_string(),
rs_file.to_str().unwrap().to_string(),
"--gen-rs-include".to_string(),
"--gen-cpp".to_string(),
"--suppress-system-headers".to_string(),
"--".to_string(),
]
.to_vec();
args.into_iter()
.chain(extra_clang_args.iter().map(|s| s.to_string()))
}
fn create_interestingness_test(
gen_cmd: &str,
test_path: &Path,
problem: Option<&str>,
rs_file: &Path,
extra_clang_args: &[&str],
precompile: bool,
postcompile: bool,
) -> Result<(), std::io::Error> {
announce_progress("Creating interestingness test");
// Ensure we refer to the input header by relative path
// because creduce will invoke us in some other directory with
// a copy thereof.
let mut args = format_gen_cmd(rs_file, "$(pwd)", extra_clang_args);
let args = args.join(" ");
let precompile_step = make_compile_step(precompile, "concat.h", extra_clang_args);
// For the compile afterwards, we have to avoid including any system headers.
// We rely on equivalent content being hermetically inside concat.h.
let postcompile_step = make_compile_step(postcompile, "gen0.cc", extra_clang_args);
let problem_grep = problem
.map(|problem| format!("| grep \"{}\" >/dev/null 2>&1", problem))
.unwrap_or_default();
let content = format!(
indoc! {"
#!/bin/sh
set -e
echo Precompile
{}
echo Move
mv concat.h concat-body.h
echo Codegen
(echo \"#ifndef __CONCAT_H__\"; echo \"#define __CONCAT_H__\"; echo '#include \"concat-body.h\"'; echo \"#endif\") > concat.h
({} {} 2>&1 && cat autocxx-ffi-default-gen.rs && cat autocxxgen*.h && {} 2>&1 ) {}
echo Remove
rm concat.h
echo Swap back
mv concat-body.h concat.h
echo Done
"},
precompile_step, gen_cmd, args, postcompile_step, problem_grep
);
println!("Interestingness test:\n{}", content);
{
let mut file = File::create(test_path)?;
file.write_all(content.as_bytes())?;
}
let mut perms = std::fs::metadata(&test_path)?.permissions();
perms.set_mode(0o700);
std::fs::set_permissions(&test_path, perms)?;
Ok(())
}
fn make_compile_step(enabled: bool, file: &str, extra_clang_args: &[&str]) -> String {
if enabled {
format!(
"{} {} -c {}",
get_clang_path(),
make_clang_args(&[PathBuf::from(".")], extra_clang_args).join(" "),
file,
)
} else {
"echo 'Skipping compilation'".into()
}
}
fn create_rs_file(rs_path: &Path, directives: &[String]) -> Result<(), std::io::Error> {
announce_progress("Creating Rust input file");
let mut file = File::create(rs_path)?;
file.write_all("use autocxx::include_cpp;\ninclude_cpp! (\n".as_bytes())?;
for directive in directives {
file.write_all(directive.as_bytes())?;
}
file.write_all(");\n".as_bytes())?;
Ok(())
}
fn create_concatenated_header(headers: &[&str], listing_path: &Path) -> Result<(), std::io::Error> {
announce_progress("Creating preprocessed header");
let mut file = File::create(listing_path)?;
for header in headers {
file.write_all(format!("#include \"{}\"\n", header).as_bytes())?;
}
Ok(())
}
fn create_file(path: &Path, content: &str) -> Result<(), std::io::Error> {
let mut file = File::create(path)?;
write!(file, "{}", content)?;
Ok(())
}
fn get_cxx_suppressions() -> Vec<String> {
let defines: IndexSet<_> = Regex::new(r"\bCXXBRIDGE1_\w+\b")
.unwrap()
.find_iter(cxx_gen::HEADER)
.map(|m| m.as_str())
.collect(); // for uniqueness
defines
.into_iter()
.map(|def| format!("-D{}", def))
.collect()
}
#[test]
fn test_get_cxx_suppressions() {
let defines = get_cxx_suppressions();
assert!(defines.contains(&"-DCXXBRIDGE1_RUST_BITCOPY_T".to_string()));
assert!(defines.contains(&"-DCXXBRIDGE1_RUST_STR".to_string()));
}