blob: 17c5bf055783a90cb9f35eb9ca2d8e05f0beeede [file] [log] [blame]
Brian Silverman4e662aa2022-05-11 23:10:19 -07001// Copyright 2020 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9#![forbid(unsafe_code)]
10
11use std::{
12 borrow::Cow,
13 fs::File,
14 io::Write,
15 os::unix::prelude::PermissionsExt,
16 path::{Path, PathBuf},
17};
18
19use autocxx_engine::{get_clang_path, make_clang_args, preprocess};
20use autocxx_parser::IncludeCppConfig;
21use clap::{crate_authors, crate_version, Arg, ArgMatches, Command};
22use indexmap::IndexSet;
23use indoc::indoc;
24use itertools::Itertools;
25use quote::ToTokens;
26use regex::Regex;
27use tempfile::TempDir;
28
29static LONG_HELP: &str = indoc! {"
30Command line utility to minimize autocxx bug cases.
31
32This is a wrapper for creduce.
33
34Example command-line:
35autocxx-reduce file -I my-inc-dir -h my-header -d 'generate!(\"MyClass\")' -k -- --n 64
36"};
37
38fn main() {
39 let matches = Command::new("autocxx-reduce")
40 .version(crate_version!())
41 .author(crate_authors!())
42 .about("Reduce a C++ test case")
43 .long_about(LONG_HELP)
44 .subcommand(Command::new("file")
45 .about("reduce a header file")
46
47 .arg(
48 Arg::new("inc")
49 .short('I')
50 .long("inc")
51 .multiple_occurrences(true)
52 .number_of_values(1)
53 .value_name("INCLUDE DIRS")
54 .help("include path")
55 .takes_value(true),
56 )
57 .arg(
58 Arg::new("define")
59 .short('D')
60 .long("define")
61 .multiple_occurrences(true)
62 .number_of_values(1)
63 .value_name("DEFINE")
64 .help("macro definition")
65 .takes_value(true),
66 )
67 .arg(
68 Arg::new("header")
69 .long("header")
70 .multiple_occurrences(true)
71 .number_of_values(1)
72 .required(true)
73 .value_name("HEADER")
74 .help("header file name")
75 .takes_value(true),
76 )
77
78 .arg(
79 Arg::new("directive")
80 .short('d')
81 .long("directive")
82 .multiple_occurrences(true)
83 .number_of_values(1)
84 .value_name("DIRECTIVE")
85 .help("directives to put within include_cpp!")
86 .takes_value(true),
87 )
88 )
89 .subcommand(Command::new("repro")
90 .about("reduce a repro case JSON file")
91 .arg(
92 Arg::new("repro")
93 .short('r')
94 .long("repro")
95 .required(true)
96 .value_name("REPRODUCTION CASE JSON")
97 .help("reproduction case JSON file name")
98 .takes_value(true),
99 )
100 .arg(
101 Arg::new("header")
102 .long("header")
103 .multiple_occurrences(true)
104 .number_of_values(1)
105 .value_name("HEADER")
106 .help("header file name; specify to resume a part-completed run")
107 .takes_value(true),
108 )
109 )
110 .arg(
111 Arg::new("problem")
112 .short('p')
113 .long("problem")
114 .required(true)
115 .value_name("PROBLEM")
116 .help("problem string we're looking for... may be in logs, or in generated C++, or generated .rs")
117 .takes_value(true),
118 )
119 .arg(
120 Arg::new("creduce")
121 .long("creduce")
122 .value_name("PATH")
123 .help("creduce binary location")
124 .default_value("creduce")
125 .takes_value(true),
126 )
127 .arg(
128 Arg::new("output")
129 .short('o')
130 .long("output")
131 .value_name("OUTPUT")
132 .help("where to write minimized output")
133 .takes_value(true),
134 )
135 .arg(
136 Arg::new("gen-cmd")
137 .short('g')
138 .long("gen-cmd")
139 .value_name("GEN-CMD")
140 .help("where to find autocxx-gen")
141 .takes_value(true),
142 )
143 .arg(
144 Arg::new("keep")
145 .short('k')
146 .long("keep-dir")
147 .help("keep the temporary directory for debugging purposes"),
148 )
149 .arg(
150 Arg::new("clang-args")
151 .short('c')
152 .long("clang-arg")
153 .multiple_occurrences(true)
154 .value_name("CLANG_ARG")
155 .help("Extra arguments to pass to Clang"),
156 )
157 .arg(
158 Arg::new("creduce-args")
159 .long("creduce-arg")
160 .multiple_occurrences(true)
161 .value_name("CREDUCE_ARG")
162 .help("Extra arguments to pass to Clang"),
163 )
164 .arg(
165 Arg::new("no-precompile")
166 .long("no-precompile")
167 .help("Do not precompile the C++ header before passing to autocxxgen"),
168 )
169 .arg(
170 Arg::new("no-postcompile")
171 .long("no-postcompile")
172 .help("Do not post-compile the C++ generated by autocxxgen"),
173 )
174 .arg(
175 Arg::new("suppress-cxx-inclusions")
176 .long("suppress-cxx-inclusions")
177 .takes_value(true)
178 .possible_value("yes")
179 .possible_value("no")
180 .possible_value("auto")
181 .default_value("auto")
182 .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.")
183 )
184 .arg_required_else_help(true)
185 .get_matches();
186 run(matches).unwrap();
187}
188
189fn run(matches: ArgMatches) -> Result<(), std::io::Error> {
190 let keep_tmp = matches.is_present("keep");
191 let tmp_dir = TempDir::new()?;
192 let r = do_run(matches, &tmp_dir);
193 if keep_tmp {
194 println!(
195 "Keeping temp dir created at: {}",
196 tmp_dir.into_path().to_str().unwrap()
197 );
198 }
199 r
200}
201
202#[derive(serde_derive::Deserialize)]
203struct ReproCase {
204 config: String,
205 header: String,
206}
207
208fn do_run(matches: ArgMatches, tmp_dir: &TempDir) -> Result<(), std::io::Error> {
209 let rs_path = tmp_dir.path().join("input.rs");
210 let concat_path = tmp_dir.path().join("concat.h");
211 match matches.subcommand_matches("repro") {
212 None => {
213 let submatches = matches.subcommand_matches("file").unwrap();
214 let incs: Vec<_> = submatches
215 .values_of("inc")
216 .unwrap_or_default()
217 .map(PathBuf::from)
218 .collect();
219 let defs: Vec<_> = submatches.values_of("define").unwrap_or_default().collect();
220 let headers: Vec<_> = submatches.values_of("header").unwrap_or_default().collect();
221 assert!(!headers.is_empty());
222 let listing_path = tmp_dir.path().join("listing.h");
223 create_concatenated_header(&headers, &listing_path)?;
224 announce_progress(&format!(
225 "Preprocessing {:?} to {:?}",
226 listing_path, concat_path
227 ));
228 preprocess(&listing_path, &concat_path, &incs, &defs)?;
229 let directives: Vec<_> = std::iter::once("#include \"concat.h\"\n".to_string())
230 .chain(
231 submatches
232 .values_of("directive")
233 .unwrap_or_default()
234 .map(|s| format!("{}\n", s)),
235 )
236 .collect();
237 create_rs_file(&rs_path, &directives)?;
238 }
239 Some(submatches) => {
240 let case: ReproCase = serde_json::from_reader(File::open(PathBuf::from(
241 submatches.value_of("repro").unwrap(),
242 ))?)
243 .unwrap();
244 // Replace the headers in the config
245 let mut config: IncludeCppConfig = syn::parse_str(&case.config).unwrap();
246 config.replace_included_headers("concat.h");
247 create_file(
248 &rs_path,
249 &format!("autocxx::include_cpp!({});", config.to_token_stream()),
250 )?;
251 if let Some(header) = submatches.value_of("header") {
252 std::fs::copy(PathBuf::from(header), &concat_path)?;
253 } else {
254 create_file(&concat_path, &case.header)?
255 }
256 }
257 }
258
259 let suppress_cxx_classes = match matches.value_of("suppress-cxx-inclusions").unwrap() {
260 "yes" => true,
261 "no" => false,
262 "auto" => detect_cxx_h(&concat_path)?,
263 _ => panic!("unexpected value"),
264 };
265
266 let cxx_suppressions = if suppress_cxx_classes {
267 get_cxx_suppressions()
268 } else {
269 Vec::new()
270 };
271
272 let extra_clang_args: Vec<_> = matches
273 .values_of("clang-args")
274 .unwrap_or_default()
275 .map(Cow::Borrowed)
276 .chain(cxx_suppressions.into_iter().map(Cow::Owned))
277 .collect();
278 let extra_clang_args: Vec<&str> = extra_clang_args.iter().map(|s| s.as_ref()).collect_vec();
279
280 let default_gen_cmd = std::env::current_exe()?
281 .parent()
282 .unwrap()
283 .join("autocxx-gen")
284 .to_str()
285 .unwrap()
286 .to_string();
287 let gen_cmd = matches.value_of("gen-cmd").unwrap_or(&default_gen_cmd);
288 if !Path::new(gen_cmd).exists() {
289 panic!(
290 "autocxx-gen not found in {}. hint: autocxx-reduce --gen-cmd /path/to/autocxx-gen",
291 gen_cmd
292 );
293 }
294 run_sample_gen_cmd(gen_cmd, &rs_path, tmp_dir.path(), &extra_clang_args)?;
295 // Create and run an interestingness test which does not filter its output through grep.
296 let demo_interestingness_test_dir = tmp_dir.path().join("demo-interestingness-test");
297 std::fs::create_dir(&demo_interestingness_test_dir).unwrap();
298 let interestingness_test = demo_interestingness_test_dir.join("test-demo.sh");
299 create_interestingness_test(
300 gen_cmd,
301 &interestingness_test,
302 None,
303 &rs_path,
304 &extra_clang_args,
305 !matches.is_present("no-precompile"),
306 !matches.is_present("no-postcompile"),
307 )?;
308 let demo_dir_concat_path = demo_interestingness_test_dir.join("concat.h");
309 std::fs::copy(&concat_path, demo_dir_concat_path).unwrap();
310 run_demo_interestingness_test(&demo_interestingness_test_dir, &interestingness_test).unwrap();
311
312 // Now the main interestingness test
313 let interestingness_test = tmp_dir.path().join("test.sh");
314 create_interestingness_test(
315 gen_cmd,
316 &interestingness_test,
317 Some(matches.value_of("problem").unwrap()),
318 &rs_path,
319 &extra_clang_args,
320 !matches.is_present("no-precompile"),
321 !matches.is_present("no-postcompile"),
322 )?;
323 run_creduce(
324 matches.value_of("creduce").unwrap(),
325 &interestingness_test,
326 &concat_path,
327 matches.values_of("creduce-args").unwrap_or_default(),
328 );
329 announce_progress("creduce completed");
330 let output_path = matches.value_of("output");
331 match output_path {
332 None => print_minimized_case(&concat_path)?,
333 Some(output_path) => {
334 std::fs::copy(&concat_path, &PathBuf::from(output_path))?;
335 }
336 };
337 Ok(())
338}
339
340/// Try to detect whether the preprocessed source code already contains
341/// a preprocessed version of cxx.h. This is hard because all the comments
342/// and preprocessor symbols may have been removed, and in fact if we're
343/// part way through reduction, parts of the code may have been removed too.
344fn detect_cxx_h(concat_path: &Path) -> Result<bool, std::io::Error> {
345 let haystack = std::fs::read_to_string(concat_path)?;
346 Ok(["class Box", "class Vec", "class Slice"]
347 .iter()
348 .all(|needle| haystack.contains(needle)))
349}
350
351fn announce_progress(msg: &str) {
352 println!("=== {} ===", msg);
353}
354
355fn print_minimized_case(concat_path: &Path) -> Result<(), std::io::Error> {
356 announce_progress("Completed. Minimized test case:");
357 let contents = std::fs::read_to_string(concat_path)?;
358 println!("{}", contents);
359 Ok(())
360}
361
362/// Arguments we pass to creduce if supported. This pass always seems to cause a crash
363/// as far as I can tell, so always exclude it. It may be environment-dependent,
364/// of course, but as I'm the primary user of this tool I am ruthlessly removing it.
365const REMOVE_PASS_LINE_MARKERS: &[&str] = &["--remove-pass", "pass_line_markers", "*"];
366const SKIP_INITIAL_PASSES: &[&str] = &["--skip-initial-passes"];
367
368fn creduce_supports_remove_pass(creduce_cmd: &str) -> bool {
369 let cmd = std::process::Command::new(creduce_cmd)
370 .arg("--help")
371 .output();
372 let msg = match cmd {
373 Err(error) => panic!("failed to run creduce. creduce_cmd = {}. hint: autocxx-reduce --creduce /path/to/creduce. error = {}", creduce_cmd, error),
374 Ok(result) => result.stdout
375 };
376 let msg = std::str::from_utf8(&msg).unwrap();
377 msg.contains("--remove-pass")
378}
379
380fn run_creduce<'a>(
381 creduce_cmd: &str,
382 interestingness_test: &'a Path,
383 concat_path: &'a Path,
384 creduce_args: impl Iterator<Item = &'a str>,
385) {
386 announce_progress("creduce");
387 let args = std::iter::once(interestingness_test.to_str().unwrap())
388 .chain(std::iter::once(concat_path.to_str().unwrap()))
389 .chain(creduce_args)
390 .chain(
391 if creduce_supports_remove_pass(creduce_cmd) {
392 REMOVE_PASS_LINE_MARKERS
393 } else {
394 SKIP_INITIAL_PASSES
395 }
396 .iter()
397 .copied(),
398 )
399 .collect::<Vec<_>>();
400 println!("Command: {} {}", creduce_cmd, args.join(" "));
401 std::process::Command::new(creduce_cmd)
402 .args(args)
403 .status()
404 .expect("failed to creduce");
405}
406
407fn run_sample_gen_cmd(
408 gen_cmd: &str,
409 rs_file: &Path,
410 tmp_dir: &Path,
411 extra_clang_args: &[&str],
412) -> Result<(), std::io::Error> {
413 let args = format_gen_cmd(rs_file, tmp_dir.to_str().unwrap(), extra_clang_args);
414 let args = args.collect::<Vec<_>>();
415 let args_str = args.join(" ");
416 announce_progress(&format!("Running sample gen cmd: {} {}", gen_cmd, args_str));
417 std::process::Command::new(gen_cmd).args(args).status()?;
418 Ok(())
419}
420
421fn run_demo_interestingness_test(demo_dir: &Path, test: &Path) -> Result<(), std::io::Error> {
422 announce_progress(&format!(
423 "Running demo interestingness test in {}",
424 demo_dir.to_string_lossy()
425 ));
426 std::process::Command::new(test)
427 .current_dir(demo_dir)
428 .status()?;
429 Ok(())
430}
431
432fn format_gen_cmd<'a>(
433 rs_file: &Path,
434 dir: &str,
435 extra_clang_args: &'a [&str],
436) -> impl Iterator<Item = String> + 'a {
437 let args = [
438 "-o".to_string(),
439 dir.to_string(),
440 "-I".to_string(),
441 dir.to_string(),
442 rs_file.to_str().unwrap().to_string(),
443 "--gen-rs-include".to_string(),
444 "--gen-cpp".to_string(),
445 "--suppress-system-headers".to_string(),
446 "--".to_string(),
447 ]
448 .to_vec();
449 args.into_iter()
450 .chain(extra_clang_args.iter().map(|s| s.to_string()))
451}
452
453fn create_interestingness_test(
454 gen_cmd: &str,
455 test_path: &Path,
456 problem: Option<&str>,
457 rs_file: &Path,
458 extra_clang_args: &[&str],
459 precompile: bool,
460 postcompile: bool,
461) -> Result<(), std::io::Error> {
462 announce_progress("Creating interestingness test");
463 // Ensure we refer to the input header by relative path
464 // because creduce will invoke us in some other directory with
465 // a copy thereof.
466 let mut args = format_gen_cmd(rs_file, "$(pwd)", extra_clang_args);
467 let args = args.join(" ");
468 let precompile_step = make_compile_step(precompile, "concat.h", extra_clang_args);
469 // For the compile afterwards, we have to avoid including any system headers.
470 // We rely on equivalent content being hermetically inside concat.h.
471 let postcompile_step = make_compile_step(postcompile, "gen0.cc", extra_clang_args);
472 let problem_grep = problem
473 .map(|problem| format!("| grep \"{}\" >/dev/null 2>&1", problem))
474 .unwrap_or_default();
475 let content = format!(
476 indoc! {"
477 #!/bin/sh
478 set -e
479 echo Precompile
480 {}
481 echo Move
482 mv concat.h concat-body.h
483 echo Codegen
484 (echo \"#ifndef __CONCAT_H__\"; echo \"#define __CONCAT_H__\"; echo '#include \"concat-body.h\"'; echo \"#endif\") > concat.h
485 ({} {} 2>&1 && cat autocxx-ffi-default-gen.rs && cat autocxxgen*.h && {} 2>&1 ) {}
486 echo Remove
487 rm concat.h
488 echo Swap back
489 mv concat-body.h concat.h
490 echo Done
491 "},
492 precompile_step, gen_cmd, args, postcompile_step, problem_grep
493 );
494 println!("Interestingness test:\n{}", content);
495 {
496 let mut file = File::create(test_path)?;
497 file.write_all(content.as_bytes())?;
498 }
499
500 let mut perms = std::fs::metadata(&test_path)?.permissions();
501 perms.set_mode(0o700);
502 std::fs::set_permissions(&test_path, perms)?;
503 Ok(())
504}
505
506fn make_compile_step(enabled: bool, file: &str, extra_clang_args: &[&str]) -> String {
507 if enabled {
508 format!(
509 "{} {} -c {}",
510 get_clang_path(),
511 make_clang_args(&[PathBuf::from(".")], extra_clang_args).join(" "),
512 file,
513 )
514 } else {
515 "echo 'Skipping compilation'".into()
516 }
517}
518
519fn create_rs_file(rs_path: &Path, directives: &[String]) -> Result<(), std::io::Error> {
520 announce_progress("Creating Rust input file");
521 let mut file = File::create(rs_path)?;
522 file.write_all("use autocxx::include_cpp;\ninclude_cpp! (\n".as_bytes())?;
523 for directive in directives {
524 file.write_all(directive.as_bytes())?;
525 }
526 file.write_all(");\n".as_bytes())?;
527 Ok(())
528}
529
530fn create_concatenated_header(headers: &[&str], listing_path: &Path) -> Result<(), std::io::Error> {
531 announce_progress("Creating preprocessed header");
532 let mut file = File::create(listing_path)?;
533 for header in headers {
534 file.write_all(format!("#include \"{}\"\n", header).as_bytes())?;
535 }
536 Ok(())
537}
538
539fn create_file(path: &Path, content: &str) -> Result<(), std::io::Error> {
540 let mut file = File::create(path)?;
541 write!(file, "{}", content)?;
542 Ok(())
543}
544
545fn get_cxx_suppressions() -> Vec<String> {
546 let defines: IndexSet<_> = Regex::new(r"\bCXXBRIDGE1_\w+\b")
547 .unwrap()
548 .find_iter(cxx_gen::HEADER)
549 .map(|m| m.as_str())
550 .collect(); // for uniqueness
551 defines
552 .into_iter()
553 .map(|def| format!("-D{}", def))
554 .collect()
555}
556
557#[test]
558fn test_get_cxx_suppressions() {
559 let defines = get_cxx_suppressions();
560 assert!(defines.contains(&"-DCXXBRIDGE1_RUST_BITCOPY_T".to_string()));
561 assert!(defines.contains(&"-DCXXBRIDGE1_RUST_STR".to_string()));
562}