Skip to content

Commit 5b2ca82

Browse files
authored
Merge pull request #171 from will-moore/integration_tests
Initial addition of integration tests
2 parents 7a2c812 + 541d92a commit 5b2ca82

File tree

8 files changed

+221
-18
lines changed

8 files changed

+221
-18
lines changed

.github/workflows/omero_plugin.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Install and test and OMERO plugin e.g. a Web app, a CLI plugin or a library
2+
#
3+
# This workflow will install omero-test-infra, start an OMERO environment
4+
# including database, server and web deployment, configure the OMERO plugin
5+
# and run integration tests.
6+
#
7+
# 1. Set up the stage variable depending on the plugin. Supported stages
8+
# are: app, cli, scripts, lib, srv
9+
#
10+
# 2. Adjust the cron schedule as necessary
11+
12+
name: OMERO
13+
on:
14+
push:
15+
pull_request:
16+
schedule:
17+
- cron: '0 0 * * 0'
18+
19+
jobs:
20+
test:
21+
name: Run integration tests against OMERO
22+
runs-on: ubuntu-22.04
23+
env:
24+
STAGE: cli
25+
steps:
26+
- uses: actions/checkout@v4
27+
- name: Checkout omero-test-infra
28+
uses: actions/checkout@master
29+
with:
30+
repository: ome/omero-test-infra
31+
path: .omero
32+
ref: ${{ secrets.OMERO_TEST_INFRA_REF }}
33+
- name: Build and run OMERO tests
34+
run: .omero/docker $STAGE

.github/workflows/precommit.yml

Lines changed: 0 additions & 11 deletions
This file was deleted.

.isort.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[settings]
2-
known_third_party = dask,numpy,ome_zarr,omero,omero_zarr,setuptools,skimage,zarr
2+
known_third_party = dask,numpy,ome_zarr,omero,omero_rois,omero_zarr,pytest,setuptools,skimage,zarr

.omeroci/py-setup

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
3+
. ${TARGET}/.omero/py-setup # Run upstream setup
4+
pip install omero-rois

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,5 @@ def get_long_description() -> str:
5757
url="https://github.com/ome/omero-cli-zarr/",
5858
setup_requires=["setuptools_scm==7.1.0"],
5959
use_scm_version={"write_to": "src/omero_zarr/_version.py"},
60+
tests_require=["omero-py>=5.18.0", "pytest", "omero-rois"],
6061
)

src/omero_zarr/masks.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import argparse
2020
import logging
21+
import os
2122
import re
2223
import time
2324
from collections import defaultdict
@@ -184,6 +185,7 @@ def image_shapes_to_zarr(
184185
args.style,
185186
args.source_image,
186187
args.overlaps,
188+
args.output,
187189
)
188190

189191
if args.style == "split":
@@ -218,6 +220,7 @@ def __init__(
218220
style: str = "labeled",
219221
source: str = "..",
220222
overlaps: str = "error",
223+
output: Optional[str] = None,
221224
) -> None:
222225
self.dtype = dtype
223226
self.path = path
@@ -226,6 +229,7 @@ def __init__(
226229
self.plate = plate
227230
self.plate_path = Optional[str]
228231
self.overlaps = overlaps
232+
self.output = output
229233
if image:
230234
self.image = image
231235
self.size_t = image.getSizeT()
@@ -278,13 +282,14 @@ def save(self, masks: List[omero.model.Shape], name: str) -> None:
278282
# Figure out whether we can flatten some dimensions
279283
unique_dims: Dict[str, Set[int]] = {
280284
"T": {unwrap(mask.theT) for shapes in masks for mask in shapes},
281-
"C": {unwrap(mask.theC) for shapes in masks for mask in shapes},
282285
"Z": {unwrap(mask.theZ) for shapes in masks for mask in shapes},
283286
}
284287
ignored_dimensions: Set[str] = set()
288+
# We always ignore the C dimension
289+
ignored_dimensions.add("C")
285290
print(f"Unique dimensions: {unique_dims}")
286291

287-
for d in "TCZ":
292+
for d in "TZ":
288293
if unique_dims[d] == {None}:
289294
ignored_dimensions.add(d)
290295

@@ -306,13 +311,16 @@ def save(self, masks: List[omero.model.Shape], name: str) -> None:
306311
source_image = f"{source_image}/{self.plate_path}"
307312

308313
print(f"source_image {source_image}")
309-
src = parse_url(source_image)
310-
assert src, "Source image does not exist"
314+
image_path = source_image
315+
if self.output:
316+
image_path = os.path.join(self.output, source_image)
317+
src = parse_url(image_path)
318+
assert src, f"Source image does not exist at {image_path}"
311319
input_pyramid = Node(src, [])
312320
assert input_pyramid.load(Multiscales), "No multiscales metadata found"
313321
input_pyramid_levels = len(input_pyramid.data)
314322

315-
store = open_store(filename)
323+
store = open_store(image_path)
316324
root = open_group(store)
317325

318326
if self.plate:
@@ -321,9 +329,10 @@ def save(self, masks: List[omero.model.Shape], name: str) -> None:
321329
label_group = root
322330

323331
_mask_shape: List[int] = list(self.image_shape)
332+
mask_shape: Tuple[int, ...] = tuple(_mask_shape)
324333
for d in ignored_dimensions:
325334
_mask_shape[DIMENSION_ORDER[d]] = 1
326-
mask_shape: Tuple[int, ...] = tuple(_mask_shape)
335+
mask_shape = tuple(_mask_shape)
327336
del _mask_shape
328337
print(f"Ignoring dimensions {ignored_dimensions}")
329338

test/integration/clitest/__init__.py

Whitespace-only changes.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python
2+
3+
#
4+
# Copyright (C) 2025 University of Dundee & Open Microscopy Environment.
5+
# All rights reserved.
6+
#
7+
# This program is free software; you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation; either version 2 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# This program is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License along
18+
# with this program; if not, write to the Free Software Foundation, Inc.,
19+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20+
21+
import json
22+
from pathlib import Path
23+
24+
import pytest
25+
from omero.model import RoiI
26+
from omero.testlib.cli import AbstractCLITest
27+
from omero_rois import mask_from_binary_image
28+
from omero_zarr.cli import ZarrControl
29+
30+
31+
class TestRender(AbstractCLITest):
32+
33+
def setup_method(self, method: str) -> None:
34+
"""Set up the test."""
35+
self.args = self.login_args()
36+
self.cli.register("zarr", ZarrControl, "TEST")
37+
self.args += ["zarr"]
38+
39+
# export tests
40+
# ========================================================================
41+
42+
def test_export_zarr(self, capsys: pytest.CaptureFixture, tmp_path: Path) -> None:
43+
"""Test export of a Zarr image."""
44+
sizec = 2
45+
images = self.import_fake_file(sizeC=sizec, client=self.client)
46+
img_id = images[0].id.val
47+
self.cli.invoke(
48+
self.args + ["export", f"Image:{img_id}", "--output", str(tmp_path)],
49+
strict=True,
50+
)
51+
out, err = capsys.readouterr()
52+
lines = out.split("\n")
53+
print(lines)
54+
all_lines = ", ".join(lines)
55+
assert "Exporting to" in all_lines
56+
assert "Finished" in all_lines
57+
58+
assert len(list(tmp_path.iterdir())) == 1
59+
assert (tmp_path / f"{img_id}.zarr").is_dir()
60+
61+
attrs_text = (tmp_path / f"{img_id}.zarr" / ".zattrs").read_text(
62+
encoding="utf-8"
63+
)
64+
attrs_json = json.loads(attrs_text)
65+
print(attrs_json)
66+
assert "multiscales" in attrs_json
67+
assert len(attrs_json["omero"]["channels"]) == sizec
68+
assert attrs_json["omero"]["channels"][0]["window"]["min"] == 0
69+
assert attrs_json["omero"]["channels"][0]["window"]["max"] == 255
70+
71+
arr_text = (tmp_path / f"{img_id}.zarr" / "0" / ".zarray").read_text(
72+
encoding="utf-8"
73+
)
74+
arr_json = json.loads(arr_text)
75+
assert arr_json["shape"] == [sizec, 512, 512]
76+
77+
def test_export_plate(self, capsys: pytest.CaptureFixture, tmp_path: Path) -> None:
78+
79+
plates = self.import_plates(
80+
client=self.client,
81+
plates=1,
82+
plate_acqs=1,
83+
plate_cols=2,
84+
plate_rows=2,
85+
fields=1,
86+
)
87+
plate_id = plates[0].id.val
88+
self.cli.invoke(
89+
self.args + ["export", f"Plate:{plate_id}", "--output", str(tmp_path)],
90+
strict=True,
91+
)
92+
out, err = capsys.readouterr()
93+
lines = out.split("\n")
94+
print(lines)
95+
all_lines = ", ".join(lines)
96+
assert "Exporting to" in all_lines
97+
assert "Finished" in all_lines
98+
assert (tmp_path / f"{plate_id}.zarr").is_dir()
99+
attrs_text = (tmp_path / f"{plate_id}.zarr" / ".zattrs").read_text(
100+
encoding="utf-8"
101+
)
102+
attrs_json = json.loads(attrs_text)
103+
print(attrs_json)
104+
assert len(attrs_json["plate"]["wells"]) == 4
105+
assert attrs_json["plate"]["rows"] == [{"name": "A"}, {"name": "B"}]
106+
assert attrs_json["plate"]["columns"] == [{"name": "1"}, {"name": "2"}]
107+
108+
arr_text = (
109+
tmp_path / f"{plate_id}.zarr" / "A" / "1" / "0" / "0" / ".zarray"
110+
).read_text(encoding="utf-8")
111+
arr_json = json.loads(arr_text)
112+
assert arr_json["shape"] == [512, 512]
113+
114+
def test_export_masks(self, capsys: pytest.CaptureFixture, tmp_path: Path) -> None:
115+
"""Test export of a Zarr image."""
116+
images = self.import_fake_file(sizeC=2, client=self.client)
117+
img_id = images[0].id.val
118+
size_xy = 512
119+
120+
# Create a mask
121+
from skimage.data import binary_blobs
122+
123+
blobs = binary_blobs(length=size_xy, volume_fraction=0.1, n_dim=2).astype(
124+
"int8"
125+
)
126+
red = [255, 0, 0, 255]
127+
mask = mask_from_binary_image(blobs, rgba=red, z=0, c=0, t=0)
128+
129+
roi = RoiI()
130+
roi.setImage(images[0])
131+
roi.addShape(mask)
132+
updateService = self.client.sf.getUpdateService()
133+
updateService.saveAndReturnObject(roi)
134+
135+
print("tmp_path", tmp_path)
136+
137+
img_args = [f"Image:{img_id}", "--output", str(tmp_path)]
138+
self.cli.invoke(
139+
self.args + ["export"] + img_args,
140+
strict=True,
141+
)
142+
143+
self.cli.invoke(
144+
self.args + ["masks"] + img_args,
145+
strict=True,
146+
)
147+
148+
out, err = capsys.readouterr()
149+
lines = out.split("\n")
150+
print(lines)
151+
all_lines = ", ".join(lines)
152+
assert "Exporting to" in all_lines
153+
assert "Finished" in all_lines
154+
assert "Found 1 mask shapes in 1 ROIs" in all_lines
155+
156+
labels_text = (
157+
tmp_path / f"{img_id}.zarr" / "labels" / "0" / ".zattrs"
158+
).read_text(encoding="utf-8")
159+
labels_json = json.loads(labels_text)
160+
assert labels_json["image-label"]["colors"] == [{"label-value": 1, "rgba": red}]
161+
162+
arr_text = (
163+
tmp_path / f"{img_id}.zarr" / "labels" / "0" / "0" / ".zarray"
164+
).read_text(encoding="utf-8")
165+
arr_json = json.loads(arr_text)
166+
assert arr_json["shape"] == [1, 512, 512]

0 commit comments

Comments
 (0)