1#![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#[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 PrintOutputFailure {
120 topic: &'static str,
122 },
123 #[doc(hidden)]
128 #[fail(display = "A hidden variant to increase expansion flexibility")]
129 __Nonexhaustive,
130}
131
132pub 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
178pub 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
309pub type OutcomesByDescription = HashMap<String, RuleOutcome>;
311
312pub trait IsSuccess {
316 fn is_success(&self) -> bool;
320
321 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#[derive(Clone, Debug, Default, PartialEq)]
437pub struct OutcomeStats {
438 pub success_count: usize,
440 pub fail_count: usize,
442 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}