diff --git a/tests/test_comments_rules.py b/tests/test_comments_rules.py new file mode 100644 index 0000000..a0dd076 --- /dev/null +++ b/tests/test_comments_rules.py @@ -0,0 +1,241 @@ +# This file is to test comment format for .rules files as stated in (Issue 23) +import unittest +import io +import sys +import tempfile +import os + +from universalmutator.mutator import parseRules + +# written tests, still not tested! + +class TestParseRulesComments(unittest.TestCase): + + def _parse(self, rule_text): + """ + Helper: write rules to temp file, run parseRules, capture stdout + """ + fd, path = tempfile.mkstemp(suffix=".rules") + os.close(fd) + + try: + with open(path, "w") as f: + f.write(rule_text) + + # capture print output + buffer = io.StringIO() + old_stdout = sys.stdout + sys.stdout = buffer + + try: + rules, ignoreRules, skipRules = parseRules([path]) + finally: + sys.stdout = old_stdout + + return rules, ignoreRules, skipRules, buffer.getvalue() + + finally: + os.remove(path) + + # --------------------------------------------------- + # TEST 1: indented comments should be ignored + # --------------------------------------------------- + def test_indented_comments_ignored(self): + rules, ignoreRules, skipRules, out = self._parse( + " # comment line\n" + "\t# tab comment\n" + "\\+ ==> -\n" + ) + + self.assertEqual(len(rules), 1) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 0) + + # --------------------------------------------------- + # TEST 2: blank lines should be ignored + # --------------------------------------------------- + def test_blank_lines_ignored(self): + rules, _, _, out = self._parse( + "\\+ ==> -\n" + "\n" + " \n" + "\\* ==> /\n" + "\t\n" + ) + + self.assertEqual(len(rules), 2) + + # --------------------------------------------------- + # TEST 3: rules starting with '#' must NOT be treated as comments + # --------------------------------------------------- + def test_hash_rules_still_parse(self): + rules, ignoreRules, skipRules, out = self._parse( + "#include ==> DO_NOT_MUTATE\n" + "# ==> SKIP_MUTATING_REST\n" + ) + + self.assertEqual(len(ignoreRules), 1) + self.assertEqual(len(skipRules), 1) + + # --------------------------------------------------- + # TEST 4: only comments should result in no rules and no warnings + # --------------------------------------------------- + + def test_only_comments(self): + rules, ignoreRules, skipRules, out = self._parse( + "# comment\n" + " # indented comment\n" + " # spaced comment\n" + "#\t tab comment\n" + ) + + self.assertEqual(len(rules), 0) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 0) + + # --------------------------------------------------- + # TEST 5: Invalid lines should still warn, but not be treated as rules + # --------------------------------------------------- + def test_invalid_lines(self): + rules, ignoreRules, skipRules, out = self._parse( + " # comment line\n" + "\t# tab comment\n" + "\\+ ==> -\n" + "# ==> SKIP_MUTATING_REST\n" + "invalid line\n" + ) + + self.assertEqual(len(rules), 1) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 1) + + # --------------------------------------------------- + # TEST 6: Mixed file test with comments, blank lines, valid rules, and invalid lines + # --------------------------------------------------- + def test_mixed_file(self): + rules, ignoreRules, skipRules, out = self._parse( + "# comment\n" + "\n" + " # indented comment\n" + " # spaced comment\n" + "\\+ ==> -\n" + "invalid line\n" + "# ==> SKIP_MUTATING_REST\n" + ) + + self.assertEqual(len(rules), 1) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 1) + + # --------------------------------------------------- + # TEST 7: Disabled rules should be ignored, but still treated as comments + # --------------------------------------------------- + def test_disabled_rules_ignored(self): + rules, ignoreRules, skipRules, out = self._parse( + "#DISABLED: \\+ ==> -\n" + "#DISABLED: #include ==> DO_NOT_MUTATE\n" + "#DISABLED: # ==> SKIP_MUTATING_REST\n" + "\\* ==> /\n" + ) + + self.assertEqual(len(rules), 1) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 0) + + # --------------------------------------------------- + # TEST 8: Disabled rules with different spacing should still be ignored (MAYBE?) Might be problems + # --------------------------------------------------- + def test_disabled_rules_varied_spacing(self): + rules, ignoreRules, skipRules, out = self._parse( + "\t\t\t#DISABLED: \\+ ==> -\n" + " #DISABLED: #include ==> DO_NOT_MUTATE\n" + "\t#DISABLED: # ==> SKIP_MUTATING_REST\n" + "\\* ==> /\n" + ) + + self.assertEqual(len(rules), 1) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 0) + + # --------------------------------------------------- + # TEST 9: Header Testing with comments, blank lines before and after header, and example rules comments + # --------------------------------------------------- + def test_header_with_comments_and_blank_lines(self): + rules, ignoreRules, skipRules, out = self._parse( + "# This is a header comment\n" + "# It should be ignored\n" + "\n" + "# Another header comment\n" + "\n" + "# Example rule comment\n" + "#DISABLED: ==> \n" + "# This is an example rule that should be ignored\n" + "#DISABLED: #include ==> DO_NOT_MUTATE\n" + ) + + self.assertEqual(len(rules), 0) + self.assertEqual(len(ignoreRules), 0) + self.assertEqual(len(skipRules), 0) + + # --------------------------------------------------- + # TEST 10: Larger file test with multiple comments, blank lines, valid rules, invalid lines, and disabled rules + # --------------------------------------------------- + def test_larger_mixed_file(self): + rules, ignoreRules, skipRules, out = self._parse( + "# =====================================================\n" + "# HEADER COMMENT BLOCK\n" + "# This file tests real-world mixed .rules behavior\n" + "# =====================================================\n" + "\n" + "# Simple arithmetic rules\n" + "\\+ ==> -\n" + "\\- ==> +\n" + "\n" + "# multiplication and division\n" + "\\* ==> /\n" + "\\/ ==> *\n" + "\n" + " # indented comment inside file\n" + "\t# tab-indented comment\n" + "\n" + "#Disabled rule section\n" + "#DISABLED: \\+ ==> *\n" + "#DISABLED: \\* ==> +\n" + "\n" + "# Special ignore rule\n" + "#include ==> DO_NOT_MUTATE\n" + "\n" + "# Special skip rule\n" + "# ==> SKIP_MUTATING_REST\n" + "\n" + "# Invalid lines (should trigger warnings)\n" + "this is not a rule\n" + "==> broken rule\n" + "\\+ == bad format\n" + "\n" + "# More valid rules\n" + "== ==> !=\n" + "!= ==> ==\n" + "< ==> >\n" + "> ==> <\n" + "\n" + "# More noise\n" + "random text here\n" + "another bad line\n" + "\n" + "# Final valid rule\n" + "\\% ==> +\n" + ) + + # Expected valid rules: + # +, -, *, /, ==, !=, <, >, % + self.assertEqual(len(rules), 9) + + # Only one ignore rule (#include ==> DO_NOT_MUTATE) + self.assertEqual(len(ignoreRules), 1) + + # Only one skip rule (# ==> SKIP_MUTATING_REST) + self.assertEqual(len(skipRules), 1) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/universalmutator/__pycache__/__init__.cpython-312.pyc b/universalmutator/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..67ad44b Binary files /dev/null and b/universalmutator/__pycache__/__init__.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/c_handler.cpython-312.pyc b/universalmutator/__pycache__/c_handler.cpython-312.pyc new file mode 100644 index 0000000..ae35c68 Binary files /dev/null and b/universalmutator/__pycache__/c_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/cpp_handler.cpython-312.pyc b/universalmutator/__pycache__/cpp_handler.cpython-312.pyc new file mode 100644 index 0000000..184a28a Binary files /dev/null and b/universalmutator/__pycache__/cpp_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/fe_handler.cpython-312.pyc b/universalmutator/__pycache__/fe_handler.cpython-312.pyc new file mode 100644 index 0000000..a1eb71d Binary files /dev/null and b/universalmutator/__pycache__/fe_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/fortran_handler.cpython-312.pyc b/universalmutator/__pycache__/fortran_handler.cpython-312.pyc new file mode 100644 index 0000000..1d47e60 Binary files /dev/null and b/universalmutator/__pycache__/fortran_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/genmutants.cpython-312.pyc b/universalmutator/__pycache__/genmutants.cpython-312.pyc new file mode 100644 index 0000000..0954065 Binary files /dev/null and b/universalmutator/__pycache__/genmutants.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/go_handler.cpython-312.pyc b/universalmutator/__pycache__/go_handler.cpython-312.pyc new file mode 100644 index 0000000..ba13a95 Binary files /dev/null and b/universalmutator/__pycache__/go_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/java_handler.cpython-312.pyc b/universalmutator/__pycache__/java_handler.cpython-312.pyc new file mode 100644 index 0000000..98e2356 Binary files /dev/null and b/universalmutator/__pycache__/java_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/javascript_handler.cpython-312.pyc b/universalmutator/__pycache__/javascript_handler.cpython-312.pyc new file mode 100644 index 0000000..473b27c Binary files /dev/null and b/universalmutator/__pycache__/javascript_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/lisp_handler.cpython-312.pyc b/universalmutator/__pycache__/lisp_handler.cpython-312.pyc new file mode 100644 index 0000000..d491187 Binary files /dev/null and b/universalmutator/__pycache__/lisp_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/mutator.cpython-312.pyc b/universalmutator/__pycache__/mutator.cpython-312.pyc new file mode 100644 index 0000000..c3e6562 Binary files /dev/null and b/universalmutator/__pycache__/mutator.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/python_handler.cpython-312.pyc b/universalmutator/__pycache__/python_handler.cpython-312.pyc new file mode 100644 index 0000000..ead8bee Binary files /dev/null and b/universalmutator/__pycache__/python_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/r_handler.cpython-312.pyc b/universalmutator/__pycache__/r_handler.cpython-312.pyc new file mode 100644 index 0000000..4973970 Binary files /dev/null and b/universalmutator/__pycache__/r_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/rust_handler.cpython-312.pyc b/universalmutator/__pycache__/rust_handler.cpython-312.pyc new file mode 100644 index 0000000..8fe0c3c Binary files /dev/null and b/universalmutator/__pycache__/rust_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/solidity_handler.cpython-312.pyc b/universalmutator/__pycache__/solidity_handler.cpython-312.pyc new file mode 100644 index 0000000..f42477a Binary files /dev/null and b/universalmutator/__pycache__/solidity_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/swift_handler.cpython-312.pyc b/universalmutator/__pycache__/swift_handler.cpython-312.pyc new file mode 100644 index 0000000..bdeb8af Binary files /dev/null and b/universalmutator/__pycache__/swift_handler.cpython-312.pyc differ diff --git a/universalmutator/__pycache__/vyper_handler.cpython-312.pyc b/universalmutator/__pycache__/vyper_handler.cpython-312.pyc new file mode 100644 index 0000000..4254426 Binary files /dev/null and b/universalmutator/__pycache__/vyper_handler.cpython-312.pyc differ diff --git a/universalmutator/mutator.py b/universalmutator/mutator.py index 1791d61..b4e65b1 100644 --- a/universalmutator/mutator.py +++ b/universalmutator/mutator.py @@ -1,11 +1,38 @@ from __future__ import print_function import re -import pkg_resources +import sys import random from comby import Comby import os from json.decoder import JSONDecodeError +# Python 3.9+ has importlib.resources with the modern files() API. +# Older Pythons (< 3.9) fall back to pkg_resources from setuptools. +if sys.version_info >= (3, 9): + import importlib.resources as _importlib_resources + _use_importlib = True +else: + try: + import importlib.resources as _importlib_resources + _use_importlib = True + except ImportError: + import pkg_resources as _pkg_resources + _use_importlib = False + + +def _open_package_resource(package, resource_path): + """Open a package data file, using importlib.resources on Python 3.9+ + and falling back to pkg_resources on older versions.""" + if _use_importlib: + parts = resource_path.replace("\\", "/").split("/") + subpackage = package + "." + ".".join(parts[:-1]) if len(parts) > 1 else package + filename = parts[-1] + ref = _importlib_resources.files(subpackage).joinpath(filename) + return ref.open("rb") + else: + return _pkg_resources.resource_stream(package, resource_path) + + def parseRules(ruleFiles, comby=False): rulesText = [] @@ -17,7 +44,7 @@ def parseRules(ruleFiles, comby=False): rulePath = os.path.join('comby', ruleFile) else: rulePath = os.path.join('static', ruleFile) - with pkg_resources.resource_stream('universalmutator', rulePath) as builtInRule: + with _open_package_resource('universalmutator', rulePath) as builtInRule: for line in builtInRule: line = line.decode() rulesText.append((line, "builtin:" + ruleFile)) @@ -37,22 +64,40 @@ def parseRules(ruleFiles, comby=False): for (r, ruleSource) in rulesText: ruleLineNo += 1 - if r == "\n": + + # remove all leading and trailing white space + line = r.strip() + + # check for blank lines + if line == "": + # ignore blank lines continue - if " ==> " not in r: - if " ==>" in r: - s = r.split(" ==>") - else: - if r[0] == "#": # Don't warn about comments - continue - print("*" * 60) - print("WARNING:") - print("RULE:", r, "FROM", ruleSource) - print("DOES NOT MATCH EXPECTED FORMAT, AND SO WAS IGNORED") - print("*" * 60) - continue # Allow blank lines and comments, just ignore lines without a transformation + + # handle comments + if line.startswith("#") and "==>" not in line: + # ignore comments '#' + continue + + # check for disabled rules + if line.startswith("#DISABLED:"): + # ignore disabled rules + continue + + # check and parse valid rules + if " ==> " in line: + s = line.split(" ==> ") + elif " ==>" in line: + s = line.split(" ==>") else: - s = r.split(" ==> ") + # otherwise it's a invalid line and warn user + print("*" * 60) + print("WARNING:") + print("RULE:", line, "FROM", ruleSource) + print("DOES NOT MATCH EXPECTED FORMAT, AND SO WAS IGNORED") + print("*" * 60) + continue + + # End of possible fix if comby: lhs = s[0]