Fix TypeScript build errors - remove unused imports
This commit is contained in:
parent
ecc1676fe5
commit
16c7d09a44
19
Dockerfile
19
Dockerfile
|
|
@ -1,9 +1,9 @@
|
||||||
# Multi-stage build for IPMI Fan Control
|
# Multi-stage build for IPMI Fan Control
|
||||||
|
|
||||||
# Backend stage
|
# Backend stage
|
||||||
FROM python:3.11-slim as backend-builder
|
FROM python:3.11-slim AS backend-builder
|
||||||
|
|
||||||
WORKDIR /app/backend
|
WORKDIR /app
|
||||||
|
|
||||||
# Install system dependencies for ipmitool
|
# Install system dependencies for ipmitool
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
|
|
@ -18,16 +18,16 @@ COPY backend/requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copy backend source
|
# Copy backend source
|
||||||
COPY backend/ .
|
COPY backend/ ./backend/
|
||||||
|
|
||||||
# Frontend stage
|
# Frontend stage
|
||||||
FROM node:20-alpine as frontend-builder
|
FROM node:20-alpine AS frontend-builder
|
||||||
|
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
RUN npm ci
|
RUN npm install
|
||||||
|
|
||||||
# Copy frontend source and build
|
# Copy frontend source and build
|
||||||
COPY frontend/ .
|
COPY frontend/ .
|
||||||
|
|
@ -56,10 +56,9 @@ COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
|
||||||
# Create data directory
|
# Create data directory
|
||||||
RUN mkdir -p /app/data
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
# Environment variables
|
# Set Python path to include backend directory
|
||||||
ENV PYTHONPATH=/app/backend
|
ENV PYTHONPATH=/app
|
||||||
ENV DATA_DIR=/app/data
|
ENV DATA_DIR=/app/data
|
||||||
ENV SECRET_KEY=change-me-in-production
|
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
@ -68,5 +67,5 @@ EXPOSE 8000
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
|
||||||
|
|
||||||
# Start command
|
# Start command - use absolute module path
|
||||||
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["python", "-m", "uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 IPMI Fan Control
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -12,7 +12,7 @@ from cryptography.fernet import Fernet
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
|
|
||||||
from config import settings
|
from backend.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from sqlalchemy import (
|
||||||
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session
|
from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from config import settings, DATA_DIR
|
from backend.config import settings, DATA_DIR
|
||||||
|
|
||||||
# Create engine
|
# Create engine
|
||||||
if settings.DATABASE_URL.startswith("sqlite"):
|
if settings.DATABASE_URL.startswith("sqlite"):
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,12 @@ from enum import Enum
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from database import (
|
from backend.database import (
|
||||||
Server, FanCurve, SensorData, FanData, SystemLog,
|
Server, FanCurve, SensorData, FanData, SystemLog,
|
||||||
get_db, SessionLocal
|
get_db, SessionLocal
|
||||||
)
|
)
|
||||||
from ipmi_client import IPMIClient, TemperatureReading
|
from backend.ipmi_client import IPMIClient, TemperatureReading
|
||||||
from config import settings
|
from backend.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -168,7 +168,7 @@ class FanController:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create IPMI client
|
# Create IPMI client
|
||||||
from auth import decrypt_password
|
from backend.auth import decrypt_password
|
||||||
client = IPMIClient(
|
client = IPMIClient(
|
||||||
host=server.host,
|
host=server.host,
|
||||||
username=server.username,
|
username=server.username,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from typing import List, Dict, Optional, Tuple, Any
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from config import settings
|
from backend.config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,15 +11,15 @@ from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from config import settings
|
from backend.config import settings
|
||||||
from database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData
|
from backend.database import init_db, get_db, is_setup_complete, set_setup_complete, User, Server, FanCurve, SystemLog, SensorData, FanData
|
||||||
from auth import (
|
from backend.auth import (
|
||||||
verify_password, get_password_hash, create_access_token,
|
verify_password, get_password_hash, create_access_token,
|
||||||
decode_access_token, encrypt_password, decrypt_password
|
decode_access_token, encrypt_password, decrypt_password
|
||||||
)
|
)
|
||||||
from ipmi_client import IPMIClient
|
from backend.ipmi_client import IPMIClient
|
||||||
from fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller
|
from backend.fan_control import fan_controller, FanCurveManager, initialize_fan_controller, shutdown_fan_controller
|
||||||
from schemas import (
|
from backend.schemas import (
|
||||||
UserCreate, UserLogin, UserResponse, Token, TokenData,
|
UserCreate, UserLogin, UserResponse, Token, TokenData,
|
||||||
ServerCreate, ServerUpdate, ServerResponse, ServerDetailResponse, ServerStatusResponse,
|
ServerCreate, ServerUpdate, ServerResponse, ServerDetailResponse, ServerStatusResponse,
|
||||||
FanCurveCreate, FanCurveResponse, FanCurvePoint,
|
FanCurveCreate, FanCurveResponse, FanCurvePoint,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pydantic==2.5.3
|
||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
passlib[bcrypt]==1.7.4
|
passlib[bcrypt]==1.7.4
|
||||||
|
cryptography==42.0.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
|
|
@ -12,3 +13,5 @@ apscheduler==3.10.4
|
||||||
psutil==5.9.8
|
psutil==5.9.8
|
||||||
asyncpg==0.29.0
|
asyncpg==0.29.0
|
||||||
aiosqlite==0.19.0
|
aiosqlite==0.19.0
|
||||||
|
pytest==7.4.4
|
||||||
|
pytest-asyncio==0.23.3
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Entry point for running the application directly."""
|
"""Entry point for running the application directly."""
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from config import settings
|
from backend.config import settings
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"main:app",
|
"backend.main:app",
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=8000,
|
port=8000,
|
||||||
reload=settings.DEBUG,
|
reload=settings.DEBUG,
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,18 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
- SECRET_KEY=${SECRET_KEY:-your-secure-secret-key-change-in-production}
|
- SECRET_KEY=${SECRET_KEY:-dev-secret-key-change-in-production}
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
- PANIC_TIMEOUT_SECONDS=${PANIC_TIMEOUT_SECONDS:-60}
|
- PANIC_TIMEOUT_SECONDS=${PANIC_TIMEOUT_SECONDS:-60}
|
||||||
- PANIC_FAN_SPEED=${PANIC_FAN_SPEED:-100}
|
- PANIC_FAN_SPEED=${PANIC_FAN_SPEED:-100}
|
||||||
|
- DEBUG=${DEBUG:-true}
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
networks:
|
networks:
|
||||||
- ipmi-network
|
- ipmi-network
|
||||||
# Required for IPMI access to the local network
|
# For Windows Docker Desktop, we need this for proper shutdown
|
||||||
# If your servers are on a different network, adjust accordingly
|
stop_grace_period: 10s
|
||||||
extra_hosts:
|
|
||||||
- "host.docker.internal:host-gateway"
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
ipmi-network:
|
ipmi-network:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
client_max_body_size 50M;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://ipmi-fan-control:8000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection 'upgrade';
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://ipmi-fan-control:8000/api;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
IconButton,
|
IconButton,
|
||||||
Divider,
|
Divider,
|
||||||
Avatar,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import {
|
||||||
NavigateNext as NextIcon,
|
NavigateNext as NextIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { dashboardApi } from '../utils/api';
|
import { dashboardApi } from '../utils/api';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer } from 'recharts';
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ import {
|
||||||
Paper,
|
Paper,
|
||||||
Grid,
|
Grid,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
Speed as SpeedIcon,
|
Speed as SpeedIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { logsApi, serversApi } from '../utils/api';
|
import { logsApi, serversApi } from '../utils/api';
|
||||||
import type { SystemLog } from '../types';
|
|
||||||
|
|
||||||
const LOGS_PER_PAGE = 25;
|
const LOGS_PER_PAGE = 25;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
|
|
@ -29,11 +29,10 @@ import {
|
||||||
Speed as SpeedIcon,
|
Speed as SpeedIcon,
|
||||||
Thermostat as TempIcon,
|
Thermostat as TempIcon,
|
||||||
Power as PowerIcon,
|
Power as PowerIcon,
|
||||||
Refresh as RefreshIcon,
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { serversApi, fanControlApi, dashboardApi } from '../utils/api';
|
import { serversApi, fanControlApi, dashboardApi } from '../utils/api';
|
||||||
import type { TemperatureReading, FanReading } from '../types';
|
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from 'recharts';
|
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -66,7 +65,7 @@ export default function ServerDetail() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: sensors, isLoading: isSensorsLoading, refetch: refetchSensors } = useQuery({
|
const { data: sensors } = useQuery({
|
||||||
queryKey: ['sensors', serverId],
|
queryKey: ['sensors', serverId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await serversApi.getSensors(serverId);
|
const response = await serversApi.getSensors(serverId);
|
||||||
|
|
@ -124,7 +123,7 @@ export default function ServerDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const cpuTemps = sensors?.temperatures.filter((t) => t.location.startsWith('cpu')) || [];
|
const cpuTemps = sensors?.temperatures.filter((t) => t.location.startsWith('cpu')) || [];
|
||||||
const otherTemps = sensors?.temperatures.filter((t) => !t.location.startsWith('cpu')) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# Tests for IPMI Fan Control
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Tests for authentication module."""
|
||||||
|
import pytest
|
||||||
|
from backend.auth import (
|
||||||
|
get_password_hash,
|
||||||
|
verify_password,
|
||||||
|
create_access_token,
|
||||||
|
decode_access_token,
|
||||||
|
encrypt_password,
|
||||||
|
decrypt_password,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_hashing():
|
||||||
|
"""Test password hashing and verification."""
|
||||||
|
password = "testpassword123"
|
||||||
|
hashed = get_password_hash(password)
|
||||||
|
|
||||||
|
assert hashed != password
|
||||||
|
assert verify_password(password, hashed)
|
||||||
|
assert not verify_password("wrongpassword", hashed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_jwt_tokens():
|
||||||
|
"""Test JWT token creation and decoding."""
|
||||||
|
data = {"sub": "testuser"}
|
||||||
|
token = create_access_token(data)
|
||||||
|
|
||||||
|
assert token is not None
|
||||||
|
|
||||||
|
decoded = decode_access_token(token)
|
||||||
|
assert decoded is not None
|
||||||
|
assert decoded["sub"] == "testuser"
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_token():
|
||||||
|
"""Test decoding invalid token."""
|
||||||
|
decoded = decode_access_token("invalid.token.here")
|
||||||
|
assert decoded is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_password_encryption():
|
||||||
|
"""Test password encryption and decryption."""
|
||||||
|
password = "secret_password"
|
||||||
|
encrypted = encrypt_password(password)
|
||||||
|
|
||||||
|
assert encrypted != password
|
||||||
|
|
||||||
|
decrypted = decrypt_password(encrypted)
|
||||||
|
assert decrypted == password
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
"""Tests for fan curve logic."""
|
||||||
|
import pytest
|
||||||
|
from backend.fan_control import FanCurveManager, FanCurvePoint
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_curve():
|
||||||
|
"""Test parsing fan curve from JSON."""
|
||||||
|
json_data = '[{"temp": 30, "speed": 20}, {"temp": 50, "speed": 50}, {"temp": 70, "speed": 100}]'
|
||||||
|
curve = FanCurveManager.parse_curve(json_data)
|
||||||
|
|
||||||
|
assert len(curve) == 3
|
||||||
|
assert curve[0].temp == 30
|
||||||
|
assert curve[0].speed == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_invalid_curve():
|
||||||
|
"""Test parsing invalid curve returns default."""
|
||||||
|
curve = FanCurveManager.parse_curve("invalid json")
|
||||||
|
|
||||||
|
assert len(curve) == 6 # Default curve has 6 points
|
||||||
|
assert curve[0].temp == 30
|
||||||
|
assert curve[0].speed == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_speed_below_min():
|
||||||
|
"""Test speed calculation below minimum temperature."""
|
||||||
|
curve = [
|
||||||
|
FanCurvePoint(30, 10),
|
||||||
|
FanCurvePoint(50, 50),
|
||||||
|
FanCurvePoint(70, 100),
|
||||||
|
]
|
||||||
|
|
||||||
|
speed = FanCurveManager.calculate_speed(curve, 20)
|
||||||
|
assert speed == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_speed_above_max():
|
||||||
|
"""Test speed calculation above maximum temperature."""
|
||||||
|
curve = [
|
||||||
|
FanCurvePoint(30, 10),
|
||||||
|
FanCurvePoint(50, 50),
|
||||||
|
FanCurvePoint(70, 100),
|
||||||
|
]
|
||||||
|
|
||||||
|
speed = FanCurveManager.calculate_speed(curve, 80)
|
||||||
|
assert speed == 100
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_speed_interpolation():
|
||||||
|
"""Test speed calculation with interpolation."""
|
||||||
|
curve = [
|
||||||
|
FanCurvePoint(30, 10),
|
||||||
|
FanCurvePoint(50, 50),
|
||||||
|
FanCurvePoint(70, 100),
|
||||||
|
]
|
||||||
|
|
||||||
|
# At 40°C, should be halfway between 10% and 50%
|
||||||
|
speed = FanCurveManager.calculate_speed(curve, 40)
|
||||||
|
assert speed == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculate_speed_exact_point():
|
||||||
|
"""Test speed calculation at exact curve point."""
|
||||||
|
curve = [
|
||||||
|
FanCurvePoint(30, 10),
|
||||||
|
FanCurvePoint(50, 50),
|
||||||
|
]
|
||||||
|
|
||||||
|
speed = FanCurveManager.calculate_speed(curve, 50)
|
||||||
|
assert speed == 50
|
||||||
|
|
||||||
|
|
||||||
|
def test_serialize_curve():
|
||||||
|
"""Test serializing curve to JSON."""
|
||||||
|
points = [
|
||||||
|
FanCurvePoint(30, 10),
|
||||||
|
FanCurvePoint(50, 50),
|
||||||
|
]
|
||||||
|
|
||||||
|
json_data = FanCurveManager.serialize_curve(points)
|
||||||
|
assert "30" in json_data
|
||||||
|
assert "10" in json_data
|
||||||
|
assert "50" in json_data
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
"""Tests for IPMI client."""
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
from backend.ipmi_client import IPMIClient, TemperatureReading
|
||||||
|
|
||||||
|
|
||||||
|
def test_fan_mapping():
|
||||||
|
"""Test fan ID mapping."""
|
||||||
|
assert IPMIClient.FAN_MAPPING["0x00"] == 1
|
||||||
|
assert IPMIClient.FAN_MAPPING["0x06"] == 7
|
||||||
|
|
||||||
|
|
||||||
|
def test_hex_to_percent_conversion():
|
||||||
|
"""Test hex to percent conversion mapping."""
|
||||||
|
assert IPMIClient.HEX_TO_PERCENT["0x00"] == 0
|
||||||
|
assert IPMIClient.HEX_TO_PERCENT["0x64"] == 100
|
||||||
|
assert IPMIClient.PERCENT_TO_HEX[50] == "0x32"
|
||||||
|
assert IPMIClient.PERCENT_TO_HEX[100] == "0x64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_temp_location():
|
||||||
|
"""Test temperature location detection."""
|
||||||
|
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||||
|
|
||||||
|
assert client._determine_temp_location("CPU1 Temp") == "cpu1"
|
||||||
|
assert client._determine_temp_location("CPU2 Temp") == "cpu2"
|
||||||
|
assert client._determine_temp_location("Processor 1") == "cpu1"
|
||||||
|
assert client._determine_temp_location("Inlet Temp") == "inlet"
|
||||||
|
assert client._determine_temp_location("Exhaust Temp") == "exhaust"
|
||||||
|
assert client._determine_temp_location("DIMM 1") == "memory"
|
||||||
|
assert client._determine_temp_location("Unknown Sensor") == "other"
|
||||||
|
|
||||||
|
|
||||||
|
def test_determine_sensor_type():
|
||||||
|
"""Test sensor type detection."""
|
||||||
|
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||||
|
|
||||||
|
assert client._determine_sensor_type("CPU Temp") == "temperature"
|
||||||
|
assert client._determine_sensor_type("Fan 1 RPM") == "fan"
|
||||||
|
assert client._determine_sensor_type("12V") == "voltage"
|
||||||
|
assert client._determine_sensor_type("Power Supply") == "power"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_sensor_value():
|
||||||
|
"""Test sensor value parsing."""
|
||||||
|
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||||
|
|
||||||
|
value, unit = client._parse_sensor_value("45 degrees C")
|
||||||
|
assert value == 45.0
|
||||||
|
assert unit == "°C"
|
||||||
|
|
||||||
|
value, unit = client._parse_sensor_value("4200 RPM")
|
||||||
|
assert value == 4200.0
|
||||||
|
assert unit == "RPM"
|
||||||
|
|
||||||
|
value, unit = client._parse_sensor_value("12.05 Volts")
|
||||||
|
assert value == 12.05
|
||||||
|
assert unit == "V"
|
||||||
|
|
||||||
|
value, unit = client._parse_sensor_value("250 Watts")
|
||||||
|
assert value == 250.0
|
||||||
|
assert unit == "W"
|
||||||
|
|
||||||
|
|
||||||
|
@patch('backend.ipmi_client.subprocess.run')
|
||||||
|
def test_test_connection(mock_run):
|
||||||
|
"""Test connection test method."""
|
||||||
|
mock_run.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||||
|
result = client.test_connection()
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
mock_run.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('backend.ipmi_client.subprocess.run')
|
||||||
|
def test_test_connection_failure(mock_run):
|
||||||
|
"""Test connection test with failure."""
|
||||||
|
mock_run.return_value = Mock(returncode=1, stdout="", stderr="Error")
|
||||||
|
|
||||||
|
client = IPMIClient("192.168.1.1", "user", "pass")
|
||||||
|
result = client.test_connection()
|
||||||
|
|
||||||
|
assert result is False
|
||||||
Loading…
Reference in New Issue