git-checks 4.3.2

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.

//! Test utilities
//!
//! This module contains utility functions and constants for use in testing checks.

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

use git_checks_core::{BranchCheck, Check, CheckResult, TopicCheck};
use git_workarea::{CommitId, GitContext, Identity};
use log::{Level, LevelFilter, Log, Metadata, Record};
use tempfile::TempDir;

pub use git_checks_core::GitCheckConfiguration;

// pub const ROOT_COMMIT: &str = "1b2c7b3dad2fb7d14fbf0b619bf7faca4dd01243";
/// An empty commit on top of the root commit which is not a root commit.
pub const FILLER_COMMIT: &str = "d02f015907371738253a22b9a7fec78607a969b2";
/// The commit which checks are "targeting" to be checked against.
pub const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
/// The commit which submodule-aware checks are "targeting" to be checked against.
pub const SUBMODULE_TARGET_COMMIT: &str = "fe90ee22ae3ce4b4dc41f8d0876e59355ff1e21c";

fn setup_logging() {
    struct SimpleLogger;

    impl Log for SimpleLogger {
        fn enabled(&self, metadata: &Metadata) -> bool {
            metadata.level() <= Level::Debug
        }

        fn log(&self, record: &Record) {
            if self.enabled(record.metadata()) {
                println!("[{}] {}", record.level(), record.args());
            }
        }

        fn flush(&self) {}
    }

    static LOGGER: SimpleLogger = SimpleLogger;

    // Since the tests run in parallel, this may get called multiple times. Just ignore errors.
    let _ = log::set_logger(&LOGGER);
    log::set_max_level(LevelFilter::Debug);
}

fn make_context() -> GitContext {
    setup_logging();

    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)
}

/// Create a temporary directory for use by a test.
pub fn make_temp_dir() -> TempDir {
    let mut working_dir = env::current_exe().unwrap();
    working_dir.pop();

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

/// Create a git context with submodule support.
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(
    ctx: &GitContext,
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration,
) -> CheckResult {
    test_check_custom_ident(
        ctx,
        test_name,
        topic,
        base,
        conf,
        Identity::new("Rust Git Checks Tests", "rust-git-checks@example.com"),
    )
}

fn test_check_custom_ident(
    ctx: &GitContext,
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration,
    identity: Identity,
) -> CheckResult {
    conf.run_topic(
        ctx,
        test_name,
        &CommitId::new(base),
        &CommitId::new(topic),
        &identity,
    )
    .unwrap()
    .into()
}

/// Run a check on a topic against a given target commit.
pub fn test_check_base(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration,
) -> CheckResult {
    let ctx = make_context();
    test_check_custom(&ctx, test_name, topic, base, conf)
}

/// Run a check on a topic against the default target commit.
pub fn test_check(test_name: &str, topic: &str, conf: &GitCheckConfiguration) -> CheckResult {
    test_check_base(test_name, topic, TARGET_COMMIT, conf)
}

/// Run a check with submodule support on a topic against a given target commit.
pub fn test_check_submodule_base(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration,
) -> CheckResult {
    let tempdir = make_temp_dir();
    let ctx = make_context_submodule(tempdir.path(), &CommitId::new(base));
    test_check_custom(&ctx, test_name, topic, base, conf)
}

/// Run a check with submodule support on a topic against the default submodule target commit.
pub fn test_check_submodule(
    test_name: &str,
    topic: &str,
    conf: &GitCheckConfiguration,
) -> CheckResult {
    test_check_submodule_base(test_name, topic, SUBMODULE_TARGET_COMMIT, conf)
}

/// Run a check with submodule support on a topic against a given target commit.
///
/// Also initializes a named submodule.
pub fn test_check_submodule_base_configure(
    test_name: &str,
    topic: &str,
    base: &str,
    conf: &GitCheckConfiguration,
    module: &str,
) -> CheckResult {
    let tempdir = make_temp_dir();
    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)
}

/// Run a check with submodule support on a topic against the default submodule target commit.
///
/// Also initializes a named submodule.
pub fn test_check_submodule_configure(
    test_name: &str,
    topic: &str,
    conf: &GitCheckConfiguration,
    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()
}

/// Test that a check results in a set of warnings.
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!(!result.temporary());
    assert!(!result.allowed());
    assert!(result.pass());
}

/// Test that a check results in a set of errors.
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!(!result.temporary());
    assert!(!result.allowed());
    assert!(!result.pass());
}

/// Test that a check results without any special reporting
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!(!result.temporary());
    assert!(!result.allowed());
    assert!(result.pass());
}

/// Create a check configuration with a given check.
pub fn make_check_conf<T>(check: &T) -> GitCheckConfiguration<'_>
where
    T: Check,
{
    let mut conf = GitCheckConfiguration::new();
    conf.add_check(check);
    conf
}

/// Run a named check against a commit.
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)
}

/// Run a named check against a commit and ensure that it doesn't report anything.
pub fn run_check_ok<T>(name: &str, commit: &str, check: T)
where
    T: Check,
{
    test_result_ok(run_check(name, commit, check));
}

/// Create a check configuration with a given branch 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
}

/// Run a named branch check against a commit with a given submitter identity.
pub fn run_branch_check_ident<T>(
    name: &str,
    commit: &str,
    check: T,
    identity: Identity,
) -> CheckResult
where
    T: BranchCheck,
{
    let conf = make_branch_check_conf(&check);
    let ctx = make_context();
    test_check_custom_ident(&ctx, name, commit, TARGET_COMMIT, &conf, identity)
}

/// Run a named branch check against a commit with a given submitter identity and ensure that it
/// doesn't report anything.
pub fn run_branch_check_ident_ok<T>(name: &str, commit: &str, check: T, identity: Identity)
where
    T: BranchCheck,
{
    test_result_ok(run_branch_check_ident(name, commit, check, identity));
}

/// Run a named branch check against a commit.
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)
}

/// Run a named branch check against a commit and ensure that it doesn't report anything.
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));
}

/// Create a check configuration with a given topic 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
}

/// Run a named topic check against a commit.
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)
}

/// Run a named topic check against a commit and ensure that it doesn't report anything.
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")]
/// Check that a JSON error represents a specific missing field.
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());

    assert_eq!(err.to_string(), format!("missing field `{}`", field));
}