cargo_culture_kit/
lib.rs

1//! cargo-culture-kit provides machinery for checking
2//! project-level rules about Rust best practices.
3//!
4//! The primary function entry points are `check_culture` and
5//! `check_culture_default`.
6//!
7//! The core trait is `Rule`, which represents a single project-level property
8//! that has a clear description and can be checked.
9//!
10//! # Examples
11//!
12//! `check_culture_default` is the easiest way to get started,
13//! as it provides a thin wrapper around the core `check_culture`
14//! function in combination with the `Rule`s provided by the
15//! `default_rules()` function.
16//!
17//! ```no_run
18//! use cargo_culture_kit::{check_culture_default, IsSuccess, OutcomeStats};
19//! use std::path::PathBuf;
20//!
21//! let cargo_manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
22//!         .join("Cargo.toml");
23//!
24//! let verbose = false;
25//!
26//! let outcomes = check_culture_default(
27//!     cargo_manifest, verbose, &mut std::io::stdout()
28//!     )
29//!     .expect("Unexpected trouble checking culture rules:");
30//!
31//! let stats = OutcomeStats::from(outcomes);
32//! assert!(stats.is_success());
33//! assert_eq!(stats.fail_count, 0);
34//! assert_eq!(stats.undetermined_count, 0);
35//! ```
36//!
37//! If you want to use a specific `Rule` or group of `Rule`s,
38//! the `check_culture` function is the right place to look.
39//!
40//! ```
41//! use cargo_culture_kit::{check_culture, IsSuccess, OutcomeStats,
42//! HasLicenseFile}; use std::path::PathBuf;
43//!
44//! let rule = HasLicenseFile::default();
45//! let cargo_manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
46//!         .join("Cargo.toml");
47//!
48//! let verbose = false;
49//!
50//! let outcomes = check_culture(
51//!     cargo_manifest, verbose, &mut std::io::stdout(), &[&rule]
52//!     )
53//!     .expect("Unexpected trouble checking culture rules: ");
54//!
55//! let stats = OutcomeStats::from(outcomes);
56//! assert!(stats.is_success());
57//! assert_eq!(stats.success_count, 1);
58//! assert_eq!(stats.fail_count, 0);
59//! assert_eq!(stats.undetermined_count, 0);
60//! ```
61//!
62
63#![deny(missing_docs)]
64
65#[cfg(test)]
66extern crate tempfile;
67
68#[macro_use]
69extern crate failure;
70
71#[cfg(test)]
72#[macro_use]
73extern crate proptest;
74
75#[macro_use]
76extern crate lazy_static;
77
78extern crate cargo_metadata;
79extern crate colored;
80
81extern crate regex;
82
83mod file;
84
85pub mod checklist;
86pub mod exit_code;
87pub mod rules;
88
89pub use checklist::{
90    filter_to_requested_rules_by_description, filter_to_requested_rules_from_checklist_file,
91    find_extant_culture_file, FilterError, DEFAULT_CULTURE_CHECKLIST_FILE_NAME,
92};
93pub use exit_code::ExitCode;
94pub use rules::{
95    default_rules, BuildsCleanlyWithoutWarningsOrErrors, CargoMetadataReadable,
96    HasContinuousIntegrationFile, HasContributingFile, HasLicenseFile, HasReadmeFile,
97    HasRustfmtFile, PassesMultipleTests, Rule, RuleContext, RuleOutcome,
98    UsesPropertyBasedTestLibrary,
99};
100
101pub use cargo_metadata::Metadata as CargoMetadata;
102use colored::*;
103use std::borrow::Borrow;
104use std::collections::HashMap;
105use std::io::Write;
106use std::path::{Path, PathBuf};
107
108/// Top-level error variants for what can go wrong with checking culture rules.
109///
110/// Note that individual rule outcomes for better or worse should *not* be
111/// interpreted as erroneous.
112#[derive(Debug, Clone, Eq, Fail, PartialEq, Hash)]
113pub enum CheckError {
114    #[fail(
115        display = "There was an error while attempting to print {} to the output writer.", topic
116    )]
117    /// Failure during writing human-oriented textual content to an output
118    /// `Write` instance.
119    PrintOutputFailure {
120        /// The sort of content that was failed to be written
121        topic: &'static str,
122    },
123    /// Destructuring should not be exhaustive.
124    ///
125    /// This enum may grow additional variants, so this hidden variant
126    /// ensures users do not rely on exhaustive matching.
127    #[doc(hidden)]
128    #[fail(display = "A hidden variant to increase expansion flexibility")]
129    __Nonexhaustive,
130}
131
132/// Execute a `check_culture` run using the set of rules available from
133/// `default_rules`.
134///
135/// See `check_culture` for more details.
136///
137/// # Examples
138///
139/// ```no_run
140/// use cargo_culture_kit::{check_culture_default, IsSuccess, OutcomeStats};
141/// use std::path::PathBuf;
142///
143/// let cargo_manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
144///         .join("Cargo.toml");
145///
146/// let verbose = false;
147///
148/// let outcomes = check_culture_default(
149///                 cargo_manifest, verbose, &mut std::io::stdout())
150///     .expect("Unexpected trouble checking culture rules:");
151///
152/// for (description, outcome) in &outcomes {
153///     println!(
154///              "For this project: {} had an outcome of {:?}",
155///              description, outcome);
156/// }
157///
158/// let stats = OutcomeStats::from(outcomes);
159/// assert!(stats.is_success());
160/// assert_eq!(stats.fail_count, 0);
161/// assert_eq!(stats.undetermined_count, 0);
162/// ```
163///
164/// # Errors
165///
166/// Returns an error if the program cannot write to the supplied `print_output`
167/// instance.
168pub fn check_culture_default<P: AsRef<Path>, W: Write>(
169    cargo_manifest_file_path: P,
170    verbose: bool,
171    print_output: &mut W,
172) -> Result<OutcomesByDescription, CheckError> {
173    let rules = default_rules();
174    let rule_refs = rules.iter().map(|r| r.as_ref()).collect::<Vec<&Rule>>();
175    check_culture(cargo_manifest_file_path, verbose, print_output, &rule_refs)
176}
177
178/// Given a set of `Rule`s, evaluate the rules
179/// and produce a summary report of the rule outcomes.
180///
181/// Primary entry point for this library.
182///
183/// `cargo_manifest_file_path` should point to a project's extant `Cargo.toml`
184/// file. Either a crate-level or a workspace-level toml file should work.
185///
186/// `verbose` controls whether or not to produce additional human-readable
187/// reporting.
188///
189/// `print_output` is the `Write` instance where `Rule` evaluation summaries
190/// are printed, as well as the location where `verbose` content may be dumped.
191/// `&mut std::io::stdout()` is a common instance used by non-test applications.
192///
193/// `rules` is the complete set of `Rule` instances which will be evaluated for
194/// the project specified by `cargo_manifest_file_path`.
195///
196/// # Examples
197///
198/// ```
199/// use cargo_culture_kit::{check_culture, IsSuccess, OutcomeStats,
200/// HasLicenseFile}; use std::path::PathBuf;
201///
202/// let rule = HasLicenseFile::default();
203/// let cargo_manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
204///         .join("Cargo.toml");
205///
206/// let verbose = false;
207///
208/// let outcomes = check_culture(cargo_manifest, verbose, &mut
209/// std::io::stdout(),     &[&rule])
210///     .expect("Unexpected trouble checking culture rules: ");
211///
212/// let stats = OutcomeStats::from(outcomes);
213/// assert!(stats.is_success());
214/// assert_eq!(stats.success_count, 1);
215/// assert_eq!(stats.fail_count, 0);
216/// assert_eq!(stats.undetermined_count, 0);
217/// ```
218///
219/// # Errors
220///
221/// Returns an error if the program cannot write to the supplied `print_output`
222/// instance.
223pub fn check_culture<P: AsRef<Path>, W: Write>(
224    cargo_manifest_file_path: P,
225    verbose: bool,
226    print_output: &mut W,
227    rules: &[&Rule],
228) -> Result<OutcomesByDescription, CheckError> {
229    let metadata_option =
230        read_cargo_metadata(cargo_manifest_file_path.as_ref(), verbose, print_output)?;
231    let outcomes = evaluate_rules(
232        cargo_manifest_file_path.as_ref(),
233        verbose,
234        &metadata_option,
235        print_output,
236        rules,
237    )?;
238    print_outcome_stats(&outcomes, print_output)?;
239    Ok(outcomes)
240}
241
242fn read_cargo_metadata<P: AsRef<Path>, W: Write>(
243    cargo_manifest_file_path: P,
244    verbose: bool,
245    print_output: &mut W,
246) -> Result<Option<CargoMetadata>, CheckError> {
247    let manifest_path: PathBuf = cargo_manifest_file_path.as_ref().to_path_buf();
248    let metadata_result = cargo_metadata::metadata(Some(manifest_path.as_ref()));
249    match metadata_result {
250        Ok(m) => Ok(Some(m)),
251        Err(e) => {
252            if verbose && writeln!(print_output, "cargo metadata problem: {}", e).is_err() {
253                return Err(CheckError::PrintOutputFailure {
254                    topic: "cargo metadata",
255                });
256            }
257            Ok(None)
258        }
259    }
260}
261
262fn evaluate_rules<P: AsRef<Path>, W: Write, M: Borrow<Option<CargoMetadata>>>(
263    cargo_manifest_file_path: P,
264    verbose: bool,
265    metadata: M,
266    print_output: &mut W,
267    rules: &[&Rule],
268) -> Result<OutcomesByDescription, CheckError> {
269    let mut outcomes = OutcomesByDescription::new();
270    for rule in rules {
271        let outcome = print_rule_evaluation(
272            *rule,
273            cargo_manifest_file_path.as_ref(),
274            verbose,
275            metadata.borrow(),
276            print_output,
277        );
278        outcomes.insert(rule.description().to_owned(), outcome?);
279    }
280    Ok(outcomes)
281}
282
283fn print_outcome_stats<W: Write>(
284    outcomes: &OutcomesByDescription,
285    mut print_output: W,
286) -> Result<(), CheckError> {
287    let outcome_stats: OutcomeStats = outcomes.into();
288    let conclusion = if outcome_stats.is_success() {
289        "ok".green()
290    } else {
291        "FAILED".red()
292    };
293    if writeln!(
294        print_output,
295        "culture result: {}. {} passed. {} failed. {} undetermined.",
296        conclusion,
297        outcome_stats.success_count,
298        outcome_stats.fail_count,
299        outcome_stats.undetermined_count
300    ).is_err()
301    {
302        return Err(CheckError::PrintOutputFailure {
303            topic: "culture check summary",
304        });
305    };
306    Ok(())
307}
308
309/// Map between the `description` of `Rule`s and the outcome of their execution
310pub type OutcomesByDescription = HashMap<String, RuleOutcome>;
311
312/// Trait for summarizing whether the outcome of culture
313/// checking was a total success for any of
314/// the various levels of outcome aggregation
315pub trait IsSuccess {
316    /// Convenience function to answer the simple question "is everything all
317    /// right?" while providing no answer at all to the useful question
318    /// "why or why not?"
319    fn is_success(&self) -> bool;
320
321    /// Panic if `is_success()` returns false for this instance
322    fn assert_success(&self) {
323        assert!(self.is_success());
324    }
325}
326
327impl IsSuccess for RuleOutcome {
328    fn is_success(&self) -> bool {
329        if let RuleOutcome::Success = *self {
330            true
331        } else {
332            false
333        }
334    }
335}
336
337impl IsSuccess for OutcomesByDescription {
338    fn is_success(&self) -> bool {
339        OutcomeStats::from(self).is_success()
340    }
341
342    fn assert_success(&self) {
343        assert!(self.len() > 0, "OutcomesByDescription::len() should be > 0 to count as a success");
344        for (description, outcome) in self {
345            assert_eq!(&RuleOutcome::Success, outcome,
346                       "The rule \"{}\" was not a success, but instead was {:?}",
347                       description, outcome)
348        }
349    }
350}
351
352impl IsSuccess for OutcomeStats {
353    fn is_success(&self) -> bool {
354        RuleOutcome::from(self) == RuleOutcome::Success
355    }
356
357    fn assert_success(&self) {
358        assert_eq!(0, self.fail_count,
359                   "OutcomeStats::fail_count was {}, which counts as not a success",
360                   self.fail_count);
361        assert_eq!(0, self.undetermined_count,
362                   "OutcomeStats::undetermined_count was {}, which counts as not a success",
363                   self.undetermined_count);
364        assert!(self.success_count > 0,
365                   "OutcomeStats::success_count was {}, which counts as not a success",
366                   self.success_count);
367    }
368}
369
370impl<T> From<T> for OutcomeStats
371where
372    T: Borrow<OutcomesByDescription>,
373{
374    fn from(full_outcomes: T) -> OutcomeStats {
375        let mut stats = OutcomeStats::default();
376        for outcome in full_outcomes.borrow().values() {
377            match outcome {
378                RuleOutcome::Success => stats.success_count += 1,
379                RuleOutcome::Failure => stats.fail_count += 1,
380                RuleOutcome::Undetermined => stats.undetermined_count += 1,
381            }
382        }
383        stats
384    }
385}
386impl<T> From<T> for RuleOutcome
387where
388    T: Borrow<OutcomesByDescription>,
389{
390    fn from(full_outcomes: T) -> Self {
391        let stats: OutcomeStats = full_outcomes.into();
392        (&stats).into()
393    }
394}
395
396fn print_rule_evaluation<P: AsRef<Path>, W: Write, M: Borrow<Option<CargoMetadata>>>(
397    rule: &Rule,
398    cargo_manifest_file_path: P,
399    verbose: bool,
400    metadata: M,
401    print_output: &mut W,
402) -> Result<RuleOutcome, CheckError> {
403    if print_output
404        .write_all(rule.description().as_bytes())
405        .and_then(|_| print_output.flush())
406        .is_err()
407    {
408        return Err(CheckError::PrintOutputFailure {
409            topic: "rule description",
410        });
411    }
412    let outcome = rule.evaluate(RuleContext {
413        cargo_manifest_file_path: cargo_manifest_file_path.as_ref(),
414        verbose,
415        metadata: metadata.borrow(),
416        print_output,
417    });
418    if writeln!(print_output, " ... {}", summary_str(&outcome)).is_err() {
419        return Err(CheckError::PrintOutputFailure {
420            topic: "rule evaluation outcome",
421        });
422    }
423    Ok(outcome)
424}
425
426fn summary_str<T: Borrow<RuleOutcome>>(outcome: T) -> colored::ColoredString {
427    match *outcome.borrow() {
428        RuleOutcome::Success => "ok".green(),
429        RuleOutcome::Failure => "FAILED".red(),
430        RuleOutcome::Undetermined => "UNDETERMINED".red(),
431    }
432}
433
434/// Summary of result statistics generated from aggregating `RuleOutcome`s
435/// results for multiple Rule evaluations
436#[derive(Clone, Debug, Default, PartialEq)]
437pub struct OutcomeStats {
438    /// The number of `RuleOutcome::Success` instances observed
439    pub success_count: usize,
440    /// The number of `RuleOutcome::Failure` instances observed
441    pub fail_count: usize,
442    /// The number of `RuleOutcome::Undetermined` instances observed
443    pub undetermined_count: usize,
444}
445
446impl<'a> From<&'a OutcomeStats> for RuleOutcome {
447    fn from(stats: &'a OutcomeStats) -> Self {
448        match (
449            stats.success_count,
450            stats.fail_count,
451            stats.undetermined_count,
452        ) {
453            (0, 0, 0) => RuleOutcome::Undetermined,
454            (s, 0, 0) if s > 0 => RuleOutcome::Success,
455            (_, 0, _) => RuleOutcome::Undetermined,
456            (_, f, _) if f > 0 => RuleOutcome::Failure,
457            _ => unreachable!(),
458        }
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465    use proptest::collection::VecStrategy;
466    use proptest::prelude::*;
467
468    fn arb_rule_outcome() -> BoxedStrategy<RuleOutcome> {
469        prop_oneof![
470            Just(RuleOutcome::Success),
471            Just(RuleOutcome::Undetermined),
472            Just(RuleOutcome::Failure),
473        ].boxed()
474    }
475
476    prop_compose! {
477        fn arb_stats()(success in any::<usize>(),
478                       fail in any::<usize>(),
479                        undetermined in any::<usize>()) -> OutcomeStats {
480            OutcomeStats {
481                success_count: success,
482                fail_count: fail,
483                undetermined_count: undetermined
484            }
485        }
486    }
487
488    prop_compose! {
489        fn arb_predetermined_rule()(fixed_outcome in arb_rule_outcome(),
490                                    description in ".*") -> PredeterminedOutcomeRule {
491            PredeterminedOutcomeRule { outcome: fixed_outcome,
492            description: description.into_boxed_str() }
493        }
494    }
495
496    prop_compose! {
497        fn arb_rule()(rule in arb_predetermined_rule()) -> Box<Rule> {
498            let b: Box<Rule> = Box::new(rule);
499            b
500        }
501    }
502
503    fn arb_vec_of_rules() -> VecStrategy<BoxedStrategy<Box<Rule>>> {
504        prop::collection::vec(arb_rule(), 0..100)
505    }
506
507    #[derive(Clone, Debug, PartialEq)]
508    struct PredeterminedOutcomeRule {
509        outcome: RuleOutcome,
510        description: Box<str>,
511    }
512
513    impl Rule for PredeterminedOutcomeRule {
514        fn description(&self) -> &str {
515            self.description.as_ref()
516        }
517
518        fn evaluate(&self, _context: RuleContext) -> RuleOutcome {
519            self.outcome.clone()
520        }
521    }
522
523    proptest! {
524        #[test]
525        fn outcome_stats_to_rule_outcome_never_panics(ref stats in arb_stats()) {
526            let _rule_outcome:RuleOutcome = RuleOutcome::from(stats);
527        }
528
529        #[test]
530        fn piles_of_fixed_outcome_rules_evaluable(ref verbose in any::<bool>(),
531                                                  ref vec_of_rules in arb_vec_of_rules()) {
532            let mut v:Vec<u8> = Vec::new();
533            let _outcome:OutcomeStats = evaluate_rules(
534                                           Path::new("./Cargo.toml"), *verbose, &None,
535                                           &mut v,
536                                           vec_of_rules.iter()
537                                               .map(|r| r.as_ref())
538                                               .collect::<Vec<&Rule>>()
539                                               .as_slice()
540                                           ).expect("Expect no trouble with eval").into();
541        }
542    }
543
544    #[allow(dead_code)]
545    #[derive(Clone, Debug, Default, PartialEq)]
546    struct IsProjectAtALuckyTime;
547
548    #[allow(dead_code)]
549    impl Rule for IsProjectAtALuckyTime {
550        fn description(&self) -> &str {
551            "Should be lucky enough to only be tested at specific times."
552        }
553
554        fn evaluate(&self, _context: RuleContext) -> RuleOutcome {
555            use std::time::{SystemTime, UNIX_EPOCH};
556            let since_the_epoch = match SystemTime::now().duration_since(UNIX_EPOCH) {
557                Ok(t) => t,
558                Err(_) => return RuleOutcome::Undetermined,
559            };
560            if since_the_epoch.as_secs() % 2 == 0 {
561                RuleOutcome::Success
562            } else {
563                RuleOutcome::Failure
564            }
565        }
566    }
567
568    #[test]
569    fn sanity_check_a_silly_rule_for_the_readme() {
570        let context = RuleContext {
571            cargo_manifest_file_path: &PathBuf::from("Cargo.toml"),
572            verbose: true,
573            metadata: &None,
574            print_output: &mut Vec::new(),
575        };
576        let _ = IsProjectAtALuckyTime::default().evaluate(context);
577    }
578
579    #[test]
580    fn rule_outcome_assert_success_success() {
581        RuleOutcome::Success.assert_success();
582    }
583
584    #[test]
585    #[should_panic]
586    fn rule_outcome_assert_success_failure() {
587        RuleOutcome::Failure.assert_success();
588    }
589
590    #[test]
591    #[should_panic]
592    fn rule_outcome_assert_success_undetermined() {
593        RuleOutcome::Undetermined.assert_success();
594    }
595
596    #[test]
597    fn outcome_stats_assert_success_success() {
598        OutcomeStats {
599            success_count: 1,
600            fail_count: 0,
601            undetermined_count: 0
602        }.assert_success();
603    }
604
605    #[test]
606    #[should_panic]
607    fn outcome_stats_assert_success_all_zero_failure() {
608        OutcomeStats {
609            success_count: 0,
610            fail_count: 0,
611            undetermined_count: 0
612        }.assert_success();
613    }
614
615    #[test]
616    #[should_panic]
617    fn outcome_stats_assert_success_any_fail_count_failure() {
618        OutcomeStats {
619            success_count: 1,
620            fail_count: 1,
621            undetermined_count: 0
622        }.assert_success();
623    }
624
625    #[test]
626    #[should_panic]
627    fn outcome_stats_assert_success_any_undetermined_count_failure() {
628        OutcomeStats {
629            success_count: 1,
630            fail_count: 0,
631            undetermined_count: 1
632        }.assert_success();
633    }
634
635    #[test]
636    #[should_panic]
637    fn outcome_stats_assert_success_both_fail_and_undetermined_failure() {
638        OutcomeStats {
639            success_count: 1,
640            fail_count: 1,
641            undetermined_count: 1
642        }.assert_success();
643    }
644
645    #[test]
646    fn outcomes_by_description_assert_success_minimal_success() {
647        let mut outcomes = OutcomesByDescription::new();
648        outcomes.insert("A".to_owned(), RuleOutcome::Success);
649        outcomes.assert_success();
650    }
651
652    #[test]
653    #[should_panic]
654    fn outcomes_by_description_assert_success_empty_failure() {
655        let outcomes = OutcomesByDescription::new();
656        outcomes.assert_success();
657    }
658
659    #[test]
660    #[should_panic]
661    fn outcomes_by_description_assert_success_any_failure_failure() {
662        let mut outcomes = OutcomesByDescription::new();
663        outcomes.insert("A".to_owned(), RuleOutcome::Success);
664        outcomes.insert("B".to_owned(), RuleOutcome::Failure);
665        outcomes.assert_success();
666    }
667
668    #[test]
669    #[should_panic]
670    fn outcomes_by_description_assert_success_any_undetermined_failure() {
671        let mut outcomes = OutcomesByDescription::new();
672        outcomes.insert("A".to_owned(), RuleOutcome::Success);
673        outcomes.insert("B".to_owned(), RuleOutcome::Undetermined);
674        outcomes.assert_success();
675    }
676
677    #[test]
678    #[should_panic]
679    fn outcomes_by_description_assert_success_both_failure_and_undetermined_failure() {
680        let mut outcomes = OutcomesByDescription::new();
681        outcomes.insert("A".to_owned(), RuleOutcome::Success);
682        outcomes.insert("B".to_owned(), RuleOutcome::Undetermined);
683        outcomes.insert("C".to_owned(), RuleOutcome::Failure);
684        outcomes.assert_success();
685    }
686
687}