Krolyxon
Commit 39f4a39

Branches

Refs →

budctlcratesbudctl-coretestsguards.rs

155 lines · 5.0 KB · Rust

1//! Checks for the CLAUDE.md hard rules that the compiler and clippy cannot make on their own.
2//!
3//! - Rule 1: no network code in our sources, and no banned crate (deny.toml) in Cargo.lock.
4//! - Rule 3: every `Spec: <file>.md#<anchor>` comment points at an anchor that exists.
5//!
6//! The CLI's exit codes are checked in crates/budctl-cli/tests/cli.rs.
7
8// Test-only file: helpers may panic (clippy.toml exempts only #[test] functions themselves).
9#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
10
11use std::collections::BTreeSet;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15fn workspace() -> PathBuf {
16 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
17}
18
19/// Every `.rs` file under `crates/*/src`.
20fn sources() -> Vec<PathBuf> {
21 fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
22 for entry in fs::read_dir(dir).unwrap() {
23 let path = entry.unwrap().path();
24 if path.is_dir() {
25 walk(&path, out);
26 } else if path.extension().is_some_and(|e| e == "rs") {
27 out.push(path);
28 }
29 }
30 }
31 let mut files = Vec::new();
32 for krate in fs::read_dir(workspace().join("crates")).unwrap() {
33 let src = krate.unwrap().path().join("src");
34 if src.is_dir() {
35 walk(&src, &mut files);
36 }
37 }
38 assert!(!files.is_empty(), "found no sources to check");
39 files
40}
41
42#[test]
43fn no_network_code_in_sources() {
44 // Built from pieces so this file does not trip its own check.
45 let banned = [
46 ["std", "::net"].concat(),
47 ["Tcp", "Stream"].concat(),
48 ["Tcp", "Listener"].concat(),
49 ["Udp", "Socket"].concat(),
50 ["ToSocket", "Addrs"].concat(),
51 ["AF_", "INET"].concat(),
52 ];
53 let mut hits = Vec::new();
54 for file in sources() {
55 let text = fs::read_to_string(&file).unwrap();
56 for (n, line) in text.lines().enumerate() {
57 for word in &banned {
58 if line.contains(word.as_str()) {
59 hits.push(format!("{}:{}: {word}", file.display(), n + 1));
60 }
61 }
62 }
63 }
64 assert!(
65 hits.is_empty(),
66 "network code is not allowed (CLAUDE.md rule 1):\n{}",
67 hits.join("\n")
68 );
69}
70
71/// The crate names in deny.toml's `[bans] deny` list.
72fn banned_crates() -> BTreeSet<String> {
73 let text = fs::read_to_string(workspace().join("deny.toml")).unwrap();
74 let mut names = BTreeSet::new();
75 let mut in_bans = false;
76 for line in text.lines() {
77 let line = line.trim();
78 if line.starts_with('[') {
79 in_bans = line == "[bans]";
80 continue;
81 }
82 if !in_bans {
83 continue;
84 }
85 if let Some(rest) = line.strip_prefix("{ crate = \"") {
86 let name = rest.split('"').next().unwrap();
87 names.insert(name.to_owned());
88 }
89 }
90 assert!(
91 names.contains("reqwest"),
92 "could not read the [bans] deny list from deny.toml"
93 );
94 names
95}
96
97#[test]
98fn no_banned_crate_in_lockfile() {
99 let lock = fs::read_to_string(workspace().join("Cargo.lock")).unwrap();
100 let locked: BTreeSet<&str> = lock
101 .lines()
102 .filter_map(|l| l.strip_prefix("name = \""))
103 .filter_map(|l| l.strip_suffix('"'))
104 .collect();
105 assert!(
106 locked.contains("budctl-core"),
107 "could not read crate names from Cargo.lock"
108 );
109 let found: Vec<String> = banned_crates()
110 .into_iter()
111 .filter(|b| locked.contains(b.as_str()))
112 .collect();
113 assert!(
114 found.is_empty(),
115 "banned crates in Cargo.lock (see deny.toml): {found:?}"
116 );
117}
118
119#[test]
120fn spec_references_point_at_existing_anchors() {
121 let docs = workspace().join("docs/protocol");
122 let marker = "Spec: ";
123 let mut checked = 0;
124 let mut broken = Vec::new();
125 for file in sources() {
126 let text = fs::read_to_string(&file).unwrap();
127 for (n, line) in text.lines().enumerate() {
128 let mut rest = line;
129 while let Some(at) = rest.find(marker) {
130 rest = &rest[at + marker.len()..];
131 let reference: String = rest
132 .chars()
133 .take_while(|c| c.is_ascii_alphanumeric() || "._-#".contains(*c))
134 .collect();
135 let Some((doc, anchor)) = reference.split_once('#') else {
136 continue;
137 };
138 if !doc.ends_with(".md") {
139 continue;
140 }
141 checked += 1;
142 let spec = fs::read_to_string(docs.join(doc)).unwrap_or_default();
143 if !spec.contains(&format!("id=\"{anchor}\"")) {
144 broken.push(format!("{}:{}: {reference}", file.display(), n + 1));
145 }
146 }
147 }
148 }
149 assert!(checked > 0, "found no Spec: references at all");
150 assert!(
151 broken.is_empty(),
152 "Spec: references with no matching anchor:\n{}",
153 broken.join("\n")
154 );
155}