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