Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.12, 3.13]
python-version: ["3.10", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -23,4 +23,4 @@ jobs:
python -m pip install --upgrade pip
pip install .[test]
- name: Run tests with coverage
run: pytest
run: python -m pytest
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description = "Explain, test, and generate examples for regular expressions"
authors = [{name = "Dev B. Makwana"}]
license = {text = "MIT"}
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.10"
dependencies = [
"pyrailroad>=0.4.0"
]
Expand All @@ -18,8 +18,6 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -46,4 +44,5 @@ path = "src/rexplain/__init__.py"

[tool.pytest.ini_options]
addopts = "--cov=src/rexplain --cov-report=term-missing --cov-fail-under=70"
testpaths = ["tests"]
testpaths = ["tests"]
norecursedirs = ["src", ".git", "dist", "build"]
9 changes: 9 additions & 0 deletions src/rexplain/core/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ def __str__(self):
f"failed_at={self.failed_at}, partial_matches={self.partial_matches})"
)

def to_dict(self):
"""Convert the result to a dictionary format"""
return {
'matches': self.matches,
'reason': self.reason,
'failed_at': self.failed_at,
'partial_matches': self.partial_matches
}

class RegexTester:
"""
Tests if a string matches a regex pattern and provides detailed feedback.
Expand Down
105 changes: 105 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""
Tests for the public API wrapper functions in __init__.py
"""
import sys
import os
import re
import tempfile
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))

import rexplain
from rexplain import explain, examples, diagram

def test_explain_api():
"""Test the explain() API function"""
result = explain(r'\d+')
assert isinstance(result, str)
assert 'digit' in result.lower() or r'\d' in result

def test_explain_api_with_flags():
"""Test explain() with regex flags"""
result = explain(r'abc', flags=re.IGNORECASE)
assert isinstance(result, str)
assert 'a' in result.lower()

def test_examples_api():
"""Test the examples() API function"""
result = examples(r'[a-z]{3}', count=5)
assert isinstance(result, list)
assert len(result) == 5
# Verify all examples match the pattern
pattern = re.compile(r'[a-z]{3}')
for ex in result:
assert pattern.fullmatch(ex)

def test_examples_api_default_count():
"""Test examples() with default count"""
result = examples(r'\d+')
assert isinstance(result, list)
assert len(result) == 3 # Default count

def test_examples_api_with_flags():
"""Test examples() with regex flags"""
result = examples(r'[a-z]+', count=2, flags=re.IGNORECASE)
assert isinstance(result, list)
assert len(result) == 2

def test_test_api_match():
"""Test the test() API function with matching string"""
result = rexplain.test(r'foo.*', 'foobar')
assert hasattr(result, 'matches')
assert result.matches is True
assert result.reason

def test_test_api_no_match():
"""Test the test() API function with non-matching string"""
result = rexplain.test(r'abc', 'xyz')
assert hasattr(result, 'matches')
assert result.matches is False
assert result.reason

def test_test_api_with_flags():
"""Test test() with regex flags"""
result = rexplain.test(r'abc', 'ABC', flags=re.IGNORECASE)
assert result.matches is True

def test_diagram_api_basic():
"""Test the diagram() API function without output file"""
result = diagram(r'\w+')
assert isinstance(result, str)
assert '<svg' in result

def test_diagram_api_with_output():
"""Test diagram() with output file"""
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as tmp:
output_path = tmp.name

try:
result = diagram(r'\d+', output_path=output_path)
assert result == output_path
assert os.path.exists(output_path)
with open(output_path) as f:
content = f.read()
assert '<svg' in content
finally:
if os.path.exists(output_path):
os.unlink(output_path)

def test_diagram_api_detailed():
"""Test diagram() with detailed flag"""
result = diagram(r'\w+', detailed=True)
assert isinstance(result, str)
assert '<svg' in result

def test_diagram_api_detailed_with_output():
"""Test diagram() with detailed flag and output file"""
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as tmp:
output_path = tmp.name

try:
result = diagram(r'[a-z]+', output_path=output_path, detailed=True)
assert result == output_path
assert os.path.exists(output_path)
finally:
if os.path.exists(output_path):
os.unlink(output_path)
99 changes: 98 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,106 @@
import subprocess
import sys
import os
import tempfile
import pytest
from unittest.mock import patch
from io import StringIO

def test_cli_help():
cli_path = os.path.join(os.path.dirname(__file__), '../src/rexplain/cli/main.py')
result = subprocess.run([sys.executable, cli_path, '--help'], capture_output=True, text=True)
assert result.returncode == 0
assert 'usage:' in result.stdout.lower()
assert 'usage:' in result.stdout.lower()

def test_cli_version():
"""Test --version flag"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', '--version'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert result.stdout.strip() # Should output version

def test_cli_about():
"""Test --about flag"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', '--about'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert 'rexplain' in result.stdout.lower()

def test_cli_explain_basic():
"""Test explain command"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'explain', r'\d+'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert 'digit' in result.stdout.lower() or r'\d' in result.stdout

def test_cli_explain_with_examples():
"""Test explain command with --examples flag"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'explain', r'\d{2}', '--examples', '2'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert 'example' in result.stdout.lower()

def test_cli_examples():
"""Test examples command"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'examples', r'[a-z]{3}', '--count', '2'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
lines = result.stdout.strip().split('\n')
assert len(lines) >= 2 # Should have at least 2 examples

def test_cli_test_match():
"""Test test command with matching string"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'test', 'abc', 'abc'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0

def test_cli_test_no_match():
"""Test test command with non-matching string"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'test', 'abc', 'xyz'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 1 # Should exit with error code

def test_cli_diagram_stdout():
"""Test diagram command without output file"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'diagram', r'\w+'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert '<svg' in result.stdout

def test_cli_diagram_with_output():
"""Test diagram command with output file"""
with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as tmp:
output_path = tmp.name

try:
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'diagram', r'\w+', '--output', output_path],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert os.path.exists(output_path)
with open(output_path) as f:
content = f.read()
assert '<svg' in content
finally:
if os.path.exists(output_path):
os.unlink(output_path)

def test_cli_diagram_detailed():
"""Test diagram command with --detailed flag"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'diagram', r'\d+', '--detailed'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 0
assert '<svg' in result.stdout

def test_cli_invalid_pattern():
"""Test CLI with invalid regex pattern"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main', 'explain', '(unclosed'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 1
assert 'error' in result.stderr.lower() or 'error' in result.stdout.lower()

def test_cli_no_command():
"""Test CLI with no command shows help"""
result = subprocess.run([sys.executable, '-m', 'rexplain.cli.main'],
capture_output=True, text=True, cwd='/home/user/rexplain/src')
assert result.returncode == 1
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Test Functions Use Hardcoded Absolute Paths

Multiple CLI test functions use a hardcoded absolute path, /home/user/rexplain/src, for the cwd parameter in subprocess.run() calls. This path is specific to a single development environment, causing tests to fail on other systems or CI/CD environments where the project isn't located there. test_cli_help() avoids this, suggesting an accidental commit of development-specific code.

Fix in Cursor Fix in Web

assert 'usage:' in result.stdout.lower() or 'usage:' in result.stderr.lower()
54 changes: 54 additions & 0 deletions tests/test_explainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,66 @@ def test_explain_quantifiers():
# Accept both the new and fallback output
assert r"\d{2,4} - matches a digit character 2 to 4 times" in result or r"\d{2,4}" in result or "digit character" in result

def test_explain_negated_charclass():
parser = RegexParser()
pattern = r'[^a-z]'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert r"[^a-z]" in result or "negated" in result.lower() or "not" in result.lower()

def test_explain_word_boundary():
parser = RegexParser()
pattern = r'\bword\b'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert r"\b" in result

def test_explain_dot_metachar():
parser = RegexParser()
pattern = r'a.b'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert '.' in result

def test_explain_star_quantifier():
parser = RegexParser()
pattern = r'a*'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert '*' in result or 'zero or more' in result.lower()

def test_explain_plus_quantifier():
parser = RegexParser()
pattern = r'a+'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert '+' in result or 'one or more' in result.lower()

def test_explain_alternation():
parser = RegexParser()
pattern = r'foo|bar'
ast = parser.parse(pattern)
result = explain(ast)
print('Explanation:', result)
assert 'f' in result and 'b' in result

def main():
test_explain_basic()
test_explain_named_group()
test_explain_lookahead()
test_explain_inline_flags()
test_explain_quantifiers()
test_explain_negated_charclass()
test_explain_word_boundary()
test_explain_dot_metachar()
test_explain_star_quantifier()
test_explain_plus_quantifier()
test_explain_alternation()
print('All explainer tests passed!')

if __name__ == '__main__':
Expand Down
Loading
Loading