feat: SWG 가스켓 전체 구성 정보 표시 개선
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
Some checks failed
SonarQube Analysis / SonarQube Scan (push) Has been cancelled
- H/F/I/O SS304/GRAPHITE/CS/CS 패턴에서 4개 구성요소 모두 표시 - 기존 SS304 + GRAPHITE → SS304/GRAPHITE/CS/CS로 완전한 구성 표시 - 외부링/필러/내부링/추가구성 모든 정보 포함 - 구매수량 계산 모달에서 정확한 재질 정보 확인 가능
This commit is contained in:
4
backend/tests/__init__.py
Normal file
4
backend/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
테스트 모듈
|
||||
자동화된 테스트 케이스들
|
||||
"""
|
||||
160
backend/tests/conftest.py
Normal file
160
backend/tests/conftest.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
pytest 설정 및 공통 픽스처
|
||||
"""
|
||||
import pytest
|
||||
import asyncio
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from fastapi.testclient import TestClient
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
from app.main import app
|
||||
from app.database import get_db
|
||||
from app.models import Base
|
||||
|
||||
|
||||
# 테스트용 데이터베이스 설정
|
||||
TEST_DATABASE_URL = "sqlite:///./test.db"
|
||||
|
||||
engine = create_engine(
|
||||
TEST_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False}
|
||||
)
|
||||
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
|
||||
def override_get_db():
|
||||
"""테스트용 데이터베이스 세션"""
|
||||
try:
|
||||
db = TestingSessionLocal()
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""이벤트 루프 픽스처"""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_session():
|
||||
"""테스트용 데이터베이스 세션 픽스처"""
|
||||
# 테이블 생성
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# 세션 생성
|
||||
session = TestingSessionLocal()
|
||||
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
# 테이블 삭제 (테스트 격리)
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(db_session):
|
||||
"""테스트 클라이언트 픽스처"""
|
||||
# 데이터베이스 의존성 오버라이드
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
|
||||
with TestClient(app) as test_client:
|
||||
yield test_client
|
||||
|
||||
# 의존성 오버라이드 정리
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_excel_file():
|
||||
"""샘플 엑셀 파일 픽스처"""
|
||||
import pandas as pd
|
||||
|
||||
# 샘플 데이터 생성
|
||||
data = {
|
||||
'Description': [
|
||||
'PIPE, SEAMLESS, A333-6, 6", SCH40',
|
||||
'ELBOW, 90DEG, A234-WPB, 4", SCH40',
|
||||
'VALVE, GATE, A216-WCB, 2", 150LB',
|
||||
'FLANGE, WELD NECK, A105, 3", 150LB',
|
||||
'BOLT, HEX HEAD, A193-B7, M16X50'
|
||||
],
|
||||
'Quantity': [10, 8, 2, 4, 20],
|
||||
'Unit': ['EA', 'EA', 'EA', 'EA', 'EA']
|
||||
}
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# 임시 파일 생성
|
||||
with tempfile.NamedTemporaryFile(suffix='.xlsx', delete=False) as tmp_file:
|
||||
df.to_excel(tmp_file.name, index=False)
|
||||
yield tmp_file.name
|
||||
|
||||
# 파일 정리
|
||||
os.unlink(tmp_file.name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_csv_file():
|
||||
"""샘플 CSV 파일 픽스처"""
|
||||
import pandas as pd
|
||||
|
||||
# 샘플 데이터 생성
|
||||
data = {
|
||||
'Description': [
|
||||
'PIPE, SEAMLESS, A333-6, 8", SCH40',
|
||||
'TEE, EQUAL, A234-WPB, 6", SCH40',
|
||||
'VALVE, BALL, A216-WCB, 4", 150LB'
|
||||
],
|
||||
'Quantity': [5, 3, 1],
|
||||
'Unit': ['EA', 'EA', 'EA']
|
||||
}
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
|
||||
# 임시 파일 생성
|
||||
with tempfile.NamedTemporaryFile(suffix='.csv', delete=False, mode='w') as tmp_file:
|
||||
df.to_csv(tmp_file.name, index=False)
|
||||
yield tmp_file.name
|
||||
|
||||
# 파일 정리
|
||||
os.unlink(tmp_file.name)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_job_data():
|
||||
"""샘플 작업 데이터 픽스처"""
|
||||
return {
|
||||
"job_no": "TEST-2025-001",
|
||||
"job_name": "테스트 프로젝트",
|
||||
"client_name": "테스트 고객사",
|
||||
"end_user": "테스트 사용자",
|
||||
"epc_company": "테스트 EPC",
|
||||
"status": "active"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_material_data():
|
||||
"""샘플 자재 데이터 픽스처"""
|
||||
return {
|
||||
"original_description": "PIPE, SEAMLESS, A333-6, 6\", SCH40",
|
||||
"classified_category": "PIPE",
|
||||
"classified_subcategory": "SEAMLESS",
|
||||
"material_grade": "A333-6",
|
||||
"schedule": "SCH40",
|
||||
"size_spec": "6\"",
|
||||
"quantity": 10.0,
|
||||
"unit": "EA",
|
||||
"classification_confidence": 0.95
|
||||
}
|
||||
|
||||
|
||||
# 테스트 설정
|
||||
pytest_plugins = []
|
||||
423
backend/tests/test_classifiers.py
Normal file
423
backend/tests/test_classifiers.py
Normal file
@@ -0,0 +1,423 @@
|
||||
"""
|
||||
자재 분류기 테스트
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestPipeClassifier:
|
||||
"""파이프 분류기 테스트"""
|
||||
|
||||
def test_pipe_classification_basic(self):
|
||||
"""기본 파이프 분류 테스트"""
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
|
||||
# 명확한 파이프 케이스
|
||||
result = classify_pipe("", "PIPE, SEAMLESS, A333-6, 6\", SCH40", "6\"", 1000)
|
||||
|
||||
assert result["category"] == "PIPE"
|
||||
assert result["confidence"] > 0.8
|
||||
assert result["material_grade"] == "A333-6"
|
||||
assert result["schedule"] == "SCH40"
|
||||
assert result["size"] == "6\""
|
||||
|
||||
def test_pipe_classification_welded(self):
|
||||
"""용접 파이프 분류 테스트"""
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
|
||||
result = classify_pipe("", "PIPE, WELDED, A53-B, 4\", SCH40", "4\"", 500)
|
||||
|
||||
assert result["category"] == "PIPE"
|
||||
assert result["subcategory"] == "WELDED"
|
||||
assert result["material_grade"] == "A53-B"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_pipe_classification_low_confidence(self):
|
||||
"""낮은 신뢰도 파이프 분류 테스트"""
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
|
||||
# 모호한 설명
|
||||
result = classify_pipe("", "STEEL TUBE, 2 INCH", "2\"", 100)
|
||||
|
||||
# 파이프로 분류되지만 신뢰도가 낮아야 함
|
||||
assert result["confidence"] < 0.7
|
||||
|
||||
def test_pipe_length_calculation(self):
|
||||
"""파이프 길이 계산 테스트"""
|
||||
from app.services.pipe_classifier import classify_pipe
|
||||
|
||||
result = classify_pipe("", "PIPE, SEAMLESS, A333-6, 6\", SCH40", "6\"", 6000)
|
||||
|
||||
assert "length_mm" in result
|
||||
assert result["length_mm"] == 6000
|
||||
assert "purchase_length" in result
|
||||
|
||||
|
||||
class TestFittingClassifier:
|
||||
"""피팅 분류기 테스트"""
|
||||
|
||||
def test_elbow_classification(self):
|
||||
"""엘보우 분류 테스트"""
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
|
||||
result = classify_fitting("", "ELBOW, 90DEG, A234-WPB, 4\", SCH40")
|
||||
|
||||
assert result["category"] == "FITTING"
|
||||
assert result["subcategory"] == "ELBOW"
|
||||
assert result["angle"] == "90DEG"
|
||||
assert result["material_grade"] == "A234-WPB"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_tee_classification(self):
|
||||
"""티 분류 테스트"""
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
|
||||
result = classify_fitting("", "TEE, EQUAL, A234-WPB, 6\", SCH40")
|
||||
|
||||
assert result["category"] == "FITTING"
|
||||
assert result["subcategory"] == "TEE"
|
||||
assert result["fitting_type"] == "EQUAL"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_reducer_classification(self):
|
||||
"""리듀서 분류 테스트"""
|
||||
from app.services.fitting_classifier import classify_fitting
|
||||
|
||||
result = classify_fitting("", "REDUCER, CONCENTRIC, A234-WPB, 8\"X6\", SCH40")
|
||||
|
||||
assert result["category"] == "FITTING"
|
||||
assert result["subcategory"] == "REDUCER"
|
||||
assert result["fitting_type"] == "CONCENTRIC"
|
||||
assert "8\"X6\"" in result["size"]
|
||||
|
||||
|
||||
class TestValveClassifier:
|
||||
"""밸브 분류기 테스트"""
|
||||
|
||||
def test_gate_valve_classification(self):
|
||||
"""게이트 밸브 분류 테스트"""
|
||||
from app.services.valve_classifier import classify_valve
|
||||
|
||||
result = classify_valve("", "VALVE, GATE, A216-WCB, 2\", 150LB")
|
||||
|
||||
assert result["category"] == "VALVE"
|
||||
assert result["subcategory"] == "GATE"
|
||||
assert result["material_grade"] == "A216-WCB"
|
||||
assert result["pressure_rating"] == "150LB"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_ball_valve_classification(self):
|
||||
"""볼 밸브 분류 테스트"""
|
||||
from app.services.valve_classifier import classify_valve
|
||||
|
||||
result = classify_valve("", "VALVE, BALL, A216-WCB, 4\", 300LB")
|
||||
|
||||
assert result["category"] == "VALVE"
|
||||
assert result["subcategory"] == "BALL"
|
||||
assert result["pressure_rating"] == "300LB"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_check_valve_classification(self):
|
||||
"""체크 밸브 분류 테스트"""
|
||||
from app.services.valve_classifier import classify_valve
|
||||
|
||||
result = classify_valve("", "VALVE, CHECK, SWING, A216-WCB, 3\", 150LB")
|
||||
|
||||
assert result["category"] == "VALVE"
|
||||
assert result["subcategory"] == "CHECK"
|
||||
assert result["valve_type"] == "SWING"
|
||||
|
||||
|
||||
class TestFlangeClassifier:
|
||||
"""플랜지 분류기 테스트"""
|
||||
|
||||
def test_weld_neck_flange_classification(self):
|
||||
"""용접목 플랜지 분류 테스트"""
|
||||
from app.services.flange_classifier import classify_flange
|
||||
|
||||
result = classify_flange("", "FLANGE, WELD NECK, A105, 3\", 150LB")
|
||||
|
||||
assert result["category"] == "FLANGE"
|
||||
assert result["subcategory"] == "WELD NECK"
|
||||
assert result["material_grade"] == "A105"
|
||||
assert result["pressure_rating"] == "150LB"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_slip_on_flange_classification(self):
|
||||
"""슬립온 플랜지 분류 테스트"""
|
||||
from app.services.flange_classifier import classify_flange
|
||||
|
||||
result = classify_flange("", "FLANGE, SLIP ON, A105, 4\", 300LB")
|
||||
|
||||
assert result["category"] == "FLANGE"
|
||||
assert result["subcategory"] == "SLIP ON"
|
||||
assert result["pressure_rating"] == "300LB"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_blind_flange_classification(self):
|
||||
"""블라인드 플랜지 분류 테스트"""
|
||||
from app.services.flange_classifier import classify_flange
|
||||
|
||||
result = classify_flange("", "FLANGE, BLIND, A105, 6\", 150LB")
|
||||
|
||||
assert result["category"] == "FLANGE"
|
||||
assert result["subcategory"] == "BLIND"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
|
||||
class TestBoltClassifier:
|
||||
"""볼트 분류기 테스트"""
|
||||
|
||||
def test_hex_bolt_classification(self):
|
||||
"""육각 볼트 분류 테스트"""
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
|
||||
result = classify_bolt("", "BOLT, HEX HEAD, A193-B7, M16X50")
|
||||
|
||||
assert result["category"] == "BOLT"
|
||||
assert result["subcategory"] == "HEX HEAD"
|
||||
assert result["material_grade"] == "A193-B7"
|
||||
assert result["size"] == "M16X50"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_stud_bolt_classification(self):
|
||||
"""스터드 볼트 분류 테스트"""
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
|
||||
result = classify_bolt("", "STUD BOLT, A193-B7, M20X80")
|
||||
|
||||
assert result["category"] == "BOLT"
|
||||
assert result["subcategory"] == "STUD"
|
||||
assert result["material_grade"] == "A193-B7"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_nut_classification(self):
|
||||
"""너트 분류 테스트"""
|
||||
from app.services.bolt_classifier import classify_bolt
|
||||
|
||||
result = classify_bolt("", "NUT, HEX, A194-2H, M16")
|
||||
|
||||
assert result["category"] == "BOLT"
|
||||
assert result["subcategory"] == "NUT"
|
||||
assert result["material_grade"] == "A194-2H"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
|
||||
class TestGasketClassifier:
|
||||
"""가스켓 분류기 테스트"""
|
||||
|
||||
def test_spiral_wound_gasket_classification(self):
|
||||
"""스파이럴 와운드 가스켓 분류 테스트"""
|
||||
from app.services.gasket_classifier import classify_gasket
|
||||
|
||||
result = classify_gasket("", "GASKET, SPIRAL WOUND, SS316+GRAPHITE, 4\", 150LB")
|
||||
|
||||
assert result["category"] == "GASKET"
|
||||
assert result["subcategory"] == "SPIRAL WOUND"
|
||||
assert "SS316" in result["material_grade"]
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_rtj_gasket_classification(self):
|
||||
"""RTJ 가스켓 분류 테스트"""
|
||||
from app.services.gasket_classifier import classify_gasket
|
||||
|
||||
result = classify_gasket("", "GASKET, RTJ, SS316, 6\", 300LB")
|
||||
|
||||
assert result["category"] == "GASKET"
|
||||
assert result["subcategory"] == "RTJ"
|
||||
assert result["material_grade"] == "SS316"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_flat_gasket_classification(self):
|
||||
"""플랫 가스켓 분류 테스트"""
|
||||
from app.services.gasket_classifier import classify_gasket
|
||||
|
||||
result = classify_gasket("", "GASKET, FLAT, RUBBER, 2\", 150LB")
|
||||
|
||||
assert result["category"] == "GASKET"
|
||||
assert result["subcategory"] == "FLAT"
|
||||
assert result["material_grade"] == "RUBBER"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
|
||||
class TestInstrumentClassifier:
|
||||
"""계기 분류기 테스트"""
|
||||
|
||||
def test_pressure_gauge_classification(self):
|
||||
"""압력계 분류 테스트"""
|
||||
from app.services.instrument_classifier import classify_instrument
|
||||
|
||||
result = classify_instrument("", "PRESSURE GAUGE, 0-10 BAR, 1/2\" NPT")
|
||||
|
||||
assert result["category"] == "INSTRUMENT"
|
||||
assert result["subcategory"] == "PRESSURE GAUGE"
|
||||
assert "0-10 BAR" in result["range"]
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_temperature_gauge_classification(self):
|
||||
"""온도계 분류 테스트"""
|
||||
from app.services.instrument_classifier import classify_instrument
|
||||
|
||||
result = classify_instrument("", "TEMPERATURE GAUGE, 0-200°C, 1/2\" NPT")
|
||||
|
||||
assert result["category"] == "INSTRUMENT"
|
||||
assert result["subcategory"] == "TEMPERATURE GAUGE"
|
||||
assert "0-200°C" in result["range"]
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_flow_meter_classification(self):
|
||||
"""유량계 분류 테스트"""
|
||||
from app.services.instrument_classifier import classify_instrument
|
||||
|
||||
result = classify_instrument("", "FLOW METER, ORIFICE, 4\", 150LB")
|
||||
|
||||
assert result["category"] == "INSTRUMENT"
|
||||
assert result["subcategory"] == "FLOW METER"
|
||||
assert result["instrument_type"] == "ORIFICE"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
|
||||
class TestIntegratedClassifier:
|
||||
"""통합 분류기 테스트"""
|
||||
|
||||
def test_integrated_classification_pipe(self):
|
||||
"""통합 분류기 파이프 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
result = classify_material_integrated("PIPE, SEAMLESS, A333-6, 6\", SCH40")
|
||||
|
||||
assert result["category"] == "PIPE"
|
||||
assert result["confidence"] > 0.8
|
||||
assert "classification_details" in result
|
||||
|
||||
def test_integrated_classification_valve(self):
|
||||
"""통합 분류기 밸브 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
result = classify_material_integrated("VALVE, GATE, A216-WCB, 2\", 150LB")
|
||||
|
||||
assert result["category"] == "VALVE"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
def test_exclusion_logic(self):
|
||||
"""제외 로직 테스트"""
|
||||
from app.services.integrated_classifier import should_exclude_material
|
||||
|
||||
# 제외되어야 하는 항목들
|
||||
assert should_exclude_material("INSULATION, MINERAL WOOL") is True
|
||||
assert should_exclude_material("PAINT, PRIMER") is True
|
||||
assert should_exclude_material("SUPPORT, STRUCTURAL") is True
|
||||
|
||||
# 제외되지 않아야 하는 항목들
|
||||
assert should_exclude_material("PIPE, SEAMLESS, A333-6") is False
|
||||
assert should_exclude_material("VALVE, GATE, A216-WCB") is False
|
||||
|
||||
def test_confidence_threshold(self):
|
||||
"""신뢰도 임계값 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
# 모호한 설명으로 낮은 신뢰도 테스트
|
||||
result = classify_material_integrated("STEEL ITEM, UNKNOWN TYPE")
|
||||
|
||||
# 신뢰도가 낮아야 함
|
||||
assert result["confidence"] < 0.5
|
||||
assert result["category"] in ["UNKNOWN", "EXCLUDE"]
|
||||
|
||||
|
||||
class TestClassificationCaching:
|
||||
"""분류 결과 캐싱 테스트"""
|
||||
|
||||
@patch('app.services.integrated_classifier.tkmp_cache')
|
||||
def test_classification_cache_hit(self, mock_cache):
|
||||
"""분류 결과 캐시 히트 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
# 캐시에서 결과 반환 설정
|
||||
cached_result = {
|
||||
"category": "PIPE",
|
||||
"confidence": 0.95,
|
||||
"cached": True
|
||||
}
|
||||
mock_cache.get_classification_result.return_value = cached_result
|
||||
|
||||
result = classify_material_integrated("PIPE, SEAMLESS, A333-6, 6\", SCH40")
|
||||
|
||||
assert result == cached_result
|
||||
mock_cache.get_classification_result.assert_called_once()
|
||||
|
||||
@patch('app.services.integrated_classifier.tkmp_cache')
|
||||
def test_classification_cache_miss(self, mock_cache):
|
||||
"""분류 결과 캐시 미스 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
# 캐시에서 None 반환 (캐시 미스)
|
||||
mock_cache.get_classification_result.return_value = None
|
||||
|
||||
result = classify_material_integrated("PIPE, SEAMLESS, A333-6, 6\", SCH40")
|
||||
|
||||
assert result["category"] == "PIPE"
|
||||
assert result["confidence"] > 0.8
|
||||
|
||||
# 캐시 저장 호출 확인
|
||||
mock_cache.set_classification_result.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.performance
|
||||
class TestClassificationPerformance:
|
||||
"""분류 성능 테스트"""
|
||||
|
||||
def test_classification_speed(self):
|
||||
"""분류 속도 테스트"""
|
||||
import time
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
descriptions = [
|
||||
"PIPE, SEAMLESS, A333-6, 6\", SCH40",
|
||||
"VALVE, GATE, A216-WCB, 2\", 150LB",
|
||||
"FLANGE, WELD NECK, A105, 3\", 150LB",
|
||||
"ELBOW, 90DEG, A234-WPB, 4\", SCH40",
|
||||
"BOLT, HEX HEAD, A193-B7, M16X50"
|
||||
]
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for desc in descriptions:
|
||||
result = classify_material_integrated(desc)
|
||||
assert result["category"] != "UNKNOWN"
|
||||
|
||||
end_time = time.time()
|
||||
total_time = end_time - start_time
|
||||
|
||||
# 5개 항목을 1초 이내에 분류해야 함
|
||||
assert total_time < 1.0
|
||||
|
||||
# 평균 분류 시간이 100ms 이하여야 함
|
||||
avg_time = total_time / len(descriptions)
|
||||
assert avg_time < 0.1
|
||||
|
||||
def test_batch_classification(self):
|
||||
"""배치 분류 테스트"""
|
||||
from app.services.integrated_classifier import classify_material_integrated
|
||||
|
||||
descriptions = [
|
||||
"PIPE, SEAMLESS, A333-6, 6\", SCH40",
|
||||
"VALVE, GATE, A216-WCB, 2\", 150LB",
|
||||
"FLANGE, WELD NECK, A105, 3\", 150LB"
|
||||
] * 10 # 30개 항목
|
||||
|
||||
results = []
|
||||
for desc in descriptions:
|
||||
result = classify_material_integrated(desc)
|
||||
results.append(result)
|
||||
|
||||
# 모든 결과가 올바르게 분류되었는지 확인
|
||||
assert len(results) == 30
|
||||
|
||||
# 각 타입별로 올바르게 분류되었는지 확인
|
||||
pipe_results = [r for r in results if r["category"] == "PIPE"]
|
||||
valve_results = [r for r in results if r["category"] == "VALVE"]
|
||||
flange_results = [r for r in results if r["category"] == "FLANGE"]
|
||||
|
||||
assert len(pipe_results) == 10
|
||||
assert len(valve_results) == 10
|
||||
assert len(flange_results) == 10
|
||||
267
backend/tests/test_file_management.py
Normal file
267
backend/tests/test_file_management.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
파일 관리 API 테스트
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
|
||||
class TestFileManagementAPI:
|
||||
"""파일 관리 API 테스트 클래스"""
|
||||
|
||||
def test_get_files_empty_list(self, client: TestClient):
|
||||
"""빈 파일 목록 조회 테스트"""
|
||||
response = client.get("/files")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["total_count"] == 0
|
||||
assert data["data"] == []
|
||||
assert data["cache_hit"] is False
|
||||
|
||||
def test_get_files_with_job_no(self, client: TestClient):
|
||||
"""특정 작업 번호로 파일 조회 테스트"""
|
||||
response = client.get("/files?job_no=TEST-001")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "data" in data
|
||||
assert "total_count" in data
|
||||
|
||||
def test_get_files_with_history(self, client: TestClient):
|
||||
"""이력 포함 파일 조회 테스트"""
|
||||
response = client.get("/files?show_history=true")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
@patch('app.api.file_management.tkmp_cache')
|
||||
def test_get_files_cache_hit(self, mock_cache, client: TestClient):
|
||||
"""캐시 히트 테스트"""
|
||||
# 캐시에서 데이터 반환 설정
|
||||
mock_cache.get_file_list.return_value = [
|
||||
{
|
||||
"id": 1,
|
||||
"filename": "test.xlsx",
|
||||
"original_filename": "test.xlsx",
|
||||
"job_no": "TEST-001",
|
||||
"status": "active"
|
||||
}
|
||||
]
|
||||
|
||||
response = client.get("/files?job_no=TEST-001")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["cache_hit"] is True
|
||||
assert len(data["data"]) == 1
|
||||
|
||||
# 캐시 호출 확인
|
||||
mock_cache.get_file_list.assert_called_once_with("TEST-001", False)
|
||||
|
||||
def test_get_files_no_cache(self, client: TestClient):
|
||||
"""캐시 사용 안 함 테스트"""
|
||||
response = client.get("/files?use_cache=false")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["cache_hit"] is False
|
||||
|
||||
def test_delete_file_not_found(self, client: TestClient):
|
||||
"""존재하지 않는 파일 삭제 테스트"""
|
||||
response = client.delete("/files/999")
|
||||
|
||||
# 파일이 없어도 에러 응답이 아닌 정상 응답 구조를 가져야 함
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# 에러 케이스이므로 error 키가 있을 수 있음
|
||||
assert "error" in data or "success" in data
|
||||
|
||||
@patch('app.api.file_management.tkmp_cache')
|
||||
def test_delete_file_cache_invalidation(self, mock_cache, client: TestClient, db_session):
|
||||
"""파일 삭제 시 캐시 무효화 테스트"""
|
||||
# 실제 파일 데이터가 없으므로 mock으로 처리
|
||||
with patch('app.api.file_management.db') as mock_db:
|
||||
mock_result = MagicMock()
|
||||
mock_result.fetchone.return_value = MagicMock(
|
||||
id=1,
|
||||
original_filename="test.xlsx",
|
||||
job_no="TEST-001"
|
||||
)
|
||||
mock_db.execute.return_value = mock_result
|
||||
|
||||
response = client.delete("/files/1")
|
||||
|
||||
# 캐시 무효화 호출 확인
|
||||
mock_cache.invalidate_file_cache.assert_called_once_with(1)
|
||||
mock_cache.invalidate_job_cache.assert_called_once_with("TEST-001")
|
||||
|
||||
|
||||
class TestFileValidation:
|
||||
"""파일 검증 테스트"""
|
||||
|
||||
def test_file_extension_validation(self):
|
||||
"""파일 확장자 검증 테스트"""
|
||||
from app.utils.file_validator import file_validator
|
||||
|
||||
# 허용된 확장자
|
||||
assert file_validator.validate_file_extension("test.xlsx") is True
|
||||
assert file_validator.validate_file_extension("test.xls") is True
|
||||
assert file_validator.validate_file_extension("test.csv") is True
|
||||
|
||||
# 허용되지 않은 확장자
|
||||
assert file_validator.validate_file_extension("test.txt") is False
|
||||
assert file_validator.validate_file_extension("test.pdf") is False
|
||||
assert file_validator.validate_file_extension("test.exe") is False
|
||||
|
||||
def test_file_size_validation(self):
|
||||
"""파일 크기 검증 테스트"""
|
||||
from app.utils.file_validator import file_validator
|
||||
|
||||
# 허용된 크기 (50MB 이하)
|
||||
assert file_validator.validate_file_size(1024) is True # 1KB
|
||||
assert file_validator.validate_file_size(10 * 1024 * 1024) is True # 10MB
|
||||
assert file_validator.validate_file_size(50 * 1024 * 1024) is True # 50MB
|
||||
|
||||
# 허용되지 않은 크기 (50MB 초과)
|
||||
assert file_validator.validate_file_size(51 * 1024 * 1024) is False # 51MB
|
||||
assert file_validator.validate_file_size(100 * 1024 * 1024) is False # 100MB
|
||||
|
||||
def test_filename_validation(self):
|
||||
"""파일명 검증 테스트"""
|
||||
from app.utils.file_validator import file_validator
|
||||
|
||||
# 안전한 파일명
|
||||
assert file_validator.validate_filename("test.xlsx") is True
|
||||
assert file_validator.validate_filename("BOM_Rev1.xlsx") is True
|
||||
assert file_validator.validate_filename("자재목록_2025.csv") is True
|
||||
|
||||
# 위험한 파일명
|
||||
assert file_validator.validate_filename("../test.xlsx") is False
|
||||
assert file_validator.validate_filename("test/file.xlsx") is False
|
||||
assert file_validator.validate_filename("test:file.xlsx") is False
|
||||
assert file_validator.validate_filename("test*file.xlsx") is False
|
||||
|
||||
def test_filename_sanitization(self):
|
||||
"""파일명 정화 테스트"""
|
||||
from app.utils.file_validator import file_validator
|
||||
|
||||
# 위험한 문자 제거
|
||||
assert file_validator.sanitize_filename("../test.xlsx") == "__test.xlsx"
|
||||
assert file_validator.sanitize_filename("test/file.xlsx") == "test_file.xlsx"
|
||||
assert file_validator.sanitize_filename("test:file*.xlsx") == "test_file_.xlsx"
|
||||
|
||||
# 연속된 언더스코어 제거
|
||||
assert file_validator.sanitize_filename("test__file.xlsx") == "test_file.xlsx"
|
||||
|
||||
# 앞뒤 공백 및 점 제거
|
||||
assert file_validator.sanitize_filename(" test.xlsx ") == "test.xlsx"
|
||||
assert file_validator.sanitize_filename(".test.xlsx.") == "test.xlsx"
|
||||
|
||||
|
||||
class TestCacheManager:
|
||||
"""캐시 매니저 테스트"""
|
||||
|
||||
@patch('app.utils.cache_manager.redis')
|
||||
def test_cache_set_get(self, mock_redis):
|
||||
"""캐시 저장/조회 테스트"""
|
||||
from app.utils.cache_manager import CacheManager
|
||||
|
||||
# Redis 클라이언트 mock 설정
|
||||
mock_client = MagicMock()
|
||||
mock_redis.from_url.return_value = mock_client
|
||||
|
||||
cache_manager = CacheManager()
|
||||
|
||||
# 데이터 저장
|
||||
test_data = {"test": "data"}
|
||||
result = cache_manager.set("test_key", test_data, 3600)
|
||||
|
||||
# Redis 호출 확인
|
||||
mock_client.setex.assert_called_once()
|
||||
|
||||
def test_tkmp_cache_file_list(self):
|
||||
"""TK-MP 캐시 파일 목록 테스트"""
|
||||
from app.utils.cache_manager import TKMPCache
|
||||
|
||||
with patch('app.utils.cache_manager.CacheManager') as mock_cache_manager:
|
||||
mock_cache = MagicMock()
|
||||
mock_cache_manager.return_value = mock_cache
|
||||
|
||||
tkmp_cache = TKMPCache()
|
||||
|
||||
# 파일 목록 캐시 저장
|
||||
files = [{"id": 1, "name": "test.xlsx"}]
|
||||
tkmp_cache.set_file_list(files, "TEST-001", False)
|
||||
|
||||
# 캐시 호출 확인
|
||||
mock_cache.set.assert_called_once()
|
||||
|
||||
# 파일 목록 캐시 조회
|
||||
mock_cache.get.return_value = files
|
||||
result = tkmp_cache.get_file_list("TEST-001", False)
|
||||
|
||||
assert result == files
|
||||
mock_cache.get.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestFileProcessor:
|
||||
"""파일 프로세서 테스트"""
|
||||
|
||||
def test_file_info_analysis(self, sample_excel_file):
|
||||
"""파일 정보 분석 테스트"""
|
||||
from app.utils.file_processor import file_processor
|
||||
|
||||
info = file_processor.get_file_info(sample_excel_file)
|
||||
|
||||
assert "file_path" in info
|
||||
assert "file_size" in info
|
||||
assert "file_extension" in info
|
||||
assert info["file_extension"] == ".xlsx"
|
||||
assert info["file_type"] == "excel"
|
||||
assert "recommended_chunk_size" in info
|
||||
|
||||
def test_dataframe_memory_optimization(self):
|
||||
"""DataFrame 메모리 최적화 테스트"""
|
||||
import pandas as pd
|
||||
from app.utils.file_processor import file_processor
|
||||
|
||||
# 테스트 데이터 생성
|
||||
df = pd.DataFrame({
|
||||
'int_col': [1, 2, 3, 4, 5],
|
||||
'float_col': [1.1, 2.2, 3.3, 4.4, 5.5],
|
||||
'str_col': ['A', 'B', 'A', 'B', 'A']
|
||||
})
|
||||
|
||||
original_memory = df.memory_usage(deep=True).sum()
|
||||
optimized_df = file_processor.optimize_dataframe_memory(df)
|
||||
optimized_memory = optimized_df.memory_usage(deep=True).sum()
|
||||
|
||||
# 메모리 사용량이 감소했거나 같아야 함
|
||||
assert optimized_memory <= original_memory
|
||||
|
||||
# 데이터 무결성 확인
|
||||
assert len(optimized_df) == len(df)
|
||||
assert list(optimized_df.columns) == list(df.columns)
|
||||
|
||||
def test_optimal_chunk_size_calculation(self):
|
||||
"""최적 청크 크기 계산 테스트"""
|
||||
from app.utils.file_processor import file_processor
|
||||
|
||||
# 작은 파일 (1MB 미만)
|
||||
chunk_size = file_processor._calculate_optimal_chunk_size(500 * 1024) # 500KB
|
||||
assert chunk_size == 500
|
||||
|
||||
# 중간 파일 (10MB 미만)
|
||||
chunk_size = file_processor._calculate_optimal_chunk_size(5 * 1024 * 1024) # 5MB
|
||||
assert chunk_size == 1000
|
||||
|
||||
# 큰 파일 (50MB 이상)
|
||||
chunk_size = file_processor._calculate_optimal_chunk_size(100 * 1024 * 1024) # 100MB
|
||||
assert chunk_size == 5000
|
||||
Reference in New Issue
Block a user