Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: test now runs each stimulus in browser and checks for errors #132

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a290ba0
tests: test now runs each stimulus in browser and checks for errors
younesStrittmatter Mar 5, 2025
8d2925e
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
697dea7
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
f95b624
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
938970b
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
166b157
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
71b46bd
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
3cf57c0
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
fc4da5f
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
547a20f
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
f150feb
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
5675bb0
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
2edf913
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
9158e2a
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
65732ed
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
ed64393
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
da13a81
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
6cc3f4d
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
e8b7fd4
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
e43fca5
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
87b4466
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
b560bf4
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
cf1c42c
tests: fix headless in github actions
younesStrittmatter Mar 5, 2025
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
107 changes: 99 additions & 8 deletions .github/workflows/test-pytest.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Test with py.test

on:
Expand All @@ -16,14 +13,108 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
- run: pip install ".[test]"
- run: pytest tests/

# 1) Install your package (and pytest)
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
pip install pytest
shell: bash

# 2) Install Chromium on Linux
- name: Install Chromium (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
# Try installing 'chromium-browser'; fallback to 'chromium'
sudo apt-get install -y chromium-browser || sudo apt-get install -y chromium

# 3) Install Chromium on macOS
- name: Install Chromium (macOS)
if: runner.os == 'macOS'
shell: bash
run: |
brew install chromium
# Set the path for Pyppeteer
echo "PYPPETEER_EXECUTABLE_PATH=$(which chromium)" >> $GITHUB_ENV

# 4) Install Chromium on Windows
- name: Install Chromium (Windows)
if: runner.os == 'Windows'
shell: powershell
run: |
choco install chromium -y
# Check the default install path:
$defaultChromeExe = "C:\Program Files\Chromium\Application\chrome.exe"
if (Test-Path $defaultChromeExe) {
# If it exists, use it
echo "PYPPETEER_EXECUTABLE_PATH=$defaultChromeExe" >> $env:GITHUB_ENV
}
else {
# Fallback: Try to detect "chrome" via Get-Command
$chromePath = (Get-Command chrome -ErrorAction SilentlyContinue).Source
if (!$chromePath) {
$chromePath = (Get-Command chrome.exe -ErrorAction SilentlyContinue).Source
}
if (!$chromePath) {
Write-Host "Chromium installation failed or not found!"
exit 1
}
echo "PYPPETEER_EXECUTABLE_PATH=$chromePath" >> $env:GITHUB_ENV
}

# 5) Remove Pyppeteer’s auto-downloaded Chromium on macOS
- name: Remove Pyppeteer Chromium Cache (macOS)
if: runner.os == 'macOS'
run: rm -rf ~/Library/Application\ Support/pyppeteer/local-chromium
shell: bash

# 6) Symlink system Chromium (macOS, Linux) so Pyppeteer finds it
- name: Set up Pyppeteer (macOS/Linux)
if: runner.os != 'Windows'
shell: bash
run: |
mkdir -p ~/.local/share/pyppeteer/local-chromium
ln -sf $(which chromium-browser || which chromium || which chromium.app/Contents/MacOS/Chromium) \
~/.local/share/pyppeteer/local-chromium/1181205

# 7) Copy system Chromium (Windows) so Pyppeteer finds it
- name: Set up Pyppeteer (Windows)
if: runner.os == 'Windows'
shell: powershell
run: |
$chromePath = $env:PYPPETEER_EXECUTABLE_PATH
if (!$chromePath) {
Write-Host "Chromium path not found in GITHUB_ENV!"
exit 1
}
if (!(Test-Path $chromePath)) {
Write-Host "Chromium file does not exist at $chromePath!"
exit 1
}
New-Item -ItemType Directory -Force "$HOME\.local\share\pyppeteer\local-chromium" | Out-Null
Copy-Item $chromePath "$HOME\.local\share\pyppeteer\local-chromium\1181205"

# 8) Verify pytest is installed
- name: Check pytest installation
run: python -m pytest --version

# 9) Run tests
- name: Run tests
env:
PYPPETEER_EXECUTABLE_PATH: ${{ env.PYPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser' }}
run: python -m pytest tests/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pip install sweetbean

SweetBean is compatible with:

- **Python**: `>=3.7, <4.0`
- **Python**: `>=3.9, <4.0`
- **jsPsych**: `7.x`

Other versions may work but are not officially supported. If you experience issues, please report them!
Expand Down
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pip install sweetbean

SweetBean is compatible with:

- **Python**: `>=3.7, <4.0`
- **Python**: `>=3.9, <4.0`
- **jsPsych**: `7.x`

Other versions may work but are not officially supported. If you experience issues, please report them!
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dynamic = ["version"]

readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.7,<4"
requires-python = ">=3.9,<4"

# ADD NEW DEPENDENCIES HERE
dependencies = [
Expand Down
143 changes: 109 additions & 34 deletions tests/test_stimuli.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,134 @@
import asyncio
import importlib
import inspect
import os
import pkgutil
import platform
import sys

import pytest
from pyppeteer import launch

import sweetbean
from sweetbean import Block, Experiment
from sweetbean.stimulus.Stimulus import _BaseStimulus

# Define the excluded stimuli
excludes = [
EXCLUDES = {
"SurveyStimulus",
"_BaseStimulus",
"_Survey",
"_KeyboardResponseStimulus",
"_Template_",
"Generic",
]
}

SKIP = {
"Video": "Requires a video file",
}

def test_compile():
# Dynamically load all modules in the 'sweetbean.stimulus' package
stimuli_package = sweetbean.stimulus

# Dynamically load all modules in the 'sweetbean.stimulus' package
def get_stimuli_list():
"""Return all valid stimulus classes from sweetbean.stimulus."""
stimuli_package = sweetbean.stimulus
# Dynamically import modules
for _, module_name, _ in pkgutil.iter_modules(stimuli_package.__path__):
importlib.import_module(f"sweetbean.stimulus.{module_name}")

# Gather all subclasses of Stimulus from the loaded modules
stimuli_list = []
found = []
for module in sys.modules.values():
if module and module.__name__.startswith("sweetbean.stimulus"):
for name, cls in inspect.getmembers(module, inspect.isclass):
if issubclass(cls, _BaseStimulus) and cls.__name__ not in excludes:
stimuli_list.append(cls)

# Debugging: Print the found stimuli
print()
print("Found stimuli:", stimuli_list)
print()

for stimulus in stimuli_list:
# Log which stimulus is being tested
print(f"Testing {stimulus.__name__}...")

# Test each stimulus
stimulus_instance = stimulus() # Adjust if parameters are required
trial_sequence = Block([stimulus_instance])
experiment = Experiment([trial_sequence])
experiment.to_html("basic.html")
assert os.path.exists(
"basic.html"
), f"{stimulus.__name__} failed to generate HTML."
os.remove("basic.html")
print(f"{stimulus.__name__} compiled successfully.")
for name, cls in getattr(module, "__dict__", {}).items():
if (
isinstance(cls, type)
and issubclass(cls, _BaseStimulus)
and cls.__name__ not in EXCLUDES
):
found.append(cls)

print("Collected stimuli:", [set(cls.__name__ for cls in found)])
return list(set(found))


# Collect the stimuli classes once
ALL_STIMULI = get_stimuli_list()


async def run_experiment_in_browser(html_path: str):
"""Launch headless Chromium, load the HTML, check for errors, then close."""
executable_path = os.getenv("PYPPETEER_EXECUTABLE_PATH", None)
print("Using Chromium path:", executable_path)

browser = await launch(
headless=True,
executablePath=executable_path,
args=[
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-gpu",
"--disable-dev-shm-usage",
],
)
page = await browser.newPage()

file_url = f"file://{os.path.abspath(html_path)}"
console_errors = []

def capture_console(msg):
if msg.type == "error":
console_errors.append(msg.text)

page.on("console", capture_console)

await page.goto(file_url, options={"timeout": 60000, "waitUntil": "networkidle2"})
await page.waitForSelector("body")

title = await page.title()
assert title == "My awesome experiment", f"Title mismatch: {title}"

filtered = [
err
for err in console_errors
if "ERR_CERT_VERIFIER_CHANGED" not in err
and "ERR_SOCKET_NOT_CONNECTED" not in err
]

assert not filtered, f"JS console errors found: {filtered}"

await browser.close()
print(f"✅ {html_path} loaded successfully.")


@pytest.mark.parametrize("stimulus_class", ALL_STIMULI, ids=lambda cls: cls.__name__)
def test_compile_stimulus(stimulus_class):
"""Test each stimulus in its own test."""
# (Optional) skip known heavy stimulus in CI
# if os.getenv("CI") and stimulus_class.__name__ == "Bandit":
# pytest.skip("Skipping Bandit on CI to avoid crashes.")
# 1) Compile the experiment
stimulus_instance = stimulus_class()
experiment = Experiment([Block([stimulus_instance])])
html_path = "basic.html"
experiment.to_html(html_path)

assert os.path.exists(html_path), f"{stimulus_class.__name__} didn't create HTML!"

# 2) Run the HTML in browser
if os.getenv("CI") and (
platform.system() == "Linux" or platform.system() == "Windows"
):
print("Skipping browser test on CI Ubuntu do to limited resources on GitHub.")
else:
if stimulus_class.__name__ not in SKIP:
asyncio.run(run_experiment_in_browser(html_path))
else:
print(
f"Skipping {stimulus_class.__name__} due to: {SKIP[stimulus_class.__name__]}"
)

# 3) Cleanup
os.remove(html_path)


if __name__ == "__main__":
test_compile()
print([s.__name__ for s in ALL_STIMULI])
for s in ALL_STIMULI:
test_compile_stimulus(s)
Loading