//! System tests for compiling Rust code with cargo. //! //! Any copyright is dedicated to the Public Domain. //! http://creativecommons.org/publicdomain/zero/1.0/ pub mod helpers; use anyhow::{Context, Result}; use helpers::{cargo_clean, stop_sccache, CARGO, CRATE_DIR}; use assert_cmd::prelude::*; use fs_err as fs; use helpers::{SccacheTest, SCCACHE_BIN}; use predicates::prelude::*; use serial_test::serial; use std::path::Path; use std::process::Command; #[macro_use] extern crate log; #[test] #[serial] fn test_rust_cargo_check() -> Result<()> { test_rust_cargo_cmd("check", SccacheTest::new(None)?) } #[test] #[serial] fn test_rust_cargo_check_readonly() -> Result<()> { test_rust_cargo_cmd_readonly("check", SccacheTest::new(None)?) } #[test] #[serial] fn test_rust_cargo_build() -> Result<()> { test_rust_cargo_cmd("build", SccacheTest::new(None)?) } #[test] #[serial] fn test_rust_cargo_build_readonly() -> Result<()> { test_rust_cargo_cmd_readonly("build", SccacheTest::new(None)?) } #[test] #[serial] #[cfg(unix)] fn test_run_log_no_perm() -> Result<()> { trace!("sccache with log"); stop_sccache()?; let mut cmd = Command::new(SCCACHE_BIN.as_os_str()); cmd.arg("gcc") .env("SCCACHE_ERROR_LOG", "/no-perm.log") // Should not work .env("SCCACHE_LOG", "debug"); cmd.assert().failure().stderr(predicate::str::contains( "Cannot open/write log file '/no-perm.log'", )); Ok(()) } #[test] #[serial] fn test_run_log() -> Result<()> { trace!("sccache with log"); stop_sccache()?; let tempdir = tempfile::Builder::new() .prefix("sccache_test_rust_cargo") .tempdir() .context("Failed to create tempdir")?; let tmppath = tempdir.path().join("perm.log"); let mut cmd = Command::new(SCCACHE_BIN.as_os_str()); cmd.arg("--start-server") .env("SCCACHE_ERROR_LOG", &tmppath) // Should not work .env("SCCACHE_LOG", "debug"); cmd.assert().success(); stop_sccache()?; assert!(Path::new(&tmppath).is_file()); Ok(()) } /// This test checks that changing an environment variable reference by env! is detected by /// sccache, causes a rebuild and is correctly printed to stdout. #[test] #[serial] fn test_rust_cargo_run_with_env_dep_parsing() -> Result<()> { test_rust_cargo_env_dep(SccacheTest::new(None)?) } #[cfg(feature = "unstable")] #[test] #[serial] fn test_rust_cargo_check_nightly() -> Result<()> { use std::ffi::OsString; test_rust_cargo_cmd( "check", SccacheTest::new(Some(&[("RUSTFLAGS", OsString::from("-Zprofile"))]))?, ) } #[cfg(feature = "unstable")] #[test] #[serial] fn test_rust_cargo_check_nightly_readonly() -> Result<()> { use std::ffi::OsString; test_rust_cargo_cmd_readonly( "check", SccacheTest::new(Some(&[("RUSTFLAGS", OsString::from("-Zprofile"))]))?, ) } #[cfg(feature = "unstable")] #[test] #[serial] fn test_rust_cargo_build_nightly() -> Result<()> { use std::ffi::OsString; test_rust_cargo_cmd( "build", SccacheTest::new(Some(&[("RUSTFLAGS", OsString::from("-Zprofile"))]))?, ) } #[cfg(feature = "unstable")] #[test] #[serial] fn test_rust_cargo_build_nightly_readonly() -> Result<()> { use std::ffi::OsString; test_rust_cargo_cmd_readonly( "build", SccacheTest::new(Some(&[("RUSTFLAGS", OsString::from("-Zprofile"))]))?, ) } /// Test that building a simple Rust crate with cargo using sccache results in a cache hit /// when built a second time and a cache miss, when the environment variable referenced via /// env! is changed. fn test_rust_cargo_cmd(cmd: &str, test_info: SccacheTest) -> Result<()> { // `cargo clean` first, just to be sure there's no leftover build objects. cargo_clean(&test_info)?; // Now build the crate with cargo. Command::new(CARGO.as_os_str()) .args([cmd, "--color=never"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8().not())? .try_success()?; // Clean it so we can build it again. cargo_clean(&test_info)?; Command::new(CARGO.as_os_str()) .args([cmd, "--color=always"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8())? .try_success()?; test_info .show_stats()? .try_stdout( predicates::str::contains( r#""cache_hits":{"counts":{"Rust":2},"adv_counts":{"rust":2}}"#, ) .from_utf8(), )? .try_success()?; Ok(()) } fn restart_sccache( test_info: &SccacheTest, additional_envs: Option>, ) -> Result<()> { let cache_dir = test_info.tempdir.path().join("cache"); stop_sccache()?; trace!("sccache --start-server"); let mut cmd = Command::new(SCCACHE_BIN.as_os_str()); cmd.arg("--start-server"); cmd.env("SCCACHE_DIR", &cache_dir); if let Some(additional_envs) = additional_envs { cmd.envs(additional_envs); } cmd.assert() .try_success() .context("Failed to start sccache server")?; Ok(()) } /// Test that building a simple Rust crate with cargo using sccache results in the following behaviors (for three different runs): /// - In read-only mode, a cache miss. /// - In read-write mode, a cache miss. /// - In read-only mode, a cache hit. /// /// The environment variable for read/write mode is added by this function. fn test_rust_cargo_cmd_readonly(cmd: &str, test_info: SccacheTest) -> Result<()> { // `cargo clean` first, just to be sure there's no leftover build objects. cargo_clean(&test_info)?; // The cache must be put into read-only mode, and that can only be configured // when the server starts up, so we need to restart it. restart_sccache( &test_info, Some(vec![("SCCACHE_LOCAL_RW_MODE".into(), "READ_ONLY".into())]), )?; // Now build the crate with cargo. Command::new(CARGO.as_os_str()) .args([cmd, "--color=never"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8().not())? .try_success()?; // Stats reset on server restart, so this needs to be run for each build. test_info .show_stats()? .try_stdout( predicates::str::contains(r#""cache_hits":{"counts":{},"adv_counts":{}}"#).from_utf8(), )? .try_stdout( predicates::str::contains( r#""cache_misses":{"counts":{"Rust":2},"adv_counts":{"rust":2}}"#, ) .from_utf8(), )? .try_success()?; cargo_clean(&test_info)?; restart_sccache( &test_info, Some(vec![("SCCACHE_LOCAL_RW_MODE".into(), "READ_WRITE".into())]), )?; Command::new(CARGO.as_os_str()) .args([cmd, "--color=always"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8())? .try_success()?; test_info .show_stats()? .try_stdout( predicates::str::contains(r#""cache_hits":{"counts":{},"adv_counts":{}}"#).from_utf8(), )? .try_stdout( predicates::str::contains( r#""cache_misses":{"counts":{"Rust":2},"adv_counts":{"rust":2}}"#, ) .from_utf8(), )? .try_success()?; cargo_clean(&test_info)?; restart_sccache( &test_info, Some(vec![("SCCACHE_LOCAL_RW_MODE".into(), "READ_ONLY".into())]), )?; Command::new(CARGO.as_os_str()) .args([cmd, "--color=always"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8())? .try_success()?; test_info .show_stats()? .try_stdout( predicates::str::contains( r#""cache_hits":{"counts":{"Rust":2},"adv_counts":{"rust":2}}"#, ) .from_utf8(), )? .try_stdout( predicates::str::contains(r#""cache_misses":{"counts":{},"adv_counts":{}}"#) .from_utf8(), )? .try_success()?; Ok(()) } fn test_rust_cargo_env_dep(test_info: SccacheTest) -> Result<()> { cargo_clean(&test_info)?; // Now build the crate with cargo. Command::new(CARGO.as_os_str()) .args(["run", "--color=never"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8().not())? .try_stdout(predicates::str::contains("Env var: 1"))? .try_success()?; // Clean it so we can build it again. cargo_clean(&test_info)?; Command::new(CARGO.as_os_str()) .args(["run", "--color=always"]) .envs(test_info.env.iter().cloned()) .env("TEST_ENV_VAR", "OTHER_VALUE") .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8())? .try_stdout(predicates::str::contains("Env var: OTHER_VALUE"))? .try_success()?; // Now get the stats and ensure that we had one cache hit for the second build. // The test crate has one dependency (itoa) so there are two separate compilations, but only // itoa should be cached (due to the changed environment variable). test_info .show_stats()? .try_stdout(predicates::str::contains(r#""cache_hits":{"counts":{"Rust":1}"#).from_utf8())? .try_success()?; drop(test_info); Ok(()) } /// Test that building a simple Rust crate with cargo using sccache in read-only mode with an empty cache results in /// a cache miss that is produced by the readonly storage wrapper (and does not attempt to write to the underlying cache). #[test] #[serial] fn test_rust_cargo_cmd_readonly_preemtive_block() -> Result<()> { let test_info = SccacheTest::new(None)?; // `cargo clean` first, just to be sure there's no leftover build objects. cargo_clean(&test_info)?; let sccache_log = test_info.tempdir.path().join("sccache.log"); stop_sccache()?; restart_sccache( &test_info, Some(vec![ ("SCCACHE_LOCAL_RW_MODE".into(), "READ_ONLY".into()), ("SCCACHE_LOG".into(), "trace".into()), ( "SCCACHE_ERROR_LOG".into(), sccache_log.to_str().unwrap().into(), ), ]), )?; // Now build the crate with cargo. // Assert that our cache miss is due to the readonly storage wrapper, not due to the underlying disk cache. Command::new(CARGO.as_os_str()) .args(["build", "--color=never"]) .envs(test_info.env.iter().cloned()) .current_dir(CRATE_DIR.as_os_str()) .assert() .try_stderr(predicates::str::contains("\x1b[").from_utf8().not())? .try_success()?; let log_contents = fs::read_to_string(sccache_log)?; assert!(predicates::str::contains("server has setup with ReadOnly").eval(log_contents.as_str())); assert!(predicates::str::contains( "Error executing cache write: Cannot write to read-only storage" ) .eval(log_contents.as_str())); assert!(predicates::str::contains("DiskCache::finish_put") .not() .eval(log_contents.as_str())); // Stats reset on server restart, so this needs to be run for each build. test_info .show_stats()? .try_stdout( predicates::str::contains(r#""cache_hits":{"counts":{},"adv_counts":{}}"#).from_utf8(), )? .try_stdout( predicates::str::contains( r#""cache_misses":{"counts":{"Rust":2},"adv_counts":{"rust":2}}"#, ) .from_utf8(), )? .try_success()?; Ok(()) }