Skip to content

Commit

Permalink
Increase test coverage for widgets (#8)
Browse files Browse the repository at this point in the history
This adds some basic tests to cover the sub-widgets that make up the
main data portal widget.

Note that coverage likely doesn't cover lines that are run via QThreads,
which explains some of the uncovered lines:
nedbat/coveragepy#582 (comment)

This also removes the preview widget, which isn't being actively used,
and ignores coverage of test code itself.
  • Loading branch information
andy-sweet committed Jun 20, 2023
1 parent 4f12593 commit b532fab
Show file tree
Hide file tree
Showing 15 changed files with 514 additions and 148 deletions.
10 changes: 7 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ napari.manifest =
[options.extras_require]
testing =
tox
pytest # https://docs.pytest.org/en/latest/contents.html
pytest-cov # https://pytest-cov.readthedocs.io/en/latest/
pytest-qt # https://pytest-qt.readthedocs.io/en/latest/
pytest
pytest-cov
pytest-mock
pytest-qt
napari
pyqt5

[options.package_data]
* = *.yaml

[coverage:run]
omit = **/_tests/**
2 changes: 1 addition & 1 deletion src/napari_cryoet_data_portal/_listing_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self.filter = QLineEdit()
self.filter.setPlaceholderText("Filter datasets and tomograms")
self.filter.setClearButtonEnabled(True)
self._progress = ProgressWidget(
self._progress: ProgressWidget = ProgressWidget(
work=self._loadDatasets,
yieldCallback=self._onDatasetLoaded,
)
Expand Down
2 changes: 1 addition & 1 deletion src/napari_cryoet_data_portal/_metadata_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def __init__(self, parent: Optional[QWidget] = None) -> None:
self._main = QSearchableTreeWidget()
self._main.layout().setContentsMargins(0, 0, 0, 0)
self._main.filter.setPlaceholderText("Filter metadata")
self._progress = ProgressWidget(
self._progress: ProgressWidget = ProgressWidget(
work=self._loadMetadata,
returnCallback=self._onMetadataLoaded,
)
Expand Down
2 changes: 1 addition & 1 deletion src/napari_cryoet_data_portal/_open_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(
self.resolution.addItem(res.name, res)
self.resolution.setCurrentText(LOW_RESOLUTION.name)
self.resolution_label.setBuddy(self.resolution)
self._progress = ProgressWidget(
self._progress: ProgressWidget = ProgressWidget(
work=self._loadTomogram,
yieldCallback=self._onLayerLoaded,
)
Expand Down
46 changes: 0 additions & 46 deletions src/napari_cryoet_data_portal/_preview_list_widget.py

This file was deleted.

88 changes: 0 additions & 88 deletions src/napari_cryoet_data_portal/_preview_widget.py

This file was deleted.

12 changes: 8 additions & 4 deletions src/napari_cryoet_data_portal/_progress_widget.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Callable, Generator, Generic, Optional, TypeVar, Union

from qtpy.QtCore import Signal
from qtpy.QtWidgets import (
QHBoxLayout,
QLabel,
Expand All @@ -24,20 +25,22 @@
class ProgressWidget(QWidget, Generic[YieldType, SendType, ReturnType]):
"""Shows progress and handles cancellation of a task."""

finished = Signal()

def __init__(
self,
*,
work: WorkType,
yieldCallback: YieldCallback = None,
returnCallback: ReturnCallback = None,
yieldCallback: Optional[YieldCallback] = None,
returnCallback: Optional[ReturnCallback] = None,
parent: Optional[QWidget] = None,
) -> None:
super().__init__(parent)

self._worker: Optional[TaskWorker] = None
self._work: WorkType = work
self._yieldCallback: YieldCallback = yieldCallback
self._returnCallback: ReturnCallback = returnCallback
self._yieldCallback: Optional[YieldCallback] = yieldCallback
self._returnCallback: Optional[ReturnCallback] = returnCallback

self._last_id: Optional[int] = None

Expand Down Expand Up @@ -101,6 +104,7 @@ def _onWorkerFinished(self, task_id: int) -> None:
if self._last_id == task_id:
self._worker = None
self._setLoaded()
self.finished.emit()

def _isTaskCancelled(self, task_id: int) -> bool:
if self._worker is None:
Expand Down
168 changes: 168 additions & 0 deletions src/napari_cryoet_data_portal/_tests/_mocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from typing import Any, Dict, Tuple

import numpy as np

from napari_cryoet_data_portal._model import Dataset, Tomogram


MOCK_S3_URI = 's3://mock-portal'


def mock_dataset(
*,
name: str,
tomograms: Tuple[Tomogram, ...],
) -> Dataset:
path = f"{MOCK_S3_URI}/{name}"
return Dataset(
name=name,
path=path,
tomograms=tomograms,
)


def mock_tomogram(
*,
dataset_name: str,
tomogram_name: str,
voxel_spacing: str,
annotation_names: Tuple[str, ...]
) -> Tomogram:
path = f"{MOCK_S3_URI}/{dataset_name}/{tomogram_name}"
tomogram_path = f"{path}/Tomograms/VoxelSpacing{voxel_spacing}"
image_path = f"{tomogram_path}/CanonicalTomogram/{tomogram_name}.zarr"
annotations_path = f"{tomogram_path}/Annotations"
annotations_paths = tuple(
f"{annotations_path}/{name}.json"
for name in annotation_names
)
return Tomogram(
name=tomogram_name,
path=path,
tomogram_path=tomogram_path,
image_path=image_path,
annotation_paths=annotations_paths,
)


MOCK_TOMOGRAM_TS_026 = mock_tomogram(
dataset_name="10000",
tomogram_name="TS_026",
voxel_spacing="13.48",
annotation_names=("ribosome", "fatty-acid-synthase"),
)

MOCK_TOMOGRAM_TS_026_METADATA = {
'run_name': 'TS_026',
'voxel_spacing': 13.48,
'size': {
'z': 8,
'y': 8,
'x': 8,
}
}

MOCK_TOMOGRAM_TS_026_IMAGE_DATA = [
np.zeros((8, 8, 8)),
np.zeros((4, 4, 4)),
np.zeros((2, 2, 2)),
]

MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS = {
'name': 'TS_026',
'scale': (1, 1, 1),
}

MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA = [[0, 0, 0], [2, 2, 2]]
MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS = {'name': 'ribosome'}

MOCK_TOMOGRAM_TS_026_FAS_DATA = [[1, 1, 1], [3, 3, 3]]
MOCK_TOMOGRAM_TS_026_FAS_ATTRS = {'name': 'fatty acid synthase'}

MOCK_TOMOGRAM_TS_027 = mock_tomogram(
dataset_name="10000",
tomogram_name="TS_027",
voxel_spacing="13.48",
annotation_names=("ribosome", "fatty-acid-synthase"),
)

MOCK_DATASET_10000 = mock_dataset(
name="10000",
tomograms=(MOCK_TOMOGRAM_TS_026, MOCK_TOMOGRAM_TS_027),
)

MOCK_DATASET_10000_METADATA = {
'dataset_title': 'mock dataset',
'authors': [
{
'name': 'mock author',
'ORCID': "0000-1111-2222-3333",
}
],
'organism': {
'name': 'mock organism',
}
}

MOCK_TOMOGRAM_POS_128 = mock_tomogram(
dataset_name="10004",
tomogram_name="Position_128_2",
voxel_spacing="7.56",
annotation_names=("ribosome"),
)

MOCK_TOMOGRAM_POS_129 = mock_tomogram(
dataset_name="10004",
tomogram_name="Position_129_2",
voxel_spacing="7.56",
annotation_names=("ribosome"),
)

MOCK_DATASET_10004 = mock_dataset(
name="10004",
tomograms=(MOCK_TOMOGRAM_POS_128, MOCK_TOMOGRAM_POS_129),
)

MOCK_DATASETS = (MOCK_DATASET_10000, MOCK_DATASET_10004)


def mock_path_exists(path: str) -> bool:
return path == MOCK_S3_URI


def mock_list_dir(path: str) -> Tuple[str, ...]:
if path == f'{MOCK_S3_URI}':
return tuple(ds.name for ds in MOCK_DATASETS)
for ds in MOCK_DATASETS:
if ds.path == path:
return tuple(tomo.name for tomo in ds.tomograms)
for tomo in ds.tomograms:
anno_path = f"{tomo.tomogram_path}/Annotations"
if path == anno_path:
return tuple(
p.split('/')[-1]
for p in tomo.annotation_paths
)
raise ValueError(f"Mock path not supported: {path}")


def mock_read_json(path: str) -> Dict[str, Any]:
if path == MOCK_DATASET_10000.metadata_path:
return MOCK_DATASET_10000_METADATA
if path == MOCK_TOMOGRAM_TS_026.tomogram_metadata_path:
return MOCK_TOMOGRAM_TS_026_METADATA
raise ValueError(f'Mock path not supported: {path}')


def mock_read_tomogram_ome_zarr(path: str) -> Dict[str, Any]:
if path == MOCK_TOMOGRAM_TS_026.image_path:
return MOCK_TOMOGRAM_TS_026_IMAGE_DATA, MOCK_TOMOGRAM_TS_026_IMAGE_ATTRS, "image"
raise ValueError(f'Mock path not supported: {path}')


def mock_read_points_annotations_json(path: str) -> Dict[str, Any]:
if path == MOCK_TOMOGRAM_TS_026.annotation_paths[0]:
return MOCK_TOMOGRAM_TS_026_RIBOSOME_DATA, MOCK_TOMOGRAM_TS_026_RIBOSOME_ATTRS, "points"
if path == MOCK_TOMOGRAM_TS_026.annotation_paths[1]:
return MOCK_TOMOGRAM_TS_026_FAS_DATA, MOCK_TOMOGRAM_TS_026_FAS_ATTRS, "points"
raise ValueError(f'Mock path not supported: {path}')
Loading

0 comments on commit b532fab

Please sign in to comment.