diff --git a/docs/adoption-plan.md b/docs/adoption-plan.md index 889eb36..12a4e76 100644 --- a/docs/adoption-plan.md +++ b/docs/adoption-plan.md @@ -71,13 +71,15 @@ Goal: make local workflow starters credible enough to become upstream submissions. Status: local nf-core-style and Snakemake wrapper-style starters are present, -pinned to the v0.5.0 Bioconda package and BioContainers image, and documented -as starters rather than official upstream submissions. +pinned to the v0.5.0 Bioconda package and BioContainers image, documented as +starters rather than official upstream submissions, and validated in dedicated +upstream-style checkouts on 2026-07-03. Next work: -- complete nf-core lint and test harness requirements -- complete Snakemake wrapper metadata, pin, and upstream test requirements +- prepare external PR branches for nf-core/modules and snakemake-wrappers +- repeat nf-core lint/test and Snakemake formatting/lint/pytest immediately + before opening upstream PRs - preserve the collect-then-gate pattern so JSON, TSV, HTML, and MultiQC evidence survives blocking FASTA results - keep `examples/workflows/check_fastaguard_gate.py` aligned with the JSON gate diff --git a/docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md b/docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md index c87743a..522fdd4 100644 --- a/docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md +++ b/docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md @@ -38,7 +38,7 @@ - Read FastaGuard JSON and return 0 for PASS, 1 for WARN, 2 for FAIL, 3 for malformed tool input. - Create: `tests/python/test_workflow_gate_helper.py` - Verify the gate helper exits correctly from small JSON fixtures. -- Create: `examples/snakemake/wrapper/environment.linux-64.pin.yaml` +- Create: `examples/snakemake/wrapper/environment.linux-64.pin.txt` - Local starter pin file for upstream wrapper preparation. - Create: `examples/snakemake/wrapper/test/Snakefile` - Official-wrapper-style local test Snakefile. @@ -90,7 +90,7 @@ Append these methods inside `AdoptionAssetsTest` before `test_benchmarking_docs_ readme = (wrapper / "README.md").read_text() test_snakefile = (wrapper / "test" / "Snakefile").read_text() test_py = (wrapper / "test" / "test_wrappers.py").read_text() - pin = (wrapper / "environment.linux-64.pin.yaml").read_text() + pin = (wrapper / "environment.linux-64.pin.txt").read_text() self.assertIn("safe local order", readme) self.assertIn("fastaguard=0.5.0", pin) @@ -454,7 +454,7 @@ Expected: PASS. ### Task 4: Harden Snakemake Wrapper Starter **Files:** -- Create: `examples/snakemake/wrapper/environment.linux-64.pin.yaml` +- Create: `examples/snakemake/wrapper/environment.linux-64.pin.txt` - Create: `examples/snakemake/wrapper/test/Snakefile` - Create: `examples/snakemake/wrapper/test/test_wrappers.py` - Create data fixtures under `examples/snakemake/wrapper/test/data/` @@ -462,7 +462,7 @@ Expected: PASS. - [ ] **Step 1: Add starter pin file** -Create `examples/snakemake/wrapper/environment.linux-64.pin.yaml`: +Create `examples/snakemake/wrapper/environment.linux-64.pin.txt`: ```yaml channels: @@ -490,7 +490,7 @@ rule fastaguard_pass: gate="pipeline", extra="" wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_warn: @@ -506,7 +506,7 @@ rule fastaguard_warn: gate="none", extra="" wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_fail: @@ -522,7 +522,7 @@ rule fastaguard_fail: gate="none", extra="" wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_invalid: @@ -538,7 +538,7 @@ rule fastaguard_invalid: gate="none", extra="" wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" ``` - [ ] **Step 3: Add copy-pasteable test runner snippet** @@ -609,7 +609,7 @@ Safe local order before upstream submission: 1. Run repository Python tests that inspect this wrapper layout. 2. Install Snakemake in a workflow test environment. 3. Run `snakemake -s test/Snakefile --cores 1 --use-conda`. -4. Generate a real upstream `environment.linux-64.pin.yaml` if the upstream +4. Generate a real upstream `environment.linux-64.pin.txt` if the upstream wrapper repository requires a solver-produced pin file. 5. Adapt `test/test_wrappers.py` into the upstream wrapper repository test harness. @@ -743,7 +743,7 @@ Run: ```bash git status --short -git add tests/python/test_adoption_assets.py tests/python/test_workflow_gate_helper.py docs/workflow-readiness.md docs/adoption-plan.md examples/nf-core/README.md examples/nf-core/modules/local/fastaguard/main.nf examples/nf-core/modules/local/fastaguard/meta.yml examples/nf-core/modules/local/fastaguard/tests examples/workflows/check_fastaguard_gate.py examples/snakemake/wrapper/README.md examples/snakemake/wrapper/environment.linux-64.pin.yaml examples/snakemake/wrapper/test docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md +git add tests/python/test_adoption_assets.py tests/python/test_workflow_gate_helper.py docs/workflow-readiness.md docs/adoption-plan.md examples/nf-core/README.md examples/nf-core/modules/local/fastaguard/main.nf examples/nf-core/modules/local/fastaguard/meta.yml examples/nf-core/modules/local/fastaguard/tests examples/workflows/check_fastaguard_gate.py examples/snakemake/wrapper/README.md examples/snakemake/wrapper/environment.linux-64.pin.txt examples/snakemake/wrapper/test docs/superpowers/plans/2026-07-03-upstream-workflow-prep.md git commit -m "docs: prepare upstream workflow starters" git push -u origin feature/upstream-workflow-prep ``` diff --git a/docs/workflow-readiness.md b/docs/workflow-readiness.md index d70a38d..b466007 100644 --- a/docs/workflow-readiness.md +++ b/docs/workflow-readiness.md @@ -21,6 +21,17 @@ The repository includes local starters for: These are workflow adoption starters. They are not yet an upstream nf-core module. They are not yet an official Snakemake wrapper. +External upstream-style validation was run on 2026-07-03 in dedicated checkouts: + +- `nf-core modules lint fastaguard`: 47 tests passed, 0 warnings, 0 failures. +- `nf-core modules test fastaguard --profile conda --once --no-prompts`: all + nf-test cases passed against Bioconda v0.5.0. +- Snakemake wrapper formatting: `black --check` and `snakefmt --check` passed. +- Snakemake wrapper lint: `snakemake --lint --snakefile test/Snakefile` passed. +- Snakemake wrapper pytest: `test_wrappers.py::test_fastaguard` passed with + PASS, WARN, FAIL, and invalid FASTA fixtures plus captured `exit_code` + outputs. + ## Safe Order 1. Run local repository tests first. @@ -60,18 +71,20 @@ matters. The important contract fields are: The local module already carries the expected interface shape: - input channel: `tuple val(meta), path(fasta)` -- outputs: HTML, JSON, TSV, MultiQC custom-content JSON, and versions metadata +- outputs: HTML, JSON, TSV, MultiQC custom-content JSON, captured exit code, + and versions metadata - runtime: `bioconda::fastaguard=0.5.0` - container: `quay.io/biocontainers/fastaguard:0.5.0--hfa8f182_0` - starter nf-test fixture layout for PASS, WARN, FAIL, and invalid FASTA cases - topic-aware `versions.yml` output for current nf-core version collection -Before an upstream nf-core module submission, complete this checklist: +Before an upstream nf-core module submission, repeat this checklist in a fresh +upstream checkout: - regenerate or validate the module against the current `nf-core/tools` template - run `nf-core modules lint fastaguard` -- run `nf-core modules test fastaguard` +- run `nf-core modules test fastaguard --profile conda --once --no-prompts` - adapt the local nf-test starter into the upstream repository layout - assert that `.fastaguard.json`, `.fastaguard.tsv`, `.fastaguard.html`, `.fastaguard_mqc.json`, and version outputs are produced @@ -90,14 +103,15 @@ The local wrapper starter already provides: - `wrapper.py` - `environment.yaml` - `meta.yaml` -- `environment.linux-64.pin.yaml` as a local starter pin file +- `environment.linux-64.pin.txt` as a local starter pin file - a copy-pasteable `Snakefile` - a `test/Snakefile` starter with PASS, WARN, FAIL, and invalid FASTA fixtures -- outputs for HTML, JSON, TSV, and MultiQC custom-content JSON +- outputs for HTML, JSON, TSV, MultiQC custom-content JSON, and captured exit + code Before an official Snakemake wrapper submission, complete this checklist: -- regenerate `environment.linux-64.pin.yaml` from the wrapper environment if +- regenerate `environment.linux-64.pin.txt` from the wrapper environment if the upstream repository requires a solver-produced pin file - adapt the local `test/Snakefile` and tiny FASTA fixtures - update `test_wrappers.py` so wrapper tests run in the upstream repository diff --git a/examples/nf-core/modules/local/fastaguard/main.nf b/examples/nf-core/modules/local/fastaguard/main.nf index 5d85f8f..1c21b5c 100644 --- a/examples/nf-core/modules/local/fastaguard/main.nf +++ b/examples/nf-core/modules/local/fastaguard/main.nf @@ -1,7 +1,11 @@ process FASTAGUARD { tag "$meta.id" label 'process_low' - container 'quay.io/biocontainers/fastaguard:0.5.0--hfa8f182_0' + + conda "${moduleDir}/environment.yml" + container "${ workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container ? + 'https://depot.galaxyproject.org/singularity/fastaguard:0.5.0--hfa8f182_0': + 'quay.io/biocontainers/fastaguard:0.5.0--hfa8f182_0' }" input: tuple val(meta), path(fasta) @@ -11,22 +15,30 @@ process FASTAGUARD { tuple val(meta), path("*.fastaguard.json"), emit: json tuple val(meta), path("*.fastaguard.tsv"), emit: tsv tuple val(meta), path("*.fastaguard_mqc.json"), emit: mqc - path "versions.yml", emit: versions, topic: versions + tuple val(meta), path("*.fastaguard.exit_code"), emit: exit_code + tuple val("${task.process}"), val('fastaguard'), eval('fastaguard --version | cut -d " " -f 2'), emit: versions_fastaguard, topic: versions + + when: + task.ext.when == null || task.ext.when script: def prefix = task.ext.prefix ?: meta.id + def args = task.ext.args ?: '--profile assembly --gate pipeline' """ + set +e fastaguard ${fasta} \ - --profile assembly \ - --gate pipeline \ + ${args} \ --out ${prefix}.fastaguard.html \ --json ${prefix}.fastaguard.json \ --tsv ${prefix}.fastaguard.tsv \ --multiqc ${prefix}.fastaguard_mqc.json + status=\$? + set -e + + printf "%s\\n" "\${status}" > ${prefix}.fastaguard.exit_code - cat <<-END_VERSIONS > versions.yml - "${task.process}": - fastaguard: \$(fastaguard --version | awk '{print \$2}') - END_VERSIONS + if [ "\${status}" -eq 3 ]; then + exit "\${status}" + fi """ } diff --git a/examples/nf-core/modules/local/fastaguard/meta.yml b/examples/nf-core/modules/local/fastaguard/meta.yml index 9950fc0..734cc8c 100644 --- a/examples/nf-core/modules/local/fastaguard/meta.yml +++ b/examples/nf-core/modules/local/fastaguard/meta.yml @@ -1,52 +1,108 @@ -name: fastaguard +name: "fastaguard" description: FASTA preflight QC for assembly pipelines keywords: - fasta - assembly - qc tools: - - fastaguard: + - "fastaguard": description: FASTA preflight QC for assembly pipelines - licence: ["MIT"] + homepage: "https://github.com/ehsanestaji/FastaGuard" + documentation: "https://github.com/ehsanestaji/FastaGuard" + tool_dev_url: "https://github.com/ehsanestaji/FastaGuard" + licence: + - "MIT" + identifier: "" input: - - meta: - type: map - description: Sample metadata map containing at least an id key. - - fasta: - type: file - description: Assembly FASTA file. - pattern: "*.{fa,fasta,fa.gz,fasta.gz}" + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - fasta: + type: file + description: Assembly FASTA file + pattern: "*.{fa,fasta,fa.gz,fasta.gz}" + ontologies: [] output: - - html: - type: file - description: FastaGuard HTML report. - pattern: "*.fastaguard.html" - - json: - type: file - description: FastaGuard JSON report. - pattern: "*.fastaguard.json" - - tsv: - type: file - description: FastaGuard TSV summary. - pattern: "*.fastaguard.tsv" - - mqc: - type: file - description: MultiQC custom-content JSON. - pattern: "*.fastaguard_mqc.json" - - versions: - type: file - description: Software versions file. - pattern: "versions.yml" + html: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "*.fastaguard.html": + type: file + description: FastaGuard HTML report + pattern: "*.fastaguard.html" + ontologies: [] + json: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "*.fastaguard.json": + type: file + description: FastaGuard JSON report + pattern: "*.fastaguard.json" + ontologies: + - edam: http://edamontology.org/format_3464 + tsv: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "*.fastaguard.tsv": + type: file + description: FastaGuard TSV summary + pattern: "*.fastaguard.tsv" + ontologies: + - edam: http://edamontology.org/format_3475 + mqc: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "*.fastaguard_mqc.json": + type: file + description: MultiQC custom-content JSON + pattern: "*.fastaguard_mqc.json" + ontologies: + - edam: http://edamontology.org/format_3464 + exit_code: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. [ id:'sample' ] + - "*.fastaguard.exit_code": + type: file + description: FastaGuard CLI exit code for collect-then-gate workflows + pattern: "*.fastaguard.exit_code" + ontologies: [] + versions_fastaguard: + - - ${task.process}: + type: string + description: The process the versions were collected from + - fastaguard: + type: string + description: The tool name + - fastaguard --version | cut -d " " -f 2: + type: eval + description: The expression to obtain the version of the tool topics: versions: - - - process: - type: string - description: The process the versions were collected from. - - tool: + - - ${task.process}: type: string - description: The tool name. - - version: + description: The process the versions were collected from + - fastaguard: type: string - description: The version reported by the tool. + description: The tool name + - fastaguard --version | cut -d " " -f 2: + type: eval + description: The expression to obtain the version of the tool authors: - - FastaGuard contributors + - "FastaGuard contributors" diff --git a/examples/nf-core/modules/local/fastaguard/tests/data/pass.fa b/examples/nf-core/modules/local/fastaguard/tests/data/pass.fa index 786da28..3d90f7c 100644 --- a/examples/nf-core/modules/local/fastaguard/tests/data/pass.fa +++ b/examples/nf-core/modules/local/fastaguard/tests/data/pass.fa @@ -1,4 +1,2 @@ ->contig1 -ACGTACGTACGTACGT ->contig2 -GCGCGCATATAT +>clean +ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT diff --git a/examples/nf-core/modules/local/fastaguard/tests/data/warn.fa b/examples/nf-core/modules/local/fastaguard/tests/data/warn.fa index e6a3cf8..5642616 100644 --- a/examples/nf-core/modules/local/fastaguard/tests/data/warn.fa +++ b/examples/nf-core/modules/local/fastaguard/tests/data/warn.fa @@ -1,4 +1,4 @@ ->tiny1 +>long +ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT +>tiny ACGT ->tiny2 -NNNNNNNN diff --git a/examples/nf-core/modules/local/fastaguard/tests/main.nf.test b/examples/nf-core/modules/local/fastaguard/tests/main.nf.test index 0020e74..abc00aa 100644 --- a/examples/nf-core/modules/local/fastaguard/tests/main.nf.test +++ b/examples/nf-core/modules/local/fastaguard/tests/main.nf.test @@ -4,90 +4,92 @@ nextflow_process { script "../main.nf" process "FASTAGUARD" + tag "modules" + tag "modules_nfcore" tag "fastaguard" tag "fastaguard_single" test("pass FASTA emits all reports") { - input[0] = [ - [id: "pass"], - file("data/pass.fa") - ] - when { process { """ - input[0] = Channel.of(input[0]) + input[0] = [ + [ id:'pass' ], + file("${moduleDir}/tests/data/pass.fa", checkIfExists: true) + ] """ } } then { assert process.success - assert process.out.html[0][1].name.endsWith(".fastaguard.html") - assert process.out.json[0][1].name.endsWith(".fastaguard.json") - assert process.out.tsv[0][1].name.endsWith(".fastaguard.tsv") - assert process.out.mqc[0][1].name.endsWith(".fastaguard_mqc.json") - assert process.out.versions + assert file(process.out.html[0][1]).name.endsWith(".fastaguard.html") + assert file(process.out.json[0][1]).name.endsWith(".fastaguard.json") + assert file(process.out.tsv[0][1]).name.endsWith(".fastaguard.tsv") + assert file(process.out.mqc[0][1]).name.endsWith(".fastaguard_mqc.json") + assert file(process.out.exit_code[0][1]).name.endsWith(".fastaguard.exit_code") + assert file(process.out.exit_code[0][1]).text.trim() == "0" + assert process.out.versions_fastaguard } } test("warn FASTA preserves reports") { - input[0] = [ - [id: "warn"], - file("data/warn.fa") - ] - when { process { """ - input[0] = Channel.of(input[0]) + input[0] = [ + [ id:'warn' ], + file("${moduleDir}/tests/data/warn.fa", checkIfExists: true) + ] """ } } then { + assert process.success assert process.out.json assert process.out.mqc + assert file(process.out.exit_code[0][1]).text.trim() == "1" } } test("fail FASTA preserves reports for gate review") { - input[0] = [ - [id: "fail"], - file("data/fail.fa") - ] - when { process { """ - input[0] = Channel.of(input[0]) + input[0] = [ + [ id:'fail' ], + file("${moduleDir}/tests/data/fail.fa", checkIfExists: true) + ] """ } } then { + assert process.success assert process.out.json assert process.out.mqc + assert file(process.out.exit_code[0][1]).text.trim() == "2" } } test("invalid FASTA is represented in the evidence path") { - input[0] = [ - [id: "invalid"], - file("data/invalid.fa") - ] - when { process { """ - input[0] = Channel.of(input[0]) + input[0] = [ + [ id:'invalid' ], + file("${moduleDir}/tests/data/invalid.fa", checkIfExists: true) + ] """ } } then { + assert process.success assert process.out.json assert process.out.mqc + assert file(process.out.exit_code[0][1]).text.trim() == "2" } } } diff --git a/examples/snakemake/wrapper/README.md b/examples/snakemake/wrapper/README.md index 7154866..eb14633 100644 --- a/examples/snakemake/wrapper/README.md +++ b/examples/snakemake/wrapper/README.md @@ -23,9 +23,11 @@ The wrapper command uses the v0.3 assembly gate: fastaguard sample.fa --profile assembly --gate pipeline ``` -That gate blocks downstream workflow steps on duplicate IDs, invalid characters, -invalid FASTA structure, and high-N content. GC and length outliers remain -advisory unless explicitly added with `--fail-on`. Gate failures intentionally exit with code `2` after writing reports, so downstream workflow steps stop while the JSON/HTML evidence remains available. +That gate marks duplicate IDs, invalid characters, invalid FASTA structure, and +high-N content as blocking findings. GC and length outliers remain advisory +unless explicitly added with `--fail-on`. The wrapper captures FastaGuard's +status in `fastaguard.exit_code` so workflows can route on PASS/WARN/FAIL while +retaining the JSON/HTML evidence. Tool-error status `3` still fails the job. The wrapper also includes a v0.5 Conda environment: @@ -58,7 +60,7 @@ Use this safe local order before upstream submission: 1. Run repository Python tests that inspect this wrapper layout. 2. Install Snakemake in a workflow test environment. 3. Run `snakemake -s test/Snakefile --cores 1 --use-conda`. -4. Generate a real upstream `environment.linux-64.pin.yaml` if the upstream +4. Generate a real upstream `environment.linux-64.pin.txt` if the upstream wrapper repository requires a solver-produced pin file. 5. Adapt `test/test_wrappers.py` into the upstream wrapper repository test harness. @@ -69,3 +71,4 @@ The wrapper emits: - `fastaguard.json` - `fastaguard.tsv` - `fastaguard_mqc.json` +- `fastaguard.exit_code` diff --git a/examples/snakemake/wrapper/Snakefile b/examples/snakemake/wrapper/Snakefile index f8160e1..1b22b88 100644 --- a/examples/snakemake/wrapper/Snakefile +++ b/examples/snakemake/wrapper/Snakefile @@ -1,15 +1,19 @@ rule fastaguard_wrapped: input: - fasta="sample.fa" + fasta="sample.fa", output: html="fastaguard_report.html", json="fastaguard.json", tsv="fastaguard.tsv", - multiqc="fastaguard_mqc.json" + multiqc="fastaguard_mqc.json", + exit_code="fastaguard.exit_code", + log: + "fastaguard.log", + conda: + "environment.yaml" params: profile="assembly", gate="pipeline", - extra="" - conda: "environment.yaml" + extra="", wrapper: - "file:wrapper/fastaguard" + "file:." diff --git a/examples/snakemake/wrapper/environment.linux-64.pin.txt b/examples/snakemake/wrapper/environment.linux-64.pin.txt new file mode 100644 index 0000000..0c6415c --- /dev/null +++ b/examples/snakemake/wrapper/environment.linux-64.pin.txt @@ -0,0 +1,9 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +# created by mamba dry-run with CONDA_OVERRIDE_GLIBC=2.17 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_19.conda#faac990cb7aedc7f3a2224f2c9b0c26c +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda#a9f577daf3de00bca7c3c76c0ecbd1de +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_19.conda#57736f29cc2b0ec0b6c2952d3f101b6a +https://conda.anaconda.org/bioconda/linux-64/fastaguard-0.5.0-hfa8f182_0.conda#0a395c874ce246dbdbe476ae2c96df78 diff --git a/examples/snakemake/wrapper/environment.linux-64.pin.yaml b/examples/snakemake/wrapper/environment.linux-64.pin.yaml deleted file mode 100644 index 220c3b3..0000000 --- a/examples/snakemake/wrapper/environment.linux-64.pin.yaml +++ /dev/null @@ -1,5 +0,0 @@ -channels: - - conda-forge - - bioconda -dependencies: - - fastaguard=0.5.0 diff --git a/examples/snakemake/wrapper/meta.yaml b/examples/snakemake/wrapper/meta.yaml index 9914541..f14a613 100644 --- a/examples/snakemake/wrapper/meta.yaml +++ b/examples/snakemake/wrapper/meta.yaml @@ -10,6 +10,7 @@ output: json: Machine-readable FastaGuard report. tsv: Tabular FastaGuard summary. multiqc: MultiQC custom-content JSON. + exit_code: Captured FastaGuard process status for workflow routing. params: profile: FastaGuard profile, normally assembly. gate: FastaGuard gate mode, normally pipeline or submission. diff --git a/examples/snakemake/wrapper/test/Snakefile b/examples/snakemake/wrapper/test/Snakefile index 904a70c..44da6fa 100644 --- a/examples/snakemake/wrapper/test/Snakefile +++ b/examples/snakemake/wrapper/test/Snakefile @@ -1,62 +1,74 @@ rule fastaguard_pass: input: - fasta="data/pass.fa" + fasta="data/pass.fa", output: html="pass/fastaguard_report.html", json="pass/fastaguard.json", tsv="pass/fastaguard.tsv", - multiqc="pass/fastaguard_mqc.json" + multiqc="pass/fastaguard_mqc.json", + exit_code="pass/fastaguard.exit_code", + log: + "logs/pass.fastaguard.log", params: profile="assembly", gate="pipeline", - extra="" + extra="", wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_warn: input: - fasta="data/warn.fa" + fasta="data/warn.fa", output: html="warn/fastaguard_report.html", json="warn/fastaguard.json", tsv="warn/fastaguard.tsv", - multiqc="warn/fastaguard_mqc.json" + multiqc="warn/fastaguard_mqc.json", + exit_code="warn/fastaguard.exit_code", + log: + "logs/warn.fastaguard.log", params: profile="assembly", - gate="none", - extra="" + gate="pipeline", + extra="", wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_fail: input: - fasta="data/fail.fa" + fasta="data/fail.fa", output: html="fail/fastaguard_report.html", json="fail/fastaguard.json", tsv="fail/fastaguard.tsv", - multiqc="fail/fastaguard_mqc.json" + multiqc="fail/fastaguard_mqc.json", + exit_code="fail/fastaguard.exit_code", + log: + "logs/fail.fastaguard.log", params: profile="assembly", - gate="none", - extra="" + gate="pipeline", + extra="", wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" rule fastaguard_invalid: input: - fasta="data/invalid.fa" + fasta="data/invalid.fa", output: html="invalid/fastaguard_report.html", json="invalid/fastaguard.json", tsv="invalid/fastaguard.tsv", - multiqc="invalid/fastaguard_mqc.json" + multiqc="invalid/fastaguard_mqc.json", + exit_code="invalid/fastaguard.exit_code", + log: + "logs/invalid.fastaguard.log", params: profile="assembly", - gate="none", - extra="" + gate="pipeline", + extra="", wrapper: - "file:../wrapper/fastaguard" + "master/bio/fastaguard" diff --git a/examples/snakemake/wrapper/test/data/pass.fa b/examples/snakemake/wrapper/test/data/pass.fa index 786da28..3d90f7c 100644 --- a/examples/snakemake/wrapper/test/data/pass.fa +++ b/examples/snakemake/wrapper/test/data/pass.fa @@ -1,4 +1,2 @@ ->contig1 -ACGTACGTACGTACGT ->contig2 -GCGCGCATATAT +>clean +ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT diff --git a/examples/snakemake/wrapper/test/data/warn.fa b/examples/snakemake/wrapper/test/data/warn.fa index e6a3cf8..5642616 100644 --- a/examples/snakemake/wrapper/test/data/warn.fa +++ b/examples/snakemake/wrapper/test/data/warn.fa @@ -1,4 +1,4 @@ ->tiny1 +>long +ACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGTACGT +>tiny ACGT ->tiny2 -NNNNNNNN diff --git a/examples/snakemake/wrapper/test/test_wrappers.py b/examples/snakemake/wrapper/test/test_wrappers.py index 16ddf0a..99f67e0 100644 --- a/examples/snakemake/wrapper/test/test_wrappers.py +++ b/examples/snakemake/wrapper/test/test_wrappers.py @@ -1,12 +1,18 @@ -"""Run this snippet with pytest in an upstream wrapper checkout.""" +"""Add this pytest test to an upstream snakemake-wrappers checkout.""" -import subprocess -from pathlib import Path - -def test_fastaguard_wrapper(): - snakefile = Path(__file__).with_name("Snakefile") - subprocess.run( - ["snakemake", "-s", str(snakefile), "--cores", "1", "--use-conda"], - check=True, +def test_fastaguard(run): + run( + "bio/fastaguard", + [ + "snakemake", + "pass/fastaguard.json", + "warn/fastaguard.json", + "fail/fastaguard.json", + "invalid/fastaguard.json", + "pass/fastaguard.exit_code", + "warn/fastaguard.exit_code", + "fail/fastaguard.exit_code", + "invalid/fastaguard.exit_code", + ], ) diff --git a/examples/snakemake/wrapper/wrapper.py b/examples/snakemake/wrapper/wrapper.py new file mode 100644 index 0000000..0416ffc --- /dev/null +++ b/examples/snakemake/wrapper/wrapper.py @@ -0,0 +1,23 @@ +from snakemake.shell import shell + +profile = snakemake.params.get("profile", "assembly") +gate = snakemake.params.get("gate", "pipeline") +extra = snakemake.params.get("extra", "") +log = snakemake.log_fmt_shell(stdout=True, stderr=True) + +shell( + "set +e; " + "fastaguard {snakemake.input.fasta} " + "--profile {profile} " + "--gate {gate} " + "--out {snakemake.output.html} " + "--json {snakemake.output.json} " + "--tsv {snakemake.output.tsv} " + "--multiqc {snakemake.output.multiqc} " + "{extra} " + "{log}; " + 'status="$?"; ' + "set -e; " + 'printf "%s\\n" "$status" > {snakemake.output.exit_code}; ' + 'if [ "$status" -eq 3 ]; then exit "$status"; fi' +) diff --git a/examples/snakemake/wrapper/wrapper/fastaguard/wrapper.py b/examples/snakemake/wrapper/wrapper/fastaguard/wrapper.py index 17150f7..0416ffc 100644 --- a/examples/snakemake/wrapper/wrapper/fastaguard/wrapper.py +++ b/examples/snakemake/wrapper/wrapper/fastaguard/wrapper.py @@ -3,8 +3,10 @@ profile = snakemake.params.get("profile", "assembly") gate = snakemake.params.get("gate", "pipeline") extra = snakemake.params.get("extra", "") +log = snakemake.log_fmt_shell(stdout=True, stderr=True) shell( + "set +e; " "fastaguard {snakemake.input.fasta} " "--profile {profile} " "--gate {gate} " @@ -12,5 +14,10 @@ "--json {snakemake.output.json} " "--tsv {snakemake.output.tsv} " "--multiqc {snakemake.output.multiqc} " - "{extra}" + "{extra} " + "{log}; " + 'status="$?"; ' + "set -e; " + 'printf "%s\\n" "$status" > {snakemake.output.exit_code}; ' + 'if [ "$status" -eq 3 ]; then exit "$status"; fi' ) diff --git a/tests/python/test_adoption_assets.py b/tests/python/test_adoption_assets.py index 9532545..2e872b8 100644 --- a/tests/python/test_adoption_assets.py +++ b/tests/python/test_adoption_assets.py @@ -85,7 +85,7 @@ def test_v0_3_gate_examples_do_not_pin_v0_2_runtimes(self): self.assertNotIn("fastaguard=0.2.0", wrapper_env) self.assertIn("--gate {gate}", wrapper_py) self.assertIn("Gate failures intentionally exit with code `2`", nf_core_readme) - self.assertIn("Gate failures intentionally exit with code `2`", snakemake_readme) + self.assertIn("fastaguard.exit_code", snakemake_readme) def test_v0_4_docs_explain_preflight_readiness_and_compare_mode(self): readme = ROOT / "README.md" @@ -562,7 +562,7 @@ def test_workflow_readiness_plan_defines_upstream_submission_path(self): self.assertIn("nf-core modules lint", readiness) self.assertIn("nf-core modules test", readiness) self.assertIn("topic channels", readiness) - self.assertIn("environment.linux-64.pin.yaml", readiness) + self.assertIn("environment.linux-64.pin.txt", readiness) self.assertIn("test_wrappers.py", readiness) self.assertIn("--gate submission", readiness) self.assertIn("quay.io/biocontainers/fastaguard:0.5.0--hfa8f182_0", readiness) @@ -579,8 +579,8 @@ def test_nf_core_starter_has_upstream_prep_test_layout(self): meta_yml = (module / "meta.yml").read_text() nf_test = (module / "tests" / "main.nf.test").read_text() - self.assertIn('path "versions.yml", emit: versions, topic: versions', main_nf) - self.assertIn("versions:", meta_yml) + self.assertIn("emit: versions_fastaguard, topic: versions", main_nf) + self.assertIn("versions_fastaguard:", meta_yml) self.assertIn("topic", meta_yml) self.assertIn('process "FASTAGUARD"', nf_test) self.assertIn("pass.fa", nf_test) @@ -591,15 +591,62 @@ def test_nf_core_starter_has_upstream_prep_test_layout(self): for name in ("pass.fa", "warn.fa", "fail.fa", "invalid.fa"): self.assertTrue((module / "tests" / "data" / name).exists(), name) + def test_nf_core_starter_matches_current_nf_core_module_shape(self): + module = ROOT / "examples" / "nf-core" / "modules" / "local" / "fastaguard" + main_nf = (module / "main.nf").read_text() + meta_yml = (module / "meta.yml").read_text() + nf_test = (module / "tests" / "main.nf.test").read_text() + + self.assertIn('conda "${moduleDir}/environment.yml"', main_nf) + self.assertIn("workflow.containerEngine in ['singularity', 'apptainer']", main_nf) + self.assertIn( + "https://depot.galaxyproject.org/singularity/fastaguard:0.5.0--hfa8f182_0", + main_nf, + ) + self.assertIn( + "quay.io/biocontainers/fastaguard:0.5.0--hfa8f182_0", + main_nf, + ) + self.assertIn("\n when:\n task.ext.when == null || task.ext.when\n", main_nf) + self.assertIn("emit: versions_fastaguard, topic: versions", main_nf) + self.assertIn("emit: exit_code", main_nf) + self.assertIn("output:\n html:", meta_yml) + self.assertNotIn("output:\n - html:", meta_yml) + self.assertIn("versions_fastaguard:", meta_yml) + self.assertIn('fastaguard --version | cut -d " " -f 2:', meta_yml) + self.assertIn("exit_code:", meta_yml) + self.assertIn('tag "modules"', nf_test) + self.assertIn('tag "modules_nfcore"', nf_test) + self.assertIn('file("${moduleDir}/tests/data/pass.fa"', nf_test) + self.assertNotIn("file('data/pass.fa'", nf_test) + self.assertIn("file(process.out.html[0][1]).name.endsWith", nf_test) + self.assertNotIn("process.out.html[0][1].name.endsWith", nf_test) + + warn_fasta = (module / "tests" / "data" / "warn.fa").read_text() + self.assertIn(">long", warn_fasta) + self.assertIn(">tiny", warn_fasta) + self.assertNotIn("NNNNNNNN", warn_fasta) + + pass_fasta = (module / "tests" / "data" / "pass.fa").read_text() + self.assertIn(">clean", pass_fasta) + self.assertNotIn(">contig1", pass_fasta) + def test_snakemake_wrapper_has_upstream_prep_test_layout(self): wrapper = ROOT / "examples" / "snakemake" / "wrapper" readme = (wrapper / "README.md").read_text() + wrapper_py = (wrapper / "wrapper.py").read_text() test_snakefile = (wrapper / "test" / "Snakefile").read_text() test_py = (wrapper / "test" / "test_wrappers.py").read_text() - pin = (wrapper / "environment.linux-64.pin.yaml").read_text() + pin = (wrapper / "environment.linux-64.pin.txt").read_text() self.assertIn("safe local order", readme) - self.assertIn("fastaguard=0.5.0", pin) + self.assertIn("fastaguard", pin) + self.assertIn("fastaguard {snakemake.input.fasta}", wrapper_py) + self.assertIn("snakemake.output.exit_code", wrapper_py) + self.assertIn('status" -eq 3', wrapper_py) + self.assertIn('"master/bio/fastaguard"', test_snakefile) + self.assertNotIn('"file:../wrapper/fastaguard"', test_snakefile) + self.assertIn('exit_code="pass/fastaguard.exit_code"', test_snakefile) self.assertIn("rule fastaguard_pass", test_snakefile) self.assertIn("rule fastaguard_warn", test_snakefile) self.assertIn("rule fastaguard_fail", test_snakefile) @@ -609,6 +656,13 @@ def test_snakemake_wrapper_has_upstream_prep_test_layout(self): for name in ("pass.fa", "warn.fa", "fail.fa", "invalid.fa"): self.assertTrue((wrapper / "test" / "data" / name).exists(), name) + pass_fasta = (wrapper / "test" / "data" / "pass.fa").read_text() + warn_fasta = (wrapper / "test" / "data" / "warn.fa").read_text() + self.assertIn(">clean", pass_fasta) + self.assertIn(">long", warn_fasta) + self.assertIn(">tiny", warn_fasta) + self.assertNotIn("NNNNNNNN", warn_fasta) + def test_workflow_readiness_safe_order_is_explicit(self): readiness = (ROOT / "docs" / "workflow-readiness.md").read_text() adoption = (ROOT / "docs" / "adoption-plan.md").read_text() @@ -983,7 +1037,7 @@ def test_snakemake_wrapper_declares_bioconda_environment(self): " - fastaguard=0.5.0", ], ) - self.assertIn('conda: "environment.yaml"', snakefile.read_text()) + self.assertIn("conda:\n \"environment.yaml\"", snakefile.read_text()) if __name__ == "__main__": diff --git a/tests/python/test_release_metadata.py b/tests/python/test_release_metadata.py index c31d793..76e9dd4 100644 --- a/tests/python/test_release_metadata.py +++ b/tests/python/test_release_metadata.py @@ -78,7 +78,8 @@ def test_release_ready_bioconda_recipe_requires_real_sha(self): placeholders = [ path for path in tracked_paths - if marker in (ROOT / path).read_text(errors="ignore") + if (ROOT / path).exists() + and marker in (ROOT / path).read_text(errors="ignore") ] self.assertEqual(placeholders, [])