108 lines
3.2 KiB
Python
108 lines
3.2 KiB
Python
"""Tests for syntax preflight check in execute_code (issue #312)."""
|
|
|
|
import ast
|
|
import json
|
|
import pytest
|
|
|
|
|
|
class TestSyntaxPreflight:
|
|
"""Verify that execute_code catches syntax errors before sandbox execution."""
|
|
|
|
def test_valid_syntax_passes_parse(self):
|
|
"""Valid Python should pass ast.parse."""
|
|
code = "print('hello')\nx = 1 + 2\n"
|
|
ast.parse(code) # should not raise
|
|
|
|
def test_syntax_error_indentation(self):
|
|
"""IndentationError is a subclass of SyntaxError."""
|
|
code = "def foo():\nbar()\n"
|
|
with pytest.raises(SyntaxError):
|
|
ast.parse(code)
|
|
|
|
def test_syntax_error_missing_colon(self):
|
|
code = "if True\n pass\n"
|
|
with pytest.raises(SyntaxError):
|
|
ast.parse(code)
|
|
|
|
def test_syntax_error_unmatched_paren(self):
|
|
code = "x = (1 + 2\n"
|
|
with pytest.raises(SyntaxError):
|
|
ast.parse(code)
|
|
|
|
def test_syntax_error_invalid_token(self):
|
|
code = "x = 1 +*\n"
|
|
with pytest.raises(SyntaxError):
|
|
ast.parse(code)
|
|
|
|
def test_syntax_error_details(self):
|
|
"""SyntaxError should provide line, offset, msg."""
|
|
code = "if True\n pass\n"
|
|
with pytest.raises(SyntaxError) as exc_info:
|
|
ast.parse(code)
|
|
e = exc_info.value
|
|
assert e.lineno is not None
|
|
assert e.msg is not None
|
|
|
|
def test_empty_string_passes(self):
|
|
"""Empty string is valid Python (empty module)."""
|
|
ast.parse("")
|
|
|
|
def test_comments_only_passes(self):
|
|
ast.parse("# just a comment\n# another\n")
|
|
|
|
def test_complex_valid_code(self):
|
|
code = '''
|
|
import os
|
|
def foo(x):
|
|
if x > 0:
|
|
return x * 2
|
|
return 0
|
|
|
|
result = [foo(i) for i in range(10)]
|
|
print(result)
|
|
'''
|
|
ast.parse(code)
|
|
|
|
|
|
class TestSyntaxPreflightResponse:
|
|
"""Test the error response format from the preflight check."""
|
|
|
|
def _check_syntax(self, code):
|
|
"""Mimic the preflight check logic from execute_code."""
|
|
try:
|
|
ast.parse(code)
|
|
return None
|
|
except SyntaxError as e:
|
|
return json.dumps({
|
|
"error": f"Python syntax error: {e.msg}",
|
|
"line": e.lineno,
|
|
"offset": e.offset,
|
|
"text": (e.text or "").strip()[:200],
|
|
})
|
|
|
|
def test_returns_json_error(self):
|
|
result = self._check_syntax("if True\n pass\n")
|
|
assert result is not None
|
|
data = json.loads(result)
|
|
assert "error" in data
|
|
assert "syntax error" in data["error"].lower()
|
|
|
|
def test_includes_line_number(self):
|
|
result = self._check_syntax("x = 1\nif True\n pass\n")
|
|
data = json.loads(result)
|
|
assert data["line"] == 2 # error on line 2
|
|
|
|
def test_includes_offset(self):
|
|
result = self._check_syntax("x = (1 + 2\n")
|
|
data = json.loads(result)
|
|
assert data["offset"] is not None
|
|
|
|
def test_includes_snippet(self):
|
|
result = self._check_syntax("if True\n")
|
|
data = json.loads(result)
|
|
assert "if True" in data["text"]
|
|
|
|
def test_none_for_valid_code(self):
|
|
result = self._check_syntax("print('ok')")
|
|
assert result is None
|