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
|
//! 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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -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