Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
126 changes: 126 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Lint and Tests

on:
push:
pull_request:
branches: [ main, develop ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true

- name: Install dependencies
run: |
uv sync --all-extras --dev

- name: Install Playwright browsers
run: |
uv run playwright install chromium

- name: Lint with ruff
run: |
uv run ruff check .
uv run ruff format --check .

- name: Run tests
run: |
uv run pytest -v --tb=short

- name: Run tests with parallel execution
run: |
uv run pytest -n auto --tb=short

frontend-tests:
runs-on: ubuntu-latest
needs: test

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true

- name: Install dependencies
run: |
uv sync --all-extras --dev

- name: Install Playwright browsers
run: |
uv run playwright install chromium --with-deps

- name: Run frontend tests only
run: |
uv run pytest align_browser/test_frontend.py -v

- name: Run real data tests (if available)
run: |
uv run pytest align_browser/test_frontend_real_data.py -v
continue-on-error: true # Real data might not be available in CI

- name: Upload screenshots on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: /tmp/*.png
retention-days: 7

build:
runs-on: ubuntu-latest
needs: test

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true

- name: Install dependencies
run: |
uv sync --all-extras --dev

- name: Test build script
run: |
# Test the build script works
uv run pytest align_browser/test_build.py -v

- name: Build package
run: |
uv build

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: python-package
path: dist/
retention-days: 7
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ wheels/
# Data directory (generated by build script)
dist/
align-browser-site/
align_browser/static/data/

# Real experiment data for testing (user-provided)
experiment-data/

# Virtual environments
venv/
Expand All @@ -46,6 +50,7 @@ Thumbs.db
.coverage
htmlcov/
.tox/
.test_data.lock

# Jupyter
.ipynb_checkpoints/
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- After you make a code change, run the build and the http server
- Don't run the http server after stopping
- Use semantic versioning commit messages
- After you make non trival changes, run ruff linting, then ruff formating, then the tests

## Testing

Expand Down
2 changes: 1 addition & 1 deletion align_browser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Align Browser - Static web application for visualizing align-system experiment results."""

__version__ = "0.2.1"
__version__ = "0.2.1"
129 changes: 84 additions & 45 deletions align_browser/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
import argparse
from datetime import datetime

try:
from importlib.resources import files
except ImportError:
Expand All @@ -22,38 +23,100 @@ def copy_static_assets(output_dir):
try:
# Use importlib.resources for robust package data access
static_files = files("align_browser.static")

for filename in ["index.html", "app.js", "state.js", "style.css"]:
try:
# Read the file content from the package
file_content = (static_files / filename).read_bytes()

# Write to destination
dst_file = output_dir / filename
dst_file.write_bytes(file_content)

except FileNotFoundError:
pass

except Exception as e:
# Fallback to filesystem approach for development
print(f"Package resource access failed, trying filesystem fallback: {e}")
script_dir = Path(__file__).parent
static_dir = script_dir / "static"

if not static_dir.exists():
raise FileNotFoundError(f"Static assets directory not found: {static_dir}")

static_files = ["index.html", "app.js", "state.js", "style.css"]

for filename in static_files:
src_file = static_dir / filename
dst_file = output_dir / filename

if src_file.exists():
shutil.copy2(src_file, dst_file)


def build_frontend(
experiments_root: Path,
output_dir: Path,
dev_mode: bool = False,
build_only: bool = True,
):
"""
Build frontend with experiment data.

Args:
experiments_root: Path to experiments directory
output_dir: Output directory for the site
dev_mode: Use development mode (no static asset copying)
build_only: Only build data, don't start server
"""
print(f"Processing experiments directory: {experiments_root}")

# Determine output directory based on mode
if dev_mode:
print("Development mode: using provided directory")
else:
# Production mode: copy static assets
print(f"Production mode: creating site in {output_dir}")
output_dir.mkdir(parents=True, exist_ok=True)
copy_static_assets(output_dir)

# Create data subdirectory and clean it
data_output_dir = output_dir / "data"
if data_output_dir.exists():
shutil.rmtree(data_output_dir)
data_output_dir.mkdir(exist_ok=True)

# Parse experiments and build manifest
experiments = parse_experiments_directory(experiments_root)
manifest = build_manifest_from_experiments(experiments, experiments_root)

# Add generation timestamp (deterministic for tests)
import os

if os.getenv("PYTEST_CURRENT_TEST"):
# Use deterministic timestamp during tests
manifest.metadata["generated_at"] = "2024-01-01T00:00:00"
else:
# Use actual timestamp in production
manifest.metadata["generated_at"] = datetime.now().isoformat()

# Copy experiment data files
copy_experiment_files(experiments, experiments_root, data_output_dir)

# Save manifest in data subdirectory
with open(data_output_dir / "manifest.json", "w") as f:
json.dump(manifest.model_dump(), f, indent=2)

print(f"Data generated in {data_output_dir}")

# Start HTTP server unless build-only is specified
if not build_only:
serve_directory(output_dir)

return output_dir


def main():
parser = argparse.ArgumentParser(
description="Generate static web app for ADM Results."
Expand Down Expand Up @@ -95,53 +158,29 @@ def main():

experiments_root = Path(args.experiments).resolve()

print(f"Processing experiments directory: {experiments_root}")

# Determine output directory based on mode
if args.dev:
# Development mode: use align-browser-site/ directory
script_dir = Path(__file__).parent
output_dir = script_dir.parent / "align-browser-site"
print("Development mode: using align-browser-site/ directory")


# Ensure development directory exists
if not output_dir.exists():
raise FileNotFoundError(f"Development mode requires align-browser-site/ directory: {output_dir}")

raise FileNotFoundError(
f"Development mode requires align-browser-site/ directory: {output_dir}"
)

build_frontend(
experiments_root, output_dir, dev_mode=True, build_only=args.build_only
)
else:
# Production mode: use specified output directory and copy static assets
# Production mode: use specified output directory
output_dir = Path(args.output_dir).resolve()
print(f"Production mode: creating site in {output_dir}")

# Ensure output directory exists
output_dir.mkdir(parents=True, exist_ok=True)

# Copy static assets to output directory
copy_static_assets(output_dir)

# Create data subdirectory and clean it
data_output_dir = output_dir / "data"
if data_output_dir.exists():
shutil.rmtree(data_output_dir)
data_output_dir.mkdir(exist_ok=True)

# Parse experiments and build manifest
experiments = parse_experiments_directory(experiments_root)
manifest = build_manifest_from_experiments(experiments, experiments_root)

# Add generation timestamp
manifest.metadata["generated_at"] = datetime.now().isoformat()

# Copy experiment data files
copy_experiment_files(experiments, experiments_root, data_output_dir)
build_frontend(
experiments_root, output_dir, dev_mode=False, build_only=args.build_only
)

# Save manifest in data subdirectory
with open(data_output_dir / "manifest.json", "w") as f:
json.dump(manifest.model_dump(), f, indent=2)

print(f"Data generated in {data_output_dir}")

# Start HTTP server unless build-only is specified
# Start HTTP server if not build-only
if not args.build_only:
serve_directory(output_dir, args.host, args.port)

Expand Down
Loading
Loading