// System tests for compiling C code. // // Copyright 2016 Mozilla Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #![allow(dead_code, unused_imports)] extern crate assert_cmd; extern crate cc; extern crate env_logger; extern crate escargot; #[macro_use] extern crate log; extern crate predicates; extern crate sccache; extern crate serde_json; extern crate tempdir; extern crate which; use assert_cmd::prelude::*; use escargot::CargoBuild; use harness::{ sccache_command, sccache_client_cfg, start_local_daemon, stop_local_daemon, write_json_cfg, write_source, get_stats, zero_stats, }; use log::Level::Trace; use predicates::prelude::*; use std::collections::HashMap; use std::env; use std::ffi::{OsStr,OsString}; use std::fmt; use std::fs::{self, File}; use std::io::{self, Read, Write}; use std::path::{Path,PathBuf}; use std::process::{ Command, Output, Stdio, }; use std::str; use tempdir::TempDir; use which::which_in; mod harness; #[derive(Clone)] struct Compiler { pub name: &'static str, pub exe: OsString, pub env_vars: Vec<(OsString, OsString)>, } // Test GCC + clang on non-OS X platforms. #[cfg(all(unix, not(target_os="macos")))] const COMPILERS: &'static [&'static str] = &["gcc", "clang"]; // OS X ships a `gcc` that's just a clang wrapper, so only test clang there. #[cfg(target_os="macos")] const COMPILERS: &'static [&'static str] = &["clang"]; //TODO: could test gcc when targeting mingw. macro_rules! vec_from { ( $t:ty, $( $x:expr ),* ) => { vec!($( Into::<$t>::into(&$x), )*) }; } fn compile_cmdline>(compiler: &str, exe: T, input: &str, output: &str) -> Vec { match compiler { "gcc" | "clang" => vec_from!(OsString, exe.as_ref(), "-c", input, "-o", output), "cl.exe" => vec_from!(OsString, exe, "-c", input, format!("-Fo{}", output)), _ => panic!("Unsupported compiler: {}", compiler), } } const INPUT: &'static str = "test.c"; const INPUT_ERR: &'static str = "test_err.c"; const OUTPUT: &'static str = "test.o"; fn test_basic_compile(compiler: Compiler, tempdir: &Path) { let Compiler { name, exe, env_vars } = compiler; trace!("run_sccache_command_test: {}", name); // Compile a source file. // Copy the source files into the tempdir so we can compile with relative paths, since the commandline winds up in the hash key. for f in &[INPUT, INPUT_ERR] { let original_source_file = Path::new(file!()).parent().unwrap().join(f); let source_file = tempdir.join(f); trace!("fs::copy({:?}, {:?})", original_source_file, source_file); fs::copy(&original_source_file, &source_file).unwrap(); } let out_file = tempdir.join("test.o"); trace!("compile"); sccache_command() .args(&compile_cmdline(name, &exe, INPUT, OUTPUT)) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); assert_eq!(true, fs::metadata(&out_file).and_then(|m| Ok(m.len() > 0)).unwrap()); trace!("request stats"); get_stats(|info| { assert_eq!(1, info.stats.compile_requests); assert_eq!(1, info.stats.requests_executed); assert_eq!(0, info.stats.cache_hits.all()); assert_eq!(1, info.stats.cache_misses.all()); assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap()); }); trace!("compile"); fs::remove_file(&out_file).unwrap(); sccache_command() .args(&compile_cmdline(name, &exe, INPUT, OUTPUT)) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); assert_eq!(true, fs::metadata(&out_file).and_then(|m| Ok(m.len() > 0)).unwrap()); trace!("request stats"); get_stats(|info| { assert_eq!(2, info.stats.compile_requests); assert_eq!(2, info.stats.requests_executed); assert_eq!(1, info.stats.cache_hits.all()); assert_eq!(1, info.stats.cache_misses.all()); assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap()); assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap()); }); } fn test_noncacheable_stats(compiler: Compiler, tempdir: &Path) { let Compiler { name, exe, env_vars } = compiler; trace!("test_noncacheable_stats: {}", name); // Copy the source file into the tempdir so we can compile with relative paths, since the commandline winds up in the hash key. let original_source_file = Path::new(file!()).parent().unwrap().join(INPUT); let source_file = tempdir.join(INPUT); trace!("fs::copy({:?}, {:?})", original_source_file, source_file); fs::copy(&original_source_file, &source_file).unwrap(); trace!("compile"); Command::main_binary().unwrap() .arg(&exe) .arg("-E") .arg(INPUT) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); trace!("request stats"); get_stats(|info| { assert_eq!(1, info.stats.compile_requests); assert_eq!(0, info.stats.requests_executed); assert_eq!(1, info.stats.not_cached.len()); assert_eq!(Some(&1), info.stats.not_cached.get("-E")); }); } fn test_msvc_deps(compiler: Compiler, tempdir: &Path) { let Compiler { name, exe, env_vars } = compiler; // Check that -deps works. trace!("compile with -deps"); let mut args = compile_cmdline(name, &exe, INPUT, OUTPUT); args.push("-depstest.d".into()); sccache_command() .args(&args) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); // Check the contents let mut f = File::open(tempdir.join("test.d")).expect("Failed to open dep file"); let mut buf = String::new(); // read_to_string should be safe because we're supplying all the filenames here, // and there are no absolute paths. f.read_to_string(&mut buf).expect("Failed to read dep file"); let lines: Vec<_> = buf.lines().map(|l| l.trim_right()).collect(); let expected = format!("{output}: {input}\n{input}:\n", output=OUTPUT, input=INPUT); let expected_lines: Vec<_> = expected.lines().collect(); assert_eq!(lines, expected_lines); } fn test_gcc_mp_werror(compiler: Compiler, tempdir: &Path) { let Compiler { name, exe, env_vars } = compiler; trace!("test -MP with -Werror"); let mut args = compile_cmdline(name, &exe, INPUT_ERR, OUTPUT); args.extend(vec_from!(OsString, "-MD", "-MP", "-MF", "foo.pp", "-Werror")); // This should fail, but the error should be from the #error! sccache_command() .args(&args) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .failure() .stderr(predicates::str::contains( "to generate dependencies you must specify either -M or -MM").from_utf8().not()); } fn test_gcc_fprofile_generate_source_changes(compiler: Compiler, tempdir: &Path) { let Compiler { name, exe, env_vars } = compiler; trace!("test -fprofile-generate with different source inputs"); zero_stats(); const SRC: &str = "source.c"; write_source(&tempdir, SRC, "/*line 1*/ #ifndef UNDEFINED /*unused line 1*/ #endif int main(int argc, char** argv) { return 0; } "); let mut args = compile_cmdline(name, &exe, SRC, OUTPUT); args.extend(vec_from!(OsString, "-fprofile-generate")); trace!("compile source.c (1)"); sccache_command() .args(&args) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); get_stats(|info| { assert_eq!(0, info.stats.cache_hits.all()); assert_eq!(1, info.stats.cache_misses.all()); assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap()); }); // Compile the same source again to ensure we can get a cache hit. trace!("compile source.c (2)"); sccache_command() .args(&args) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); get_stats(|info| { assert_eq!(1, info.stats.cache_hits.all()); assert_eq!(1, info.stats.cache_misses.all()); assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap()); assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap()); }); // Now write out a slightly different source file that will preprocess to the same thing, // modulo line numbers. This should not be a cache hit because line numbers are important // with -fprofile-generate. write_source(&tempdir, SRC, "/*line 1*/ #ifndef UNDEFINED /*unused line 1*/ /*unused line 2*/ #endif int main(int argc, char** argv) { return 0; } "); trace!("compile source.c (3)"); sccache_command() .args(&args) .current_dir(tempdir) .envs(env_vars.clone()) .assert() .success(); get_stats(|info| { assert_eq!(1, info.stats.cache_hits.all()); assert_eq!(2, info.stats.cache_misses.all()); assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap()); assert_eq!(&2, info.stats.cache_misses.get("C/C++").unwrap()); }); } fn run_sccache_command_tests(compiler: Compiler, tempdir: &Path) { test_basic_compile(compiler.clone(), tempdir); if compiler.name == "cl.exe" { test_msvc_deps(compiler.clone(), tempdir); } if compiler.name == "gcc" { test_gcc_mp_werror(compiler.clone(), tempdir); test_gcc_fprofile_generate_source_changes(compiler.clone(), tempdir); } } #[cfg(unix)] fn find_compilers() -> Vec { let cwd = env::current_dir().unwrap(); COMPILERS.iter() .filter_map(|c| { match which_in(c, env::var_os("PATH"), &cwd) { Ok(full_path) => match full_path.canonicalize() { Ok(full_path_canon) => Some(Compiler { name: *c, exe: full_path_canon.into_os_string(), env_vars: vec![], }), Err(_) => None, }, Err(_) => None, } }) .collect::>() } #[cfg(target_env="msvc")] fn find_compilers() -> Vec { let tool = cc::Build::new() .opt_level(1) .host("x86_64-pc-windows-msvc") .target("x86_64-pc-windows-msvc") .debug(false) .get_compiler(); vec![ Compiler { name: "cl.exe", exe: tool.path().as_os_str().to_os_string(), env_vars: tool.env().to_vec(), }, ] } #[test] #[cfg(any(unix, target_env="msvc"))] fn test_sccache_command() { match env_logger::try_init() { Ok(_) => {}, Err(_) => {}, } let tempdir = TempDir::new("sccache_system_test").unwrap(); let compilers = find_compilers(); if compilers.is_empty() { warn!("No compilers found, skipping test"); } else { // Ensure there's no existing sccache server running. stop_local_daemon(); // Create the configurations let sccache_cfg = sccache_client_cfg(tempdir.path()); write_json_cfg(tempdir.path(), "sccache-cfg.json", &sccache_cfg); let sccache_cached_cfg_path = tempdir.path().join("sccache-cached-cfg"); // Start a server. trace!("start server"); start_local_daemon(&tempdir.path().join("sccache-cfg.json"), &sccache_cached_cfg_path); for compiler in compilers { run_sccache_command_tests(compiler, tempdir.path()); zero_stats(); } stop_local_daemon(); } }