From 5b7a5b2494ea77d17589b39179e16c0a4883c701 Mon Sep 17 00:00:00 2001 From: Tim Scheckenbach Date: Tue, 17 Dec 2024 16:38:34 +0100 Subject: [PATCH 01/28] adding tests, progress on #173 --- tests/resources/digit.fan | 3 ++ tests/test_cli.py | 75 +++++++++++++++++++++++++++++++++++++++ tests/test_constraints.py | 56 +++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 tests/resources/digit.fan create mode 100644 tests/test_cli.py diff --git a/tests/resources/digit.fan b/tests/resources/digit.fan new file mode 100644 index 00000000..6a0d32c4 --- /dev/null +++ b/tests/resources/digit.fan @@ -0,0 +1,3 @@ + + ::= +; + ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..3e730f45 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,75 @@ +import unittest +import subprocess +import os + +from fandango.cli import get_parser + + +class test_cli(unittest.TestCase): + + def run_command(self, command): + proc = subprocess.Popen(command, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE, + ) + out, err = proc.communicate() + return out.decode(), err.decode(), proc.returncode + + + + def test_help(self): + command = ["fandango", "--help"] + out, err, code = self.run_command(command) + parser = get_parser(True) + self.assertEqual(0, code) + self.assertEqual(out, parser.format_help()) + self.assertEqual(err, "") + + def test_fuzz_basic(self): + command = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912"] + expected = """35716 +4 +9768 +30 +5658 +5 +9 +649 +20 +41""" + out, err, code = self.run_command(command) + self.assertEqual(0, code) + self.assertEqual(expected, out.strip()) + self.assertEqual("", err) + + def test_output_to_file(self): + command = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912", "-o", "tests/resources/test.txt", "-s", ";"] + expected = "35716;4;9768;30;5658;5;9;649;20;41;" + out, err, code = self.run_command(command) + self.assertEqual(0, code) + self.assertEqual("", out) + self.assertEqual("", err) + with open("tests/resources/test.txt", "r") as fd: + actual = fd.read() + os.remove("tests/resources/test.txt") + self.assertEqual(expected, actual) + + def test_output_multiple_files(self): + command = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912", "-d", "tests/resources/test"] + expected = ["35716","4","9768","30","5658","5","9","649","20","41"] + out, err, code, = self.run_command(command) + self.assertEqual(0, code) + self.assertEqual("", out) + self.assertEqual("", err) + for i in range(10): + filename = "tests/resources/test/fandango-" + str(i+1).zfill(4) + ".txt" + with open(filename, "r") as fd: + actual = fd.read() + self.assertEqual(expected[i], actual) + os.remove(filename) + os.rmdir("tests/resources/test") + + + + + diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 335278aa..5323c7f1 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -349,6 +349,62 @@ def test_exists_constraint(self): ) self.assertTrue(constraint.check(example)) + def test_constraint_check(self): + self.assertRaises(ValueError, self.get_constraint, ". == 'a'") + self.assertRaises(ValueError, self.get_constraint, ".. == 'a'") + + def test_direct_children(self): + constraint = self.get_constraint("str(.) == 'a';") + counter_example = DerivationTree( + NonTerminal(""), + [ + DerivationTree( + NonTerminal(""), + [ + DerivationTree( + Terminal("b"), + [] + ) + ] + ) + ] + ) + + self.assertFalse(constraint.check(counter_example)) + example = DerivationTree( + NonTerminal(""), + [ + DerivationTree( + NonTerminal(""), + [ + DerivationTree( + Terminal("a"), + [] + ) + ] + ) + ] + ) + self.assertTrue(constraint.check(example)) + + def test_indirect_children(self): + grammar = """ + ::= ; + ::= | ; + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; +""" + constraint = "int(..) == 1;" + grammar, constraint = parse(grammar + constraint) + + self.assertEqual(1, len(constraint)) + constraint = constraint[0] + + counter_example = grammar.parse("19") + self.assertFalse(constraint.check(counter_example)) + + example = grammar.parse("11") + self.assertTrue(constraint.check(example)) + def test_complex_constraint(self): grammar = """ ::= ; From 8f04f7f39c49f1fc5cf5adffeab8842b94e8deb6 Mon Sep 17 00:00:00 2001 From: Tim Scheckenbach Date: Thu, 19 Dec 2024 17:17:58 +0100 Subject: [PATCH 02/28] more tests, started fixing bug --- src/fandango/language/parse.py | 66 ++++++++++++++++++++++++++-------- tests/test_cli.py | 33 ++++++++++++++++- tests/test_constraints.py | 16 +++++++++ 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/src/fandango/language/parse.py b/src/fandango/language/parse.py index a86c4929..7882300a 100644 --- a/src/fandango/language/parse.py +++ b/src/fandango/language/parse.py @@ -22,7 +22,7 @@ ConstraintProcessor, PythonProcessor, ) -from fandango.language.grammar import Grammar, NodeType +from fandango.language.grammar import Grammar, NodeType, Alternative from fandango.language.parser.FandangoLexer import FandangoLexer from fandango.language.parser.FandangoParser import FandangoParser from fandango.language.symbol import NonTerminal @@ -96,10 +96,12 @@ def check_constraints_existence(grammar, constraints): constraint_symbols = constraint.get_symbols() for value in constraint_symbols: - # LOGGER.debug(f"Constraint {constraint}: Checking {value}") - - constraint_matches = re.findall(r"<([^>]*)>", str(value)) # was <(.*?)> + LOGGER.debug(f"Constraint {constraint}: Checking {value}") + constraint_matches = re.findall(r"<([^>]*)>", str(value)) # was <(.*?)> then <([^>]*)> + constraint_matches_children = re.findall(r"<([^>]*)>(\[.\])*", str(value)) + LOGGER.debug(f"Found following constraint matches: {str(constraint_matches_children)}") + missing = [ match for match in constraint_matches if match not in grammar_matches ] @@ -116,21 +118,55 @@ def check_constraints_existence(grammar, constraints): error.add_note(f"Possible symbols: {defined_symbols_str}") raise error - for i in range(len(constraint_matches) - 1): - parent = constraint_matches[i] - symbol = constraint_matches[i + 1] - indirect = f"<{parent}>..<{symbol}>" in str(value) - if not check_constraints_existence_children( - grammar, parent, symbol, indirect, indirect_child - ): - msg = f"Constraint {constraint}: <{parent}> has no child <{symbol}>" - raise ValueError(msg) + parent = constraint_matches_children[0] + for i in range(len(constraint_matches_children) - 1): + parent_sym = parent[0] + if parent[1] == "": + symbol = constraint_matches[i + 1][0] + indirect = f"<{parent_sym}>..<{symbol}>" in str(value) + if not check_constraints_existence_children( + grammar, parent_sym, symbol, indirect, indirect_child + ): + msg = f"Constraint {constraint}: <{parent_sym}> has no child <{symbol}>" + raise ValueError(msg) + else: + indirect = f"<{parent_sym}>{parent[1]}..<{constraint_matches[i+1][0]}>" in str(value) + if not check_constraint_existence_access( + grammar, parent_sym, parent[1][1:-1], indirect, constraint_matches[i+1][0] + ): + msg = f"Constraint {constraint}: <{parent_sym}> has no child <{symbol}>" + raise ValueError(msg) + + + + +def check_constraint_existence_access(grammar, parent, id, recurse, next): + rules = grammar.rules[NonTerminal(f"<{parent}>")] + if isinstance(rules, Alternative): + child = None + for rule in rules: + nonterminals = re.findall(r"<([^>]*)>", str(rule)) + if len(nonterminals) < id: + continue + child = nonterminals[id] + if check_constraints_existence_children(grammar, child, next, recurse): + return True + if child is None: + raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") + else: + nonterminals = re.findall(r"<([^>]*)>", str(rule)) + if len(nonterminals) < id: + raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") + child = nonterminals[id] + return check_constraints_existence_children(grammar, child, next, recurse) + return False + def check_constraints_existence_children( grammar, parent, symbol, recurse, indirect_child ): - # LOGGER.debug(f"Checking {parent}, {symbol}") + LOGGER.debug(f"Checking {parent}, {symbol}") if indirect_child[f"<{parent}>"][f"<{symbol}>"] is not None: return indirect_child[f"<{parent}>"][f"<{symbol}>"] @@ -199,7 +235,7 @@ def run_code(self): def parse(fan_contents: str, /, lazy: bool = False, - check_constraints: bool = True, given_grammar=None, use_cache: bool = True) -> Tuple[Grammar, List[Constraint]]: + check_constraints: bool = True, given_grammar=None, use_cache: bool = False) -> Tuple[Grammar, List[Constraint]]: """ Extract grammar and constraints from the given content :param fan: Fandango specification diff --git a/tests/test_cli.py b/tests/test_cli.py index 3e730f45..306347cd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -69,7 +69,38 @@ def test_output_multiple_files(self): os.remove(filename) os.rmdir("tests/resources/test") - + def test_command_line_constraints(self): + command_single = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912", "-c", "25 <= int() and int() <= 45"] + expected = """30 +41 +29 +44 +44 +36 +28 +30 +41 +29 +""" + out, err, code = self.run_command(command_single) + self.assertEqual(0, code) + self.assertEqual(expected, out) + self.assertEqual("", err) + command_multiple = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912", "-c", "25 <= int()", "-c", "int() <= 45"] + out, err, code = self.run_command(command_multiple) + self.assertEqual(0, code) + self.assertEqual(expected, out) + self.assertEqual("", err) + + def test_unsat(self): + command = ["fandango", "fuzz", "-f", "tests/resources/digit.fan", "-n", "10", "--random-seed", "426912", "-c", "False"] + expected ="""fandango:ERROR: Population did not converge to a perfect population +fandango:ERROR: Only found 0 perfect solutions, instead of the required 10 +""" + out, err, code = self.run_command(command) + self.assertEqual(0, code) + self.assertEqual("", out) + self.assertEqual(expected, err) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 5323c7f1..5278d0e4 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -405,6 +405,22 @@ def test_indirect_children(self): example = grammar.parse("11") self.assertTrue(constraint.check(example)) + def test_accessing_children(self): + grammar = """ + ::= ; + ::= | ; + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; +""" + constraint = "int([0].) == 0" + grammar, constraint = parse(grammar + constraint) + constraint = constraint[0] + + counter_example = grammar.parse("11") + self.assertFalse(constraint.check(counter_example)) + + example = grammar.parse("01") + self.assertTrue(constraint.check(example)) + def test_complex_constraint(self): grammar = """ ::= ; From 52e1f3f02f9af78e35f2c3ae12bbed4ab06da477 Mon Sep 17 00:00:00 2001 From: Tim Scheckenbach Date: Tue, 7 Jan 2025 09:24:20 +0100 Subject: [PATCH 03/28] fixed test, fixed #188 but not allowing [0][0] --- src/fandango/language/parse.py | 15 +++++++++------ tests/test_constraints.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/fandango/language/parse.py b/src/fandango/language/parse.py index 7882300a..df2f0532 100644 --- a/src/fandango/language/parse.py +++ b/src/fandango/language/parse.py @@ -118,11 +118,13 @@ def check_constraints_existence(grammar, constraints): error.add_note(f"Possible symbols: {defined_symbols_str}") raise error - parent = constraint_matches_children[0] + #parent = constraint_matches_children[0] for i in range(len(constraint_matches_children) - 1): + parent = constraint_matches_children[i] + LOGGER.debug(f"Parent: {parent}") parent_sym = parent[0] if parent[1] == "": - symbol = constraint_matches[i + 1][0] + symbol = constraint_matches[i + 1] indirect = f"<{parent_sym}>..<{symbol}>" in str(value) if not check_constraints_existence_children( grammar, parent_sym, symbol, indirect, indirect_child @@ -132,7 +134,7 @@ def check_constraints_existence(grammar, constraints): else: indirect = f"<{parent_sym}>{parent[1]}..<{constraint_matches[i+1][0]}>" in str(value) if not check_constraint_existence_access( - grammar, parent_sym, parent[1][1:-1], indirect, constraint_matches[i+1][0] + grammar, parent_sym, int(parent[1][1:-1]), indirect, constraint_matches[i+1], indirect_child ): msg = f"Constraint {constraint}: <{parent_sym}> has no child <{symbol}>" raise ValueError(msg) @@ -140,7 +142,8 @@ def check_constraints_existence(grammar, constraints): -def check_constraint_existence_access(grammar, parent, id, recurse, next): +def check_constraint_existence_access(grammar, parent, id, recurse, next, indirect_child): + LOGGER.debug(f"In Access with {parent}, {id}, {next}") rules = grammar.rules[NonTerminal(f"<{parent}>")] if isinstance(rules, Alternative): child = None @@ -154,11 +157,11 @@ def check_constraint_existence_access(grammar, parent, id, recurse, next): if child is None: raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") else: - nonterminals = re.findall(r"<([^>]*)>", str(rule)) + nonterminals = re.findall(r"<([^>]*)>", str(rules)) if len(nonterminals) < id: raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") child = nonterminals[id] - return check_constraints_existence_children(grammar, child, next, recurse) + return check_constraints_existence_children(grammar, child, next, recurse, indirect_child) return False diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 5278d0e4..7275b2a5 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -411,7 +411,7 @@ def test_accessing_children(self): ::= | ; ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; """ - constraint = "int([0].) == 0" + constraint = "int([0].) == 0;" grammar, constraint = parse(grammar + constraint) constraint = constraint[0] From 09b21765b8717426695e046cae42bd5818a6d9d8 Mon Sep 17 00:00:00 2001 From: Tim Scheckenbach Date: Tue, 7 Jan 2025 11:08:43 +0100 Subject: [PATCH 04/28] removed debug statements --- src/fandango/language/parse.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/fandango/language/parse.py b/src/fandango/language/parse.py index df2f0532..5336d13e 100644 --- a/src/fandango/language/parse.py +++ b/src/fandango/language/parse.py @@ -96,11 +96,10 @@ def check_constraints_existence(grammar, constraints): constraint_symbols = constraint.get_symbols() for value in constraint_symbols: - LOGGER.debug(f"Constraint {constraint}: Checking {value}") + #LOGGER.debug(f"Constraint {constraint}: Checking {value}") constraint_matches = re.findall(r"<([^>]*)>", str(value)) # was <(.*?)> then <([^>]*)> constraint_matches_children = re.findall(r"<([^>]*)>(\[.\])*", str(value)) - LOGGER.debug(f"Found following constraint matches: {str(constraint_matches_children)}") missing = [ match for match in constraint_matches if match not in grammar_matches @@ -118,10 +117,8 @@ def check_constraints_existence(grammar, constraints): error.add_note(f"Possible symbols: {defined_symbols_str}") raise error - #parent = constraint_matches_children[0] for i in range(len(constraint_matches_children) - 1): parent = constraint_matches_children[i] - LOGGER.debug(f"Parent: {parent}") parent_sym = parent[0] if parent[1] == "": symbol = constraint_matches[i + 1] @@ -143,7 +140,6 @@ def check_constraints_existence(grammar, constraints): def check_constraint_existence_access(grammar, parent, id, recurse, next, indirect_child): - LOGGER.debug(f"In Access with {parent}, {id}, {next}") rules = grammar.rules[NonTerminal(f"<{parent}>")] if isinstance(rules, Alternative): child = None @@ -169,8 +165,6 @@ def check_constraint_existence_access(grammar, parent, id, recurse, next, indire def check_constraints_existence_children( grammar, parent, symbol, recurse, indirect_child ): - LOGGER.debug(f"Checking {parent}, {symbol}") - if indirect_child[f"<{parent}>"][f"<{symbol}>"] is not None: return indirect_child[f"<{parent}>"][f"<{symbol}>"] From 46944851aede61deffe61fc06cba2acb3075fddf Mon Sep 17 00:00:00 2001 From: Tim Scheckenbach Date: Tue, 7 Jan 2025 11:10:27 +0100 Subject: [PATCH 05/28] more tests #173 --- tests/test_grammar.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_grammar.py b/tests/test_grammar.py index c8cf0c46..490d15e0 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -6,6 +6,7 @@ from fandango.language.grammar import Disambiguator, Node, NonTerminalNode, Grammar from fandango.language.parse import parse from fandango.language.tree import DerivationTree +from fandango.evolution.algorithm import Fandango class ConstraintTest(unittest.TestCase): @@ -41,3 +42,51 @@ def test_parse(self): for path in grammar.traverse_derivation(tree): print(path) + def get_solutions(self, grammar): + grammar, constraints = parse(grammar) + fandango = Fandango(grammar=grammar, constraints=constraints, desired_solutions=10) + return fandango.evolve() + + def test_generators(self): + GRAMMAR = """ + ::= * := test(); + ::= "a" | "b" | "c" | "r"; + +def test(): + return "bar" +""" + expected = ["bar" for _ in range(10)] + actual = self.get_solutions(GRAMMAR) + + self.assertEqual(expected, actual) + + def test_repetitions(self): + GRAMMAR = """ + ::= {3}; + ::= "a"; +""" + expected = ["aaa" for _ in range(10)] + actual = self.get_solutions(GRAMMAR) + + self.assertEqual(expected, actual) + + def test_repetitions_slice(self): + GRAMMAR = """ + ::= {3, 10}; + ::= "a"; +""" + solutions = self.get_solutions(GRAMMAR) + for solution in solutions: + self.assertGreaterEqual(len(str(solution)), 3) + self.assertLessEqual(len(str(solution)), 10) + + def test_repetition_min(self): + GRAMMAR = """ + ::= {3, }; + ::= "a"; +""" + solutions = self.get_solutions(GRAMMAR) + for solution in solutions: + self.assertGreaterEqual(len(str(solution)), 3) + + From 74de8b76a788d25e17e88d9f46198bf064462ac6 Mon Sep 17 00:00:00 2001 From: joszamama Date: Tue, 14 Jan 2025 21:13:44 +0100 Subject: [PATCH 06/28] fix: dependency issues --- .github/workflows/deploy-book.yml | 2 +- docs/_config.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index 5d7f2394..f84a2381 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -29,7 +29,7 @@ jobs: - name: Install dependencies run: | pip install -e . - pip install jupyter-book pyppeteer ghp-import black graphviz + pip install jupyter-book pyppeteer ghp-import black - name: Locate Fandango and add to PATH run: | diff --git a/docs/_config.yml b/docs/_config.yml index 44381e78..2fe98c73 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -14,7 +14,7 @@ latex: execute: execute_notebooks: cache stderr_output: warn - timeout: 600 + timeout: 300 parse: myst_enable_extensions: From 11a7dae6ad7d09ac3613a92033f08f9567259d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 22:09:24 +0100 Subject: [PATCH 07/28] Create SECURITY.md --- SECURITY.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..3a764c22 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,49 @@ +# Security Policy + +## Supported Versions + +We actively maintain and provide security updates for the following versions of this project: + +| Version | Supported | +|-----------|--------------------| +| 0.0.5 | ✅ Fully supported | +| 0.0.4 | ❌ No longer supported | +| 0.0.3 | ❌ No longer supported | +| 0.0.2 | ❌ No longer supported | +| 0.0.1 | ❌ No longer supported | + +Please update to the latest version to ensure you receive security fixes and improvements. + +--- + +## Reporting a Vulnerability + +If you find a security vulnerability in this project, we strongly encourage you to report it as soon as possible. Please follow the steps below: + +1. **Do not publicly disclose the vulnerability** until we have had a chance to address it. +2. Contact us directly via **[fandango-fuzzer@protonmail.com](mailto:fandango-fuzzer@protonmail.com)** with the following details: + - A detailed description of the vulnerability. + - Steps to reproduce the issue, if applicable. + - The impact or potential impact of the vulnerability. + +We will confirm receipt of your report within 48 hours and provide a timeline for resolving the issue. + +--- + +## Security Measures + +To help ensure the security of this project, we: +- Use **best practices** for secure coding. +- Regularly update dependencies to address vulnerabilities. +- Monitor the project for potential security threats. + +--- + +## Coordinated Disclosure + +We follow a coordinated disclosure process. Once a vulnerability has been verified and fixed, we will: +1. Acknowledge the contribution of the reporter (if agreed upon). +2. Publicly disclose the details in a **security advisory**. +3. Notify affected users, if applicable. + +Thank you for helping us keep this project secure. From a87df231213398f8aa1ec182bc3a089ee21a9494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 22:17:27 +0100 Subject: [PATCH 08/28] fix: typo in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14e09c59..c3fd5431 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Here, you'll find the following sections: - [Statistical Distributions](https://fandango-fuzzer.github.io/fandango/Distributions.html) - [Coverage-Guided Fuzzing](https://fandango-fuzzer.github.io/fandango/Whitebox.html) - [Hatching Specs](https://fandango-fuzzer.github.io/fandango/Hatching.html) - - [Fandango Reference](https://fandango-fuzzer.github.io/fandango/Reference.html) + - [Fandango Reference](https://fandango-fuzzer.github.io/fandango/Reference.html) --- From 5ef2f694a5c08f4390fbb14f6ec8a11243f6fb0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 22:36:42 +0100 Subject: [PATCH 09/28] feat: status badges --- .github/workflows/deploy-book.yml | 2 +- .github/workflows/deploy-pypi.yml | 2 +- .github/workflows/python-tests.yml | 2 +- README.md | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index f84a2381..b4b82adf 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: Deploy Docs to GitHub Pages +name: GitHub Pages on: push: diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml index 063b6a04..7b58a97e 100644 --- a/.github/workflows/deploy-pypi.yml +++ b/.github/workflows/deploy-pypi.yml @@ -1,4 +1,4 @@ -name: Publish to PyPI +name: PyPI on: release: diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index b8634822..723f47e1 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -1,7 +1,7 @@ # GitHub Actions Workflow: Python Tests with Caching # This workflow automates running tests on Python code using pytest and speeds up the process using caching. -name: Python Tests # Workflow name, visible in the Actions tab +name: Tests on: push: diff --git a/README.md b/README.md index c3fd5431..a442c1a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # FANDANGO: Evolving Language-Based Testing +[![Python Tests](https://github.com/fandango-fuzzer/fandango/actions/workflows/python-tests.yml/badge.svg)](https://github.com/fandango-fuzzer/fandango/actions/workflows/python-tests.yml) +[![GitHub Pages](https://github.com/fandango-fuzzer/fandango/actions/workflows/deploy-book.yml/badge.svg)](https://github.com/fandango-fuzzer/fandango/actions/workflows/deploy-book.yml) +[![CodeQL](https://github.com/fandango-fuzzer/fandango/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/fandango-fuzzer/fandango/actions/workflows/github-code-scanning/codeql) +[![PyPI](https://github.com/fandango-fuzzer/fandango/actions/workflows/deploy-pypi.yml/badge.svg)](https://github.com/fandango-fuzzer/fandango/actions/workflows/deploy-pypi.yml) + + FANDANGO is a language-based fuzzer that leverages formal input specifications (grammars) combined with constraints to generate diverse sets of valid inputs for programs under test. Unlike traditional symbolic constraint solvers, FANDANGO uses a search-based approach to systematically evolve a population of inputs through syntactically valid mutations until semantic input constraints are satisfied. ## Table of Contents From 150298b2c411b89b7f85c18c2996b9b71a7ba8ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:01:09 +0100 Subject: [PATCH 10/28] test: book url --- .github/workflows/deploy-book.yml | 35 +++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index b4b82adf..84f8ce33 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: GitHub Pages +name: GitHub Pages Deployment on: push: @@ -9,7 +9,7 @@ on: workflow_dispatch: # Enables manual execution from the Actions tab permissions: - pages: write + contents: write # Needed for pushing to the other repository id-token: write jobs: @@ -42,13 +42,26 @@ jobs: run: | jupyter-book build docs - # Upload the book's HTML as an artifact - - name: Upload artifact - uses: actions/upload-pages-artifact@v2 - with: - path: "docs/_build/html" + # Push built HTML to fandango-fuzzer.github.io repository + - name: Deploy to fandango-fuzzer.github.io + env: + TARGET_REPO: fandango-fuzzer/fandango-fuzzer.github.io + TARGET_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Clone the target repository + git clone --depth 1 https://github.com/${{ env.TARGET_REPO }} target-repo + cd target-repo + + # Remove all existing files + git rm -rf . || true + + # Copy new HTML files + cp -r ../docs/_build/html/* . - # Deploy the book's HTML to GitHub Pages - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 + # Commit and push changes + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git add . + git commit -m "Update GitHub Pages site" + git push origin ${{ env.TARGET_BRANCH }} From ee727c8b1ac3356030cb480212b913938f6baa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:12:32 +0100 Subject: [PATCH 11/28] test: book url --- .github/workflows/deploy-book.yml | 56 +++++++++++++++++-------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index 84f8ce33..0d99bbff 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: GitHub Pages Deployment +name: Publish GitHub Pages on: push: @@ -6,62 +6,66 @@ on: - main paths: - docs/** - workflow_dispatch: # Enables manual execution from the Actions tab + workflow_dispatch: # Allows manual triggering from the Actions tab permissions: - contents: write # Needed for pushing to the other repository + pages: write id-token: write jobs: - deploy-book: + deploy: runs-on: ubuntu-latest steps: - - name: Checkout the repository + # Step 1: Checkout the source repository + - name: Checkout the source repository uses: actions/checkout@v3 - # Install dependencies + # Step 2: Set up Python - name: Set up Python 3.13 uses: actions/setup-python@v4 with: python-version: 3.13 + # Step 3: Install dependencies - name: Install dependencies run: | pip install -e . pip install jupyter-book pyppeteer ghp-import black + # Step 4: Locate Fandango and add to PATH - name: Locate Fandango and add to PATH run: | - which fandango + which fandango echo "/opt/hostedtoolcache/Python/3.13.1/x64/bin/fandango" >> $GITHUB_PATH fandango --help - # Build the book + # Step 5: Build the documentation - name: Build the book run: | jupyter-book build docs - # Push built HTML to fandango-fuzzer.github.io repository - - name: Deploy to fandango-fuzzer.github.io - env: - TARGET_REPO: fandango-fuzzer/fandango-fuzzer.github.io - TARGET_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Step 6: Clone the target repository + - name: Clone the target repository run: | - # Clone the target repository - git clone --depth 1 https://github.com/${{ env.TARGET_REPO }} target-repo + git clone https://github.com/fandango-fuzzer/fandango-fuzzer.github.io.git target-repo cd target-repo - - # Remove all existing files - git rm -rf . || true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Copy new HTML files - cp -r ../docs/_build/html/* . + # Step 7: Copy the built HTML files to the target repository + - name: Copy built files to the target repository + run: | + cp -r docs/_build/html/* target-repo/ - # Commit and push changes - git config user.name "GitHub Actions" - git config user.email "actions@github.com" + # Step 8: Commit and push changes to the target repository + - name: Deploy to GitHub Pages + run: | + cd target-repo + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" git add . - git commit -m "Update GitHub Pages site" - git push origin ${{ env.TARGET_BRANCH }} + git commit -m "Update GitHub Pages site" || echo "No changes to commit" + git push origin main + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From cee59541f21cad38a9b68b1e399642d732ce5fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:13:03 +0100 Subject: [PATCH 12/28] test: book url --- .github/workflows/deploy-book.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index 0d99bbff..e13eba8f 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: Publish GitHub Pages +name: GitHub Pages on: push: From f51546ae36cff90d4973dbb8faf220d3f5d48d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:18:06 +0100 Subject: [PATCH 13/28] test: book url --- .github/workflows/deploy-book.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index e13eba8f..ec36bd67 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -48,20 +48,19 @@ jobs: # Step 6: Clone the target repository - name: Clone the target repository run: | - git clone https://github.com/fandango-fuzzer/fandango-fuzzer.github.io.git target-repo - cd target-repo + git clone https://github.com/fandango-fuzzer/fandango-fuzzer.github.io.git gh-pages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Step 7: Copy the built HTML files to the target repository - name: Copy built files to the target repository run: | - cp -r docs/_build/html/* target-repo/ + cp -r docs/_build/html/* gh-pages/ # Step 8: Commit and push changes to the target repository - name: Deploy to GitHub Pages run: | - cd target-repo + cd gh-pages git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add . From c51bda1ddaea0696c2cdcf3a3d6afe4a5af55c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:30:46 +0100 Subject: [PATCH 14/28] test: book url --- .github/workflows/deploy-book.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index ec36bd67..05138013 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -57,6 +57,7 @@ jobs: run: | cp -r docs/_build/html/* gh-pages/ + # Step 8: Commit and push changes to the target repository # Step 8: Commit and push changes to the target repository - name: Deploy to GitHub Pages run: | @@ -65,6 +66,6 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add . git commit -m "Update GitHub Pages site" || echo "No changes to commit" - git push origin main + git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/fandango-fuzzer/fandango-fuzzer.github.io.git main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 374ac504d56e0479e202fee446d6efd4d5eb3faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:51:08 +0100 Subject: [PATCH 15/28] test: book url --- .github/workflows/deploy-book.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index 05138013..1e8e042f 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: GitHub Pages +name: Publish GitHub Pages on: push: @@ -9,8 +9,7 @@ on: workflow_dispatch: # Allows manual triggering from the Actions tab permissions: - pages: write - id-token: write + contents: write jobs: deploy: @@ -49,23 +48,18 @@ jobs: - name: Clone the target repository run: | git clone https://github.com/fandango-fuzzer/fandango-fuzzer.github.io.git gh-pages - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Step 7: Copy the built HTML files to the target repository - name: Copy built files to the target repository run: | cp -r docs/_build/html/* gh-pages/ - # Step 8: Commit and push changes to the target repository # Step 8: Commit and push changes to the target repository - name: Deploy to GitHub Pages run: | cd gh-pages - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" + git config --global user.name "${{ secrets.CI_COMMIT_AUTHOR }}" + git config --global user.email "${{ secrets.CI_COMMIT_EMAIL }}" git add . git commit -m "Update GitHub Pages site" || echo "No changes to commit" - git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/fandango-fuzzer/fandango-fuzzer.github.io.git main - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + git push https://x-access-token:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/fandango-fuzzer/fandango-fuzzer.github.io.git main \ No newline at end of file From 1e88f35f8da542979b93832b3bcdebb0cdbef7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Tue, 14 Jan 2025 23:52:08 +0100 Subject: [PATCH 16/28] test: book url --- .github/workflows/deploy-book.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index 1e8e042f..f15d1b2d 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -1,4 +1,4 @@ -name: Publish GitHub Pages +name: GitHub Pages on: push: From 2db1e60b34cc7d723036264f8ff9536cf227a4c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:00:00 +0100 Subject: [PATCH 17/28] test: book url --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index 2fe98c73..e0b8ae3f 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -34,7 +34,7 @@ html: announcement: '

💃 Fandango is currently in development - see our list of bugs and incomplete features. Expect a first stable release in April 2025.

' use_issues_button: true use_repository_button: true - baseurl: "https://fandango-fuzzer.github.io/fandango/" + baseurl: "https://fandango-fuzzer.github.io/" repository: url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From 0f73c82bf5aeeeb9b09f4deecb9205f24bf2eb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:14:22 +0100 Subject: [PATCH 18/28] test: book url --- docs/_config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index e0b8ae3f..155cb0a7 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -34,7 +34,7 @@ html: announcement: '

💃 Fandango is currently in development - see our list of bugs and incomplete features. Expect a first stable release in April 2025.

' use_issues_button: true use_repository_button: true - baseurl: "https://fandango-fuzzer.github.io/" + baseurl: "/" repository: url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From 71ddbfeea1f8f351f282c12cadf6ebb0ea5c4a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:26:14 +0100 Subject: [PATCH 19/28] test: book url --- docs/_config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_config.yml b/docs/_config.yml index 155cb0a7..e3de06e9 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -35,6 +35,7 @@ html: use_issues_button: true use_repository_button: true baseurl: "/" + use_relative_urls: true repository: url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From c2fcf48a92b38316a502de9dfd7b9d2ddbe55720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:39:38 +0100 Subject: [PATCH 20/28] test: book url --- docs/_config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index e3de06e9..155cb0a7 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -35,7 +35,6 @@ html: use_issues_button: true use_repository_button: true baseurl: "/" - use_relative_urls: true repository: url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From 44fcc6cfaa29f5f42b026182664a43fe0778ff63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:45:27 +0100 Subject: [PATCH 21/28] test: book url --- docs/_config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index 155cb0a7..8f318b93 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -34,7 +34,6 @@ html: announcement: '

💃 Fandango is currently in development - see our list of bugs and incomplete features. Expect a first stable release in April 2025.

' use_issues_button: true use_repository_button: true - baseurl: "/" repository: url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From 94a882e53208265b5671157ae58722f7238e31d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:45:53 +0100 Subject: [PATCH 22/28] test: book url --- docs/_config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/_config.yml b/docs/_config.yml index 8f318b93..b09f03a1 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -33,7 +33,4 @@ parse: html: announcement: '

💃 Fandango is currently in development - see our list of bugs and incomplete features. Expect a first stable release in April 2025.

' use_issues_button: true - use_repository_button: true - -repository: - url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file + use_repository_button: true \ No newline at end of file From f3b8a470a792bd6a2208de3f1a95f7d686750cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 00:47:45 +0100 Subject: [PATCH 23/28] test: book url --- docs/_config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/_config.yml b/docs/_config.yml index b09f03a1..8f318b93 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -33,4 +33,7 @@ parse: html: announcement: '

💃 Fandango is currently in development - see our list of bugs and incomplete features. Expect a first stable release in April 2025.

' use_issues_button: true - use_repository_button: true \ No newline at end of file + use_repository_button: true + +repository: + url: "https://github.com/fandango-fuzzer/fandango" \ No newline at end of file From a4e2f4e4a1668e1942c85467b104860595817110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 01:25:22 +0100 Subject: [PATCH 24/28] feat: new release --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c4f6d037..2ab8952f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta" [project] name = "fandango-fuzzer" -version = "0.0.5" +version = "0.1.0" authors = [ { name = "José Antonio Zamudio Amaya", email = "jose.zamudio@cispa.de" }, { name = "Marius Smytzek", email = "marius.smytzek@cispa.de" }, From c3f37f28535723920c374523b56fa68b0ca12d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 09:54:54 +0100 Subject: [PATCH 25/28] fix: parser rollback --- src/fandango/language/parse.py | 669 ++++++++++++------ ...56bac2cb13abdfd209e2873361d3ef3e543.pickle | 0 ...c49fe83ded6aa032981786afe0426cff52a.pickle | Bin 0 -> 8248 bytes ...363806cef43a3ad2a767ff38322e7b3bd7b.pickle | Bin 0 -> 10135 bytes ...6767cedfc4410246e3888104e43268bf487.pickle | Bin 0 -> 8546 bytes ...1e05caf92738e7629e5796451b058761f2f.pickle | Bin 0 -> 8812 bytes ...528ad11d53e6b79784de156984e49631e5b.pickle | Bin 0 -> 7849 bytes ...e0910b07c25bd85e95277931b33a6571088.pickle | Bin 0 -> 8525 bytes ...ee03b8e42a7dc5314a5684a4862566768b1.pickle | Bin 0 -> 8753 bytes ...a3a5348112e2a6469ddbc9382e298a51020.pickle | Bin 0 -> 9807 bytes tests/.fandango_cache/CACHEDIR.TAG | 1 + ...2f338b3a714c40fbf60d723970d6bc94b84.pickle | Bin 0 -> 8927 bytes ...bd16fad303bad5d66ecfb7c959ba924e556.pickle | Bin 0 -> 8552 bytes ...5dc2327a424d5304e2021db949c8e42c148.pickle | Bin 0 -> 8937 bytes ...b1be4ff953cfe0482a23500fdec7f010b6c.pickle | 0 ...e03072e75bdeff2829850d334b2c3b13a0d.pickle | Bin 0 -> 8220 bytes ...6e00deb7733099d280e2716f391b8fecf4e.pickle | Bin 0 -> 8926 bytes ...252f6b3fcc6f77ed5f0c9d5d131acf03517.pickle | Bin 0 -> 8606 bytes 18 files changed, 450 insertions(+), 220 deletions(-) create mode 100644 tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle create mode 100644 tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle create mode 100644 tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle create mode 100644 tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle create mode 100644 tests/.fandango_cache/511d364a2dbae25b4704db77013c11e05caf92738e7629e5796451b058761f2f.pickle create mode 100644 tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle create mode 100644 tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle create mode 100644 tests/.fandango_cache/7bad955f7612264662929d472bbd2ee03b8e42a7dc5314a5684a4862566768b1.pickle create mode 100644 tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle create mode 100644 tests/.fandango_cache/CACHEDIR.TAG create mode 100644 tests/.fandango_cache/ac0afb847e10c18a3abe62a28d35f2f338b3a714c40fbf60d723970d6bc94b84.pickle create mode 100644 tests/.fandango_cache/baf5d9c51ca38faac740b61c42a7abd16fad303bad5d66ecfb7c959ba924e556.pickle create mode 100644 tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle create mode 100644 tests/.fandango_cache/d8ee33bdaa6a4f44d23be4ddfe1c0b1be4ff953cfe0482a23500fdec7f010b6c.pickle create mode 100644 tests/.fandango_cache/dc4092715f58afb4223b63e27dec3e03072e75bdeff2829850d334b2c3b13a0d.pickle create mode 100644 tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle create mode 100644 tests/.fandango_cache/fff8fd93fd0091eb80cccef89b662252f6b3fcc6f77ed5f0c9d5d131acf03517.pickle diff --git a/src/fandango/language/parse.py b/src/fandango/language/parse.py index 5336d13e..23d90181 100644 --- a/src/fandango/language/parse.py +++ b/src/fandango/language/parse.py @@ -1,204 +1,145 @@ import ast -import re -import os -import sys +import hashlib import importlib.metadata +import os +import platform +import re +from copy import deepcopy +from pathlib import Path +from typing import IO, Any, List, Optional, Set, Tuple -from typing import Tuple, List, Any -from fandango.logger import LOGGER, print_exception - -from antlr4 import InputStream, CommonTokenStream - -import hashlib -import dill as pickle -from xdg_base_dirs import xdg_cache_home import cachedir_tag +import dill as pickle +from antlr4 import CommonTokenStream, InputStream +from antlr4.error.ErrorListener import ErrorListener +from thefuzz import process as thefuzz_process +from xdg_base_dirs import xdg_cache_home, xdg_data_dirs, xdg_data_home from fandango.constraints import predicates -from fandango.constraints.base import Constraint from fandango.language.convert import ( + ConstraintProcessor, FandangoSplitter, GrammarProcessor, - ConstraintProcessor, PythonProcessor, ) -from fandango.language.grammar import Grammar, NodeType, Alternative +from fandango.language.grammar import Grammar, NodeType from fandango.language.parser.FandangoLexer import FandangoLexer from fandango.language.parser.FandangoParser import FandangoParser +from fandango.language.stdlib import stdlib from fandango.language.symbol import NonTerminal - -from antlr4.error.ErrorListener import ErrorListener -from antlr4.error.Errors import ParseCancellationException +from fandango.logger import LOGGER, print_exception class MyErrorListener(ErrorListener): - def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): - raise ParseCancellationException(f"Line %{line}, Column {column}: error: {msg}") - -def check_grammar(grammar, start_symbol=""): - if not grammar: - return - - LOGGER.debug("Checking grammar") - - used_symbols = set() - undefined_symbols = set() - defined_symbols = set() - - for symbol in grammar.rules.keys(): - defined_symbols.add(symbol) + """This is invoked from ANTLR when a syntax error is encountered""" - def collect_used_symbols(tree): - if tree.node_type == NodeType.NON_TERMINAL: - used_symbols.add(tree.symbol) - if tree.node_type == NodeType.REPETITION: - collect_used_symbols(tree.node) - for child in tree.children(): - collect_used_symbols(child) + def __init__(self, filename=None): + self.filename = filename + super().__init__() - for tree in grammar.rules.values(): - collect_used_symbols(tree) - - for symbol in used_symbols: - if symbol not in defined_symbols: - undefined_symbols.add(symbol) - - for symbol in defined_symbols: - if symbol not in used_symbols and str(symbol) != start_symbol: - LOGGER.info(f"Symbol {symbol} defined, but not used") - - if undefined_symbols: - for symbol in grammar.rules.keys(): - defined_symbols_str = ", ".join(symbol) - - error = ValueError(f"Undefined symbols {undefined_symbols} in grammar") - error.add_note(f"Possible symbols: {defined_symbols_str}") - raise error - - -def check_constraints_existence(grammar, constraints): - LOGGER.debug("Checking constraints") + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + raise SyntaxError(f"{repr(self.filename)}, line {line}, column {column}: {msg}") - indirect_child = { - str(k): {str(l): None for l in grammar.rules.keys()} - for k in grammar.rules.keys() - } - defined_symbols = [] - for symbol in grammar.rules.keys(): - defined_symbols.append(str(symbol)) - defined_symbols_str = ", ".join(defined_symbols) +def closest_match(word, candidates): + """ + `word` raises a syntax error; + return alternate suggestion for `word` from `candidates` + """ + return thefuzz_process.extractOne(word, candidates)[0] - grammar_symbols = grammar.rules.keys() - grammar_matches = re.findall(r"<([^>]*)>", str(grammar_symbols)) - # LOGGER.debug(f"All used symbols: {grammar_matches}") - for constraint in constraints: - constraint_symbols = constraint.get_symbols() +### Including Files - for value in constraint_symbols: - #LOGGER.debug(f"Constraint {constraint}: Checking {value}") +# Some global variables for `include()`, bwlow - constraint_matches = re.findall(r"<([^>]*)>", str(value)) # was <(.*?)> then <([^>]*)> - constraint_matches_children = re.findall(r"<([^>]*)>(\[.\])*", str(value)) - - missing = [ - match for match in constraint_matches if match not in grammar_matches - ] +# The current file name, for error messages +CURRENT_FILENAME: str = "" - if len(missing) > 1: - missing_symbols = ", ".join([ '<' + symbol + '>' for symbol in missing ]) - error = ValueError(f"Constraint {constraint}: undefined symbols {missing_symbols}") - error.add_note(f"Possible symbols: {defined_symbols_str}") - raise error +# The list of directories to search for include files +INCLUDES: List[str] = [] - if len(missing) == 1: - missing_symbol = missing[0] - error = ValueError(f"Constraint {constraint}: undefined symbol <{missing_symbol}>") - error.add_note(f"Possible symbols: {defined_symbols_str}") - raise error +# The list of files to parse, with their include depth. +# An include depth of 0 means the file was given as input. +# A higher include depth means the file was included from another file; +# hence its grammar and constraints should be processed _before_ the current file. +FILES_TO_PARSE: List[Tuple[IO, int]] = [] - for i in range(len(constraint_matches_children) - 1): - parent = constraint_matches_children[i] - parent_sym = parent[0] - if parent[1] == "": - symbol = constraint_matches[i + 1] - indirect = f"<{parent_sym}>..<{symbol}>" in str(value) - if not check_constraints_existence_children( - grammar, parent_sym, symbol, indirect, indirect_child - ): - msg = f"Constraint {constraint}: <{parent_sym}> has no child <{symbol}>" - raise ValueError(msg) - else: - indirect = f"<{parent_sym}>{parent[1]}..<{constraint_matches[i+1][0]}>" in str(value) - if not check_constraint_existence_access( - grammar, parent_sym, int(parent[1][1:-1]), indirect, constraint_matches[i+1], indirect_child - ): - msg = f"Constraint {constraint}: <{parent_sym}> has no child <{symbol}>" - raise ValueError(msg) - - - - -def check_constraint_existence_access(grammar, parent, id, recurse, next, indirect_child): - rules = grammar.rules[NonTerminal(f"<{parent}>")] - if isinstance(rules, Alternative): - child = None - for rule in rules: - nonterminals = re.findall(r"<([^>]*)>", str(rule)) - if len(nonterminals) < id: - continue - child = nonterminals[id] - if check_constraints_existence_children(grammar, child, next, recurse): - return True - if child is None: - raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") - else: - nonterminals = re.findall(r"<([^>]*)>", str(rules)) - if len(nonterminals) < id: - raise IndexError(f"Nonterminal <{parent}> has no {id}-th children") - child = nonterminals[id] - return check_constraints_existence_children(grammar, child, next, recurse, indirect_child) - return False - +# The current include depth +INCLUDE_DEPTH: int = 0 -def check_constraints_existence_children( - grammar, parent, symbol, recurse, indirect_child -): - if indirect_child[f"<{parent}>"][f"<{symbol}>"] is not None: - return indirect_child[f"<{parent}>"][f"<{symbol}>"] +def include(file_to_be_included: str): + """ + Include FILE_TO_BE_INCLUDED in the current context. + This function is invoked from .fan files. + """ + global FILES_TO_PARSE + global CURRENT_FILENAME + global INCLUDE_DEPTH + + path = os.path.dirname(CURRENT_FILENAME) + if not path: + path = "." # For strings and standard input + if INCLUDES: + path += ":" + ":".join(INCLUDES) + if os.environ.get("FANDANGO_PATH"): + path += ":" + os.environ["FANDANGO_PATH"] + dirs = [Path(dir) for dir in path.split(":")] + + if platform.system() == "Darwin": + dirs += [Path.home() / "Library" / "Fandango"] # ~/Library/Fandango + dirs += [Path("/Library/Fandango")] # /Library/Fandango + + dirs += [xdg_data_home() / "fandango"] # sth like ~/.local/share/fandango + dirs += [ + dir / "fandango" for dir in xdg_data_dirs() + ] # sth like /usr/local/share/fandango + + for dir in dirs: + try: + full_file_name = dir / file_to_be_included + full_file = open(full_file_name, "r") + except FileNotFoundError: + continue + LOGGER.debug(f"{CURRENT_FILENAME}: including {full_file_name}") + + INCLUDE_DEPTH += 1 # Will be lowered when the included file is done processing + FILES_TO_PARSE.append((full_file, INCLUDE_DEPTH)) + return - grammar_symbols = grammar.rules[NonTerminal(f"<{parent}>")] - grammar_matches = re.findall(r'(?]*)>(?!.*")', str(grammar_symbols)) + raise FileNotFoundError( + f"{CURRENT_FILENAME}: {repr(file_to_be_included)} not found in {':'.join(str(dir) for dir in dirs)}" + ) - if symbol not in grammar_matches: - if recurse: - is_child = False - for match in grammar_matches: - is_child = is_child or check_constraints_existence_children( - grammar, match, symbol, recurse, indirect_child - ) - indirect_child[f"<{parent}>"][f"<{symbol}>"] = is_child - return is_child - else: - return False - indirect_child[f"<{parent}>"][f"<{symbol}>"] = True - return True +### Parsing class FandangoSpec: + """ + Helper class to pickle and unpickle parsed Fandango specifications. + This is necessary because the ANTLR4 parse trees cannot be pickled, + so we pickle the code text, grammar, and constraints instead. + """ + GLOBALS = predicates.__dict__ + GLOBALS.update({"include": include}) LOCALS = None # Must be None to ensure top-level imports - def __init__(self, tree: Any, fan_contents: str, lazy: bool = False): - self.version = importlib.metadata.version("fandango") + def __init__( + self, + tree: Any, + fan_contents: str, + lazy: bool = False, + filename: str = "", + ): + self.version = importlib.metadata.version("fandango-fuzzer") self.fan_contents = fan_contents self.global_vars = self.GLOBALS.copy() self.local_vars = self.LOCALS self.lazy = lazy - LOGGER.debug("Extracting code") + LOGGER.debug(f"{filename}: extracting code") splitter = FandangoSplitter() splitter.visit(tree) python_processor = PythonProcessor() @@ -206,107 +147,121 @@ def __init__(self, tree: Any, fan_contents: str, lazy: bool = False): ast.fix_missing_locations(code_tree) self.code_text = ast.unparse(code_tree) - LOGGER.debug("Running code") - self.run_code() + LOGGER.debug(f"{filename}: running code") + self.run_code(filename=filename) - LOGGER.debug("Extracting grammar") + LOGGER.debug(f"{filename}: extracting grammar") grammar_processor = GrammarProcessor( - local_variables=self.local_vars, - global_variables=self.global_vars + local_variables=self.local_vars, global_variables=self.global_vars + ) + self.grammar: Grammar = grammar_processor.get_grammar( + splitter.productions, prime=False ) - self.grammar: Grammar = \ - grammar_processor.get_grammar(splitter.productions) - LOGGER.debug("Extracting constraints") + LOGGER.debug(f"{filename}: extracting constraints") constraint_processor = ConstraintProcessor( self.grammar, local_variables=self.local_vars, global_variables=self.global_vars, lazy=self.lazy, ) - self.constraints: List[Constraint] = \ - constraint_processor.get_constraints(splitter.constraints) + self.constraints: List[str] = constraint_processor.get_constraints( + splitter.constraints + ) + + def run_code(self, filename: str = ""): + global CURRENT_FILENAME + CURRENT_FILENAME = filename - def run_code(self): exec(self.code_text, self.global_vars, self.local_vars) -def parse(fan_contents: str, /, lazy: bool = False, - check_constraints: bool = True, given_grammar=None, use_cache: bool = False) -> Tuple[Grammar, List[Constraint]]: +def parse_content( + fan_contents: str, + *, + filename: str = "", + use_cache: bool = True, + lazy: bool = False, +) -> Tuple[Grammar, List[str]]: """ - Extract grammar and constraints from the given content - :param fan: Fandango specification + Parse given content into a grammar and constraints. + This is a helper function; use `parse()` as the main entry point. + :param fan_contents: Fandango specification text + :param filename: The file name of the content (for error messages) + :param use_cache: If True (default), cache parsing results. :param lazy: If True, the constraints are evaluated lazily - :param check_constraints: If True, check if the constraints contain non-terminal symbols that are not in the grammar + :return: A tuple of the grammar and constraints """ + spec: Optional[FandangoSpec] = None from_cache = False CACHE_DIR = xdg_cache_home() / "fandango" + if platform.system() == "Darwin": + cache_path = Path.home() / "Library" / "Caches" + if os.path.exists(cache_path): + CACHE_DIR = cache_path / "Fandango" if use_cache: if not os.path.exists(CACHE_DIR): - os.makedirs(CACHE_DIR) + os.makedirs(CACHE_DIR, mode=0o700) cachedir_tag.tag(CACHE_DIR, application="Fandango") hash = hashlib.sha256(fan_contents.encode()).hexdigest() - pickle_file = CACHE_DIR / (hash + '.pickle') + pickle_file = CACHE_DIR / (hash + ".pickle") if os.path.exists(pickle_file): try: - with open(pickle_file, 'rb') as fp: - LOGGER.info(f"Loading cached spec from {pickle_file}") - spec: FandangoSpec = pickle.load(fp) + with open(pickle_file, "rb") as fp: + LOGGER.info(f"{filename}: loading cached spec from {pickle_file}") + spec = pickle.load(fp) + assert spec is not None LOGGER.debug(f"Cached spec version: {spec.version}") if spec.fan_contents != fan_contents: - e = ValueError("Hash collision") - e.add_note("If you get this, you'll be real famous") + e = ValueError( + "Hash collision (If you get this, you'll be real famous)" + ) raise e + from_cache = True except Exception as e: LOGGER.debug(type(e).__name__ + ":" + str(e)) - if from_cache: - LOGGER.debug("Running code") + if spec: + LOGGER.debug(f"{filename}: running code") try: - spec.run_code() + spec.run_code(filename=filename) except Exception as e: - print_exception(e) - # In case the error has anything to do with caching, play it safe - del spec + LOGGER.debug(f"Cached spec failed; removing {pickle_file}") os.remove(pickle_file) + raise e + + if not spec: + LOGGER.debug(f"{filename}: setting up .fan parser and lexer") + error_listener = MyErrorListener(filename) - if not from_cache: - LOGGER.debug("Setting up .fan parser") input_stream = InputStream(fan_contents) - error_listener = MyErrorListener() lexer = FandangoLexer(input_stream) + lexer.removeErrorListeners() lexer.addErrorListener(error_listener) + token_stream = CommonTokenStream(lexer) parser = FandangoParser(token_stream) + parser.removeErrorListeners() parser.addErrorListener(error_listener) - LOGGER.debug("Parsing .fan content") - tree = parser.fandango() - - LOGGER.debug("Splitting content") - spec = FandangoSpec(tree, fan_contents, lazy) + LOGGER.debug(f"{filename}: parsing .fan content") + tree = parser.fandango() # Invoke the ANTLR parser - if len(spec.grammar.rules) > 0: - check_grammar(spec.grammar) + LOGGER.debug(f"{filename}: splitting content") + spec = FandangoSpec(tree, fan_contents, lazy, filename=filename) - if check_constraints: - if not spec.grammar or len(spec.grammar.rules) == 0: - g = given_grammar - else: - g = spec.grammar - if g and len(g.rules) > 0: - check_constraints_existence(g, spec.constraints) + assert spec is not None if use_cache and not from_cache: try: - with open(pickle_file, 'wb') as fp: - LOGGER.info(f"Saving spec to cache {pickle_file}") + with open(pickle_file, "wb") as fp: + LOGGER.info(f"{filename}: saving spec to cache {pickle_file}") pickle.dump(spec, fp) except Exception as e: print_exception(e) @@ -315,22 +270,296 @@ def parse(fan_contents: str, /, lazy: bool = False, except Exception: pass - LOGGER.debug("Parsing complete") + LOGGER.debug(f"{filename}: parsing complete") return spec.grammar, spec.constraints -def parse_file(*filenames, lazy: bool = False) -> Tuple[Grammar, List[Constraint]]: - contents = "" - errors = False +# Save the set of symbols used in the standard library and imported grammars +USED_SYMBOLS: Set[str] = set() + +# Save the standard library grammar and constraints +STDLIB_GRAMMAR: Optional[Grammar] = None +STDLIB_CONSTRAINTS: Optional[List[str]] = None + + +def parse( + fan_files: str | IO | List[IO], + constraints: List[str] = None, + *, + use_cache: bool = True, + use_stdlib: bool = True, + lazy: bool = False, + given_grammars: List[Grammar] = [], + start_symbol: Optional[str] = None, + includes: List[str] = [], +) -> Tuple[Optional[Grammar], List[str]]: + """ + Parse .fan content, handling multiple files, standard library, and includes. + :param fan_files: One (open) .fan file, or a list of these + :param constraints: List of constraints (as strings); default: [] + :param use_cache: If True (default), cache parsing results + :param use_stdlib: If True (default), use the standard library + :param lazy: If True, the constraints are evaluated lazily + :param given_grammars: Grammars to use in addition to the standard library + :param start_symbol: The grammar start symbol (default: "") + :param includes: A list of directories to search for include files + :return: A tuple of the grammar and constraints + """ - for file in filenames: - try: - with open(file, "r") as fp: - contents += fp.read() - except Exception as e: - print_exception(e) - errors = True + if not isinstance(fan_files, list): + fan_files = [fan_files] + + if not fan_files and not constraints: + return None, [] + + if constraints is None: + constraints = [] + + if start_symbol is None: + start_symbol = "" + + global STDLIB_SYMBOLS, STDLIB_GRAMMAR, STDLIB_CONSTRAINTS + if use_stdlib and STDLIB_GRAMMAR is None: + LOGGER.debug("Reading standard library") + STDLIB_GRAMMAR, STDLIB_CONSTRAINTS = parse_content( + stdlib, filename="", use_cache=use_cache + ) + + global USED_SYMBOLS + USED_SYMBOLS = set() + if use_stdlib: + assert STDLIB_GRAMMAR is not None + for symbol in STDLIB_GRAMMAR.rules.keys(): + # Do not complain about unused symbols in the standard library + USED_SYMBOLS.add(str(symbol)) + + global INCLUDES + INCLUDES = includes + + grammars = [] + parsed_constraints: List[str] = [] + if use_stdlib: + assert STDLIB_GRAMMAR is not None + assert STDLIB_CONSTRAINTS is not None + grammars = [deepcopy(STDLIB_GRAMMAR)] + parsed_constraints = STDLIB_CONSTRAINTS.copy() + + grammars += given_grammars + + LOGGER.debug("Reading files") + more_grammars = [] + global FILES_TO_PARSE + FILES_TO_PARSE = [(file, 0) for file in fan_files] + + global INCLUDE_DEPTH + INCLUDE_DEPTH = 0 + + while FILES_TO_PARSE: + (file, depth) = FILES_TO_PARSE.pop(0) + LOGGER.debug(f"Reading {file.name} (depth = {depth})") + fan_contents = file.read() + new_grammar, new_constraints = parse_content( + fan_contents, filename=file.name, use_cache=use_cache, lazy=lazy + ) + parsed_constraints += new_constraints + assert new_grammar is not None + + if depth == 0: + # Given file: process in order + more_grammars.append(new_grammar) + else: + # Included file: process _before_ current grammar + more_grammars = [new_grammar] + more_grammars + # Do not complain about unused symbols in included files + for symbol in new_grammar.rules.keys(): + USED_SYMBOLS.add(str(symbol)) + + if INCLUDE_DEPTH > 0: + INCLUDE_DEPTH -= 1 + + grammars += more_grammars + + LOGGER.debug(f"Processing {len(grammars)} grammars") + grammar = grammars[0] + LOGGER.debug(f"Grammar #1: {grammar.rules.keys()}") + n = 2 + for g in grammars[1:]: + LOGGER.debug(f"Grammar #{n}: {g.rules.keys()}") + # LOGGER.debug(f"Grammar: {g}") + + for symbol in g.rules.keys(): + if symbol in grammar.rules: + LOGGER.info(f"Redefining {symbol}") + grammar.update(g, prime=False) + n += 1 + + LOGGER.debug(f"Final grammar: {grammar.rules.keys()}") + + LOGGER.debug("Processing constraints") + for constraint in constraints or []: + LOGGER.debug(f"Constraint {constraint}") + _, new_constraints = parse_content( + "where " + constraint, filename=constraint, use_cache=use_cache, lazy=lazy + ) + parsed_constraints += new_constraints + + LOGGER.debug("Checking and finalizing content") + if grammar and len(grammar.rules) > 0: + check_grammar_consistency( + grammar, given_used_symbols=USED_SYMBOLS, start_symbol=start_symbol + ) + + if grammar and parsed_constraints: + check_constraints_existence(grammar, parsed_constraints) + + # We invoke this at the very end, now that all data is there + grammar.prime() + + LOGGER.debug("All contents parsed") + return grammar, parsed_constraints + + +### Consistency Checks + + +def check_grammar_consistency( + grammar, /, given_used_symbols=set(), start_symbol="" +): + if not grammar: + return + + LOGGER.debug("Checking grammar") + + used_symbols = set() + undefined_symbols = set() + defined_symbols = set() + + for symbol in grammar.rules.keys(): + defined_symbols.add(str(symbol)) + + if start_symbol not in defined_symbols: + closest = closest_match(start_symbol, defined_symbols) + raise NameError( + f"Start symbol {start_symbol} not defined in grammar. Did you mean {closest}?" + ) + + def collect_used_symbols(tree): + if tree.node_type == NodeType.NON_TERMINAL: + used_symbols.add(str(tree.symbol)) + if tree.node_type == NodeType.REPETITION: + collect_used_symbols(tree.node) + for child in tree.children(): + collect_used_symbols(child) + + for tree in grammar.rules.values(): + collect_used_symbols(tree) + + for symbol in used_symbols: + if symbol not in defined_symbols: + undefined_symbols.add(symbol) + + for symbol in defined_symbols: + if ( + symbol not in used_symbols + and symbol not in given_used_symbols + and symbol != start_symbol + ): + LOGGER.info(f"Symbol {symbol} defined, but not used") + + if undefined_symbols: + first_undefined_symbol = undefined_symbols.pop() + error = NameError(f"Undefined symbol {first_undefined_symbol} in grammar") + raise error + + +def check_constraints_existence(grammar, constraints): + LOGGER.debug("Checking constraints") - if errors: - raise FileNotFoundError("No input files") + indirect_child = { + str(k): {str(l): None for l in grammar.rules.keys()} + for k in grammar.rules.keys() + } + + defined_symbols = [] + for symbol in grammar.rules.keys(): + defined_symbols.append(str(symbol)) + + grammar_symbols = grammar.rules.keys() + grammar_matches = re.findall(r"<([^>]*)>", str(grammar_symbols)) + # LOGGER.debug(f"All used symbols: {grammar_matches}") + + for constraint in constraints: + constraint_symbols = constraint.get_symbols() + + for value in constraint_symbols: + # LOGGER.debug(f"Constraint {constraint}: Checking {value}") + + constraint_matches = re.findall(r"<([^>]*)>", str(value)) # was <(.*?)> + + missing = [ + match for match in constraint_matches if match not in grammar_matches + ] + + if missing: + first_missing_symbol = missing[0] + closest = closest_match(first_missing_symbol, defined_symbols) + + if len(missing) > 1: + missing_symbols = ", ".join(["<" + symbol + ">" for symbol in missing]) + error = NameError( + f"{constraint}: undefined symbols {missing_symbols}. Did you mean {closest}?" + ) + raise error + + if len(missing) == 1: + missing_symbol = missing[0] + error = NameError( + f"{constraint}: undefined symbol <{missing_symbol}>. Did you mean {closest}?" + ) + raise error + + for i in range(len(constraint_matches) - 1): + parent = constraint_matches[i] + symbol = constraint_matches[i + 1] + # This handles [...]. as ... + # We could also interpret the actual [...] contents here, + # but slices and chains could make this hard -- AZ + recurse = f"<{parent}>[" in str(value) or f"..<{symbol}>" in str(value) + if not check_constraints_existence_children( + grammar, parent, symbol, recurse, indirect_child + ): + msg = f"{constraint}: <{parent}> has no child <{symbol}>" + raise ValueError(msg) - return parse(contents, lazy=lazy) \ No newline at end of file + +def check_constraints_existence_children( + grammar, parent, symbol, recurse, indirect_child +): + # LOGGER.debug(f"Checking if <{symbol}> is a child of <{parent}>") + + if indirect_child[f"<{parent}>"][f"<{symbol}>"] is not None: + return indirect_child[f"<{parent}>"][f"<{symbol}>"] + + grammar_symbols = grammar.rules[NonTerminal(f"<{parent}>")] + + # Original code; fails on "b" -- AZ + # grammar_matches = re.findall(r'(?]*)>(?!.*")', + # str(grammar_symbols)) + # + # Simpler version; may overfit (e.g. matches <...> in strings), + # but that should not hurt us -- AZ + grammar_matches = re.findall(r"<([^>]*)>", str(grammar_symbols)) + + if symbol not in grammar_matches: + if recurse: + is_child = False + for match in grammar_matches: + is_child = is_child or check_constraints_existence_children( + grammar, match, symbol, recurse, indirect_child + ) + indirect_child[f"<{parent}>"][f"<{symbol}>"] = is_child + return is_child + else: + return False + + indirect_child[f"<{parent}>"][f"<{symbol}>"] = True + return True \ No newline at end of file diff --git a/tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle b/tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle new file mode 100644 index 00000000..e69de29b diff --git a/tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle b/tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle new file mode 100644 index 0000000000000000000000000000000000000000..c7a8fca606716f2bc4a3fa7de36da05455e67310 GIT binary patch literal 8248 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 literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle b/tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle new file mode 100644 index 0000000000000000000000000000000000000000..c2bdc64941eb18d2a7a65c12b92b2ffcdbd56692 GIT binary patch literal 10135 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 I#AUJeA2Wgk@&Et; literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle b/tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle new file mode 100644 index 0000000000000000000000000000000000000000..9e5682a246c08d47e03ac720deada976ca044a6b GIT binary patch literal 8546 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 literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle b/tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle new file mode 100644 index 0000000000000000000000000000000000000000..fd83b079004f680900dd6f04ca7552dda2fadcc0 GIT binary patch literal 7849 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 literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle b/tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle new file mode 100644 index 0000000000000000000000000000000000000000..19896094701f59f744fce476d2f88a76fe6c56b9 GIT binary patch literal 8525 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 literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle b/tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle new file mode 100644 index 0000000000000000000000000000000000000000..02006540f33d4bcee3e47c05bea23e087f93648a GIT binary patch literal 9807 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 I&!T4Izf}8D)IBqqJP_ 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 Ni+gezUSx5*^*{MY($)X~ literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle b/tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle new file mode 100644 index 0000000000000000000000000000000000000000..f2ee51af18131d5a4d1d7cad63a577acde037764 GIT binary patch literal 8937 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 literal 0 HcmV?d00001 diff --git a/tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle b/tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle new file mode 100644 index 0000000000000000000000000000000000000000..fa6d6846705a4e9b29c5ecbdbee42fd5608a2026 GIT binary patch literal 8926 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 LmoN$}Hd_A$mTv6W literal 0 HcmV?d00001 From 454c710550f82d1d8e2f8d9d1497e540f0487658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Antonio=20Zamudio?= Date: Wed, 15 Jan 2025 09:55:58 +0100 Subject: [PATCH 26/28] clean: up repo --- ...e356bac2cb13abdfd209e2873361d3ef3e543.pickle | 0 ...f8c49fe83ded6aa032981786afe0426cff52a.pickle | Bin 8248 -> 0 bytes ...e0363806cef43a3ad2a767ff38322e7b3bd7b.pickle | Bin 10135 -> 0 bytes ...226767cedfc4410246e3888104e43268bf487.pickle | Bin 8546 -> 0 bytes ...c11e05caf92738e7629e5796451b058761f2f.pickle | Bin 8812 -> 0 bytes ...16528ad11d53e6b79784de156984e49631e5b.pickle | Bin 7849 -> 0 bytes ...82e0910b07c25bd85e95277931b33a6571088.pickle | Bin 8525 -> 0 bytes ...d2ee03b8e42a7dc5314a5684a4862566768b1.pickle | Bin 8753 -> 0 bytes ...eca3a5348112e2a6469ddbc9382e298a51020.pickle | Bin 9807 -> 0 bytes tests/.fandango_cache/CACHEDIR.TAG | 1 - ...5f2f338b3a714c40fbf60d723970d6bc94b84.pickle | Bin 8927 -> 0 bytes ...7abd16fad303bad5d66ecfb7c959ba924e556.pickle | Bin 8552 -> 0 bytes ...575dc2327a424d5304e2021db949c8e42c148.pickle | Bin 8937 -> 0 bytes ...c0b1be4ff953cfe0482a23500fdec7f010b6c.pickle | 0 ...c3e03072e75bdeff2829850d334b2c3b13a0d.pickle | Bin 8220 -> 0 bytes ...de6e00deb7733099d280e2716f391b8fecf4e.pickle | Bin 8926 -> 0 bytes ...62252f6b3fcc6f77ed5f0c9d5d131acf03517.pickle | Bin 8606 -> 0 bytes 17 files changed, 1 deletion(-) delete mode 100644 tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle delete mode 100644 tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle delete mode 100644 tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle delete mode 100644 tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle delete mode 100644 tests/.fandango_cache/511d364a2dbae25b4704db77013c11e05caf92738e7629e5796451b058761f2f.pickle delete mode 100644 tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle delete mode 100644 tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle delete mode 100644 tests/.fandango_cache/7bad955f7612264662929d472bbd2ee03b8e42a7dc5314a5684a4862566768b1.pickle delete mode 100644 tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle delete mode 100644 tests/.fandango_cache/CACHEDIR.TAG delete mode 100644 tests/.fandango_cache/ac0afb847e10c18a3abe62a28d35f2f338b3a714c40fbf60d723970d6bc94b84.pickle delete mode 100644 tests/.fandango_cache/baf5d9c51ca38faac740b61c42a7abd16fad303bad5d66ecfb7c959ba924e556.pickle delete mode 100644 tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle delete mode 100644 tests/.fandango_cache/d8ee33bdaa6a4f44d23be4ddfe1c0b1be4ff953cfe0482a23500fdec7f010b6c.pickle delete mode 100644 tests/.fandango_cache/dc4092715f58afb4223b63e27dec3e03072e75bdeff2829850d334b2c3b13a0d.pickle delete mode 100644 tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle delete mode 100644 tests/.fandango_cache/fff8fd93fd0091eb80cccef89b662252f6b3fcc6f77ed5f0c9d5d131acf03517.pickle diff --git a/tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle b/tests/.fandango_cache/06bd0a68d120e507b53ecf186f6e356bac2cb13abdfd209e2873361d3ef3e543.pickle deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle b/tests/.fandango_cache/087e071ceb1a8ef1f4ba3e1d839f8c49fe83ded6aa032981786afe0426cff52a.pickle deleted file mode 100644 index c7a8fca606716f2bc4a3fa7de36da05455e67310..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8248 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 diff --git a/tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle b/tests/.fandango_cache/13084ea1318eae4e6618678dc0ae0363806cef43a3ad2a767ff38322e7b3bd7b.pickle deleted file mode 100644 index c2bdc64941eb18d2a7a65c12b92b2ffcdbd56692..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10135 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 I#AUJeA2Wgk@&Et; diff --git a/tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle b/tests/.fandango_cache/4b9326df0b261e17c963f38b43a226767cedfc4410246e3888104e43268bf487.pickle deleted file mode 100644 index 9e5682a246c08d47e03ac720deada976ca044a6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8546 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 diff --git a/tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle b/tests/.fandango_cache/6c45342b0128f098aa13bd699e716528ad11d53e6b79784de156984e49631e5b.pickle deleted file mode 100644 index fd83b079004f680900dd6f04ca7552dda2fadcc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7849 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 diff --git a/tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle b/tests/.fandango_cache/748cbdf7b502b906ec4c9bb70bb82e0910b07c25bd85e95277931b33a6571088.pickle deleted file mode 100644 index 19896094701f59f744fce476d2f88a76fe6c56b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8525 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 diff --git a/tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle b/tests/.fandango_cache/7cdb6b47c69370b2851893ea8cfeca3a5348112e2a6469ddbc9382e298a51020.pickle deleted file mode 100644 index 02006540f33d4bcee3e47c05bea23e087f93648a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9807 zcmcIq>u()L70=_^*S@~CleDBwDRi5vO(MlNcG9GIBR^A<#0jxWtIE}~ckkG{o4vcs zKJ46xN`O?L(n>Ar!WSgO7ryW}_-_D-3h@aQ5EZ3GLP#JGU*LDn?CU-fC33NnojJdA z=FFKhvu9?`-fvfawD8g%{)?a5(43a$Y`P5_&q&)84Nvm}k;L_-l(XsyJ^3Ix@tx%D zMji5QGXE;~`XDg3LKYFSOf zEpyX^$hVZsvX*fnm$In!(V3$VIeMC(XZZOtKhN^>96w*-=Xri!;OF$wOSPKmgvT?j zCzKbJ)5?`A%Jiik{HiiDjlW*_vV?&r@z|#AHZ@z_f?wDLBdY3XZK0}3{9NunI3Ngp z4ZQ{p&lfFI*FphzR8_U*>X2QD$5hqR^tW1&$7mjMS zJ=YIyv#E-Eq3|8ePT~WrZshA?$+X1{Zc$89h3rd=dR+0e&`9E2^B31&3ovo(mK)sH z))!noYSVPBUo*p3qvm>M%X;AJ1yLH)dNHPk*GZyg3@-#&8D!?cJ~J_nn{F$zMc3jn z4#T8jIlRPpG9Gq)ECVMoo{sD3ps{p90be~6M9ns9Py?a)x&g1iqgZ$WxJT5;FB zHQT%|T52vFByT10IGTj&2K=Ihp`ZK!bD(RwAzDdn?A{TW#15-d)v{A`uM--sGj(QW z1`g86irhle*-YM*E+ebpGK?4#Ev+K*SX@`tW@Or-=>!1qrWs@LNQNZF_#402yJycF zNlclJvJp8tJL9Ahc03WBRKz`9cqxPzS8+o__=?-KgpL(itF0Mkph%A@p6_m%EfFX! z(+6A8bX^3261s|J+sZNS6}=p*LC6Ws4HGLzaW|CPJj6@NhU=q0t?k+3q>|a#u!W^K4T$LtxDgh7p7{%_B%I z39=$obj?wkLWu%_{uOvh4i^J6v@jcVEp>^qZH5LoLJ7!DHdYH1t*~@Uvm+i~2wk;p z!_=jGJ`{L_HFH~-uGxXBI6|~UtC1KJ@t(vuU_28~&dGUQ=66{5cukEX#;+2>( zo-<|)C7o^fJu#kNF^;YnFT9l)FTQKMw1TBGDVIk})om@H6=F>FV;^w%kg-CFV3Fk{ zxcjwcKrOjs+O{Q)l4++SX_TOcv_LmaLed(^G|58fpb~Vg=?m?y=OXeETqeOC(J_%) z(?&F!DiNedH+`_1xCj`m74Hx+xHg5I?Bjtj;)Z2kghH+!_M5-_U|wKC4rIs2kiL zSAgs{#61GI1ijB>tEm7rWIA5N#-uf1H8Vh52sKA%_moM=MF8Z5L2ALxNpLGBHVp2c zE7XIQL1_)xe!Pxs6P2J5r7?3n$gE0IZB1HECDZ9zXv?)waj-pcUtEF2kd{Gdjas=n z3vtP`&koq{q9odLx7ijdNr&gBiz}@GtN6l`qv8rl`EKN}p0oz6ij5$jldKMWIKXa- z{Q%y|szCP#QJedv5_E-ke8|WOAuFy!yVJ;`ZJSb9e4!?8cZ7O;tG&afuo)9X}hML3ktkp zY;(NocJeCFp$%;EtR$_G7J0zI+_08{0&fKSlqdXT(q$v{9L4q27hGn)>6(a2x=)e{+!uG6aR+m-i^G1U#5wFlLJ7(FVo)my7MC$9saxFawaf&C2vD4*bv(g)J~{(-|jkJqU*M_$@>a1iI~9um^$6!8e8PE|^=`)k{lr01Daq z90l=`Y25(Xw}7os$dhnMZV(LV$wfzZTNq7Av=4*a_)-)`z9>ocAS5GKc3Gwyfy|NH z!lCX48~G3}*?LC=e8(;I(1$^8B3-8?v0jL*l`E?x)PsDG$3Bg47t@rAvCc zXz(ZJaC*9nK`-CRS3w^Jxr6hDX}50sE?vKJ$pI+ooOc}um$mYW>BC5l@&SmOL0D+) z!>DhP!x?3XPEAGM{U~G)`@OQE$Nh3p~pK0tU74C%?+!f)eb z+co}PjI^fiEC*4FNCRL<*Vho<+^Be~y8%dGm~;u|(3TIVAB1F$B#XdRCcz!S?w7YO z2ymXb$$5^xxfqC(eD}#RS2_@fJOaBOan3Fm?S&w9@HMANOW_8c`*XnoC}ivDTat@) z1CW@U^WD3`xowtWaUTY`!^NteZSt8s*ZCk;cPs2;xbGLMvec@Jg;q}%eIc%hV$KIM z&|+jwYv%S81UQFM4=f0Z7P-tIxFpS?w_mypL;4G;?ll}v16+NJ1`pt%%|w1Slc%`y zwWOB^@dJy_mg&0=-A$zN)*RAHaWxBAlB~aOIxTlQ4I~n);7E?-?>^JTk(BGuSNUdy zQy(SXaI-8fMAfeWrh7n^JvI(of1-Z&3wHxfoDY3_>P^M7qd>VMu-!P6SgZ&-LRx=@ zV3s4X{tCkSlpdeq0ijXd^*YEN8j$=L5Ymyi>6TdPES|_9nXgT(zmZ@GZE2yFym1ET ze5Oq~uAfj&PfyQGot{2@Ce3&$tL{d)t@%RXXvwjlQJY1M>ke5!#$!X+LfWTR>(ex? z%!ym@Sp)fvM{6{5;lf#TKRu(&yFT*1+#hGsR=BF6Uvx-WQDvF4prN@XkmP$5i=cdF zih591aeOxDT%+7Xl2{-EVg}GSk2T>(?UTxE8wV@Dr9HrEyyS&{ZPUR`b0vPFfN%vA zlo_LxEGDd~Q&Jos2&})u-*RHDf1qmphaUgLBmSv^zcoX1?s776{`KXP%KXY5JPQ|| zRNlm@>+%exJ2hngf{>DwxYAT6=lz9q%wo4v~b0Cqa&Bt_%v>RdQ1s6}9 z>h9%~7i^qLRh;Tpemq|8$yCpJ-U62EI>+aA>}H$3)(+O?fYJiF=k*(AbG^}6zp*^O zxU#y~2$7G87e5zWNxZZRJzWOP?OqY0P0uJtb2h2juY$;8LljRe2;bb| zmubxx_}Fzg`v6jW1`CN{P3+sV2LZ08>0yL#K!`Aj@o8L7$uyzyhj=0dD2K_}liwJh z754-(@elpmq|5aowZHEae_P2GA*DjxxoYN}gDBt}xmuizVw^~KZ#Ai4Uw^(Bdp z0&NPcQKSay)^mkIZG&PVu;&?z;u>f4Wnmq86nyHDAIF~VZy0piO(eDAF=Wry=oCkvukyCT8M~T(H%zQLy*&-s zO?d!nOLsc7uucJcC%J=oVjUy)WJ%)+22yApb$DFVPKu5YKh<-6n3TkaTR0_CPE`%v z)U|ZHPOew1Gth2L?Kdh z4yy4GIrVe4w6WS9{#JlDl!QbqjTJHG8O@E!eK*RdP_ZaL^t( z1H5oYvUY`2c@6EINhjhQ^%&wI*E1}4L6oO9APJt@RB==YNL{B>UH8G@h4}a?&g@>r z%ez-`cK0gI?Ow$zyH|03_bM*zUd42e3cMGH|C?EgmI5`tiSzF~{4_jHq%pm5n|}RK zoY>>>H0-M~#9LEXYw>VO;WM>01K>TB^C;g*-nW)gdX;JG8z?M+$M?W9Qr=G_?}xI8 zWw9lTpUL9qvUpn-KbFOJW$}G3-j8qsHL>5)hvD2H*v6mLF-9B$4)D(g__rG-eTC$q zA5@E9bflFlYb2gSq_ZgCR?@ O!3^_YXlf8y8vg?-?SpkNE*l;`&<3+4i`RyqjG3N%EuQ zd|chYiAU!f^Nj@vLWOR)PRN}wNMg74Vh}Pvyrx~b@{;x8p+2+}lJgW*^*?E;^p;hqh?y{6WZlhuKMdeA|tDgRcpj-;y;7FILFD#H_~^ zkA-Fu-&wh``$_=M?pki}knLV~ji^KZ+`S>fm!sxxX3K8i8wFA7({9nHhSyD^W(+R` z7&dki%C`22rykHRu6n zzG1>E@F-?R0PYz*a)cv7!EEu6xAa^%NM29kDO3sdHvGcE&`;jN7#Pejc`J#{FYX8o zVppou^|Dh8uN#`KGka-a0S+?Airhls>?Ln2myuO)8G4L?R#p*tF0Si(GZJMlBFvM}p(Ep>@@AVL!yt_5VLG?oP# zD=gh-b|m{p0it$b3PZ`~U4ciWW?2`eGdplKhw~P1H4<|=K9ZQn%_rlTWi_trat(7I z%hWt$KBWR4mc*?2w7Fnv>1f06iTTWid3MA6#_Ngst+&l@Z(!=osOizt4VwitL(JJh z>|Gf?RIE@Um}EH#?g(oJR8xK>ZQF{*SJF;b(fEQMWq~0CA!&(Znq(n#&=+*A>2r49 za}oInE|cJn8yHB|v=NPFO9bgLOke7uFX*b_c08;O!tyfVEpE$oOn6qN`k1`3K7B!t zwZuLKoS>`-w8AlrgsFl=9$>?f2o$4)z>K;cdLj0h1v$DkHb|C#&Ra-+K z?KfGF%V%Y34`oxfCksGEO#XlX`2~GUNUP}sYE(F0B#lW+z-l5uTnL$CNcWUU#YF&= z6NA)(o0H&H1U3xWzAR7+EQ8V#un{arX%l@xBT8fBWGAyCNp+aAoW7Dy=R!x#g^Gji zNwy^mkQikdl$NNLYe*sam9#G%Fyi7%wC5g3Tj)zVJU^XWX$e@x=bq}7ERdA%Mvl~z zmVi~U5#(c%m4S~1*iEq?z*|`n=tvNCWV`eQU6DIJWK@BW4cDRFX?)eTg%^kb6FiH5 zDCQ_cr#IXXE8ga`&r(x|0}!M}*WhHHyd76|{D@<#sx(~}^Yt(-K%~~;9oO&f3l66o z_K@ICG`k`8N1tKc9+oLmPCN|ZVO9b*DW^$?W3Q4n_0ZtUm1ooZJS@t%?#&|sG= z0GZ$p&JaFr*VJ-Bfj5S2PR_c+ya;r(jZI!ENlTrv_CBY8}~V#LDi5I1nEqzlwYbH zU;|RMRS8&~4lu$f9NY^?EfiB=8IFr`b_WqUNyw-wk>)VM%#oNX0jtnPPmn5*riViH zk-nfOaMa|@Xb++Pt$1pg1$;F-6i~fB1Qky9$lSmJlKO_hq`pU6i2`rx26t#Kxc=$` zAxEMQfy{}UyxVk{-&)6l@cqb>azjwa*0#Gr$UB*`G70X?k}pCNJ{HER@4M1@0~q9{ zmjWDm=}?r5^+Qq$QC{Ex!AnxT5TquTqjsD7xkwKU{`l&hwVYWHR8wvIIkSf*{w>$N zA9)48OcVdM3;+ecOoKnMfdfp=Em7cA?`-FsybOGLi=#6f`$^93hagqoig5fZDena! zadJC!y``Y*80#2Qc^IRqHVlUJ6iN38_92j&-0nIddyt#eLo2?V8QHjlU(n!B>|mMX zwM!IuwO8p#mov*GxYfJN$E{+II*^xvpIoD`1;!?waq`;3U`S8hLG(bNJIDq55XhXk z$9?y@*vGD3nwmpU$kvxBh?h+31<1bZ*b2Ei376!C!H}L=bqu$K-jqZKFvv}#LTkq-sjGyD8=Fd405N7S-sShXYyRy>Oejdb+uR8m}cOAN$DC4bVq?h7q7O)gqzbl-Udyobag;j7Q zM|Sp@aB(D+W$3H?Fv6*i5^uO!78j!G_W;v9AWQF>r>#FwJ0sj}!-*@QZ_nP-JUa@s zEspKRp~PZ^(-G4869lsyiS-u{)+hAyXZ%2D!f?GVvWF%l{|E@_$W6K>mKuwvGf3rY z6YH-eSVCJYWXWrnfUacPwDZOVZE=2nVRmtT@lu-c(ze}pc))zF$!IBKL8G>W9M=|E zK*nQ3*hku@RvXYXuPyUC@L2=-jn7tT;e{8TL-X?s+KTHV?~kdfK8rAC{)J!FC%XSRxW5nd z=|6d&&XIOA487pW++1%fb6(J%OI6JEDnB2u_hqV=JnuTD>#mH?yV%Y4eAWqeRfp0K za?iWBM02;%*uAyBvbwRo+6a-4iC4cAT}iz51@!b7wEV>tA=>oyaw4Y_PGQ94wwDHn zs{S&FEH*^(^mXoweYu!+e2&+s)7b+^@lF*I!jTvEtHy}iq#QY?#r(~MY z_9tBT5>T&Fe!HPk*(?n7$o<#O+hfZ? zpQ2ZDMI+W-l*u<=N@Lc&zLvh9x0L$g2Zey@elzVp2ZQDU+5365m~(2jmvmsM?q%6D zT2pwwlyByKI+Lf<<=5Dxm-OOr6;GurR9CCfnuT5QSW4kV(OLlTGkjO@eKUE-T1#n$ zuVv!rr-}7Y@+^;_NJ8&K*y#o08a=?}+MrlIU&25H+vDI0-zNm*<~*c5(4p zeG#lA9u^Pf(G8o*7Cn$FB3;X$HSh$@z(>yGapH4y{83#W$GK3Z zpp*EiY)EkE&ZbGwXp0c1)Buhz`iIFAAH9~y{h(s=Hm(!DA+tx--6B4^2(NJKo51jb I&!T4Izf}8D)IBqqJP_ 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 Ni+gezUSx5*^*{MY($)X~ diff --git a/tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle b/tests/.fandango_cache/ca9751b3788dd548fdab201a5b1575dc2327a424d5304e2021db949c8e42c148.pickle deleted file mode 100644 index f2ee51af18131d5a4d1d7cad63a577acde037764..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8937 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 diff --git a/tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle b/tests/.fandango_cache/f2ff9c4361f1274137ec5f26e10de6e00deb7733099d280e2716f391b8fecf4e.pickle deleted file mode 100644 index fa6d6846705a4e9b29c5ecbdbee42fd5608a2026..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8926 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 LmoN$}Hd_A$mTv6W From 69b9693deae563fe02640fa4a3b50e910ab56b39 Mon Sep 17 00:00:00 2001 From: joszamama Date: Wed, 15 Jan 2025 10:18:32 +0100 Subject: [PATCH 27/28] fix: python tests --- tests/resources/bar.fan | 5 ++++ tests/resources/children.fan | 5 ++++ tests/resources/indirect_children.fan | 5 ++++ tests/resources/min_reps.fan | 2 ++ tests/resources/repetitions.fan | 2 ++ tests/resources/slicing.fan | 2 ++ tests/test_constraints.py | 22 +++------------- tests/test_grammar.py | 38 +++++++++------------------ 8 files changed, 38 insertions(+), 43 deletions(-) create mode 100644 tests/resources/bar.fan create mode 100644 tests/resources/children.fan create mode 100644 tests/resources/indirect_children.fan create mode 100644 tests/resources/min_reps.fan create mode 100644 tests/resources/repetitions.fan create mode 100644 tests/resources/slicing.fan diff --git a/tests/resources/bar.fan b/tests/resources/bar.fan new file mode 100644 index 00000000..0853a41a --- /dev/null +++ b/tests/resources/bar.fan @@ -0,0 +1,5 @@ + ::= * := test(); + ::= "a" | "b" | "c" | "r"; + +def test(): + return "bar" \ No newline at end of file diff --git a/tests/resources/children.fan b/tests/resources/children.fan new file mode 100644 index 00000000..3c98eb78 --- /dev/null +++ b/tests/resources/children.fan @@ -0,0 +1,5 @@ + ::= ; + ::= | ; + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; + +int(..) == 0; \ No newline at end of file diff --git a/tests/resources/indirect_children.fan b/tests/resources/indirect_children.fan new file mode 100644 index 00000000..f9d5fbea --- /dev/null +++ b/tests/resources/indirect_children.fan @@ -0,0 +1,5 @@ + ::= ; + ::= | ; + ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; + +int(..) == 1; \ No newline at end of file diff --git a/tests/resources/min_reps.fan b/tests/resources/min_reps.fan new file mode 100644 index 00000000..7403ce9c --- /dev/null +++ b/tests/resources/min_reps.fan @@ -0,0 +1,2 @@ + ::= {3, }; + ::= "a"; \ No newline at end of file diff --git a/tests/resources/repetitions.fan b/tests/resources/repetitions.fan new file mode 100644 index 00000000..7c646bd5 --- /dev/null +++ b/tests/resources/repetitions.fan @@ -0,0 +1,2 @@ + ::= {3}; + ::= "a"; \ No newline at end of file diff --git a/tests/resources/slicing.fan b/tests/resources/slicing.fan new file mode 100644 index 00000000..c581be16 --- /dev/null +++ b/tests/resources/slicing.fan @@ -0,0 +1,2 @@ + ::= {3, 10}; + ::= "a"; \ No newline at end of file diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 4c364760..bd478f33 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -343,10 +343,6 @@ def test_exists_constraint(self): ) self.assertTrue(constraint.check(example)) - def test_constraint_check(self): - self.assertRaises(ValueError, self.get_constraint, ". == 'a'") - self.assertRaises(ValueError, self.get_constraint, ".. == 'a'") - def test_direct_children(self): constraint = self.get_constraint("str(.) == 'a';") counter_example = DerivationTree( @@ -382,13 +378,8 @@ def test_direct_children(self): self.assertTrue(constraint.check(example)) def test_indirect_children(self): - grammar = """ - ::= ; - ::= | ; - ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; -""" - constraint = "int(..) == 1;" - grammar, constraint = parse(grammar + constraint) + file = open("tests/resources/indirect_children.fan", "r") + grammar, constraint = parse(file, use_stdlib=False) self.assertEqual(1, len(constraint)) constraint = constraint[0] @@ -400,13 +391,8 @@ def test_indirect_children(self): self.assertTrue(constraint.check(example)) def test_accessing_children(self): - grammar = """ - ::= ; - ::= | ; - ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0"; -""" - constraint = "int([0].) == 0;" - grammar, constraint = parse(grammar + constraint) + file = open("tests/resources/children.fan", "r") + grammar, constraint = parse(file, use_stdlib=False) constraint = constraint[0] counter_example = grammar.parse("11") diff --git a/tests/test_grammar.py b/tests/test_grammar.py index e9cdab6d..2f9cc85a 100644 --- a/tests/test_grammar.py +++ b/tests/test_grammar.py @@ -38,50 +38,38 @@ def test_parse(self): for path in GRAMMAR.traverse_derivation(tree): print(path) - def get_solutions(self, grammar): - grammar, constraints = parse(grammar) + def get_solutions(self, grammar, constraints): fandango = Fandango(grammar=grammar, constraints=constraints, desired_solutions=10) return fandango.evolve() def test_generators(self): - GRAMMAR = """ - ::= * := test(); - ::= "a" | "b" | "c" | "r"; - -def test(): - return "bar" -""" + file = open("tests/resources/bar.fan", "r") + GRAMMAR, constraints = parse(file, use_stdlib=False) expected = ["bar" for _ in range(10)] - actual = self.get_solutions(GRAMMAR) + actual = self.get_solutions(GRAMMAR, constraints) self.assertEqual(expected, actual) def test_repetitions(self): - GRAMMAR = """ - ::= {3}; - ::= "a"; -""" + file = open("tests/resources/repetitions.fan", "r") + GRAMMAR, c = parse(file, use_stdlib=False) expected = ["aaa" for _ in range(10)] - actual = self.get_solutions(GRAMMAR) + actual = self.get_solutions(GRAMMAR, c) self.assertEqual(expected, actual) def test_repetitions_slice(self): - GRAMMAR = """ - ::= {3, 10}; - ::= "a"; -""" - solutions = self.get_solutions(GRAMMAR) + file = open("tests/resources/slicing.fan", "r") + GRAMMAR, c = parse(file, use_stdlib=False) + solutions = self.get_solutions(GRAMMAR, c) for solution in solutions: self.assertGreaterEqual(len(str(solution)), 3) self.assertLessEqual(len(str(solution)), 10) def test_repetition_min(self): - GRAMMAR = """ - ::= {3, }; - ::= "a"; -""" - solutions = self.get_solutions(GRAMMAR) + file = open("tests/resources/min_reps.fan", "r") + GRAMMAR, c = parse(file, use_stdlib=False) + solutions = self.get_solutions(GRAMMAR, c) for solution in solutions: self.assertGreaterEqual(len(str(solution)), 3) From 5927996f9e776be509ddfb6e5e4c6b22721587e8 Mon Sep 17 00:00:00 2001 From: joszamama Date: Wed, 15 Jan 2025 10:29:50 +0100 Subject: [PATCH 28/28] fix: graphviz missing --- .github/workflows/deploy-book.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deploy-book.yml b/.github/workflows/deploy-book.yml index f15d1b2d..8f83b9e8 100644 --- a/.github/workflows/deploy-book.yml +++ b/.github/workflows/deploy-book.yml @@ -26,35 +26,41 @@ jobs: with: python-version: 3.13 - # Step 3: Install dependencies + # Step 3: Ensure Graphviz system package is installed + - name: Install Graphviz system package + run: | + sudo apt-get update + sudo apt-get install -y graphviz + + # Step 4: Install dependencies - name: Install dependencies run: | pip install -e . - pip install jupyter-book pyppeteer ghp-import black + pip install jupyter-book pyppeteer ghp-import black graphviz - # Step 4: Locate Fandango and add to PATH + # Step 5: Locate Fandango and add to PATH - name: Locate Fandango and add to PATH run: | which fandango echo "/opt/hostedtoolcache/Python/3.13.1/x64/bin/fandango" >> $GITHUB_PATH fandango --help - # Step 5: Build the documentation + # Step 6: Build the documentation - name: Build the book run: | jupyter-book build docs - # Step 6: Clone the target repository + # Step 7: Clone the target repository - name: Clone the target repository run: | git clone https://github.com/fandango-fuzzer/fandango-fuzzer.github.io.git gh-pages - # Step 7: Copy the built HTML files to the target repository + # Step 8: Copy the built HTML files to the target repository - name: Copy built files to the target repository run: | cp -r docs/_build/html/* gh-pages/ - # Step 8: Commit and push changes to the target repository + # Step 9: Commit and push changes to the target repository - name: Deploy to GitHub Pages run: | cd gh-pages @@ -62,4 +68,4 @@ jobs: git config --global user.email "${{ secrets.CI_COMMIT_EMAIL }}" git add . git commit -m "Update GitHub Pages site" || echo "No changes to commit" - git push https://x-access-token:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/fandango-fuzzer/fandango-fuzzer.github.io.git main \ No newline at end of file + git push https://x-access-token:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/fandango-fuzzer/fandango-fuzzer.github.io.git main