From 9d93dfaa3dbe224588d2f7c10dce082528854dec Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Wed, 23 Sep 2026 11:42:53 -0400 Subject: [PATCH 1/8] Add modkit/dmr module Thin wrapper around `modkit dmr pair`, comparing methylation between a pair of bgzip+tabix bedMethyl inputs (e.g. two haplotypes, or tumor and normal) and reporting differentially methylated regions. Modeled on modkit/pileup and modkit/callmods' existing conventions in this repo (same container, same versions-topic pattern). Test data (test_hp1.bed.gz/test_hp2.bed.gz, bgzip+tabix'd) generated with modkit pileup --phased --modified-bases 5mC 5hmC on the existing genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam fixture -- the same inputs and modified-bases args modkit/pileup's own --phased tests already use. Added via a separate PR to nf-core/test-datasets (branch add-modkit-dmr-bedmethyl on YannVRB/test-datasets_YV), not yet merged -- tests/config/nf-test.config here still points at the canonical nf-core/test-datasets modules_testdata_base_path and will resolve once that PR lands. Verified locally by temporarily pointing modules_testdata_base_path at the fork branch: all 3 tests pass against real `modkit dmr pair` output (Nextflow 25.10.2, matching this repo's nf-test.yml CI pin -- 26.04.6 fails nf-test's generated harness under strict syntax, an unrelated tooling issue). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/environment.yml | 7 + modules/nf-core/modkit/dmr/main.nf | 47 +++++ modules/nf-core/modkit/dmr/meta.yml | 122 ++++++++++++ modules/nf-core/modkit/dmr/tests/main.nf.test | 126 +++++++++++++ .../modkit/dmr/tests/main.nf.test.snap | 173 ++++++++++++++++++ .../nf-core/modkit/dmr/tests/nextflow.config | 5 + 6 files changed, 480 insertions(+) create mode 100644 modules/nf-core/modkit/dmr/environment.yml create mode 100644 modules/nf-core/modkit/dmr/main.nf create mode 100644 modules/nf-core/modkit/dmr/meta.yml create mode 100644 modules/nf-core/modkit/dmr/tests/main.nf.test create mode 100644 modules/nf-core/modkit/dmr/tests/main.nf.test.snap create mode 100644 modules/nf-core/modkit/dmr/tests/nextflow.config diff --git a/modules/nf-core/modkit/dmr/environment.yml b/modules/nf-core/modkit/dmr/environment.yml new file mode 100644 index 000000000000..62b9786305ae --- /dev/null +++ b/modules/nf-core/modkit/dmr/environment.yml @@ -0,0 +1,7 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - ont-modkit=0.6.1 diff --git a/modules/nf-core/modkit/dmr/main.nf b/modules/nf-core/modkit/dmr/main.nf new file mode 100644 index 000000000000..2ee99685240c --- /dev/null +++ b/modules/nf-core/modkit/dmr/main.nf @@ -0,0 +1,47 @@ +process MODKIT_DMR { + tag "${meta.id}" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/ont-modkit:0.6.1--hcdda2d0_0' + : 'quay.io/biocontainers/ont-modkit:0.6.1--hcdda2d0_0'}" + + input: + tuple val(meta), path(bedmethyl_a), path(bedmethyl_a_tbi) + tuple val(meta2), path(bedmethyl_b), path(bedmethyl_b_tbi) + tuple val(meta3), path(regions_bed) + tuple val(meta4), path(fasta) + + output: + tuple val(meta), path("*.bed"), emit: bed + tuple val(meta), path("*.log"), emit: log + tuple val("${task.process}"), val('modkit'), eval("modkit --version | sed 's/modkit //'"), emit: versions_modkit, topic: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def regions = regions_bed ? "-r ${regions_bed}" : '' + """ + modkit \\ + dmr pair \\ + -a ${bedmethyl_a} \\ + -b ${bedmethyl_b} \\ + ${regions} \\ + --ref ${fasta} \\ + -o ${prefix}.bed \\ + --log-filepath ${prefix}.log \\ + -t ${task.cpus} \\ + ${args} + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}.bed + touch ${prefix}.log + """ +} diff --git a/modules/nf-core/modkit/dmr/meta.yml b/modules/nf-core/modkit/dmr/meta.yml new file mode 100644 index 000000000000..a20cfda798cd --- /dev/null +++ b/modules/nf-core/modkit/dmr/meta.yml @@ -0,0 +1,122 @@ +name: modkit_dmr +description: Compare regions between a pair of bedMethyl samples (e.g. two haplotypes, + or tumor and normal) and report differentially methylated regions +keywords: + - methylation + - ont + - long-read + - differential methylation +tools: + - "modkit": + description: A bioinformatics tool for working with modified bases in Oxford + Nanopore sequencing data + homepage: https://github.com/nanoporetech/modkit + documentation: https://github.com/nanoporetech/modkit + tool_dev_url: https://github.com/nanoporetech/modkit + licence: ["Oxford Nanopore Technologies PLC. Public License Version 1.0"] + identifier: "" +input: + - - meta: + type: map + description: | + Groovy Map containing sample information for the first (control) input + e.g. `[ id:'test' ]` + - bedmethyl_a: + type: file + description: Bgzipped bedMethyl file for the first (control) sample, as + produced by modkit pileup with --bgzf + pattern: "*.bed.gz" + ontologies: [] + - bedmethyl_a_tbi: + type: file + description: Tabix index for bedmethyl_a. Must have the same basename with + a .tbi extension next to bedmethyl_a + pattern: "*.bed.gz.tbi" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing sample information for the second (experimental) + input e.g. `[ id:'test' ]` + - bedmethyl_b: + type: file + description: Bgzipped bedMethyl file for the second (experimental) sample, + as produced by modkit pileup with --bgzf + pattern: "*.bed.gz" + ontologies: [] + - bedmethyl_b_tbi: + type: file + description: Tabix index for bedmethyl_b. Must have the same basename with + a .tbi extension next to bedmethyl_b + pattern: "*.bed.gz.tbi" + ontologies: [] + - - meta3: + type: map + description: | + Groovy Map containing regions information + e.g. `[ id:'regions' ]` + - regions_bed: + type: file + description: Optional BED file of regions over which to compare methylation + levels. When omitted, methylation levels are compared at each site instead + pattern: "*.bed" + ontologies: [] + - - meta4: + type: map + description: | + Groovy Map containing reference information + e.g. `[ id:'hg38' ]` + - fasta: + type: file + description: Reference sequence in FASTA format, used during pileup/alignment + pattern: "*.{fa,fasta}" + ontologies: [] +output: + bed: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - "*.bed": + type: file + description: BED file with the score column indicating the magnitude of + the difference in methylation between the two samples + pattern: "*.bed" + ontologies: [] + log: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'test' ]` + - "*.log": + type: file + description: File for debug logs to be written to + pattern: "*.log" + ontologies: [] + versions_modkit: + - - ${task.process}: + type: string + description: The name of the process + - modkit: + type: string + description: The name of the tool + - modkit --version | sed 's/modkit //': + type: eval + description: The expression to obtain the version of the tool +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - modkit: + type: string + description: The name of the tool + - modkit --version | sed 's/modkit //': + type: eval + description: The expression to obtain the version of the tool +authors: + - "@YannVRB" +maintainers: + - "@YannVRB" diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test new file mode 100644 index 000000000000..e6d776522a2f --- /dev/null +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -0,0 +1,126 @@ +nextflow_process { + + name "Test Process MODKIT_DMR" + script "../main.nf" + tag "modules" + tag "modules_nfcore" + tag "modkit" + tag "modkit/dmr" + process "MODKIT_DMR" + config "./nextflow.config" + + test("[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta") { + + when { + params { + module_args = '--base C' + } + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) + ] + input[1] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) + ] + input[2] = Channel.of('chr22\t0\t40001') + .collectFile(name: 'chr22.bed', newLine: true) + .map { file -> [ [ id:'chr22' ], file ] } + input[3] = [ + [ id: 'test_ref' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta") { + + when { + params { + module_args = '--base C' + } + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) + ] + input[1] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) + ] + input[2] = [ [ id:'regions' ], [] ] + input[3] = [ + [ id: 'test_ref' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + + test("[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub") { + + options "-stub" + + when { + params { + module_args = '--base C' + } + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) + ] + input[1] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) + ] + input[2] = Channel.of('chr22\t0\t40001') + .collectFile(name: 'chr22.bed', newLine: true) + .map { file -> [ [ id:'chr22' ], file ] } + input[3] = [ + [ id: 'test_ref' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + ] + """ + } + } + + then { + assertAll ( + { assert process.success }, + { assert snapshot(process.out).match() } + ) + } + + } + +} diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap new file mode 100644 index 000000000000..7b82e67663bd --- /dev/null +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap @@ -0,0 +1,173 @@ +{ + "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test.log:md5,ee178bddc200e65da14440f5710f762e" + ] + ], + "2": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ], + "bed": [ + [ + { + "id": "test" + }, + "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" + ] + ], + "log": [ + [ + { + "id": "test" + }, + "test.log:md5,ee178bddc200e65da14440f5710f762e" + ] + ], + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.10.2" + }, + "timestamp": "2026-09-23T11:41:19.857952367" + }, + "[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test.log:md5,8861572cf65176e7d07256479c169269" + ] + ], + "2": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ], + "bed": [ + [ + { + "id": "test" + }, + "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" + ] + ], + "log": [ + [ + { + "id": "test" + }, + "test.log:md5,8861572cf65176e7d07256479c169269" + ] + ], + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.10.2" + }, + "timestamp": "2026-09-23T11:41:25.583014363" + }, + "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test" + }, + "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + { + "id": "test" + }, + "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "2": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ], + "bed": [ + [ + { + "id": "test" + }, + "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "log": [ + [ + { + "id": "test" + }, + "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ] + } + ], + "meta": { + "nf-test": "0.9.0", + "nextflow": "25.10.2" + }, + "timestamp": "2026-09-23T11:41:31.474420326" + } +} \ No newline at end of file diff --git a/modules/nf-core/modkit/dmr/tests/nextflow.config b/modules/nf-core/modkit/dmr/tests/nextflow.config new file mode 100644 index 000000000000..3ed1ba5e0f37 --- /dev/null +++ b/modules/nf-core/modkit/dmr/tests/nextflow.config @@ -0,0 +1,5 @@ +process { + withName: 'MODKIT_DMR' { + ext.args = params.module_args + } +} From 303926f99e5db1a30c8ca0db15dbb6d8e1672430 Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Thu, 24 Sep 2026 08:31:12 -0400 Subject: [PATCH 2/8] Add EDAM ontologies to modkit/dmr's meta.yml Addresses kubranarci's review comment on nf-core/modules#13021: bedmethyl in/out files get format_3003 (BED) + format_3989 (GZIP), their tabix indexes get format_3700, the optional regions_bed and reference fasta get format_3003 and format_1929 respectively. log output left as-is (no EDAM term fits an arbitrary tool log). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/meta.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/modules/nf-core/modkit/dmr/meta.yml b/modules/nf-core/modkit/dmr/meta.yml index a20cfda798cd..b59ff45093aa 100644 --- a/modules/nf-core/modkit/dmr/meta.yml +++ b/modules/nf-core/modkit/dmr/meta.yml @@ -26,13 +26,16 @@ input: description: Bgzipped bedMethyl file for the first (control) sample, as produced by modkit pileup with --bgzf pattern: "*.bed.gz" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3003" # BED + - edam: "http://edamontology.org/format_3989" # GZIP format - bedmethyl_a_tbi: type: file description: Tabix index for bedmethyl_a. Must have the same basename with a .tbi extension next to bedmethyl_a pattern: "*.bed.gz.tbi" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3700" # Tabix index format - - meta2: type: map description: | @@ -43,13 +46,16 @@ input: description: Bgzipped bedMethyl file for the second (experimental) sample, as produced by modkit pileup with --bgzf pattern: "*.bed.gz" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3003" # BED + - edam: "http://edamontology.org/format_3989" # GZIP format - bedmethyl_b_tbi: type: file description: Tabix index for bedmethyl_b. Must have the same basename with a .tbi extension next to bedmethyl_b pattern: "*.bed.gz.tbi" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3700" # Tabix index format - - meta3: type: map description: | @@ -60,7 +66,8 @@ input: description: Optional BED file of regions over which to compare methylation levels. When omitted, methylation levels are compared at each site instead pattern: "*.bed" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3003" # BED - - meta4: type: map description: | @@ -70,7 +77,8 @@ input: type: file description: Reference sequence in FASTA format, used during pileup/alignment pattern: "*.{fa,fasta}" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_1929" # FASTA output: bed: - - meta: @@ -83,7 +91,8 @@ output: description: BED file with the score column indicating the magnitude of the difference in methylation between the two samples pattern: "*.bed" - ontologies: [] + ontologies: + - edam: "http://edamontology.org/format_3003" # BED log: - - meta: type: map From 0eeb097121564303832a27d02f239c898f27dd10 Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Thu, 24 Sep 2026 08:54:10 -0400 Subject: [PATCH 3/8] Generate modkit/dmr's bedmethyl test fixture in-test instead of committing one Per kubranarci's review suggestion on #13021: chain modkit/pileup (--phased, on the existing genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam fixture) and htslib/bgziptabix (to index modkit pileup's --bgzf output, which is compressed but not indexed) in a setup block, instead of pointing at static test_hp1.bed.gz/test_hp2.bed.gz files. This removes the need for the companion test-datasets PR (#2289) entirely -- no new binary fixture required, and the test now exercises the exact real-world chain (MODKIT_PILEUP -> HTSLIB_BGZIPTABIX -> MODKIT_DMR) rather than a pre-generated snapshot of it. --modified-bases 5mC 5hmC on the pileup step (matching modkit/pileup's own phased test) avoids a real bug: this BAM's basecalling calls both 5mC and 5hmC, and requesting only one leaves the other counted as "other" in modkit's bedMethyl output, which MODKIT_DMR's internal consistency check rejects as invalid-bedmethyl-data. All 3 tests pass against the real chained output (verified: real per-region haplotype methylation delta values in the resulting .bed, not stub/fixture content). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/tests/main.nf.test | 101 ++++++++++++------ .../modkit/dmr/tests/main.nf.test.snap | 54 +++------- .../nf-core/modkit/dmr/tests/nextflow.config | 9 ++ 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index e6d776522a2f..49768be44420 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -6,9 +6,56 @@ nextflow_process { tag "modules_nfcore" tag "modkit" tag "modkit/dmr" + tag "modkit/pileup" + tag "htslib/bgziptabix" process "MODKIT_DMR" config "./nextflow.config" + setup { + run("MODKIT_PILEUP") { + script "../../pileup/main.nf" + process { + """ + input[0] = [ + [ id: 'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam.bai', checkIfExists: true) + ] + input[1] = [ + [ id: 'test_ref' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) + ] + input[2] = [[],[]] + """ + } + } + + run("HTSLIB_BGZIPTABIX") { + script "../../../htslib/bgziptabix/main.nf" + process { + """ + // MODKIT_PILEUP --phased emits one [meta, [combined, hp1, hp2]] tuple per sample; + // split it into one [meta, bedgz] tuple per haplotype so each can be tabix-indexed + // (modkit pileup --bgzf already bgzips, but never indexes) and fed to MODKIT_DMR + // as its own bedmethyl_a/bedmethyl_b input, exactly like real --modkit_phased usage. + input[0] = MODKIT_PILEUP.out.bedgz + .flatMap { meta, bedgz_files -> + bedgz_files + .findAll { f -> f.name =~ /_hp[12]\\.bed\\.gz\$/ } + .collect { f -> + def haplotype = (f.name =~ /_(hp[12])\\.bed\\.gz\$/)[0][1] + [ meta + [ id: "\${meta.id}_\${haplotype}" ], f, [], [] ] + } + } + input[1] = "compress" + input[2] = true + input[3] = [] + """ + } + } + } + test("[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta") { when { @@ -17,16 +64,14 @@ nextflow_process { } process { """ - input[0] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) - ] - input[1] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) - ] + input[0] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp1') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } + input[1] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp2') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } input[2] = Channel.of('chr22\t0\t40001') .collectFile(name: 'chr22.bed', newLine: true) .map { file -> [ [ id:'chr22' ], file ] } @@ -55,16 +100,14 @@ nextflow_process { } process { """ - input[0] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) - ] - input[1] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) - ] + input[0] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp1') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } + input[1] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp2') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } input[2] = [ [ id:'regions' ], [] ] input[3] = [ [ id: 'test_ref' ], @@ -93,16 +136,14 @@ nextflow_process { } process { """ - input[0] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp1.bed.gz.tbi', checkIfExists: true) - ] - input[1] = [ - [ id: 'test' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bedmethyl/test_hp2.bed.gz.tbi', checkIfExists: true) - ] + input[0] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp1') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } + input[1] = HTSLIB_BGZIPTABIX.out.output + .join(HTSLIB_BGZIPTABIX.out.index) + .filter { meta, bedgz, tbi -> meta.id.endsWith('_hp2') } + .map { meta, bedgz, tbi -> [ [ id: 'test' ], bedgz, tbi ] } input[2] = Channel.of('chr22\t0\t40001') .collectFile(name: 'chr22.bed', newLine: true) .map { file -> [ [ id:'chr22' ], file ] } diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap index 7b82e67663bd..1db00ee84052 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap @@ -15,7 +15,7 @@ { "id": "test" }, - "test.log:md5,ee178bddc200e65da14440f5710f762e" + "test.log:md5,7c42f282ac03cc14a2810bbbf8aab470" ] ], "2": [ @@ -38,7 +38,7 @@ { "id": "test" }, - "test.log:md5,ee178bddc200e65da14440f5710f762e" + "test.log:md5,7c42f282ac03cc14a2810bbbf8aab470" ] ], "versions_modkit": [ @@ -54,7 +54,7 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-23T11:41:19.857952367" + "timestamp": "2026-09-24T08:52:45.413964821" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta": { "content": [ @@ -72,7 +72,7 @@ { "id": "test" }, - "test.log:md5,8861572cf65176e7d07256479c169269" + "test.log:md5,3094b4a21e24767096a204c8333e9201" ] ], "2": [ @@ -95,7 +95,7 @@ { "id": "test" }, - "test.log:md5,8861572cf65176e7d07256479c169269" + "test.log:md5,3094b4a21e24767096a204c8333e9201" ] ], "versions_modkit": [ @@ -111,56 +111,28 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-23T11:41:25.583014363" + "timestamp": "2026-09-24T08:52:53.914290674" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub": { "content": [ { "0": [ - [ - { - "id": "test" - }, - "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "1": [ - [ - { - "id": "test" - }, - "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "2": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] + ], "bed": [ - [ - { - "id": "test" - }, - "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "log": [ - [ - { - "id": "test" - }, - "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" - ] + ], "versions_modkit": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] + ] } ], @@ -168,6 +140,6 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-23T11:41:31.474420326" + "timestamp": "2026-09-24T08:53:00.232562904" } } \ No newline at end of file diff --git a/modules/nf-core/modkit/dmr/tests/nextflow.config b/modules/nf-core/modkit/dmr/tests/nextflow.config index 3ed1ba5e0f37..a6b69568628b 100644 --- a/modules/nf-core/modkit/dmr/tests/nextflow.config +++ b/modules/nf-core/modkit/dmr/tests/nextflow.config @@ -1,4 +1,13 @@ process { + withName: 'MODKIT_PILEUP' { + // --modified-bases 5mC 5hmC (matching modkit/pileup's own phased test): if this BAM's + // basecalling calls both, requesting only one leaves the other counted as "other" in + // modkit's bedMethyl output, which MODKIT_DMR's internal consistency check rejects. + ext.args = '--phased --modified-bases 5mC 5hmC' + } + withName: 'HTSLIB_BGZIPTABIX' { + ext.args2 = '-p bed' + } withName: 'MODKIT_DMR' { ext.args = params.module_args } From d886465c00aa9f724c92fa89b30d84dd64c61ea6 Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Thu, 24 Sep 2026 09:12:18 -0400 Subject: [PATCH 4/8] Fix empty -stub test: handle modkit pileup's unsplit stub output The stub variant was silently passing with entirely empty output channels (caught by the user, not by the earlier green checkmark). Root cause: -stub-run makes the setup-chained MODKIT_PILEUP also run in stub mode, and its stub script never splits by haplotype -- it always emits one generic .bed.gz, with no _hp1/_hp2 suffix. The setup's regex filter for that suffix therefore matched nothing, so HTSLIB_BGZIPTABIX's setup run had an empty input channel and never executed, leaving MODKIT_DMR's own -stub test with nothing to consume either. Fixed by normalizing MODKIT_PILEUP.out.bedgz's payload to a list first ([x].flatten(), since Nextflow emits a single Path rather than a List when only one file matches the module's output glob -- the normal case for -stub), then falling back to reusing that single stub file for both haplotypes when no _hp[12] suffix is found. MODKIT_DMR's own stub script never reads its inputs, so reusing one file for both bedmethyl_a/b is correct for this case. Verified: the stub test now actually submits HTSLIB_BGZIPTABIX/MODKIT_DMR tasks and produces real (touch'd) output files, not an empty channel. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/tests/main.nf.test | 26 ++++++--- .../modkit/dmr/tests/main.nf.test.snap | 54 ++++++++++++++----- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index 49768be44420..d15258748182 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -35,18 +35,30 @@ nextflow_process { script "../../../htslib/bgziptabix/main.nf" process { """ - // MODKIT_PILEUP --phased emits one [meta, [combined, hp1, hp2]] tuple per sample; - // split it into one [meta, bedgz] tuple per haplotype so each can be tabix-indexed - // (modkit pileup --bgzf already bgzips, but never indexes) and fed to MODKIT_DMR - // as its own bedmethyl_a/bedmethyl_b input, exactly like real --modkit_phased usage. + // MODKIT_PILEUP --phased emits one [meta, [combined, hp1, hp2]] tuple per sample + // ("bedgz_files" is a single Path rather than a List when only one file matches + // the module's output glob -- true for -stub, since modkit pileup's stub script + // never splits by haplotype -- so normalize via [x].flatten() first). Split into + // one [meta, bedgz] tuple per haplotype so each can be tabix-indexed (modkit + // pileup --bgzf already bgzips, but never indexes) and fed to MODKIT_DMR as its + // own bedmethyl_a/bedmethyl_b input, exactly like real --modkit_phased usage. input[0] = MODKIT_PILEUP.out.bedgz .flatMap { meta, bedgz_files -> - bedgz_files - .findAll { f -> f.name =~ /_hp[12]\\.bed\\.gz\$/ } - .collect { f -> + def files = [ bedgz_files ].flatten() + def phased = files.findAll { f -> f.name =~ /_hp[12]\\.bed\\.gz\$/ } + if (phased) { + phased.collect { f -> def haplotype = (f.name =~ /_(hp[12])\\.bed\\.gz\$/)[0][1] [ meta + [ id: "\${meta.id}_\${haplotype}" ], f, [], [] ] } + } else { + // -stub: modkit pileup's stub always emits one generic, unsplit file + // (it never reads --phased in stub mode) -- reuse it for both + // haplotypes, since MODKIT_DMR's own stub never reads its inputs. + files.collectMany { f -> + ['hp1', 'hp2'].collect { haplotype -> [ meta + [ id: "\${meta.id}_\${haplotype}" ], f, [], [] ] } + } + } } input[1] = "compress" input[2] = true diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap index 1db00ee84052..b8ff079865ab 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap @@ -15,7 +15,7 @@ { "id": "test" }, - "test.log:md5,7c42f282ac03cc14a2810bbbf8aab470" + "test.log:md5,c30c1b33203d139e5648d847cac4ab05" ] ], "2": [ @@ -38,7 +38,7 @@ { "id": "test" }, - "test.log:md5,7c42f282ac03cc14a2810bbbf8aab470" + "test.log:md5,c30c1b33203d139e5648d847cac4ab05" ] ], "versions_modkit": [ @@ -54,7 +54,7 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T08:52:45.413964821" + "timestamp": "2026-09-24T09:10:47.411811907" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta": { "content": [ @@ -72,7 +72,7 @@ { "id": "test" }, - "test.log:md5,3094b4a21e24767096a204c8333e9201" + "test.log:md5,256a0a5feab178024236ffc0b3a18c38" ] ], "2": [ @@ -95,7 +95,7 @@ { "id": "test" }, - "test.log:md5,3094b4a21e24767096a204c8333e9201" + "test.log:md5,256a0a5feab178024236ffc0b3a18c38" ] ], "versions_modkit": [ @@ -111,28 +111,56 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T08:52:53.914290674" + "timestamp": "2026-09-24T09:10:55.676443925" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub": { "content": [ { "0": [ - + [ + { + "id": "test" + }, + "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "1": [ - + [ + { + "id": "test" + }, + "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "2": [ - + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] ], "bed": [ - + [ + { + "id": "test" + }, + "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "log": [ - + [ + { + "id": "test" + }, + "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" + ] ], "versions_modkit": [ - + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] ] } ], @@ -140,6 +168,6 @@ "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T08:53:00.232562904" + "timestamp": "2026-09-24T09:11:03.403287933" } } \ No newline at end of file From f574d7a9578df1284b64757b0d57a0f2084e6bee Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Mon, 28 Sep 2026 11:17:48 -0400 Subject: [PATCH 5/8] Assert log-file existence instead of snapshotting its content Per manascripts's review: modkit dmr pair's log includes a timestamp/runtime that varies between runs, making it unsuitable for an exact-content snapshot. Assert process.out.log.size() == 1 for existence, and snapshot only the deterministic outputs (bed, versions_modkit). Verified: all 3 tests pass against real output. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/tests/main.nf.test | 15 +- .../modkit/dmr/tests/main.nf.test.snap | 189 +++++------------- 2 files changed, 57 insertions(+), 147 deletions(-) diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index d15258748182..642f9d72df8e 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -98,7 +98,10 @@ nextflow_process { then { assertAll ( { assert process.success }, - { assert snapshot(process.out).match() } + // log-file content includes a timestamp/runtime that varies between runs -- + // assert existence only, and snapshot everything else. + { assert process.out.log.size() == 1 }, + { assert snapshot(process.out.bed, process.out.versions_modkit).match() } ) } @@ -132,7 +135,10 @@ nextflow_process { then { assertAll ( { assert process.success }, - { assert snapshot(process.out).match() } + // log-file content includes a timestamp/runtime that varies between runs -- + // assert existence only, and snapshot everything else. + { assert process.out.log.size() == 1 }, + { assert snapshot(process.out.bed, process.out.versions_modkit).match() } ) } @@ -170,7 +176,10 @@ nextflow_process { then { assertAll ( { assert process.success }, - { assert snapshot(process.out).match() } + // log-file content includes a timestamp/runtime that varies between runs -- + // assert existence only, and snapshot everything else. + { assert process.out.log.size() == 1 }, + { assert snapshot(process.out.bed, process.out.versions_modkit).match() } ) } diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap index b8ff079865ab..9816c1eae46a 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap @@ -1,173 +1,74 @@ { "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta": { "content": [ - { - "0": [ - [ - { - "id": "test" - }, - "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" - ] - ], - "1": [ - [ - { - "id": "test" - }, - "test.log:md5,c30c1b33203d139e5648d847cac4ab05" - ] - ], - "2": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] - ], - "bed": [ - [ - { - "id": "test" - }, - "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" - ] - ], - "log": [ - [ - { - "id": "test" - }, - "test.log:md5,c30c1b33203d139e5648d847cac4ab05" - ] - ], - "versions_modkit": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] + [ + [ + { + "id": "test" + }, + "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" ] - } + ], + [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] + ] ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T09:10:47.411811907" + "timestamp": "2026-09-28T11:10:25.124660502" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta": { "content": [ - { - "0": [ - [ - { - "id": "test" - }, - "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" - ] - ], - "1": [ - [ - { - "id": "test" - }, - "test.log:md5,256a0a5feab178024236ffc0b3a18c38" - ] - ], - "2": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] - ], - "bed": [ - [ - { - "id": "test" - }, - "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" - ] - ], - "log": [ - [ - { - "id": "test" - }, - "test.log:md5,256a0a5feab178024236ffc0b3a18c38" - ] - ], - "versions_modkit": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] + [ + [ + { + "id": "test" + }, + "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" + ] + ], + [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" ] - } + ] ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T09:10:55.676443925" + "timestamp": "2026-09-28T11:10:54.148420006" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub": { "content": [ - { - "0": [ - [ - { - "id": "test" - }, - "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "1": [ - [ - { - "id": "test" - }, - "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "2": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] - ], - "bed": [ - [ - { - "id": "test" - }, - "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "log": [ - [ - { - "id": "test" - }, - "test.log:md5,d41d8cd98f00b204e9800998ecf8427e" - ] - ], - "versions_modkit": [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" - ] + [ + [ + { + "id": "test" + }, + "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" ] - } + ] ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-24T09:11:03.403287933" + "timestamp": "2026-09-28T11:11:21.973535205" } } \ No newline at end of file From 25d5ea8a58c7b8f3f129ed572d901d31c703ba99 Mon Sep 17 00:00:00 2001 From: Manas Date: Tue, 29 Sep 2026 16:38:44 +0200 Subject: [PATCH 6/8] Update snapshot assertions for version in main.nf.test Tackles failing lint (still need to update snapshots) --- modules/nf-core/modkit/dmr/tests/main.nf.test | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index 642f9d72df8e..44256e9a7e40 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -101,7 +101,8 @@ nextflow_process { // log-file content includes a timestamp/runtime that varies between runs -- // assert existence only, and snapshot everything else. { assert process.out.log.size() == 1 }, - { assert snapshot(process.out.bed, process.out.versions_modkit).match() } + { assert snapshot(process.out.bed, process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } ) } @@ -138,7 +139,8 @@ nextflow_process { // log-file content includes a timestamp/runtime that varies between runs -- // assert existence only, and snapshot everything else. { assert process.out.log.size() == 1 }, - { assert snapshot(process.out.bed, process.out.versions_modkit).match() } + { assert snapshot(process.out.bed, process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } ) } From 0a6dc102d77fb1c639f5f42c849abf740321d488 Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Tue, 29 Sep 2026 11:01:43 -0400 Subject: [PATCH 7/8] Complete manascripts's lint fix: apply to the stub test too, regenerate snapshots manascripts's fix (25d5ea8a) applied process.out.findAll { key, val -> key.startsWith('versions') } to the two real-execution tests but not the third (-stub) test, which still had the old snapshot(process.out.bed, process.out.versions_modkit) form -- same substring-search lint failure would apply there too. Applied the same pattern there, then regenerated all 3 snapshots (their commit had updated the test code but not yet the .snap file, as their message noted). Verified against nf-core/tools' dev branch (same install CI uses, not the stale 2.14.1 in this environment's default conda env, which gives a false-positive CRITICAL 'name' on every module including known-good ones): nf-core modules lint modkit/dmr now passes cleanly, 62/62. All 3 nf-tests still pass against real output. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/tests/main.nf.test | 3 +- .../modkit/dmr/tests/main.nf.test.snap | 48 +++++++++++-------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index 44256e9a7e40..a0876b49c6e8 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -181,7 +181,8 @@ nextflow_process { // log-file content includes a timestamp/runtime that varies between runs -- // assert existence only, and snapshot everything else. { assert process.out.log.size() == 1 }, - { assert snapshot(process.out.bed, process.out.versions_modkit).match() } + { assert snapshot(process.out.bed, process.out.findAll { key, val -> key.startsWith('versions') } +).match() } ) } diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap index 9816c1eae46a..a7039d7dac2d 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test.snap +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test.snap @@ -9,19 +9,21 @@ "test.bed:md5,c57c0b35ce975dbafa19f3b4b5604dec" ] ], - [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" + { + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] ] - ] + } ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-28T11:10:25.124660502" + "timestamp": "2026-09-29T11:00:29.834449326" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], [], fasta": { "content": [ @@ -33,19 +35,21 @@ "test.bed:md5,e1ed04436fd4aee1c05fa4a612846417" ] ], - [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" + { + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] ] - ] + } ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-28T11:10:54.148420006" + "timestamp": "2026-09-29T11:00:38.70209544" }, "[bedmethyl_a, tbi], [bedmethyl_b, tbi], regions_bed, fasta - stub": { "content": [ @@ -57,18 +61,20 @@ "test.bed:md5,d41d8cd98f00b204e9800998ecf8427e" ] ], - [ - [ - "MODKIT_DMR", - "modkit", - "0.6.1" + { + "versions_modkit": [ + [ + "MODKIT_DMR", + "modkit", + "0.6.1" + ] ] - ] + } ], "meta": { "nf-test": "0.9.0", "nextflow": "25.10.2" }, - "timestamp": "2026-09-28T11:11:21.973535205" + "timestamp": "2026-09-29T11:00:46.52021922" } } \ No newline at end of file From fbbd19f488dcc8cb0318e92c895a431b791a237f Mon Sep 17 00:00:00 2001 From: Yann Vanrobaeys Date: Wed, 30 Sep 2026 09:47:37 -0400 Subject: [PATCH 8/8] Accept an optional fai alongside fasta, to avoid rebuilding it every run Per ljwharbers's review of the downstream lrsomatic PR: modkit dmr pair gets --ref with no .fai present, and has no --fai flag of its own -- it looks for .fai next to the reference automatically. Confirmed by direct testing: with no .fai present, modkit dmr pair builds one; with one already staged next to fasta, it's read (mtime unchanged) rather than rebuilt. For a several-GB genome, rebuilding that index on every task invocation is a real, avoidable cost. fasta is now a 3-tuple (meta, fasta, fai) instead of 2. fai is never referenced directly in the script -- passing it only makes Nextflow stage it alongside fasta so modkit's own auto-discovery finds it. Pass [] to omit, same as before. Verified: all 3 tests pass against real output (bed content is identical either way, as expected -- fai only affects whether the index gets rebuilt, not the DMR calculation), and nf-core modules lint modkit/dmr passes cleanly (62/62). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Wit78XR11V9AMAgY7gGrMT --- modules/nf-core/modkit/dmr/main.nf | 8 +++++++- modules/nf-core/modkit/dmr/meta.yml | 9 +++++++++ modules/nf-core/modkit/dmr/tests/main.nf.test | 9 ++++++--- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/modules/nf-core/modkit/dmr/main.nf b/modules/nf-core/modkit/dmr/main.nf index 2ee99685240c..0a5ce6799d25 100644 --- a/modules/nf-core/modkit/dmr/main.nf +++ b/modules/nf-core/modkit/dmr/main.nf @@ -11,7 +11,7 @@ process MODKIT_DMR { tuple val(meta), path(bedmethyl_a), path(bedmethyl_a_tbi) tuple val(meta2), path(bedmethyl_b), path(bedmethyl_b_tbi) tuple val(meta3), path(regions_bed) - tuple val(meta4), path(fasta) + tuple val(meta4), path(fasta), path(fai) output: tuple val(meta), path("*.bed"), emit: bed @@ -25,6 +25,12 @@ process MODKIT_DMR { def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" def regions = regions_bed ? "-r ${regions_bed}" : '' + // fai is never referenced directly -- modkit dmr pair has no --fai flag and looks for + // .fai next to --ref itself. Declaring it as an input only stages it alongside + // fasta so modkit finds and reuses it instead of building one from scratch, which for a + // multi-GB genome is a real, repeated cost across every task invocation. Confirmed by + // direct testing: with no .fai present, modkit dmr pair creates one; with one already + // staged next to fasta, it's read (unmodified mtime) rather than rebuilt. """ modkit \\ dmr pair \\ diff --git a/modules/nf-core/modkit/dmr/meta.yml b/modules/nf-core/modkit/dmr/meta.yml index b59ff45093aa..63c958daa17f 100644 --- a/modules/nf-core/modkit/dmr/meta.yml +++ b/modules/nf-core/modkit/dmr/meta.yml @@ -79,6 +79,15 @@ input: pattern: "*.{fa,fasta}" ontologies: - edam: "http://edamontology.org/format_1929" # FASTA + - fai: + type: file + description: | + Optional FASTA index for fasta. modkit dmr pair has no --fai flag and looks for + .fai next to --ref itself; passing one here only stages it alongside fasta + so modkit reuses it instead of building one from scratch on every invocation, which + for a multi-GB genome is a real, repeated cost. Pass `[]` to omit. + pattern: "*.fai" + ontologies: [] output: bed: - - meta: diff --git a/modules/nf-core/modkit/dmr/tests/main.nf.test b/modules/nf-core/modkit/dmr/tests/main.nf.test index a0876b49c6e8..d7d4e6de8cb1 100644 --- a/modules/nf-core/modkit/dmr/tests/main.nf.test +++ b/modules/nf-core/modkit/dmr/tests/main.nf.test @@ -89,7 +89,8 @@ nextflow_process { .map { file -> [ [ id:'chr22' ], file ] } input[3] = [ [ id: 'test_ref' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) ] """ } @@ -127,7 +128,8 @@ nextflow_process { input[2] = [ [ id:'regions' ], [] ] input[3] = [ [ id: 'test_ref' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) ] """ } @@ -169,7 +171,8 @@ nextflow_process { .map { file -> [ [ id:'chr22' ], file ] } input[3] = [ [ id: 'test_ref' ], - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true) + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/genome/genome.fasta.fai', checkIfExists: true) ] """ }