M1b: Brücke und Hülle #6
@@ -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" },
|
||||
]
|
||||
@@ -1,5 +1,13 @@
|
||||
//! Guards the lockstep rules: the core must not use anything whose result
|
||||
//! 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::path::{Path, PathBuf};
|
||||
@@ -29,9 +37,33 @@ const FORBIDDEN: &[(&str, &str)] = &[
|
||||
"std::net",
|
||||
"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"),
|
||||
];
|
||||
|
||||
/// 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>) {
|
||||
for entry in fs::read_dir(dir).expect("source directory is readable") {
|
||||
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();
|
||||
for file in &files {
|
||||
let text = fs::read_to_string(file).expect("source file is readable");
|
||||
for (number, line) in text.lines().enumerate() {
|
||||
for (needle, reason) in FORBIDDEN {
|
||||
if line.contains(needle) {
|
||||
problems.push(format!(
|
||||
"{}:{}: `{needle}`: {reason}",
|
||||
file.display(),
|
||||
number + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
for (line, needle, reason) in violations(&text) {
|
||||
problems.push(format!("{}:{line}: `{needle}`: {reason}", file.display()));
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
@@ -76,3 +100,23 @@ fn core_source_uses_no_forbidden_apis() {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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()]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user