crates/gunnar-server/tests/admin_docs_head_their_fn.rs
//! RA2-11: a doc comment that names an `Admin` RPC must head THAT RPC's function.
//!
//! The settings change once slid two docs down one function: `Admin.AuditTrail`'s
//! doc ended up on `get_setting` and the control plane's audit-trail doc on
//! `setting()`, leaving `audit_trail` with none on either side. rustdoc cannot
//! catch that (any doc on any fn is valid), so this reads the two files and
//! checks each heading against the fn it heads.
//!
//! A heading is the FIRST line of a `///` block, written either
//! ``**`Admin.Foo`**`` (the gRPC service) or ``**…** (`Admin.Foo`…`` (the control
//! plane). Its RPC name in snake case must be the name of the `fn` the block heads,
//! and a block holds at most one heading.
use std::path::Path;
/// The RPC a heading line names, if the line is a heading.
fn heading_rpc(line: &str) -> Option<String> {
let line = line.trim_start().strip_prefix("///")?.trim();
let rest = if let Some(r) = line.strip_prefix("**`Admin.") {
r
} else {
let at = line.find("** (`Admin.")?;
if !line.starts_with("**") {
return None;
}
&line[at + "** (`Admin.".len()..]
};
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric())
.collect();
(!name.is_empty()).then_some(name)
}
fn snake(camel: &str) -> String {
let mut out = String::new();
for (i, c) in camel.chars().enumerate() {
if c.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(c.to_ascii_lowercase());
} else {
out.push(c);
}
}
out
}
/// The name of the `fn` declared on this line, if any.
fn fn_name(line: &str) -> Option<String> {
let at = line.find("fn ")?;
let before = line[..at].trim();
if !before
.split_whitespace()
.all(|w| matches!(w, "pub" | "pub(crate)" | "async" | "const" | "unsafe"))
{
return None;
}
let name: String = line[at + 3..]
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
(!name.is_empty()).then_some(name)
}
/// Every (heading RPC, fn it heads) pair in `src`, and every block with two headings.
fn pairs(src: &str) -> (Vec<(String, String)>, Vec<String>) {
let lines: Vec<&str> = src.lines().collect();
let (mut out, mut doubled) = (Vec::new(), Vec::new());
let mut i = 0;
while i < lines.len() {
if !lines[i].trim_start().starts_with("///") {
i += 1;
continue;
}
let start = i;
while i < lines.len() && lines[i].trim_start().starts_with("///") {
i += 1;
}
let headings: Vec<String> = lines[start..i]
.iter()
.filter_map(|l| heading_rpc(l))
.collect();
if headings.len() > 1 {
doubled.push(format!("line {}: {headings:?}", start + 1));
}
let Some(first) = heading_rpc(lines[start]) else {
continue;
};
// Skip attributes between the doc and its item.
let mut j = i;
while j < lines.len() && lines[j].trim_start().starts_with("#[") {
j += 1;
}
let headed = lines
.get(j)
.and_then(|l| fn_name(l))
.unwrap_or_else(|| "(not a fn)".to_owned());
out.push((first, headed));
}
(out, doubled)
}
fn check(rel: &str, must_have: &[&str]) {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(rel);
let src =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let (pairs, doubled) = pairs(&src);
assert!(
doubled.is_empty(),
"{rel}: a doc block names more than one Admin RPC: {doubled:?}"
);
let wrong: Vec<_> = pairs.iter().filter(|(rpc, f)| snake(rpc) != *f).collect();
assert!(
wrong.is_empty(),
"{rel}: an `Admin.X` doc heads the wrong fn (rpc, fn): {wrong:?}"
);
for rpc in must_have {
assert!(
pairs.iter().any(|(r, f)| r == rpc && *f == snake(rpc)),
"{rel}: `{}` has no doc naming `Admin.{rpc}`",
snake(rpc)
);
}
}
#[test]
fn the_grpc_service_docs_head_their_rpcs() {
check(
"../gunnar-grpc/src/service/admin.rs",
&["AuditTrail", "GetSetting", "SetSetting", "Topology"],
);
}
#[test]
fn the_control_plane_docs_head_their_rpcs() {
check("src/control_plane.rs", &["AuditTrail", "IssuerInfo"]);
}
#[test]
fn the_reader_sees_both_heading_forms() {
assert_eq!(
heading_rpc(" /// **`Admin.AuditTrail`** (lane T11): x").as_deref(),
Some("AuditTrail")
);
assert_eq!(
heading_rpc(" /// **The chain** (`Admin.AuditTrail`, lane T11).").as_deref(),
Some("AuditTrail")
);
assert_eq!(
heading_rpc(" /// **What replication is doing**, for `Admin.Stats`."),
None
);
assert_eq!(snake("GetSetting"), "get_setting");
assert_eq!(
fn_name(" async fn audit_trail(").as_deref(),
Some("audit_trail")
);
assert_eq!(
fn_name(" pub(crate) fn setting(&self"),
Some("setting".to_owned())
);
let (p, d) = pairs(
"/// **`Admin.AuditTrail`** a\n/// **`Admin.GetSetting`** b\nasync fn get_setting(\n",
);
assert_eq!(p, vec![("AuditTrail".to_owned(), "get_setting".to_owned())]);
assert_eq!(d.len(), 1);
}