|
| 1 | +import os |
| 2 | +import shutil |
1 | 3 | from pathlib import Path |
| 4 | +from pickle import UnpicklingError |
| 5 | +from unittest import mock |
2 | 6 |
|
3 | 7 | import pytest |
| 8 | +from lir.data.models import FeatureData |
| 9 | +from lir.lrsystems.lrsystems import LRSystem |
| 10 | + |
4 | 11 | from lrmodule import ModelSettings |
5 | 12 | from lrmodule.data_types import MarkType, ScoreType |
6 | | -from lrmodule.lrsystem import load_lrsystem |
7 | | -from lrmodule.persistence import load_model, save_model |
| 13 | +from lrmodule.persistence import load_model, save_model, _get_model_filename |
| 14 | + |
| 15 | + |
| 16 | +MODEL_STORAGE_PATH = Path(__file__).parent / "test_model_storage" |
| 17 | + |
| 18 | + |
| 19 | +@pytest.fixture(autouse=True) |
| 20 | +def clear_test_model_storage_directory(): |
| 21 | + """Clean up 'test_model_storage' directory before running each test. |
| 22 | +
|
| 23 | + This ensures a fresh environment for each test. The generated artifacts are not |
| 24 | + cleaned up after each test to allow easy debugging of the generated pickle files. |
| 25 | + """ |
| 26 | + if MODEL_STORAGE_PATH.exists(): |
| 27 | + shutil.rmtree(MODEL_STORAGE_PATH) |
| 28 | + MODEL_STORAGE_PATH.mkdir(parents=True) |
| 29 | + |
| 30 | + |
| 31 | +def test_serialize_trained_lr_system(trained_lr_system: LRSystem): |
| 32 | + """Check that a trained LR system can be serialized.""" |
| 33 | + # Given that we have a trained LR system |
| 34 | + settings = ModelSettings(MarkType.FIRING_PIN_IMPRESSION, ScoreType.ACCF) |
| 35 | + mark_type = settings.mark_type.value |
| 36 | + score_type = settings.score_type.value |
| 37 | + |
| 38 | + # When we serialize the LR system |
| 39 | + save_model(trained_lr_system, settings, MODEL_STORAGE_PATH) |
| 40 | + |
| 41 | + # There should be a file we can load |
| 42 | + model_filename = _get_model_filename(settings) |
| 43 | + model_file_path = MODEL_STORAGE_PATH / model_filename |
| 44 | + assert model_file_path.exists() |
| 45 | + |
| 46 | + |
| 47 | +def test_deserialize_trained_lr_system(trained_lr_system: LRSystem, sample_feature_data: FeatureData): |
| 48 | + """Check that a deserialized, trained LR system yields exactly the same results.""" |
| 49 | + # Given that we have a certain LR system serialized |
| 50 | + settings = ModelSettings(MarkType.FIRING_PIN_IMPRESSION, ScoreType.ACCF) |
| 51 | + save_model(trained_lr_system, settings, MODEL_STORAGE_PATH) |
| 52 | + |
| 53 | + # When the model is deserialized |
| 54 | + deserialized_model = load_model(settings, MODEL_STORAGE_PATH) |
| 55 | + |
| 56 | + # The deserialized model and the model it originated from should be of the same type of LR system |
| 57 | + assert type(trained_lr_system) == type(trained_lr_system) |
| 58 | + |
| 59 | + # The calculated LLR output should be identical to the LR system output of the serialized model |
| 60 | + expected_llr_data = trained_lr_system.apply(sample_feature_data) |
| 61 | + deserialized_model_data = deserialized_model.apply(sample_feature_data) |
| 62 | + |
| 63 | + assert deserialized_model_data == expected_llr_data |
| 64 | + |
| 65 | + |
| 66 | +@pytest.mark.parametrize('mark_type,score_type', [ |
| 67 | + (MarkType.FIRING_PIN_IMPRESSION, ScoreType.CMC), # other score type |
| 68 | + (MarkType.BREECH_PIN_IMPRESSION, ScoreType.ACCF), # other mark type |
| 69 | + (MarkType.BREECH_PIN_IMPRESSION, ScoreType.CMC), # other mark and other score type |
| 70 | +]) |
| 71 | +def test_deserialize_inexistent_lr_system(trained_lr_system: LRSystem, mark_type: MarkType, score_type: ScoreType): |
| 72 | + """Check that an appropriate error is raised when there is no serialized model.""" |
| 73 | + # Given that the LR model storage directory is empty |
| 74 | + assert os.listdir(MODEL_STORAGE_PATH) == [] |
| 75 | + |
| 76 | + # Given that we have a serialized model for a given type of `ModelSettings` |
| 77 | + settings = ModelSettings(MarkType.FIRING_PIN_IMPRESSION, ScoreType.ACCF) |
| 78 | + save_model(trained_lr_system, settings, MODEL_STORAGE_PATH) |
| 79 | + assert len(os.listdir(MODEL_STORAGE_PATH)) == 1 |
| 80 | + |
| 81 | + # When we try to deserialize a model for a different type of `ModelSettings` |
| 82 | + other_settings = ModelSettings(mark_type, score_type) |
| 83 | + |
| 84 | + # An exception should be raised mentioning that we can't find that particular deserialized LR model |
| 85 | + with pytest.raises(FileNotFoundError) as exception_info: |
| 86 | + load_model(other_settings, MODEL_STORAGE_PATH) |
| 87 | + |
| 88 | + # The exception should mention no models found for the requested mark/score types |
| 89 | + assert "No model found for mark type" in str(exception_info.value) |
| 90 | + assert other_settings.mark_type.value in str(exception_info.value) |
| 91 | + assert other_settings.score_type.value in str(exception_info.value) |
8 | 92 |
|
9 | 93 |
|
10 | | -def test_persistence(): |
| 94 | +def test_deserialize_from_invalid_pickle_file(trained_lr_system: LRSystem): |
| 95 | + """Check that an appropriate error is raised when unable to unpickle serialized model.""" |
| 96 | + # Given that we have a serialized model for a given type of `ModelSettings` |
11 | 97 | settings = ModelSettings(MarkType.FIRING_PIN_IMPRESSION, ScoreType.ACCF) |
| 98 | + save_model(trained_lr_system, settings, MODEL_STORAGE_PATH) |
12 | 99 |
|
13 | | - # not implemented |
14 | | - with pytest.raises(Exception): |
15 | | - load_model(settings, "dataset_id", Path("/")) |
| 100 | + with mock.patch('pickle.load', side_effect=UnpicklingError("Some pickle error")): |
| 101 | + # When pickle can't load the given file, we expect an appropriate error to be raised |
| 102 | + with pytest.raises(RuntimeError) as exception_info: |
| 103 | + load_model(settings, MODEL_STORAGE_PATH) |
16 | 104 |
|
17 | | - # not implemented |
18 | | - lrsystem = load_lrsystem(settings) |
19 | | - with pytest.raises(Exception): |
20 | | - save_model(lrsystem, settings, "dataset_id", Path("/")) |
| 105 | + assert "Could not load model from .pkl file for mark type" in str(exception_info.value) |
| 106 | + assert settings.mark_type.value in str(exception_info.value) |
| 107 | + assert settings.score_type.value in str(exception_info.value) |
0 commit comments