Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
531e90e
Make several tests structure-based instead of hash-based
nictru Mar 24, 2026
8c4135c
Remove doubletdetection from pipeline-level tests
nictru Mar 24, 2026
79ac2d1
Update test snapshots
nictru Mar 24, 2026
3ba8985
Update CI nf-test version
nictru Mar 24, 2026
d0c5e97
Disable OpenMP CPU topology detection
fasterius Mar 23, 2026
e93537c
Add `MPLCONFIGDIR` to `PREPCELLXGENE` module
fasterius Mar 23, 2026
2d40395
Add missing CPU topology detection disables
fasterius Mar 24, 2026
278b7e6
Update snapshots
fasterius Mar 24, 2026
32210e4
Simplify umap tests
nictru Mar 24, 2026
25eab93
Update HVG tests
nictru Mar 24, 2026
ec3648d
Update SCDS tests
nictru Mar 24, 2026
6af93ad
Simplify per-group tests
nictru Mar 24, 2026
ded90e2
Update testing strategy
nictru Mar 25, 2026
fd03662
Update snapshots
fasterius Mar 26, 2026
dc7e2b1
Improve module tests
nictru Mar 28, 2026
f32147f
Improve subworkflow tests
nictru Mar 28, 2026
da0d51a
Remove variance_ratio implementations
nictru Mar 28, 2026
c448905
Update soupx/decontx tests
nictru Mar 28, 2026
bfd1d32
Update stale snapshots for scanpy/filter, scanpy/leiden, scanpy/pca
nictru Mar 28, 2026
1362a8e
Add rule for preventing direct snapshot editing
nictru Mar 28, 2026
913f1aa
Fix integrate stub snapshots after variance_ratio removal
nictru Mar 28, 2026
55937e7
Rewrite reproducibility.md with accurate test strategy column
nictru Mar 28, 2026
ec9d2f5
Update pipeline-level snapshots
nictru Mar 29, 2026
2d20283
Improve snapshot anndata yaml formatting
nictru Mar 29, 2026
0182e9e
Switch to nft-anndata 0.4.1 from registry
nictru Mar 29, 2026
3cf6370
Update some more module and sw tests
nictru Mar 29, 2026
8488d0d
Update combine tests
nictru Mar 29, 2026
4f483a0
Update some more module tests
nictru Mar 29, 2026
2879704
Update adata/merge test snapshots
nictru Mar 29, 2026
fb58df5
Change scanpy/hvgs classification in reproducibility doc
nictru Mar 29, 2026
491fce6
Update some more tests
nictru Mar 29, 2026
b051578
Update more tests
nictru Mar 29, 2026
6f0481e
Fix adata/merge determinism issue
nictru Mar 29, 2026
1f7e98a
Another attempt of fixing adata/merge
nictru Mar 29, 2026
30f1689
Update snapshots
fasterius Mar 29, 2026
dfc9be7
Update scanpy/pca test snapshot
nictru Mar 30, 2026
e22e0e2
Update upstream modules
nictru Mar 30, 2026
38ab8d8
Adjust for updated modules
nictru Mar 30, 2026
7c2b51c
Align tests with module updates
nictru Mar 30, 2026
8ff74dd
Make liana/rankaggregate and scanpy/rankgenesgroups tests structural
nictru Mar 30, 2026
72bb8b1
Update snapshots
fasterius Mar 30, 2026
2825823
Update snapshots
nictru Mar 31, 2026
3cba325
Update test snapshots
nictru Mar 31, 2026
38d3f46
Update snapshots
fasterius Apr 1, 2026
101ae65
Align scanpy/pca stub with other scanpy stubs
nictru Apr 1, 2026
ce1cc39
Update reproducibility rule+docs
nictru Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions .cursor/rules/nf-test-assertions.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
description: nf-test assertion patterns for scdownstream — how to write platform-independent, targeted tests
globs: "**/*.nf.test"
alwaysApply: false
---

# nf-test Assertion Patterns

## Core principle

Omit **platform-sensitive** content from snapshots when outputs are **non-deterministic** (randomised ML, unstable floats in matrices, live APIs): use **`structural`** snapshots (`versions` YAML + `adata.yaml`, no `process.out` MD5s).

When a module is **deterministic** (see [`docs/reproducibility.md`](../docs/reproducibility.md)) and **emits H5AD**, **`hash`** snapshots **include** `process.out` file MD5s plus `adata.yaml` — that is intentional regression coverage, not “raw float arrays” in the snap.

## Assertion order — sequential, no `assertAll`

All `then {}` blocks use **plain sequential `assert` statements** in this fixed order:

1. `assert process.success` / `assert workflow.success` — bare, so the real failure mode is visible before deeper checks
2. `def adata = anndata(...)` — when an output H5AD (or similar) is used in field checks or in `adata.yaml` inside `snapshot()`; extract once if referenced **2+** times
3. **Field checks** — specific assertions before the snapshot (e.g. `assert "X_pca" in adata.obsm`)
4. `assert snapshot(...).match()` — last

**Why this order**: missing fields fail in step 3 with a short message; the snapshot is the final regression net.

If a test **must** open AnnData only after success and you keep `def adata` before `assert success` for historical reasons, treat **success before field checks and snapshot** as the hard rule when refactoring.

**Never use `assertAll`** — it groups all failures into one report which makes CI output harder to read when a process fails.

## Stub tests

Stub tests always use the simple pattern:

```groovy
test("Should run without failures - stub") {
options '-stub'
then {
assert process.success
assert snapshot(process.out).match()
}
}
```

## Non-stub tests — full example

```groovy
then {
assert process.success
def adata = anndata(process.out.h5ad[0][1])
assert "X_pca" in adata.obsm // field checks BEFORE snapshot
assert "X_pca" in adata.uns
assert snapshot(
process.out,
path(process.out.versions[0]).yaml,
adata.yaml
).match() // snapshot LAST — comprehensive regression
}
```

## `def adata` variable

When `anndata(path)` would appear more than once in the same `then {}` block, always extract it:

```groovy
def adata = anndata(process.out.h5ad[0][1])
// then use adata.obsm, adata.yaml, with(adata) { ... }
```

For subworkflow tests, the path comes from `workflow.out`:

```groovy
def adata = anndata(workflow.out.h5ad[0][1])
// or workflow.out.integrations[0][1] etc.
```

## `with(adata)` for grouped checks

When asserting multiple properties of the same AnnData object, use `with(adata)`:

```groovy
assert process.success
def adata = anndata(process.out.h5ad[0][1])
with(adata) {
assert "batch" in obs.colnames
assert "label" in obs.colnames
assert !("counts" in layers)
}
assert snapshot(process.out, path(process.out.versions[0]).yaml, adata.yaml).match()
```

## What to include in `snapshot()`

Strategy names are **identical** to [`docs/reproducibility.md`](../docs/reproducibility.md) → **Test strategy (this branch)**. The doc assigns each module/subworkflow to a strategy; this table is the **default `snapshot(...)` shape per strategy** (modules use `process.*`; subworkflows use `workflow.out.*` as in the examples below).

### Module strategies — default snapshot contents

| Strategy | Typical `snapshot(...)` arguments |
|----------|----------------------------------|
| `hash` | `process.out`, `path(process.out.versions[0]).yaml`, `adata.yaml` — module **emits H5AD** |
| `hash (no H5AD output)` | `process.out`, `path(process.out.versions[0]).yaml` — **no** `adata.yaml` |
| `hash + structural` | Same as **`hash`** (label in doc marks modules like `scanpy/filter` with multiple scenarios) |
| `structural` | `path(process.out.versions[0]).yaml`, `adata.yaml` — **omit** `process.out` |
| `column names` | e.g. `path(process.out.versions[0]).yaml`, `adata.obs.colnames` — **omit** `process.out` MD5s |
| `column names only` | Same as **`column names`** (shorthand in `docs/reproducibility.md` module tables only) |
| `obs CSV + file names` | Per module row in doc (obs artefacts + versions; not full H5AD hash) |
| `range assertion` | Imperative range checks **plus** the snapshot mix given in that doc row |
| `stub only` | No non-stub test; stub only: `snapshot(process.out).match()` |

### Subworkflow tests

Patterns vary by workflow. Use the **Subworkflows** table in `docs/reproducibility.md` as the source of truth. Common building blocks:

```groovy
// Versions as YAML (often + adata.yaml); omit full workflow.out when unstable
assert snapshot(
workflow.out.versions.collect { path(it).yaml },
adata.yaml
).match()
```

```groovy
// Deterministic outputs + versions + schema
assert snapshot(
workflow.out,
workflow.out.versions.collect { path(it).yaml },
adata.yaml
).match()
```

**Passthrough / unstaged H5AD paths:** `snapshot(workflow.out)` only — see later section.

## Common mistake — dropping `process.out` from **`hash`** modules that emit H5AD

For modules whose strategy is **`hash`** (H5AD output) in `docs/reproducibility.md`, **`process.out` must stay** in the snapshot.

Do **not** apply this to **`structural`**, **`column names`**, **`hash (no H5AD output)`**, or other strategies that intentionally omit output MD5s.

```groovy
// ❌ WRONG for strategy `hash` (H5AD) — drops output file hashes
assert snapshot(path(process.out.versions[0]).yaml, adata.yaml).match()

// ✅ CORRECT for strategy `hash` (H5AD)
assert snapshot(process.out, path(process.out.versions[0]).yaml, adata.yaml).match()
```

## Versions channel

- **Module tests**: `path(process.out.versions[0]).yaml` — reads the single `versions.yml` as YAML
- **Subworkflow tests**: `workflow.out.versions.collect { path(it).yaml }` — parses each file for human-readable version names

The versions channel should never include `meta`. In module tests, `process.out.versions[0]` is a plain path (no tuple).

## Spread multi-argument snapshots over multiple lines

```groovy
// ✅ readable
assert snapshot(
process.out,
path(process.out.versions[0]).yaml,
adata.yaml
).match()

// ❌ hard to read
assert snapshot(process.out, path(process.out.versions[0]).yaml, adata.yaml).match()
```

## One snapshot call per test

Each test can only have **one** `snapshot(...).match()` call — nf-test keys snapshots by test name. Always combine into one call.

## Explicit field checks — always assert created fields

Every test should explicitly assert the key outputs/fields that the module creates:
- `assert "X_pca" in adata.obsm` for new embeddings
- `assert "leiden" in adata.obs.colnames` for new obs columns
- `assert "rank_genes_groups" in adata.uns` for uns keys
- `assert "ambient_corrected" in adata.layers` for new layers

## Known nft-anndata limitation: `uns` is top-level keys only

`adata.uns` (and `uns` inside a `with(adata)` block) is a `Set<String>` — it exposes only the **top-level key names**, not the nested values. This means:

```groovy
assert "paga" in adata.uns // ✅ works — checks key presence
assert "connectivities" in adata.uns["paga"] // ❌ fails — cannot navigate into nested uns
```

Sub-key structure of nested `uns` entries (e.g. `uns["paga"]["connectivities"]`) is only verifiable via `adata.yaml` in the snapshot.

## Known nft-anndata limitation: passthrough h5ad files

`nft-anndata 0.4.0` tries to open `.h5ad` files when they appear as **staged** files in snapshot serialisation. It does **NOT** open files that were never staged into the Nextflow work directory (e.g., direct references to `/nf-core/test-datasets/...`).

**What triggers HDF5 read** (fails on unstaged files):
- `workflow.out.h5ad.toList()` inside a snapshot
- Explicit `anndata(path).yaml` where path is a passthrough/unstaged file

**What does NOT trigger HDF5 read** (safe for passthrough):
- `snapshot(workflow.out)` — passthrough paths are serialised as raw strings
- `workflow.out.h5ad.size()` — only counts elements

For **load_h5ad** (which passes through files from test datasets), use:

```groovy
then {
assert workflow.success
assert snapshot(workflow.out).match()
}
```

Do NOT use `anndata(workflow.out.h5ad[0][1]).yaml` for passthrough files.

## API references

- **nft-anndata**: [github.com/nictru/nft-anndata](https://github.com/nictru/nft-anndata)
- **nf-test path API** (yaml, json, md5, linesGzip): [nf-test.com/docs/assertions/files](https://www.nf-test.com/docs/assertions/files/)
35 changes: 35 additions & 0 deletions .cursor/rules/nf-test-snapshots.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: Never directly edit nf-test snapshot files; always use nftu to regenerate them
globs: "**/*.nf.test.snap"
alwaysApply: false
---

# nf-test Snapshot Files

**Never manually edit `.nf.test.snap` files.**

Directly editing snapshots cannot guarantee that the written content matches what the pipeline would actually produce. Always regenerate them using the `--update-snapshot` flag of `nf-test`:

```bash
nf-test test --update-snapshot <path/to/test.nf.test>
```

The exact invocation depends on the environment (profiles, container engine, etc.). Check local aliases or project documentation for the recommended command on the current machine.

## Example

```bash
# ✅ GOOD — regenerate snapshots from actual test output
nf-test test --update-snapshot modules/local/scanpy/filter/tests/main.nf.test

# ❌ BAD — manually editing the snap file
# vim modules/local/scanpy/filter/tests/main.nf.test.snap
```

You can pass multiple test files in one invocation:

```bash
nf-test test --update-snapshot \
modules/local/scanpy/filter/tests/main.nf.test \
modules/local/scanpy/leiden/tests/main.nf.test
```
2 changes: 1 addition & 1 deletion .github/workflows/nf-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ concurrency:

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NFT_VER: "0.9.3"
NFT_VER: "0.9.5"
NFT_WORKDIR: "~"
NXF_ANSI_LOG: false
NXF_SINGULARITY_CACHEDIR: ${{ github.workspace }}/.singularity
Expand Down
Loading
Loading