1 // System tests for compiling C code.
2 //
3 // Copyright 2016 Mozilla Foundation
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 // http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16
17 #![deny(rust_2018_idioms)]
18 #![allow(dead_code, unused_imports)]
19
20 #[macro_use]
21 extern crate log;
22 use crate::harness::{
23 get_stats, sccache_client_cfg, sccache_command, start_local_daemon, stop_local_daemon,
24 write_json_cfg, write_source, zero_stats,
25 };
26 use assert_cmd::prelude::*;
27 use log::Level::Trace;
28 use predicates::prelude::*;
29 use std::collections::HashMap;
30 use std::env;
31 use std::ffi::{OsStr, OsString};
32 use std::fmt;
33 use std::fs::{self, File};
34 use std::io::{self, Read, Write};
35 use std::path::{Path, PathBuf};
36 use std::process::{Command, Output, Stdio};
37 use std::str;
38 use which::which_in;
39
40 mod harness;
41
42 #[derive(Clone)]
43 struct Compiler {
44 pub name: &'static str,
45 pub exe: OsString,
46 pub env_vars: Vec<(OsString, OsString)>,
47 }
48
49 // Test GCC + clang on non-OS X platforms.
50 #[cfg(all(unix, not(target_os = "macos")))]
51 const COMPILERS: &[&str] = &["gcc", "clang"];
52
53 // OS X ships a `gcc` that's just a clang wrapper, so only test clang there.
54 #[cfg(target_os = "macos")]
55 const COMPILERS: &[&str] = &["clang"];
56
57 //TODO: could test gcc when targeting mingw.
58
59 macro_rules! vec_from {
60 ( $t:ty, $( $x:expr ),* ) => {
61 vec!($( Into::<$t>::into(&$x), )*)
62 };
63 }
64
65 // TODO: This will fail if gcc/clang is actually a ccache wrapper, as it is the
66 // default case on Fedora, e.g.
compile_cmdline<T: AsRef<OsStr>>( compiler: &str, exe: T, input: &str, output: &str, ) -> Vec<OsString>67 fn compile_cmdline<T: AsRef<OsStr>>(
68 compiler: &str,
69 exe: T,
70 input: &str,
71 output: &str,
72 ) -> Vec<OsString> {
73 match compiler {
74 "gcc" | "clang" => vec_from!(OsString, exe.as_ref(), "-c", input, "-o", output),
75 "cl.exe" => vec_from!(OsString, exe, "-c", input, format!("-Fo{}", output)),
76 _ => panic!("Unsupported compiler: {}", compiler),
77 }
78 }
79
80 const INPUT: &str = "test.c";
81 const INPUT_ERR: &str = "test_err.c";
82 const INPUT_MACRO_EXPANSION: &str = "test_macro_expansion.c";
83 const INPUT_WITH_DEFINE: &str = "test_with_define.c";
84 const OUTPUT: &str = "test.o";
85
86 // Copy the source files into the tempdir so we can compile with relative paths, since the commandline winds up in the hash key.
copy_to_tempdir(inputs: &[&str], tempdir: &Path)87 fn copy_to_tempdir(inputs: &[&str], tempdir: &Path) {
88 for f in inputs {
89 let original_source_file = Path::new(file!()).parent().unwrap().join(&*f);
90 let source_file = tempdir.join(f);
91 trace!("fs::copy({:?}, {:?})", original_source_file, source_file);
92 fs::copy(&original_source_file, &source_file).unwrap();
93 }
94 }
95
test_basic_compile(compiler: Compiler, tempdir: &Path)96 fn test_basic_compile(compiler: Compiler, tempdir: &Path) {
97 let Compiler {
98 name,
99 exe,
100 env_vars,
101 } = compiler;
102 trace!("run_sccache_command_test: {}", name);
103 // Compile a source file.
104 copy_to_tempdir(&[INPUT, INPUT_ERR], tempdir);
105
106 let out_file = tempdir.join("test.o");
107 trace!("compile");
108 sccache_command()
109 .args(&compile_cmdline(name, &exe, INPUT, OUTPUT))
110 .current_dir(tempdir)
111 .envs(env_vars.clone())
112 .assert()
113 .success();
114 assert!(fs::metadata(&out_file).map(|m| m.len() > 0).unwrap());
115 trace!("request stats");
116 get_stats(|info| {
117 assert_eq!(1, info.stats.compile_requests);
118 assert_eq!(1, info.stats.requests_executed);
119 assert_eq!(0, info.stats.cache_hits.all());
120 assert_eq!(1, info.stats.cache_misses.all());
121 assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap());
122 });
123 trace!("compile");
124 fs::remove_file(&out_file).unwrap();
125 sccache_command()
126 .args(&compile_cmdline(name, &exe, INPUT, OUTPUT))
127 .current_dir(tempdir)
128 .envs(env_vars)
129 .assert()
130 .success();
131 assert!(fs::metadata(&out_file).map(|m| m.len() > 0).unwrap());
132 trace!("request stats");
133 get_stats(|info| {
134 assert_eq!(2, info.stats.compile_requests);
135 assert_eq!(2, info.stats.requests_executed);
136 assert_eq!(1, info.stats.cache_hits.all());
137 assert_eq!(1, info.stats.cache_misses.all());
138 assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap());
139 assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap());
140 });
141 }
142
test_noncacheable_stats(compiler: Compiler, tempdir: &Path)143 fn test_noncacheable_stats(compiler: Compiler, tempdir: &Path) {
144 let Compiler {
145 name,
146 exe,
147 env_vars,
148 } = compiler;
149 trace!("test_noncacheable_stats: {}", name);
150 copy_to_tempdir(&[INPUT], tempdir);
151
152 trace!("compile");
153 Command::new(assert_cmd::cargo::cargo_bin("sccache"))
154 .arg(&exe)
155 .arg("-E")
156 .arg(INPUT)
157 .current_dir(tempdir)
158 .envs(env_vars)
159 .assert()
160 .success();
161 trace!("request stats");
162 get_stats(|info| {
163 assert_eq!(1, info.stats.compile_requests);
164 assert_eq!(0, info.stats.requests_executed);
165 assert_eq!(1, info.stats.not_cached.len());
166 assert_eq!(Some(&1), info.stats.not_cached.get("-E"));
167 });
168 }
169
test_msvc_deps(compiler: Compiler, tempdir: &Path)170 fn test_msvc_deps(compiler: Compiler, tempdir: &Path) {
171 let Compiler {
172 name,
173 exe,
174 env_vars,
175 } = compiler;
176 // Check that -deps works.
177 trace!("compile with -deps");
178 let mut args = compile_cmdline(name, &exe, INPUT, OUTPUT);
179 args.push("-depstest.d".into());
180 sccache_command()
181 .args(&args)
182 .current_dir(tempdir)
183 .envs(env_vars)
184 .assert()
185 .success();
186 // Check the contents
187 let mut f = File::open(tempdir.join("test.d")).expect("Failed to open dep file");
188 let mut buf = String::new();
189 // read_to_string should be safe because we're supplying all the filenames here,
190 // and there are no absolute paths.
191 f.read_to_string(&mut buf).expect("Failed to read dep file");
192 let lines: Vec<_> = buf.lines().map(|l| l.trim_end()).collect();
193 let expected = format!(
194 "{output}: {input}\n{input}:\n",
195 output = OUTPUT,
196 input = INPUT
197 );
198 let expected_lines: Vec<_> = expected.lines().collect();
199 assert_eq!(lines, expected_lines);
200 }
201
test_gcc_mp_werror(compiler: Compiler, tempdir: &Path)202 fn test_gcc_mp_werror(compiler: Compiler, tempdir: &Path) {
203 let Compiler {
204 name,
205 exe,
206 env_vars,
207 } = compiler;
208 trace!("test -MP with -Werror");
209 let mut args = compile_cmdline(name, &exe, INPUT_ERR, OUTPUT);
210 args.extend(vec_from!(
211 OsString, "-MD", "-MP", "-MF", "foo.pp", "-Werror"
212 ));
213 // This should fail, but the error should be from the #error!
214 sccache_command()
215 .args(&args)
216 .current_dir(tempdir)
217 .envs(env_vars)
218 .assert()
219 .failure()
220 .stderr(
221 predicates::str::contains("to generate dependencies you must specify either -M or -MM")
222 .from_utf8()
223 .not(),
224 );
225 }
226
test_gcc_fprofile_generate_source_changes(compiler: Compiler, tempdir: &Path)227 fn test_gcc_fprofile_generate_source_changes(compiler: Compiler, tempdir: &Path) {
228 let Compiler {
229 name,
230 exe,
231 env_vars,
232 } = compiler;
233 trace!("test -fprofile-generate with different source inputs");
234 zero_stats();
235 const SRC: &str = "source.c";
236 write_source(
237 &tempdir,
238 SRC,
239 "/*line 1*/
240 #ifndef UNDEFINED
241 /*unused line 1*/
242 #endif
243
244 int main(int argc, char** argv) {
245 return 0;
246 }
247 ",
248 );
249 let mut args = compile_cmdline(name, &exe, SRC, OUTPUT);
250 args.extend(vec_from!(OsString, "-fprofile-generate"));
251 trace!("compile source.c (1)");
252 sccache_command()
253 .args(&args)
254 .current_dir(tempdir)
255 .envs(env_vars.clone())
256 .assert()
257 .success();
258 get_stats(|info| {
259 assert_eq!(0, info.stats.cache_hits.all());
260 assert_eq!(1, info.stats.cache_misses.all());
261 assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap());
262 });
263 // Compile the same source again to ensure we can get a cache hit.
264 trace!("compile source.c (2)");
265 sccache_command()
266 .args(&args)
267 .current_dir(tempdir)
268 .envs(env_vars.clone())
269 .assert()
270 .success();
271 get_stats(|info| {
272 assert_eq!(1, info.stats.cache_hits.all());
273 assert_eq!(1, info.stats.cache_misses.all());
274 assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap());
275 assert_eq!(&1, info.stats.cache_misses.get("C/C++").unwrap());
276 });
277 // Now write out a slightly different source file that will preprocess to the same thing,
278 // modulo line numbers. This should not be a cache hit because line numbers are important
279 // with -fprofile-generate.
280 write_source(
281 &tempdir,
282 SRC,
283 "/*line 1*/
284 #ifndef UNDEFINED
285 /*unused line 1*/
286 /*unused line 2*/
287 #endif
288
289 int main(int argc, char** argv) {
290 return 0;
291 }
292 ",
293 );
294 trace!("compile source.c (3)");
295 sccache_command()
296 .args(&args)
297 .current_dir(tempdir)
298 .envs(env_vars)
299 .assert()
300 .success();
301 get_stats(|info| {
302 assert_eq!(1, info.stats.cache_hits.all());
303 assert_eq!(2, info.stats.cache_misses.all());
304 assert_eq!(&1, info.stats.cache_hits.get("C/C++").unwrap());
305 assert_eq!(&2, info.stats.cache_misses.get("C/C++").unwrap());
306 });
307 }
308
test_gcc_clang_no_warnings_from_macro_expansion(compiler: Compiler, tempdir: &Path)309 fn test_gcc_clang_no_warnings_from_macro_expansion(compiler: Compiler, tempdir: &Path) {
310 let Compiler {
311 name,
312 exe,
313 env_vars,
314 } = compiler;
315 trace!("test_gcc_clang_no_warnings_from_macro_expansion: {}", name);
316 // Compile a source file.
317 copy_to_tempdir(&[INPUT_MACRO_EXPANSION], tempdir);
318
319 trace!("compile");
320 sccache_command()
321 .args(
322 [
323 &compile_cmdline(name, &exe, INPUT_MACRO_EXPANSION, OUTPUT)[..],
324 &vec_from!(OsString, "-Wunreachable-code")[..],
325 ]
326 .concat(),
327 )
328 .current_dir(tempdir)
329 .envs(env_vars)
330 .assert()
331 .success()
332 .stderr(predicates::str::contains("warning:").from_utf8().not());
333 }
334
test_compile_with_define(compiler: Compiler, tempdir: &Path)335 fn test_compile_with_define(compiler: Compiler, tempdir: &Path) {
336 let Compiler {
337 name,
338 exe,
339 env_vars,
340 } = compiler;
341 trace!("test_compile_with_define: {}", name);
342 // Compile a source file.
343 copy_to_tempdir(&[INPUT_WITH_DEFINE], tempdir);
344
345 trace!("compile");
346 sccache_command()
347 .args(
348 [
349 &compile_cmdline(name, &exe, INPUT_WITH_DEFINE, OUTPUT)[..],
350 &vec_from!(OsString, "-DSCCACHE_TEST_DEFINE")[..],
351 ]
352 .concat(),
353 )
354 .current_dir(tempdir)
355 .envs(env_vars)
356 .assert()
357 .success()
358 .stderr(predicates::str::contains("warning:").from_utf8().not());
359 }
360
run_sccache_command_tests(compiler: Compiler, tempdir: &Path)361 fn run_sccache_command_tests(compiler: Compiler, tempdir: &Path) {
362 test_basic_compile(compiler.clone(), tempdir);
363 test_compile_with_define(compiler.clone(), tempdir);
364 if compiler.name == "cl.exe" {
365 test_msvc_deps(compiler.clone(), tempdir);
366 }
367 if compiler.name == "gcc" {
368 test_gcc_mp_werror(compiler.clone(), tempdir);
369 test_gcc_fprofile_generate_source_changes(compiler.clone(), tempdir);
370 }
371 if compiler.name == "clang" || compiler.name == "gcc" {
372 test_gcc_clang_no_warnings_from_macro_expansion(compiler, tempdir);
373 }
374 }
375
376 #[cfg(unix)]
find_compilers() -> Vec<Compiler>377 fn find_compilers() -> Vec<Compiler> {
378 let cwd = env::current_dir().unwrap();
379 COMPILERS
380 .iter()
381 .filter_map(|c| match which_in(c, env::var_os("PATH"), &cwd) {
382 Ok(full_path) => match full_path.canonicalize() {
383 Ok(full_path_canon) => Some(Compiler {
384 name: *c,
385 exe: full_path_canon.into_os_string(),
386 env_vars: vec![],
387 }),
388 Err(_) => None,
389 },
390 Err(_) => None,
391 })
392 .collect::<Vec<_>>()
393 }
394
395 #[cfg(target_env = "msvc")]
find_compilers() -> Vec<Compiler>396 fn find_compilers() -> Vec<Compiler> {
397 let tool = cc::Build::new()
398 .opt_level(1)
399 .host("x86_64-pc-windows-msvc")
400 .target("x86_64-pc-windows-msvc")
401 .debug(false)
402 .get_compiler();
403 vec![Compiler {
404 name: "cl.exe",
405 exe: tool.path().as_os_str().to_os_string(),
406 env_vars: tool.env().to_vec(),
407 }]
408 }
409
410 // TODO: This runs multiple test cases, for multiple compilers. It should be
411 // split up to run them individually. In the current form, it is hard to see
412 // which sub test cases are executed, and if one fails, the remaining tests
413 // are not run.
414 #[test]
415 #[cfg(any(unix, target_env = "msvc"))]
test_sccache_command()416 fn test_sccache_command() {
417 let _ = env_logger::try_init();
418 let tempdir = tempfile::Builder::new()
419 .prefix("sccache_system_test")
420 .tempdir()
421 .unwrap();
422 let compilers = find_compilers();
423 if compilers.is_empty() {
424 warn!("No compilers found, skipping test");
425 } else {
426 // Ensure there's no existing sccache server running.
427 stop_local_daemon();
428 // Create the configurations
429 let sccache_cfg = sccache_client_cfg(tempdir.path());
430 write_json_cfg(tempdir.path(), "sccache-cfg.json", &sccache_cfg);
431 let sccache_cached_cfg_path = tempdir.path().join("sccache-cached-cfg");
432 // Start a server.
433 trace!("start server");
434 start_local_daemon(
435 &tempdir.path().join("sccache-cfg.json"),
436 &sccache_cached_cfg_path,
437 );
438 for compiler in compilers {
439 run_sccache_command_tests(compiler, tempdir.path());
440 zero_stats();
441 }
442 stop_local_daemon();
443 }
444 }
445