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.
Files changed (24) hide show
  1. {syntax_symphony-0.2.0/src/syntax_symphony.egg-info → syntax_symphony-0.3.0}/PKG-INFO +10 -4
  2. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/README.md +9 -3
  3. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/pyproject.toml +1 -1
  4. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/cli.py +16 -4
  5. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/derivation_tree.py +4 -1
  6. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/fuzzer.py +10 -3
  7. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/grammar.py +6 -6
  8. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0/src/syntax_symphony.egg-info}/PKG-INFO +10 -4
  9. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/SOURCES.txt +2 -1
  10. syntax_symphony-0.3.0/tests/test_fuzzer.py +230 -0
  11. syntax_symphony-0.3.0/tests/test_validation.py +78 -0
  12. syntax_symphony-0.2.0/tests/test_fuzzer.py +0 -21
  13. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/LICENSE +0 -0
  14. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/setup.cfg +0 -0
  15. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/__init__.py +0 -0
  16. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony/py.typed +0 -0
  17. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/dependency_links.txt +0 -0
  18. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/entry_points.txt +0 -0
  19. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/requires.txt +0 -0
  20. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/src/syntax_symphony.egg-info/top_level.txt +0 -0
  21. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_cli.py +0 -0
  22. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_derivation_tree.py +0 -0
  23. {syntax_symphony-0.2.0 → syntax_symphony-0.3.0}/tests/test_grammar.py +0 -0
  24. {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.2.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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "syntax_symphony"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Efficient grammar-based fuzzer."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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(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
+ )
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
- assert isinstance(symbol, str)
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
- random.shuffle(self.uncovered_k_paths[symbol])
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
- assert item.children is None
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 = random.choice(grammar[item.symbol])
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
- assert start_symbol in self, (
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
- assert len(self[start_symbol]) == 1, (
52
- "Start symbol must have exactly one expansion alternative."
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.2.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
 
@@ -17,4 +17,5 @@ tests/test_cli.py
17
17
  tests/test_derivation_tree.py
18
18
  tests/test_fuzzer.py
19
19
  tests/test_grammar.py
20
- tests/test_public_api.py
20
+ tests/test_public_api.py
21
+ tests/test_validation.py
@@ -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