rust-covfix 0.2.1

Fix Rust coverage data based on source code
Documentation
#[macro_use]
extern crate rust_covfix;

use argparse::{ArgumentParser, Print, Store, StoreOption, StoreTrue};
use error_chain::{bail, ChainedError};
use std::env;
use std::io::BufWriter;
use std::path::PathBuf;
use std::process::{self, Command};

use rust_covfix::error::*;
use rust_covfix::rule;
use rust_covfix::{parser::LcovParser, CoverageFixer, CoverageReader, CoverageWriter};

fn main() {
    if let Err(e) = run() {
        eprintln!("{}", e.display_chain());
        process::exit(1);
    }
}

fn run() -> Result<(), Error> {
    let options = Arguments::parse()?;

    if options.verbose {
        rust_covfix::set_verbosity(4);
    } else {
        rust_covfix::set_verbosity(3);
    }

    let root_dir = options
        .root
        .clone()
        .or_else(find_root_dir)
        .ok_or("cannot find the project root directory. Did you run `cargo test` at first?")?;

    debugln!("Project root directory: {:?}", root_dir);

    let parser = LcovParser::new(root_dir);

    let fixer = match options.rules {
        Some(ref rule_str) => {
            let mut rules = vec![];
            for segment in rule_str.split(',').filter(|v| !v.is_empty()) {
                rules.push(rule::from_str(segment)?);
            }
            CoverageFixer::with_rules(rules)
        }
        None => CoverageFixer::default(),
    };

    debugln!("Reading data file {:?}", options.input_file);

    let mut coverage = parser
        .read_from_file(&options.input_file)
        .chain_err(|| format!("Failed to read coverage from {:?}", options.input_file))?;

    debugln!("Found {} entries", coverage.file_coverages().len());

    if !options.nofix {
        fixer
            .fix(&mut coverage)
            .chain_err(|| "Failed to fix coverage")?;
    }

    if let Some(file) = options.output_file {
        debugln!("Writing coverage to {:?}", file);
        parser
            .write_to_file(&coverage, &file)
            .chain_err(|| format!("Failed to save coverage into file {:?}", file))?;
    } else {
        debugln!("Writing coverage to stdout");
        let stdout = std::io::stdout();
        let mut writer = BufWriter::new(stdout.lock());
        parser.write(&coverage, &mut writer)?;
    }

    Ok(())
}

struct Arguments {
    input_file: PathBuf,
    output_file: Option<PathBuf>,
    root: Option<PathBuf>,
    rules: Option<String>,
    nofix: bool,
    verbose: bool,
}

impl Arguments {
    fn parse() -> Result<Arguments, Error> {
        let mut args = Arguments {
            root: None,
            input_file: PathBuf::new(),
            output_file: None,
            rules: None,
            nofix: false,
            verbose: false,
        };

        let mut ap = ArgumentParser::new();
        ap.set_description("Rust coverage fixer");
        ap.refer(&mut args.input_file)
            .required()
            .add_argument("file", Store, "coverage file");
        ap.add_option(
            &["-V", "--version"],
            Print(env!("CARGO_PKG_VERSION").to_owned()),
            "display version",
        );
        ap.refer(&mut args.verbose)
            .add_option(&["-v", "--verbose"], StoreTrue, "verbose output");
        ap.refer(&mut args.nofix)
            .add_option(&["-n", "--no-fix"], StoreTrue, "do not fix coverage");
        ap.refer(&mut args.output_file).metavar("FILE").add_option(
            &["-o", "--output"],
            StoreOption,
            "output file name (default: stdout)",
        );
        ap.refer(&mut args.root).metavar("DIR").add_option(
            &["--root"],
            StoreOption,
            "project root directory",
        );
        ap.refer(&mut args.rules).metavar("STR[,STR..]").add_option(
            &["--rules"],
            StoreOption,
            "use specified rules to fix coverages. Valid names are [close, test, loop, derive]",
        );

        ap.parse_args_or_exit();
        drop(ap);

        args.validate().chain_err(|| "Argument validation failed")?;
        Ok(args)
    }

    fn validate(&mut self) -> Result<(), Error> {
        if let Some(ref root) = self.root {
            if !root.is_dir() {
                bail!("Directory not found: {:?}", root);
            }
        }

        if !self.input_file.is_file() {
            bail!("Input file not found: {:?}", self.input_file);
        }

        Ok(())
    }
}

fn find_root_dir() -> Option<PathBuf> {
    if let Some(mut target_dir) = find_cargo_target_dir() {
        target_dir.pop();
        return Some(target_dir);
    }

    let mut path = env::current_dir().expect("cannot detect the current directory.");
    path.push("target");

    if path.is_dir() {
        path.pop();
        return Some(path);
    }

    path.pop();

    while path.pop() {
        path.push("target");

        if path.is_dir() {
            path.pop();
            return Some(path);
        }

        path.pop();
    }

    None
}

fn find_cargo_target_dir() -> Option<PathBuf> {
    let output = Command::new("cargo")
        .args(&["metadata", "--format-version", "1"])
        .output()
        .ok()?;

    let stdout = unsafe { String::from_utf8_unchecked(output.stdout) };
    let start = stdout.rfind("\"target_directory\":")? + 20;
    let end = start + stdout[start..].find("\"")?;
    Some(PathBuf::from(&stdout[start..end]))
}