syntax-symphony 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {syntax_symphony-0.2.0/src/syntax_symphony.egg-info → syntax_symphony-0.3.0}/PKG-INFO +10 -4
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/README.md +9 -3
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/pyproject.toml +1 -1
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/cli.py +16 -4
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/derivation_tree.py +4 -1
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/fuzzer.py +10 -3
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/grammar.py +6 -6
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0/src/syntax_symphony.egg-info}/PKG-INFO +10 -4
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/SOURCES.txt +2 -1
- syntax_symphony-0.3.0/tests/test_fuzzer.py +230 -0
- syntax_symphony-0.3.0/tests/test_validation.py +78 -0
- syntax_symphony-0.2.0/tests/test_fuzzer.py +0 -21
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/LICENSE +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/setup.cfg +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/__init__.py +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/py.typed +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/dependency_links.txt +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/entry_points.txt +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/requires.txt +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/top_level.txt +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_cli.py +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_derivation_tree.py +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_grammar.py +0 -0
- {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_public_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syntax_symphony
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Efficient grammar-based fuzzer.
|
|
5
5
|
Author-email: Stanimir Iglev <iglev.stanimir@gmail.com>
|
|
6
6
|
Maintainer-email: Stanimir Iglev <iglev.stanimir@gmail.com>
|
|
@@ -129,11 +129,14 @@ ssfuzz -g examples/expr_grammar.json -c 100 --start begin
|
|
|
129
129
|
|
|
130
130
|
# Set the file extension
|
|
131
131
|
ssfuzz -g examples/expr_grammar.json -c 100 -e json
|
|
132
|
+
|
|
133
|
+
# Reproduce the same output sequence across runs
|
|
134
|
+
ssfuzz -g examples/expr_grammar.json -c 100 --seed 42
|
|
132
135
|
```
|
|
133
136
|
|
|
134
137
|
### Full syntax:
|
|
135
138
|
```
|
|
136
|
-
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER]
|
|
139
|
+
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER] [--seed NUMBER]
|
|
137
140
|
|
|
138
141
|
Syntax Symphony Fuzzer
|
|
139
142
|
|
|
@@ -152,6 +155,7 @@ options:
|
|
|
152
155
|
--min-depth NUMBER Minimum depth for the derivation trees. Default: 1
|
|
153
156
|
-k NUMBER, --kcov NUMBER
|
|
154
157
|
Number of strings to generate for k-cov. Default: 1
|
|
158
|
+
--seed NUMBER Random seed for reproducible fuzzing. Default: non-deterministic
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
## API
|
|
@@ -177,8 +181,8 @@ grammar = Grammar({
|
|
|
177
181
|
"<number>": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
178
182
|
})
|
|
179
183
|
|
|
180
|
-
# Create the fuzzer
|
|
181
|
-
fuzzer = SyntaxSymphony(grammar)
|
|
184
|
+
# Create the fuzzer (pass seed=42 for reproducible output)
|
|
185
|
+
fuzzer = SyntaxSymphony(grammar, seed=42)
|
|
182
186
|
|
|
183
187
|
# Generate 10 test cases
|
|
184
188
|
for i in range(10):
|
|
@@ -186,6 +190,8 @@ for i in range(10):
|
|
|
186
190
|
print(test_case)
|
|
187
191
|
```
|
|
188
192
|
|
|
193
|
+
Pass `seed` to get the same sequence of outputs across runs. The fuzzer uses a private `random.Random` instance, so it does not affect global random state. Omit `seed` (or pass `None`) for non-deterministic fuzzing.
|
|
194
|
+
|
|
189
195
|
## Contributing
|
|
190
196
|
We welcome contributions from the community. If you have ideas for improvements, new features, or bug fixes, please submit a pull request or open an issue on our [GitHub repository](https://github.com/StanimirIglev/syntax-symphony).
|
|
191
197
|
|
|
@@ -82,11 +82,14 @@ ssfuzz -g examples/expr_grammar.json -c 100 --start begin
|
|
|
82
82
|
|
|
83
83
|
# Set the file extension
|
|
84
84
|
ssfuzz -g examples/expr_grammar.json -c 100 -e json
|
|
85
|
+
|
|
86
|
+
# Reproduce the same output sequence across runs
|
|
87
|
+
ssfuzz -g examples/expr_grammar.json -c 100 --seed 42
|
|
85
88
|
```
|
|
86
89
|
|
|
87
90
|
### Full syntax:
|
|
88
91
|
```
|
|
89
|
-
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER]
|
|
92
|
+
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER] [--seed NUMBER]
|
|
90
93
|
|
|
91
94
|
Syntax Symphony Fuzzer
|
|
92
95
|
|
|
@@ -105,6 +108,7 @@ options:
|
|
|
105
108
|
--min-depth NUMBER Minimum depth for the derivation trees. Default: 1
|
|
106
109
|
-k NUMBER, --kcov NUMBER
|
|
107
110
|
Number of strings to generate for k-cov. Default: 1
|
|
111
|
+
--seed NUMBER Random seed for reproducible fuzzing. Default: non-deterministic
|
|
108
112
|
```
|
|
109
113
|
|
|
110
114
|
## API
|
|
@@ -130,8 +134,8 @@ grammar = Grammar({
|
|
|
130
134
|
"<number>": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
131
135
|
})
|
|
132
136
|
|
|
133
|
-
# Create the fuzzer
|
|
134
|
-
fuzzer = SyntaxSymphony(grammar)
|
|
137
|
+
# Create the fuzzer (pass seed=42 for reproducible output)
|
|
138
|
+
fuzzer = SyntaxSymphony(grammar, seed=42)
|
|
135
139
|
|
|
136
140
|
# Generate 10 test cases
|
|
137
141
|
for i in range(10):
|
|
@@ -139,6 +143,8 @@ for i in range(10):
|
|
|
139
143
|
print(test_case)
|
|
140
144
|
```
|
|
141
145
|
|
|
146
|
+
Pass `seed` to get the same sequence of outputs across runs. The fuzzer uses a private `random.Random` instance, so it does not affect global random state. Omit `seed` (or pass `None`) for non-deterministic fuzzing.
|
|
147
|
+
|
|
142
148
|
## Contributing
|
|
143
149
|
We welcome contributions from the community. If you have ideas for improvements, new features, or bug fixes, please submit a pull request or open an issue on our [GitHub repository](https://github.com/StanimirIglev/syntax-symphony).
|
|
144
150
|
|
|
@@ -85,6 +85,15 @@ def ssfuzz() -> None:
|
|
|
85
85
|
type=int,
|
|
86
86
|
help="Number of strings to generate for k-cov. Default: 1",
|
|
87
87
|
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--seed",
|
|
90
|
+
dest="seed",
|
|
91
|
+
metavar="NUMBER",
|
|
92
|
+
default=None,
|
|
93
|
+
required=False,
|
|
94
|
+
type=int,
|
|
95
|
+
help="Random seed for reproducible fuzzing. Default: non-deterministic",
|
|
96
|
+
)
|
|
88
97
|
|
|
89
98
|
args = parser.parse_args()
|
|
90
99
|
|
|
@@ -112,11 +121,14 @@ def ssfuzz() -> None:
|
|
|
112
121
|
except TypeError as exc:
|
|
113
122
|
print(f"Error: {exc}", file=sys.stderr)
|
|
114
123
|
sys.exit(1)
|
|
115
|
-
except AssertionError as exc:
|
|
116
|
-
print(f"Error: {exc}", file=sys.stderr)
|
|
117
|
-
sys.exit(1)
|
|
118
124
|
|
|
119
|
-
fuzzer = SyntaxSymphony(
|
|
125
|
+
fuzzer = SyntaxSymphony(
|
|
126
|
+
grammar,
|
|
127
|
+
args.kcov,
|
|
128
|
+
args.min_depth,
|
|
129
|
+
args.max_depth,
|
|
130
|
+
seed=args.seed,
|
|
131
|
+
)
|
|
120
132
|
|
|
121
133
|
if not os.path.isdir(args.output_dir):
|
|
122
134
|
os.makedirs(args.output_dir)
|
|
@@ -203,7 +203,10 @@ class DT:
|
|
|
203
203
|
DT: The derivation tree.
|
|
204
204
|
"""
|
|
205
205
|
symbol = data.get("symbol")
|
|
206
|
-
|
|
206
|
+
if not isinstance(symbol, str):
|
|
207
|
+
raise TypeError(
|
|
208
|
+
f"Derivation tree symbol must be a string, got {type(symbol).__name__}."
|
|
209
|
+
)
|
|
207
210
|
children = None
|
|
208
211
|
if data["children"] is not None:
|
|
209
212
|
children = [cls.from_dict(child) for child in data["children"]]
|
|
@@ -28,6 +28,7 @@ class SyntaxSymphony:
|
|
|
28
28
|
kcov: int = 1,
|
|
29
29
|
min_depth: int = 0,
|
|
30
30
|
max_depth: int = 10,
|
|
31
|
+
seed: int | None = None,
|
|
31
32
|
):
|
|
32
33
|
"""Initialize a fuzzer.
|
|
33
34
|
|
|
@@ -38,12 +39,15 @@ class SyntaxSymphony:
|
|
|
38
39
|
Defaults to 0.
|
|
39
40
|
max_depth (int, optional): Maximal depth for derivation trees.
|
|
40
41
|
Defaults to 10.
|
|
42
|
+
seed (int | None, optional): Random seed for reproducible fuzzing.
|
|
43
|
+
Defaults to None (non-deterministic).
|
|
41
44
|
"""
|
|
42
45
|
self.grammar = grammar
|
|
43
46
|
self.start_symbol = grammar.start_symbol
|
|
44
47
|
self._kcov = kcov
|
|
45
48
|
self._min_depth = min_depth
|
|
46
49
|
self._max_depth = max_depth
|
|
50
|
+
self._rng = random.Random(seed)
|
|
47
51
|
self.symbol_costs: dict[str, int | float] = {}
|
|
48
52
|
_logger.info("Computing costs...")
|
|
49
53
|
self.costs = self.compute_cost()
|
|
@@ -56,7 +60,7 @@ class SyntaxSymphony:
|
|
|
56
60
|
self.uncovered_k_paths = copy.deepcopy(self.k_paths)
|
|
57
61
|
# NOTE: Shuffle the paths, so that we only need to pop from the list.
|
|
58
62
|
for symbol in self.uncovered_k_paths:
|
|
59
|
-
|
|
63
|
+
self._rng.shuffle(self.uncovered_k_paths[symbol])
|
|
60
64
|
# self.k_tree_generator = self.convert_paths_to_trees(self.k_paths)
|
|
61
65
|
|
|
62
66
|
def symbol_cost(self, symbol: str, seen: set[str]) -> int | float:
|
|
@@ -312,7 +316,10 @@ class SyntaxSymphony:
|
|
|
312
316
|
tree.children = children
|
|
313
317
|
return tree
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
if item.children is not None:
|
|
320
|
+
raise RuntimeError(
|
|
321
|
+
"k-path expansion requires an unexpanded derivation tree node."
|
|
322
|
+
)
|
|
316
323
|
return expand_tree(DT(item.symbol, None), path, 0)
|
|
317
324
|
|
|
318
325
|
def remaining_k_paths(self) -> int:
|
|
@@ -349,7 +356,7 @@ class SyntaxSymphony:
|
|
|
349
356
|
item.children = k_tree.children
|
|
350
357
|
else:
|
|
351
358
|
grammar = self._pick_grammar(depth)
|
|
352
|
-
expansion =
|
|
359
|
+
expansion = self._rng.choice(grammar[item.symbol])
|
|
353
360
|
tree_expansion = [self.symbol_to_tree(t) for t in expansion]
|
|
354
361
|
item.children = tree_expansion
|
|
355
362
|
queue.extend((depth + 1, t) for t in tree_expansion)
|
|
@@ -44,13 +44,13 @@ class Grammar(UserDict[str, list[list[str]]]):
|
|
|
44
44
|
grammar_schema.validate(productions)
|
|
45
45
|
|
|
46
46
|
super().__init__(productions, **kwargs) # type: ignore
|
|
47
|
-
|
|
48
|
-
f"Start symbol '{start_symbol}' not found in grammar."
|
|
49
|
-
)
|
|
47
|
+
if start_symbol not in self:
|
|
48
|
+
raise ValueError(f"Start symbol '{start_symbol}' not found in grammar.")
|
|
50
49
|
self._start_symbol = start_symbol
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
if len(self[start_symbol]) != 1:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"Start symbol must have exactly one expansion alternative."
|
|
53
|
+
)
|
|
54
54
|
|
|
55
55
|
@property
|
|
56
56
|
def start_symbol(self) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: syntax_symphony
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Efficient grammar-based fuzzer.
|
|
5
5
|
Author-email: Stanimir Iglev <iglev.stanimir@gmail.com>
|
|
6
6
|
Maintainer-email: Stanimir Iglev <iglev.stanimir@gmail.com>
|
|
@@ -129,11 +129,14 @@ ssfuzz -g examples/expr_grammar.json -c 100 --start begin
|
|
|
129
129
|
|
|
130
130
|
# Set the file extension
|
|
131
131
|
ssfuzz -g examples/expr_grammar.json -c 100 -e json
|
|
132
|
+
|
|
133
|
+
# Reproduce the same output sequence across runs
|
|
134
|
+
ssfuzz -g examples/expr_grammar.json -c 100 --seed 42
|
|
132
135
|
```
|
|
133
136
|
|
|
134
137
|
### Full syntax:
|
|
135
138
|
```
|
|
136
|
-
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER]
|
|
139
|
+
ssfuzz [-h] -g FILE [-s SYMBOL] -c NUMBER [-d DIR] [-e EXT] [--max-depth NUMBER] [--min-depth NUMBER] [-k NUMBER] [--seed NUMBER]
|
|
137
140
|
|
|
138
141
|
Syntax Symphony Fuzzer
|
|
139
142
|
|
|
@@ -152,6 +155,7 @@ options:
|
|
|
152
155
|
--min-depth NUMBER Minimum depth for the derivation trees. Default: 1
|
|
153
156
|
-k NUMBER, --kcov NUMBER
|
|
154
157
|
Number of strings to generate for k-cov. Default: 1
|
|
158
|
+
--seed NUMBER Random seed for reproducible fuzzing. Default: non-deterministic
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
## API
|
|
@@ -177,8 +181,8 @@ grammar = Grammar({
|
|
|
177
181
|
"<number>": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
178
182
|
})
|
|
179
183
|
|
|
180
|
-
# Create the fuzzer
|
|
181
|
-
fuzzer = SyntaxSymphony(grammar)
|
|
184
|
+
# Create the fuzzer (pass seed=42 for reproducible output)
|
|
185
|
+
fuzzer = SyntaxSymphony(grammar, seed=42)
|
|
182
186
|
|
|
183
187
|
# Generate 10 test cases
|
|
184
188
|
for i in range(10):
|
|
@@ -186,6 +190,8 @@ for i in range(10):
|
|
|
186
190
|
print(test_case)
|
|
187
191
|
```
|
|
188
192
|
|
|
193
|
+
Pass `seed` to get the same sequence of outputs across runs. The fuzzer uses a private `random.Random` instance, so it does not affect global random state. Omit `seed` (or pass `None`) for non-deterministic fuzzing.
|
|
194
|
+
|
|
189
195
|
## Contributing
|
|
190
196
|
We welcome contributions from the community. If you have ideas for improvements, new features, or bug fixes, please submit a pull request or open an issue on our [GitHub repository](https://github.com/StanimirIglev/syntax-symphony).
|
|
191
197
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from syntax_symphony.derivation_tree import DT
|
|
6
|
+
from syntax_symphony.fuzzer import SyntaxSymphony
|
|
7
|
+
from syntax_symphony.grammar import Grammar
|
|
8
|
+
|
|
9
|
+
# --- k-path computation ---
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_compute_k_paths_k1(tiny_grammar: Grammar) -> None:
|
|
13
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
14
|
+
paths = fuzzer._compute_k_paths(1)
|
|
15
|
+
|
|
16
|
+
assert paths["<start>"] == [[["<A>"]]]
|
|
17
|
+
assert paths["<A>"] == [[["<B>", "x"]], [["y"]]]
|
|
18
|
+
assert paths["<B>"] == [[["z"]]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_compute_k_paths_k2(tiny_grammar: Grammar) -> None:
|
|
22
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
23
|
+
paths = fuzzer._compute_k_paths(2)
|
|
24
|
+
|
|
25
|
+
assert paths["<A>"] == [[["<B>", "x"], ["z"]]]
|
|
26
|
+
assert paths["<start>"] == [[["<A>"], ["<B>", "x"]], [["<A>"], ["y"]]]
|
|
27
|
+
assert paths["<B>"] == []
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_compute_k_paths_k3(tiny_grammar: Grammar) -> None:
|
|
31
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
32
|
+
paths = fuzzer._compute_k_paths(3)
|
|
33
|
+
|
|
34
|
+
assert paths["<start>"] == [[["<A>"], ["<B>", "x"], ["z"]]]
|
|
35
|
+
assert paths["<A>"] == []
|
|
36
|
+
assert paths["<B>"] == []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_compute_k_paths_accumulates_through_kcov(tiny_grammar: Grammar) -> None:
|
|
40
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=2)
|
|
41
|
+
|
|
42
|
+
assert len(fuzzer.k_paths["<start>"]) == 3
|
|
43
|
+
assert len(fuzzer.k_paths["<A>"]) == 3
|
|
44
|
+
assert len(fuzzer.k_paths["<B>"]) == 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_compute_k_paths_invalid_k(tiny_grammar: Grammar) -> None:
|
|
48
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
49
|
+
|
|
50
|
+
with pytest.raises(ValueError, match="max_k must be at least 1"):
|
|
51
|
+
fuzzer.compute_k_paths(0)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_terminal_only_k_paths(terminal_only_grammar: Grammar) -> None:
|
|
55
|
+
fuzzer = SyntaxSymphony(terminal_only_grammar, kcov=1)
|
|
56
|
+
|
|
57
|
+
assert fuzzer.k_paths == {"<start>": [[["hello"]]]}
|
|
58
|
+
assert fuzzer.remaining_k_paths() == 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- cost computation and biased grammars ---
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_compute_cost(tiny_grammar: Grammar) -> None:
|
|
65
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
66
|
+
|
|
67
|
+
assert fuzzer.costs["<A>"] == {"<B>x": 2, "y": 1}
|
|
68
|
+
assert fuzzer.costs["<B>"] == {"z": 1}
|
|
69
|
+
assert fuzzer.costs["<start>"] == {"<A>": 2}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_biased_grammar_min_max(tiny_grammar: Grammar) -> None:
|
|
73
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
74
|
+
|
|
75
|
+
assert fuzzer.minimizing_grammar["<A>"] == [["y"]]
|
|
76
|
+
assert fuzzer.maximizing_grammar["<A>"] == [["<B>", "x"]]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# --- coverage tracking ---
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_remaining_k_paths_initial_count(tiny_grammar: Grammar) -> None:
|
|
83
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=2, seed=42)
|
|
84
|
+
|
|
85
|
+
assert fuzzer.remaining_k_paths() == 7
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_k_paths_depleted_after_fuzzing(tiny_grammar: Grammar) -> None:
|
|
89
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=2, seed=42)
|
|
90
|
+
initial = fuzzer.remaining_k_paths()
|
|
91
|
+
|
|
92
|
+
for _ in range(initial):
|
|
93
|
+
fuzzer.fuzz()
|
|
94
|
+
|
|
95
|
+
assert fuzzer.remaining_k_paths() == 0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_no_paths_reused_after_depletion(tiny_grammar: Grammar) -> None:
|
|
99
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=2, seed=42)
|
|
100
|
+
|
|
101
|
+
while fuzzer.remaining_k_paths() > 0:
|
|
102
|
+
fuzzer.fuzz()
|
|
103
|
+
|
|
104
|
+
for _ in range(5):
|
|
105
|
+
before = fuzzer.remaining_k_paths()
|
|
106
|
+
fuzzer.fuzz()
|
|
107
|
+
assert fuzzer.remaining_k_paths() == before == 0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --- depth control ---
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_max_depth_prefers_shallower_expansions(
|
|
114
|
+
depth_chain_grammar: Grammar,
|
|
115
|
+
) -> None:
|
|
116
|
+
shallow = SyntaxSymphony(
|
|
117
|
+
depth_chain_grammar,
|
|
118
|
+
kcov=1,
|
|
119
|
+
min_depth=0,
|
|
120
|
+
max_depth=1,
|
|
121
|
+
seed=42,
|
|
122
|
+
)
|
|
123
|
+
deep = SyntaxSymphony(
|
|
124
|
+
depth_chain_grammar,
|
|
125
|
+
kcov=1,
|
|
126
|
+
min_depth=0,
|
|
127
|
+
max_depth=10,
|
|
128
|
+
seed=42,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
shallow_tree = shallow.tree_fuzz(DT(shallow.start_symbol, None))
|
|
132
|
+
deep_tree = deep.tree_fuzz(DT(deep.start_symbol, None))
|
|
133
|
+
|
|
134
|
+
assert shallow_tree.height() < deep_tree.height()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_min_depth_biases_toward_deeper_expansions(
|
|
138
|
+
depth_chain_grammar: Grammar,
|
|
139
|
+
) -> None:
|
|
140
|
+
low_min = SyntaxSymphony(
|
|
141
|
+
depth_chain_grammar,
|
|
142
|
+
kcov=1,
|
|
143
|
+
min_depth=0,
|
|
144
|
+
max_depth=10,
|
|
145
|
+
seed=7,
|
|
146
|
+
)
|
|
147
|
+
high_min = SyntaxSymphony(
|
|
148
|
+
depth_chain_grammar,
|
|
149
|
+
kcov=1,
|
|
150
|
+
min_depth=2,
|
|
151
|
+
max_depth=10,
|
|
152
|
+
seed=7,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
low_tree = low_min.tree_fuzz(DT(low_min.start_symbol, None))
|
|
156
|
+
high_tree = high_min.tree_fuzz(DT(high_min.start_symbol, None))
|
|
157
|
+
|
|
158
|
+
assert high_tree.height() >= low_tree.height()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- seed reproducibility ---
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_same_seed_same_output(tiny_grammar: Grammar) -> None:
|
|
165
|
+
first = SyntaxSymphony(tiny_grammar, kcov=2, seed=99)
|
|
166
|
+
second = SyntaxSymphony(tiny_grammar, kcov=2, seed=99)
|
|
167
|
+
|
|
168
|
+
assert [first.fuzz() for _ in range(5)] == [second.fuzz() for _ in range(5)]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_different_seed_different_output(tiny_grammar: Grammar) -> None:
|
|
172
|
+
first = SyntaxSymphony(tiny_grammar, kcov=2, seed=99)
|
|
173
|
+
second = SyntaxSymphony(tiny_grammar, kcov=2, seed=100)
|
|
174
|
+
|
|
175
|
+
assert [first.fuzz() for _ in range(5)] != [second.fuzz() for _ in range(5)]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_same_seed_same_shuffle_order(tiny_grammar: Grammar) -> None:
|
|
179
|
+
first = SyntaxSymphony(tiny_grammar, kcov=2, seed=42)
|
|
180
|
+
second = SyntaxSymphony(tiny_grammar, kcov=2, seed=42)
|
|
181
|
+
|
|
182
|
+
assert first.uncovered_k_paths == second.uncovered_k_paths
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --- helpers and edge cases ---
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_symbol_to_tree(tiny_grammar: Grammar) -> None:
|
|
189
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1)
|
|
190
|
+
|
|
191
|
+
nonterminal = fuzzer.symbol_to_tree("<A>")
|
|
192
|
+
terminal = fuzzer.symbol_to_tree("x")
|
|
193
|
+
|
|
194
|
+
assert nonterminal.symbol == "<A>"
|
|
195
|
+
assert nonterminal.children is None
|
|
196
|
+
assert terminal.symbol == "x"
|
|
197
|
+
assert terminal.children == []
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_complete_tree(tiny_grammar: Grammar) -> None:
|
|
201
|
+
fuzzer = SyntaxSymphony(tiny_grammar, kcov=1, seed=42)
|
|
202
|
+
partial = DT("<start>", [DT("<A>", None)])
|
|
203
|
+
completed = fuzzer.complete_tree(partial)
|
|
204
|
+
|
|
205
|
+
assert completed.to_str()
|
|
206
|
+
assert "<" not in completed.to_str()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_fuzz_returns_only_terminals(expr_grammar: Grammar) -> None:
|
|
210
|
+
fuzzer = SyntaxSymphony(expr_grammar, kcov=1, seed=42)
|
|
211
|
+
|
|
212
|
+
for _ in range(10):
|
|
213
|
+
result = fuzzer.fuzz()
|
|
214
|
+
assert isinstance(result, str)
|
|
215
|
+
assert len(result) > 0
|
|
216
|
+
assert not re.search(r"<[^>]+>", result)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_recursive_grammar_fuzz_succeeds(expr_grammar: Grammar) -> None:
|
|
220
|
+
fuzzer = SyntaxSymphony(expr_grammar, kcov=2, seed=42)
|
|
221
|
+
|
|
222
|
+
results = {fuzzer.fuzz() for _ in range(20)}
|
|
223
|
+
assert len(results) > 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_fuzz(expr_grammar: Grammar) -> None:
|
|
227
|
+
symphony = SyntaxSymphony(expr_grammar, seed=42)
|
|
228
|
+
result = symphony.fuzz()
|
|
229
|
+
assert isinstance(result, str)
|
|
230
|
+
assert len(result) > 0
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from syntax_symphony.derivation_tree import DT
|
|
7
|
+
from syntax_symphony.fuzzer import SyntaxSymphony
|
|
8
|
+
from syntax_symphony.grammar import Grammar
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_grammar_rejects_missing_start_symbol():
|
|
12
|
+
with pytest.raises(ValueError, match="not found in grammar"):
|
|
13
|
+
Grammar({"<other>": [["x"]]}, start_symbol="<start>")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_grammar_rejects_multiple_start_expansions():
|
|
17
|
+
with pytest.raises(ValueError, match="exactly one expansion"):
|
|
18
|
+
Grammar({"<start>": [["a"], ["b"]]})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_grammar_accepts_valid_start_symbol():
|
|
22
|
+
grammar = Grammar({"<start>": [["end"]]})
|
|
23
|
+
assert grammar.start_symbol == "<start>"
|
|
24
|
+
assert grammar["<start>"] == [["end"]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_dt_from_dict_rejects_non_string_symbol():
|
|
28
|
+
with pytest.raises(TypeError, match="must be a string"):
|
|
29
|
+
DT.from_dict({"symbol": 123, "children": None})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_dt_from_dict_accepts_valid_data():
|
|
33
|
+
dt = DT.from_dict({"symbol": "S", "children": None})
|
|
34
|
+
assert dt.symbol == "S"
|
|
35
|
+
assert dt.children is None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_k_path_to_tree_rejects_expanded_node():
|
|
39
|
+
grammar = Grammar({"<start>": [["x"]]})
|
|
40
|
+
fuzzer = SyntaxSymphony(grammar)
|
|
41
|
+
with pytest.raises(RuntimeError, match="unexpanded derivation tree node"):
|
|
42
|
+
fuzzer._k_path_to_tree(DT("<start>", []), [["x"]])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_validation_under_python_optimize_flag():
|
|
46
|
+
"""Validation must hold when asserts are stripped (python -O)."""
|
|
47
|
+
script = """
|
|
48
|
+
from syntax_symphony.grammar import Grammar
|
|
49
|
+
from syntax_symphony.derivation_tree import DT
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
Grammar({"<other>": [["x"]]}, start_symbol="<start>")
|
|
53
|
+
except ValueError:
|
|
54
|
+
pass
|
|
55
|
+
else:
|
|
56
|
+
raise SystemExit("missing start symbol was not rejected")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
Grammar({"<start>": [["a"], ["b"]]})
|
|
60
|
+
except ValueError:
|
|
61
|
+
pass
|
|
62
|
+
else:
|
|
63
|
+
raise SystemExit("multiple start expansions were not rejected")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
DT.from_dict({"symbol": 123, "children": None})
|
|
67
|
+
except TypeError:
|
|
68
|
+
pass
|
|
69
|
+
else:
|
|
70
|
+
raise SystemExit("non-string symbol was not rejected")
|
|
71
|
+
"""
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
[sys.executable, "-O", "-c", script],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
assert result.returncode == 0, result.stderr or result.stdout
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
from syntax_symphony.fuzzer import SyntaxSymphony
|
|
2
|
-
from syntax_symphony.grammar import Grammar
|
|
3
|
-
|
|
4
|
-
grammar = Grammar(
|
|
5
|
-
{
|
|
6
|
-
"<start>": ["<expr>"],
|
|
7
|
-
"<expr>": ["<term> + <expr>", "<term> - <expr>", "<term>"],
|
|
8
|
-
"<term>": ["<factor> * <term>", "<factor> / <term>", "<factor>"],
|
|
9
|
-
"<factor>": ["<number>", "( <expr> )"],
|
|
10
|
-
"<number>": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
|
|
11
|
-
}
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
# TODO: More tests!
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_fuzz():
|
|
18
|
-
symphony = SyntaxSymphony(grammar)
|
|
19
|
-
result = symphony.fuzz()
|
|
20
|
-
assert isinstance(result, str)
|
|
21
|
-
assert len(result) > 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|