Skip to content

Commit

Permalink
Increase test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasteuwen committed Sep 23, 2023
1 parent ddd3c2b commit 3bb96c2
Show file tree
Hide file tree
Showing 14 changed files with 247 additions and 14 deletions.
3 changes: 2 additions & 1 deletion dlup/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

from dlup import UnsupportedSlideError
from dlup._region import BoundaryMode, RegionView
from dlup.experimental_backends import AbstractSlideBackend, ImageBackend
from dlup.backends.common import AbstractSlideBackend
from dlup.experimental_backends import ImageBackend
from dlup.types import GenericFloatArray, GenericIntArray, GenericNumber, PathLike
from dlup.utils.image import check_if_mpp_is_valid

Expand Down
1 change: 1 addition & 0 deletions dlup/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Copyright (c) dlup contributors
12 changes: 11 additions & 1 deletion dlup/experimental_backends/common.py → dlup/backends/common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding=utf-8
# Copyright (c) dlup contributors
from __future__ import annotations

Expand Down Expand Up @@ -160,6 +159,17 @@ def get_thumbnail(self, size: int | tuple[int, int]) -> PIL.Image.Image:

downsample = max(*(dim / thumb for dim, thumb in zip(self.dimensions, size)))
level = self.get_best_level_for_downsample(downsample)

# Debugging logs:
print(f"Downsample: {downsample}")
print(f"Level chosen: {level}")
print(f"Level dimensions at chosen level: {self.level_dimensions[level]}")

region = self.read_region((0, 0), level, self.level_dimensions[level])

# More debugging:
print(f"Region type: {type(region)}")

thumbnail = (
self.read_region((0, 0), level, self.level_dimensions[level])
.convert("RGB")
Expand Down
1 change: 0 additions & 1 deletion dlup/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding=utf-8
# Copyright (c) dlup contributors
"""DLUP Command-line interface. This is the file which builds the main parser."""
import argparse
Expand Down
2 changes: 1 addition & 1 deletion dlup/experimental_backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from dlup import UnsupportedSlideError

from .common import AbstractSlideBackend
from ..backends.common import AbstractSlideBackend
from .openslide_backend import OpenSlideSlide
from .pyvips_backend import PyVipsSlide
from .tifffile_backend import TifffileSlide
Expand Down
3 changes: 1 addition & 2 deletions dlup/experimental_backends/openslide_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding=utf-8
# Copyright (c) dlup contributors
from __future__ import annotations

Expand All @@ -8,7 +7,7 @@
import openslide
import PIL.Image

from dlup.experimental_backends.common import AbstractSlideBackend
from dlup.backends.common import AbstractSlideBackend
from dlup.types import PathLike
from dlup.utils.image import check_if_mpp_is_valid

Expand Down
3 changes: 1 addition & 2 deletions dlup/experimental_backends/pyvips_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding=utf-8
# Copyright (c) dlup contributors
from __future__ import annotations

Expand All @@ -11,7 +10,7 @@
import pyvips

from dlup import UnsupportedSlideError
from dlup.experimental_backends.common import AbstractSlideBackend, numpy_to_pil
from dlup.backends.common import AbstractSlideBackend, numpy_to_pil
from dlup.types import PathLike
from dlup.utils.image import check_if_mpp_is_valid

Expand Down
3 changes: 1 addition & 2 deletions dlup/experimental_backends/tifffile_backend.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# coding=utf-8
# Copyright (c) dlup contributors
from typing import Any

import numpy as np
import PIL.Image
import tifffile

from dlup.experimental_backends.common import AbstractSlideBackend, numpy_to_pil
from dlup.backends.common import AbstractSlideBackend, numpy_to_pil
from dlup.types import PathLike
from dlup.utils.tifffile_utils import get_tile

Expand Down
4 changes: 0 additions & 4 deletions dlup/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# coding=utf-8
# Copyright (c) dlup contributors
import json
import warnings
Expand All @@ -7,9 +6,6 @@

from dlup.utils.imports import _PYTORCH_AVAILABLE

if _PYTORCH_AVAILABLE:
import torch # type: ignore # pylint: disable=import-error


class ArrayEncoder(json.JSONEncoder):
def default(self, obj):
Expand Down
Empty file added tests/backends/__init__.py
Empty file.
115 changes: 115 additions & 0 deletions tests/backends/test_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Copyright (c) dlup contributors

import numpy as np
import pytest
from PIL import Image

from dlup.backends.common import AbstractSlideBackend, numpy_to_pil


def test_numpy_to_pil_single_channel():
arr = np.random.randint(0, 256, (10, 10, 1), dtype=np.uint8)
pil_img = numpy_to_pil(arr)
assert pil_img.mode == "L"


def test_numpy_to_pil_rgb():
arr = np.random.randint(0, 256, (10, 10, 3), dtype=np.uint8)
pil_img = numpy_to_pil(arr)
assert pil_img.mode == "RGB"


def test_numpy_to_pil_rgba():
arr = np.random.randint(0, 256, (10, 10, 4), dtype=np.uint8)
pil_img = numpy_to_pil(arr)
assert pil_img.mode == "RGBA"


def test_numpy_to_pil_invalid_channels():
arr = np.random.randint(0, 256, (10, 10, 5), dtype=np.uint8)
with pytest.raises(RuntimeError):
numpy_to_pil(arr)


class TestAbstractBackend:
class DummySlideBackend(AbstractSlideBackend):
# Minimal implementation to avoid the ABC restriction.
def __init__(self, filename: str):
super().__init__(filename)
# Dummy data for testing
self._level_count = 3
self._downsamples = [1.0, 2.0, 4.0]
self._spacings = [(0.5, 0.5), (1.0, 1.0), (2.0, 2.0)]
self._shapes = [(1000, 1000), (500, 500), (250, 250)]

def read_region(self, coordinates, level, size) -> Image.Image:
return Image.new("RGB", size, color="white")

@property
def properties(self):
return {}

@property
def magnification(self):
return 10.0

@property
def vendor(self):
return "TestVendor"

def close(self):
pass

def test_dummy_slide_backend_properties(self):
slide = self.DummySlideBackend("test_filename.tiff")

# Testing the level_count
assert slide.level_count == 3

# Testing the dimensions
assert slide.dimensions == (1000, 1000)

# Testing the spacing
assert slide.spacing == (0.5, 0.5)

# Testing the level_dimensions
assert slide.level_dimensions == [(1000, 1000), (500, 500), (250, 250)]

# Testing the level_spacings
assert slide.level_spacings == ((0.5, 0.5), (1.0, 1.0), (2.0, 2.0))

# Testing the level_downsamples
assert slide.level_downsamples == (1.0, 2.0, 4.0)

# Testing slide bounds
assert slide.slide_bounds == ((0, 0), (1000, 1000))

# Testing get_best_level_for_downsample
assert slide.get_best_level_for_downsample(0.5) == 0
assert slide.get_best_level_for_downsample(1.0) == 0
assert slide.get_best_level_for_downsample(2.0) == 1
assert slide.get_best_level_for_downsample(3.0) == 1
assert slide.get_best_level_for_downsample(4.5) == 2

def test_repr(self):
slide = self.DummySlideBackend("test_filename.tiff")
assert repr(slide) == "<DummySlideBackend(test_filename.tiff)>"

def test_spacing_without_set(self):
slide = self.DummySlideBackend("test_filename.tiff")
slide._spacings = None
assert slide.spacing is None

def test_get_thumbnail(self):
slide = self.DummySlideBackend("test_filename.tiff")

# Getting a 200x200 thumbnail
thumbnail = slide.get_thumbnail(200)
assert isinstance(thumbnail, Image.Image)
assert thumbnail.size == (200, 200)

# Getting a 300x150 thumbnail
thumbnail = slide.get_thumbnail((300, 150))
assert isinstance(thumbnail, Image.Image)
# The aspect ratio should be preserved, so width might be less than 300
assert thumbnail.size[1] == 150
40 changes: 40 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import argparse
import pathlib
from argparse import ArgumentTypeError
from unittest.mock import patch

import pytest

from dlup.cli import dir_path, file_path, main


def test_dir_path_valid_directory(tmpdir):
path = tmpdir.mkdir("subdir")
assert dir_path(str(path)) == pathlib.Path(path)


def test_dir_path_invalid_directory():
with pytest.raises(ArgumentTypeError):
dir_path("/path/which/does/not/exist")


def test_file_path_valid_file(tmpdir):
path = tmpdir.join("test_file.txt")
path.write("content")
assert file_path(str(path)) == pathlib.Path(path)


def test_file_path_invalid_file():
with pytest.raises(ArgumentTypeError):
file_path("/path/which/does/not/exist.txt")


def test_file_path_no_need_exists():
_path = "/path/which/does/not/need/to/exist.txt"
assert file_path(_path, need_exists=False) == pathlib.Path(_path)


def test_main_no_arguments(capsys):
with patch("sys.argv", ["dlup"]):
with pytest.raises(SystemExit):
main()
44 changes: 44 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import logging

import pytest

from dlup.logging import build_cli_logger, setup_logging


@pytest.mark.usefixtures("caplog")
class TestLogging:
def test_setup_logging_valid_log_level(self, caplog):
setup_logging(log_level="DEBUG")
assert len(caplog.records) == 0

def test_setup_logging_invalid_log_level(self):
with pytest.raises(ValueError, match="Unexpected log level got INVALID"):
setup_logging(log_level="INVALID")

def test_setup_logging_filename_creation(self, tmp_path):
log_file = tmp_path / "log.txt"
setup_logging(filename=log_file)
assert log_file.exists()

def test_setup_logging_log_message(self, caplog):
setup_logging(log_level="DEBUG")
logging.debug("This is a debug message.")
assert caplog.records[0].message == "This is a debug message."

@pytest.mark.usefixtures("tmp_path")
class TestCLILogger:
def test_build_cli_logger_filename_creation(self, tmp_path):
build_cli_logger("test_logger", True, 1, tmp_path)
assert any(tmp_path.iterdir()) # checks if any file is created in tmp_path

def test_build_cli_logger_valid_verbosity(self, caplog):
build_cli_logger("test_logger", True, 1)
logging.info("This is an info message.")
assert caplog.records[-1].message == "This is an info message."

def test_build_cli_logger_warning_message(self, caplog):
build_cli_logger("test_logger", True, 1)
assert (
caplog.records[0].message
== "Beta software. In case you run into issues report at https://github.com/NKI-AI/dlup/."
)
30 changes: 30 additions & 0 deletions tests/utils/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import json

import numpy as np
import pytest

from dlup.utils import ArrayEncoder


class TestArrayEncoder:
def test_encode_numpy_array(self):
arr = np.array([1, 2, 3, 4, 5])
result = json.dumps(arr, cls=ArrayEncoder)
assert result == "[1, 2, 3, 4, 5]"

def test_large_numpy_array_warning(self):
large_arr = np.zeros(int(10e4 + 1))
with pytest.warns(UserWarning, match=r"Trying to JSON serialize a very large array"):
json.dumps(large_arr, cls=ArrayEncoder)

def test_encode_numpy_integers(self):
int32_val = np.int32(42)
int64_val = np.int64(42)
result_int32 = json.dumps(int32_val, cls=ArrayEncoder)
result_int64 = json.dumps(int64_val, cls=ArrayEncoder)
assert result_int32 == "42"
assert result_int64 == "42"

def test_unhandled_data_type(self):
with pytest.raises(TypeError, match=r"Object of type .* is not JSON serializable"):
json.dumps({"key": object()}, cls=ArrayEncoder) # Here `object()` is an unhandled data type

0 comments on commit 3bb96c2

Please sign in to comment.