') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Reduce time cost of tests by LucaMarconato · Pull Request #1116 · scverse/spatialdata · GitHub
Skip to content

Reduce time cost of tests - #1116

Merged
LucaMarconato merged 18 commits into
mainfrom
faster-tests
May 6, 2026
Merged

Reduce time cost of tests#1116
LucaMarconato merged 18 commits into
mainfrom
faster-tests

Conversation

@LucaMarconato

Copy link
Copy Markdown
Member

Reducing the time required to run tests. This complements #1113, which reduces the number of emitted warnings (@melonora observed that this was impacting the runtime).

LucaMarconatoand others added 4 commits May 5, 2026 17:01
Cache `blobs(256, 300, 3)` and `BlobsDataset()._labels_blobs()` once
per session via private session-scoped fixtures, then deepcopy into
each function-scoped fixture. Cuts fixture setup from 44.8s to 35.0s
(-9.8s) and total suite from 186s to 180s. All 1332 tests still pass.
Benchmark CSVs committed for reference (pytest_*.csv).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
to_circles() on labels scales linearly with pixel count.
Dropping from 512×512 to 128×128 cuts test_labels_2d_to_circles
from ~3.9s to ~1.0s per parametrized variant (−4.7s across the file).
Updated hardcoded coordinate/radius assertions to match the new size.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecovBot commented May 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.97%. Comparing base (8d7f72f) to head (58b64c8).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files
@@ Coverage Diff @@## main #1116 +/- ##
==========================================
+ Coverage 91.93% 91.97% +0.04% 
==========================================
Files 51 51 Lines 7772 7750 -22 ==========================================
- Hits 7145 7128 -17 + Misses 627 622 -5 
Files with missing linesCoverage Δ
src/spatialdata/_core/_elements.py93.15% <100.00%> (+0.92%)⬆️
src/spatialdata/_core/centroids.py100.00% <100.00%> (ø)
src/spatialdata/models/models.py87.80% <100.00%> (-0.87%)⬇️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… deepcopy
Two orthogonal wins:
1. _elements.py: `get_model()` already calls `schema.validate()` internally;
the explicit second validate() + get_axes_names() call in every __setitem__
was redundant. Removing it halves the DataTree (_to_dataset_view) overhead
per element insertion — directly speeds up fixture setup.
2. conftest.py: introduce `_fast_deepcopy_sdata` (copy.deepcopy + manual attrs
restoration for DaskDataFrame/#503 and GeoDataFrame/#286) that is ~13x faster
than sd_deepcopy (7ms vs 93ms for full_sdata). Session-scope full_sdata,
images, labels and the 'full' sdata parametrized fixture; each test gets
a fresh 7ms copy instead of an 87ms full reconstruction. Also switch
sdata_blobs from sd_deepcopy to fast_deepcopy_sdata (2ms vs 25ms).
Full suite: 186s → 163s (~12% reduction, ~23s saved).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucaMarconato

Copy link
Copy Markdown
MemberAuthor

This PR also adjusts the tests in the CI (modified in #1114) to keep only lower-bound and upper-bound Python versions, which is the philosophy used in integration-testing.

Actually since we test 3.11 and 3.14, and integration testing tests 3.12 every night, we cover also the 3.12 case, while keeping the per-commit CI leaner.

…ion comment
- get_model() now accepts validate=False to skip schema.validate() when the
caller only needs to infer the element type without re-running validation
- add comment to Elements.__setitem__ noting that subclass overrides call
get_model() which performs the validation
- drop Python 3.13 CI matrix entries (superseded by 3.14)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucaMarconato
LucaMarconato marked this pull request as ready for review May 5, 2026 17:07
@LucaMarconato

Copy link
Copy Markdown
MemberAuthor

So far the tests should be running ~12% faster than in main. We could merge already, or optimize further.

LucaMarconatoand others added 9 commits May 5, 2026 19:20
… elements
Add skip_element_validation() context manager (backed by a ContextVar) that
makes __setitem__ call get_model(validate=False) — type inference only, no
schema.validate(). Use it in every code path that constructs a SpatialData
from elements that originated from an existing SpatialData and were never
externally mutated: bounding_box_query, polygon_query, query_by_coordinate_system,
transform_to_coordinate_system, subset, and init_from_elements.
test_query_spatial_data: 0.77s → 0.64s (the remaining time is the query work
itself — filtering, shapely ops, raster cropping).
Also inline a minimal 2-image SpatialData in
test_transformations_between_coordinate_systems instead of relying on the
full 8-element images fixture; the test only ever uses image2d and
image2d_multiscale, so writing the other 6 to disk was pure waste.
test_transformations_between_coordinate_systems: 0.61s → 0.44s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace per-slice O(H+W) approach (512 dask compute() calls for a 256×256
array) with a single array materialization + np.bincount O(n_pixels) pass.
This speeds up get_centroids() on labels from ~1.5s to ~50ms, cutting
to_circles(labels) from ~1.6s to ~53ms. Affects test_validation dataloader
variants (~2.5s → ~0.2s each, saving ~9s), test_labels_2d_to_circles, and
any production call to get_centroids or to_circles on label arrays.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add blobs_factory fixture that returns fast deepcopies of the session-scoped
blobs dataset, and update test_concatenate_* and test_no_shared_transformations
to use it instead of calling blobs() per test. Also update test_get_attrs.py
sdata_attrs fixture and test_empty_attrs to use sdata_blobs.
The concatenate parametrized tests (6 variants) drop from ~0.55s to ~0.02s
each (~3s total savings); test_get_attrs drops from ~0.28s to ~0.02s per test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ned_dataframe
Drop from 200 to 20 for both N_PARTITIONS and the number of gene categories.
The test's assertions (round-trip values, category-order mismatch between pandas
and dask, dtype) are all preserved at the smaller scale; the 200-partition
stress test was over-specified for what's actually being verified.
~1.56s → ~0.95s (39% faster) for that test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…seed
Drop from 20 to 10 for both N_PARTITIONS and the number of gene categories,
and replace the module-level RNG with a local default_rng(seed=0) so the
test is independent of other tests' RNG consumption.
With seed=0 and N=10, partition 0 is deterministically missing gene_4,
which is sufficient to trigger the dask category-order mismatch being tested.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add pytest-xdist to test dependencies and pass -n auto --dist worksteal
to pytest in CI. worksteal distributes tests dynamically across workers
so slow IO tests don't block fast unit tests on a single worker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dexing
- Remove reference to the old per-slice implementation from the docstring
- Add inline comment explaining why indexing="ij" is correct for any
number of spatial dimensions (2D and 3D labels)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucaMarconato

Copy link
Copy Markdown
MemberAuthor

The last fail is unrelated to the code change and likely due to the parallel test execution. I'll investigate.

…fixtures to session scope
- test_delete_element_from_disk: subset full_sdata to [element_name, points_0_1]
before writing, cutting the initial write from 19 elements to 2 (2–13× speedup
per parametrize case)
- conftest: set NUMBA_DISABLE_JIT=1 to avoid ~1.4s JIT overhead per worker on
first datashader/rasterize call
- test_partial_read: promote all module-scoped fixtures to session scope so the
corrupted zarr stores are built once per session instead of once per module
- test_transform: use small_translation=True to avoid out-of-bounds raster
operations that trigger large dask computations
- test_spatialdata_operations: reduce target_width 1000→100 in
test_transform_to_data_extent to shrink the output raster
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucaMarconato

Copy link
Copy Markdown
MemberAuthor

Tests are significantly faster now!

This below is from main
image

This is with this PR:
image

LucaMarconatoand others added 2 commits May 6, 2026 13:56
… napari plugin
Napari registers a pytest11 entry point that loads (and breaks) numba before
conftest.py runs. Fix: block it in pyproject.toml addopts with -p no:napari.
datashader's @jit(cache=True) raises "no locator available" on Python 3.13 +
numba. With napari blocked, conftest.py now runs before any plugin imports
numba, so NUMBA_DISABLE_JIT=1 consistently disables all @jit decorators
(datashader, xrspatial) and avoids the mixed JIT/non-JIT crash.
Promote all module-scoped fixtures in test_partial_read to session scope so
corrupted zarr stores are built once per session.
For tests that write a full SpatialData object but only need a single element
(or a small subset) to trigger the condition under test, subset the fixture
before writing. The initial full write dominates test time (19 elements
including 3-D multiscale rasters), so even subsetting to 1-6 elements gives
2-13× speedups per parametrize case:
- test_delete_element_from_disk: subset to [element_name, points_0_1]
- test_incremental_io_on_disk: subset to the 7 elements the loop accesses
- test_overwrite_fails_when_zarr_store_present: use empty SpatialData()
- test_element_already_on_disk_different_type: subset to [element_name]
- test_self_contained: subset to [image2d, labels2d, points_0, circles]
- test_change_path_of_subset: subset to the 5 elements + points_0_1 (needed
so only_on_disk > 0 assertion passes)
- test_validate_can_write_metadata_on_element: subset to [element_name]
- test_save_transformations_incremental: subset to [element_name, image2d]
(image2d anchors the non-self-contained assertion for the circles case)
- test_consolidated_metadata: subset to one element per type
- test_channel_names_raster_images_v1_to_v2_to_v3: subset to
[image2d, image2d_multiscale]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@LucaMarconato

Copy link
Copy Markdown
MemberAuthor

Tests now locally run under 1 minute!
image

@LucaMarconato
LucaMarconato merged commit 6ee1013 into mainMay 6, 2026
9 checks passed
@LucaMarconato
LucaMarconato deleted the faster-tests branch May 6, 2026 12:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@LucaMarconato