mind-body: types, forbid(unsafe_code) and pair-wise gate properties carry the Grant rule
Per ruling mind-launch-forge-scanner-bound: compile_fail doctests for the struct literal and each derive-provided path on Grant; forbid(unsafe_code) in the library; the gate property draws the mind's own instance from the same slug generator as the Grant's and tests pairs; Persona claim shapes are drawn and rotated by seed; the path-literal and static-keyword scanners are deleted from the public-surface test, whose limits are written at its top. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7f70bbda13
commit
8e8fb0d17d
5 changed files with 237 additions and 216 deletions
|
|
@ -131,6 +131,41 @@ pub enum Effective {
|
|||
/// `read_effective` makes one; `open_and_launch` takes it by value and refuses
|
||||
/// one read for another instance, so one reading opens at most one run, of the
|
||||
/// instance it was read for.
|
||||
///
|
||||
/// Outside this crate the type system leaves no way to make one: the fields are
|
||||
/// private, there is no public constructor, and the derives give no path (no
|
||||
/// `Default`, `Clone`, `Deserialize` or `From`). Each way is tried here and
|
||||
/// refused:
|
||||
///
|
||||
/// ```compile_fail,E0451
|
||||
/// use mind_body::control::Grant;
|
||||
/// // a struct literal
|
||||
/// let _ = Grant { instance: eureka_pipeline::Slug("eureka".into()), cadence: std::time::Duration::ZERO, run_cap_usd: rust_decimal::Decimal::ONE };
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// // not Default
|
||||
/// let _: mind_body::control::Grant = Default::default();
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0599
|
||||
/// // not Clone, so a reading cannot be copied into a second run
|
||||
/// fn copy(grant: &mind_body::control::Grant) -> mind_body::control::Grant {
|
||||
/// grant.clone()
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// // not Deserialize
|
||||
/// fn read<T: serde::de::DeserializeOwned>() {}
|
||||
/// read::<mind_body::control::Grant>();
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail,E0277
|
||||
/// // not From its parts
|
||||
/// use mind_body::control::Grant;
|
||||
/// let _ = Grant::from((eureka_pipeline::Slug("eureka".into()), std::time::Duration::ZERO, rust_decimal::Decimal::ONE));
|
||||
/// ```
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Grant {
|
||||
instance: Slug,
|
||||
|
|
@ -253,12 +288,6 @@ pub fn load_state(source: &impl ControlSource) -> Result<ControlState> {
|
|||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use mind_body::control::Grant;
|
||||
/// // a Grant cannot be written out
|
||||
/// let _ = Grant { instance: eureka_pipeline::Slug("eureka".into()), cadence: std::time::Duration::ZERO, run_cap_usd: rust_decimal::Decimal::ONE };
|
||||
/// ```
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// // and the test constructor is not in the library build
|
||||
/// let _ = mind_body::control::Grant::for_test;
|
||||
/// ```
|
||||
|
|
|
|||
|
|
@ -371,11 +371,10 @@ fn breaker_question(item: &PipelineRef, label: &Label, now: DateTime<Utc>) -> Pi
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
use eureka_pipeline::PipelineKind as K;
|
||||
use huginn_mind::{PipelineAdmissionOutcome as Outcome, PipelineStatus};
|
||||
use huginn_mind::{PipelineAdmissionOutcome as Outcome, PipelineSelectionPage, PipelineStatus};
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
use super::*;
|
||||
|
|
@ -847,155 +846,206 @@ mod tests {
|
|||
assert_eq!((rig.runs(), rig.started()), (3, 0));
|
||||
}
|
||||
|
||||
/// Every name that differs from `name` and is shaped to pass a compare weaker
|
||||
/// than equality. The set is generated from the real name, not listed: every
|
||||
/// single-character substitution (over ASCII and some wide characters), deletion
|
||||
/// and insertion; every transposition; every permutation; rotations; every
|
||||
/// prefix and suffix; case flips; whitespace and NUL padding at either end;
|
||||
/// repetitions past any plausible length bound; and seeded random names of the
|
||||
/// same length and of other lengths. A compare that agrees with equality on all
|
||||
/// of them, and so passes the gate test, is equality for every name a caller
|
||||
/// can plausibly build.
|
||||
fn near_misses(name: &str) -> Vec<String> {
|
||||
/// The one source of every drawn case: splitmix64 from a fixed seed, so a
|
||||
/// failure names a case that runs again.
|
||||
struct Draw(u64);
|
||||
|
||||
impl Draw {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||||
let mut z = self.0;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
fn below(&mut self, bound: usize) -> usize {
|
||||
(self.next() % bound as u64) as usize
|
||||
}
|
||||
}
|
||||
|
||||
const LENGTH_EDGES: [usize; 13] = [1, 2, 3, 8, 9, 15, 16, 17, 31, 32, 33, 63, 64];
|
||||
const ALPHABETS: [&str; 6] = [
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789",
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
||||
"ab-_",
|
||||
"a-b_c-d_e0",
|
||||
"abcdefghijklmnopqrstuvwxyz-_",
|
||||
];
|
||||
|
||||
/// A valid slug of `length` characters, over an alphabet the draw picks, with
|
||||
/// both separators (`-`, `_`) mixed in at random positions.
|
||||
fn slug_of(draw: &mut Draw, length: usize) -> String {
|
||||
loop {
|
||||
let alphabet: Vec<char> = ALPHABETS[draw.below(ALPHABETS.len())].chars().collect();
|
||||
let name: String = (0..length)
|
||||
.map(|_| match draw.below(6) {
|
||||
0 => '-',
|
||||
1 => '_',
|
||||
_ => alphabet[draw.below(alphabet.len())],
|
||||
})
|
||||
.collect();
|
||||
if Slug(name.clone()).validate_slug().is_ok() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Valid slugs that differ from `name`: edits of it at every position (a
|
||||
/// drawn character, the other separator, the other case), its prefixes, the
|
||||
/// same name with every separator swapped, and independent draws.
|
||||
fn distinct_valid(name: &str, draw: &mut Draw) -> Vec<String> {
|
||||
let chars: Vec<char> = name.chars().collect();
|
||||
let wide = ['é', 'ÿ', '\u{ffff}', '😀'];
|
||||
let alphabet: Vec<char> = (0u8..128).map(char::from).chain(wide).collect();
|
||||
let text = |parts: &[char]| parts.iter().collect::<String>();
|
||||
let mut out: BTreeSet<String> = BTreeSet::new();
|
||||
for i in 0..chars.len() {
|
||||
for &c in &alphabet {
|
||||
let mut changed = chars.clone();
|
||||
changed[i] = c;
|
||||
out.insert(text(&changed));
|
||||
}
|
||||
let mut deleted = chars.clone();
|
||||
deleted.remove(i);
|
||||
out.insert(text(&deleted));
|
||||
for j in i + 1..chars.len() {
|
||||
let mut swapped = chars.clone();
|
||||
swapped.swap(i, j);
|
||||
out.insert(text(&swapped));
|
||||
}
|
||||
out.insert(text(&chars[..i]));
|
||||
out.insert(text(&chars[i + 1..]));
|
||||
out.insert(text(&[&chars[i..], &chars[..i]].concat()));
|
||||
let mut flipped = chars.clone();
|
||||
flipped[i] = if chars[i].is_uppercase() { chars[i].to_ascii_lowercase() } else { chars[i].to_ascii_uppercase() };
|
||||
out.insert(text(&flipped));
|
||||
}
|
||||
for i in 0..=chars.len() {
|
||||
for &c in &alphabet {
|
||||
let mut grown = chars.clone();
|
||||
grown.insert(i, c);
|
||||
out.insert(text(&grown));
|
||||
}
|
||||
}
|
||||
for pad in ['\0', ' ', '\n', '\t', '\u{a0}'] {
|
||||
for count in 1..=4 {
|
||||
out.insert(format!("{name}{}", pad.to_string().repeat(count)));
|
||||
out.insert(format!("{}{name}", pad.to_string().repeat(count)));
|
||||
}
|
||||
for width in [32, 64, 255] {
|
||||
out.insert(format!("{name}{}", pad.to_string().repeat(width - chars.len())));
|
||||
}
|
||||
}
|
||||
for copies in [2, 3, 8, 40, 400, 4000] {
|
||||
out.insert(name.repeat(copies));
|
||||
}
|
||||
out.insert(name.to_uppercase());
|
||||
out.insert(name.to_lowercase());
|
||||
// Every permutation (Heap's algorithm).
|
||||
let mut order = chars.clone();
|
||||
let mut counters = vec![0usize; order.len()];
|
||||
out.insert(text(&order));
|
||||
let mut at = 0;
|
||||
while at < order.len() {
|
||||
if counters[at] < at {
|
||||
order.swap(if at % 2 == 0 { 0 } else { counters[at] }, at);
|
||||
out.insert(text(&order));
|
||||
counters[at] += 1;
|
||||
at = 0;
|
||||
} else {
|
||||
counters[at] = 0;
|
||||
at += 1;
|
||||
}
|
||||
}
|
||||
// Seeded random names: same length over the name's own alphabet, over all of
|
||||
// ASCII, and of any length up to 300 over ASCII.
|
||||
let mut state = 0x9e37_79b9_7f4a_7c15_u64;
|
||||
let mut next = move || {
|
||||
state ^= state << 13;
|
||||
state ^= state >> 7;
|
||||
state ^= state << 17;
|
||||
state
|
||||
let pool: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_".chars().collect();
|
||||
let swapped = |c: char| match c {
|
||||
'-' => '_',
|
||||
'_' => '-',
|
||||
other => other,
|
||||
};
|
||||
for _ in 0..4096 {
|
||||
out.insert((0..chars.len()).map(|_| chars[(next() % chars.len() as u64) as usize]).collect());
|
||||
out.insert((0..chars.len()).map(|_| char::from((next() % 128) as u8)).collect());
|
||||
let mut out = Vec::new();
|
||||
for i in 0..chars.len() {
|
||||
let mut edited = chars.clone();
|
||||
edited[i] = swapped(chars[i]);
|
||||
out.push(edited.iter().collect::<String>());
|
||||
let mut edited = chars.clone();
|
||||
edited[i] = if chars[i].is_ascii_uppercase() { chars[i].to_ascii_lowercase() } else { chars[i].to_ascii_uppercase() };
|
||||
out.push(edited.iter().collect::<String>());
|
||||
for _ in 0..3 {
|
||||
let mut edited = chars.clone();
|
||||
edited[i] = pool[draw.below(pool.len())];
|
||||
out.push(edited.iter().collect::<String>());
|
||||
}
|
||||
out.push(chars[..i].iter().collect::<String>());
|
||||
}
|
||||
for _ in 0..1024 {
|
||||
let length = (next() % 301) as usize;
|
||||
out.insert((0..length).map(|_| char::from((next() % 128) as u8)).collect());
|
||||
out.push(chars.iter().map(|&c| swapped(c)).collect::<String>());
|
||||
out.push(name.to_uppercase());
|
||||
out.push(name.to_lowercase());
|
||||
if chars.len() < 64 {
|
||||
out.push(format!("{name}-"));
|
||||
out.push(format!("{name}_"));
|
||||
out.push(format!("{name}0"));
|
||||
}
|
||||
out.remove(name);
|
||||
out.into_iter().collect()
|
||||
for _ in 0..8 {
|
||||
let length = 1 + draw.below(64);
|
||||
out.push(slug_of(draw, length));
|
||||
}
|
||||
out.retain(|other| other != name && Slug(other.clone()).validate_slug().is_ok());
|
||||
out
|
||||
}
|
||||
|
||||
/// A mind that knows only its instance. Any read or write is an error that
|
||||
/// says so and is counted, so a gate that lets a launch through is seen by
|
||||
/// the error it gets back, and a gate that refuses is seen to have touched
|
||||
/// nothing.
|
||||
struct Named {
|
||||
instance: Slug,
|
||||
reads: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Named {
|
||||
fn new(instance: &str) -> Self {
|
||||
Self { instance: Slug(instance.into()), reads: Cell::new(0) }
|
||||
}
|
||||
}
|
||||
|
||||
impl MindPort for Named {
|
||||
fn instance(&self) -> &Slug {
|
||||
&self.instance
|
||||
}
|
||||
|
||||
fn query(&self, _: &Selection) -> Result<PipelineSelectionPage> {
|
||||
self.reads.set(self.reads.get() + 1);
|
||||
Err(anyhow!("reached the mind"))
|
||||
}
|
||||
|
||||
fn admit(&self, _: &str, _: &str, _: Vec<PipelineDocument>) -> Result<Outcome> {
|
||||
self.reads.set(self.reads.get() + 1);
|
||||
Err(anyhow!("reached the mind"))
|
||||
}
|
||||
}
|
||||
|
||||
/// One launch by `turn` on a mind named `mind`, holding a Grant read for `grant`.
|
||||
fn gated(mind: &Named, grant: &str, turn: RunTurn, claims: Vec<PipelineRef>) -> Result<LaunchOutcome> {
|
||||
let (launcher, clock) = (Recording::default(), Fixed(Cell::new(at(0))));
|
||||
let ports = Ports { mind, launcher: &launcher, clock: &clock, host: "yggdrasil-host" };
|
||||
let grant = Grant::for_test(Slug(grant.into()), StdDuration::from_secs(60), five());
|
||||
let outcome = open_and_launch(&ports, LaunchRequest { turn, claims, agent: "agent-x".into(), grant });
|
||||
assert!(launcher.started.borrow().is_empty());
|
||||
outcome
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_grant_read_for_any_other_instance_opens_nothing_and_starts_nothing() {
|
||||
let rig = Rig::new();
|
||||
rig.mind.committed(vec![spec("x")]);
|
||||
let forged = near_misses(INSTANCE);
|
||||
// The generator itself: wide enough to carry each family, and the mind's own name is not in it.
|
||||
assert!(forged.len() > 100_000, "{}", forged.len());
|
||||
for member in [format!("{}k", &INSTANCE[..INSTANCE.len() - 1]), INSTANCE.to_uppercase(), format!("{INSTANCE}\0"), INSTANCE.chars().rev().collect::<String>()] {
|
||||
assert!(forged.contains(&member), "{member:?}");
|
||||
fn a_grant_passes_for_exactly_the_minds_own_instance_and_for_no_other_valid_slug() {
|
||||
let mut draw = Draw(0x6a09_e667_f3bc_c908);
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
for i in 0..780 {
|
||||
names.push(slug_of(&mut draw, LENGTH_EDGES[i % LENGTH_EDGES.len()]));
|
||||
}
|
||||
assert!(!forged.iter().any(|name| name == INSTANCE));
|
||||
for name in &forged {
|
||||
for _ in 0..220 {
|
||||
let length = 1 + draw.below(64);
|
||||
names.push(slug_of(&mut draw, length));
|
||||
}
|
||||
let mut distinct = 0;
|
||||
for name in &names {
|
||||
for turn in [RunTurn::SelfRun, RunTurn::PersonaTurn] {
|
||||
let claims = if turn == RunTurn::SelfRun { vec![spec_ref("x")] } else { vec![] };
|
||||
let ports = Ports { mind: &rig.mind, launcher: &rig.launcher, clock: &rig.clock, host: "yggdrasil-host" };
|
||||
let grant = Grant::for_test(Slug(name.clone()), StdDuration::from_secs(60), five());
|
||||
let outcome = open_and_launch(&ports, LaunchRequest { turn, claims, agent: "agent-x".into(), grant }).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "{name:?} {turn:?}");
|
||||
let mind = Named::new(name);
|
||||
let through = gated(&mind, name, turn, claims.clone()).unwrap_err();
|
||||
assert_eq!(format!("{through:#}"), "reached the mind", "{name:?} {turn:?}: the mind's own Grant passes the gate");
|
||||
for other in distinct_valid(name, &mut draw) {
|
||||
let mind = Named::new(name);
|
||||
let outcome = gated(&mind, &other, turn, claims.clone()).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::GrantForOtherInstance), "mind {name:?}, grant {other:?}, {turn:?}");
|
||||
assert_eq!(mind.reads.get(), 0, "a refused launch reads nothing");
|
||||
distinct += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!((rig.runs(), rig.started()), (0, 0));
|
||||
assert!(matches!(rig.launch(RunTurn::SelfRun, &[spec_ref("x")], five()), LaunchOutcome::Launched { .. }), "the mind's own Grant launches");
|
||||
assert!(distinct > 100_000, "{distinct}");
|
||||
// The families the property exists for are in the draw: long names, and both separators.
|
||||
assert!(names.iter().any(|n| n.len() > 32) && names.iter().any(|n| n.contains('-')) && names.iter().any(|n| n.contains('_')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_persona_turn_with_any_claims_at_all_is_refused_and_opens_nothing() {
|
||||
let rig = Rig::new();
|
||||
rig.mind.committed(vec![spec("x"), spec("blocked")]);
|
||||
rig.mind.committed(vec![question_in("fork", &spec_ref("blocked"))]);
|
||||
fn a_persona_turn_with_any_claims_at_all_is_refused_and_touches_nothing() {
|
||||
let kinds = PipelineKind::ALL;
|
||||
let refused = |claims: &[PipelineRef]| {
|
||||
let outcome = rig.launch(RunTurn::PersonaTurn, claims, five());
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::PersonaRunTakesNoClaims), "{} claims", claims.len());
|
||||
};
|
||||
// Claim sets of every size from one to well past the number of kinds, built four ways:
|
||||
// one queued spec repeated (duplicates), queued and blocked specs alternating, every kind
|
||||
// in turn over one id, and every kind in turn over distinct ids.
|
||||
for size in 1..=300 {
|
||||
refused(&(0..size).map(|_| spec_ref("x")).collect::<Vec<_>>());
|
||||
refused(&(0..size).map(|i| spec_ref(if i % 2 == 0 { "x" } else { "blocked" })).collect::<Vec<_>>());
|
||||
refused(&(0..size).map(|i| reference(kinds[i % kinds.len()], "no-such-document")).collect::<Vec<_>>());
|
||||
refused(&(0..size).map(|i| reference(kinds[i % kinds.len()], &format!("no-such-document-{i}"))).collect::<Vec<_>>());
|
||||
}
|
||||
// And sets far past any bound a refusal could plausibly be written with.
|
||||
for size in [1_000, 10_000, 100_000] {
|
||||
refused(&(0..size).map(|i| reference(kinds[i % kinds.len()], &format!("no-such-document-{i}"))).collect::<Vec<_>>());
|
||||
}
|
||||
// And every kind alone, repeated, whether or not the document exists.
|
||||
for kind in kinds {
|
||||
let mut draw = Draw(0xbb67_ae85_84ca_a73b);
|
||||
let mut sets: Vec<Vec<PipelineRef>> = Vec::new();
|
||||
// Every rotation of the kinds, so no kind or position is the one that starts, at every small size.
|
||||
for start in 0..kinds.len() {
|
||||
for size in 1..=40 {
|
||||
refused(&(0..size).map(|_| reference(*kind, "no-such-document")).collect::<Vec<_>>());
|
||||
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], &format!("doc-{i}"))).collect());
|
||||
sets.push((0..size).map(|i| reference(kinds[(start + i) % kinds.len()], "doc")).collect());
|
||||
}
|
||||
}
|
||||
assert_eq!((rig.runs(), rig.started()), (0, 0));
|
||||
assert!(matches!(rig.launch(RunTurn::PersonaTurn, &[], five()), LaunchOutcome::Launched { .. }));
|
||||
// Every kind alone, repeated.
|
||||
for kind in kinds {
|
||||
for size in 1..=40 {
|
||||
sets.push((0..size).map(|_| reference(*kind, "doc")).collect());
|
||||
}
|
||||
}
|
||||
// Drawn: every position a kind and an id from a pool of any size (one id repeated, to all distinct),
|
||||
// at sizes from one to well past any bound a refusal could be written with.
|
||||
let sizes = [1, 2, 3, 4, 5, 12, 13, 14, 26, 27, 64, 65, 100, 255, 256, 300];
|
||||
for round in 0..3_000 {
|
||||
let size = if round < 600 { sizes[round % sizes.len()] } else { 1 + draw.below(300) };
|
||||
let pool = 1 + draw.below(size);
|
||||
sets.push((0..size).map(|_| reference(kinds[draw.below(kinds.len())], &format!("doc-{}", draw.below(pool)))).collect());
|
||||
}
|
||||
for size in [1_000, 10_000, 100_000] {
|
||||
sets.push((0..size).map(|i| reference(kinds[draw.below(kinds.len())], &format!("doc-{i}"))).collect());
|
||||
}
|
||||
for claims in sets {
|
||||
let name = slug_of(&mut draw, LENGTH_EDGES[draw.below(LENGTH_EDGES.len())]);
|
||||
let mind = Named::new(&name);
|
||||
let outcome = gated(&mind, &name, RunTurn::PersonaTurn, claims.clone()).unwrap();
|
||||
assert_eq!(outcome, LaunchOutcome::Refused(Declined::PersonaRunTakesNoClaims), "{} claims, first {:?}", claims.len(), claims.first());
|
||||
assert_eq!(mind.reads.get(), 0);
|
||||
}
|
||||
// Without claims the same launch goes on to the mind.
|
||||
let mind = Named::new("yggdrasil");
|
||||
assert_eq!(format!("{:#}", gated(&mind, "yggdrasil", RunTurn::PersonaTurn, vec![]).unwrap_err()), "reached the mind");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@
|
|||
|
||||
// The control root is a constant, so the library reads no part of the process
|
||||
// environment (clippy.toml lists the readers); `forbid` keeps an `allow` from
|
||||
// putting one back.
|
||||
// putting one back. `forbid(unsafe_code)` closes the readers the list cannot
|
||||
// name: a foreign `getenv` or any other FFI call needs `unsafe`. The rest of the
|
||||
// Grant rule is ruling `eureka-body:ruling:mind-launch-forge-scanner-bound`.
|
||||
#![forbid(clippy::disallowed_methods)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod cli;
|
||||
pub mod control;
|
||||
|
|
|
|||
|
|
@ -1,27 +1,29 @@
|
|||
//! The library's public surface, pinned as a whole.
|
||||
//! The library's public surface, pinned as a tripwire.
|
||||
//!
|
||||
//! The brake's rule is that no caller can name a root, a source or a way to make
|
||||
//! a `Grant`. A test that names the routes it knows (`with_root`, `TEST_ROOT`)
|
||||
//! passes on every route it does not name. This one reads the library's source
|
||||
//! the way a consumer sees it and compares everything a consumer could reach to
|
||||
//! `public_surface.txt`: every `pub` item with its signature, every `pub` field,
|
||||
//! the derives and the header of every impl, every string literal that looks like
|
||||
//! a path, and every `static` keyword (a process-global is the one channel
|
||||
//! through which an existing public function's body could carry a root). A new
|
||||
//! `pub static`, a new `pub fn` under any name, a `Clone` or `Default` derived
|
||||
//! onto `Grant`, a root read from a file other than the constant: each is a line
|
||||
//! this file does not have, and the test fails.
|
||||
//! This reads the library's source the way a consumer sees it and compares
|
||||
//! every `pub` item with its signature, every `pub` field, the derives and the
|
||||
//! header of every impl, to `public_surface.txt`. A new `pub fn` under any name,
|
||||
//! a `pub static`, a `Clone` or `Default` derived onto `Grant`, a new way to
|
||||
//! reach the brake from outside: each is a line the snapshot does not have, and
|
||||
//! the test fails. A deliberate change is a change to `public_surface.txt` in the
|
||||
//! same commit, where a reviewer sees it; the failure prints the full current
|
||||
//! surface to copy from.
|
||||
//!
|
||||
//! Items under `#[cfg(test)]` are not the library and are skipped; `cfg(not(test))`
|
||||
//! and the `unix`/`windows` splits are the library and are read. Any other `cfg`, a `#[path]`, an item macro, a
|
||||
//! `macro_rules!`, an `extern` block or an exported symbol attribute would hide
|
||||
//! an item from this reader, so each fails the test outright. A seam that is
|
||||
//! `pub(crate)` and not `cfg(test)` is outside this reader's concern and inside
|
||||
//! clippy's: it is dead code in the library build, and `-D warnings` refuses it.
|
||||
//! What it sees: items reached through `mod` declarations and module files,
|
||||
//! `cfg(test)` skipped (a test-only item is not the library), the `unix` and
|
||||
//! `windows` splits read on both sides. An item macro, `macro_rules!`, `extern`
|
||||
//! block, `#[path]`, an exported-symbol attribute or any other `cfg` fails the
|
||||
//! test outright, because each would hide an item from this reader.
|
||||
//!
|
||||
//! A deliberate change to the surface is a change to `public_surface.txt` in the
|
||||
//! same commit, which is where a reviewer sees it. The failure prints the full
|
||||
//! current surface to copy from.
|
||||
//! What it cannot see, by design: items nested in function or `const _` bodies
|
||||
//! (an `impl Grant` inside a const block), and what a function body does. It
|
||||
//! proves nothing about how the control root is derived, and it does not defend
|
||||
//! the crate against edits to its own source. That bound is ruling
|
||||
//! `eureka-body:ruling:mind-launch-forge-scanner-bound`: the Grant is held by the
|
||||
//! type system (private fields, no constructor, the `compile_fail` examples on
|
||||
//! `Grant`), unsafe code is forbidden in the library, and changes to
|
||||
//! `control.rs` and `launch.rs` are defended by review and by the root-owned
|
||||
//! brake check at the unit. Do not extend this file into a walker of bodies.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::fs;
|
||||
|
|
@ -72,40 +74,6 @@ fn text(node: impl ToTokens) -> String {
|
|||
without_docs(node.to_token_stream()).to_string()
|
||||
}
|
||||
|
||||
/// The string literals in `stream` that look like a path.
|
||||
fn path_literals(stream: TokenStream, out: &mut BTreeSet<String>) {
|
||||
for token in stream {
|
||||
match token {
|
||||
TokenTree::Group(group) => path_literals(group.stream(), out),
|
||||
TokenTree::Literal(literal) => {
|
||||
let rendered = literal.to_string();
|
||||
if rendered.starts_with('"') && rendered.contains('/') {
|
||||
out.insert(format!("path-literal {rendered}"));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many `static` items or declarations `stream` holds (a `'static` lifetime is not one).
|
||||
/// A process-global is the one way an existing public function's body could carry a
|
||||
/// root from one call to another, so every one in the library is in the snapshot.
|
||||
fn static_keywords(stream: TokenStream) -> usize {
|
||||
let mut count = 0;
|
||||
let mut after_tick = false;
|
||||
for token in stream {
|
||||
let is_tick = matches!(&token, TokenTree::Punct(p) if p.as_char() == '\'');
|
||||
match token {
|
||||
TokenTree::Group(group) => count += static_keywords(group.stream()),
|
||||
TokenTree::Ident(name) if name == "static" && !after_tick => count += 1,
|
||||
_ => {}
|
||||
}
|
||||
after_tick = is_tick;
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn attributes(item: &Item) -> &[Attribute] {
|
||||
match item {
|
||||
Item::Const(i) => &i.attrs,
|
||||
|
|
@ -157,32 +125,11 @@ fn derives(attrs: &[Attribute]) -> String {
|
|||
attrs.iter().filter(|a| a.path().is_ident("derive") || a.path().is_ident("repr")).map(text).collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn label(item: &Item) -> String {
|
||||
match item {
|
||||
Item::Fn(i) => i.sig.ident.to_string(),
|
||||
Item::Const(i) => i.ident.to_string(),
|
||||
Item::Static(i) => i.ident.to_string(),
|
||||
Item::Struct(i) => i.ident.to_string(),
|
||||
Item::Enum(i) => i.ident.to_string(),
|
||||
Item::Trait(i) => i.ident.to_string(),
|
||||
Item::Type(i) => i.ident.to_string(),
|
||||
_ => "an item".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn walk(items: &[Item], path: &str, dir: &Path, out: &mut BTreeSet<String>) {
|
||||
for item in items {
|
||||
if !in_library(attributes(item), path) {
|
||||
continue;
|
||||
}
|
||||
if !matches!(item, Item::Impl(_) | Item::Mod(_)) {
|
||||
let stream = without_docs(item.to_token_stream());
|
||||
path_literals(stream.clone(), out);
|
||||
let statics = static_keywords(stream);
|
||||
if statics > 0 {
|
||||
out.insert(format!("{path}: {statics} static keyword(s) in {}", label(item)));
|
||||
}
|
||||
}
|
||||
match item {
|
||||
Item::Fn(f) if is_pub(&f.vis) => {
|
||||
out.insert(format!("{path}: {}", text(&f.sig)));
|
||||
|
|
@ -220,7 +167,6 @@ fn walk(items: &[Item], path: &str, dir: &Path, out: &mut BTreeSet<String>) {
|
|||
None => format!("impl{} {}", text(&i.generics), text(&i.self_ty)),
|
||||
};
|
||||
out.insert(format!("{path}: {header}"));
|
||||
path_literals(without_docs(i.generics.to_token_stream()), out);
|
||||
for member in &i.items {
|
||||
let attrs = match member {
|
||||
ImplItem::Const(m) => &m.attrs,
|
||||
|
|
@ -232,12 +178,6 @@ fn walk(items: &[Item], path: &str, dir: &Path, out: &mut BTreeSet<String>) {
|
|||
if !in_library(attrs, path) {
|
||||
continue;
|
||||
}
|
||||
let stream = without_docs(member.to_token_stream());
|
||||
path_literals(stream.clone(), out);
|
||||
let statics = static_keywords(stream);
|
||||
if statics > 0 {
|
||||
out.insert(format!("{path}: {statics} static keyword(s) in a member of impl {}", text(&i.self_ty)));
|
||||
}
|
||||
if i.trait_.is_none() {
|
||||
match member {
|
||||
ImplItem::Fn(f) if is_pub(&f.vis) => {
|
||||
|
|
@ -308,7 +248,7 @@ fn the_librarys_public_surface_is_the_committed_one() {
|
|||
fn the_reader_sees_what_it_claims_to() {
|
||||
// The snapshot must hold the brake's own names, or the reader is blind to them.
|
||||
let actual = surface();
|
||||
for needle in ["mind_body::control: fn read_effective", "mind_body::control: struct Grant", "path-literal \"/etc/gamecult/minds\""] {
|
||||
for needle in ["mind_body::control: fn read_effective", "mind_body::control: struct Grant"] {
|
||||
assert!(actual.iter().any(|line| line.contains(needle)), "{needle} is not in the surface");
|
||||
}
|
||||
// And nothing under cfg(test) leaked in.
|
||||
|
|
|
|||
|
|
@ -69,4 +69,3 @@ mind_body::queue: impl MindPort for HuginnMind < '_ >
|
|||
mind_body::queue: impl< 'a > HuginnMind < 'a >
|
||||
mind_body::queue: pub trait MindPort { fn instance (& self) -> & Slug ; fn query (& self , selection : & Selection) -> Result < PipelineSelectionPage > ; fn admit (& self , agent : & str , session : & str , documents : Vec < PipelineDocument >) -> Result < PipelineAdmissionOutcome > ; }
|
||||
mind_body::queue: struct HuginnMind< 'a > pub-fields[]
|
||||
path-literal "/etc/gamecult/minds"
|
||||
|
|
|
|||
Loading…
Reference in a new issue