syntax-symphony 0.2.1__tar.gz → 0.4.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.
Files changed (24) hide show
  1. {syntax_symphony-0.2.1/src/syntax_symphony.egg-info → syntax_symphony-0.4.0}/PKG-INFO +11 -5
  2. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/README.md +10 -4
  3. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/pyproject.toml +1 -1
  4. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/__init__.py +5 -0
  5. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/cli.py +18 -3
  6. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/derivation_tree.py +1 -1
  7. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/fuzzer.py +13 -46
  8. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0/src/syntax_symphony.egg-info}/PKG-INFO +11 -5
  9. syntax_symphony-0.4.0/tests/test_fuzzer.py +230 -0
  10. syntax_symphony-0.2.1/tests/test_fuzzer.py +0 -21
  11. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/LICENSE +0 -0
  12. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/setup.cfg +0 -0
  13. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/grammar.py +0 -0
  14. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony/py.typed +0 -0
  15. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony.egg-info/SOURCES.txt +0 -0
  16. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony.egg-info/dependency_links.txt +0 -0
  17. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony.egg-info/entry_points.txt +0 -0
  18. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony.egg-info/requires.txt +0 -0
  19. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/src/syntax_symphony.egg-info/top_level.txt +0 -0
  20. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/tests/test_cli.py +0 -0
  21. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/tests/test_derivation_tree.py +0 -0
  22. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/tests/test_grammar.py +0 -0
  23. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/tests/test_public_api.py +0 -0
  24. {syntax_symphony-0.2.1 → syntax_symphony-0.4.0}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntax_symphony
3
- Version: 0.2.1
3
+ Version: 0.4.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
 
@@ -149,9 +152,10 @@ options:
149
152
  -e EXT, --file-extension EXT
150
153
  The file extension to be used for the produced documents. Default: txt
151
154
  --max-depth NUMBER Maximum depth for the derivation trees. Default: 10
152
- --min-depth NUMBER Minimum depth for the derivation trees. Default: 1
155
+ --min-depth NUMBER Minimum depth for the derivation trees. Default: 0
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
 
@@ -102,9 +105,10 @@ options:
102
105
  -e EXT, --file-extension EXT
103
106
  The file extension to be used for the produced documents. Default: txt
104
107
  --max-depth NUMBER Maximum depth for the derivation trees. Default: 10
105
- --min-depth NUMBER Minimum depth for the derivation trees. Default: 1
108
+ --min-depth NUMBER Minimum depth for the derivation trees. Default: 0
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "syntax_symphony"
7
- version = "0.2.1"
7
+ version = "0.4.0"
8
8
  description = "Efficient grammar-based fuzzer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,12 +1,17 @@
1
1
  """Efficient grammar-based fuzzer with k-path coverage."""
2
2
 
3
+ from importlib.metadata import version
4
+
3
5
  from .derivation_tree import DT
4
6
  from .fuzzer import SyntaxSymphony
5
7
  from .grammar import Grammar, load_grammar_from_file
6
8
 
9
+ __version__ = version("syntax_symphony")
10
+
7
11
  __all__ = [
8
12
  "DT",
9
13
  "Grammar",
10
14
  "SyntaxSymphony",
15
+ "__version__",
11
16
  "load_grammar_from_file",
12
17
  ]
@@ -69,11 +69,11 @@ def ssfuzz() -> None:
69
69
  parser.add_argument(
70
70
  "--min-depth",
71
71
  dest="min_depth",
72
- default=1,
72
+ default=0,
73
73
  metavar="NUMBER",
74
74
  required=False,
75
75
  type=int,
76
- help="Minimum depth for the derivation trees. Default: 1",
76
+ help="Minimum depth for the derivation trees. Default: 0",
77
77
  )
78
78
  parser.add_argument(
79
79
  "-k",
@@ -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
 
@@ -113,7 +122,13 @@ def ssfuzz() -> None:
113
122
  print(f"Error: {exc}", file=sys.stderr)
114
123
  sys.exit(1)
115
124
 
116
- fuzzer = SyntaxSymphony(grammar, args.kcov, args.min_depth, args.max_depth)
125
+ fuzzer = SyntaxSymphony(
126
+ grammar,
127
+ args.kcov,
128
+ args.min_depth,
129
+ args.max_depth,
130
+ seed=args.seed,
131
+ )
117
132
 
118
133
  if not os.path.isdir(args.output_dir):
119
134
  os.makedirs(args.output_dir)
@@ -31,7 +31,7 @@ class DT:
31
31
 
32
32
  def __getitem__(self, index: int | slice) -> DT | list[DT]:
33
33
  if self.children is None:
34
- raise IndexError("Unxpanded symbols do not have children!")
34
+ raise IndexError("Unexpanded symbols do not have children!")
35
35
  return self.children[index]
36
36
 
37
37
  def __iter__(self) -> Iterator[DT]:
@@ -2,8 +2,7 @@ import copy
2
2
  import logging
3
3
  import random
4
4
  from collections import deque
5
- from collections.abc import Callable, Generator, Iterable
6
- from itertools import chain
5
+ from collections.abc import Callable, Iterable
7
6
 
8
7
  from .derivation_tree import DT
9
8
  from .grammar import Grammar, is_nonterminal
@@ -28,6 +27,7 @@ class SyntaxSymphony:
28
27
  kcov: int = 1,
29
28
  min_depth: int = 0,
30
29
  max_depth: int = 10,
30
+ seed: int | None = None,
31
31
  ):
32
32
  """Initialize a fuzzer.
33
33
 
@@ -38,12 +38,15 @@ class SyntaxSymphony:
38
38
  Defaults to 0.
39
39
  max_depth (int, optional): Maximal depth for derivation trees.
40
40
  Defaults to 10.
41
+ seed (int | None, optional): Random seed for reproducible fuzzing.
42
+ Defaults to None (non-deterministic).
41
43
  """
42
44
  self.grammar = grammar
43
45
  self.start_symbol = grammar.start_symbol
44
46
  self._kcov = kcov
45
47
  self._min_depth = min_depth
46
48
  self._max_depth = max_depth
49
+ self._rng = random.Random(seed)
47
50
  self.symbol_costs: dict[str, int | float] = {}
48
51
  _logger.info("Computing costs...")
49
52
  self.costs = self.compute_cost()
@@ -56,8 +59,10 @@ class SyntaxSymphony:
56
59
  self.uncovered_k_paths = copy.deepcopy(self.k_paths)
57
60
  # NOTE: Shuffle the paths, so that we only need to pop from the list.
58
61
  for symbol in self.uncovered_k_paths:
59
- random.shuffle(self.uncovered_k_paths[symbol])
60
- # self.k_tree_generator = self.convert_paths_to_trees(self.k_paths)
62
+ self._rng.shuffle(self.uncovered_k_paths[symbol])
63
+ self._remaining_k_paths = sum(
64
+ len(paths) for paths in self.uncovered_k_paths.values()
65
+ )
61
66
 
62
67
  def symbol_cost(self, symbol: str, seen: set[str]) -> int | float:
63
68
  """Computes the cost of a symbol.
@@ -225,45 +230,6 @@ class SyntaxSymphony:
225
230
  kpaths[symbol].extend(paths)
226
231
  return kpaths
227
232
 
228
- def convert_paths_to_trees(
229
- self, paths: dict[str, list[list[list[str]]]]
230
- ) -> Generator[DT, None, None]:
231
- """Converts the k-path symbol map to derivation trees.
232
-
233
- Args:
234
- paths (dict[str, list[list[str]]]):
235
- A dictionary mapping each nonterminal to a list of paths.
236
-
237
- Returns:
238
- Generator[DT, None, None]: A generator for derivation trees.
239
- """
240
- # NOTE: Legacy function.
241
-
242
- def expand_tree(tree: DT, path: list[list[str]], depth: int) -> DT:
243
- if depth >= len(path):
244
- return tree
245
-
246
- expansion = path[depth]
247
- children: list[DT] = []
248
-
249
- if expansion not in self.grammar[tree.symbol]:
250
- tree.children = None
251
- return tree
252
-
253
- for symbol in expansion:
254
- if is_nonterminal(symbol):
255
- child_tree = expand_tree(DT(symbol, None), path, depth + 1)
256
- children.append(child_tree)
257
- else:
258
- children.append(DT(symbol, []))
259
-
260
- tree.children = children
261
- return tree
262
-
263
- for nonterminal in paths:
264
- for path in paths[nonterminal]:
265
- yield expand_tree(DT(nonterminal, None), path, 0)
266
-
267
233
  def complete_tree(self, dtree: DT) -> DT:
268
234
  """Completes a derivation tree by expanding the unexpanded nonterminals.
269
235
 
@@ -319,12 +285,12 @@ class SyntaxSymphony:
319
285
  return expand_tree(DT(item.symbol, None), path, 0)
320
286
 
321
287
  def remaining_k_paths(self) -> int:
322
- """Computes the number of remaining uncovered k-paths.
288
+ """Return the number of remaining uncovered k-paths.
323
289
 
324
290
  Returns:
325
291
  int: The number of remaining uncovered k-paths.
326
292
  """
327
- return len(list(chain.from_iterable(list(self.uncovered_k_paths.values()))))
293
+ return self._remaining_k_paths
328
294
 
329
295
  def tree_fuzz(self, tree: DT) -> DT:
330
296
  """Fuzzes a derivation tree by expanding the unexpanded nonterminals.
@@ -345,6 +311,7 @@ class SyntaxSymphony:
345
311
 
346
312
  if len(self.uncovered_k_paths[item.symbol]) > 0 and depth < self._max_depth:
347
313
  path = self.uncovered_k_paths[item.symbol].pop()
314
+ self._remaining_k_paths -= 1
348
315
  k_tree = self._k_path_to_tree(item, path)
349
316
  for i in k_tree:
350
317
  if i.children is None:
@@ -352,7 +319,7 @@ class SyntaxSymphony:
352
319
  item.children = k_tree.children
353
320
  else:
354
321
  grammar = self._pick_grammar(depth)
355
- expansion = random.choice(grammar[item.symbol])
322
+ expansion = self._rng.choice(grammar[item.symbol])
356
323
  tree_expansion = [self.symbol_to_tree(t) for t in expansion]
357
324
  item.children = tree_expansion
358
325
  queue.extend((depth + 1, t) for t in tree_expansion)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: syntax_symphony
3
- Version: 0.2.1
3
+ Version: 0.4.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
 
@@ -149,9 +152,10 @@ options:
149
152
  -e EXT, --file-extension EXT
150
153
  The file extension to be used for the produced documents. Default: txt
151
154
  --max-depth NUMBER Maximum depth for the derivation trees. Default: 10
152
- --min-depth NUMBER Minimum depth for the derivation trees. Default: 1
155
+ --min-depth NUMBER Minimum depth for the derivation trees. Default: 0
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
@@ -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