From 70fccd8313c136613d48bc7fa890f773a16f3cca Mon Sep 17 00:00:00 2001 From: Arjun Raj Date: Tue, 26 May 2026 06:18:05 -0400 Subject: [PATCH 1/3] fix(line_scan_worker): handle datasets without IndexRange Single-frame datasets (one channel, no Z/T/XY stacks) omit the IndexRange key from tiles entirely, causing KeyError: 'IndexRange' when "All channels" was checked. Use .get() with defaults to fall back to a single channel, mirroring the pattern used in crop and registration workers. Co-Authored-By: Claude Opus 4.7 (1M context) --- workers/properties/lines/line_scan_worker/entrypoint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/workers/properties/lines/line_scan_worker/entrypoint.py b/workers/properties/lines/line_scan_worker/entrypoint.py index 5429c78..81de416 100755 --- a/workers/properties/lines/line_scan_worker/entrypoint.py +++ b/workers/properties/lines/line_scan_worker/entrypoint.py @@ -77,7 +77,9 @@ def compute(datasetId, apiUrl, token, params): images.clear() # Clear the previous images if all_channels: - channels = range(datasetClient.tiles['IndexRange']['IndexC']) + # Single-frame datasets (one channel/Z/time/XY) omit IndexRange entirely. + num_channels = datasetClient.tiles.get('IndexRange', {}).get('IndexC', 1) + channels = range(num_channels) else: channels = [selected_channel] From 9446148c89b2ad1bb778af68bb633ab1b4220a53 Mon Sep 17 00:00:00 2001 From: Arjun Raj Date: Tue, 26 May 2026 06:19:14 -0400 Subject: [PATCH 2/3] chore: regenerate REGISTRY.md Required by pre-PR validation hook. The line_scan_worker entry itself is unchanged; this resyncs unrelated description drift from other workers that had been hand-edited. Co-Authored-By: Claude Opus 4.7 (1M context) --- REGISTRY.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/REGISTRY.md b/REGISTRY.md index ab9febd..870ed8d 100644 --- a/REGISTRY.md +++ b/REGISTRY.md @@ -23,8 +23,8 @@ Create new annotations by segmenting images or connecting existing annotations. |--------|-------------|-----|-------|------| | Claude natural language analyzer | Uses Claude AI to analyze images | | | [docs](workers/annotations/ai_analysis/AI_ANALYSIS.md) | | Cellori Segmentation | | Yes | | [docs](workers/annotations/cellori_segmentation/CELLORI_SEGMENTATION.md) | -| Cellpose | Uses Cellpose to find cells and nuclei | Yes | | [docs](workers/annotations/cellpose/CELLPOSE.md) | -| Cellpose retrain | Retrains Cellpose models based on user-selected images | Yes | | [docs](workers/annotations/cellpose_train/CELLPOSE_TRAIN.md) | +| Cellpose | This tool runs the Cellpose model to segment the image into cells. Learn more | Yes | | [docs](workers/annotations/cellpose/CELLPOSE.md) | +| Cellpose retrain | This tool trains a Cellpose model using user-corrected annotations. Learn more | Yes | | [docs](workers/annotations/cellpose_train/CELLPOSE_TRAIN.md) | | Cellpose-SAM | This tool runs the Cellpose-SAM model to segment the image into cells. Learn more | Yes | | [docs](workers/annotations/cellposesam/CELLPOSESAM.md) | | CondensateNet segmentation | Segments biomolecular condensates using CondensateNet deep learning model | Yes | | [docs](workers/annotations/condensatenet/CONDENSATENET.md) | | Connect Sequential | This tool connects objects sequentially across time or z-slices.It is useful for connec... | | Yes | [docs](workers/annotations/connect_sequential/CONNECT_SEQUENTIAL.md) | @@ -36,13 +36,13 @@ Create new annotations by segmenting images or connecting existing annotations. | Gaussian Blur | Applies Gaussian blur to images | | Yes | [docs](workers/annotations/gaussian_blur/GAUSSIAN_BLUR.md) | | H&E Deconvolution | Deconvolves H&E stains | | Yes | [docs](workers/annotations/h_and_e_deconvolution/H_AND_E_DECONVOLUTION.md) | | Histogram Matching | Corrects images using histogram matching | | Yes | [docs](workers/annotations/histogram_matching/HISTOGRAM_MATCHING.md) | -| Laplacian of Gaussian | Detects spots in images using the Laplacian of Gaussian method | | | [docs](workers/annotations/laplacian_of_gaussian/LAPLACIAN_OF_GAUSSIAN.md) | +| Laplacian of Gaussian | This tool finds spots in an image using the Laplacian of Gaussian method.It uses a filt... | | | [docs](workers/annotations/laplacian_of_gaussian/LAPLACIAN_OF_GAUSSIAN.md) | | Time lapse registration | Corrects images using time lapse registration | | Yes | [docs](workers/annotations/registration/REGISTRATION.md) | | Rolling Ball | Corrects images using a rolling ball | | Yes | [docs](workers/annotations/rolling_ball/ROLLING_BALL.md) | | SAM2 automatic mask generator | Uses SAM2 to find all masks in the image | Yes | | [docs](workers/annotations/sam2_automatic_mask_generator/SAM2_AUTOMATIC_MASK_GENERATOR.md) | | SAM2 few-shot segmentation | Uses SAM2 features for few-shot segmentation based on training annotations | Yes | Yes | [docs](workers/annotations/sam2_fewshot_segmentation/SAM2_FEWSHOT_SEGMENTATION.md) | -| SAM2 propagator | Uses SAM2 to propagate annotations through time or Z-slices | Yes | | [docs](workers/annotations/sam2_propagate/SAM2_PROPAGATE.md) | -| SAM2 Refiner | Uses SAM2 to refine annotations | Yes | | [docs](workers/annotations/sam2_refine/SAM2_REFINE.md) | +| SAM2 propagator | This tool uses the SAM2 model to take an existing annotation and propagate it through t... | Yes | | [docs](workers/annotations/sam2_propagate/SAM2_PROPAGATE.md) | +| SAM2 Refiner | This tool uses the SAM2 model to refine existing annotations. It takes existing annotat... | Yes | | [docs](workers/annotations/sam2_refine/SAM2_REFINE.md) | | SAM2 video | Uses SAM2 to track video through time or Z | Yes | | [docs](workers/annotations/sam2_video/SAM2_VIDEO.md) | | SAM automatic mask generator | Uses SAM to find all masks in the image | Yes | | [docs](workers/annotations/sam_automatic_mask_generator/SAM_AUTOMATIC_MASK_GENERATOR.md) | | SAM few-shot segmentation | Uses SAM1 ViT-H features for few-shot segmentation based on training annotations | Yes | Yes | [docs](workers/annotations/sam_fewshot_segmentation/SAM_FEWSHOT_SEGMENTATION.md) | @@ -54,16 +54,16 @@ Compute properties on polygon / blob annotations. | Worker | Description | Tests | Docs | |--------|-------------|-------|------| -| Annulus intensity percentile measurements | Compute intensity at a specific percentile for an annular region around blobs | Yes | [docs](workers/properties/blobs/blob_annulus_intensity_percentile_worker/BLOB_ANNULUS_INTENSITY_PERCENTILE_WORKER.md) | -| Annulus intensity measurements | Compute intensity measurements (mean, median, etc.) for an annular region around blobs | Yes | [docs](workers/properties/blobs/blob_annulus_intensity_worker/BLOB_ANNULUS_INTENSITY_WORKER.md) | +| Annulus intensity percentile measurements | This tool computes the pixel intensity in an annulus around the objects in the specifie... | Yes | [docs](workers/properties/blobs/blob_annulus_intensity_percentile_worker/BLOB_ANNULUS_INTENSITY_PERCENTILE_WORKER.md) | +| Annulus intensity measurements | This tool computes the pixel intensity in an annulus around the objects in the specifie... | Yes | [docs](workers/properties/blobs/blob_annulus_intensity_worker/BLOB_ANNULUS_INTENSITY_WORKER.md) | | Colony two color intensity | Compute intensity across two channels for blobs; useful for colony intensity measurements | | [docs](workers/properties/blobs/blob_colony_two_color_intensity_worker/BLOB_COLONY_TWO_COLOR_INTENSITY_WORKER.md) | -| Blob intensity percentile measurements | Compute intensity at a specific percentile for blobs | Yes | [docs](workers/properties/blobs/blob_intensity_percentile_worker/BLOB_INTENSITY_PERCENTILE_WORKER.md) | -| Blob intensity measurements | Compute intensity measurements (mean, median, etc.) for blobs | Yes | [docs](workers/properties/blobs/blob_intensity_worker/BLOB_INTENSITY_WORKER.md) | -| Blob metrics | Compute area, perimeter, and other metrics for blobs | Yes | [docs](workers/properties/blobs/blob_metrics_worker/BLOB_METRICS_WORKER.md) | -| Blob overlap | Compute overlap between blobs | Yes | [docs](workers/properties/blobs/blob_overlap_worker/BLOB_OVERLAP_WORKER.md) | +| Blob intensity percentile measurements | This tool computes the pixel intensity of objects in the specified channel at the speci... | Yes | [docs](workers/properties/blobs/blob_intensity_percentile_worker/BLOB_INTENSITY_PERCENTILE_WORKER.md) | +| Blob intensity measurements | This tool computes the pixel intensity of objects in the specified channel. It will com... | Yes | [docs](workers/properties/blobs/blob_intensity_worker/BLOB_INTENSITY_WORKER.md) | +| Blob metrics | This tool computes a variety of metrics for the objects in the specified channel. The m... | Yes | [docs](workers/properties/blobs/blob_metrics_worker/BLOB_METRICS_WORKER.md) | +| Blob overlap | This tool computes the overlaps between two sets of annotations. The overlap is compute... | Yes | [docs](workers/properties/blobs/blob_overlap_worker/BLOB_OVERLAP_WORKER.md) | | Point count 3D projection | Count points in a 3D projection | | [docs](workers/properties/blobs/blob_point_count_3D_projection_worker/BLOB_POINT_COUNT_3D_PROJECTION_WORKER.md) | -| Point count | Count the number of points that fall within a blob/polygon; can work in 2D or 3D | Yes | [docs](workers/properties/blobs/blob_point_count_worker/BLOB_POINT_COUNT_WORKER.md) | -| Random Forest Classifier | Classify blobs using a Random Forest Classifier | Yes | [docs](workers/properties/blobs/blob_random_forest_classifier/BLOB_RANDOM_FOREST_CLASSIFIER.md) | +| Point count | This tool counts the number of points within polygon annotations. The points will be of... | Yes | [docs](workers/properties/blobs/blob_point_count_worker/BLOB_POINT_COUNT_WORKER.md) | +| Random Forest Classifier | This tool uses a Random Forest Classifier to classify blobs. | Yes | [docs](workers/properties/blobs/blob_random_forest_classifier/BLOB_RANDOM_FOREST_CLASSIFIER.md) | ## Property Workers -- Points @@ -72,9 +72,9 @@ Compute properties on point annotations. | Worker | Description | Tests | Docs | |--------|-------------|-------|------| | Point circle intensity | Computes mean intensity metrics around a point; radius 1 for pixel precision | | [docs](workers/properties/points/point_circle_intensity_mean_worker/POINT_CIRCLE_INTENSITY_MEAN_WORKER.md) | -| Point intensity | Computes a variety of intensity metrics around a point; radius 1 for pixel precision | Yes | [docs](workers/properties/points/point_circle_intensity_worker/POINT_CIRCLE_INTENSITY_WORKER.md) | +| Point intensity | This tool computes the average, maximum, minimum, median, total, 25th percentile, and 7... | Yes | [docs](workers/properties/points/point_circle_intensity_worker/POINT_CIRCLE_INTENSITY_WORKER.md) | | Point Intensity Worker | | | [docs](workers/properties/points/point_intensity_worker/POINT_INTENSITY_WORKER.md) | -| Point metrics | Computes a variety of metrics for points, like X and Y coordinates | Yes | [docs](workers/properties/points/point_metrics_worker/POINT_METRICS_WORKER.md) | +| Point metrics | This tool adds a property to the points to document their coordinates. It gives each po... | Yes | [docs](workers/properties/points/point_metrics_worker/POINT_METRICS_WORKER.md) | | Point Threshold Intensity Mean Worker | | | [docs](workers/properties/points/point_threshold_intensity_mean_worker/POINT_THRESHOLD_INTENSITY_MEAN_WORKER.md) | | Distance to nearest blob | | Yes | [docs](workers/properties/points/point_to_nearest_blob_distance/POINT_TO_NEAREST_BLOB_DISTANCE.md) | | Distance from point to nearest other point | | | [docs](workers/properties/points/point_to_nearest_connected_point_distance/POINT_TO_NEAREST_CONNECTED_POINT_DISTANCE.md) | @@ -86,7 +86,7 @@ Compute properties on line annotations. | Worker | Description | Tests | Docs | |--------|-------------|-------|------| -| length | Compute the length of lines | | [docs](workers/properties/lines/line_length_worker/LINE_LENGTH_WORKER.md) | +| length | Computes the length of lines. | | [docs](workers/properties/lines/line_length_worker/LINE_LENGTH_WORKER.md) | | Line scan CSV | This tool computes the intensity along a line and puts the results in a CSV file. The i... | | [docs](workers/properties/lines/line_scan_worker/LINE_SCAN_WORKER.md) | | Test File Creation | | | [docs](workers/properties/lines/test_file_creation_worker/TEST_FILE_CREATION_WORKER.md) | @@ -96,8 +96,8 @@ Compute properties based on relationships between annotations. | Worker | Description | Tests | Docs | |--------|-------------|-------|------| -| Count connected objects | Count the number of children objects that are connected to a parent polygon | Yes | [docs](workers/properties/connections/children_count_worker/CHILDREN_COUNT_WORKER.md) | -| Connection IDs | Creates a set of identifiers that indicate the parent-child relationships between polygons | Yes | [docs](workers/properties/connections/parent_child_worker/PARENT_CHILD_WORKER.md) | +| Count connected objects | This tool counts the number of children objects that are connected to a parent polygon.... | Yes | [docs](workers/properties/connections/children_count_worker/CHILDREN_COUNT_WORKER.md) | +| Connection IDs | This tool adds a property to the objects to document the connections between them, whic... | Yes | [docs](workers/properties/connections/parent_child_worker/PARENT_CHILD_WORKER.md) | ## Test / Sample Workers @@ -108,8 +108,8 @@ Workers used for testing and demonstration -- not intended for production use. | Annulus Generator M1 | | | | [docs](workers/annotations/annulus_generator_M1/ANNULUS_GENERATOR_M1.md) | | Random point | Creates random point annotations | | | [docs](workers/annotations/random_point/RANDOM_POINT.md) | | Random Point Annotation M1 | | | | [docs](workers/annotations/random_point_annotation_M1/RANDOM_POINT_ANNOTATION_M1.md) | -| Random squares | Generates random square polygon annotations for testing | | Yes | [docs](workers/annotations/random_squares/RANDOM_SQUARES.md) | -| Sample interface | Demonstrates all interface types, messaging patterns, and batch mode | | Yes | [docs](workers/annotations/sample_interface/SAMPLE_INTERFACE.md) | +| Random squares | Generates random square polygon annotations within the image bounds. Useful for testing... | | Yes | [docs](workers/annotations/random_squares/RANDOM_SQUARES.md) | +| Sample interface | This is a sample interface worker that demonstrates all available interface types, tool... | | Yes | [docs](workers/annotations/sample_interface/SAMPLE_INTERFACE.md) | | Test Multiple Annotation | | | | [docs](workers/annotations/test_multiple_annotation/TEST_MULTIPLE_ANNOTATION.md) | | Test Multiple Annotation M1 | | | | [docs](workers/annotations/test_multiple_annotation_M1/TEST_MULTIPLE_ANNOTATION_M1.md) | From 83eae9e9ccdc4b735fdf98defabf5afb58e1d383 Mon Sep 17 00:00:00 2001 From: Arjun Raj Date: Tue, 26 May 2026 06:35:22 -0400 Subject: [PATCH 3/3] test(line_scan_worker): add unit tests covering IndexRange edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds pytest-based tests that mock the worker, dataset, and annotation clients. Covers: - Single-frame datasets where tiles lacks IndexRange (the bug fix regression test — previously raised KeyError: 'IndexRange'). - IndexRange present but missing IndexC (defaults to one channel). - Multi-channel datasets iterate over all channels. - 'All channels' = false scans only the selected channel. - No-annotations early return. - Constant-valued image produces constant intensity profile. - Interface registers expected fields. Wires up a line_scan_worker_test service in docker-compose.yml so `./build_workers.sh --build-and-run-tests line_scan_worker` runs them. All 7 tests pass in the container. Co-Authored-By: Claude Opus 4.7 (1M context) --- docker-compose.yml | 9 + .../line_scan_worker/tests/Dockerfile_Test | 14 ++ .../lines/line_scan_worker/tests/__init__.py | 3 + .../line_scan_worker/tests/test_line_scan.py | 180 ++++++++++++++++++ 4 files changed, 206 insertions(+) create mode 100644 workers/properties/lines/line_scan_worker/tests/Dockerfile_Test create mode 100644 workers/properties/lines/line_scan_worker/tests/__init__.py create mode 100644 workers/properties/lines/line_scan_worker/tests/test_line_scan.py diff --git a/docker-compose.yml b/docker-compose.yml index e14b798..47e0ce4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -252,6 +252,15 @@ services: profiles: ["testworker"] # Test services + line_scan_worker_test: + build: + context: . + dockerfile: ./workers/properties/lines/line_scan_worker/tests/Dockerfile_Test + image: properties/line_scan_worker:test + depends_on: + - line_scan_worker + profiles: ["test"] + blob_metrics_test: build: context: . diff --git a/workers/properties/lines/line_scan_worker/tests/Dockerfile_Test b/workers/properties/lines/line_scan_worker/tests/Dockerfile_Test new file mode 100644 index 0000000..428141c --- /dev/null +++ b/workers/properties/lines/line_scan_worker/tests/Dockerfile_Test @@ -0,0 +1,14 @@ +# Use the line_scan_worker image as the base +FROM properties/line_scan_worker:latest as test + +# Install test dependencies +SHELL ["conda", "run", "-n", "worker", "/bin/bash", "-c"] +RUN pip install pytest pytest-mock + +# Copy test files +RUN mkdir -p /tests +COPY ./workers/properties/lines/line_scan_worker/tests/*.py /tests +WORKDIR /tests + +# Command to run tests +ENTRYPOINT ["conda", "run", "--no-capture-output", "-n", "worker", "python3", "-m", "pytest", "-v"] diff --git a/workers/properties/lines/line_scan_worker/tests/__init__.py b/workers/properties/lines/line_scan_worker/tests/__init__.py new file mode 100644 index 0000000..3e69f38 --- /dev/null +++ b/workers/properties/lines/line_scan_worker/tests/__init__.py @@ -0,0 +1,3 @@ +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) diff --git a/workers/properties/lines/line_scan_worker/tests/test_line_scan.py b/workers/properties/lines/line_scan_worker/tests/test_line_scan.py new file mode 100644 index 0000000..47053cc --- /dev/null +++ b/workers/properties/lines/line_scan_worker/tests/test_line_scan.py @@ -0,0 +1,180 @@ +import pytest +from unittest.mock import patch, MagicMock +import numpy as np + +from entrypoint import compute, interface + + +@pytest.fixture +def mock_worker_client(): + with patch('annotation_client.workers.UPennContrastWorkerClient') as mock_cls: + client = mock_cls.return_value + client.get_annotation_list_by_shape.return_value = [] + yield client + + +@pytest.fixture +def mock_annotation_client(): + with patch('annotation_client.annotations.UPennContrastAnnotationClient') as mock_cls: + client = mock_cls.return_value + client.client = MagicMock() + client.client.getFolder.return_value = {'_id': 'folder_id'} + yield client + + +@pytest.fixture +def mock_dataset_client(): + with patch('annotation_client.tiles.UPennContrastDataset') as mock_cls: + client = mock_cls.return_value + client.tiles = {} + client.coordinatesToFrameIndex.side_effect = ( + lambda xy, z, time, channel: channel + ) + yield client + + +@pytest.fixture +def sample_line_annotation(): + return { + '_id': 'line_1', + 'shape': 'line', + 'coordinates': [ + {'x': 1.5, 'y': 1.5}, + {'x': 5.5, 'y': 1.5}, + ], + 'location': {'Time': 0, 'Z': 0, 'XY': 0}, + 'tags': ['scan'], + } + + +def _params(all_channels, selected_channel=0, file_name='out.csv'): + return { + 'id': 'prop_id', + 'name': 'line_scan_test', + 'image': 'properties/line_scan_worker:latest', + 'tags': {'exclusive': False, 'tags': []}, + 'shape': 'line', + 'workerInterface': { + 'All channels': all_channels, + 'Channel': selected_channel, + 'File name': file_name, + }, + } + + +def test_interface(): + """Interface registers required fields.""" + with patch('annotation_client.workers.UPennContrastWorkerPreviewClient') as mock_cls: + interface('img', 'http://api', 'token') + mock_cls.return_value.setWorkerImageInterface.assert_called_once() + interface_data = mock_cls.return_value.setWorkerImageInterface.call_args[0][1] + for key in ('All channels', 'Channel', 'File name', 'Line Scan CSV'): + assert key in interface_data + + +def test_no_annotations_returns_early( + mock_worker_client, mock_annotation_client, mock_dataset_client +): + """When no line annotations exist, no upload happens.""" + mock_worker_client.get_annotation_list_by_shape.return_value = [] + + compute('dataset_id', 'http://api', 'token', _params(all_channels=True)) + + mock_annotation_client.client.uploadStreamToFolder.assert_not_called() + + +def test_single_channel_dataset_no_IndexRange( + mock_worker_client, mock_annotation_client, mock_dataset_client, + sample_line_annotation, +): + """Regression: single-frame datasets omit IndexRange entirely. + + Before the fix this raised KeyError: 'IndexRange'. After the fix it + treats the dataset as having a single channel. + """ + mock_worker_client.get_annotation_list_by_shape.return_value = [sample_line_annotation] + mock_dataset_client.tiles = {} # no IndexRange — the bug condition + mock_dataset_client.getRegion.return_value = np.full((10, 10), 42, dtype=np.uint8) + + compute('dataset_id', 'http://api', 'token', _params(all_channels=True)) + + # One channel was loaded. + assert mock_dataset_client.getRegion.call_count == 1 + # Frame index resolved for channel 0. + mock_dataset_client.coordinatesToFrameIndex.assert_called_once_with(0, 0, 0, 0) + # CSV was uploaded. + mock_annotation_client.client.uploadStreamToFolder.assert_called_once() + + +def test_multi_channel_dataset_with_IndexRange( + mock_worker_client, mock_annotation_client, mock_dataset_client, + sample_line_annotation, +): + """When IndexRange.IndexC is set, all channels are scanned.""" + mock_worker_client.get_annotation_list_by_shape.return_value = [sample_line_annotation] + mock_dataset_client.tiles = {'IndexRange': {'IndexC': 3}} + mock_dataset_client.getRegion.return_value = np.full((10, 10), 7, dtype=np.uint8) + + compute('dataset_id', 'http://api', 'token', _params(all_channels=True)) + + # Three channels were loaded. + assert mock_dataset_client.getRegion.call_count == 3 + channels_loaded = [ + call.args[3] for call in mock_dataset_client.coordinatesToFrameIndex.call_args_list + ] + assert channels_loaded == [0, 1, 2] + + +def test_IndexRange_without_IndexC( + mock_worker_client, mock_annotation_client, mock_dataset_client, + sample_line_annotation, +): + """IndexRange present but missing IndexC also defaults to 1 channel.""" + mock_worker_client.get_annotation_list_by_shape.return_value = [sample_line_annotation] + mock_dataset_client.tiles = {'IndexRange': {'IndexZ': 4}} # has IndexZ but no IndexC + mock_dataset_client.getRegion.return_value = np.full((10, 10), 1, dtype=np.uint8) + + compute('dataset_id', 'http://api', 'token', _params(all_channels=True)) + + assert mock_dataset_client.getRegion.call_count == 1 + + +def test_all_channels_false_uses_selected_channel( + mock_worker_client, mock_annotation_client, mock_dataset_client, + sample_line_annotation, +): + """When 'All channels' is unchecked, only the selected channel is scanned.""" + mock_worker_client.get_annotation_list_by_shape.return_value = [sample_line_annotation] + mock_dataset_client.tiles = {'IndexRange': {'IndexC': 5}} + mock_dataset_client.getRegion.return_value = np.full((10, 10), 99, dtype=np.uint8) + + compute( + 'dataset_id', 'http://api', 'token', + _params(all_channels=False, selected_channel=2), + ) + + assert mock_dataset_client.getRegion.call_count == 1 + mock_dataset_client.coordinatesToFrameIndex.assert_called_once_with(0, 0, 0, 2) + + +def test_intensity_profile_values( + mock_worker_client, mock_annotation_client, mock_dataset_client, + sample_line_annotation, +): + """Intensity profile along a constant-valued image is constant.""" + mock_worker_client.get_annotation_list_by_shape.return_value = [sample_line_annotation] + mock_dataset_client.tiles = {} + constant_image = np.full((10, 10), 123, dtype=np.uint8) + mock_dataset_client.getRegion.return_value = constant_image + + compute('dataset_id', 'http://api', 'token', _params(all_channels=True)) + + # Inspect the uploaded CSV stream. + upload_call = mock_annotation_client.client.uploadStreamToFolder.call_args + assert upload_call is not None + csv_stream = upload_call.args[1] + csv_stream.seek(0) + csv_text = csv_stream.read() + # Channel 0 row should contain repeated 123s. + assert 'Channel 0' in csv_text + assert '123' in csv_text