Files
2026-07-13 13:22:52 +08:00

193 lines
6.2 KiB
Python

"""Tests for force plot using JavaScript/React rendering with Selenium."""
import os
import platform
import tempfile
import time
import numpy as np
import pytest
from PIL import Image
from sklearn.ensemble import RandomForestClassifier
import shap
from shap.plots._force import save_html
# Skip if selenium is not installed
pytest.importorskip("selenium")
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
@pytest.fixture(scope="module")
def driver():
"""Setup headless Chrome/Chromium for testing."""
options = Options()
options.add_argument("--headless")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
# Try with both 'chrome' and 'chromium' executable names
try:
driver = webdriver.Chrome(options=options)
except Exception:
try:
driver = webdriver.Chrome(service=Service("/usr/bin/chromium"), options=options)
except Exception:
pytest.skip("Chrome/Chromium not available for Selenium tests")
driver.set_window_size(1000, 600)
yield driver
driver.quit()
def get_sample_force_plot():
"""Create a sample force plot for testing."""
# Create a simple model
X, y = shap.datasets.adult(n_points=100)
model = RandomForestClassifier(n_estimators=10, max_depth=5, random_state=0)
model.fit(X, y)
# Create the explainer
ex = shap.TreeExplainer(model)
shap_values = ex(X)
# Get the first instance's explanation
return shap.plots.force(ex.expected_value[0], shap_values.values[:, 0])
def get_sample_force_array_plot():
"""Create a sample force array plot with multiple instances for testing."""
# Create a simple model
X, y = shap.datasets.adult(n_points=100)
model = RandomForestClassifier(n_estimators=10, max_depth=5, random_state=0)
model.fit(X, y)
# Create the explainer
explainer = shap.TreeExplainer(model)
shap_values = explainer(X)
# Get multiple explanations
return shap.plots.force(explainer.expected_value[0], shap_values.values[:5, 0])
def capture_plot_screenshot(driver, plot, filename=None, wait_time=2):
"""
Render a force plot and capture a screenshot using Selenium.
Parameters
----------
driver : selenium.webdriver
The selenium webdriver instance
plot : AdditiveForceVisualizer or AdditiveForceArrayVisualizer
The SHAP force plot to render
filename : str, optional
Path where to save the HTML file, if None a temporary file is used
wait_time : int, optional
Time to wait for JavaScript execution in seconds
Returns
-------
PIL.Image
The screenshot image
"""
import io
# Create temp HTML file if no filename provided
if filename is None:
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
filename = tmp.name
# Save the force plot as HTML
save_html(filename, plot)
# Open the HTML in the browser
driver.get(f"file://{filename}")
# Wait for the plot to render
time.sleep(wait_time)
# Take a screenshot
screenshot = driver.get_screenshot_as_png()
# Clean up the temp file
if not filename.startswith(os.path.join(os.path.dirname(__file__), "baseline")):
os.unlink(filename)
# Convert to PIL image
return Image.open(io.BytesIO(screenshot))
@pytest.mark.skipif(platform.system() == "Windows", reason="Selenium force plot tests have different sizes on windows.")
def test_force_js_visual(driver):
"""Test that force plot renders correctly."""
# Create directory for baseline images if it doesn't exist
baseline_dir = os.path.join(os.path.dirname(__file__), "baseline")
os.makedirs(baseline_dir, exist_ok=True)
# Get sample force plot
plot = get_sample_force_plot()
# Define baseline path
baseline_path = os.path.join(baseline_dir, "test_force_js.png")
# Capture screenshot
screenshot = capture_plot_screenshot(driver, plot)
# If baseline doesn't exist, save current screenshot as baseline
if not os.path.exists(baseline_path):
screenshot.save(baseline_path)
pytest.skip(f"Baseline image created at {baseline_path}")
# Compare with baseline
baseline = Image.open(baseline_path)
# Ensure same dimensions
assert screenshot.size == baseline.size, "Screenshot dimensions don't match baseline"
# Convert to numpy arrays for comparison
screenshot_array = np.array(screenshot)
baseline_array = np.array(baseline)
# Calculate difference (allowing for some variation)
diff = np.mean(np.abs(screenshot_array.astype(float) - baseline_array.astype(float)))
assert diff < 10.0, f"Images differ by {diff} average pixel value"
@pytest.mark.skipif(platform.system() == "Windows", reason="Selenium force plot tests have different sizes on windows.")
def test_force_array_js_visual(driver):
"""Test that force array plot renders correctly."""
# Create directory for baseline images if it doesn't exist
baseline_dir = os.path.join(os.path.dirname(__file__), "baseline")
os.makedirs(baseline_dir, exist_ok=True)
# Get sample force array plot
plot = get_sample_force_array_plot()
# Define baseline path
baseline_path = os.path.join(baseline_dir, "test_force_array_js.png")
# Capture screenshot
screenshot = capture_plot_screenshot(driver, plot, wait_time=3) # Array plot might need more time
# If baseline doesn't exist, save current screenshot as baseline
if not os.path.exists(baseline_path):
screenshot.save(baseline_path)
pytest.skip(f"Baseline image created at {baseline_path}")
# Compare with baseline
baseline = Image.open(baseline_path)
# Ensure same dimensions
assert screenshot.size == baseline.size, "Screenshot dimensions don't match baseline"
# Convert to numpy arrays for comparison
screenshot_array = np.array(screenshot)
baseline_array = np.array(baseline)
# Calculate difference (allowing for some variation)
diff = np.mean(np.abs(screenshot_array.astype(float) - baseline_array.astype(float)))
assert diff < 15.0, f"Images differ by {diff} average pixel value"