From 858a86279ff550d1284b35894743a5ba2850c438 Mon Sep 17 00:00:00 2001 From: manascripts Date: Fri, 15 May 2026 11:51:13 +0200 Subject: [PATCH 1/2] New module: savana/run --- modules/nf-core/savana/run/environment.yml | 7 ++ modules/nf-core/savana/run/main.nf | 49 ++++++++++ modules/nf-core/savana/run/meta.yml | 90 +++++++++++++++++++ modules/nf-core/savana/run/tests/main.nf.test | 85 ++++++++++++++++++ 4 files changed, 231 insertions(+) create mode 100644 modules/nf-core/savana/run/environment.yml create mode 100644 modules/nf-core/savana/run/main.nf create mode 100644 modules/nf-core/savana/run/meta.yml create mode 100644 modules/nf-core/savana/run/tests/main.nf.test diff --git a/modules/nf-core/savana/run/environment.yml b/modules/nf-core/savana/run/environment.yml new file mode 100644 index 00000000000..60063b5bf6b --- /dev/null +++ b/modules/nf-core/savana/run/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: + - "bioconda::savana=1.3.7" diff --git a/modules/nf-core/savana/run/main.nf b/modules/nf-core/savana/run/main.nf new file mode 100644 index 00000000000..da275b8fb78 --- /dev/null +++ b/modules/nf-core/savana/run/main.nf @@ -0,0 +1,49 @@ +process SAVANA_RUN { + tag "${meta.id}" + label 'process_high' + + conda "${moduleDir}/environment.yml" + container "${workflow.containerEngine in ['singularity', 'apptainer'] && !task.ext.singularity_pull_docker_container + ? 'https://depot.galaxyproject.org/singularity/savana:1.3.7--pyhdfd78af_0' + : 'quay.io/biocontainers/savana:1.3.7--pyhdfd78af_0'}" + + input: + tuple val(meta), path(tumour), path(tumour_index), path(normal), path(normal_index), path(ref), path(ref_index) + + output: + tuple val(meta), path("${prefix}.sv_breakpoints.vcf"), emit: sv_breakpoints_vcf + tuple val(meta), path("${prefix}.sv_breakpoints.bedpe"), emit: sv_breakpoints_bedpe + tuple val(meta), path("${prefix}.sv_breakpoints_read_support.tsv"), emit: sv_breakpoints_read_support + tuple val(meta), path("${prefix}.inserted_sequences.fa"), emit: inserted_sequences + tuple val("${task.process}"), val("savana"), eval("python -c \"import importlib.metadata as m; print(m.version('savana'))\""), emit: versions, topic: versions + + when: + task.ext.when == null || task.ext.when + + script: + def args = (task.ext.args ?: '').trim() + prefix = task.ext.prefix ?: "${meta.id}" + def outdir = "." + """ + savana run \\ + --tumour ${tumour} \\ + --normal ${normal} \\ + --ref ${ref} \\ + --ref_index ${ref_index} \\ + --outdir ${outdir} \\ + --sample ${prefix} \\ + --threads ${task.cpus ?: 1} \\ + ${args} + """ + + stub: + prefix = task.ext.prefix ?: "${meta.id}" + def outdir = "." + """ + mkdir -p ${outdir} + touch ${outdir}/${prefix}.sv_breakpoints.vcf + touch ${outdir}/${prefix}.sv_breakpoints.bedpe + touch ${outdir}/${prefix}.sv_breakpoints_read_support.tsv + touch ${outdir}/${prefix}.inserted_sequences.fa + """ +} diff --git a/modules/nf-core/savana/run/meta.yml b/modules/nf-core/savana/run/meta.yml new file mode 100644 index 00000000000..00a7d4b0c75 --- /dev/null +++ b/modules/nf-core/savana/run/meta.yml @@ -0,0 +1,90 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: "savana_run" +description: "Identify and cluster SV breakpoints from long-read alignments." +keywords: + - structural variants + - long read + - genomics +tools: + - "savana": + description: "SAVANA: a somatic structural variant and copy-number caller for long-read data." + homepage: "https://github.com/cortes-ciriano-lab/savana" + documentation: "https://github.com/cortes-ciriano-lab/savana" + tool_dev_url: "https://github.com/cortes-ciriano-lab/savana" + doi: "10.1038/s41592-025-02708-0" + licence: ["Apache-2.0"] + identifier: "" + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information + e.g. `[ id:'sample1' ]` + - bam: + type: file + description: Sorted BAM/CRAM file + pattern: "*.{bam,cram}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_2573" # CRAM + +output: + sv_breakpoints_vcf: + - - meta: + type: map + description: Sample information + - "*.sv_breakpoints.vcf": + type: file + description: Raw SV breakpoints in VCF format + pattern: "*.sv_breakpoints.vcf" + sv_breakpoints_bedpe: + - - meta: + type: map + description: Sample information + - "*.sv_breakpoints.bedpe": + type: file + description: Raw SV breakpoints in BEDPE format + pattern: "*.sv_breakpoints.bedpe" + sv_breakpoints_read_support: + - - meta: + type: map + description: Sample information + - "*_sv_breakpoints_read_support.tsv": + type: file + description: Read-support summary for each variant in TSV format + pattern: "*_sv_breakpoints_read_support.tsv" + inserted_sequences: + - - meta: + type: map + description: Sample information + - "*.inserted_sequences.fa": + type: file + description: Information on insertions in FASTA format + pattern: "*.inserted_sequences.fa" + versions: + - - ${task.process}: + type: string + description: Process name + - savana: + type: string + description: Tool version + - python -c "import importlib.metadata as m; print(m.version('savana'))": + type: eval + description: Command to obtain the version of the tool + +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - savana: + type: string + description: The name of the tool + - python -c "import importlib.metadata as m; print(m.version('savana'))": + type: eval + description: The expression to obtain the version of the tool +authors: + - "@manascripts" +maintainers: + - "@manascripts" diff --git a/modules/nf-core/savana/run/tests/main.nf.test b/modules/nf-core/savana/run/tests/main.nf.test new file mode 100644 index 00000000000..1890e56e9c9 --- /dev/null +++ b/modules/nf-core/savana/run/tests/main.nf.test @@ -0,0 +1,85 @@ +nextflow_process { + + name "Test Process SAVANA_RUN" + script "../main.nf" + process "SAVANA_RUN" + + tag "modules" + tag "modules_nfcore" + tag "savana" + tag "savana/run" + + test("homo_sapiens - nanopore") { + + when { + 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), + 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), + 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) + ] + """ + } + } + + then { + assert process.success + assert snapshot(sanitizeOutput(process.out)).match() + } + } + + test("homo_sapiens - pacbio") { + + when { + process { + """ + input[0] = [ + [ id:'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam.bai', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam.bai', 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) + ] + """ + } + } + + then { + assert process.success + assert snapshot(sanitizeOutput(process.out)).match() + } + } + + test("homo_sapiens - nanopore - stub") { + + options "-stub" + + when { + 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), + 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), + 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) + ] + """ + } + } + + then { + assert process.success + assert snapshot(sanitizeOutput(process.out)).match() + } + } +} From 3859080a4542457d75234bbe8a830f08463ac059 Mon Sep 17 00:00:00 2001 From: manascripts Date: Fri, 15 May 2026 14:25:50 +0200 Subject: [PATCH 2/2] Update tests and meta.yml --- modules/nf-core/savana/run/main.nf | 5 +- modules/nf-core/savana/run/meta.yml | 84 +++++++++++----- modules/nf-core/savana/run/tests/main.nf.test | 52 +++++++--- .../savana/run/tests/main.nf.test.snap | 96 +++++++++++++++++++ 4 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 modules/nf-core/savana/run/tests/main.nf.test.snap diff --git a/modules/nf-core/savana/run/main.nf b/modules/nf-core/savana/run/main.nf index da275b8fb78..ff55160da49 100644 --- a/modules/nf-core/savana/run/main.nf +++ b/modules/nf-core/savana/run/main.nf @@ -8,14 +8,15 @@ process SAVANA_RUN { : 'quay.io/biocontainers/savana:1.3.7--pyhdfd78af_0'}" input: - tuple val(meta), path(tumour), path(tumour_index), path(normal), path(normal_index), path(ref), path(ref_index) + tuple val(meta), path(tumour), path(tumour_index), path(normal), path(normal_index) + tuple val(meta2),path(ref), path(ref_index) output: tuple val(meta), path("${prefix}.sv_breakpoints.vcf"), emit: sv_breakpoints_vcf tuple val(meta), path("${prefix}.sv_breakpoints.bedpe"), emit: sv_breakpoints_bedpe tuple val(meta), path("${prefix}.sv_breakpoints_read_support.tsv"), emit: sv_breakpoints_read_support tuple val(meta), path("${prefix}.inserted_sequences.fa"), emit: inserted_sequences - tuple val("${task.process}"), val("savana"), eval("python -c \"import importlib.metadata as m; print(m.version('savana'))\""), emit: versions, topic: versions + tuple val("${task.process}"), val("savana"), eval("python -c \"import importlib.metadata as m; print(m.version('savana'))\""), emit: versions_savana, topic: versions when: task.ext.when == null || task.ext.when diff --git a/modules/nf-core/savana/run/meta.yml b/modules/nf-core/savana/run/meta.yml index 00a7d4b0c75..4061b89912d 100644 --- a/modules/nf-core/savana/run/meta.yml +++ b/modules/nf-core/savana/run/meta.yml @@ -1,4 +1,3 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json name: "savana_run" description: "Identify and cluster SV breakpoints from long-read alignments." keywords: @@ -7,62 +6,104 @@ keywords: - genomics tools: - "savana": - description: "SAVANA: a somatic structural variant and copy-number caller for long-read data." + description: "SAVANA: a somatic structural variant and copy-number caller for + long-read data." homepage: "https://github.com/cortes-ciriano-lab/savana" documentation: "https://github.com/cortes-ciriano-lab/savana" tool_dev_url: "https://github.com/cortes-ciriano-lab/savana" doi: "10.1038/s41592-025-02708-0" - licence: ["Apache-2.0"] + licence: + - "Apache-2.0" identifier: "" - input: - - meta: type: map description: | Groovy Map containing sample information e.g. `[ id:'sample1' ]` - - bam: + - tumour: type: file - description: Sorted BAM/CRAM file + description: Tumour BAM/CRAM file pattern: "*.{bam,cram}" ontologies: - edam: "http://edamontology.org/format_2572" # BAM - - edam: "http://edamontology.org/format_2573" # CRAM - + - edam: "http://edamontology.org/format_3462" # CRAM + - tumour_index: + type: file + description: Tumour BAM/CRAM index + pattern: "*.{bai,crai}" + ontologies: [] + - normal: + type: file + description: Normal BAM/CRAM file + pattern: "*.{bam,cram}" + ontologies: + - edam: "http://edamontology.org/format_2572" # BAM + - edam: "http://edamontology.org/format_3462" # CRAM + - normal_index: + type: file + description: Normal BAM/CRAM index + pattern: "*.{bai,crai}" + ontologies: [] + - - meta2: + type: map + description: | + Groovy Map containing reference information + e.g. `[ id:'hg38' ]` + - ref: + type: file + description: Reference genome FASTA file used to align tumor and normal + BAM/CRAM files + pattern: "*.{fa,fasta}" + ontologies: + - edam: "http://edamontology.org/format_1929" # FASTA + - ref_index: + type: file + description: Reference FASTA index + pattern: "*.fai" + ontologies: [] output: sv_breakpoints_vcf: - - meta: type: map description: Sample information - - "*.sv_breakpoints.vcf": + - ${prefix}.sv_breakpoints.vcf: type: file - description: Raw SV breakpoints in VCF format + description: VCF file containing raw SV breakpoints pattern: "*.sv_breakpoints.vcf" + ontologies: + - edam: "http://edamontology.org/format_3016" sv_breakpoints_bedpe: - - meta: type: map description: Sample information - - "*.sv_breakpoints.bedpe": + - ${prefix}.sv_breakpoints.bedpe: type: file - description: Raw SV breakpoints in BEDPE format + description: BEDPE file containing SV breakpoints pattern: "*.sv_breakpoints.bedpe" + ontologies: + - edam: "http://edamontology.org/format_3003" # BED sv_breakpoints_read_support: - - meta: type: map description: Sample information - - "*_sv_breakpoints_read_support.tsv": + - ${prefix}.sv_breakpoints_read_support.tsv: type: file - description: Read-support summary for each variant in TSV format - pattern: "*_sv_breakpoints_read_support.tsv" + description: TSV file containing read support information for each SV breakpoint + pattern: "*.sv_breakpoints_read_support.tsv" + ontologies: + - edam: "http://edamontology.org/format_3475" # TSV inserted_sequences: - - meta: type: map description: Sample information - - "*.inserted_sequences.fa": + - ${prefix}.inserted_sequences.fa: type: file - description: Information on insertions in FASTA format + description: FASTA file containing inserted sequences pattern: "*.inserted_sequences.fa" - versions: + ontologies: + - edam: "http://edamontology.org/format_1929" # FASTA + versions_savana: - - ${task.process}: type: string description: Process name @@ -72,18 +113,17 @@ output: - python -c "import importlib.metadata as m; print(m.version('savana'))": type: eval description: Command to obtain the version of the tool - topics: versions: - - ${task.process}: type: string - description: The name of the process + description: Process name - savana: type: string - description: The name of the tool + description: Tool version - python -c "import importlib.metadata as m; print(m.version('savana'))": type: eval - description: The expression to obtain the version of the tool + description: Command to obtain the version of the tool authors: - "@manascripts" maintainers: diff --git a/modules/nf-core/savana/run/tests/main.nf.test b/modules/nf-core/savana/run/tests/main.nf.test index 1890e56e9c9..f397e17585c 100644 --- a/modules/nf-core/savana/run/tests/main.nf.test +++ b/modules/nf-core/savana/run/tests/main.nf.test @@ -16,10 +16,14 @@ nextflow_process { """ input[0] = [ [ id:'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam.bai', checkIfExists: true), 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), - 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), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam.bai', checkIfExists: true) + ] + + input[1] = [ + [ id:'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) ] @@ -28,8 +32,16 @@ nextflow_process { } then { - assert process.success - assert snapshot(sanitizeOutput(process.out)).match() + assertAll( + { assert process.success }, + { assert snapshot( + path(process.out.sv_breakpoints_vcf.get(0).get(1)).vcf.summary, + file(process.out.sv_breakpoints_bedpe.get(0).get(1)).name, + file(process.out.sv_breakpoints_read_support.get(0).get(1)).readLines().size(), + file(process.out.inserted_sequences.get(0).get(1)).name, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) } } @@ -42,8 +54,12 @@ nextflow_process { [ id:'test' ], file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam', checkIfExists: true), file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam.bai', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam', checkIfExists: true), - file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test.sorted.bam.bai', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test_hifi_aligned_to_assembly.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/pacbio/bam/test_hifi_aligned_to_assembly.bam.bai', checkIfExists: true) + ] + + input[1] = [ + [ id:'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) ] @@ -52,8 +68,16 @@ nextflow_process { } then { - assert process.success - assert snapshot(sanitizeOutput(process.out)).match() + assertAll( + { assert process.success }, + { assert snapshot( + path(process.out.sv_breakpoints_vcf.get(0).get(1)).vcf.summary, + file(process.out.sv_breakpoints_bedpe.get(0).get(1)).name, + file(process.out.sv_breakpoints_read_support.get(0).get(1)).readLines().size(), + file(process.out.inserted_sequences.get(0).get(1)).name, + process.out.findAll { key, val -> key.startsWith('versions') } + ).match() } + ) } } @@ -66,10 +90,14 @@ nextflow_process { """ input[0] = [ [ id:'test' ], + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam', checkIfExists: true), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test2.sorted.bam.bai', checkIfExists: true), 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), - 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), + file(params.modules_testdata_base_path + 'genomics/homo_sapiens/nanopore/bam/test.sorted.phased.bam.bai', checkIfExists: true) + ] + + input[1] = [ + [ id:'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) ] diff --git a/modules/nf-core/savana/run/tests/main.nf.test.snap b/modules/nf-core/savana/run/tests/main.nf.test.snap new file mode 100644 index 00000000000..3e5563f05bc --- /dev/null +++ b/modules/nf-core/savana/run/tests/main.nf.test.snap @@ -0,0 +1,96 @@ +{ + "homo_sapiens - pacbio": { + "content": [ + "VcfFile [chromosomes=[], sampleCount=1, variantCount=0, phased=true, phasedAutodetect=true]", + "test.sv_breakpoints.bedpe", + 1, + "test.inserted_sequences.fa", + { + "versions_savana": [ + [ + "SAVANA_RUN", + "savana", + "1.3.7" + ] + ] + } + ], + "timestamp": "2026-05-15T14:24:46.790612822", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + }, + "homo_sapiens - nanopore": { + "content": [ + "VcfFile [chromosomes=[chr22], sampleCount=1, variantCount=28, phased=false, phasedAutodetect=false]", + "test.sv_breakpoints.bedpe", + 16, + "test.inserted_sequences.fa", + { + "versions_savana": [ + [ + "SAVANA_RUN", + "savana", + "1.3.7" + ] + ] + } + ], + "timestamp": "2026-05-15T14:24:30.660792995", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + }, + "homo_sapiens - nanopore - stub": { + "content": [ + { + "inserted_sequences": [ + [ + { + "id": "test" + }, + "test.inserted_sequences.fa:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "sv_breakpoints_bedpe": [ + [ + { + "id": "test" + }, + "test.sv_breakpoints.bedpe:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "sv_breakpoints_read_support": [ + [ + { + "id": "test" + }, + "test.sv_breakpoints_read_support.tsv:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "sv_breakpoints_vcf": [ + [ + { + "id": "test" + }, + "test.sv_breakpoints.vcf:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_savana": [ + [ + "SAVANA_RUN", + "savana", + "1.3.7" + ] + ] + } + ], + "timestamp": "2026-05-15T14:24:57.388853983", + "meta": { + "nf-test": "0.9.5", + "nextflow": "25.10.4" + } + } +} \ No newline at end of file