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
203 changes: 203 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
name: CI - Build and Test Application

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

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]
python-version: ['3.10', '3.11']

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

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

- name: Cache Python dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Install Node.js dependencies
run: npm ci

- name: Lint frontend code
run: npm run lint
continue-on-error: true

- name: Build frontend
run: npm run build

- name: Run Python tests
run: |
python -m pytest test/ -v

- name: Start backend server and health check
run: |
# Start the backend server in the background
python -m src.backend &
BACKEND_PID=$!

# Wait for the server to start
echo "Waiting for backend server to start..."
sleep 10

# Health check - test if the server is responding
max_attempts=30
attempt=1

while [ $attempt -le $max_attempts ]; do
if curl -f http://localhost:8000/health 2>/dev/null; then
echo "Backend server is healthy!"
break
elif curl -f http://localhost:8000/ 2>/dev/null; then
echo "Backend server is responding!"
break
else
echo "Attempt $attempt/$max_attempts: Backend not ready yet..."
sleep 2
attempt=$((attempt + 1))
fi
done

if [ $attempt -gt $max_attempts ]; then
echo "Backend server failed to start properly"
kill $BACKEND_PID 2>/dev/null || true
exit 1
fi

# Test basic API endpoints if they exist
echo "Testing backend endpoints..."

# Clean up
kill $BACKEND_PID 2>/dev/null || true
echo "Backend server test completed successfully!"

- name: Test frontend build serves correctly
run: |
# Start the preview server in the background
npm run preview &
FRONTEND_PID=$!

# Wait for the server to start
echo "Waiting for frontend server to start..."
sleep 5

# Health check for frontend
max_attempts=15
attempt=1

while [ $attempt -le $max_attempts ]; do
if curl -f http://localhost:4173/ 2>/dev/null; then
echo "Frontend server is serving correctly!"
break
else
echo "Attempt $attempt/$max_attempts: Frontend not ready yet..."
sleep 2
attempt=$((attempt + 1))
fi
done

if [ $attempt -gt $max_attempts ]; then
echo "Frontend server failed to start properly"
kill $FRONTEND_PID 2>/dev/null || true
exit 1
fi

# Clean up
kill $FRONTEND_PID 2>/dev/null || true
echo "Frontend server test completed successfully!"

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

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
npm ci

- name: Build frontend
run: npm run build

- name: Test full application stack
run: |
# Start both frontend and backend
echo "Starting full application stack..."

# Start backend
python -m src.backend &
BACKEND_PID=$!

# Start frontend preview
npm run preview &
FRONTEND_PID=$!

# Wait for both services
sleep 15

# Test both services are running
echo "Testing backend..."
if ! curl -f http://localhost:8000/ 2>/dev/null; then
echo "Backend failed to start"
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
exit 1
fi

echo "Testing frontend..."
if ! curl -f http://localhost:4173/ 2>/dev/null; then
echo "Frontend failed to start"
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true
exit 1
fi

echo "Both services are running successfully!"

# Clean up
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null || true

echo "Integration test completed successfully!"
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"start:backend": "python ./src/backend.py",
"start:backend": "python -m src.backend",
"start:both": "concurrently \"npm run dev\" \"npm run start:backend\""
},
"dependencies": {
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ Werkzeug==3.1.3
pathsim==0.7.0
matplotlib==3.7.0
numpy==1.24.0
plotly~=6
plotly~=6.0
pytest
3 changes: 3 additions & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import custom_pathsim_blocks
from . import convert_to_python
from . import backend
109 changes: 75 additions & 34 deletions src/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import json
from flask import Flask, request, jsonify
from flask_cors import CORS
from convert_to_python import convert_graph_to_python
import math
import numpy as np
import plotly.graph_objects as go
Expand All @@ -11,6 +10,7 @@
import json as plotly_json

from pathsim import Simulation, Connection
from pathsim.events import Event
import pathsim.solvers
from pathsim.blocks import (
Scope,
Expand All @@ -28,14 +28,37 @@
PID,
Schedule,
)
from custom_pathsim_blocks import Process, Splitter
from .custom_pathsim_blocks import Process, Splitter
from .convert_to_python import convert_graph_to_python

NAME_TO_SOLVER = {
"SSPRK22": pathsim.solvers.SSPRK22,
"SSPRK33": pathsim.solvers.SSPRK33,
"RKF21": pathsim.solvers.RKF21,
}

map_str_to_object = {
"constant": Constant,
"stepsource": StepSource,
"pulsesource": PulseSource,
"amplifier": Amplifier,
"amplifier_reverse": Amplifier,
"scope": Scope,
"splitter2": Splitter,
"splitter3": Splitter,
"adder": Adder,
"adder_reverse": Adder,
"multiplier": Multiplier,
"process": Process,
"process_horizontal": Process,
"rng": RNG,
"pid": PID,
"integrator": Integrator,
"function": Function,
"delay": Delay,
}


# app = Flask(__name__)
# CORS(app, supports_credentials=True)

Expand All @@ -52,6 +75,15 @@
os.makedirs(SAVE_DIR, exist_ok=True)


# Health check endpoint for CI/CD
@app.route("/", methods=["GET"])
@app.route("/health", methods=["GET"])
def health_check():
return jsonify(
{"status": "healthy", "message": "Fuel Cycle Simulator Backend is running"}
), 200


# Function to save graphs
@app.route("/save", methods=["POST"])
def save_graph():
Expand Down Expand Up @@ -384,29 +416,48 @@ def make_solver_params(solver_prms, eval_namespace=None):
return solver_prms, extra_params, duration


map_str_to_object = {
"constant": Constant,
"stepsource": StepSource,
"pulsesource": PulseSource,
"amplifier": Amplifier,
"amplifier_reverse": Amplifier,
"scope": Scope,
"splitter2": Splitter,
"splitter3": Splitter,
"adder": Adder,
"adder_reverse": Adder,
"multiplier": Multiplier,
"process": Process,
"process_horizontal": Process,
"rng": RNG,
"pid": PID,
"integrator": Integrator,
"function": Function,
"delay": Delay,
}
def auto_block_construction(node: dict, eval_namespace: dict = None) -> Block:
"""
Automatically constructs a block object from a node dictionary.

Args:
node: The node dictionary containing block information.
eval_namespace: A namespace for evaluating expressions. Defaults to None.

Raises:
ValueError: If the block type is unknown or if there are issues with evaluation.

def make_blocks(nodes, edges, eval_namespace=None):
Returns:
The constructed block object.
"""
if eval_namespace is None:
eval_namespace = globals()

block_type = node["type"]

if eval_namespace is None:
eval_namespace = globals()

block_type = node["type"]
if block_type not in map_str_to_object:
raise ValueError(f"Unknown block type: {block_type}")

block_class = map_str_to_object[block_type]

# skip 'self'
parameters_for_class = block_class.__init__.__code__.co_varnames[1:]

parameters = {
k: eval(v, eval_namespace)
for k, v in node["data"].items()
if k in parameters_for_class
}
return block_class(**parameters)


def make_blocks(
nodes: list[dict], edges: list[dict], eval_namespace: dict = None
) -> tuple[list[Block], list[Event]]:
blocks, events = [], []

for node in nodes:
Expand Down Expand Up @@ -445,17 +496,7 @@ def make_blocks(nodes, edges, eval_namespace=None):
],
)
else: # try automated construction
block_class = map_str_to_object[block_type]

# skip 'self'
parameters_for_class = block_class.__init__.__code__.co_varnames[1:]

parameters = {
k: eval(v, eval_namespace)
for k, v in node["data"].items()
if k in parameters_for_class
}
block = block_class(**parameters)
block = auto_block_construction(node, eval_namespace)

block.id = node["id"]
block.label = node["data"]["label"]
Expand Down
Empty file added test/__init__.py
Empty file.
Loading
Loading