Data_analysis_agent / tests /unit /test_python_sandbox.py
rohitdeshmukh318's picture
initial commit
abd4352
"""
tests/unit/test_python_sandbox.py
Tests for RestrictedPython-based Pandas execution sandbox.
"""
import pandas as pd
import pytest
from sandbox.python_sandbox import validate_python, run_pandas
@pytest.fixture
def df():
return pd.DataFrame({
"product": ["Widget", "Gadget", "Widget", "Doohickey"],
"amount": [99.99, 149.99, 99.99, 49.99],
"region": ["North", "South", "North", "East"],
})
# ── validate_python ───────────────────────────────────────────────────────────
@pytest.mark.unit
class TestValidatePython:
def test_safe_code_passes(self):
code = "result = df.groupby('product')['amount'].sum().to_frame()"
ok, err = validate_python(code)
assert ok, err
def test_blocks_open(self):
ok, err = validate_python("open('/etc/passwd')")
assert not ok
assert "open" in err
def test_blocks_os_import(self):
ok, err = validate_python("import os\nresult = df")
assert not ok
def test_blocks_subprocess(self):
ok, err = validate_python("import subprocess\nresult = df")
assert not ok
def test_blocks_dunder_attr(self):
ok, err = validate_python("df.__class__.__bases__")
assert not ok
def test_blocks_exec(self):
ok, err = validate_python("exec('import os')")
assert not ok
def test_blocks_eval(self):
ok, err = validate_python("eval('1+1')")
assert not ok
def test_allows_pandas_import(self):
ok, err = validate_python("import pandas as pd\nresult = df")
assert ok
def test_allows_numpy_import(self):
ok, err = validate_python("import numpy as np\nresult = df")
assert ok
def test_blocks_requests_import(self):
ok, err = validate_python("import requests\nresult = df")
assert not ok
def test_syntax_error(self):
ok, err = validate_python("def broken(:\n pass")
assert not ok
# ── run_pandas ────────────────────────────────────────────────────────────────
@pytest.mark.unit
class TestRunPandas:
def test_basic_groupby(self, df):
code = "result = df.groupby('product')['amount'].sum().to_frame()"
out = run_pandas(code, df)
assert isinstance(out, pd.DataFrame)
assert "amount" in out.columns
def test_filter(self, df):
code = "result = df[df['region'] == 'North'].copy()"
out = run_pandas(code, df)
assert len(out) == 2
def test_new_column(self, df):
code = "result = df.copy()\nresult['doubled'] = result['amount'] * 2"
out = run_pandas(code, df)
assert "doubled" in out.columns
def test_series_result_coerced_to_dataframe(self, df):
code = "result = df['amount']"
out = run_pandas(code, df)
assert isinstance(out, pd.DataFrame)
def test_missing_result_raises(self, df):
with pytest.raises(ValueError, match="result"):
run_pandas("x = 1", df)
def test_non_dataframe_result_raises(self, df):
with pytest.raises(TypeError):
run_pandas("result = 42", df)
def test_safety_block_raises_permission_error(self, df):
with pytest.raises(PermissionError, match="SAFETY_BLOCK"):
run_pandas("import os\nresult = df", df)
def test_augmented_assignment(self, df):
# RestrictedPython disallows augmented assignment on subscripts (result['col'] += x)
# Use explicit assignment instead β€” the sandbox enforces this restriction correctly
code = "result = df.copy()\nresult['amount'] = result['amount'] + 10"
out = run_pandas(code, df)
assert out["amount"].min() > 50
def test_sort_and_limit(self, df):
code = "result = df.sort_values('amount', ascending=False).head(2)"
out = run_pandas(code, df)
assert len(out) == 2
assert out.iloc[0]["amount"] == pytest.approx(149.99)
def test_pivot_table(self, df):
code = """
result = df.pivot_table(
index='region', values='amount', aggfunc='sum'
).reset_index()
"""
out = run_pandas(code, df)
assert "region" in out.columns