1use std::ffi::OsStr;
10use std::fmt;
11use std::io::{self, BufReader, Read};
12use std::iter;
13use std::os::unix::process::ExitStatusExt;
14use std::path::{Path, PathBuf};
15use std::process::{Command, Stdio};
16use std::time::Duration;
17
18use derive_builder::Builder;
19use git_checks_core::impl_prelude::*;
20use git_checks_core::AttributeError;
21use git_workarea::{GitContext, GitWorkArea};
22use itertools::Itertools;
23use log::{debug, info, warn};
24use rayon::prelude::*;
25use thiserror::Error;
26use wait_timeout::ChildExt;
27
28#[derive(Debug, Clone, Copy)]
29enum FormattingExecStage {
30 Run,
31 Wait,
32 Kill,
33 TimeoutWait,
34}
35
36impl fmt::Display for FormattingExecStage {
37 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38 let what = match self {
39 FormattingExecStage::Run => "execute",
40 FormattingExecStage::Wait => "wait on",
41 FormattingExecStage::Kill => "kill (timed out)",
42 FormattingExecStage::TimeoutWait => "wait on (timed out)",
43 };
44
45 write!(f, "{}", what)
46 }
47}
48
49#[derive(Debug, Clone, Copy)]
50enum ListFilesReason {
51 Modified,
52 Untracked,
53}
54
55impl fmt::Display for ListFilesReason {
56 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57 let what = match self {
58 ListFilesReason::Modified => "modified",
59 ListFilesReason::Untracked => "untracked",
60 };
61
62 write!(f, "{}", what)
63 }
64}
65
66#[derive(Debug, Error)]
67enum FormattingError {
68 #[error("failed to {} the {} formatter: {}", stage, command.display(), source)]
69 ExecFormatter {
70 command: PathBuf,
71 stage: FormattingExecStage,
72 #[source]
73 source: io::Error,
74 },
75 #[error("failed to collect stderr from the {} formatter: {}", command.display(), source)]
76 CollectStderr {
77 command: PathBuf,
78 #[source]
79 source: io::Error,
80 },
81 #[error("failed to list {} file in the work area: {}", reason, output)]
82 ListFiles {
83 reason: ListFilesReason,
84 output: String,
85 },
86 #[error("attribute extraction error: {}", source)]
87 Attribute {
88 #[from]
89 source: AttributeError,
90 },
91}
92
93impl FormattingError {
94 fn exec_formatter(command: PathBuf, stage: FormattingExecStage, source: io::Error) -> Self {
95 FormattingError::ExecFormatter {
96 command,
97 stage,
98 source,
99 }
100 }
101
102 fn collect_stderr(command: PathBuf, source: io::Error) -> Self {
103 FormattingError::CollectStderr {
104 command,
105 source,
106 }
107 }
108
109 fn list_files(reason: ListFilesReason, output: &[u8]) -> Self {
110 FormattingError::ListFiles {
111 reason,
112 output: String::from_utf8_lossy(output).into(),
113 }
114 }
115}
116
117#[derive(Builder, Debug, Clone)]
142#[builder(field(private))]
143pub struct Formatting {
144 #[builder(setter(into, strip_option), default)]
151 name: Option<String>,
152 #[builder(setter(into))]
158 kind: String,
159 #[builder(setter(into))]
165 formatter: PathBuf,
166 #[builder(private)]
167 #[builder(setter(name = "_config_files"))]
168 #[builder(default)]
169 config_files: Vec<String>,
170 #[builder(setter(into, strip_option), default)]
178 fix_message: Option<String>,
179 #[builder(setter(into, strip_option), default)]
186 timeout: Option<Duration>,
187}
188
189const MAX_EXPLICIT_FILE_LIST: usize = 5;
192const ZOMBIE_TIMEOUT: Duration = Duration::from_secs(1);
195
196impl FormattingBuilder {
197 pub fn config_files<I, F>(&mut self, files: I) -> &mut Self
202 where
203 I: IntoIterator<Item = F>,
204 F: Into<String>,
205 {
206 self.config_files = Some(files.into_iter().map(Into::into).collect());
207 self
208 }
209}
210
211impl Formatting {
212 pub fn builder() -> FormattingBuilder {
214 Default::default()
215 }
216
217 fn check_path<'a>(
219 &self,
220 ctx: &GitWorkArea,
221 formatter: &Path,
222 env_key: Option<&OsStr>,
223 path: &'a FileName,
224 attr_value: Option<String>,
225 ) -> Result<Option<&'a FileName>, FormattingError> {
226 let mut cmd = Command::new(formatter);
227 ctx.cd_to_work_tree(&mut cmd);
228 cmd.arg(path.as_path());
229 if let Some(attr_value) = attr_value {
230 cmd.arg(attr_value);
231 }
232 if let Some(env_key) = env_key {
233 cmd.env(env_key, "check");
234 }
235
236 let (success, output) = if let Some(timeout) = self.timeout {
237 let mut child = cmd
238 .stdin(Stdio::null())
240 .stdout(Stdio::null())
242 .stderr(Stdio::piped())
246 .spawn()
247 .map_err(|err| {
248 FormattingError::exec_formatter(
249 formatter.to_path_buf(),
250 FormattingExecStage::Run,
251 err,
252 )
253 })?;
254 let check = child.wait_timeout(timeout).map_err(|err| {
255 FormattingError::exec_formatter(
256 formatter.to_path_buf(),
257 FormattingExecStage::Wait,
258 err,
259 )
260 })?;
261
262 if let Some(status) = check {
263 let stderr = child.stderr.expect("spawned with stderr");
264 let stderr = BufReader::new(stderr);
265 let bytes_output = stderr
266 .bytes()
267 .collect::<Result<Vec<u8>, _>>()
268 .map_err(|err| FormattingError::collect_stderr(formatter.to_path_buf(), err))?;
269 (
270 status.success(),
271 format!(
272 "failed with exit code {:?}, signal {:?}, output: {:?}",
273 status.code(),
274 status.signal(),
275 String::from_utf8_lossy(&bytes_output),
276 ),
277 )
278 } else {
279 child.kill().map_err(|err| {
280 FormattingError::exec_formatter(
281 formatter.to_path_buf(),
282 FormattingExecStage::Kill,
283 err,
284 )
285 })?;
286 let timed_out_status = child.wait_timeout(ZOMBIE_TIMEOUT).map_err(|err| {
287 FormattingError::exec_formatter(
288 formatter.to_path_buf(),
289 FormattingExecStage::TimeoutWait,
290 err,
291 )
292 })?;
293 if timed_out_status.is_none() {
294 warn!(
295 target: "git-checks/formatting",
296 "leaving a zombie '{}' process; it did not respond to kill",
297 self.kind,
298 );
299 }
300 (false, "timeout reached".into())
301 }
302 } else {
303 let check = cmd.output().map_err(|err| {
304 FormattingError::exec_formatter(
305 formatter.to_path_buf(),
306 FormattingExecStage::Run,
307 err,
308 )
309 })?;
310 (
311 check.status.success(),
312 String::from_utf8_lossy(&check.stderr).into_owned(),
313 )
314 };
315
316 Ok(if success {
317 debug!(
318 target: "git-checks/formatting",
319 "succeeded at running the {} formatting command: {output}",
320 self.kind,
321 );
322 None
323 } else {
324 info!(
325 target: "git-checks/formatting",
326 "failed to run the {} formatting command: {}",
327 self.kind,
328 output,
329 );
330 Some(path)
331 })
332 }
333
334 #[allow(clippy::needless_collect)]
336 fn message_for_paths<P>(
337 &self,
338 results: &mut CheckResult,
339 content: &dyn Content,
340 paths: Vec<P>,
341 description: &str,
342 ) where
343 P: fmt::Display,
344 {
345 if !paths.is_empty() {
346 let mut all_paths = paths.into_iter();
347 let explicit_paths = all_paths
349 .by_ref()
350 .take(MAX_EXPLICIT_FILE_LIST)
351 .map(|path| format!("`{}`", path))
352 .collect::<Vec<_>>();
353 let next_path = all_paths.next();
355 let tail_paths = if let Some(next_path) = next_path {
356 let remaining_paths = all_paths.count();
357 if remaining_paths == 0 {
358 iter::once(format!("`{}`", next_path)).collect::<Vec<_>>()
360 } else {
361 iter::once(format!("and {} others", remaining_paths + 1)).collect::<Vec<_>>()
362 }
363 } else {
364 iter::empty().collect::<Vec<_>>()
365 }
366 .into_iter();
367 let paths = explicit_paths.into_iter().chain(tail_paths).join(", ");
368 let fix = self
369 .fix_message
370 .as_ref()
371 .map_or_else(String::new, |fix_message| format!(" {}", fix_message));
372 results.add_error(format!(
373 "{}the following files {} the '{}' check: {}.{}",
374 commit_prefix_str(content, "is not allowed because"),
375 description,
376 self.name.as_ref().unwrap_or(&self.kind),
377 paths,
378 fix,
379 ));
380 }
381 }
382}
383
384impl ContentCheck for Formatting {
385 fn name(&self) -> &str {
386 "formatting"
387 }
388
389 fn check(
390 &self,
391 ctx: &CheckGitContext,
392 content: &dyn Content,
393 ) -> Result<CheckResult, Box<dyn Error>> {
394 let changed_paths = content.modified_files();
395
396 let gitctx = GitContext::new(ctx.gitdir());
397 let mut workarea = content.workarea(&gitctx)?;
398
399 let files_to_checkout = changed_paths
401 .iter()
402 .map(|path| path.as_path())
403 .chain(self.config_files.iter().map(AsRef::as_ref))
404 .collect::<Vec<_>>();
405 workarea.checkout(&files_to_checkout)?;
406
407 let formatter = ctx
408 .configuration(&format!("formatter.{}.path", self.kind))
409 .map(Path::new)
410 .unwrap_or(&self.formatter);
411 let env_key = ctx.configuration("formatter.env_key").map(OsStr::new);
412
413 let attr = format!("format.{}", self.kind);
414 let failed_paths = changed_paths
415 .par_iter()
416 .map(|path| {
417 match ctx.check_attr(&attr, path.as_path())? {
418 AttributeState::Set => {
419 self.check_path(&workarea, formatter, env_key, path, None)
420 },
421 AttributeState::Value(v) => {
422 self.check_path(&workarea, formatter, env_key, path, Some(v))
423 },
424 _ => Ok(None),
425 }
426 })
427 .collect::<Vec<Result<_, _>>>()
428 .into_iter()
429 .collect::<Result<Vec<_>, _>>()?
430 .into_iter()
431 .flatten()
432 .collect::<Vec<_>>();
433
434 let ls_files_m = workarea
435 .git()
436 .arg("ls-files")
437 .arg("-m")
438 .output()
439 .map_err(|err| GitError::subcommand("ls-files -m", err))?;
440 if !ls_files_m.status.success() {
441 return Err(
442 FormattingError::list_files(ListFilesReason::Modified, &ls_files_m.stderr).into(),
443 );
444 }
445 let modified_paths = String::from_utf8_lossy(&ls_files_m.stdout);
446
447 let modified_paths_in_commit = modified_paths
451 .lines()
452 .filter(|&path| {
453 changed_paths
454 .iter()
455 .any(|diff_path| diff_path.as_str() == path)
456 })
457 .collect();
458
459 let ls_files_o = workarea
460 .git()
461 .arg("ls-files")
462 .arg("-o")
463 .output()
464 .map_err(|err| GitError::subcommand("ls-files -o", err))?;
465 if !ls_files_o.status.success() {
466 return Err(FormattingError::list_files(
467 ListFilesReason::Untracked,
468 &ls_files_o.stderr,
469 )
470 .into());
471 }
472 let untracked_paths = String::from_utf8_lossy(&ls_files_o.stdout);
473
474 let mut results = CheckResult::new();
475
476 self.message_for_paths(
477 &mut results,
478 content,
479 failed_paths,
480 "could not be formatted by",
481 );
482 self.message_for_paths(
483 &mut results,
484 content,
485 modified_paths_in_commit,
486 "are not formatted according to",
487 );
488 self.message_for_paths(
489 &mut results,
490 content,
491 untracked_paths.lines().collect(),
492 "were created by",
493 );
494
495 Ok(results)
496 }
497}
498
499#[cfg(feature = "config")]
500pub(crate) mod config {
501 #[cfg(test)]
502 use std::path::Path;
503 use std::time::Duration;
504
505 use git_checks_config::{register_checks, CommitCheckConfig, IntoCheck, TopicCheckConfig};
506 use serde::Deserialize;
507 #[cfg(test)]
508 use serde_json::json;
509
510 #[cfg(test)]
511 use crate::test;
512 use crate::Formatting;
513
514 #[derive(Deserialize, Debug)]
546 pub struct FormattingConfig {
547 #[serde(default)]
548 name: Option<String>,
549 kind: String,
550 formatter: String,
551 #[serde(default)]
552 config_files: Option<Vec<String>>,
553 #[serde(default)]
554 fix_message: Option<String>,
555 #[serde(default)]
556 timeout: Option<u64>,
557 }
558
559 impl IntoCheck for FormattingConfig {
560 type Check = Formatting;
561
562 fn into_check(self) -> Self::Check {
563 let mut builder = Formatting::builder();
564
565 builder.kind(self.kind).formatter(self.formatter);
566
567 if let Some(name) = self.name {
568 builder.name(name);
569 }
570
571 if let Some(config_files) = self.config_files {
572 builder.config_files(config_files);
573 }
574
575 if let Some(fix_message) = self.fix_message {
576 builder.fix_message(fix_message);
577 }
578
579 if let Some(timeout) = self.timeout {
580 builder.timeout(Duration::from_secs(timeout));
581 }
582
583 builder
584 .build()
585 .expect("configuration mismatch for `Formatting`")
586 }
587 }
588
589 register_checks! {
590 FormattingConfig {
591 "formatting" => CommitCheckConfig,
592 "formatting/topic" => TopicCheckConfig,
593 },
594 }
595
596 #[test]
597 fn test_formatting_config_empty() {
598 let json = json!({});
599 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
600 test::check_missing_json_field(err, "kind");
601 }
602
603 #[test]
604 fn test_formatting_config_kind_is_required() {
605 let exp_formatter = "/path/to/formatter";
606 let json = json!({
607 "formatter": exp_formatter,
608 });
609 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
610 test::check_missing_json_field(err, "kind");
611 }
612
613 #[test]
614 fn test_formatting_config_formatter_is_required() {
615 let exp_kind = "kind";
616 let json = json!({
617 "kind": exp_kind,
618 });
619 let err = serde_json::from_value::<FormattingConfig>(json).unwrap_err();
620 test::check_missing_json_field(err, "formatter");
621 }
622
623 #[test]
624 fn test_formatting_config_minimum_fields() {
625 let exp_kind = "kind";
626 let exp_formatter = "/path/to/formatter";
627 let json = json!({
628 "kind": exp_kind,
629 "formatter": exp_formatter,
630 });
631 let check: FormattingConfig = serde_json::from_value(json).unwrap();
632
633 assert_eq!(check.name, None);
634 assert_eq!(check.kind, exp_kind);
635 assert_eq!(check.formatter, exp_formatter);
636 assert_eq!(check.config_files, None);
637 assert_eq!(check.fix_message, None);
638 assert_eq!(check.timeout, None);
639
640 let check = check.into_check();
641
642 assert_eq!(check.name, None);
643 assert_eq!(check.kind, exp_kind);
644 assert_eq!(check.formatter, Path::new(exp_formatter));
645 itertools::assert_equal(&check.config_files, &[] as &[&str]);
646 assert_eq!(check.fix_message, None);
647 assert_eq!(check.timeout, None);
648 }
649
650 #[test]
651 fn test_formatting_config_all_fields() {
652 let exp_name: String = "formatter name".into();
653 let exp_kind = "kind";
654 let exp_formatter = "/path/to/formatter";
655 let exp_config: String = "path/to/config/file".into();
656 let exp_fix_message: String = "instructions for fixing".into();
657 let exp_timeout = 10;
658 let json = json!({
659 "name": exp_name,
660 "kind": exp_kind,
661 "formatter": exp_formatter,
662 "config_files": [exp_config],
663 "fix_message": exp_fix_message,
664 "timeout": exp_timeout,
665 });
666 let check: FormattingConfig = serde_json::from_value(json).unwrap();
667
668 assert_eq!(check.name, Some(exp_name.clone()));
669 assert_eq!(check.kind, exp_kind);
670 assert_eq!(check.formatter, exp_formatter);
671 itertools::assert_equal(
672 check.config_files.as_ref().unwrap(),
673 std::slice::from_ref(&exp_config),
674 );
675 assert_eq!(check.fix_message, Some(exp_fix_message.clone()));
676 assert_eq!(check.timeout, Some(exp_timeout));
677
678 let check = check.into_check();
679
680 assert_eq!(check.name, Some(exp_name));
681 assert_eq!(check.kind, exp_kind);
682 assert_eq!(check.formatter, Path::new(exp_formatter));
683 itertools::assert_equal(&check.config_files, &[exp_config]);
684 assert_eq!(check.fix_message, Some(exp_fix_message));
685 assert_eq!(check.timeout, Some(Duration::from_secs(exp_timeout)));
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use std::time::Duration;
692
693 use git_checks_core::{Check, TopicCheck};
694
695 use crate::builders::FormattingBuilder;
696 use crate::test::*;
697 use crate::Formatting;
698
699 const MISSING_CONFIG_COMMIT: &str = "220efbb4d0380fe932b70444fe15e787506080b0";
700 const ADD_CONFIG_COMMIT: &str = "e08e9ac1c5b6a0a67e0b2715cb0dbf99935d9cbf";
701 const BAD_FORMAT_COMMIT: &str = "e9a08d956553f94e9c8a0a02b11ca60f62de3c2b";
702 const FIX_BAD_FORMAT_COMMIT: &str = "7fe590bdb883e195812cae7602ce9115cbd269ee";
703 const OK_FORMAT_COMMIT: &str = "b77d2a5d63cd6afa599d0896dafff95f1ace50b6";
704 const ENV_FORMAT_COMMIT: &str = "11107007b2875ac2f98a89085ff8f4f631b369ae";
705 const ENV_BAD_FORMAT_COMMIT: &str = "8358d940dbe6cdd6ee75ad229dbaf210a604870a";
706 const IGNORE_UNTRACKED_COMMIT: &str = "c0154d1087906d50c5551ff8f60e544e9a492a48";
707 const DELETE_FORMAT_COMMIT: &str = "31446c81184df35498814d6aa3c7f933dddf91c2";
708 const MANY_BAD_FORMAT_COMMIT: &str = "f0d10d9385ef697175c48fa72324c33d5e973f4b";
709 const MANY_MORE_BAD_FORMAT_COMMIT: &str = "0e80ff6dd2495571b7d255e39fb3e7cc9f487fb7";
710 const TIMEOUT_CONFIG_COMMIT: &str = "62f5eac20c5021cf323c757a4d24234c81c9c7ad";
711 const WITH_ARG_COMMIT: &str = "dfa4c021e96f1e94634ad1787f31c2abdbeaff99";
712 const NOEXEC_CONFIG_COMMIT: &str = "1b5be48f8ce45b8fec155a1787cabb6995194ce5";
713
714 #[test]
715 fn test_exec_stage_display() {
716 assert_eq!(format!("{}", super::FormattingExecStage::Run), "execute");
717 assert_eq!(format!("{}", super::FormattingExecStage::Wait), "wait on");
718 assert_eq!(
719 format!("{}", super::FormattingExecStage::Kill),
720 "kill (timed out)"
721 );
722 assert_eq!(
723 format!("{}", super::FormattingExecStage::TimeoutWait),
724 "wait on (timed out)"
725 );
726 }
727
728 #[test]
729 fn test_list_files_reason_display() {
730 assert_eq!(format!("{}", super::ListFilesReason::Modified), "modified");
731 assert_eq!(
732 format!("{}", super::ListFilesReason::Untracked),
733 "untracked"
734 );
735 }
736
737 #[test]
738 fn test_formatting_builder_default() {
739 assert!(Formatting::builder().build().is_err());
740 }
741
742 #[test]
743 fn test_formatting_builder_kind_is_required() {
744 assert!(Formatting::builder()
745 .formatter("path/to/formatter")
746 .build()
747 .is_err());
748 }
749
750 #[test]
751 fn test_formatting_builder_formatter_is_required() {
752 assert!(Formatting::builder().kind("kind").build().is_err());
753 }
754
755 #[test]
756 fn test_formatting_builder_minimum_fields() {
757 assert!(Formatting::builder()
758 .formatter("path/to/formatter")
759 .kind("kind")
760 .build()
761 .is_ok());
762 }
763
764 #[test]
765 fn test_formatting_name_commit() {
766 let check = Formatting::builder()
767 .formatter("path/to/formatter")
768 .kind("kind")
769 .build()
770 .unwrap();
771 assert_eq!(Check::name(&check), "formatting");
772 }
773
774 #[test]
775 fn test_formatting_name_topic() {
776 let check = Formatting::builder()
777 .formatter("path/to/formatter")
778 .kind("kind")
779 .build()
780 .unwrap();
781 assert_eq!(TopicCheck::name(&check), "formatting");
782 }
783
784 fn formatting_check(kind: &str) -> FormattingBuilder {
785 let formatter = format!("{}/test/format.{}", env!("CARGO_MANIFEST_DIR"), kind);
786 let mut builder = Formatting::builder();
787 builder
788 .kind(kind)
789 .formatter(formatter)
790 .config_files(["format-config"].iter().cloned());
791 builder
792 }
793
794 #[test]
795 fn test_formatting_pass() {
796 let check = formatting_check("simple").build().unwrap();
797 let conf = make_check_conf(&check);
798
799 let result = test_check_base(
800 "test_formatting_pass",
801 OK_FORMAT_COMMIT,
802 BAD_FORMAT_COMMIT,
803 &conf,
804 );
805 test_result_ok(result);
806 }
807
808 #[test]
809 fn test_formatting_env() {
810 let check = formatting_check("env").build().unwrap();
811 let conf = {
812 let mut conf = make_check_conf(&check);
813 conf.add_configuration("formatter.env_key", "GIT_CHECKS_TEST_FORMATTING_ENV");
814 conf
815 };
816
817 let result = test_check_base(
818 "test_formatting_env",
819 ENV_FORMAT_COMMIT,
820 ENV_BAD_FORMAT_COMMIT,
821 &conf,
822 );
823 test_result_ok(result);
824 }
825
826 #[test]
827 fn test_formatting_env_missing() {
828 let check = formatting_check("env").build().unwrap();
829 let conf = make_check_conf(&check);
830
831 let result = test_check_base(
832 "test_formatting_env_missing",
833 ENV_FORMAT_COMMIT,
834 ENV_BAD_FORMAT_COMMIT,
835 &conf,
836 );
837 test_result_errors(result, &[
838 "commit 11107007b2875ac2f98a89085ff8f4f631b369ae is not allowed because the following \
839 files could not be formatted by the 'env' check: `ok.txt`.",
840 ]);
841 }
842
843 #[test]
844 fn test_formatting_pass_with_arg() {
845 let check = formatting_check("with_arg").build().unwrap();
846 let conf = make_check_conf(&check);
847
848 let result = test_check("test_formatting_pass_with_arg", WITH_ARG_COMMIT, &conf);
849 test_result_errors(result, &[
850 "commit dfa4c021e96f1e94634ad1787f31c2abdbeaff99 is not allowed because the following \
851 files are not formatted according to the 'with_arg' check: `with-arg.txt`.",
852 ]);
853 }
854
855 #[test]
856 fn test_formatting_formatter_fail() {
857 let check = formatting_check("simple").build().unwrap();
858 let result = run_check(
859 "test_formatting_formatter_fail",
860 MISSING_CONFIG_COMMIT,
861 check,
862 );
863 test_result_errors(result, &[
864 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
865 files could not be formatted by the 'simple' check: `empty.txt`.",
866 ]);
867 }
868
869 #[test]
870 fn test_formatting_formatter_fail_named() {
871 let check = formatting_check("simple").name("renamed").build().unwrap();
872 let result = run_check(
873 "test_formatting_formatter_fail_named",
874 MISSING_CONFIG_COMMIT,
875 check,
876 );
877 test_result_errors(result, &[
878 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
879 files could not be formatted by the 'renamed' check: `empty.txt`.",
880 ]);
881 }
882
883 #[test]
884 fn test_formatting_formatter_fail_fix_message() {
885 let check = formatting_check("simple")
886 .fix_message("These may be fixed by magic.")
887 .build()
888 .unwrap();
889 let result = run_check(
890 "test_formatting_formatter_fail_fix_message",
891 MISSING_CONFIG_COMMIT,
892 check,
893 );
894 test_result_errors(result, &[
895 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
896 files could not be formatted by the 'simple' check: `empty.txt`. These may be fixed \
897 by magic.",
898 ]);
899 }
900
901 #[test]
902 fn test_formatting_formatter_untracked_files() {
903 let check = formatting_check("untracked").build().unwrap();
904 let result = run_check(
905 "test_formatting_formatter_untracked_files",
906 MISSING_CONFIG_COMMIT,
907 check,
908 );
909 test_result_errors(result, &[
910 "commit 220efbb4d0380fe932b70444fe15e787506080b0 is not allowed because the following \
911 files were created by the 'untracked' check: `untracked`.",
912 ]);
913 }
914
915 #[test]
916 fn test_formatting_formatter_timeout() {
917 let check = formatting_check("timeout")
918 .timeout(Duration::from_secs(1))
919 .build()
920 .unwrap();
921 let result = run_check(
922 "test_formatting_formatter_timeout",
923 TIMEOUT_CONFIG_COMMIT,
924 check,
925 );
926 test_result_errors(result, &[
927 "commit 62f5eac20c5021cf323c757a4d24234c81c9c7ad is not allowed because the following \
928 files could not be formatted by the 'timeout' check: `empty.txt`.",
929 ]);
930 }
931
932 #[test]
933 fn test_formatting_formatter_untracked_files_ignored() {
934 let check = formatting_check("untracked").build().unwrap();
935 let conf = make_check_conf(&check);
936
937 let result = test_check_base(
938 "test_formatting_formatter_untracked_files_ignored",
939 IGNORE_UNTRACKED_COMMIT,
940 OK_FORMAT_COMMIT,
941 &conf,
942 );
943 test_result_ok(result);
944 }
945
946 #[test]
947 fn test_formatting_formatter_modified_files() {
948 let check = formatting_check("simple").build().unwrap();
949 let conf = make_check_conf(&check);
950
951 let result = test_check_base(
952 "test_formatting_formatter_modified_files",
953 BAD_FORMAT_COMMIT,
954 ADD_CONFIG_COMMIT,
955 &conf,
956 );
957 test_result_errors(result, &[
958 "commit e9a08d956553f94e9c8a0a02b11ca60f62de3c2b is not allowed because the following \
959 files are not formatted according to the 'simple' check: `bad.txt`.",
960 ]);
961 }
962
963 #[test]
964 fn test_formatting_formatter_modified_files_topic() {
965 let check = formatting_check("simple").build().unwrap();
966 let conf = make_topic_check_conf(&check);
967
968 let result = test_check_base(
969 "test_formatting_formatter_modified_files_topic",
970 BAD_FORMAT_COMMIT,
971 ADD_CONFIG_COMMIT,
972 &conf,
973 );
974 test_result_errors(
975 result,
976 &["the following files are not formatted according to the 'simple' check: `bad.txt`."],
977 );
978 }
979
980 #[test]
981 fn test_formatting_formatter_modified_files_topic_fixed() {
982 let check = formatting_check("simple").build().unwrap();
983 run_topic_check_ok(
984 "test_formatting_formatter_modified_files_topic_fixed",
985 FIX_BAD_FORMAT_COMMIT,
986 check,
987 );
988 }
989
990 #[test]
991 fn test_formatting_formatter_many_modified_files() {
992 let check = formatting_check("simple").build().unwrap();
993 let conf = make_check_conf(&check);
994
995 let result = test_check_base(
996 "test_formatting_formatter_many_modified_files",
997 MANY_BAD_FORMAT_COMMIT,
998 ADD_CONFIG_COMMIT,
999 &conf,
1000 );
1001 test_result_errors(result, &[
1002 "commit f0d10d9385ef697175c48fa72324c33d5e973f4b is not allowed because the following \
1003 files are not formatted according to the 'simple' check: `1.bad.txt`, `2.bad.txt`, \
1004 `3.bad.txt`, `4.bad.txt`, `5.bad.txt`, `6.bad.txt`.",
1005 ]);
1006 }
1007
1008 #[test]
1009 fn test_formatting_formatter_many_more_modified_files() {
1010 let check = formatting_check("simple").build().unwrap();
1011 let conf = make_check_conf(&check);
1012
1013 let result = test_check_base(
1014 "test_formatting_formatter_many_more_modified_files",
1015 MANY_MORE_BAD_FORMAT_COMMIT,
1016 ADD_CONFIG_COMMIT,
1017 &conf,
1018 );
1019 test_result_errors(result, &[
1020 "commit 0e80ff6dd2495571b7d255e39fb3e7cc9f487fb7 is not allowed because the following \
1021 files are not formatted according to the 'simple' check: `1.bad.txt`, `2.bad.txt`, \
1022 `3.bad.txt`, `4.bad.txt`, `5.bad.txt`, and 2 others.",
1023 ]);
1024 }
1025
1026 #[test]
1027 fn test_formatting_script_deleted_files() {
1028 let check = formatting_check("delete").build().unwrap();
1029 let result = run_check(
1030 "test_formatting_script_deleted_files",
1031 DELETE_FORMAT_COMMIT,
1032 check,
1033 );
1034 test_result_errors(result, &[
1035 "commit 31446c81184df35498814d6aa3c7f933dddf91c2 is not allowed because the following \
1036 files are not formatted according to the 'delete' check: `remove.txt`.",
1037 ]);
1038 }
1039
1040 #[test]
1041 fn test_formatting_formatter_noexec() {
1042 let check = formatting_check("noexec").build().unwrap();
1043 let result = run_check(
1044 "test_formatting_formatter_noexec",
1045 NOEXEC_CONFIG_COMMIT,
1046 check,
1047 );
1048
1049 assert_eq!(result.warnings().len(), 0);
1050 assert_eq!(result.alerts().len(), 1);
1051 assert_eq!(
1052 result.alerts()[0],
1053 "failed to run the formatting check on commit 1b5be48f8ce45b8fec155a1787cabb6995194ce5",
1054 );
1055 assert_eq!(result.errors().len(), 0);
1056 assert!(!result.temporary());
1057 assert!(!result.allowed());
1058 assert!(!result.pass());
1059 }
1060
1061 #[test]
1062 fn test_formatting_replace_path() {
1063 let check = formatting_check("simple").build().unwrap();
1064 let conf = {
1065 let mut conf = make_check_conf(&check);
1066 conf.add_configuration(
1067 "formatter.simple.path",
1068 format!("{}/test/noexist/format.simple", env!("CARGO_MANIFEST_DIR")),
1069 );
1070 conf
1071 };
1072
1073 let result = test_check_base(
1074 "test_formatting_replace_path",
1075 OK_FORMAT_COMMIT,
1076 BAD_FORMAT_COMMIT,
1077 &conf,
1078 );
1079
1080 assert_eq!(result.warnings().len(), 0);
1081 assert_eq!(result.alerts().len(), 1);
1082 assert_eq!(
1083 result.alerts()[0],
1084 "failed to run the formatting check on commit b77d2a5d63cd6afa599d0896dafff95f1ace50b6",
1085 );
1086 assert_eq!(result.errors().len(), 0);
1087 assert!(!result.temporary());
1088 assert!(!result.allowed());
1089 assert!(!result.pass());
1090 }
1091}