diff --git a/cg/cli/workflow/nf_analysis.py b/cg/cli/workflow/nf_analysis.py index a4012949bb..e4e6d69e87 100644 --- a/cg/cli/workflow/nf_analysis.py +++ b/cg/cli/workflow/nf_analysis.py @@ -77,6 +77,13 @@ default=None, help="NF-Tower ID of run to relaunch. If not provided the latest NF-Tower ID for a case will be used.", ) +OPTION_FROM_START = click.option( + "--from-start", + is_flag=True, + default=False, + show_default=True, + help="Start workflow from start without resuming execution", +) @click.command("metrics-deliver") diff --git a/cg/cli/workflow/raredisease/base.py b/cg/cli/workflow/raredisease/base.py index d94b9dbc1c..5553436ac7 100644 --- a/cg/cli/workflow/raredisease/base.py +++ b/cg/cli/workflow/raredisease/base.py @@ -3,13 +3,16 @@ import logging import click +from pydantic.v1 import ValidationError from cg.cli.utils import echo_lines from cg.cli.workflow.commands import ARGUMENT_CASE_ID, OPTION_DRY -from cg.constants.constants import MetaApis +from cg.constants.constants import DRY_RUN, MetaApis from cg.meta.workflow.analysis import AnalysisAPI from cg.meta.workflow.raredisease import RarediseaseAnalysisAPI from cg.models.cg_config import CGConfig +from cg.exc import CgError + LOG = logging.getLogger(__name__) @@ -22,6 +25,22 @@ def raredisease(context: click.Context) -> None: context.obj.meta_apis[MetaApis.ANALYSIS_API] = RarediseaseAnalysisAPI(config=context.obj) +@raredisease.command("config-case") +@ARGUMENT_CASE_ID +@DRY_RUN +@click.pass_obj +def config_case(context: CGConfig, case_id: str, dry_run: bool) -> None: + """Create sample sheet file and params file for a given case.""" + analysis_api: RarediseaseAnalysisAPI = context.meta_apis[MetaApis.ANALYSIS_API] + LOG.info(f"Creating config files for {case_id}.") + try: + analysis_api.status_db.verify_case_exists(case_internal_id=case_id) + analysis_api.write_config_case(case_id=case_id, dry_run=dry_run) + except (CgError, ValidationError) as error: + LOG.error(f"Could not create config files for {case_id}: {error}") + raise click.Abort() from error + + @raredisease.command("panel") @OPTION_DRY @ARGUMENT_CASE_ID diff --git a/cg/cli/workflow/rnafusion/base.py b/cg/cli/workflow/rnafusion/base.py index 236c67f329..192e16a9d8 100644 --- a/cg/cli/workflow/rnafusion/base.py +++ b/cg/cli/workflow/rnafusion/base.py @@ -11,6 +11,7 @@ from cg.cli.workflow.nf_analysis import ( OPTION_COMPUTE_ENV, OPTION_CONFIG, + OPTION_FROM_START, OPTION_LOG, OPTION_PARAMS_FILE, OPTION_PROFILE, @@ -22,7 +23,6 @@ report_deliver, ) from cg.cli.workflow.rnafusion.options import ( - OPTION_FROM_START, OPTION_REFERENCES, OPTION_STRANDEDNESS, ) diff --git a/cg/cli/workflow/rnafusion/options.py b/cg/cli/workflow/rnafusion/options.py index 05beab3ad3..554639ae7b 100644 --- a/cg/cli/workflow/rnafusion/options.py +++ b/cg/cli/workflow/rnafusion/options.py @@ -2,14 +2,6 @@ from cg.constants.constants import Strandedness -OPTION_FROM_START = click.option( - "--from-start", - is_flag=True, - default=False, - show_default=True, - help="Start workflow from start without resuming execution", -) - OPTION_STRANDEDNESS = click.option( "--strandedness", type=str, diff --git a/cg/cli/workflow/taxprofiler/base.py b/cg/cli/workflow/taxprofiler/base.py index 4c68a77956..8dcd774e2a 100644 --- a/cg/cli/workflow/taxprofiler/base.py +++ b/cg/cli/workflow/taxprofiler/base.py @@ -9,6 +9,7 @@ from cg.cli.workflow.nf_analysis import ( OPTION_COMPUTE_ENV, OPTION_CONFIG, + OPTION_FROM_START, OPTION_LOG, OPTION_PARAMS_FILE, OPTION_PROFILE, @@ -19,7 +20,9 @@ metrics_deliver, report_deliver, ) -from cg.cli.workflow.taxprofiler.options import OPTION_FROM_START, OPTION_INSTRUMENT_PLATFORM +from cg.cli.workflow.taxprofiler.options import ( + OPTION_INSTRUMENT_PLATFORM, +) from cg.constants import EXIT_FAIL, EXIT_SUCCESS from cg.constants.constants import DRY_RUN, CaseActions, MetaApis from cg.constants.nf_analysis import NfTowerStatus diff --git a/cg/cli/workflow/taxprofiler/options.py b/cg/cli/workflow/taxprofiler/options.py index b9a1c04243..66f43f1fd3 100644 --- a/cg/cli/workflow/taxprofiler/options.py +++ b/cg/cli/workflow/taxprofiler/options.py @@ -2,14 +2,6 @@ from cg.constants.sequencing import SequencingPlatform -OPTION_FROM_START = click.option( - "--from-start", - is_flag=True, - default=False, - show_default=True, - help="Start workflow from the start", -) - OPTION_INSTRUMENT_PLATFORM = click.option( "--instrument-platform", show_default=True, diff --git a/cg/constants/constants.py b/cg/constants/constants.py index 8b7b611fb5..b90603e386 100644 --- a/cg/constants/constants.py +++ b/cg/constants/constants.py @@ -181,6 +181,7 @@ class HastaSlurmPartitions(StrEnum): class FileExtensions(StrEnum): BED: str = ".bed" COMPLETE: str = ".complete" + CONFIG: str = ".config" CRAM: str = ".cram" CSV: str = ".csv" FASTQ: str = ".fastq" diff --git a/cg/io/config.py b/cg/io/config.py new file mode 100644 index 0000000000..107fcb94dd --- /dev/null +++ b/cg/io/config.py @@ -0,0 +1,17 @@ +"""Module to read or write config files""" + +from pathlib import Path +from typing import Any +from cg.constants.symbols import EMPTY_STRING + + +def write_config_nextflow_style(content: dict[str, Any] | None) -> str: + """Write content to stream accepted by Nextflow config files with non-quoted booleans and quoted strings.""" + string: str = EMPTY_STRING + double_quotes: str = '"' + for parameter, value in content.items(): + if isinstance(value, Path): + value: str = value.as_posix() + quotes = double_quotes if type(value) is str else EMPTY_STRING + string += f"params.{parameter} = {quotes}{value}{quotes}\n" + return string diff --git a/cg/io/txt.py b/cg/io/txt.py index 7f3a19162a..0f0402c791 100644 --- a/cg/io/txt.py +++ b/cg/io/txt.py @@ -1,7 +1,8 @@ """Module to read or write txt files.""" from pathlib import Path -from typing import Any +from typing import List, Optional +from cg.constants.symbols import EMPTY_STRING def read_txt(file_path: Path, read_to_string: bool = False) -> list[str] | str: @@ -19,3 +20,17 @@ def write_txt(content: list[str] | str, file_path: Path) -> None: file.writelines(content) else: file.write(content) + + +def concat_txt( + file_paths: list[Path], target_file: Path, str_content: Optional[List[str]] = None +) -> None: + """Concatenate files and eventual string content.""" + content: str = EMPTY_STRING + if str_content: + for txt in str_content: + content += f"{txt}\n" + for file_path in file_paths: + file_content: str = read_txt(file_path, read_to_string=True) + content += f"{file_content}\n" + write_txt(content=content, file_path=target_file) diff --git a/cg/meta/workflow/nf_analysis.py b/cg/meta/workflow/nf_analysis.py index 700f4fc0e5..2f92737753 100644 --- a/cg/meta/workflow/nf_analysis.py +++ b/cg/meta/workflow/nf_analysis.py @@ -148,6 +148,9 @@ def get_workdir_path(self, case_id: str, work_dir: Path | None = None) -> Path: return work_dir.absolute() return Path(self.get_case_path(case_id), NFX_WORK_DIR) + def set_cluster_options(self, case_id: str) -> str: + return f'process.clusterOptions = "-A {self.account} --qos={self.get_slurm_qos_for_case(case_id=case_id)}"\n' + @staticmethod def extract_read_files( metadata: list[FastqFileMeta], forward_read: bool = False, reverse_read: bool = False diff --git a/cg/meta/workflow/raredisease.py b/cg/meta/workflow/raredisease.py index 011fe4711b..951d955c15 100644 --- a/cg/meta/workflow/raredisease.py +++ b/cg/meta/workflow/raredisease.py @@ -1,13 +1,26 @@ """Module for Raredisease Analysis API.""" import logging +from typing import Any from pathlib import Path +from cg.io.txt import concat_txt +from cg.io.config import write_config_nextflow_style from cg.constants import GenePanelMasterList, Workflow +from cg.constants.constants import FileExtensions +from cg.constants.subject import PlinkPhenotypeStatus, PlinkSex from cg.constants.gene_panel import GENOME_BUILD_37 from cg.meta.workflow.analysis import add_gene_panel_combo from cg.meta.workflow.nf_analysis import NfAnalysisAPI from cg.models.cg_config import CGConfig +from cg.models.fastq import FastqFileMeta +from cg.models.raredisease.raredisease import ( + RarediseaseSampleSheetEntry, + RarediseaseSampleSheetHeaders, +) +from cg.models.nf_analysis import WorkflowParameters +from cg.store.models import Case, CaseSample + LOG = logging.getLogger(__name__) @@ -22,6 +35,129 @@ def __init__( workflow: Workflow = Workflow.RAREDISEASE, ): super().__init__(config=config, workflow=workflow) + self.root_dir: str = config.raredisease.root + self.nfcore_workflow_path: str = config.raredisease.workflow_path + self.references: str = config.raredisease.references + self.profile: str = config.raredisease.profile + self.conda_env: str = config.raredisease.conda_env + self.conda_binary: str = config.raredisease.conda_binary + self.config_platform: str = config.raredisease.config_platform + self.config_params: str = config.raredisease.config_params + self.config_resources: str = config.raredisease.config_resources + self.tower_binary_path: str = config.tower_binary_path + self.tower_workflow: str = config.raredisease.tower_workflow + self.account: str = config.raredisease.slurm.account + self.compute_env: str = config.raredisease.compute_env + self.revision: str = config.raredisease.revision + + def write_config_case( + self, + case_id: str, + dry_run: bool, + ) -> None: + """Create a parameter (.config) files and a Nextflow sample sheet input for Raredisease analysis.""" + self.create_case_directory(case_id=case_id, dry_run=dry_run) + sample_sheet_content: list[list[Any]] = self.get_sample_sheet_content(case_id=case_id) + workflow_parameters: WorkflowParameters = self.get_workflow_parameters(case_id=case_id) + if dry_run: + LOG.info("Dry run: nextflow sample sheet and parameter file will not be written") + return + self.write_sample_sheet( + content=sample_sheet_content, + file_path=self.get_sample_sheet_path(case_id=case_id), + header=RarediseaseSampleSheetHeaders.headers(), + ) + self.write_params_file(case_id=case_id, workflow_parameters=workflow_parameters.dict()) + + def get_sample_sheet_content_per_sample( + self, case: Case = "", case_sample: CaseSample = "" + ) -> list[list[str]]: + """Get sample sheet content per sample.""" + sample_metadata: list[FastqFileMeta] = self.gather_file_metadata_for_sample( + case_sample.sample + ) + fastq_forward_read_paths: list[str] = self.extract_read_files( + metadata=sample_metadata, forward_read=True + ) + fastq_reverse_read_paths: list[str] = self.extract_read_files( + metadata=sample_metadata, reverse_read=True + ) + sample_sheet_entry = RarediseaseSampleSheetEntry( + name=case_sample.sample.internal_id, + fastq_forward_read_paths=fastq_forward_read_paths, + fastq_reverse_read_paths=fastq_reverse_read_paths, + sex=self.get_sex_code(case_sample.sample.sex), + phenotype=self.get_phenotype_code(case_sample.status), + paternal_id=case_sample.get_paternal_sample_id, + maternal_id=case_sample.get_maternal_sample_id, + case_id=case.internal_id, + ) + return sample_sheet_entry.reformat_sample_content + + def get_sample_sheet_content( + self, + case_id: str, + ) -> list[list[Any]]: + """Return Raredisease nextflow sample sheet content for a case.""" + case: Case = self.status_db.get_case_by_internal_id(internal_id=case_id) + sample_sheet_content = [] + LOG.info("Getting sample sheet information") + LOG.info(f"Samples linked to case {case_id}: {len(case.links)}") + for link in case.links: + sample_sheet_content.extend( + self.get_sample_sheet_content_per_sample(case=case, case_sample=link) + ) + return sample_sheet_content + + def get_workflow_parameters(self, case_id: str) -> WorkflowParameters: + """Return parameters.""" + LOG.info("Getting parameters information") + return WorkflowParameters( + sample_sheet_path=self.get_sample_sheet_path(case_id=case_id), + outdir=self.get_case_path(case_id=case_id), + ) + + def get_params_file_path(self, case_id: str, params_file: Path | None = None) -> Path: + """Return parameters file or a path where the default parameters file for a case id should be located.""" + if params_file: + return params_file.absolute() + case_path: Path = self.get_case_path(case_id) + return Path(case_path, f"{case_id}_params_file{FileExtensions.CONFIG}") + # This function should be moved to nf-analysis to replace the current one when all nextflow pipelines are using the same config files approach + + def write_params_file(self, case_id: str, workflow_parameters: dict) -> None: + """Write params-file for analysis.""" + LOG.debug("Writing parameters file") + config_files_list = [self.config_platform, self.config_params, self.config_resources] + extra_parameters_str = [ + write_config_nextflow_style(workflow_parameters), + self.set_cluster_options(case_id=case_id), + ] + concat_txt( + file_paths=config_files_list, + target_file=self.get_params_file_path(case_id=case_id), + str_content=extra_parameters_str, + ) + + @staticmethod + def get_phenotype_code(phenotype: str) -> int: + """Return Raredisease phenotype code.""" + LOG.debug("Translate phenotype to integer code") + try: + code = PlinkPhenotypeStatus[phenotype.upper()] + except KeyError: + raise ValueError(f"{phenotype} is not a valid phenotype") + return code + + @staticmethod + def get_sex_code(sex: str) -> int: + """Return Raredisease sex code.""" + LOG.debug("Translate sex to integer code") + try: + code = PlinkSex[sex.upper()] + except KeyError: + raise ValueError(f"{sex} is not a valid sex") + return code @property def root(self) -> str: diff --git a/cg/models/cg_config.py b/cg/models/cg_config.py index f69e62cdd4..63f38a0fb3 100644 --- a/cg/models/cg_config.py +++ b/cg/models/cg_config.py @@ -168,10 +168,14 @@ class MipConfig(BaseModel): script: str -class RareDiseaseConfig(CommonAppConfig): +class RarediseaseConfig(CommonAppConfig): + binary_path: str | None = None compute_env: str conda_binary: str | None = None conda_env: str + config_platform: str + config_params: str + config_resources: str launch_directory: str workflow_path: str profile: str @@ -339,7 +343,7 @@ class CGConfig(BaseModel): mip_rd_dna: MipConfig = Field(None, alias="mip-rd-dna") mip_rd_rna: MipConfig = Field(None, alias="mip-rd-rna") mutant: MutantConfig = None - raredisease: RareDiseaseConfig = Field(None, alias="raredisease") + raredisease: RarediseaseConfig = Field(None, alias="raredisease") rnafusion: RnafusionConfig = Field(None, alias="rnafusion") taxprofiler: TaxprofilerConfig = Field(None, alias="taxprofiler") diff --git a/cg/models/nf_analysis.py b/cg/models/nf_analysis.py index 06fe623598..3b6a305c0c 100644 --- a/cg/models/nf_analysis.py +++ b/cg/models/nf_analysis.py @@ -5,16 +5,16 @@ from cg.exc import SampleSheetError -class PipelineParameters(BaseModel): - clusterOptions: str = Field(..., alias="cluster_options") - priority: str +class WorkflowParameters(BaseModel): + input: Path = Field(..., alias="sample_sheet_path") + outdir: Path = Field(..., alias="outdir") class NextflowSampleSheetEntry(BaseModel): - """Nextflow samplesheet model. + """Nextflow sample sheet model. Attributes: - name: sample name, corresponds to case_id + name: sample name, or case id fastq_forward_read_paths: list of all fastq read1 file paths corresponding to sample fastq_reverse_read_paths: list of all fastq read2 file paths corresponding to sample """ diff --git a/cg/models/raredisease/raredisease.py b/cg/models/raredisease/raredisease.py new file mode 100644 index 0000000000..33b8ffa07e --- /dev/null +++ b/cg/models/raredisease/raredisease.py @@ -0,0 +1,54 @@ +from pathlib import Path + +from pydantic.v1 import Field +from enum import StrEnum + + +from cg.models.nf_analysis import NextflowSampleSheetEntry + + +class RarediseaseSampleSheetEntry(NextflowSampleSheetEntry): + """Raredisease sample model is used when building the sample sheet.""" + + sex: str + phenotype: int + sex: int + paternal_id: str + maternal_id: str + case_id: str + + @property + def reformat_sample_content(self) -> list[list[str]]: + """Reformat sample sheet content as a list of lists, where each list represents a line in the final file.""" + return [ + [ + self.name, + lane + 1, + self.fastq_forward_read_paths, + self.fastq_reverse_read_paths, + self.sex, + self.phenotype, + self.paternal_id, + self.maternal_id, + self.case_id, + ] + for lane, (self.fastq_forward_read_paths, self.fastq_reverse_read_paths) in enumerate( + zip(self.fastq_forward_read_paths, self.fastq_reverse_read_paths) + ) + ] + + +class RarediseaseSampleSheetHeaders(StrEnum): + sample: str = "sample" + lane: str = "lane" + fastq_1: str = "fastq_1" + fastq_2: str = "fastq_2" + sex: str = "sex" + phenotype: str = "phenotype" + paternal_id: str = "paternal_id" + maternal_id: str = "maternal_id" + case_id: str = "case_id" + + @classmethod + def headers(cls) -> list[str]: + return list(map(lambda header: header.value, cls)) diff --git a/cg/models/rnafusion/rnafusion.py b/cg/models/rnafusion/rnafusion.py index 9140807a6b..9e9b209d5c 100644 --- a/cg/models/rnafusion/rnafusion.py +++ b/cg/models/rnafusion/rnafusion.py @@ -4,7 +4,7 @@ from cg.constants.constants import Strandedness from cg.models.analysis import AnalysisModel -from cg.models.nf_analysis import NextflowSampleSheetEntry, PipelineParameters +from cg.models.nf_analysis import NextflowSampleSheetEntry, WorkflowParameters class RnafusionQCMetrics(BaseModel): @@ -25,7 +25,7 @@ class RnafusionQCMetrics(BaseModel): uniquely_mapped_percent: float | None -class RnafusionParameters(PipelineParameters): +class RnafusionParameters(WorkflowParameters): """Rnafusion parameters.""" genomes_base: Path diff --git a/cg/models/taxprofiler/taxprofiler.py b/cg/models/taxprofiler/taxprofiler.py index c9b224c1f2..9a33274ebf 100644 --- a/cg/models/taxprofiler/taxprofiler.py +++ b/cg/models/taxprofiler/taxprofiler.py @@ -3,7 +3,7 @@ from pydantic import Field, BaseModel from cg.constants.sequencing import SequencingPlatform -from cg.models.nf_analysis import NextflowSampleSheetEntry, PipelineParameters +from cg.models.nf_analysis import NextflowSampleSheetEntry, WorkflowParameters class TaxprofilerQCMetrics(BaseModel): @@ -15,7 +15,7 @@ class TaxprofilerQCMetrics(BaseModel): paired_aligned_none: float | None -class TaxprofilerParameters(PipelineParameters): +class TaxprofilerParameters(WorkflowParameters): """Model for Taxprofiler parameters.""" input: Path = Field(..., alias="sample_sheet_path") diff --git a/cg/store/models.py b/cg/store/models.py index 7b5fa7e9f4..86c6b086e5 100644 --- a/cg/store/models.py +++ b/cg/store/models.py @@ -29,6 +29,7 @@ StatusOptions, ) from cg.constants.priority import SlurmQos +from cg.constants.symbols import EMPTY_STRING BigInt = Annotated[int, None] Blob = Annotated[bytes, None] @@ -648,6 +649,16 @@ def to_dict(self, parents: bool = False, samples: bool = False, family: bool = F def __str__(self) -> str: return f"{self.case.internal_id} | {self.sample.internal_id}" + @property + def get_maternal_sample_id(self) -> str: + """Return parental id.""" + return self.mother.internal_id if self.mother else EMPTY_STRING + + @property + def get_paternal_sample_id(self) -> str: + """Return parental id.""" + return self.father.internal_id if self.father else EMPTY_STRING + class Flowcell(Base): __tablename__ = "flowcell" diff --git a/tests/cli/workflow/raredisease/test_cli_raredisease_config_case.py b/tests/cli/workflow/raredisease/test_cli_raredisease_config_case.py new file mode 100644 index 0000000000..1b4b5607c0 --- /dev/null +++ b/tests/cli/workflow/raredisease/test_cli_raredisease_config_case.py @@ -0,0 +1,92 @@ +"""Tests cli methods to create the case config for Raredisease.""" + +import logging + +from _pytest.logging import LogCaptureFixture +from click.testing import CliRunner + +from cg.cli.workflow.raredisease.base import config_case +from cg.constants import EXIT_SUCCESS +from cg.models.cg_config import CGConfig + + +def test_config_case_dry_run( + cli_runner: CliRunner, + raredisease_context: CGConfig, + caplog: LogCaptureFixture, + raredisease_case_id: str, +): + """Test dry-run.""" + caplog.set_level(logging.DEBUG) + + # GIVEN a valid case + + # WHEN performing a dry-run + result = cli_runner.invoke(config_case, [raredisease_case_id, "-d"], obj=raredisease_context) + + # THEN command should should exit successfully + assert result.exit_code == EXIT_SUCCESS + + # THEN sample sheet and parameters information should be collected + assert "Getting sample sheet information" in caplog.text + assert "Getting parameters information" in caplog.text + + # THEN sample sheet and parameters information files should not be written + + assert "Dry run: nextflow sample sheet and parameter file will not be written" in caplog.text + assert "Writing sample sheet" not in caplog.text + assert "Writing parameters file" not in caplog.text + + +def test_config_case_without_samples( + cli_runner: CliRunner, + raredisease_context: CGConfig, + caplog: LogCaptureFixture, + no_sample_case_id: str, +): + """Test config_case with a case without samples.""" + caplog.set_level(logging.ERROR) + # GIVEN a case + + # WHEN running config case + result = cli_runner.invoke(config_case, [no_sample_case_id], obj=raredisease_context) + + # THEN command should not exit successfully + assert result.exit_code != EXIT_SUCCESS + + # THEN warning should be printed that no sample is found + assert no_sample_case_id in caplog.text + assert "has no samples" in caplog.text + + +def test_config_case( + cli_runner: CliRunner, + raredisease_context: CGConfig, + # rnafusion_sample_sheet_path: Path, + # rnafusion_params_file_path: Path, + caplog: LogCaptureFixture, + raredisease_case_id: str, +): + """Test case-config.""" + caplog.set_level(logging.DEBUG) + + # GIVEN a valid case + + # WHEN performing a dry-run + result = cli_runner.invoke(config_case, [raredisease_case_id], obj=raredisease_context) + + # THEN command should should exit successfully + assert result.exit_code == EXIT_SUCCESS + + # THEN sample sheet and parameters information should be collected + assert "Getting sample sheet information" in caplog.text + assert "Getting parameters information" in caplog.text + + # THEN sample sheet and parameters information files should be written + + assert ( + "Dry run: nextflow sample sheet and parameter file will not be written" not in caplog.text + ) + assert "Could not create config files" not in caplog.text + assert "Writing sample sheet" in caplog.text + assert "Writing parameters file" in caplog.text diff --git a/tests/cli/workflow/rnafusion/test_cli_rnafusion_config_case.py b/tests/cli/workflow/rnafusion/test_cli_rnafusion_config_case.py index cdfe940409..7b22e5ea5d 100644 --- a/tests/cli/workflow/rnafusion/test_cli_rnafusion_config_case.py +++ b/tests/cli/workflow/rnafusion/test_cli_rnafusion_config_case.py @@ -81,7 +81,7 @@ def test_config_case_default_parameters( # WHEN running config case result = cli_runner.invoke(config_case, [rnafusion_case_id], obj=rnafusion_context) - # THEN command should exit succesfully + # THEN command should exit successfully assert result.exit_code == EXIT_SUCCESS # THEN logs should be as expected diff --git a/tests/conftest.py b/tests/conftest.py index 0296949a43..3c6850854e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -742,6 +742,18 @@ def mip_dna_analysis_dir(mip_analysis_dir: Path) -> Path: return Path(mip_analysis_dir, "dna") +@pytest.fixture +def nf_analysis_analysis_dir(fixtures_dir: Path) -> Path: + """Return the path to the directory with nf-analysis files.""" + return Path(fixtures_dir, "analysis", "nf-analysis") + + +@pytest.fixture +def raredisease_analysis_dir(analysis_dir: Path) -> Path: + """Return the path to the directory with raredisease analysis files.""" + return Path(analysis_dir, "raredisease") + + @pytest.fixture def rnafusion_analysis_dir(analysis_dir: Path) -> Path: """Return the path to the directory with rnafusion analysis files.""" @@ -1686,6 +1698,9 @@ def context_config( illumina_demultiplexed_runs_directory: Path, downsample_dir: Path, pdc_archiving_directory: PDCArchivingDirectory, + nf_analysis_platform_config_path: Path, + nf_analysis_pipeline_params_path: Path, + nf_analysis_pipeline_resource_optimisation_path: Path, ) -> dict: """Return a context config.""" return { @@ -1847,6 +1862,9 @@ def context_config( "compute_env": "nf_tower_compute_env", "conda_binary": Path("path", "to", "bin", "conda").as_posix(), "conda_env": "S_raredisease", + "config_platform": str(nf_analysis_platform_config_path), + "config_params": str(nf_analysis_pipeline_params_path), + "config_resources": str(nf_analysis_pipeline_resource_optimisation_path), "launch_directory": Path("path", "to", "launchdir").as_posix(), "workflow_path": Path("workflow", "path").as_posix(), "profile": "myprofile", @@ -2134,18 +2152,6 @@ def no_sample_case_id() -> str: return "no_sample_case" -@pytest.fixture(scope="session") -def strandedness() -> str: - """Return a default strandedness.""" - return Strandedness.REVERSE - - -@pytest.fixture(scope="session") -def strandedness_not_permitted() -> str: - """Return a not permitted strandedness.""" - return "double_stranded" - - @pytest.fixture(scope="session") def workflow_version() -> str: """Return a workflow version.""" @@ -2193,6 +2199,22 @@ def raredisease_dir(tmpdir_factory, apps_dir: Path) -> str: return Path(raredisease_dir).absolute().as_posix() +# Raredisease fixtures + + +@pytest.fixture(scope="function") +def raredisease_dir(tmpdir_factory, apps_dir: Path) -> str: + """Return the path to the raredisease apps dir.""" + raredisease_dir = tmpdir_factory.mktemp("raredisease") + return Path(raredisease_dir).absolute().as_posix() + + +@pytest.fixture(scope="session") +def raredisease_case_id() -> str: + """Returns a rnafusion case id.""" + return "raredisease_case_enough_reads" + + # Rnafusion fixtures @@ -2227,6 +2249,18 @@ def rnafusion_sample_sheet_content( ) +@pytest.fixture(scope="session") +def strandedness() -> str: + """Return a default strandedness.""" + return Strandedness.REVERSE + + +@pytest.fixture(scope="session") +def strandedness_not_permitted() -> str: + """Return a not permitted strandedness.""" + return "double_stranded" + + @pytest.fixture(scope="function") def hermes_deliverables(deliverable_data: dict, rnafusion_case_id: str) -> dict: hermes_output: dict = {"workflow": "rnafusion", "bundle_id": rnafusion_case_id, "files": []} @@ -2294,6 +2328,26 @@ def rnafusion_deliverables_file_path(rnafusion_dir, rnafusion_case_id) -> Path: ) +@pytest.fixture(scope="function") +def nf_analysis_platform_config_path(nf_analysis_analysis_dir) -> Path: + """Path to platform config file.""" + return Path(nf_analysis_analysis_dir, "platform").with_suffix(FileExtensions.CONFIG) + + +@pytest.fixture(scope="function") +def nf_analysis_pipeline_params_path(nf_analysis_analysis_dir) -> Path: + """Path to pipeline params file.""" + return Path(nf_analysis_analysis_dir, "pipeline_params").with_suffix(FileExtensions.CONFIG) + + +@pytest.fixture(scope="function") +def nf_analysis_pipeline_resource_optimisation_path(nf_analysis_analysis_dir) -> Path: + """Path to pipeline resource optimisation file.""" + return Path(nf_analysis_analysis_dir, "pipeline_resource_optimisation").with_suffix( + FileExtensions.CONFIG + ) + + @pytest.fixture(scope="session") def tower_id() -> int: """Returns a NF-Tower ID.""" @@ -3043,28 +3097,24 @@ def downsample_api( ) -@pytest.fixture(scope="session") -def raredisease_case_id() -> str: - """Returns a raredisease case id.""" - return "raredisease_case_enough_reads" - - @pytest.fixture(scope="function") def raredisease_context( cg_context: CGConfig, helpers: StoreHelpers, nf_analysis_housekeeper: HousekeeperAPI, - raredisease_case_id: str, + trailblazer_api: MockTB, sample_id: str, no_sample_case_id: str, total_sequenced_reads_pass: int, apptag_rna: str, + raredisease_case_id: str, case_id_not_enough_reads: str, sample_id_not_enough_reads: str, total_sequenced_reads_not_pass: int, ) -> CGConfig: """Raredisease context to use in CLI.""" cg_context.housekeeper_api_ = nf_analysis_housekeeper + cg_context.trailblazer_api_ = trailblazer_api cg_context.meta_apis["analysis_api"] = RarediseaseAnalysisAPI(config=cg_context) status_db: Store = cg_context.status_db diff --git a/tests/fixtures/analysis/nf-analysis/pipeline_params.config b/tests/fixtures/analysis/nf-analysis/pipeline_params.config new file mode 100644 index 0000000000..03cf45d7f2 --- /dev/null +++ b/tests/fixtures/analysis/nf-analysis/pipeline_params.config @@ -0,0 +1 @@ +singularity.cacheDir = "/home/proj/stage/workflows/singularity-cache" diff --git a/tests/fixtures/analysis/nf-analysis/pipeline_resource_optimisation.config b/tests/fixtures/analysis/nf-analysis/pipeline_resource_optimisation.config new file mode 100644 index 0000000000..1a6cf50267 --- /dev/null +++ b/tests/fixtures/analysis/nf-analysis/pipeline_resource_optimisation.config @@ -0,0 +1,5 @@ +params { + config_profile_description = 'A test config_profile_contact.' + config_profile_contact = 'Clinical Genomics, Stockholm' + config_profile_url = 'https://github.com/Clinical-Genomics' +} diff --git a/tests/fixtures/analysis/nf-analysis/platform.config b/tests/fixtures/analysis/nf-analysis/platform.config new file mode 100644 index 0000000000..1a6cf50267 --- /dev/null +++ b/tests/fixtures/analysis/nf-analysis/platform.config @@ -0,0 +1,5 @@ +params { + config_profile_description = 'A test config_profile_contact.' + config_profile_contact = 'Clinical Genomics, Stockholm' + config_profile_url = 'https://github.com/Clinical-Genomics' +} diff --git a/tests/fixtures/io/example2.txt b/tests/fixtures/io/example2.txt new file mode 100644 index 0000000000..769d47c510 --- /dev/null +++ b/tests/fixtures/io/example2.txt @@ -0,0 +1,3 @@ +Line 4 +Line 5 +Line 6 \ No newline at end of file diff --git a/tests/io/conftest.py b/tests/io/conftest.py index 209bf0bfd1..754da91728 100644 --- a/tests/io/conftest.py +++ b/tests/io/conftest.py @@ -66,12 +66,24 @@ def txt_file_path(fixtures_dir: Path) -> Path: return Path(fixtures_dir, "io", "example.txt") +@pytest.fixture +def txt_file_path_2(fixtures_dir: Path) -> Path: + """Return a file path to example TXT file, 2 files needed to test concatenation.""" + return Path(fixtures_dir, "io", "example2.txt") + + @pytest.fixture def tsv_stream() -> str: """Return string with TSV format.""" return """Lorem ipsum sit amet""" +@pytest.fixture +def config_dict() -> dict: + """Return dictionary format.""" + return {"input": "input_path", "output": "output_path"} + + @pytest.fixture def txt_temp_path(cg_dir: Path) -> Path: """Return a temp file path to use when writing text files.""" diff --git a/tests/io/test_io_config.py b/tests/io/test_io_config.py new file mode 100644 index 0000000000..7521cbe084 --- /dev/null +++ b/tests/io/test_io_config.py @@ -0,0 +1,17 @@ +from pathlib import Path + +import pytest + +from cg.io.config import write_config_nextflow_style + + +def test_write_config_nextflow_style(config_dict: Path): + """ + Test output content from stream into nextflow config format. + """ + + # THEN assert a config format is returned + assert ( + write_config_nextflow_style(content=config_dict) + == 'params.input = "input_path"\nparams.output = "output_path"\n' + ) diff --git a/tests/io/test_io_txt.py b/tests/io/test_io_txt.py index eab0e38cc7..cac5782b2d 100644 --- a/tests/io/test_io_txt.py +++ b/tests/io/test_io_txt.py @@ -2,7 +2,7 @@ import pytest -from cg.io.txt import read_txt, write_txt +from cg.io.txt import read_txt, write_txt, concat_txt def test_read_txt_to_list(txt_file_path: Path): @@ -65,3 +65,44 @@ def test_write_txt(txt_temp_path: Path, txt_file_path: Path): # THEN the content should match the original content assert content == read_txt(file_path=txt_temp_path) + + +def test_concat_txt(txt_file_path: Path, txt_file_path_2: Path, txt_temp_path: Path): + """Test concatenating two files, no optional string content""" + # GIVEN a list of file paths to concatenate + + # WHEN concatenating two files + concat_txt( + file_paths=[txt_file_path, txt_file_path_2], target_file=txt_temp_path, str_content=None + ) + + # THEN the target file should exist + assert txt_temp_path.exists() + + +def test_concat_txt_with_string( + txt_file_path: Path, txt_file_path_2: Path, txt_temp_path: Path, csv_stream: str +): + """Test concatenating two files, no optional string content""" + # GIVEN a list of file paths to concatenate + + # WHEN concatenating two files + concat_txt( + file_paths=[txt_file_path, txt_file_path_2], + target_file=txt_temp_path, + str_content=[csv_stream], + ) + + # THEN the target file should exist + assert txt_temp_path.exists() + + # THEN the content should match the input string + assert read_txt(file_path=txt_temp_path) == [ + "Lorem,ipsum,sit,amet\n", + "Line 1\n", + "Line 2\n", + "Line 3\n", + "Line 4\n", + "Line 5\n", + "Line 6\n", + ] diff --git a/tests/meta/workflow/test_raredisease.py b/tests/meta/workflow/test_raredisease.py new file mode 100644 index 0000000000..1bed0f460f --- /dev/null +++ b/tests/meta/workflow/test_raredisease.py @@ -0,0 +1,52 @@ +"""Module for Rnafusion analysis API tests.""" + +from cg.meta.workflow.raredisease import RarediseaseAnalysisAPI +from cg.models.cg_config import CGConfig +from cg.constants import EXIT_SUCCESS +from pathlib import Path +import os + + +def test_get_sample_sheet_content( + raredisease_context: CGConfig, + raredisease_case_id: str, +): + """Test Raredisease nextflow sample sheet creation.""" + + # GIVEN Raredisease analysis API + analysis_api: RarediseaseAnalysisAPI = raredisease_context.meta_apis["analysis_api"] + + # WHEN getting the sample sheet content + result = analysis_api.get_sample_sheet_content(case_id=raredisease_case_id) + + # THEN return should contain patterns + patterns = [ + "ADM1", + "XXXXXXXXX_000000_S000_L001_R1_001.fastq.gz", + "raredisease_case_enough_reads", + ] + + contains_pattern = any( + any(any(pattern in sub_element for pattern in patterns) for sub_element in element) + for element in result + ) + assert contains_pattern + + +def test_write_params_file(raredisease_context: CGConfig, raredisease_case_id: str): + + # GIVEN Raredisease analysis API and input (nextflow sample sheet path)/output (case directory) parameters + analysis_api: RarediseaseAnalysisAPI = raredisease_context.meta_apis["analysis_api"] + in_out = {"input": "input_path", "output": "output_path"} + + # WHEN creating case directory + analysis_api.create_case_directory(case_id=raredisease_case_id, dry_run=False) + + # THEN care directory is created + assert os.path.exists(analysis_api.get_case_path(case_id=raredisease_case_id)) + + # WHEN writing parameters file + analysis_api.write_params_file(case_id=raredisease_case_id, workflow_parameters=in_out) + + # THEN the file is created + assert os.path.isfile(analysis_api.get_params_file_path(case_id=raredisease_case_id))