cargo_tarpaulin/
lib.rs

1use crate::cargo::TestBinary;
2use crate::config::*;
3use crate::errors::*;
4use crate::event_log::*;
5use crate::path_utils::*;
6use crate::process_handling::*;
7use crate::report::report_coverage;
8use crate::source_analysis::{LineAnalysis, SourceAnalysis};
9use crate::test_loader::*;
10use crate::traces::*;
11use std::ffi::OsString;
12use std::fs::{create_dir_all, remove_dir_all};
13use std::io;
14use tracing::{debug, error, info, warn};
15use tracing_subscriber::{filter::LevelFilter, EnvFilter};
16
17pub mod args;
18pub mod cargo;
19pub mod config;
20pub mod errors;
21pub mod event_log;
22pub mod path_utils;
23mod process_handling;
24pub mod report;
25pub mod source_analysis;
26pub mod statemachine;
27pub mod test_loader;
28pub mod traces;
29
30const RUST_LOG_ENV: &str = "RUST_LOG";
31
32#[cfg(not(tarpaulin_include))]
33pub fn setup_logging(color: Color, debug: bool, verbose: bool, stderr: bool) {
34    //By default, we set tarpaulin to info,debug,trace while all dependencies stay at INFO
35    let base_exceptions = |env: EnvFilter| {
36        if debug {
37            env.add_directive("cargo_tarpaulin=trace".parse().unwrap())
38                .add_directive("llvm_profparser=trace".parse().unwrap())
39        } else if verbose {
40            env.add_directive("cargo_tarpaulin=debug".parse().unwrap())
41                .add_directive("llvm_profparser=warn".parse().unwrap())
42        } else {
43            env.add_directive("cargo_tarpaulin=info".parse().unwrap())
44                .add_directive("llvm_profparser=error".parse().unwrap())
45        }
46        .add_directive(LevelFilter::INFO.into())
47    };
48
49    //If RUST_LOG is set, then first apply our default directives (which are controlled by debug an verbose).
50    // Then RUST_LOG will overwrite those default directives.
51    // e.g. `RUST_LOG="trace" cargo-tarpaulin` will end up printing TRACE for everything
52    // `cargo-tarpaulin -v` will print DEBUG for tarpaulin and INFO for everything else.
53    // `RUST_LOG="error" cargo-tarpaulin -v` will print ERROR for everything.
54    let filter = match std::env::var_os(RUST_LOG_ENV).map(OsString::into_string) {
55        Some(Ok(env)) => {
56            let mut filter = base_exceptions(EnvFilter::new(""));
57            for s in env.split(',') {
58                match s.parse() {
59                    Ok(d) => filter = filter.add_directive(d),
60                    Err(err) => println!("WARN ignoring log directive: `{s}`: {err}"),
61                };
62            }
63            filter
64        }
65        _ => base_exceptions(EnvFilter::from_env(RUST_LOG_ENV)),
66    };
67
68    let with_ansi = color != Color::Never;
69
70    let builder = tracing_subscriber::FmtSubscriber::builder()
71        .with_max_level(tracing::Level::ERROR)
72        .with_env_filter(filter)
73        .with_ansi(with_ansi);
74
75    let res = if stderr {
76        builder.with_writer(io::stderr).try_init()
77    } else {
78        builder.try_init()
79    };
80
81    if let Err(e) = res {
82        eprintln!("Logging may be misconfigured: {e}");
83    }
84
85    debug!("set up logging");
86}
87
88pub fn trace(configs: &[Config]) -> Result<(TraceMap, i32), RunError> {
89    let logger = create_logger(configs);
90    let mut tracemap = TraceMap::new();
91    let mut ret = 0;
92    let mut fail_fast_ret = 0;
93    let mut tarpaulin_result = Ok(());
94    let mut bad_threshold = Ok(());
95
96    for config in configs.iter() {
97        if config.name == "report" {
98            continue;
99        }
100
101        if let Some(log) = logger.as_ref() {
102            let name = config_name(config);
103            log.push_config(name);
104        }
105
106        create_target_dir(config);
107
108        match launch_tarpaulin(config, &logger) {
109            Ok((t, r)) => {
110                if config.no_fail_fast {
111                    fail_fast_ret |= r;
112                } else {
113                    ret |= r;
114                }
115                if configs.len() > 1 {
116                    // Otherwise threshold is a global one and we'll let the caller handle it
117                    bad_threshold = check_fail_threshold(&t, config);
118                }
119                tracemap.merge(&t);
120            }
121            Err(e) => {
122                error!("{e}");
123                tarpaulin_result = tarpaulin_result.and(Err(e));
124            }
125        }
126    }
127
128    tracemap.dedup();
129
130    // It's OK that bad_threshold, tarpaulin_result may be overwritten in a loop
131    if let Err(bad_limit) = bad_threshold {
132        // Failure threshold probably more important than reporting failing
133        let _ = report_coverage(&configs[0], &tracemap);
134        Err(bad_limit)
135    } else if ret == 0 {
136        tarpaulin_result.map(|_| (tracemap, fail_fast_ret))
137    } else {
138        Err(RunError::TestFailed)
139    }
140}
141
142fn create_logger(configs: &[Config]) -> Option<EventLog> {
143    if configs.iter().any(|c| c.dump_traces) {
144        let config = if let Some(c) = configs.iter().find(|c| c.output_directory.is_some()) {
145            c
146        } else {
147            &configs[0]
148        };
149
150        Some(EventLog::new(
151            configs.iter().map(|x| x.root()).collect(),
152            config,
153        ))
154    } else {
155        None
156    }
157}
158
159fn create_target_dir(config: &Config) {
160    let path = config.target_dir();
161    if !path.exists() {
162        if let Err(e) = create_dir_all(&path) {
163            warn!("Failed to create target-dir {}", e);
164        }
165    }
166}
167
168fn config_name(config: &Config) -> String {
169    if config.name.is_empty() {
170        "<anonymous>".to_string()
171    } else {
172        config.name.clone()
173    }
174}
175
176fn check_fail_threshold(traces: &TraceMap, config: &Config) -> Result<(), RunError> {
177    let percent = traces.coverage_percentage() * 100.0;
178    match config.fail_under.as_ref() {
179        Some(limit) if percent < *limit => {
180            let error = RunError::BelowThreshold(percent, *limit);
181            error!("{}", error);
182            Err(error)
183        }
184        _ => Ok(()),
185    }
186}
187
188pub fn run(configs: &[Config]) -> Result<(), RunError> {
189    if configs.iter().any(|x| x.engine() == TraceEngine::Llvm) {
190        let profraw_dir = configs[0].profraw_dir();
191        let _ = remove_dir_all(&profraw_dir);
192        if let Err(e) = create_dir_all(&profraw_dir) {
193            warn!(
194                "Unable to create profraw directory in tarpaulin's target folder: {}",
195                e
196            );
197        }
198    }
199    let (tracemap, ret) = collect_tracemap(configs)?;
200    report_tracemap(configs, tracemap)?;
201    if ret != 0 {
202        // So we had a test fail in a way where we still want to report coverage so since we've now
203        // done that we can return the test failed error.
204        Err(RunError::TestFailed)
205    } else {
206        Ok(())
207    }
208}
209
210fn collect_tracemap(configs: &[Config]) -> Result<(TraceMap, i32), RunError> {
211    let (mut tracemap, ret) = trace(configs)?;
212    if !configs.is_empty() {
213        // Assumption: all configs are for the same project
214        for dir in get_source_walker(&configs[0]) {
215            tracemap.add_file(dir.path());
216        }
217    }
218
219    Ok((tracemap, ret))
220}
221
222pub fn report_tracemap(configs: &[Config], tracemap: TraceMap) -> Result<(), RunError> {
223    let mut reported = false;
224    for c in configs.iter() {
225        if c.no_run || c.name != "report" {
226            continue;
227        }
228
229        report_coverage_with_check(c, &tracemap)?;
230        reported = true;
231    }
232
233    if !reported && !configs.is_empty() && !configs[0].no_run {
234        report_coverage_with_check(&configs[0], &tracemap)?;
235    }
236
237    Ok(())
238}
239
240fn report_coverage_with_check(c: &Config, tracemap: &TraceMap) -> Result<(), RunError> {
241    report_coverage(c, tracemap)?;
242    check_fail_threshold(tracemap, c)
243}
244
245/// Launches tarpaulin with the given configuration.
246pub fn launch_tarpaulin(
247    config: &Config,
248    logger: &Option<EventLog>,
249) -> Result<(TraceMap, i32), RunError> {
250    if !config.name.is_empty() {
251        info!("Running config {}", config.name);
252    }
253
254    info!("Running Tarpaulin");
255
256    let mut result = TraceMap::new();
257    let mut return_code = 0i32;
258    info!("Building project");
259    let executables = cargo::get_tests(config)?;
260    if !config.no_run {
261        let project_analysis = SourceAnalysis::get_analysis(config);
262        result.set_functions(project_analysis.create_function_map());
263        let project_analysis = project_analysis.lines;
264        let mut other_bins = config.objects().to_vec();
265        other_bins.extend(executables.binaries.iter().cloned());
266        for exe in &executables.test_binaries {
267            if exe.should_panic() {
268                info!("Running a test executable that is expected to panic");
269            }
270            let coverage =
271                get_test_coverage(exe, &other_bins, &project_analysis, config, false, logger);
272
273            let coverage = match coverage {
274                Ok(coverage) => coverage,
275                Err(run_error) => {
276                    if config.no_fail_fast {
277                        info!("No failing fast!");
278                        return_code = 101;
279                        None
280                    } else {
281                        return Err(run_error);
282                    }
283                }
284            };
285            if let Some(res) = coverage {
286                result.merge(&res.0);
287                return_code |= if exe.should_panic() {
288                    (res.1 == 0).into()
289                } else {
290                    res.1
291                };
292            }
293            if config.run_ignored {
294                let coverage =
295                    get_test_coverage(exe, &other_bins, &project_analysis, config, true, logger);
296                let coverage = match coverage {
297                    Ok(coverage) => coverage,
298                    Err(run_error) => {
299                        if config.no_fail_fast {
300                            return_code = 101;
301                            None
302                        } else {
303                            return Err(run_error);
304                        }
305                    }
306                };
307                if let Some(res) = coverage {
308                    result.merge(&res.0);
309                    return_code |= res.1;
310                }
311            }
312
313            if config.fail_immediately && return_code != 0 {
314                return Err(RunError::TestFailed);
315            }
316        }
317        result.dedup();
318    }
319    Ok((result, return_code))
320}