|
|
""" |
|
|
Pytest configuration and fixtures for behavioral tests. |
|
|
""" |
|
|
import pytest |
|
|
import numpy as np |
|
|
import joblib |
|
|
from pathlib import Path |
|
|
from sklearn.feature_extraction.text import TfidfVectorizer |
|
|
|
|
|
from hopcroft_skill_classification_tool_competition.config import DATA_PATHS |
|
|
from hopcroft_skill_classification_tool_competition.features import ( |
|
|
clean_github_text, |
|
|
get_label_columns, |
|
|
load_data_from_db |
|
|
) |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
def trained_model(): |
|
|
"""Load the trained model for testing.""" |
|
|
model_path = Path(DATA_PATHS["models_dir"]) / "random_forest_tfidf_gridsearch_smote.pkl" |
|
|
|
|
|
|
|
|
if not model_path.exists(): |
|
|
model_path = Path(DATA_PATHS["models_dir"]) / "random_forest_tfidf_gridsearch.pkl" |
|
|
|
|
|
if not model_path.exists(): |
|
|
pytest.skip(f"Model not found at {model_path}. Please train a model first.") |
|
|
|
|
|
return joblib.load(model_path) |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
def tfidf_vectorizer(trained_model): |
|
|
""" |
|
|
Extract or reconstruct the TF-IDF vectorizer from the trained model. |
|
|
|
|
|
Note: In a production setting, you should save and load the vectorizer separately. |
|
|
For now, we'll create a new one fitted on the training data with max_features=1000. |
|
|
""" |
|
|
|
|
|
features_path = Path(DATA_PATHS["features"]) |
|
|
|
|
|
if not features_path.exists(): |
|
|
pytest.skip(f"Features not found at {features_path}. Please run feature extraction first.") |
|
|
|
|
|
|
|
|
|
|
|
from hopcroft_skill_classification_tool_competition.features import extract_tfidf_features |
|
|
|
|
|
try: |
|
|
df = load_data_from_db() |
|
|
|
|
|
_, vectorizer = extract_tfidf_features(df, max_features=1000) |
|
|
return vectorizer |
|
|
except Exception as e: |
|
|
pytest.skip(f"Could not load vectorizer: {e}") |
|
|
|
|
|
|
|
|
@pytest.fixture(scope="session") |
|
|
def label_names(): |
|
|
"""Get the list of label names from the database.""" |
|
|
try: |
|
|
df = load_data_from_db() |
|
|
return get_label_columns(df) |
|
|
except Exception as e: |
|
|
pytest.skip(f"Could not load label names: {e}") |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def predict_text(trained_model, tfidf_vectorizer): |
|
|
""" |
|
|
Factory fixture that returns a function to predict skills from raw text. |
|
|
|
|
|
Returns: |
|
|
Function that takes text and returns predicted label indices |
|
|
""" |
|
|
def _predict(text: str, return_proba: bool = False): |
|
|
""" |
|
|
Predict skills from raw text. |
|
|
|
|
|
Args: |
|
|
text: Raw GitHub issue text |
|
|
return_proba: If True, return probabilities instead of binary predictions |
|
|
|
|
|
Returns: |
|
|
If return_proba=False: indices of predicted labels (numpy array) |
|
|
If return_proba=True: probability matrix (n_samples, n_labels) |
|
|
""" |
|
|
|
|
|
cleaned = clean_github_text(text) |
|
|
features = tfidf_vectorizer.transform([cleaned]).toarray() |
|
|
|
|
|
if return_proba: |
|
|
|
|
|
|
|
|
try: |
|
|
probas = np.array([ |
|
|
estimator.predict_proba(features)[0][:, 1] |
|
|
for estimator in trained_model.estimators_ |
|
|
]).T |
|
|
return probas |
|
|
except Exception: |
|
|
|
|
|
return trained_model.predict(features) |
|
|
|
|
|
|
|
|
predictions = trained_model.predict(features)[0] |
|
|
|
|
|
|
|
|
return np.where(predictions == 1)[0] |
|
|
|
|
|
return _predict |
|
|
|
|
|
|
|
|
@pytest.fixture |
|
|
def predict_with_labels(predict_text, label_names): |
|
|
""" |
|
|
Factory fixture that returns a function to predict skills with label names. |
|
|
|
|
|
Returns: |
|
|
Function that takes text and returns list of predicted label names |
|
|
""" |
|
|
def _predict(text: str): |
|
|
""" |
|
|
Predict skill labels from raw text. |
|
|
|
|
|
Args: |
|
|
text: Raw GitHub issue text |
|
|
|
|
|
Returns: |
|
|
List of predicted label names |
|
|
""" |
|
|
indices = predict_text(text) |
|
|
return [label_names[i] for i in indices] |
|
|
|
|
|
return _predict |
|
|
|
|
|
|
|
|
def pytest_configure(config): |
|
|
"""Register custom markers.""" |
|
|
config.addinivalue_line( |
|
|
"markers", "invariance: Tests for invariance (changes should not affect predictions)" |
|
|
) |
|
|
config.addinivalue_line( |
|
|
"markers", "directional: Tests for directional expectations (changes should affect predictions predictably)" |
|
|
) |
|
|
config.addinivalue_line( |
|
|
"markers", "mft: Minimum Functionality Tests (basic examples with expected outputs)" |
|
|
) |
|
|
config.addinivalue_line( |
|
|
"markers", "training: Tests for model training validation (loss, overfitting, devices)" |
|
|
) |
|
|
|