M1b: Brücke und Hülle #6

Merged
steven merged 21 commits from m1b-bruecke-und-huelle into main 2026-10-01 11:46:02 +00:00
3 changed files with 145 additions and 10 deletions
Showing only changes of commit 8573a68901 - Show all commits
+24
View File
@@ -0,0 +1,24 @@
# Paths the deterministic core must never use. Clippy resolves imports, so
# aliases and grouped imports such as `use std::{fs, env}` are caught too.
# The text search in core/tests/forbidden_apis.rs covers the rest.
disallowed-types = [
{ path = "std::collections::HashMap", reason = "iteration order differs between runs; use BTreeMap or Vec" },
{ path = "std::collections::HashSet", reason = "iteration order differs between runs; use BTreeSet or Vec" },
{ path = "std::hash::RandomState", reason = "seeded differently in every run" },
{ path = "std::hash::DefaultHasher", reason = "seeded differently in every run" },
{ path = "std::time::Instant", reason = "the core must not read the clock" },
{ path = "std::time::SystemTime", reason = "the core must not read the clock" },
]
disallowed-methods = [
{ path = "std::env::var", reason = "the core must not depend on the environment" },
{ path = "std::env::args", reason = "the core must not depend on the environment" },
{ path = "std::thread::spawn", reason = "the core must stay single-threaded" },
{ path = "std::fs::read", reason = "the core must not touch files" },
{ path = "std::fs::read_to_string", reason = "the core must not touch files" },
{ path = "std::fs::write", reason = "the core must not touch files" },
{ path = "std::fs::File::open", reason = "the core must not touch files" },
{ path = "std::fs::File::create", reason = "the core must not touch files" },
{ path = "std::process::exit", reason = "the core must not end the process" },
]
+54 -10
View File
@@ -1,5 +1,13 @@
//! Guards the lockstep rules: the core must not use anything whose result //! Guards the lockstep rules: the core must not use anything whose result
//! can differ between machines or between runs. //! can differ between machines or between runs.
//!
//! Clippy checks the same rules on resolved paths (see `core/clippy.toml`),
//! which also catches aliases and grouped imports. This text search adds
//! what Clippy cannot express, such as floating point types and global
//! mutable state, and it runs even when Clippy does not.
// Tests may read files. The rules of core/clippy.toml apply to core/src.
#![allow(clippy::disallowed_methods)]
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -29,9 +37,33 @@ const FORBIDDEN: &[(&str, &str)] = &[
"std::net", "std::net",
"the core must not open connections; the shell does that", "the core must not open connections; the shell does that",
), ),
(
"std::io",
"the core must not read or write streams; the shell does that",
),
("std::process", "the core must not start or end processes"),
("static mut", "global mutable state can differ between runs"),
(
"thread_local",
"global mutable state can differ between runs",
),
("Atomic", "global mutable state can differ between runs"),
("unsafe", "the core forbids unsafe code"), ("unsafe", "the core forbids unsafe code"),
]; ];
/// Every forbidden term in `text`, as (line number, term, reason).
fn violations(text: &str) -> Vec<(usize, &'static str, &'static str)> {
let mut found = Vec::new();
for (index, line) in text.lines().enumerate() {
for &(needle, reason) in FORBIDDEN {
if line.contains(needle) {
found.push((index + 1, needle, reason));
}
}
}
found
}
fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) { fn rust_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in fs::read_dir(dir).expect("source directory is readable") { for entry in fs::read_dir(dir).expect("source directory is readable") {
let path = entry.expect("directory entry is readable").path(); let path = entry.expect("directory entry is readable").path();
@@ -58,16 +90,8 @@ fn core_source_uses_no_forbidden_apis() {
let mut problems = Vec::new(); let mut problems = Vec::new();
for file in &files { for file in &files {
let text = fs::read_to_string(file).expect("source file is readable"); let text = fs::read_to_string(file).expect("source file is readable");
for (number, line) in text.lines().enumerate() { for (line, needle, reason) in violations(&text) {
for (needle, reason) in FORBIDDEN { problems.push(format!("{}:{line}: `{needle}`: {reason}", file.display()));
if line.contains(needle) {
problems.push(format!(
"{}:{}: `{needle}`: {reason}",
file.display(),
number + 1
));
}
}
} }
} }
assert!( assert!(
@@ -76,3 +100,23 @@ fn core_source_uses_no_forbidden_apis() {
problems.join("\n") problems.join("\n")
); );
} }
#[test]
fn every_forbidden_term_is_detected() {
for &(needle, _) in FORBIDDEN {
let text = format!("pub fn fine() {{}}\nlet x = {needle};\n");
let found = violations(&text);
assert!(
found
.iter()
.any(|&(line, term, _)| line == 2 && term == needle),
"`{needle}` was not detected: {found:?}"
);
}
}
#[test]
fn clean_code_has_no_violations() {
let text = "pub fn add(a: u32, b: u32) -> u32 {\n a.wrapping_add(b)\n}\n";
assert!(violations(text).is_empty());
}
+67
View File
@@ -0,0 +1,67 @@
//! The core builds from the standard library alone. A foreign crate could
//! bring floating point, hash maps or threads in through the back door.
// Tests may read files. The rules of core/clippy.toml apply to core/src.
#![allow(clippy::disallowed_methods)]
use std::fs;
use std::path::Path;
/// Every manifest section that may add a dependency, with the lines it
/// declares. Development dependencies are allowed, because they never reach
/// the game.
fn dependency_sections(manifest: &str) -> Vec<(String, Vec<String>)> {
let mut sections: Vec<(String, Vec<String>)> = Vec::new();
let mut inside = false;
for line in manifest.lines() {
let line = line.trim();
if line.starts_with('[') {
inside = line.contains("dependencies") && !line.contains("dev-dependencies");
if inside {
sections.push((line.to_owned(), Vec::new()));
}
continue;
}
if inside && !line.is_empty() && !line.starts_with('#') {
if let Some((_, entries)) = sections.last_mut() {
entries.push(line.to_owned());
}
}
}
sections
}
#[test]
fn the_core_has_no_dependencies() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let manifest = fs::read_to_string(&path).expect("the manifest is readable");
for (header, entries) in dependency_sections(&manifest) {
assert!(
!header.contains("dependencies."),
"the core declares a dependency table: {header}"
);
assert!(
entries.is_empty(),
"{header} of the core must stay empty, found: {entries:?}"
);
}
}
#[test]
fn dependency_sections_are_found_with_their_entries() {
let manifest = "[package]\nname = \"x\"\n\n[dependencies]\n# a comment\nserde = \"1\"\n\n\
[dev-dependencies]\nproptest = \"1\"\n\n[dependencies.rand]\nversion = \"0.8\"\n";
assert_eq!(
dependency_sections(manifest),
vec![
(
"[dependencies]".to_owned(),
vec!["serde = \"1\"".to_owned()]
),
(
"[dependencies.rand]".to_owned(),
vec!["version = \"0.8\"".to_owned()]
),
]
);
}