Skip to content

Commit 4d94837

Browse files
authored
[anodized-fmt] test: basic fuzzing (#141)
- Add test utils to fuzz `anodized-fmt`'s whitespace invariance. - Set up a simple fuzz target: `fmt_ws_invariant`. - Add a basic `fuzz/README.md`.
1 parent 187d5a8 commit 4d94837

8 files changed

Lines changed: 232 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ members = [
55
"crates/anodized-fmt",
66
"crates/anodized-logic",
77
"crates/anodized-macros",
8+
"crates/test-util",
9+
"fuzz",
810
]
911
resolver = "2"
1012

@@ -28,8 +30,10 @@ keywords = [
2830

2931
[workspace.dependencies]
3032
anodized-core = { version = "0.5.1", path = "crates/anodized-core" }
33+
anodized-fmt = { version = "0.5.1", path = "crates/anodized-fmt" }
3134
anodized-logic = { version = "0.5.1", path = "crates/anodized-logic" }
3235
anodized-macros = { version = "0.5.1", path = "crates/anodized-macros" }
36+
test-util = { path = "crates/test-util" }
3337
num-bigint = "0.4.6"
3438
proc-macro2 = { version = "1.0", features = ["span-locations"] }
3539
quote = "1.0"

crates/test-util/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "test-util"
3+
version = "0.0.0"
4+
publish = false
5+
edition = "2024"
6+
7+
[dependencies]
8+
arbitrary = { version = "1", features = ["derive"] }

crates/test-util/src/fmt.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//! This module contains utilities meant only for testing.
2+
//! Do not use outside of tests.
3+
4+
use arbitrary::Arbitrary;
5+
6+
/// A template to generate formatting variations of a piece of code.
7+
#[derive(Debug, Clone)]
8+
pub struct Template(Vec<Span>);
9+
10+
#[derive(Debug, Clone)]
11+
pub enum Span {
12+
/// A fixed string.
13+
F(String),
14+
/// Zero or more whitespace characters.
15+
Z,
16+
/// One or more whitespace characters.
17+
P,
18+
}
19+
20+
/// Describes a variation the template can generate.
21+
#[derive(Debug, Clone, Arbitrary)]
22+
pub struct Variation(Vec<Whitespace>);
23+
24+
impl Default for Variation {
25+
fn default() -> Self {
26+
Variation(vec![])
27+
}
28+
}
29+
30+
impl Template {
31+
/// New empty template.
32+
pub fn new() -> Self {
33+
Template(vec![])
34+
}
35+
36+
/// Add a placeholder for zero or more whitespace characters.
37+
pub fn z(mut self) -> Self {
38+
self.0.push(Span::Z);
39+
self
40+
}
41+
42+
/// Add a placeholder for one or more whitespace characters.
43+
pub fn p(mut self) -> Self {
44+
self.0.push(Span::P);
45+
self
46+
}
47+
48+
/// Add a fixed span of text.
49+
pub fn fixed(mut self: Self, text: &str) -> Self {
50+
self.0.push(Span::F(text.to_string()));
51+
self
52+
}
53+
54+
/// Add tokens, replacing each internal span of whitespace with a `.z()`.
55+
pub fn tokens(mut self: Self, text: &str) -> Self {
56+
for (i, non_whitespace) in text.split_whitespace().enumerate() {
57+
if i > 0 {
58+
self.0.push(Span::Z);
59+
}
60+
self.0.push(Span::F(non_whitespace.to_string()));
61+
}
62+
self
63+
}
64+
}
65+
66+
impl Template {
67+
/// Generates an instantiation of the template based on variation information.
68+
pub fn generate(&self, variation: Variation) -> String {
69+
let mut output = String::new();
70+
71+
let mut whitespaces = variation.0.into_iter();
72+
73+
for span in &self.0 {
74+
match span {
75+
Span::F(text) => output.push_str(text),
76+
Span::Z => {
77+
let whitespace = whitespaces.next().unwrap_or_default();
78+
output.push_str(&whitespace.to_string());
79+
}
80+
Span::P => {
81+
let whitespace = whitespaces.next().unwrap_or_default();
82+
output.push_str(&whitespace.to_non_empty_string());
83+
}
84+
}
85+
}
86+
87+
output
88+
}
89+
}
90+
91+
/// A non-empty sequence of whitespace characters.
92+
#[derive(Debug, Clone, Arbitrary)]
93+
pub struct Whitespace(Vec<WsChar>, WsChar);
94+
95+
impl Default for Whitespace {
96+
fn default() -> Self {
97+
Whitespace(vec![], WsChar::Space)
98+
}
99+
}
100+
101+
impl Whitespace {
102+
fn to_string(self: Whitespace) -> String {
103+
self.0.into_iter().map(|w| char::from(w)).collect()
104+
}
105+
106+
fn to_non_empty_string(self: Whitespace) -> String {
107+
self.0
108+
.into_iter()
109+
.chain(std::iter::once(self.1))
110+
.map(|w| char::from(w))
111+
.collect()
112+
}
113+
}
114+
115+
/// A code point Rust recognizes as whitespace.
116+
#[derive(Debug, Clone, Copy, Arbitrary)]
117+
pub enum WsChar {
118+
Space,
119+
Tab,
120+
Newline,
121+
// TODO: expand
122+
}
123+
124+
impl From<WsChar> for char {
125+
fn from(value: WsChar) -> Self {
126+
match value {
127+
WsChar::Space => ' ',
128+
WsChar::Tab => '\t',
129+
WsChar::Newline => '\n',
130+
}
131+
}
132+
}

crates/test-util/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod fmt;

fuzz/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
target
2+
corpus
3+
artifacts
4+
coverage

fuzz/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "fuzz"
3+
version = "0.0.0"
4+
publish = false
5+
edition = "2024"
6+
7+
[package.metadata]
8+
cargo-fuzz = true
9+
10+
[dependencies]
11+
anodized-fmt.workspace = true
12+
test-util.workspace = true
13+
14+
libfuzzer-sys = "0.4"
15+
pretty_assertions = "1"
16+
17+
[[bin]]
18+
name = "fmt_ws_invariant"
19+
path = "fuzz_targets/fmt_ws_invariant.rs"
20+
test = false
21+
doc = false
22+
bench = false

fuzz/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Fuzz Targets for Testing Anodized Components
2+
3+
## fmt_ws_invariant
4+
5+
Tests that spec formatting is invariant to whitespace changes.
6+
7+
**IMPORTANT**: Due to `proc-macro2` special-casing its behavior based on `cfg(fuzzing)` which `cargo-fuzz` sets by default, it must be run with the `--no-cfg-fuzzing` option as follows:
8+
9+
```sh
10+
cargo +nightly fuzz run --no-cfg-fuzzing fmt_ws_invariant
11+
```
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#![no_main]
2+
3+
use std::sync::OnceLock;
4+
5+
use libfuzzer_sys::fuzz_target;
6+
use pretty_assertions::assert_eq;
7+
8+
use anodized_fmt::{Config, format_file};
9+
use test_util::fmt::{Template, Variation};
10+
11+
static TEMPLATE: OnceLock<Template> = OnceLock::new();
12+
13+
/// Build a template for the following fragment:
14+
///
15+
/// #[spec(
16+
/// // precondition
17+
/// requires: x > 0,
18+
/// // postcondition
19+
/// ensures: *output > 0,
20+
/// )]
21+
/// fn func(x: i32) -> i32 { todo!() }
22+
///
23+
#[rustfmt::skip]
24+
fn make_template() -> Template {
25+
Template::new()
26+
.fixed("#[spec(\n")
27+
.z().fixed("// precondition\n")
28+
.z().tokens("requires : x > 0 ,").fixed("\n")
29+
.z().fixed("// postcondition\n")
30+
.z().tokens("ensures : * output > 0 ,").fixed("\n")
31+
.z().fixed(")]\n")
32+
.fixed("fn func(x: i32) -> i32 { todo!() }\n")
33+
}
34+
35+
fuzz_target!(
36+
init: {
37+
TEMPLATE.set(make_template()).unwrap();
38+
},
39+
|variation: Variation| {
40+
let template = TEMPLATE.get().unwrap();
41+
let default_input = template.generate(Variation::default());
42+
let variant_input = template.generate(variation);
43+
44+
let config = Config::default();
45+
let fmt_default = format_file(&default_input, &config).expect("formatting default");
46+
let fmt_variant = format_file(&variant_input, &config).expect("formatting variant");
47+
48+
assert_eq!(fmt_variant, fmt_default);
49+
}
50+
);

0 commit comments

Comments
 (0)