git_checks_core/
run.rs

1// Copyright Kitware, Inc.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::collections::BTreeMap;
10use std::fmt::{self, Debug};
11use std::iter;
12use std::slice;
13
14use git_workarea::{CommitId, GitContext, GitError, Identity, WorkAreaError};
15use log::{debug, error};
16use rayon::prelude::*;
17use thiserror::Error;
18
19use crate::check::{BranchCheck, Check, CheckResult, TopicCheck};
20use crate::commit::{Commit, CommitError, Topic};
21use crate::context::CheckGitContext;
22
23/// Errors which can occur when running checks.
24#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum RunError {
27    /// Command preparation failure.
28    #[error("git error: {}", source)]
29    Git {
30        /// The cause of the error.
31        #[from]
32        source: GitError,
33    },
34    /// An error occurred when working with the workarea.
35    #[error("git workarea error: {}", source)]
36    WorkArea {
37        /// The cause of the error.
38        #[from]
39        source: WorkAreaError,
40    },
41    /// An error occurred when working with a commit.
42    #[error("commit error: {}", source)]
43    Commit {
44        /// The cause of the error.
45        #[from]
46        source: CommitError,
47    },
48    /// Failure to create a ref to refer to the checked commit.
49    #[error("run check error: failed to update the {} ref: {}", base_ref, output)]
50    UpdateRef {
51        /// The base name of the ref.
52        base_ref: CommitId,
53        /// Git's output for the error.
54        output: String,
55    },
56    /// Failure to list revisions to check.
57    #[error(
58        "run check error: failed to list refs from {} to {}",
59        base_ref,
60        new_ref
61    )]
62    RevList {
63        /// The base ref for the topic.
64        base_ref: CommitId,
65        /// The head of the topic.
66        new_ref: CommitId,
67        /// Git's output for the error.
68        output: String,
69    },
70}
71
72impl RunError {
73    fn update_ref(base_ref: CommitId, output: &[u8]) -> Self {
74        RunError::UpdateRef {
75            base_ref,
76            output: String::from_utf8_lossy(output).into(),
77        }
78    }
79
80    fn rev_list(base_ref: CommitId, new_ref: CommitId, output: &[u8]) -> Self {
81        RunError::RevList {
82            base_ref,
83            new_ref,
84            output: String::from_utf8_lossy(output).into(),
85        }
86    }
87}
88
89/// Configuration for checks to run against a repository.
90#[derive(Default, Clone)]
91pub struct GitCheckConfiguration<'a> {
92    /// Checks to run on each commit.
93    checks: Vec<&'a dyn Check>,
94    /// Checks to run on the branch as a whole.
95    checks_branch: Vec<&'a dyn BranchCheck>,
96    /// Checks to run on the topic.
97    checks_topic: Vec<&'a dyn TopicCheck>,
98    /// Runtime configuration.
99    configuration: BTreeMap<String, String>,
100}
101
102/// Results from checking a topic.
103#[derive(Debug)]
104pub struct TopicCheckResult {
105    /// The results for each commit in the topic.
106    commit_results: Vec<(CommitId, CheckResult)>,
107    /// The results for the branch as a whole.
108    topic_result: CheckResult,
109}
110
111impl TopicCheckResult {
112    /// The results for each commit checked.
113    pub fn commit_results(&self) -> slice::Iter<'_, (CommitId, CheckResult)> {
114        self.commit_results.iter()
115    }
116
117    /// The results for the topic as a whole.
118    pub fn topic_result(&self) -> &CheckResult {
119        &self.topic_result
120    }
121}
122
123impl From<TopicCheckResult> for CheckResult {
124    fn from(res: TopicCheckResult) -> Self {
125        res.commit_results
126            .into_iter()
127            .map(|(_, result)| result)
128            .chain(iter::once(res.topic_result))
129            .fold(Self::new(), Self::combine)
130    }
131}
132
133impl<'a> GitCheckConfiguration<'a> {
134    /// Create a new check configuration.
135    pub fn new() -> Self {
136        GitCheckConfiguration {
137            checks: Vec::new(),
138            checks_branch: Vec::new(),
139            checks_topic: Vec::new(),
140            configuration: BTreeMap::new(),
141        }
142    }
143
144    /// Add a check to be run on every commit.
145    pub fn add_check(&mut self, check: &'a dyn Check) -> &mut Self {
146        self.checks.push(check);
147
148        self
149    }
150
151    /// Add a check to be once for the entire branch.
152    pub fn add_branch_check(&mut self, check: &'a dyn BranchCheck) -> &mut Self {
153        self.checks_branch.push(check);
154
155        self
156    }
157
158    /// Add a check to be once for the entire topic.
159    pub fn add_topic_check(&mut self, check: &'a dyn TopicCheck) -> &mut Self {
160        self.checks_topic.push(check);
161
162        self
163    }
164
165    /// Add a configuration value for runtime behavior modifications of checks.
166    ///
167    /// Checks may look up configuration values for runtime behavior changes. Any existing
168    /// configuration value is overwritten.
169    pub fn add_configuration<K, V>(&mut self, key: K, value: V) -> &mut Self
170    where
171        K: Into<String>,
172        V: Into<String>,
173    {
174        self.configuration.insert(key.into(), value.into());
175
176        self
177    }
178
179    /// Add a configuration value for runtime behavior modifications of checks.
180    ///
181    /// Checks may look up configuration values for runtime behavior changes. Any existing
182    /// configuration values are overwritten.
183    pub fn add_configurations<I, K, V>(&mut self, iter: I) -> &mut Self
184    where
185        I: IntoIterator<Item = (K, V)>,
186        K: Into<String>,
187        V: Into<String>,
188    {
189        self.configuration
190            .extend(iter.into_iter().map(|(k, v)| (k.into(), v.into())));
191
192        self
193    }
194
195    /// Find refs that should be checked given a target branch and the topic names.
196    fn list<'b, B>(
197        &self,
198        ctx: &GitContext,
199        reason: &str,
200        target_branch: &CommitId,
201        bases: B,
202        topic: &CommitId,
203    ) -> Result<Vec<CommitId>, RunError>
204    where
205        B: IntoIterator<Item = &'b CommitId>,
206    {
207        let (new_ref, base_ref) = ctx.reserve_refs(format!("check/{}", reason), topic)?;
208        let update_ref = ctx
209            .git()
210            .arg("update-ref")
211            .args(["-m", reason])
212            .arg(&base_ref)
213            .arg(target_branch.as_str())
214            .output()
215            .map_err(|err| GitError::subcommand("update-ref", err))?;
216        if !update_ref.status.success() {
217            return Err(RunError::update_ref(
218                CommitId::new(base_ref),
219                &update_ref.stderr,
220            ));
221        }
222
223        let rev_list = ctx
224            .git()
225            .arg("rev-list")
226            .arg("--reverse")
227            .arg("--topo-order")
228            .arg(&new_ref)
229            .arg(format!("^{}", base_ref))
230            .args(bases.into_iter().map(|base| format!("^{}", base)))
231            .output()
232            .map_err(|err| GitError::subcommand("rev-list", err))?;
233        if !rev_list.status.success() {
234            return Err(RunError::rev_list(
235                CommitId::new(base_ref),
236                CommitId::new(new_ref),
237                &rev_list.stderr,
238            ));
239        }
240        let refs = String::from_utf8_lossy(&rev_list.stdout);
241
242        Ok(refs.lines().map(CommitId::new).collect())
243    }
244
245    /// Run a commit check over a commit.
246    fn run_check(ctx: &CheckGitContext, check: &dyn Check, commit: &Commit) -> CheckResult {
247        debug!(
248            target: "git-checks",
249            "running check {} on commit {}",
250            check.name(),
251            commit.sha1,
252        );
253
254        check.check(ctx, commit).unwrap_or_else(|err| {
255            error!(
256                target: "git-checks",
257                "check {} failed on commit {}: {:?}",
258                check.name(),
259                commit.sha1,
260                err,
261            );
262
263            let mut res = CheckResult::new();
264            res.add_alert(
265                format!(
266                    "failed to run the {} check on commit {}",
267                    check.name(),
268                    commit.sha1,
269                ),
270                true,
271            );
272            res
273        })
274    }
275
276    /// Run a branch check over a commit.
277    fn run_branch_check(
278        ctx: &CheckGitContext,
279        check: &dyn BranchCheck,
280        commit: &CommitId,
281    ) -> CheckResult {
282        debug!(target: "git-checks", "running check {}", check.name());
283
284        check.check(ctx, commit).unwrap_or_else(|err| {
285            error!(
286                target: "git-checks",
287                "branch check {}: {:?}",
288                check.name(),
289                err,
290            );
291
292            let mut res = CheckResult::new();
293            res.add_alert(
294                format!("failed to run the {} branch check", check.name()),
295                true,
296            );
297            res
298        })
299    }
300
301    /// Run a topic check over a topic.
302    fn run_topic_check(
303        ctx: &CheckGitContext,
304        check: &dyn TopicCheck,
305        topic: &Topic,
306    ) -> CheckResult {
307        debug!(target: "git-checks", "running check {}", check.name());
308
309        check.check(ctx, topic).unwrap_or_else(|err| {
310            error!(
311                target: "git-checks",
312                "topic check {}: {:?}",
313                check.name(),
314                err,
315            );
316
317            let mut res = CheckResult::new();
318            res.add_alert(
319                format!("failed to run the {} topic check", check.name()),
320                true,
321            );
322            res
323        })
324    }
325
326    /// Run checks over a given topic and collect results from the checks.
327    fn run_topic_impl(
328        &self,
329        ctx: &GitContext,
330        base: &CommitId,
331        refs: Vec<CommitId>,
332        owner: &Identity,
333    ) -> Result<TopicCheckResult, RunError> {
334        let topic_result = refs.last().map_or_else(
335            || Ok(CheckResult::new()) as Result<_, RunError>,
336            |head_commit| {
337                // Avoid setting up the workarea if there aren't any branch or topic checks.
338                if self.checks_branch.is_empty() && self.checks_topic.is_empty() {
339                    return Ok(CheckResult::new());
340                }
341
342                let workarea = ctx.prepare(head_commit)?;
343                let check_ctx = {
344                    let mut ctx = CheckGitContext::new(workarea, owner.clone());
345                    ctx.add_configurations(self.configuration.iter());
346                    ctx
347                };
348                let topic = Topic::new(ctx, base, head_commit)?;
349
350                Ok(self
351                    .checks_branch
352                    .par_iter()
353                    .map(|&check| Self::run_branch_check(&check_ctx, check, head_commit))
354                    .chain(
355                        self.checks_topic
356                            .par_iter()
357                            .map(|&check| Self::run_topic_check(&check_ctx, check, &topic)),
358                    )
359                    .reduce(CheckResult::new, CheckResult::combine))
360            },
361        )?;
362        let commit_results = refs
363            .into_par_iter()
364            .map(|sha1| {
365                self.run_commit(ctx, &sha1, owner)
366                    .map(|result| (sha1, result))
367            })
368            .collect::<Vec<Result<_, RunError>>>()
369            .into_iter()
370            .collect::<Result<Vec<_>, RunError>>()?;
371
372        Ok(TopicCheckResult {
373            commit_results,
374            topic_result,
375        })
376    }
377
378    /// Run checks over a given commit.
379    pub fn run_commit(
380        &self,
381        ctx: &GitContext,
382        commit: &CommitId,
383        owner: &Identity,
384    ) -> Result<CheckResult, RunError> {
385        // Avoid setting up the workarea if there aren't any per-commit checks.
386        if self.checks.is_empty() {
387            return Ok(CheckResult::new());
388        }
389
390        let workarea = ctx.prepare(commit)?;
391        let check_ctx = {
392            let mut ctx = CheckGitContext::new(workarea, owner.clone());
393            ctx.add_configurations(self.configuration.iter());
394            ctx
395        };
396
397        let commit = Commit::new(ctx, commit)?;
398
399        Ok(self
400            .checks
401            .par_iter()
402            .map(|&check| Self::run_check(&check_ctx, check, &commit))
403            .reduce(CheckResult::new, CheckResult::combine))
404    }
405
406    /// Run checks over a given topic and collect results from the checks.
407    pub fn run_topic<R>(
408        &self,
409        ctx: &GitContext,
410        reason: R,
411        target_branch: &CommitId,
412        topic: &CommitId,
413        owner: &Identity,
414    ) -> Result<TopicCheckResult, RunError>
415    where
416        R: AsRef<str>,
417    {
418        let refs = self.list(ctx, reason.as_ref(), target_branch, &[], topic)?;
419        self.run_topic_impl(ctx, target_branch, refs, owner)
420    }
421
422    /// Run checks over a given topic involving multiple base commits.
423    pub fn run_topic_multi_base<'b, R, B>(
424        &self,
425        ctx: &GitContext,
426        reason: R,
427        target_branch: &CommitId,
428        bases: B,
429        topic: &CommitId,
430        owner: &Identity,
431    ) -> Result<TopicCheckResult, RunError>
432    where
433        R: AsRef<str>,
434        B: IntoIterator<Item = &'b CommitId>,
435    {
436        let refs = self.list(ctx, reason.as_ref(), target_branch, bases, topic)?;
437        self.run_topic_impl(ctx, target_branch, refs, owner)
438    }
439}
440
441impl Debug for GitCheckConfiguration<'_> {
442    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
443        write!(
444            f,
445            "GitCheckConfiguration {{ {} commit checks, {} branch checks, {} topic checks }}",
446            self.checks.len(),
447            self.checks_branch.len(),
448            self.checks_topic.len(),
449        )
450    }
451}
452
453#[cfg(test)]
454mod test {
455    use std::iter;
456    use std::path::Path;
457
458    use git_workarea::{CommitId, GitContext, Identity};
459
460    use crate::run::{CheckResult, GitCheckConfiguration, TopicCheckResult};
461
462    mod checks {
463        use thiserror::Error;
464
465        use crate::impl_prelude::*;
466
467        #[derive(Debug)]
468        pub struct FailingCheck {}
469
470        #[derive(Debug, Error)]
471        #[error("the failing check did its thing")]
472        struct FailingCheckError;
473
474        impl Check for FailingCheck {
475            fn name(&self) -> &str {
476                "test-failing-check-commit"
477            }
478
479            fn check(
480                &self,
481                _: &CheckGitContext,
482                _: &Commit,
483            ) -> Result<CheckResult, Box<dyn Error>> {
484                Err(FailingCheckError.into())
485            }
486        }
487
488        impl BranchCheck for FailingCheck {
489            fn name(&self) -> &str {
490                "test-failing-check-branch"
491            }
492
493            fn check(
494                &self,
495                _: &CheckGitContext,
496                _: &CommitId,
497            ) -> Result<CheckResult, Box<dyn Error>> {
498                Err(FailingCheckError.into())
499            }
500        }
501
502        impl TopicCheck for FailingCheck {
503            fn name(&self) -> &str {
504                "test-failing-check-topic"
505            }
506
507            fn check(&self, _: &CheckGitContext, _: &Topic) -> Result<CheckResult, Box<dyn Error>> {
508                Err(FailingCheckError.into())
509            }
510        }
511    }
512
513    #[test]
514    fn test_configuration_debug() {
515        let config = GitCheckConfiguration::new();
516        assert_eq!(
517            format!("{:?}", config),
518            "GitCheckConfiguration { 0 commit checks, 0 branch checks, 0 topic checks }",
519        );
520    }
521
522    const TARGET_COMMIT: &str = "27ff3ef5532d76afa046f76f4dd8f588dc3e83c3";
523    const TOPIC_COMMIT: &str = "a61fd3759b61a4a1f740f3fe656bc42151cefbdd";
524    const TOPIC2_COMMIT: &str = "112e9b34401724bff57f68cf47c5065d4342b263";
525
526    fn git_context() -> GitContext {
527        let gitdir = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../.git"));
528        if !gitdir.exists() {
529            panic!("The tests must be run from a git checkout.");
530        }
531
532        GitContext::new(gitdir)
533    }
534
535    fn run_commit(config: &GitCheckConfiguration) -> CheckResult {
536        let ctx = git_context();
537        config
538            .run_commit(
539                &ctx,
540                &CommitId::new(TOPIC_COMMIT),
541                &Identity::new(
542                    "Rust Git Checks Core Tests",
543                    "rust-git-checks@example.invalid",
544                ),
545            )
546            .unwrap()
547    }
548
549    fn run_topic(
550        test_name: &str,
551        config: &GitCheckConfiguration,
552        commit: &str,
553    ) -> TopicCheckResult {
554        let ctx = git_context();
555        config
556            .run_topic(
557                &ctx,
558                test_name,
559                &CommitId::new(TARGET_COMMIT),
560                &CommitId::new(commit),
561                &Identity::new(
562                    "Rust Git Checks Core Tests",
563                    "rust-git-checks@example.invalid",
564                ),
565            )
566            .unwrap()
567    }
568
569    fn run_topic_bases(
570        test_name: &str,
571        config: &GitCheckConfiguration,
572        commit: &str,
573        bases: &[&str],
574    ) -> TopicCheckResult {
575        let ctx = git_context();
576        let bases: Vec<_> = bases.iter().copied().map(CommitId::new).collect();
577        config
578            .run_topic_multi_base(
579                &ctx,
580                test_name,
581                &CommitId::new(TARGET_COMMIT),
582                bases.iter(),
583                &CommitId::new(commit),
584                &Identity::new(
585                    "Rust Git Checks Core Tests",
586                    "rust-git-checks@example.invalid",
587                ),
588            )
589            .unwrap()
590    }
591
592    fn no_strings<'a>() -> iter::Empty<&'a String> {
593        iter::empty()
594    }
595
596    fn check_result(result: &CheckResult, errors: &[&str]) {
597        itertools::assert_equal(result.warnings(), no_strings());
598        itertools::assert_equal(result.alerts(), errors);
599        itertools::assert_equal(result.errors(), no_strings());
600        assert!(!result.temporary());
601        assert!(!result.allowed());
602        assert!(!result.pass());
603    }
604
605    fn check_result_ok(result: &CheckResult) {
606        itertools::assert_equal(result.warnings(), no_strings());
607        itertools::assert_equal(result.alerts(), no_strings());
608        itertools::assert_equal(result.errors(), no_strings());
609        assert!(!result.temporary());
610        assert!(!result.allowed());
611        assert!(result.pass());
612    }
613
614    #[test]
615    fn test_commit_check_errors_commit() {
616        let check = self::checks::FailingCheck {};
617        let mut config = GitCheckConfiguration::new();
618        config.add_check(&check);
619
620        let result = run_commit(&config);
621
622        check_result(
623            &result,
624            &[
625                "failed to run the test-failing-check-commit check on commit \
626                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
627            ],
628        );
629    }
630
631    #[test]
632    fn test_branch_check_errors_commit() {
633        let check = self::checks::FailingCheck {};
634        let mut config = GitCheckConfiguration::new();
635        config.add_branch_check(&check);
636
637        let result = run_commit(&config);
638
639        check_result_ok(&result);
640    }
641
642    #[test]
643    fn test_topic_check_errors_commit() {
644        let check = self::checks::FailingCheck {};
645        let mut config = GitCheckConfiguration::new();
646        config.add_topic_check(&check);
647
648        let result = run_commit(&config);
649
650        check_result_ok(&result);
651    }
652
653    #[test]
654    fn test_commit_check_errors_topic() {
655        let check = self::checks::FailingCheck {};
656        let mut config = GitCheckConfiguration::new();
657        config.add_check(&check);
658
659        let result = run_topic("test_commit_check_errors_topic", &config, TOPIC_COMMIT);
660
661        let mut commit_results = result.commit_results();
662        let (commit, commit_result) = commit_results.next().unwrap();
663        assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
664        check_result(
665            commit_result,
666            &[
667                "failed to run the test-failing-check-commit check on commit \
668                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
669            ],
670        );
671        if let Some(res) = commit_results.next() {
672            panic!(
673                "multiple commits returned from a single-commit topic: {:?}",
674                res,
675            );
676        }
677        check_result_ok(result.topic_result());
678        check_result(
679            &result.into(),
680            &[
681                "failed to run the test-failing-check-commit check on commit \
682                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
683            ],
684        );
685    }
686
687    #[test]
688    fn test_commit_check_errors_topic_bases() {
689        let check = self::checks::FailingCheck {};
690        let mut config = GitCheckConfiguration::new();
691        config.add_check(&check);
692
693        let result = run_topic_bases(
694            "test_commit_check_errors_topic_bases",
695            &config,
696            TOPIC2_COMMIT,
697            &[TOPIC_COMMIT],
698        );
699
700        let mut commit_results = result.commit_results();
701        let (commit, commit_result) = commit_results.next().unwrap();
702        assert_eq!(commit.as_str(), "112e9b34401724bff57f68cf47c5065d4342b263");
703        check_result(
704            commit_result,
705            &[
706                "failed to run the test-failing-check-commit check on commit \
707                 112e9b34401724bff57f68cf47c5065d4342b263",
708            ],
709        );
710        if let Some(res) = commit_results.next() {
711            panic!(
712                "multiple commits returned from a single-commit topic: {:?}",
713                res,
714            );
715        }
716        check_result_ok(result.topic_result());
717        check_result(
718            &result.into(),
719            &[
720                "failed to run the test-failing-check-commit check on commit \
721                 112e9b34401724bff57f68cf47c5065d4342b263",
722            ],
723        );
724    }
725
726    #[test]
727    fn test_branch_check_errors_topic() {
728        let check = self::checks::FailingCheck {};
729        let mut config = GitCheckConfiguration::new();
730        config.add_branch_check(&check);
731
732        let result = run_topic("test_branch_check_errors_topic", &config, TOPIC_COMMIT);
733
734        let mut commit_results = result.commit_results();
735        let (commit, commit_result) = commit_results.next().unwrap();
736        assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
737        check_result_ok(commit_result);
738        if let Some(res) = commit_results.next() {
739            panic!(
740                "multiple commits returned from a single-commit topic: {:?}",
741                res,
742            );
743        }
744        check_result(
745            result.topic_result(),
746            &["failed to run the test-failing-check-branch branch check"],
747        );
748        check_result(
749            &result.into(),
750            &["failed to run the test-failing-check-branch branch check"],
751        );
752    }
753
754    #[test]
755    fn test_branch_check_errors_topic_bases() {
756        let check = self::checks::FailingCheck {};
757        let mut config = GitCheckConfiguration::new();
758        config.add_branch_check(&check);
759
760        let result = run_topic_bases(
761            "test_branch_check_errors_topic_bases",
762            &config,
763            TOPIC2_COMMIT,
764            &[TOPIC_COMMIT],
765        );
766
767        let mut commit_results = result.commit_results();
768        let (commit, commit_result) = commit_results.next().unwrap();
769        assert_eq!(commit.as_str(), "112e9b34401724bff57f68cf47c5065d4342b263");
770        check_result_ok(commit_result);
771        if let Some(res) = commit_results.next() {
772            panic!(
773                "multiple commits returned from a single-commit topic: {:?}",
774                res,
775            );
776        }
777        check_result(
778            result.topic_result(),
779            &["failed to run the test-failing-check-branch branch check"],
780        );
781        check_result(
782            &result.into(),
783            &["failed to run the test-failing-check-branch branch check"],
784        );
785    }
786
787    #[test]
788    fn test_topic_check_errors_topic() {
789        let check = self::checks::FailingCheck {};
790        let mut config = GitCheckConfiguration::new();
791        config.add_topic_check(&check);
792
793        let result = run_topic("test_topic_check_errors_topic", &config, TOPIC_COMMIT);
794
795        let mut commit_results = result.commit_results();
796        let (commit, commit_result) = commit_results.next().unwrap();
797        assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
798        check_result_ok(commit_result);
799        if let Some(res) = commit_results.next() {
800            panic!(
801                "multiple commits returned from a single-commit topic: {:?}",
802                res,
803            );
804        }
805        check_result(
806            result.topic_result(),
807            &["failed to run the test-failing-check-topic topic check"],
808        );
809        check_result(
810            &result.into(),
811            &["failed to run the test-failing-check-topic topic check"],
812        );
813    }
814
815    #[test]
816    fn test_topic_check_errors_topic_bases() {
817        let check = self::checks::FailingCheck {};
818        let mut config = GitCheckConfiguration::new();
819        config.add_topic_check(&check);
820
821        let result = run_topic_bases(
822            "test_topic_check_errors_topic_bases",
823            &config,
824            TOPIC2_COMMIT,
825            &[TOPIC_COMMIT],
826        );
827
828        let mut commit_results = result.commit_results();
829        let (commit, commit_result) = commit_results.next().unwrap();
830        assert_eq!(commit.as_str(), "112e9b34401724bff57f68cf47c5065d4342b263");
831        check_result_ok(commit_result);
832        if let Some(res) = commit_results.next() {
833            panic!(
834                "multiple commits returned from a single-commit topic: {:?}",
835                res,
836            );
837        }
838        check_result(
839            result.topic_result(),
840            &["failed to run the test-failing-check-topic topic check"],
841        );
842        check_result(
843            &result.into(),
844            &["failed to run the test-failing-check-topic topic check"],
845        );
846    }
847
848    #[test]
849    fn test_multiple_check_commit() {
850        let check = self::checks::FailingCheck {};
851        let mut config = GitCheckConfiguration::new();
852        config.add_check(&check);
853        config.add_check(&check);
854        config.add_branch_check(&check);
855        config.add_branch_check(&check);
856        config.add_topic_check(&check);
857        config.add_topic_check(&check);
858
859        let result = run_commit(&config);
860
861        check_result(
862            &result,
863            &[
864                "failed to run the test-failing-check-commit check on commit \
865                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
866                "failed to run the test-failing-check-commit check on commit \
867                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
868            ],
869        );
870    }
871
872    #[test]
873    fn test_multiple_check_topic() {
874        let check = self::checks::FailingCheck {};
875        let mut config = GitCheckConfiguration::new();
876        config.add_check(&check);
877        config.add_check(&check);
878        config.add_branch_check(&check);
879        config.add_branch_check(&check);
880        config.add_topic_check(&check);
881        config.add_topic_check(&check);
882
883        let result = run_topic("test_multiple_check_topic", &config, TOPIC_COMMIT);
884
885        let mut commit_results = result.commit_results();
886        let (commit, commit_result) = commit_results.next().unwrap();
887        assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
888        check_result(
889            commit_result,
890            &[
891                "failed to run the test-failing-check-commit check on commit \
892                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
893                "failed to run the test-failing-check-commit check on commit \
894                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
895            ],
896        );
897        if let Some(res) = commit_results.next() {
898            panic!(
899                "multiple commits returned from a single-commit topic: {:?}",
900                res,
901            );
902        }
903        check_result(
904            result.topic_result(),
905            &[
906                "failed to run the test-failing-check-branch branch check",
907                "failed to run the test-failing-check-branch branch check",
908                "failed to run the test-failing-check-topic topic check",
909                "failed to run the test-failing-check-topic topic check",
910            ],
911        );
912        check_result(
913            &result.into(),
914            &[
915                "failed to run the test-failing-check-commit check on commit \
916                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
917                "failed to run the test-failing-check-commit check on commit \
918                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
919                "failed to run the test-failing-check-branch branch check",
920                "failed to run the test-failing-check-branch branch check",
921                "failed to run the test-failing-check-topic topic check",
922                "failed to run the test-failing-check-topic topic check",
923            ],
924        );
925    }
926
927    #[test]
928    fn test_check_multiple_topic() {
929        let check = self::checks::FailingCheck {};
930        let mut config = GitCheckConfiguration::new();
931        config.add_check(&check);
932        config.add_branch_check(&check);
933        config.add_topic_check(&check);
934
935        let result = run_topic("test_check_multiple_topic", &config, TOPIC2_COMMIT);
936
937        let mut commit_results = result.commit_results();
938        let (commit, commit_result) = commit_results.next().unwrap();
939        assert_eq!(commit.as_str(), "a61fd3759b61a4a1f740f3fe656bc42151cefbdd");
940        check_result(
941            commit_result,
942            &[
943                "failed to run the test-failing-check-commit check on commit \
944                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
945            ],
946        );
947        let (commit, commit_result) = commit_results.next().unwrap();
948        assert_eq!(commit.as_str(), "112e9b34401724bff57f68cf47c5065d4342b263");
949        check_result(
950            commit_result,
951            &[
952                "failed to run the test-failing-check-commit check on commit \
953                 112e9b34401724bff57f68cf47c5065d4342b263",
954            ],
955        );
956        if let Some(res) = commit_results.next() {
957            panic!(
958                "multiple commits returned from a single-commit topic: {:?}",
959                res,
960            );
961        }
962        check_result(
963            result.topic_result(),
964            &[
965                "failed to run the test-failing-check-branch branch check",
966                "failed to run the test-failing-check-topic topic check",
967            ],
968        );
969        check_result(
970            &result.into(),
971            &[
972                "failed to run the test-failing-check-commit check on commit \
973                 a61fd3759b61a4a1f740f3fe656bc42151cefbdd",
974                "failed to run the test-failing-check-commit check on commit \
975                 112e9b34401724bff57f68cf47c5065d4342b263",
976                "failed to run the test-failing-check-branch branch check",
977                "failed to run the test-failing-check-topic topic check",
978            ],
979        );
980    }
981
982    #[test]
983    fn test_check_configuration_overwrite() {
984        let config = {
985            let mut config = GitCheckConfiguration::new();
986            config
987                .add_configuration("key1", "value1")
988                .add_configuration("key2", "value2")
989                .add_configuration("key3", "value3")
990                .add_configuration("key1", "value1_overwrite")
991                .add_configurations([("key2", "value2_overwrite"), ("key4", "value4")]);
992            config
993        };
994
995        let expect = [
996            ("key1", Some("value1_overwrite")),
997            ("key2", Some("value2_overwrite")),
998            ("key3", Some("value3")),
999            ("key4", Some("value4")),
1000            ("key5", None),
1001        ];
1002
1003        for (k, v) in expect {
1004            assert_eq!(config.configuration.get(k).map(|v| v.as_str()), v);
1005        }
1006        assert_eq!(config.configuration.len(), 4);
1007    }
1008}