Skip to content

Commit

Permalink
Clean up tests and reorganize test artifacts (#58)
Browse files Browse the repository at this point in the history
* reorganize test artifacts, refactor tests, add more fixtures

* readme edits

* Update tests/test_reader.py

Co-authored-by: Evan Kiefl <[email protected]>
Signed-off-by: Keith Cheveralls <[email protected]>

* address comments

---------

Signed-off-by: Keith Cheveralls <[email protected]>
Co-authored-by: Evan Kiefl <[email protected]>
  • Loading branch information
keithchev and ekiefl authored Sep 18, 2024
1 parent 02b9bd4 commit 9d338e2
Show file tree
Hide file tree
Showing 17 changed files with 246 additions and 143 deletions.
37 changes: 37 additions & 0 deletions tests/artifacts/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Test artifacts

This directory contains test artifacts for the `readlif` package. These artifacts consist of example LIF files from various sources. In some cases, they are paired with expected outputs in the form of TIFF files containing the image data for selected 2D (X-Y) planes in the corresponding LIF file. In these cases, there is one TIFF file for each plane, and the TIFF files are named according to the plane they represent.

## About the LIF files

These are brief informal notes about the contents of the LIF files in this directory. They were determined by manually opening the LIF files in Fiji using the BioFormats plugin and examining the metadata.

### `xyzt-example/xyzt-example.lif`

Contains a single image. 1024 x 1024. 8-bit. Shape: 2C, 3T, 3Z.

### `xz-example/xz-example.lif`

Contains three images. All three are XZ in the first two dimensions.

BioFormats labels:

- xzt: 128 x 128; 2C x 20T
- xzy: 128 x 128; 2C x 23Z (note: the 'y' dimension in 'xzy' is loaded as Z by BioFormats)
- xz: 512 x 512; 2C.

### `misc/LeicaLASX_wavelength-sweep_example.lif`

Contains three images. All 64 x 64. The time dimension appears to be a Lambda scan but the metadata implies it was a time dimension (it contains `Plane` elements with `"deltaT"` attributes). BioFormats loads the images as XYZT.

The BioFormats labels of the three images are (in pseudo-regex):

```
x y (lambdaEmi|lambdaExc): 64x64; (20T|15T|(11Z x 20T))
```

### `misc/new_lasx.lif`

Contains a single image. 1024 x 1024. 8-bit. Shape: 2C, 39Z.

This file was added in #36; there is no other documentation of what it contains.
File renamed without changes.
Binary file added tests/artifacts/misc/valid-tiff.tif
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
57 changes: 57 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pathlib

import pytest


@pytest.fixture
def artifacts_dirpath():
return pathlib.Path(__file__).parent / "artifacts"


@pytest.fixture
def valid_lif_filepath(artifacts_dirpath):
"""
The filepath to a LIF file that is valid and can be read by the reader.
"""
return artifacts_dirpath / "misc" / "new_lasx.lif"


@pytest.fixture
def valid_single_image_lif_filepath(artifacts_dirpath):
"""
The filepath to a LIF file that contains only one image.
"""
return artifacts_dirpath / "xyzt-example" / "xyzt-example.lif"


@pytest.fixture
def valid_multi_image_lif_filepath(artifacts_dirpath):
"""
The filepath to a LIF file that contains multiple images.
"""
return artifacts_dirpath / "xz-example" / "xz-example.lif"


@pytest.fixture
def valid_tiff_filepath(artifacts_dirpath):
"""
The filepath to a TIFF file that is valid.
"""
return artifacts_dirpath / "misc" / "valid-tiff.tif"


@pytest.fixture
def xyzt_example_lif_filepath(artifacts_dirpath):
"""
The filepath to a LIF file that contains XYZT data.
"""
return artifacts_dirpath / "xyzt-example" / "xyzt-example.lif"


@pytest.fixture
def xz_example_lif_filepath(artifacts_dirpath):
"""
The filepath to a LIF file that contains an image in which the second dimension
is Z rather than Y.
"""
return artifacts_dirpath / "xz-example" / "xz-example.lif"
146 changes: 146 additions & 0 deletions tests/test_reader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import pytest
from PIL import Image

from readlif.reader import LifFile


def test_lif_file_with_file_path(valid_lif_filepath):
lif_image = LifFile(valid_lif_filepath).get_image(0)
# This should not raise an error.
lif_image.get_frame(z=0, t=0, c=0)


def test_lif_file_with_file_buffer(valid_lif_filepath):
with open(valid_lif_filepath, "rb") as file:
lif_image = LifFile(file).get_image(0)
# This should not raise an error.
lif_image.get_frame(z=0, t=0, c=0)


def test_lif_file_with_non_lif_files(tmp_path, artifacts_dirpath):
with open(tmp_path / "not-a-lif-file.txt", "w") as file:
with pytest.raises(ValueError):
LifFile(file)

with pytest.raises(ValueError):
LifFile(artifacts_dirpath / "misc" / "valid-tiff.tif")


def test_lif_file_with_single_image_lif(valid_single_image_lif_filepath):
lif_file = LifFile(valid_single_image_lif_filepath)
assert repr(lif_file) == "'LifFile object with 1 image'"
assert len(lif_file.image_list) == 1
with pytest.raises(ValueError):
lif_file.get_image(1)


def test_lif_file_get_iter_image(valid_single_image_lif_filepath, valid_multi_image_lif_filepath):
images = [image for image in LifFile(valid_single_image_lif_filepath).get_iter_image()]
assert len(images) == 1

images = [image for image in LifFile(valid_multi_image_lif_filepath).get_iter_image()]
assert len(images) > 1


def test_lif_file_with_new_lasx(artifacts_dirpath):
"""
TODO(KC): figure out what is special or "new" about the "new_lasx.lif" file.
"""
lif_file = LifFile(artifacts_dirpath / "misc" / "new_lasx.lif")
assert len(lif_file.image_list) == 1


def test_lif_image_get_frame_out_of_range_args(xyzt_example_lif_filepath):
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)

for dimension_kwarg, lif_image_attr in [
("z", "nz"),
("t", "nt"),
("c", "channels"),
("m", "n_mosaic"),
]:
max_index_for_dimension = getattr(lif_image, lif_image_attr) - 1
lif_image.get_frame(**{dimension_kwarg: max_index_for_dimension})
with pytest.raises(ValueError):
lif_image.get_frame(**{dimension_kwarg: max_index_for_dimension + 1})

# TODO: use a better-justified out of range index for this test.
with pytest.raises(ValueError):
lif_image._get_item(100)


def test_lif_image_settings(xz_example_lif_filepath):
"""
TODO: expand this test to cover more attributes and use more than one LIF file.
"""
lif_image = LifFile(xz_example_lif_filepath).get_image(0)
assert lif_image.settings["ObjectiveNumber"] == "11506353"


def test_lif_image_attributes(xyzt_example_lif_filepath):
"""
TODO: expand this test to cover more attributes and use more than one LIF file.
"""
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)
assert lif_image.scale[0] == pytest.approx(9.8709062997224)
assert lif_image.bit_depth[0] == 8


def test_lif_image_get_frame(xyzt_example_lif_filepath):
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)
czt_pairs = [[0, 0, 0], [0, 2, 0], [0, 2, 2], [1, 0, 0]]
for c, z, t in czt_pairs:
reference_image = Image.open(
xyzt_example_lif_filepath.parent / "expected-outputs" / f"c{c}z{z}t{t}.tif"
)
lif_image_frame = lif_image.get_frame(z=z, t=t, c=c)
assert lif_image_frame.tobytes() == reference_image.tobytes()


def test_lif_image_get_plane(xyzt_example_lif_filepath):
"""
TODO: eliminate duplication with `test_lif_image_get_frame`.
"""
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)
czt_pairs = [[0, 0, 0], [0, 2, 0], [0, 2, 2], [1, 0, 0]]
for c, z, t in czt_pairs:
reference_image = Image.open(
xyzt_example_lif_filepath.parent / "expected-outputs" / f"c{c}z{z}t{t}.tif"
)
lif_image_plane = lif_image.get_plane(c=c, requested_dims={3: z, 4: t})
assert lif_image_plane.tobytes() == reference_image.tobytes()


def test_lif_image_get_plane_on_xz_image(xz_example_lif_filepath):
lif_image = LifFile(xz_example_lif_filepath).get_image(0)
for c, t in [(0, 0), (1, 8)]:
reference_image = Image.open(
xz_example_lif_filepath.parent / "expected-outputs" / f"c{c}t{t}.tif"
)
lif_image_plane = lif_image.get_plane(c=c, requested_dims={4: t})
assert lif_image_plane.tobytes() == reference_image.tobytes()


def test_lif_image_get_plane_nonexistent_plane(artifacts_dirpath):
"""
TODO: determine if another, less mysterious LIF file can be used for this test.
"""
lif_image = LifFile(
artifacts_dirpath / "misc" / "LeicaLASX_wavelength-sweep_example.lif"
).get_image(0)
with pytest.raises(NotImplementedError):
lif_image.get_plane(display_dims=(1, 5), c=0, requested_dims={2: 31})


def test_lif_image_repr(xyzt_example_lif_filepath):
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)
assert (
repr(lif_image) == "'LifImage object with dimensions: Dims(x=1024, y=1024, z=3, t=3, m=1)'"
)


def test_lif_image_get_iters(xyzt_example_lif_filepath):
lif_image = LifFile(xyzt_example_lif_filepath).get_image(0)
assert len([image for image in lif_image.get_iter_c()]) == 2
assert len([image for image in lif_image.get_iter_t()]) == 3
assert len([image for image in lif_image.get_iter_z()]) == 3
143 changes: 0 additions & 143 deletions tests/test_readlif.py

This file was deleted.

6 changes: 6 additions & 0 deletions tests/test_utilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from readlif.utilities import get_xml


def test_get_xml_header(xyzt_example_lif_filepath):
_, test = get_xml(xyzt_example_lif_filepath)
assert test.startswith('<LMSDataContainerHeader Version="2">')

0 comments on commit 9d338e2

Please sign in to comment.