diff --git a/scripts/smoke.bat b/scripts/smoke.bat new file mode 100644 index 00000000..6a53a276 --- /dev/null +++ b/scripts/smoke.bat @@ -0,0 +1,5 @@ +@echo off +REM smoke.bat - Windows wrapper for the minimal stdlib unittest smoke harness. +echo Running MUIOGO Smoke Tests... +python "%~dp0..\tests\smoke_test.py" +pause diff --git a/scripts/smoke.sh b/scripts/smoke.sh new file mode 100755 index 00000000..de28a0f4 --- /dev/null +++ b/scripts/smoke.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# smoke.sh - Wrapper for the minimal stdlib unittest smoke harness. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "Running MUIOGO Smoke Tests..." +python3 "$PROJECT_ROOT/tests/smoke_test.py" diff --git a/scripts/verify_base.sh b/scripts/verify_base.sh new file mode 100755 index 00000000..7cc41936 --- /dev/null +++ b/scripts/verify_base.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ============================================================================== +# MUIOGO VERIFICATION GATEWAY +# ============================================================================== +# This script is the primary safety gate for upstream pulls from OSeMOSYS/MUIO. +# It ensures that MUIOGO's portability, security, and stability are preserved. +# +# MAINTAINER NOTES (Always Review these files during a v5.x pull): +# 1. API/app.py - Core Flask config and blueprint registration. +# 2. API/Classes/Case/OsemosysClass.py - Model execution logic. +# 3. API/Routes/Case/CaseRoute.py - Backend API for case management. +# 4. WebAPP/Classes/Osemosys.Class.js - Frontend solver coordination. +# +# REJECTED PATTERNS: +# - Hardcoded paths (e.g. C:\) +# - Insecure session/path handling +# - Breaking Python 3.10-3.12 support +# ============================================================================== + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +BOLD="\033[1m" +GREEN="\033[92m" +YELLOW="\033[93m" +RED="\033[91m" +RESET="\033[0m" + +# Disable colours when not in a real terminal +if [ ! -t 1 ]; then + BOLD="" GREEN="" YELLOW="" RED="" RESET="" +fi + +_print_header() { + echo -e "\n${BOLD}=== $1 ===${RESET}" +} + +_print_status() { + if [ "$1" -eq 0 ]; then + echo -e " ${GREEN}[PASS]${RESET} $2" + else + echo -e " ${RED}[FAIL]${RESET} $2" + [ -n "${3:-}" ] && echo -e "\n${3}" + exit 1 + fi +} + +# 1. Environment & Solvers check +_print_header "Step 1: Environment & Solvers" +# Run setup check quietly, only show output on failure +if OUTPUT=$("$SCRIPT_DIR/setup.sh" --check --no-demo-data 2>&1); then + _print_status 0 "Python venv and solvers (GLPK/CBC) are ready." +else + _print_status 1 "Environment check failed. Run ./scripts/setup.sh first." "$OUTPUT" +fi + +# 2. Syntax Check (py_compile) +_print_header "Step 2: Python Syntax Check" +if python3 -m py_compile "$PROJECT_ROOT/API/app.py" 2>/dev/null; then + _print_status 0 "app.py is syntactically valid." +else + _print_status 1 "Syntax error detected in app.py." +fi + +# 3. Smoke Test Harness +_print_header "Step 3: API & Route Smoke Tests" +if OUTPUT=$(python3 "$PROJECT_ROOT/tests/smoke_test.py" 2>&1); then + _print_status 0 "Smoke tests passed (API boot and core routes)." +else + _print_status 1 "Smoke tests failed." "$OUTPUT" +fi + +# 4. Unresolved Merge state +_print_header "Step 4: Git Merge Integrity" +if [ -d "$PROJECT_ROOT/.git" ]; then + if [ -f "$PROJECT_ROOT/.git/MERGE_HEAD" ] || [ -f "$PROJECT_ROOT/.git/REBASE_HEAD" ] || [ -f "$PROJECT_ROOT/.git/CHERRY_PICK_HEAD" ]; then + _print_status 1 "Unresolved git merge/rebase detected (.git/MERGE_HEAD exists)." + else + _print_status 0 "No unresolved git merge/rebase state found." + fi +else + echo -e " ${YELLOW}[SKIP]${RESET} Not a git repository." +fi + +# 5. Conflict Markers scan +_print_header "Step 5: Conflict Marker Scan" +# Use ^ to match markers at the beginning of the line to avoid decorative comments +if grep -rnE "^<<<<<<<|^=======|^>>>>>>>" "$PROJECT_ROOT/API" "$PROJECT_ROOT/WebAPP/App" --exclude-dir=__pycache__ > /dev/null 2>&1; then + _print_status 1 "Conflict markers found in the codebase!" + grep -rnE "^<<<<<<<|^=======|^>>>>>>>" "$PROJECT_ROOT/API" "$PROJECT_ROOT/WebAPP/App" --exclude-dir=__pycache__ +else + _print_status 0 "No conflict markers found in key backend/frontend folders." +fi + +echo -e "\n${BOLD}${GREEN}Verification Complete: MUIOGO is healthy and ready for upstream pull.${RESET}\n" diff --git a/tests/smoke_test.py b/tests/smoke_test.py new file mode 100644 index 00000000..ef7c3314 --- /dev/null +++ b/tests/smoke_test.py @@ -0,0 +1,55 @@ +import unittest +import sys +import os +from pathlib import Path + +# Add project root to sys.path to allow importing API.app +PROJECT_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT_ROOT / "API")) + +class SmokeTest(unittest.TestCase): + def setUp(self): + """Initialize the Flask test client.""" + try: + from app import app + app.config['TESTING'] = True + app.config['DEBUG'] = False + self.app = app.test_client() + except ImportError as e: + self.fail(f"Failed to import API.app: {e}") + except Exception as e: + self.fail(f"Failed to initialize Flask app: {e}") + + def test_app_import(self): + """Basic check if the app module can be imported.""" + import app + self.assertIsNotNone(app.app) + + def test_index_route(self): + """Check if the home route returns 200.""" + response = self.app.get('/') + self.assertEqual(response.status_code, 200) + + def test_get_session_route(self): + """Check if /getSession returns 200 (even if session is empty).""" + response = self.app.get('/getSession') + self.assertEqual(response.status_code, 200) + self.assertIn('session', response.get_json()) + + def test_get_cases_route(self): + """Check if /getCases returns 200 or 404 (handled error).""" + response = self.app.get('/getCases') + # /getCases returns 200 with list or 404 if directory error + self.assertIn(response.status_code, [200, 404]) + + def test_config_validity(self): + """Verify that the Config class resolves paths correctly.""" + from Classes.Base import Config + self.assertTrue(Config.BASE_DIR.exists()) + self.assertTrue(Config.WEBAPP_PATH.exists()) + # Note: We don't check for writability here as per 'read-only' constraint, + # but we check if the paths are resolved to the correct absolute locations. + self.assertIn("MUIOGO", str(Config.BASE_DIR)) + +if __name__ == '__main__': + unittest.main()