doc_cli_flags.rs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. //! Guards that the PII/TOTP migration commands documented in
  2. //! `docs/OPERATIONS.md` match the real CLI.
  3. //!
  4. //! The docs are part of the change, so a flag added to (or removed from) these
  5. //! commands without updating the runbook must fail here. The flags are parsed
  6. //! from the real binary's `--help`, so the check cannot drift from the CLI the
  7. //! way a hand-maintained list would. `unwrap`/`expect`/`panic` are denied
  8. //! workspace-wide, so the body returns a `Result`.
  9. use std::{collections::BTreeSet, path::PathBuf, process::Command};
  10. /// Test body result (no `unwrap`/`expect` under the deny lints).
  11. type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
  12. /// The migration commands, as the CLI path segments that select them.
  13. const COMMANDS: &[&[&str]] = &[
  14. &["pii", "backfill"],
  15. &["pii", "verify"],
  16. &["pii", "rotate"],
  17. &["totp", "rewrap"],
  18. ];
  19. /// The real binary Cargo builds for the integration test target.
  20. fn binary() -> PathBuf {
  21. PathBuf::from(env!("CARGO_BIN_EXE_d-oauth-server"))
  22. }
  23. /// Run `<command> --help` and return its stdout.
  24. fn run_help(command: &[&str]) -> TestResult<String> {
  25. let output = Command::new(binary())
  26. .args(command)
  27. .arg("--help")
  28. .output()?;
  29. if !output.status.success() {
  30. return Err(
  31. format!(
  32. "`d-oauth-server {} --help` exited with {}",
  33. command.join(" "),
  34. output.status
  35. )
  36. .into(),
  37. );
  38. }
  39. Ok(String::from_utf8_lossy(&output.stdout).into_owned())
  40. }
  41. /// Long flags in the `Options:` section of a clap help page, excluding the
  42. /// universal `--help` / `--version`.
  43. fn help_flags(help: &str) -> BTreeSet<String> {
  44. let mut flags = BTreeSet::new();
  45. let mut in_options = false;
  46. for line in help.lines() {
  47. if line.trim() == "Options:" {
  48. in_options = true;
  49. continue;
  50. }
  51. if !in_options {
  52. continue;
  53. }
  54. // A blank line ends the Options block; the block is the last section of a
  55. // clap help page.
  56. if line.trim().is_empty() {
  57. in_options = false;
  58. continue;
  59. }
  60. for token in line.split_whitespace() {
  61. let token = token.trim_end_matches(',');
  62. if let Some(name) = token.strip_prefix("--")
  63. && name != "help"
  64. && name != "version"
  65. {
  66. flags.insert(format!("--{name}"));
  67. }
  68. }
  69. }
  70. flags
  71. }
  72. /// Long flags a `docs/OPERATIONS.md` line documents for a command.
  73. fn doc_flags(docs: &str, command: &[&str]) -> BTreeSet<String> {
  74. let needle = format!("d-oauth-server {}", command.join(" "));
  75. let mut flags = BTreeSet::new();
  76. for line in docs.lines() {
  77. if !line.contains(&needle) {
  78. continue;
  79. }
  80. for token in line.split_whitespace() {
  81. let token = token.trim_matches(|ch| ch == '`' || ch == ',');
  82. if let Some(name) = token.strip_prefix("--")
  83. && name != "help"
  84. && name != "version"
  85. {
  86. flags.insert(format!("--{name}"));
  87. }
  88. }
  89. }
  90. flags
  91. }
  92. #[test]
  93. fn doc_cli_flags_match_help() -> TestResult {
  94. let docs_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("docs/OPERATIONS.md");
  95. let docs = std::fs::read_to_string(&docs_path)?;
  96. for command in COMMANDS {
  97. let label = command.join(" ");
  98. let help = run_help(command)?;
  99. // The resolved help must name the command, so a renamed or removed command
  100. // fails instead of silently checking the wrong surface.
  101. let usage = format!("d-oauth-server {label}");
  102. if !help.contains(&usage) {
  103. return Err(format!("`{label} --help` did not show usage `{usage}`").into());
  104. }
  105. let from_help = help_flags(&help);
  106. let from_docs = doc_flags(&docs, command);
  107. if from_help != from_docs {
  108. return Err(
  109. format!("flag mismatch for `{label}`: help={from_help:?} docs={from_docs:?}").into(),
  110. );
  111. }
  112. }
  113. Ok(())
  114. }