Krolyxon
Commit b9e5c00

Branches

Refs →

budctlcratesbudctl-coretestsguards.rs

181 lines · 5.8 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 a Linux build.
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};
14use std::process::Command;
15
16fn workspace() -> PathBuf {
17 Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
18}
19
20/// Every `.rs` file under `crates/*/src`.
21fn sources() -> Vec<PathBuf> {
22 fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
23 for entry in fs::read_dir(dir).unwrap() {
24 let path = entry.unwrap().path();
25 if path.is_dir() {
26 walk(&path, out);
27 } else if path.extension().is_some_and(|e| e == "rs") {
28 out.push(path);
29 }
30 }
31 }
32 let mut files = Vec::new();
33 for krate in fs::read_dir(workspace().join("crates")).unwrap() {
34 let src = krate.unwrap().path().join("src");
35 if src.is_dir() {
36 walk(&src, &mut files);
37 }
38 }
39 assert!(!files.is_empty(), "found no sources to check");
40 files
41}
42
43#[test]
44fn no_network_code_in_sources() {
45 // Built from pieces so this file does not trip its own check.
46 let banned = [
47 ["std", "::net"].concat(),
48 ["Tcp", "Stream"].concat(),
49 ["Tcp", "Listener"].concat(),
50 ["Udp", "Socket"].concat(),
51 ["ToSocket", "Addrs"].concat(),
52 ["AF_", "INET"].concat(),
53 ];
54 let mut hits = Vec::new();
55 for file in sources() {
56 let text = fs::read_to_string(&file).unwrap();
57 for (n, line) in text.lines().enumerate() {
58 for word in &banned {
59 if line.contains(word.as_str()) {
60 hits.push(format!("{}:{}: {word}", file.display(), n + 1));
61 }
62 }
63 }
64 }
65 assert!(
66 hits.is_empty(),
67 "network code is not allowed (CLAUDE.md rule 1):\n{}",
68 hits.join("\n")
69 );
70}
71
72/// The crate names in deny.toml's `[bans] deny` list.
73fn banned_crates() -> BTreeSet<String> {
74 let text = fs::read_to_string(workspace().join("deny.toml")).unwrap();
75 let mut names = BTreeSet::new();
76 let mut in_bans = false;
77 for line in text.lines() {
78 let line = line.trim();
79 if line.starts_with('[') {
80 in_bans = line == "[bans]";
81 continue;
82 }
83 if !in_bans {
84 continue;
85 }
86 if let Some(rest) = line.strip_prefix("{ crate = \"") {
87 let name = rest.split('"').next().unwrap();
88 names.insert(name.to_owned());
89 }
90 }
91 assert!(
92 names.contains("reqwest"),
93 "could not read the [bans] deny list from deny.toml"
94 );
95 names
96}
97
98/// The crates a Linux build of the workspace uses, tests included, for the targets deny.toml
99/// checks. Cargo.lock is no help here: it also lists crates that only other platforms build (iced's
100/// lock entries include tokio, for wasm).
101fn built_crates() -> BTreeSet<String> {
102 let out = Command::new(env!("CARGO"))
103 .current_dir(workspace())
104 .args(["tree", "--offline", "--locked", "--workspace", "--all-features"])
105 .args(["--edges", "normal,build,dev", "--prefix", "none", "--format", "{p}"])
106 .args([
107 "--target",
108 "x86_64-unknown-linux-gnu",
109 "--target",
110 "aarch64-unknown-linux-gnu",
111 ])
112 .output()
113 .unwrap();
114 assert!(
115 out.status.success(),
116 "cargo tree failed: {}",
117 String::from_utf8_lossy(&out.stderr)
118 );
119 let names: BTreeSet<String> = String::from_utf8(out.stdout)
120 .unwrap()
121 .lines()
122 .filter_map(|line| line.split_whitespace().next())
123 .map(str::to_owned)
124 .collect();
125 assert!(
126 names.contains("budctl-core") && names.contains("libc"),
127 "could not read crate names from cargo tree"
128 );
129 names
130}
131
132#[test]
133fn no_banned_crate_in_the_build() {
134 let built = built_crates();
135 let found: Vec<String> = banned_crates()
136 .into_iter()
137 .filter(|banned| built.contains(banned))
138 .collect();
139 assert!(
140 found.is_empty(),
141 "banned crates in the build (see deny.toml): {found:?}"
142 );
143}
144
145#[test]
146fn spec_references_point_at_existing_anchors() {
147 let docs = workspace().join("docs/protocol");
148 let marker = "Spec: ";
149 let mut checked = 0;
150 let mut broken = Vec::new();
151 for file in sources() {
152 let text = fs::read_to_string(&file).unwrap();
153 for (n, line) in text.lines().enumerate() {
154 let mut rest = line;
155 while let Some(at) = rest.find(marker) {
156 rest = &rest[at + marker.len()..];
157 let reference: String = rest
158 .chars()
159 .take_while(|c| c.is_ascii_alphanumeric() || "._-#".contains(*c))
160 .collect();
161 let Some((doc, anchor)) = reference.split_once('#') else {
162 continue;
163 };
164 if !doc.ends_with(".md") {
165 continue;
166 }
167 checked += 1;
168 let spec = fs::read_to_string(docs.join(doc)).unwrap_or_default();
169 if !spec.contains(&format!("id=\"{anchor}\"")) {
170 broken.push(format!("{}:{}: {reference}", file.display(), n + 1));
171 }
172 }
173 }
174 }
175 assert!(checked > 0, "found no Spec: references at all");
176 assert!(
177 broken.is_empty(),
178 "Spec: references with no matching anchor:\n{}",
179 broken.join("\n")
180 );
181}