/ test / lint / test_runner / src / main.rs
main.rs
  1  // Copyright (c) The Bitcoin Core developers
  2  // Distributed under the MIT software license, see the accompanying
  3  // file COPYING or https://opensource.org/license/mit/.
  4  
  5  mod lint_cpp;
  6  mod lint_docs;
  7  mod lint_py;
  8  mod lint_repo_hygiene;
  9  mod lint_text_format;
 10  mod util;
 11  
 12  use std::env;
 13  use std::fs;
 14  use std::process::{Command, ExitCode};
 15  
 16  use lint_cpp::{
 17      lint_boost_assert, lint_includes_build_config, lint_remove_all, lint_rpc_assert,
 18      lint_std_filesystem,
 19  };
 20  use lint_docs::{lint_doc_args, lint_doc_release_note_snippets, lint_markdown};
 21  use lint_py::{lint_py_lint, lint_rmtree};
 22  use lint_repo_hygiene::{lint_scripted_diff, lint_subtree};
 23  use lint_text_format::{
 24      lint_commit_msg, lint_tabs_whitespace, lint_trailing_newline, lint_trailing_whitespace,
 25  };
 26  use util::{check_output, commit_range, get_git_root, git, LintFn, LintResult};
 27  
 28  struct Linter {
 29      pub description: &'static str,
 30      pub name: &'static str,
 31      pub lint_fn: LintFn,
 32  }
 33  
 34  fn get_linter_list() -> Vec<&'static Linter> {
 35      vec![
 36          &Linter {
 37              description: "Check that all command line arguments are documented.",
 38              name: "doc",
 39              lint_fn: lint_doc_args
 40          },
 41          &Linter {
 42              description: "Check that no symbol from bitcoin-build-config.h is used without the header being included",
 43              name: "includes_build_config",
 44              lint_fn: lint_includes_build_config
 45          },
 46          &Linter {
 47              description: "Check that markdown links resolve",
 48              name: "markdown",
 49              lint_fn: lint_markdown
 50          },
 51          &Linter {
 52              description: "Lint Python code",
 53              name: "py_lint",
 54              lint_fn: lint_py_lint,
 55          },
 56          &Linter {
 57              description: "Check that shutil.rmtree is not used",
 58              name: "rmtree",
 59              lint_fn: lint_rmtree,
 60          },
 61          &Linter {
 62              description: "Check that std::filesystem is not used directly",
 63              name: "std_filesystem",
 64              lint_fn: lint_std_filesystem
 65          },
 66          &Linter {
 67              description: "Check that remove_all is not used",
 68              name: "remove_all",
 69              lint_fn: lint_remove_all
 70          },
 71          &Linter {
 72              description: "Check that fatal assertions are not used in RPC code",
 73              name: "rpc_assert",
 74              lint_fn: lint_rpc_assert
 75          },
 76          &Linter {
 77              description: "Check that boost assertions are not used",
 78              name: "boost_assert",
 79              lint_fn: lint_boost_assert
 80          },
 81          &Linter {
 82              description: "Check that release note snippets are in the right folder",
 83              name: "doc_release_note_snippets",
 84              lint_fn: lint_doc_release_note_snippets
 85          },
 86          &Linter {
 87              description: "Check that subtrees are pure subtrees",
 88              name: "subtree",
 89              lint_fn: lint_subtree
 90          },
 91          &Linter {
 92              description: "Check scripted-diffs",
 93              name: "scripted_diff",
 94              lint_fn: lint_scripted_diff
 95          },
 96          &Linter {
 97              description: "Check that commit messages have a new line before the body or no body at all.",
 98              name: "commit_msg",
 99              lint_fn: lint_commit_msg
100          },
101          &Linter {
102              description: "Check that tabs are not used as whitespace",
103              name: "tabs_whitespace",
104              lint_fn: lint_tabs_whitespace
105          },
106          &Linter {
107              description: "Check for trailing whitespace",
108              name: "trailing_whitespace",
109              lint_fn: lint_trailing_whitespace
110          },
111          &Linter {
112              description: "Check for trailing newline",
113              name: "trailing_newline",
114              lint_fn: lint_trailing_newline
115          },
116          &Linter {
117              description: "Run all linters of the form: test/lint/lint-*.py",
118              name: "all_python_linters",
119              lint_fn: run_all_python_linters
120          },
121      ]
122  }
123  
124  fn print_help_and_exit() {
125      print!(
126          r#"
127  Usage: test_runner [--lint=LINTER_TO_RUN]
128  Runs all linters in the lint test suite, printing any errors
129  they detect.
130  
131  If you wish to only run some particular lint tests, pass
132  '--lint=' with the name of the lint test you wish to run.
133  You can set as many '--lint=' values as you wish, e.g.:
134  test_runner --lint=doc --lint=subtree
135  
136  The individual linters available to run are:
137  "#
138      );
139      for linter in get_linter_list() {
140          println!("{}: \"{}\"", linter.name, linter.description)
141      }
142  
143      std::process::exit(1);
144  }
145  
146  fn parse_lint_args(args: &[String]) -> Vec<&'static Linter> {
147      let linter_list = get_linter_list();
148      let mut lint_values = Vec::new();
149  
150      for arg in args {
151          #[allow(clippy::if_same_then_else)]
152          if arg.starts_with("--lint=") {
153              let lint_arg_value = arg
154                  .trim_start_matches("--lint=")
155                  .trim_matches('"')
156                  .trim_matches('\'');
157  
158              let try_find_linter = linter_list
159                  .iter()
160                  .find(|linter| linter.name == lint_arg_value);
161              match try_find_linter {
162                  Some(linter) => {
163                      lint_values.push(*linter);
164                  }
165                  None => {
166                      println!("No linter {lint_arg_value} found!");
167                      print_help_and_exit();
168                  }
169              }
170          } else if arg.eq("--help") || arg.eq("-h") {
171              print_help_and_exit();
172          } else {
173              print_help_and_exit();
174          }
175      }
176  
177      lint_values
178  }
179  
180  fn run_all_python_linters() -> LintResult {
181      let mut good = true;
182      let lint_dir = get_git_root().join("test/lint");
183      for entry in fs::read_dir(lint_dir).unwrap() {
184          let entry = entry.unwrap();
185          let entry_fn = entry.file_name().into_string().unwrap();
186          if entry_fn.starts_with("lint-")
187              && entry_fn.ends_with(".py")
188              && !Command::new("python3")
189                  .arg(entry.path())
190                  .status()
191                  .expect("command error")
192                  .success()
193          {
194              good = false;
195              println!("^---- ⚠️ Failure generated from {entry_fn}");
196          }
197      }
198      if good {
199          Ok(())
200      } else {
201          Err("".to_string())
202      }
203  }
204  
205  fn main() -> ExitCode {
206      let linters_to_run: Vec<&Linter> = if env::args().count() > 1 {
207          let args: Vec<String> = env::args().skip(1).collect();
208          parse_lint_args(&args)
209      } else {
210          // If no arguments are passed, run all linters.
211          get_linter_list()
212      };
213  
214      let git_root = get_git_root();
215      let commit_range = commit_range();
216      let commit_log = check_output(git().args(["log", "--no-merges", "--oneline", &commit_range]))
217          .expect("check_output failed");
218      println!("Checking commit range ({commit_range}):\n{commit_log}\n");
219  
220      let mut test_failed = false;
221      for linter in linters_to_run {
222          // chdir to root before each lint test
223          env::set_current_dir(&git_root).unwrap();
224          if let Err(err) = (linter.lint_fn)() {
225              println!(
226                  "^^^\n{err}\n^---- ⚠️ Failure generated from lint check '{}' ({})!\n\n",
227                  linter.name, linter.description,
228              );
229              test_failed = true;
230          }
231      }
232      if test_failed {
233          ExitCode::FAILURE
234      } else {
235          ExitCode::SUCCESS
236      }
237  }