Skip to content

Commit

Permalink
Merge pull request #38 from fractal-napari-plugins-collection/improve…
Browse files Browse the repository at this point in the history
…_testing

[WIP] Improve testing
  • Loading branch information
jluethi authored Nov 2, 2023
2 parents 1d76ab6 + 3a76ff7 commit 9e8520b
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test_and_deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ jobs:
run: |
pip install .
# stop the build if there are Python syntax errors, undefined symbols or failed code style checks
python -m pylint --fail-under=8 -f parseable src
python -m pylint --ignore-patterns=test_.*.py --fail-under=8 -f parseable src
# this runs the platform-specific tests declared in tox.ini
- name: Test with tox
Expand Down
25 changes: 25 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# enable pre-commit.ci at https://pre-commit.ci/
# it adds:
# 1. auto fixing pull requests
# 2. auto updating the pre-commit configuration
ci:
autoupdate_schedule: monthly
autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]"
autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate"

repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.12.1
hooks:
- id: validate-pyproject

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.257
hooks:
- id: ruff
args: [--fix]

- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,7 @@ napari-feature-classifier =
[options.entry_points]
napari.manifest =
napari-feature-classifier = napari_feature_classifier:napari.yaml

[coverage:run]
omit =
src/napari_feature_classifier/dev_main.py
34 changes: 19 additions & 15 deletions src/napari_feature_classifier/_tests/test_annotator_widgets.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
""" Tests for annotator widget initialization"""
import numpy as np
import imageio
from pathlib import Path
from napari_feature_classifier.annotator_init_widget import InitializeLabelAnnotatorWidget
from napari_feature_classifier.annotator_init_widget import (
InitializeLabelAnnotatorWidget,
)
from napari_feature_classifier.annotator_widget import (
LabelAnnotator,
get_class_selection
get_class_selection,
)

lbl_img_np = imageio.v2.imread(Path("src/napari_feature_classifier/sample_data/test_labels.tif"))
lbl_img_np = imageio.v2.imread(
Path("src/napari_feature_classifier/sample_data/test_labels.tif")
)


# make_napari_viewer is a pytest fixture that returns a napari viewer object
Expand All @@ -22,15 +25,21 @@ def test_annotator_widgets(make_napari_viewer):
label_layer = viewer.add_labels(lbl_img_np)

# Start init widget
_ = InitializeLabelAnnotatorWidget(viewer)
init_widget = InitializeLabelAnnotatorWidget(viewer)
init_widget.initialize_annotator()

# Start the annotator widget
annotator_widget = LabelAnnotator(viewer)

# call our widget method
#my_widget._on_click()
# my_widget._on_click()
annotator_widget._init_annotation(label_layer)

# TODO: Test toggle_label


# TODO: Test saving annotations


def test_custom_class_selection(make_napari_viewer):
"""
Expand All @@ -40,12 +49,10 @@ def test_custom_class_selection(make_napari_viewer):
viewer = make_napari_viewer()
viewer.add_labels(lbl_img_np)

class_names = ['Class Test', 'Class XYZ', '12345']
class_names = ["Class Test", "Class XYZ", "12345"]
# Start the annotator widget with a list of named classes
LabelAnnotator(
viewer,
ClassSelection = get_class_selection(class_names = class_names)
)
LabelAnnotator(viewer, ClassSelection=get_class_selection(class_names=class_names))


def test_numbered_class_selection(make_napari_viewer):
"""
Expand All @@ -55,7 +62,4 @@ def test_numbered_class_selection(make_napari_viewer):
viewer = make_napari_viewer()
viewer.add_labels(lbl_img_np)
# Start the annotator widget with a given number of classes
LabelAnnotator(
viewer,
ClassSelection = get_class_selection(n_classes = 8)
)
LabelAnnotator(viewer, ClassSelection=get_class_selection(n_classes=8))
155 changes: 152 additions & 3 deletions src/napari_feature_classifier/_tests/test_classifier_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@
LoadClassifierContainer,
)

lbl_img_np = imageio.v2.imread(Path("src/napari_feature_classifier/sample_data/test_labels.tif"))
clf_path = Path("src/napari_feature_classifier/sample_data/test_labels_classifier.clf")
lbl_img_np = imageio.v2.imread(
Path("src/napari_feature_classifier/sample_data/test_labels.tif")
)
clf_path = Path(
"src/napari_feature_classifier/sample_data/" "test_labels_classifier.clf"
)


# make_napari_viewer is a pytest fixture that returns a napari viewer object
# capsys is a pytest fixture that captures stdout and stderr output streams
Expand All @@ -32,14 +37,21 @@ def test_classifier_widgets_initialization_no_features_selected(make_napari_view

classifier_widget.initialize_run_widget()

# TODO: Catch that it doesn't actually initialize, but instead shows a
# TODO: Catch that it doesn't actually initialize, but instead shows a
# message to the user that now features were selected
assert classifier_widget._run_container is None


def test_classifier_initializtion_without_label_image(make_napari_viewer):
viewer = make_napari_viewer()
classifier_widget = ClassifierWidget(viewer)
assert classifier_widget._init_container._last_selected_label_layer is None


features = make_features(np.unique(lbl_img_np)[1:], roi_id="ROI1", n_features=6)
features_no_roi_id = features.drop(columns=["roi_id"])


# make_napari_viewer is a pytest fixture that returns a napari viewer object
@pytest.mark.parametrize("features", [features, features_no_roi_id])
def test_running_classification_through_widget(features, make_napari_viewer):
Expand Down Expand Up @@ -85,9 +97,137 @@ def test_running_classification_through_widget(features, make_napari_viewer):
# Delete the classifier file (cleanup to avoid overwriting confirmation)
os.remove("lbl_img_np_classifier.clf")


# Test classifier results export
def test_prediction_export(make_napari_viewer, capsys):
"""
Tests if the main widget launches
"""
# make viewer and add an image layer using our fixture
viewer = make_napari_viewer()
label_layer = viewer.add_labels(lbl_img_np)
label_layer.features = features

# Start init widget
classifier_widget = ClassifierWidget(viewer)

# Select relevant features
classifier_widget._init_container._feature_combobox.value = [
"feature_1",
"feature_2",
"feature_3",
]

classifier_widget.initialize_run_widget()

# Check that the run widget is initialized
assert classifier_widget._run_container is not None

# Add some annotations manually
label_layer.features.loc[0, "annotations"] = 1.0
label_layer.features.loc[1, "annotations"] = 1.0
label_layer.features.loc[3, "annotations"] = 3.0

# Run the classifier
classifier_widget._run_container.run()

# Test result export
classifier_widget._run_container.export_results()
df = pd.read_csv(classifier_widget._run_container._export_destination.value)
assert df.shape == (16, 5)
assert df["prediction"].isna().sum() == 0

# Delete the classifier file (cleanup to avoid overwriting confirmation)
os.remove("lbl_img_np_classifier.clf")
os.remove("lbl_img_np_predictions.csv")

message = "INFO: Annotations were saved at lbl_img_np_predictions.csv"
assert message in capsys.readouterr().out


# TODO: Add a test to check the overwrite confirmations working correctly
# For classifier files, for annotations and for exported predictions


features = make_features(np.unique(lbl_img_np)[1:], roi_id="ROI1", n_features=6)


# make_napari_viewer is a pytest fixture that returns a napari viewer object
def test_classifier_fails_running_without_annotation(make_napari_viewer, capsys):
"""
Tests if the main widget launches
"""
# make viewer and add an image layer using our fixture
viewer = make_napari_viewer()
label_layer = viewer.add_labels(lbl_img_np)
label_layer.features = features

# Start init widget
classifier_widget = ClassifierWidget(viewer)

# Select relevant features
classifier_widget._init_container._feature_combobox.value = [
"feature_1",
"feature_2",
"feature_3",
]

classifier_widget.initialize_run_widget()

# Run the classifier
classifier_widget._run_container.run()

expected_message = (
"INFO: Training failed. A typical reason are not "
"having enough annotations. \nThe error message was: Found array "
"with 0 sample(s) (shape=(0, 3)) while a minimum of 1 is required "
"by RandomForestClassifier."
)
assert expected_message in capsys.readouterr().out


def test_layer_selection_changes(make_napari_viewer):
"""
Tests if the main widget launches
"""
# make viewer and add an image layer using our fixture
viewer = make_napari_viewer()
label_layer1 = viewer.add_labels(lbl_img_np, name="test_labels")
label_layer2 = viewer.add_labels(lbl_img_np, name="test_labels_2")
label_layer2.features = features
label_layer1.features = features

# Start init widget
classifier_widget = ClassifierWidget(viewer)

# Select relevant features
classifier_widget._init_container._feature_combobox.value = [
"feature_1",
"feature_2",
"feature_3",
]

classifier_widget.initialize_run_widget()

assert (
classifier_widget._run_container._last_selected_label_layer.name
== "test_labels_2"
)
assert (
str(classifier_widget._run_container._export_destination.value)
== "test_labels_2_predictions.csv"
)
viewer.layers.selection.active = label_layer1
assert (
classifier_widget._run_container._last_selected_label_layer.name
== "test_labels"
)
assert (
classifier_widget._run_container._export_destination.value.name
== "test_labels_predictions.csv"
)


# make_napari_viewer is a pytest fixture that returns a napari viewer object
# capsys is a pytest fixture that captures stdout and stderr output streams
def test_load_classifier_widget(make_napari_viewer, capsys):
Expand All @@ -109,3 +249,12 @@ def test_load_classifier_widget(make_napari_viewer, capsys):
assert loading_widget._run_container._prediction_layer.visible
assert "prediction" in label_layer.features.columns


def test_change_of_loader_filter(make_napari_viewer, capsys):
viewer = make_napari_viewer()
loading_widget = LoadClassifierContainer(viewer)
assert loading_widget._filter.value == "*.clf"
assert loading_widget._clf_destination.filter is None
loading_widget._filter.value = "*.pkl"
assert loading_widget._clf_destination.filter == "*.pkl"
assert loading_widget._filter.value == "*.pkl"
35 changes: 35 additions & 0 deletions src/napari_feature_classifier/_tests/test_feature_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
""" Tests feature loading"""
import pandas as pd

import imageio
from pathlib import Path
from napari_feature_classifier.feature_loader_widget import (
load_features_factory,
)

lbl_img_np = imageio.v2.imread(
Path("src/napari_feature_classifier/sample_data/test_labels.tif")
)
csv_path = Path("src/napari_feature_classifier/sample_data/test_df.csv")


def test_feature_loading_csv(make_napari_viewer, capsys):
"""
Tests if the main widget launches
"""
# make viewer and add an image layer using our fixture
viewer = make_napari_viewer()
labels_layer = viewer.add_labels(lbl_img_np)

# Start feature loading widget
loading_widget = load_features_factory()
loading_widget.layer.value = labels_layer
loading_widget.path.value = csv_path
features = pd.read_csv(csv_path)
loading_widget.__call__()
assert (labels_layer.features == features).all().all()

# Assert that this message is logged
expected = "INFO: Loaded features and attached them "
expected += 'to "lbl_img_np" layer\n'
assert expected in capsys.readouterr().out
9 changes: 5 additions & 4 deletions src/napari_feature_classifier/classifier_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
PushButton,
Select,
)
from qtpy.QtWidgets import QMessageBox # pylint: disable=E0611

from napari_feature_classifier.annotator_init_widget import LabelAnnotatorTextSelector
from napari_feature_classifier.annotator_widget import (
Expand Down Expand Up @@ -117,7 +116,9 @@ def get_class_names(self):
def get_feature_options(self, layer):
"""
Get the feature options of the currently selected layer
Only works if a label layer is selected (we don't load features from other layers)
Only works if a label layer is selected (we don't load features from
other layers)
"""
if isinstance(layer, napari.layers.Labels):
return list(layer.features.columns)
Expand Down Expand Up @@ -265,7 +266,7 @@ def __init__(
# Export options
self._export_destination = FileEdit(
label="Prediction Export Path",
value=f"{self._last_selected_label_layer}_prediction.csv",
value=f"{self._last_selected_label_layer}_predictions.csv",
mode="w",
)
self._export_button = PushButton(text="Export Classifier Result")
Expand Down Expand Up @@ -552,7 +553,7 @@ class LoadClassifierContainer(Container):
The button that launches the `ClassifierRunContainer`
_filter: magicgui.widgets.RadioButtons
The radio button widget that allows the user to select the file filter
to use for selecting the classifier file. See
to use for selecting the classifier file. See
https://github.com/fractal-napari-plugins-collection/napari-feature-classifier/issues/36
for more details.
_run_container: ClassifierRunContainer
Expand Down

0 comments on commit 9e8520b

Please sign in to comment.