Cover more `main` functionality with native Rust tests.
PiperOrigin-RevId: 477448741
diff --git a/cc_bindings_from_rs/cc_bindings_from_rs.rs b/cc_bindings_from_rs/cc_bindings_from_rs.rs
index 24a9f58..9eb0afe 100644
--- a/cc_bindings_from_rs/cc_bindings_from_rs.rs
+++ b/cc_bindings_from_rs/cc_bindings_from_rs.rs
@@ -139,14 +139,17 @@
}
}
-// TODO(lukasza): Add end-to-end tests that verify that the exit code is
-// non-zero when:
-// * input contains syntax errors - test coverage for `RunCompiler::run`.
-// * mandatory parameters (e.g. `--h_out`) are missing - test coverage for how
-// `main` calls `Cmdline::new`.
-// * `--h_out` cannot be written to (in this case, the error message should
-// include the os-level error + Crubit-level error that includes the file
-// name) - test coverage for `write_file`.
+/// Main entrypoint that (unlike `main`) doesn't do any intitializations that
+/// should only happen once for the binary (e.g. it doesn't call
+/// `install_ice_hook`) and therefore can be used from the tests module below.
+fn run_with_cmdline_args(args: &[String]) -> anyhow::Result<()> {
+ let cmdline = Cmdline::new(args)?;
+ bindings_driver::RunCompiler::new(&cmdline).run()
+}
+
+// TODO(lukasza): Add end-to-end shell tests that invoke our executable
+// and verify 1) the happy path (zero exit code) and 2) any random
+// error path (non-zero exit code).
fn main() -> anyhow::Result<()> {
rustc_driver::init_env_logger("CRUBIT_LOG");
@@ -155,15 +158,182 @@
rustc_driver::install_ice_hook();
- // Parse Crubit's cmdline arguments.
- let cmdline = {
- // `std::env::args()` will panic if any of the cmdline arguments are not valid
- // Unicode. This seems okay.
- let args = std::env::args().collect_vec();
- Cmdline::new(&args)?
- };
+ // `std::env::args()` will panic if any of the cmdline arguments are not valid
+ // Unicode. This seems okay.
+ let args = std::env::args().collect_vec();
- // Invoke the Rust compiler and call `bindings_main::main` after parsing and
- // analysis are done.
- bindings_driver::RunCompiler::new(&cmdline).run()
+ run_with_cmdline_args(&args)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::run_with_cmdline_args;
+
+ use crate::lib::tests::get_sysroot_for_testing;
+ use itertools::Itertools;
+ use std::path::PathBuf;
+ use tempfile::{tempdir, TempDir};
+
+ /// Test data builder (see also
+ /// https://testing.googleblog.com/2018/02/testing-on-toilet-cleanly-create-test.html).
+ struct TestArgs {
+ h_path: Option<String>,
+ extra_crubit_args: Vec<String>,
+ extra_rustc_args: Vec<String>,
+ tempdir: TempDir,
+ }
+
+ impl TestArgs {
+ fn default_args() -> anyhow::Result<Self> {
+ Ok(Self {
+ h_path: None,
+ extra_crubit_args: vec![],
+ extra_rustc_args: vec![],
+ tempdir: tempdir()?,
+ })
+ }
+
+ /// Use the specified `h_path` rather than auto-generating one in
+ /// `self`-managed temporary directory.
+ fn with_h_path(mut self, h_path: &str) -> Self {
+ self.h_path = Some(h_path.to_string());
+ self
+ }
+
+ /// Appends `extra_rustc_args` at the end of the cmdline (i.e. as
+ /// additional rustc args, in addition to `--sysroot`,
+ /// `--crate-type=...`, etc.).
+ fn with_extra_rustc_args<T>(mut self, extra_rustc_args: T) -> Self
+ where
+ T: IntoIterator,
+ T::Item: Into<String>,
+ {
+ self.extra_rustc_args = extra_rustc_args.into_iter().map(|t| t.into()).collect_vec();
+ self
+ }
+
+ /// Appends `extra_crubit_args` before the first `--`.
+ fn with_extra_crubit_args<T>(mut self, extra_crubit_args: T) -> Self
+ where
+ T: IntoIterator,
+ T::Item: Into<String>,
+ {
+ self.extra_crubit_args = extra_crubit_args.into_iter().map(|t| t.into()).collect_vec();
+ self
+ }
+
+ /// Invokes `super::run_with_cmdline_args` with default `test_crate.rs`
+ /// input (and with other default args + args gathered by
+ /// `self`).
+ ///
+ /// Returns the path to the `h_out` file. The file's lifetime is the
+ /// same as `&self`.
+ fn run(&self) -> anyhow::Result<PathBuf> {
+ let h_path = match self.h_path.as_ref() {
+ None => self.tempdir.path().join("test_crate.rs"),
+ Some(s) => PathBuf::from(s),
+ };
+
+ let rs_input_path = self.tempdir.path().join("test_crate.rs");
+ std::fs::write(
+ &rs_input_path,
+ r#" pub fn public_function() {
+ private_function()
+ }
+
+ fn private_function() {}
+ "#,
+ )?;
+
+ let mut args = vec![
+ "cc_bindings_from_rs_unittest_executable".to_string(),
+ format!("--h_out={}", h_path.display()),
+ ];
+ args.extend(self.extra_crubit_args.iter().cloned());
+ args.extend([
+ "--".to_string(),
+ "--crate-type=lib".to_string(),
+ format!("--sysroot={}", get_sysroot_for_testing().display()),
+ rs_input_path.display().to_string(),
+ ]);
+ args.extend(self.extra_rustc_args.iter().cloned());
+
+ run_with_cmdline_args(&args)?;
+
+ Ok(h_path)
+ }
+ }
+
+ #[test]
+ fn test_happy_path() -> anyhow::Result<()> {
+ let test_args = TestArgs::default_args()?;
+ let h_path = test_args.run().expect("Default args should succeed");
+
+ assert!(h_path.exists());
+ let h_body = std::fs::read_to_string(&h_path)?;
+ assert_eq!(
+ h_body,
+ "// Automatically @generated C++ bindings for the following Rust crate:\n\
+ // test_crate\n\
+ \n\
+ // List of public functions:\n\
+ // public_function\n"
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn test_cmdline_error_propagation() -> anyhow::Result<()> {
+ // Tests that errors from `Cmdline::new` get propagated. Broader coverage of
+ // various error types can be found in tests in `cmdline.rs`.
+ let err = TestArgs::default_args()?
+ .with_extra_crubit_args(["--unrecognized-crubit-flag"])
+ .run()
+ .expect_err("--unrecognized_crubit_flag should trigger an error");
+
+ let msg = err.to_string();
+ assert_eq!("Unrecognized option: 'unrecognized-crubit-flag'", msg);
+ Ok(())
+ }
+
+ #[test]
+ fn test_rustc_error_propagation() -> anyhow::Result<()> {
+ // Tests that `rustc` errors are propagated.
+ let err = TestArgs::default_args()?
+ .with_extra_rustc_args(["--unrecognized-rustc-flag"])
+ .run()
+ .expect_err("--unrecognized-rustc-flag should trigger an error");
+
+ let msg = err.to_string();
+ assert_eq!("Errors reported by Rust compiler.", msg);
+ Ok(())
+ }
+
+ #[test]
+ fn test_invalid_h_out_path() -> anyhow::Result<()> {
+ // Tests not only the specific problem of an invalid `--h_out` argument, but
+ // also tests that errors from `bindings_main::main` are propagated.
+ let err = TestArgs::default_args()?
+ .with_h_path("../..")
+ .run()
+ .expect_err("Unwriteable --h_out should trigger an error");
+
+ let msg = err.to_string();
+ assert_eq!("Error when writing to ../..", msg);
+ Ok(())
+ }
+
+ #[test]
+ fn test_no_output_file() -> anyhow::Result<()> {
+ // Tests that we stop the compilation midway.
+ let tmpdir = tempdir()?;
+ let out_path = tmpdir.path().join("unexpected_output.o");
+ TestArgs::default_args()?
+ .with_extra_rustc_args(vec!["-o", &out_path.display().to_string()])
+ .run()
+ .expect("No rustc or Crubit errors are expected in this test");
+
+ assert!(!out_path.exists());
+ Ok(())
+ }
}
diff --git a/cc_bindings_from_rs/cmdline.rs b/cc_bindings_from_rs/cmdline.rs
index ba5e28b..8c81174 100644
--- a/cc_bindings_from_rs/cmdline.rs
+++ b/cc_bindings_from_rs/cmdline.rs
@@ -43,21 +43,26 @@
use itertools::Itertools;
use tempfile::tempdir;
- fn new_cmdline(args: &[&str]) -> Result<Cmdline, Fail> {
- let args = args.iter().map(|s| s.to_string()).collect_vec();
+ fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline, Fail> {
+ // When `Cmdline::new` is invoked from `main.rs`, it includes not only the
+ // "real" cmdline arguments, but also the name of the executable.
+ let args = std::iter::once("cc_bindings_from_rs_unittest_executable")
+ .chain(args)
+ .map(|s| s.to_string())
+ .collect_vec();
Cmdline::new(&args)
}
#[test]
fn test_h_out_happy_path() -> Result<(), Fail> {
- let cmdline = new_cmdline(&["--h_out=foo.h"])?;
+ let cmdline = new_cmdline(["--h_out=foo.h"])?;
assert_eq!(Path::new("foo.h"), cmdline.h_out());
Ok(())
}
#[test]
fn test_h_out_missing() {
- match new_cmdline(&[]) {
+ match new_cmdline([]) {
Err(Fail::OptionMissing(arg)) if arg == H_OUT => (),
other => panic!("Unexpected success or unrecognized error: {:?}", other),
}
@@ -65,7 +70,7 @@
#[test]
fn test_h_out_without_arg() {
- match new_cmdline(&["--h_out"]) {
+ match new_cmdline(["--h_out"]) {
Err(Fail::ArgumentMissing(arg)) if arg == H_OUT => (),
other => panic!("Unexpected success or unrecognized error: {:?}", other),
}
@@ -73,7 +78,7 @@
#[test]
fn test_h_out_duplicated() {
- match new_cmdline(&["--h_out=foo.h", "--h_out=bar.h"]) {
+ match new_cmdline(["--h_out=foo.h", "--h_out=bar.h"]) {
Err(Fail::OptionDuplicated(arg)) if arg == H_OUT => (),
other => panic!("Unexpected success or unrecognized error: {:?}", other),
}
@@ -82,10 +87,10 @@
#[test]
fn test_rustc_args_happy_path() -> Result<(), Fail> {
// Note that this test would fail without the `--` separator.
- let cmdline = new_cmdline(&["--h_out=foo.h", "--", "test.rs", "--crate-type=lib"])?;
+ let cmdline = new_cmdline(["--h_out=foo.h", "--", "test.rs", "--crate-type=lib"])?;
let rustc_args = cmdline.rustc_args();
assert!(
- itertools::equal(&["test.rs", "--crate-type=lib"], rustc_args),
+ itertools::equal(&["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"], rustc_args),
"rustc_args = {:?}",
rustc_args
);
diff --git a/cc_bindings_from_rs/lib.rs b/cc_bindings_from_rs/lib.rs
index 9d3cb2e..482c60f 100644
--- a/cc_bindings_from_rs/lib.rs
+++ b/cc_bindings_from_rs/lib.rs
@@ -74,14 +74,24 @@
}
#[cfg(test)]
-mod tests {
+pub mod tests {
use super::GeneratedBindings;
use anyhow::Result;
use itertools::Itertools;
+ use std::path::PathBuf;
use token_stream_printer::tokens_to_string;
+ pub fn get_sysroot_for_testing() -> PathBuf {
+ let runfiles = runfiles::Runfiles::create().unwrap();
+ runfiles.rlocation(if std::env::var("LEGACY_TOOLCHAIN_RUST_TEST").is_ok() {
+ "google3/third_party/unsupported_toolchains/rust/toolchains/nightly"
+ } else {
+ "google3/nowhere/llvm/rust"
+ })
+ }
+
#[test]
fn test_get_names_of_exported_fns_public_vs_private() {
let test_src = r#"
@@ -156,16 +166,9 @@
// `Mir`, etc. would also trigger code gen).
let output_types = OutputTypes::new(&[(OutputType::Bitcode, None /* PathBuf */)]);
- let runfiles = runfiles::Runfiles::create().unwrap();
- let sysroot_path =
- runfiles.rlocation(if std::env::var("LEGACY_TOOLCHAIN_RUST_TEST").is_ok() {
- "google3/third_party/unsupported_toolchains/rust/toolchains/nightly"
- } else {
- "google3/nowhere/llvm/rust"
- });
let opts = Options {
crate_types: vec![CrateType::Rlib], // Test inputs simulate library crates.
- maybe_sysroot: Some(sysroot_path),
+ maybe_sysroot: Some(get_sysroot_for_testing()),
output_types,
..Default::default()
};