-
Notifications
You must be signed in to change notification settings - Fork 47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable GUI tests in CI #205
Comments
Testing napari plugins
Github actions CIThe complications around GUI testing in the GitHub actions CI can be solved by adding two actions to the workflow yaml file: Setup Qt librariesFirst, to set up the Qt graphical libraries on the remote CI machine, we can use Talley Lambert's tlambert03/setup-qt-libs action. Click here to see the exact line where it is used in the napari workflow file - uses: tlambert03/setup-qt-libs@v1 Run headless GUI during testingThen, in the step where the tests are run, we can use Ashley Anderson's aganders3/headless-gui action. This gives us a virtual display for the tests. Click here to see the exact line where it is used in the napari workflow file - uses: aganders3/headless-gui@v1 Workflow YAML fileWe'll use this section of the napari yaml workflow to base ours on. It can likely be simplified a lot for our use. |
Ok here's a demo you can try.
test_gui.py: import skimage.data
from micro_sam.sam_annotator import annotator_2d
from micro_sam.sam_annotator.annotator_2d import _initialize_viewer
def test_something_with_a_viewer(make_napari_viewer_proxy):
"""The demo example from the napari plugin testing docs."""
viewer = make_napari_viewer_proxy()
# carry on with your test
image = skimage.data.chelsea()
viewer.add_image(image, name="raw")
viewer.close()
def test_annotator_2d(make_napari_viewer_proxy, tmp_path):
"""Integration test for annotator_2d widget."""
model_type = "vit_b"
image = skimage.data.camera()
embedding_path = tmp_path / "test-embedding.zarr"
viewer = make_napari_viewer_proxy()
viewer = _initialize_viewer(image, None, None, None) # TODO: fix hacky workaround
# test generating image embedding, then adding micro-sam dock widgets to the GUI
viewer = annotator_2d(
image,
embedding_path,
show_embeddings=False,
model_type=model_type,
v=viewer,
return_viewer=True
)
assert len(viewer.layers) == 6
expected_layer_names = ['raw', 'auto_segmentation', 'committed_objects', 'current_object', 'point_prompts', 'prompts']
for layername in expected_layer_names:
assert layername in viewer.layers
# ... continue if you want to actually segment, commit, or clear annotatons
viewer.close() # must close the viewer at the end of tests |
👍
Thanks! I will give this a try later and then give some more feedback. |
Hi @GenevieveBuckley,
Regarding the test itself:
But we can iterate on the exact details of the test later (and I can also take over some of the actual functionality testing once we have a scaffold for the test and it runs in the CI). |
#214 is ready for review and merge now. It's not a blocker to work on this issue (but yes it is nice to have the test coverage information)
Good idea, will do.
I've just spent some time on this problem today.
Possible problems I've found:
|
Today I learned about pytest-gui, I'd always been using pytest-qt. That's cool to know! (Looks like it might also work for Tkinter, whereas pytest-qt is only for pyqt and pyside - I'll have to look at the docs more sometime) |
Ok, here's the work I did extending tests to actually run some of the annotation functions. Here's an example GUI test involving interaction with the annotation functions (click to expand)import numpy as np
import skimage.data
from micro_sam.sam_annotator import annotator_2d
from micro_sam.sam_annotator.annotator_2d import _initialize_viewer, _segment_widget
from micro_sam.sam_annotator.util import _clear_widget, _commit_segmentation_widget
def test_annotator_2d(make_napari_viewer_proxy, tmp_path):
"""Integration test for annotator_2d widget.
* Creates 2D image embedding
* Opens annotator_2d widget in napari
* Test point prompts (add points, segment object, clear, and commit)
* Test box prompt (add rectangle prompt, segment object, clear, and commit)
...
"""
model_type = "vit_t"
image = skimage.data.camera()
embedding_path = tmp_path / "test-embedding.zarr"
viewer = make_napari_viewer_proxy()
viewer = _initialize_viewer(image, None, None, None) # TODO: fix hacky workaround
# test generating image embedding, then adding micro-sam dock widgets to the GUI
viewer = annotator_2d(
image,
embedding_path,
show_embeddings=False,
model_type=model_type,
v=viewer,
return_viewer=True
)
# check the initial layer setup is correct
assert len(viewer.layers) == 6
expected_layer_names = ['raw', 'auto_segmentation', 'committed_objects', 'current_object', 'point_prompts', 'prompts']
for layername in expected_layer_names:
assert layername in viewer.layers
# Check layers are empty before beginning tests
np.testing.assert_equal(viewer.layers["auto_segmentation"].data, 0)
np.testing.assert_equal(viewer.layers["current_object"].data, 0)
np.testing.assert_equal(viewer.layers["committed_objects"].data, 0)
np.testing.assert_equal(viewer.layers["point_prompts"].data, 0)
assert viewer.layers["prompts"].data == [] # shape data is list, not numpy array
# ========================================================================
# TEST POINT PROMPTS
# Add three points in the sky region of the camera image
sky_point_prompts = np.array([[70, 80],[50, 320],[80, 470 ]])
viewer.layers["point_prompts"].data = sky_point_prompts
# Segment sky region of image
_segment_widget(v=viewer) # segment slice
# We expect roughly 25% of the image to be sky
sky_segmentation = np.copy(viewer.layers["current_object"].data)
segmented_pixel_percentage = (np.sum(sky_segmentation == 1) / image.size) * 100
assert segmented_pixel_percentage > 25
assert segmented_pixel_percentage < 30
# Clear segmentation current object and prompts
_clear_widget(v=viewer)
np.testing.assert_equal(viewer.layers["current_object"].data, 0)
np.testing.assert_equal(viewer.layers["point_prompts"].data, 0)
assert viewer.layers["prompts"].data == [] # shape data is list, not numpy array
# Repeat segmentation and commit segmentation result
viewer.layers["point_prompts"].data = sky_point_prompts
_segment_widget(v=viewer) # segment slice
np.testing.assert_equal(sky_segmentation, viewer.layers["current_object"].data)
# Commit segmentation
_commit_segmentation_widget(v=viewer)
np.testing.assert_equal(sky_segmentation, viewer.layers["committed_objects"].data)
# ========================================================================
# TEST BOX PROMPTS
# Add rechangle bounding box prompt
camera_bounding_box_prompt = np.array([[139, 254],[139, 324],[183, 324],[183, 254]])
viewer.layers["prompts"].data = [camera_bounding_box_prompt]
# Segment slice
_segment_widget(v=viewer) # segment slice
# Check segmentation results
camera_segmentation = np.copy(viewer.layers["current_object"].data)
segmented_pixels = np.sum(camera_segmentation == 1)
assert segmented_pixels > 2500 # we expect roughly 2770 pixels
assert segmented_pixels < 3000 # we expect roughly 2770 pixels
assert (camera_segmentation[150:175,275:310] == 1).all() # small patch which should definitely be inside segmentation
# Clear segmentation current object and prompts
_clear_widget(v=viewer)
np.testing.assert_equal(viewer.layers["current_object"].data, 0)
np.testing.assert_equal(viewer.layers["point_prompts"].data, 0)
assert viewer.layers["prompts"].data == [] # shape data is list, not numpy array
# Repeat segmentation and commit segmentation result
viewer.layers["prompts"].data = [camera_bounding_box_prompt]
_segment_widget(v=viewer) # segment slice
np.testing.assert_equal(camera_segmentation, viewer.layers["current_object"].data)
# Commit segmentation
_commit_segmentation_widget(v=viewer)
committed_objects = viewer.layers["committed_objects"].data
# We expect two committed objects
# label id 1: sky segmentation
# label id 2: camera segmentation
np.testing.assert_equal(np.unique(committed_objects), np.array([0, 1, 2]))
np.testing.assert_equal(committed_objects == 2, camera_segmentation == 1)
# ========================================================================
viewer.close() # must close the viewer at the end of tests Here's a GUI test involving automatic mask generation (click to expand)import numpy as np
from micro_sam.sam_annotator import annotator_2d
from micro_sam.sam_annotator.annotator_2d import _initialize_viewer, _autosegment_widget
def test_annotator_2d_amg(make_napari_viewer_proxy, tmp_path):
"""Integration test for annotator_2d widget with automatic mask generation.
* Creates 2D image embedding
* Opens annotator_2d widget in napari
* Test automatic mask generation
"""
model_type = "vit_t"
embedding_path = tmp_path / "test-embedding.zarr"
# example data - a basic checkerboard pattern
image = np.zeros((16,16))
image[:8,:8] = 1
image[8:,8:] = 1
viewer = make_napari_viewer_proxy()
viewer = _initialize_viewer(image, None, None, None) # TODO: fix hacky workaround
# test generating image embedding, then adding micro-sam dock widgets to the GUI
viewer = annotator_2d(
image,
embedding_path,
show_embeddings=False,
model_type=model_type,
v=viewer,
return_viewer=True
)
# check the initial layer setup is correct
assert len(viewer.layers) == 6
expected_layer_names = ['raw', 'auto_segmentation', 'committed_objects', 'current_object', 'point_prompts', 'prompts']
for layername in expected_layer_names:
assert layername in viewer.layers
# Check layers are empty before beginning tests
np.testing.assert_equal(viewer.layers["auto_segmentation"].data, 0)
np.testing.assert_equal(viewer.layers["current_object"].data, 0)
np.testing.assert_equal(viewer.layers["committed_objects"].data, 0)
np.testing.assert_equal(viewer.layers["point_prompts"].data, 0)
assert viewer.layers["prompts"].data == [] # shape data is list, not numpy array
# ========================================================================
# Automatic mask generation
_autosegment_widget(v=viewer, min_object_size=30)
# We expect four segmentation regions to be identified
expected_segmentation_label_ids = np.array([0,1,2,3])
np.testing.assert_equal(np.unique(viewer.layers["auto_segmentation"].data),
expected_segmentation_label_ids)
viewer.close() # must close the viewer at the end of tests I have also run into problems with errors when I run two GUI tests. I'm not entirely sure how to fix that, something funny might be going on with the widgets and garbage collection(?) |
Yes, the functions are covered by unit tests, but I think it makes sense to add an integration test. |
Hi @GenevieveBuckley ,
Let me know if you think we should go ahead differently, or if you run into any issues. |
This is implemented now. Thanks @GenevieveBuckley! |
It would help with all the refactoring tasks to enable tests for the GUI. Currently this is not possible, becaue napari cannot be imported in the Github CLI due to missing graphical libraries (QT, but might lack even more things like OpenGL).
The best strategy here would probably look into what napari does for the CI. (I tried this myself at some point, but gave up because the napari test suite is quite extensive and it was unclear what the best starting point is).
The text was updated successfully, but these errors were encountered: