Skip to content

Commit 9042053

Browse files
committed
Add test mode to suppress browser and file operations
1 parent 4693b5c commit 9042053

File tree

2 files changed

+108
-55
lines changed

2 files changed

+108
-55
lines changed

pointblank_mcp_server/pointblank_server.py

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import json
22
import logging
33
import math
4+
import os
5+
import sys
46
import uuid
57
import webbrowser
68
from contextlib import asynccontextmanager
@@ -21,6 +23,13 @@
2123

2224
import pointblank as pb
2325

26+
# Detect if we're running in a test environment
27+
TESTING_MODE = (
28+
"pytest" in sys.modules
29+
or os.environ.get("PYTEST_CURRENT_TEST") is not None
30+
or os.environ.get("POINTBLANK_TESTING") == "true"
31+
)
32+
2433
# Try to import Pandas, but make it optional
2534
try:
2635
import pandas as pd
@@ -113,6 +122,14 @@ def _save_dataframe_to_csv(df: Any, output_path: Path) -> None:
113122
raise TypeError(f"Unsupported DataFrame type '{type(df).__name__}' for CSV export.")
114123

115124

125+
def _open_browser_conditionally(url: str) -> None:
126+
"""Open browser only if not in testing mode."""
127+
if not TESTING_MODE:
128+
webbrowser.open(url)
129+
else:
130+
logger.debug(f"Browser opening suppressed in testing mode for: {url}")
131+
132+
116133
def _load_dataframe_from_path(input_path: str, backend: str = "auto") -> Any:
117134
"""Load DataFrame from file using specified backend or auto-detect."""
118135
p_path = Path(input_path)
@@ -307,6 +324,16 @@ def _generate_validation_report_html(validator: pb.Validate, validator_id: str)
307324
Returns the file path.
308325
"""
309326
try:
327+
# Generate timestamped filename
328+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
329+
filename = f"pointblank_validation_report_{validator_id}_{timestamp}.html"
330+
file_path = Path.cwd() / filename
331+
332+
# Skip file generation during testing
333+
if TESTING_MODE:
334+
logger.debug(f"Skipping HTML file generation during testing: {filename}")
335+
return str(file_path.resolve()) # Return path but don't create file
336+
310337
# Get the validation report as a GT table
311338
gt_report = validator.get_tabular_report()
312339

@@ -329,11 +356,6 @@ def _generate_validation_report_html(validator: pb.Validate, validator_id: str)
329356
if isinstance(html_content, bytes):
330357
html_content = html_content.decode("utf-8", errors="replace")
331358

332-
# Generate timestamped filename
333-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
334-
filename = f"pointblank_validation_report_{validator_id}_{timestamp}.html"
335-
file_path = Path.cwd() / filename
336-
337359
# Save HTML file with explicit UTF-8 encoding
338360
with open(file_path, "w", encoding="utf-8", newline="") as f:
339361
f.write(html_content)
@@ -1858,7 +1880,7 @@ async def interrogate_validator(
18581880
# Generate HTML report table and open in browser
18591881
try:
18601882
html_report_path = _generate_validation_report_html(validator, validator_id)
1861-
webbrowser.open(f"file://{html_report_path}")
1883+
_open_browser_conditionally(f"file://{html_report_path}")
18621884
await ctx.report_progress(
18631885
50, 100, f"Validation report opened in browser: {html_report_path}"
18641886
)
@@ -2142,15 +2164,19 @@ async def preview_table(
21422164
)
21432165
html_path = Path.cwd() / html_filename
21442166

2145-
with open(html_path, "w", encoding="utf-8") as f:
2146-
f.write(full_html)
2167+
# Skip file generation during testing
2168+
if TESTING_MODE:
2169+
browser_msg = f"HTML preview generated (file creation skipped during testing)\n\nFile location: {html_path}"
2170+
else:
2171+
with open(html_path, "w", encoding="utf-8") as f:
2172+
f.write(full_html)
21472173

2148-
# Open in default browser
2149-
try:
2150-
webbrowser.open(f"file://{html_path}")
2151-
browser_msg = f"HTML preview saved and opened in default browser!\n\nFile location: {html_path}"
2152-
except Exception as browser_error:
2153-
browser_msg = f"HTML preview saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
2174+
# Open in default browser
2175+
try:
2176+
_open_browser_conditionally(f"file://{html_path}")
2177+
browser_msg = f"HTML preview saved and opened in default browser!\n\nFile location: {html_path}"
2178+
except Exception as browser_error:
2179+
browser_msg = f"HTML preview saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
21542180

21552181
except Exception as e:
21562182
browser_msg = f"Error saving HTML file: {str(e)}"
@@ -2228,15 +2254,19 @@ async def missing_values_table(
22282254
html_filename = f"pointblank_missing_values_{dataframe_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
22292255
html_path = Path.cwd() / html_filename
22302256

2231-
with open(html_path, "w", encoding="utf-8") as f:
2232-
f.write(full_html)
2257+
# Skip file generation during testing
2258+
if TESTING_MODE:
2259+
browser_msg = f"HTML missing values analysis generated (file creation skipped during testing)\n\nFile location: {html_path}"
2260+
else:
2261+
with open(html_path, "w", encoding="utf-8") as f:
2262+
f.write(full_html)
22332263

2234-
# Open in default browser
2235-
try:
2236-
webbrowser.open(f"file://{html_path}")
2237-
browser_msg = f"HTML missing values analysis saved and opened in default browser!\n\nFile location: {html_path}"
2238-
except Exception as browser_error:
2239-
browser_msg = f"HTML analysis saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
2264+
# Open in default browser
2265+
try:
2266+
_open_browser_conditionally(f"file://{html_path}")
2267+
browser_msg = f"HTML missing values analysis saved and opened in default browser!\n\nFile location: {html_path}"
2268+
except Exception as browser_error:
2269+
browser_msg = f"HTML analysis saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
22402270

22412271
except Exception as e:
22422272
browser_msg = f"Error saving HTML file: {str(e)}"
@@ -2315,15 +2345,19 @@ async def column_summary_table(
23152345
html_filename = f"pointblank_column_summary_{dataframe_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
23162346
html_path = Path.cwd() / html_filename
23172347

2318-
with open(html_path, "w", encoding="utf-8") as f:
2319-
f.write(full_html)
2348+
# Skip file generation during testing
2349+
if TESTING_MODE:
2350+
browser_msg = f"HTML column summary generated (file creation skipped during testing)\n\nFile location: {html_path}"
2351+
else:
2352+
with open(html_path, "w", encoding="utf-8") as f:
2353+
f.write(full_html)
23202354

2321-
# Open in default browser
2322-
try:
2323-
webbrowser.open(f"file://{html_path}")
2324-
browser_msg = f"HTML column summary saved and opened in default browser!\n\nFile location: {html_path}"
2325-
except Exception as browser_error:
2326-
browser_msg = f"HTML summary saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
2355+
# Open in default browser
2356+
try:
2357+
_open_browser_conditionally(f"file://{html_path}")
2358+
browser_msg = f"HTML column summary saved and opened in default browser!\n\nFile location: {html_path}"
2359+
except Exception as browser_error:
2360+
browser_msg = f"HTML summary saved to: {html_path}\n\n📖 Could not open browser automatically: {str(browser_error)}\nPlease open the file manually in your browser."
23272361

23282362
except Exception as e:
23292363
browser_msg = f"Error saving HTML file: {str(e)}"

tests/test_mcp_server.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pandas as pd
44
import pytest
55

6-
# Import the fastmcp Client and your mcp application instance
76
from fastmcp import Client
87
from pointblank_mcp_server.pointblank_server import mcp
98

@@ -35,54 +34,50 @@ def csv_file(tmp_path_factory, sample_df) -> str:
3534
return str(file_path)
3635

3736

38-
# --- Test Functions ---
39-
40-
4137
@pytest.mark.asyncio
4238
async def test_list_available_tools(mcp_server):
4339
"""Tests if the MCP is up and correctly lists its registered tools."""
44-
print("\n=== Testing Tool Listing ===")
40+
4541
async with Client(mcp_server) as client:
4642
tools = await client.list_tools()
4743
tool_names = [tool.name for tool in tools]
4844

49-
print(f"✅ Found {len(tool_names)} tools.")
5045
assert "load_dataframe" in tool_names
5146
assert "create_validator" in tool_names
5247
assert "add_validation_step" in tool_names
5348
assert "interrogate_validator" in tool_names
5449
assert "get_validation_step_output" in tool_names
55-
print("✅ All expected tools are registered.")
5650

5751

5852
@pytest.mark.asyncio
5953
async def test_full_validation_workflow(mcp_server, csv_file: str, tmp_path: Path):
60-
"""
61-
Tests a complete user workflow using in-memory calls to the MCP server.
62-
"""
63-
print("\n=== Testing Full Validation Workflow ===")
54+
"""Tests a complete user workflow using in-memory calls to the MCP server."""
6455

6556
# The client connects directly to the mcp object in memory
6657
async with Client(mcp_server) as client:
6758
# 1. Load a DataFrame from the CSV file
68-
print("🔧 Step 1: Calling 'load_dataframe'...")
59+
6960
result = await client.call_tool("load_dataframe", {"input_path": csv_file})
61+
7062
assert not result.is_error
63+
7164
df_info = result.data
65+
7266
assert tuple(df_info.shape) == (5, 4)
67+
7368
df_id = df_info.df_id
74-
print(f"✅ DataFrame loaded with ID: {df_id}")
7569

7670
# 2. Create a validator for the loaded DataFrame
77-
print("\n🔧 Step 2: Calling 'create_validator'...")
71+
7872
result = await client.call_tool("create_validator", {"df_id": df_id})
73+
7974
assert not result.is_error
75+
8076
validator_info = result.data
8177
validator_id = validator_info.validator_id
82-
print(f"✅ Validator created with ID: {validator_id}")
8378

8479
# 3. Add validation steps
85-
print("\n🔧 Step 3: Calling 'add_validation_step'...")
80+
8681
# This is STEP 1
8782
step1_params = {"columns": "id"}
8883
result = await client.call_tool(
@@ -94,7 +89,6 @@ async def test_full_validation_workflow(mcp_server, csv_file: str, tmp_path: Pat
9489
},
9590
)
9691
assert not result.is_error
97-
print("✅ Added passing step: 'id' column is not null")
9892

9993
# This is STEP 2
10094
step2_params = {"columns": "age", "value": 10}
@@ -107,21 +101,41 @@ async def test_full_validation_workflow(mcp_server, csv_file: str, tmp_path: Pat
107101
},
108102
)
109103
assert not result.is_error
110-
print("✅ Added failing step: 'age' column values < 10")
111104

112105
# 4. Interrogate the validator
113-
print("\n🔧 Step 4: Calling 'interrogate_validator'...")
106+
114107
result = await client.call_tool("interrogate_validator", {"validator_id": validator_id})
108+
115109
assert not result.is_error
110+
116111
interrogate_result = result.data
117112
summary = interrogate_result["validation_summary"]
118113

114+
# Verify our new functionality - Python code generation
115+
assert "python_code" in interrogate_result
116+
117+
python_code = interrogate_result["python_code"]
118+
119+
assert isinstance(python_code, str)
120+
assert "import pointblank as pb" in python_code
121+
assert "pb.Validate(df)" in python_code
122+
assert ".col_vals_not_null(columns='id')" in python_code
123+
assert ".col_vals_lt(columns='age', value=10)" in python_code
124+
assert ".interrogate()" in python_code
125+
126+
# Verify instructions are provided
127+
assert "instructions" in interrogate_result
128+
129+
instructions = interrogate_result["instructions"]
130+
131+
assert "html_report" in instructions
132+
assert "python_code" in instructions
133+
119134
# summary is 0-indexed, so summary[1] is the second step
120135
assert summary[1]["f_passed"] < 1.0
121-
print("✅ Interrogation complete. Found expected failures.")
122136

123137
# 5. Get the output for the specific failing step (step_index=2)
124-
print("\n🔧 Step 5: Calling 'get_validation_step_output' for a specific failing step...")
138+
125139
failed_step_path = str(tmp_path / "failed_step_output.csv")
126140
result = await client.call_tool(
127141
"get_validation_step_output",
@@ -133,15 +147,18 @@ async def test_full_validation_workflow(mcp_server, csv_file: str, tmp_path: Pat
133147
},
134148
)
135149
assert not result.is_error
150+
136151
output_info = result.data
152+
137153
assert output_info.output_file is not None, f"No CSV was written: {output_info.message}"
138154
assert Path(output_info.output_file).exists()
155+
139156
failed_df = pd.read_csv(output_info.output_file)
157+
140158
assert len(failed_df) == 4 # Should contain the 4 rows that failed the age check
141-
print(f"✅ Failing rows for step 2 correctly saved to '{output_info.output_file}'")
142159

143160
# 6. Get the output for all passing data across the entire run
144-
print("\n🔧 Step 6: Calling 'get_validation_step_output' for all passing data...")
161+
145162
passed_run_path = str(tmp_path / "passed_run_output.csv")
146163
result = await client.call_tool(
147164
"get_validation_step_output",
@@ -152,11 +169,13 @@ async def test_full_validation_workflow(mcp_server, csv_file: str, tmp_path: Pat
152169
},
153170
)
154171
assert not result.is_error
172+
155173
pass_info = result.data
174+
156175
assert pass_info.output_file is not None, f"No CSV was written: {pass_info.message}"
157176
assert Path(pass_info.output_file).exists()
177+
158178
pass_df = pd.read_csv(pass_info.output_file)
179+
159180
assert len(pass_df) == 1 # Only the row with age=9 passed all validations
160181
assert pass_df.iloc[0]["age"] == 9
161-
print(f"✅ All passing rows correctly saved to '{pass_info.output_file}'")
162-
print("\n🎉 Full workflow test passed!")

0 commit comments

Comments
 (0)