Skip to content

Commit

Permalink
Increase test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
janfreyberg committed Sep 3, 2021
1 parent 905c141 commit c1d95c3
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 15 deletions.
2 changes: 1 addition & 1 deletion ipyannotations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def submit(self, sender: Any = None):
"""
if hasattr(self, "data"):
value = self.data
else:
else: # pragma: no cover
raise NotImplementedError(
"Submission for this widget doesn't seem to be implemented."
)
Expand Down
2 changes: 1 addition & 1 deletion ipyannotations/images/annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def _handle_keystroke(self, event):
if event["key"] == f"{(i + 1) % 10}":
self.class_selector.value = option
if i == 10:
break
break # pragma: no cover


class PolygonAnnotator(Annotator):
Expand Down
2 changes: 1 addition & 1 deletion ipyannotations/text/tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,4 @@ def _handle_keystroke(self, event):
if event["key"] == f"{(i + 1) % 10}":
self.class_selector.value = option
if i == 10:
break
break # pragma: no cover
87 changes: 87 additions & 0 deletions tests/generic/test_classifier.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import time
from unittest.mock import MagicMock

from ipyannotations.controls import buttongroup, dropdownbutton
from ipyannotations.generic import classification
from ipyannotations import generic


def test_submit_with_button(mocker):
Expand Down Expand Up @@ -74,3 +76,88 @@ def test_max_buttons_switches_to_dropdown():
assert isinstance(widget.control_elements, buttongroup.ButtonGroup)
widget.options = ["a", "b", "c", "d", "e", "f", "g", "h"]
assert isinstance(widget.control_elements, dropdownbutton.DropdownButton)


def test_that_multiclass_submits_toggled_buttons(mocker):
widget = generic.MulticlassLabeller(options=["a", "b"])
widget.class_selector.buttons[0].button.value = True
assert widget.data == ["a"]
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
widget.submit()
submission_function.assert_called_with(["a"])
widget.class_selector.buttons[1].button.value = True
assert widget.data == ["a", "b"]
widget.submit()
submission_function.assert_called_with(["a", "b"])


def test_that_display_resets_toggles(mocker):
widget = generic.MulticlassLabeller(options=["a", "b"])
widget.class_selector.buttons[0].button.value = True
assert widget.data == ["a"]
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
widget.submit()
submission_function.assert_called_with(["a"])

widget.display("test data")
assert widget.data == []
assert not any(
button.button.value for button in widget.class_selector.buttons
)


def test_that_keys_toggle_buttons():
widget = generic.MulticlassLabeller(options=["a", "b"])
event = {"type": "keyup", "key": "1"}
widget._handle_keystroke(event)
assert widget.data == ["a"]
event = {"type": "keyup", "key": "2"}
widget._handle_keystroke(event)
assert widget.data == ["a", "b"]


def test_that_freetext_adds_buttons():
widget = generic.MulticlassLabeller(options=["a", "b"])
widget.freetext_widget.value = "c"
widget.freetext_submission(widget.freetext_widget)
assert widget.options == ["a", "b", "c"]
assert len(widget.class_selector.buttons) == 3
assert widget.data == ["c"]


def test_that_recent_freetext_blocks_submission_on_enter(mocker):
widget = generic.MulticlassLabeller(options=["a", "b"])
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
event = {"type": "keyup", "key": "Enter"}
widget._freetext_timestamp = time.time()
widget._handle_keystroke(event)
submission_function.assert_not_called()
widget._freetext_timestamp = time.time() - 1.0
widget._handle_keystroke(event)
submission_function.assert_called_once_with([])


def test_that_new_buttons_are_removed(mocker):
widget = generic.MulticlassLabeller(options=["a", "b"])
widget.freetext_widget.value = "c"
widget.freetext_submission(widget.freetext_widget)
undo_function: MagicMock = mocker.MagicMock()
widget.on_undo(undo_function)
assert widget.options == ["a", "b", "c"]
assert len(widget.class_selector.buttons) == 3
assert widget.data == ["c"]
assert len(widget._undo_queue) == 1
widget.undo()
assert len(widget._undo_queue) == 0
assert widget.options == ["a", "b"]
assert len(widget.class_selector.buttons) == 2
assert widget.data == []
undo_function.assert_not_called()
widget.undo()
assert widget.options == ["a", "b"]
assert len(widget.class_selector.buttons) == 2
assert widget.data == []
undo_function.assert_called_once()
29 changes: 26 additions & 3 deletions tests/images/test_abstract_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@
from ipyannotations.images.canvases.abstract_canvas import (
AbstractAnnotationCanvas,
)
import ipyannotations.images.canvases.image_utils
from ipyannotations.images.canvases.image_utils import fit_image

# ImageTypes =

class TestCanvas(AbstractAnnotationCanvas):
"""Test canvas to test the abstract canvas."""

def init_empty_data(self):
self.data = []


@settings(deadline=None)
Expand All @@ -34,7 +40,7 @@ def test_that_loading_image_clears_data(

@settings(deadline=None)
@given(img=infer)
def test_that_loading_image_from_path(img: Image.Image):
def test_that_loading_image_from_path_succeeds(img: Image.Image):

with tempfile.TemporaryDirectory(dir=".") as tmp:
tmp = pathlib.Path(tmp)
Expand Down Expand Up @@ -91,4 +97,21 @@ def test_that_points_clicked_get_translated_correctly(
round_trip_y, click_y, atol=1
)

# assert (0, 0) <= canvas.image_to_canvas_coordinates((0, 0)) <= canvas.size

@settings(deadline=None)
@given(img=infer)
def test_that_images_are_adjusted(img: widgets.Image):
with patch(
"ipyannotations.images.canvases.abstract_canvas.adjust", autospec=True
) as mock_adjust:
mock_adjust.return_value = img
canvas = TestCanvas()
canvas.image_brightness = 1.1
canvas.image_contrast = 1.1
canvas.load_image(img)

mock_adjust.assert_called_once_with(
img,
contrast_factor=1.1,
brightness_factor=1.1,
)
104 changes: 97 additions & 7 deletions tests/images/test_annotators.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,110 @@
from typing import Union
from unittest.mock import MagicMock, patch

from ipyannotations.images import PointAnnotator, PolygonAnnotator
import ipywidgets as widgets
import numpy as np
from PIL import Image
from ipyannotations.images import (
PointAnnotator,
PolygonAnnotator,
BoxAnnotator,
)
from ipyannotations.images.canvases import shapes
from ipyannotations.images.annotator import Annotator
from ipyannotations.images.canvases import (
PointAnnotationCanvas,
PolygonAnnotationCanvas,
BoundingBoxAnnotationCanvas,
)


def test_creating_widgets():

from ipyannotations.images.canvases.abstract_canvas import (
AbstractAnnotationCanvas,
)
from hypothesis import assume, given, infer, settings, strategies


class TestCanvas(AbstractAnnotationCanvas):
"""Test canvas to test the abstract canvas."""

def init_empty_data(self):
self.data = []


class TestAnnotator(Annotator):
"""Test Annotator to test the base implementation."""

CanvasClass = TestCanvas


@settings(deadline=None)
@given(img=infer)
def test_annotator_load_image(
img: Union[widgets.Image, np.ndarray, Image.Image]
):
with patch.object(
AbstractAnnotationCanvas, "load_image", autospec=True
) as patch_load_image, patch.object(
AbstractAnnotationCanvas, "clear", autospec=True
) as patch_clear:
annotator = TestAnnotator()
annotator.display(img)
patch_load_image.assert_called_once()
patch_clear.assert_called_once()


def test_handling_keystrokes(mocker):
widget = TestAnnotator(options=["a", "b"])
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
# test enter:
widget.canvas.data = [{"test_data": "hello"}]
widget._handle_keystroke({"type": "keyup", "key": "Enter"})
submission_function.assert_called_once_with([{"test_data": "hello"}])
# test the class selection:
# (self.class_selector, "value"), (self.canvas, "current_class")
assert widget.canvas.current_class == "a"
widget._handle_keystroke({"type": "keyup", "key": "2"})
assert widget.canvas.current_class == "b"
widget._handle_keystroke({"type": "keyup", "key": "1"})
assert widget.canvas.current_class == "a"


def test_data_post_processing(mocker):
widget = TestAnnotator(
options=["a", "b"], data_postprocessor=lambda data: data + ["extra"]
)
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
# test enter:
widget.canvas.data = [{"test_data": "hello"}]
assert widget.data == [{"test_data": "hello"}, "extra"]
widget.submit()
submission_function.assert_called_once_with(
[{"test_data": "hello"}, "extra"]
)


@given(polygon=infer, point=infer, box=infer, img=infer)
def test_creating_widgets_setting_and_reading_data(
polygon: shapes.Polygon,
point: shapes.Point,
box: shapes.BoundingBox,
img: widgets.Image,
):
"""Smoke test for initialisation."""
annotator = PolygonAnnotator()
annotator.display(img)
assert isinstance(annotator.canvas, PolygonAnnotationCanvas)

annotator.data = [polygon.data]
assert annotator.canvas.data == [polygon.data] == annotator.data
annotator = PointAnnotator()
annotator.display(img)
assert isinstance(annotator.canvas, PointAnnotationCanvas)
annotator.data = [point.data]
assert annotator.canvas.data == [point.data] == annotator.data
annotator = BoxAnnotator()
annotator.display(img)
assert isinstance(annotator.canvas, BoundingBoxAnnotationCanvas)
annotator.data = [box.data]
assert annotator.canvas.data == [box.data] == annotator.data


def test_undo():
Expand Down
7 changes: 7 additions & 0 deletions tests/images/test_color_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re

from hypothesis import given, strategies
import pytest

from ipyannotations.images.canvases.color_utils import (
hex_to_rgb,
Expand Down Expand Up @@ -36,6 +37,12 @@ def test_rgb_to_hex_and_back(color):
assert hex_to_rgb(hex_col) == color


def test_rgb_to_hex_failure():
color = (277, 128, 0)
with pytest.raises(ValueError):
hex_col = rgb_to_hex(color)


@given(rgb_colors)
def test_rgb_to_html(color):
rgb_string = rgba_to_html_string(color)
Expand Down
16 changes: 16 additions & 0 deletions tests/images/test_image_class_labellers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,19 @@ def test_that_displaying_images_doesnt_error():
widget = images.ClassLabeller()
widget.display(Image.new("RGB", size=(50, 50), color=(0, 0, 0)))
widget.display(np.zeros((50, 50)))


def test_that_multiclass_submits_toggled_buttons(mocker):
widget = images.MulticlassLabeller(options=["a", "b"])
widget.class_selector.buttons[0].button.value = True
assert widget.data == ["a"]
submission_function: MagicMock = mocker.MagicMock()
widget.on_submit(submission_function)
widget.submit()
submission_function.assert_called_with(["a"])


def test_that_multiclass_doesnt_error_when_displaying(mocker):
widget = images.MulticlassLabeller(options=["a", "b"])
widget.display(Image.new("RGB", size=(50, 50), color=(0, 0, 0)))
widget.display(np.zeros((50, 50)))
6 changes: 4 additions & 2 deletions tests/images/test_point_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ipyannotations.images.canvases.shapes import Point

IMAGE = np.random.randint(0, 256, size=(500, 700, 3), dtype=np.uint8)
IMAGE_SMALL = np.random.randint(0, 256, size=(500, 500, 3), dtype=np.uint8)
IMAGE_SMALL = np.random.randint(0, 256, size=(400, 600, 3), dtype=np.uint8)


@settings(deadline=None)
Expand Down Expand Up @@ -43,9 +43,11 @@ def test_click_handling(point: Point):
def test_data_is_translated():
canvas = PointAnnotationCanvas()
canvas.load_image(IMAGE_SMALL)
canvas.on_click(0, 0)
canvas.on_click(0, 100)
assert canvas.points == []
canvas.on_click(100, 0)
assert canvas.points == []
canvas.on_click(50, 50)
assert canvas.points[-1].coordinates == (0, 0)


Expand Down
44 changes: 44 additions & 0 deletions tests/test_base_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from ipyannotations import base
from unittest.mock import MagicMock
import ipywidgets
import pytest


ENTER_KEYUP = {"type": "keyup", "key": "Enter"}
ENTER_KEYDOWN = {"type": "keydown", "key": "Enter"}
BACKSPACE_KEYDOWN = {"type": "keyup", "key": "Backspace"}


class TestWidget(base.LabellingWidgetMixin, ipywidgets.VBox):
"""
Widget required as the mixin doesn't work if not also inheriting from VBox.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)


def test_error_on_passing_non_callable():
widget = TestWidget()
with pytest.raises(ValueError):
widget.on_submit(1)


def test_key_handling(mocker):
widget = TestWidget()
submission_function: MagicMock = mocker.MagicMock()
undo_function: MagicMock = mocker.MagicMock()
undo_spy: MagicMock = mocker.spy(widget, "undo")
widget.on_submit(submission_function)
widget.on_submit(undo_function)
widget.data = "test data"

widget._handle_keystroke(ENTER_KEYDOWN)
submission_function.assert_not_called()

widget._handle_keystroke(ENTER_KEYUP)
submission_function.assert_called_with("test data")

widget._handle_keystroke(BACKSPACE_KEYDOWN)
undo_spy.assert_called_once()
undo_function.assert_called_once()
Loading

0 comments on commit c1d95c3

Please sign in to comment.