git-checks 4.0.1

Checks to run against a topic in git to enforce coding standards.
Documentation
// Copyright Kitware, Inc.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::env;
use std::fs;
use std::iter;
use std::path::Path;
use std::process::Command;

use crates::git_checks_core::{BranchCheck, Check, CheckResult, TopicCheck};
use crates::git_workarea::{CommitId, GitContext, Identity};
use crates::itertools;
#[cfg(feature = "config")]
use crates::serde_json;
use crates::tempdir::TempDir;

pub use crates::git_checks_core::GitCheckConfiguration;

// pub const ROOT_COMMIT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
pub const FILLER_COMMIT: &str = "d02f015907371738253a22b9a7fec78607a969b2";
pub const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
pub const SUBMODULE_TARGET_COMMIT: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";

fn make_context() -> GitContext {
    let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
    if !gitdir.exists() {
        panic!("The tests must be run from a git checkout.");
    }

    GitContext::new(gitdir)
}

pub fn make_temp_dir(test_name: &str) -> TempDir {
    let mut working_dir = env::current_exe().unwrap();
    working_dir.pop();

    TempDir::new_in(working_dir, test_name).unwrap()
}

pub fn make_context_submodule(gitdir: &Path, commit: &CommitId) -> GitContext {
    let gitdir = gitdir.join("origin");
    let clone = Command::new("git")
        .arg("clone")
        .arg(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"))
        .arg(&gitdir)
        .output()
        .unwrap();
    if !clone.status.success() {
        panic!(
            "origin clone failed: {}",
            String::from_utf8_lossy(&clone.stderr),
        );
    }

    let ctx = GitContext::new(gitdir.join(".git"));

    let checkout = ctx
        .git()
        .arg("checkout")
        .arg(commit.as_str())
        .current_dir(&gitdir)
        .output()
        .unwrap();
    if !checkout.status.success() {
        panic!(
            "checkout failed: {}",
            String::from_utf8_lossy(&checkout.stderr),
        );
    }

    let submodule_update = ctx
        .git()
        .arg("submodule")
        .arg("update")
        .arg("--init")
        .current_dir(&gitdir)
        .output()
        .unwrap();
    if !submodule_update.status.success() {
        panic!(
            "submodule update failed: {}",
            String::from_utf8_lossy(&submodule_update.stderr),
        );
    }

    ctx
}

fn test_check_custom<'a>(
    ctx: &GitContext,
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration<'a>,
) -> CheckResult {
    conf.run_topic(
        &ctx,
        test_name,
        &CommitId::new(base),
        &CommitId::new(topic),
        &Identity::new("Rust Git Checks Tests", "rust-git-checks@example.com"),
    )
    .unwrap()
    .into()
}

pub fn test_check_base<'a>(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration<'a>,
) -> CheckResult {
    let ctx = make_context();
    test_check_custom(&ctx, test_name, topic, base, conf)
}

pub fn test_check<'a>(
    test_name: &str,
    topic: &str,
    conf: &GitCheckConfiguration<'a>,
) -> CheckResult {
    test_check_base(test_name, topic, TARGET_COMMIT, conf)
}

pub fn test_check_submodule_base<'a>(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration<'a>,
) -> CheckResult {
    let tempdir = make_temp_dir(test_name);
    let ctx = make_context_submodule(tempdir.path(), &CommitId::new(base));
    test_check_custom(&ctx, test_name, topic, base, conf)
}

pub fn test_check_submodule<'a>(
    test_name: &str,
    topic: &str,
    conf: &GitCheckConfiguration<'a>,
) -> CheckResult {
    test_check_submodule_base(test_name, topic, SUBMODULE_TARGET_COMMIT, conf)
}

pub fn test_check_submodule_base_configure<'a>(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration<'a>,
    module: &str,
) -> CheckResult {
    let tempdir = make_temp_dir(test_name);
    let ctx = make_context_submodule(tempdir.path(), &CommitId::new(base));
    let moduledir = ctx.gitdir().join("modules").join(module);
    fs::create_dir_all(moduledir).unwrap();
    test_check_custom(&ctx, test_name, topic, base, conf)
}

pub fn test_check_submodule_configure<'a>(
    test_name: &str,
    topic: &str,
    conf: &GitCheckConfiguration<'a>,
    module: &str,
) -> CheckResult {
    test_check_submodule_base_configure(test_name, topic, SUBMODULE_TARGET_COMMIT, conf, module)
}

fn no_strings<'a>() -> iter::Empty<&'a String> {
    iter::empty()
}

pub fn test_result_warnings(result: CheckResult, warnings: &[&str]) {
    itertools::assert_equal(result.warnings(), warnings);
    itertools::assert_equal(result.alerts(), no_strings());
    itertools::assert_equal(result.errors(), no_strings());
    assert_eq!(result.temporary(), false);
    assert_eq!(result.allowed(), false);
    assert_eq!(result.pass(), true);
}

pub fn test_result_errors(result: CheckResult, errors: &[&str]) {
    itertools::assert_equal(result.warnings(), no_strings());
    itertools::assert_equal(result.alerts(), no_strings());
    itertools::assert_equal(result.errors(), errors);
    assert_eq!(result.temporary(), false);
    assert_eq!(result.allowed(), false);
    assert_eq!(result.pass(), false);
}

pub fn test_result_ok(result: CheckResult) {
    itertools::assert_equal(result.warnings(), no_strings());
    itertools::assert_equal(result.alerts(), no_strings());
    itertools::assert_equal(result.errors(), no_strings());
    assert_eq!(result.temporary(), false);
    assert_eq!(result.allowed(), false);
    assert_eq!(result.pass(), true);
}

pub fn make_check_conf<T>(check: &T) -> GitCheckConfiguration
where
    T: Check,
{
    let mut conf = GitCheckConfiguration::new();
    conf.add_check(check);
    conf
}

pub fn run_check<T>(name: &str, commit: &str, check: T) -> CheckResult
where
    T: Check,
{
    let conf = make_check_conf(&check);
    test_check(name, commit, &conf)
}

pub fn run_check_ok<T>(name: &str, commit: &str, check: T)
where
    T: Check,
{
    test_result_ok(run_check(name, commit, check));
}

pub fn make_branch_check_conf<T>(check: &T) -> GitCheckConfiguration
where
    T: BranchCheck,
{
    let mut conf = GitCheckConfiguration::new();
    conf.add_branch_check(check);
    conf
}

pub fn run_branch_check<T>(name: &str, commit: &str, check: T) -> CheckResult
where
    T: BranchCheck,
{
    let conf = make_branch_check_conf(&check);
    test_check(name, commit, &conf)
}

pub fn run_branch_check_ok<T>(name: &str, commit: &str, check: T)
where
    T: BranchCheck,
{
    test_result_ok(run_branch_check(name, commit, check));
}

pub fn make_topic_check_conf<T>(check: &T) -> GitCheckConfiguration
where
    T: TopicCheck,
{
    let mut conf = GitCheckConfiguration::new();
    conf.add_topic_check(check);
    conf
}

pub fn run_topic_check<T>(name: &str, commit: &str, check: T) -> CheckResult
where
    T: TopicCheck,
{
    let conf = make_topic_check_conf(&check);
    test_check(name, commit, &conf)
}

pub fn run_topic_check_ok<T>(name: &str, commit: &str, check: T)
where
    T: TopicCheck,
{
    test_result_ok(run_topic_check(name, commit, check));
}

#[cfg(feature = "config")]
pub fn check_missing_json_field(err: serde_json::Error, field: &str) {
    assert!(!err.is_io());
    assert!(!err.is_syntax());
    assert!(err.is_data());
    assert!(!err.is_eof());

    let msg = format!("{}", err);
    if msg != format!("missing field `{}`", field) {
        println!(
            "Error message doesn't match. Was a new required field added? ({})",
            msg,
        );
    }
}