Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 241 additions & 0 deletions tests/test_comments_rules.py
Original file line number Diff line number Diff line change
@@ -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: <code expression> ==> <replacement>\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()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
77 changes: 61 additions & 16 deletions universalmutator/mutator.py
Original file line number Diff line number Diff line change
@@ -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 = []

Expand All @@ -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))
Expand All @@ -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]
Expand Down