diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 00000000..f8f438a2 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,20 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: NF1-CellPainting-Data-Repository +message: NF1 dataset repository +type: dataset +authors: + - given-names: Jenna + family-names: Tomkinson + - given-names: Greg + family-names: Way +identifiers: + - type: url + value: >- + https://github.com/WayScience/nf1_cellpainting_data/tree/main + description: Github repository of the dataset +repository-code: >- + https://github.com/WayScience/nf1_cellpainting_data/tree/main +license: CC0-1.0 diff --git a/all_cellprofiler.sqlite b/all_cellprofiler.sqlite deleted file mode 120000 index 39247b71..00000000 --- a/all_cellprofiler.sqlite +++ /dev/null @@ -1 +0,0 @@ -/home/erikserrano/Development/NF1_SchwannCell_data/CellProfiler_pipelines/Analysis_Output/Plate1_Output/all_cellprofiler.sqlite \ No newline at end of file diff --git a/cytosnake/tests/functional/datasets/emptyfiles/metadata/platemap/platemap1.csv b/cytosnake/tests/functional/datasets/emptyfiles/metadata/platemap/platemap1.csv deleted file mode 100644 index e69de29b..00000000 diff --git a/cytosnake/tests/functional/datasets/emptyfiles/metadata/platemap/platemap2.csv b/cytosnake/tests/functional/datasets/emptyfiles/metadata/platemap/platemap2.csv deleted file mode 100644 index e69de29b..00000000 diff --git a/cytosnake/tests/functional/datasets/emptyfiles/plate_data1.sqlite b/cytosnake/tests/functional/datasets/emptyfiles/plate_data1.sqlite deleted file mode 100644 index e69de29b..00000000 diff --git a/cytosnake/tests/functional/datasets/emptyfiles/plate_data2.sqlite b/cytosnake/tests/functional/datasets/emptyfiles/plate_data2.sqlite deleted file mode 100644 index e69de29b..00000000 diff --git a/cytosnake/tests/functional/test_cli.py b/cytosnake/tests/functional/test_cli.py deleted file mode 100644 index 2e7ab625..00000000 --- a/cytosnake/tests/functional/test_cli.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -module: test_cli.py - -This testing module composes of functional tests that contains checks for both positive -negative cases when using CytoSnake's CLI - -A positive case indicates that given the user parameters we expect it to run -successfully. - -A negative case indicates that given with the user parameters, our tests are able to -capture the errors. - -Ultimately, test_cli.py will contains functional test to all modes that CytoSnake -contains. -""" - -import os -import pathlib -import shutil -import subprocess -from typing import Optional - -from cytosnake.common import errors - - -# ----------------- -# Helper functions -# ----------------- -class CleanUpHandler: - """Used to clean up directories in every single test run""" - - def __init__(self, tmp_path): - self.tmp_path = tmp_path - - def __call__(self) -> None: - shutil.rmtree(self.tmp_path) - - -def transfer_data( - test_dir: pathlib.Path, - n_plates: int, - n_platemaps: int, - metadata_dir_name: Optional[str] = "metadata", - testing_data_dir="emptyfiles", -) -> None: - """Wrapper function that transfer datasets found within the pytest module and - transfers it to the assigned directory where pytest is conducting the functional - tests. - - The test dataset is transfered from the `dataset` directory located in the - same directory as the functional test module. The dataset directory contains - empty files that emulates inputs that a user will put in to CytoSnake. - - Parameters - ---------- - test_dir : pathlib.Path - testing directory - - n_plates: int - number of plates - - n_platesmaps: int - number of plate maps - - metadata_dir_name : Optional[str] - name of the metadata directory (default=metadata) - - Return - ------ - None - Transfers datafiles from the PyTest module to the testing directory - """ - - # get files to transfer - # dataset folder is within the same directory as the test modules, changes - # in this path will cause the tests to fail - dataset_dir = pathlib.Path(f"./datasets/{testing_data_dir}").resolve(strict=True) - - # grabbing all input paths - sqlite_file_paths = list(dataset_dir.glob("*sqlite"))[:n_plates] - platemaps_dir = dataset_dir / "metadata" / "platemap" - plate_map_files = [ - str(_path.absolute()) for _path in platemaps_dir.glob("platemap*") - ][:n_platemaps] - barcode = dataset_dir / "barcode.txt" - - # create a metadata_dir in tmp_dir - if not isinstance(metadata_dir_name, str): - raise ValueError("metadata dir name must be a string") - - tmpdir_metadata_path = test_dir / metadata_dir_name / "platemap" - tmpdir_metadata_path.mkdir(exist_ok=True, parents=True) - - # transferring all files to tmp dir - for _path in plate_map_files: - shutil.copy(_path, str(tmpdir_metadata_path)) - for _path in sqlite_file_paths: - shutil.copy(_path, test_dir) - shutil.copy(barcode, test_dir) - - -def get_raised_error(traceback: str) -> str: - """Parses traceback and attempts to obtain raised exception error. - - Traceback is parsed in this order: - 1. split by new lines - 2. grab the last line as it contains the raised exception and message - 3. split by ":" to separate exception name and exception message - 4. grab the first element since it contains that path to exception - 5. split by "." and grab last element, which is the exception name - - Parameters - ---------- - traceback : str - complete traceback generated by executing CLI - - Returns - ------- - str - return raised exception error - """ - - # returns exception name, refer to function documentation to understand - # the order of parsing the traceback to obtain exception name. - return traceback.splitlines()[-1].split(":")[0].split(".")[-1] - - -# -------------------------- -# init mode functional tests -# -------------------------- -# The tests below focuses on only executing the init mode. -def test_barcode_logic_no_barcode_one_platemap(tmp_path, request) -> None: - """Positive case: This tests expects a successful run where the user provides - multiple plate datasets, plate map, and no barcode. Since this is only one plate_map - , this means that the generated dataset came from one experiment and multiple - samples (plates) were used to generated the datasets. - """ - # starting path - test_module = str(pathlib.Path().absolute()) - - # transfer placeholder data to tmpdir - transfer_data(test_dir=tmp_path, n_plates=2, n_platemaps=1) - - # change directory to tmpdir - os.chdir(tmp_path) - - # execute CytoSnake - cmd = "cytosnake init -d *.sqlite -m metadata".split() - proc = subprocess.run(cmd, capture_output=True, text=True, check=False) - - # leave test directory - os.chdir(test_module) - - # clean directory, - cleanup_handler = CleanUpHandler(tmp_path) - request.addfinalizer(cleanup_handler) - - # checking for success return code - assert proc.returncode == 0 - - -def test_barcode_logic_barcode_multi_platemaps(tmp_path, request) -> None: - """Positive case: This tests expects a successful run where the user provides - multiple plate datasets, multiple plate map, and barcode. Since this is only one - plate_map , this means that the generated dataset came from one experiment and - multiple samples (plates) were used to generated the datasets. - """ - # PyTest module directory - test_module = str(pathlib.Path().absolute()) - - # transfer placeholder data to tmpdir - transfer_data(test_dir=tmp_path, n_plates=2, n_platemaps=2) - - # change directory to tmpdir - os.chdir(tmp_path) - - # execute CytoSnake - cmd = "cytosnake init -d *.sqlite -m metadata -b barcode.txt".split() - proc = subprocess.run(cmd, capture_output=True, text=True, check=False) - - # leave testing dir - os.chdir(test_module) - - # clean directory, - cleanup_handler = CleanUpHandler(tmp_path) - request.addfinalizer(cleanup_handler) - - # checking for success return code - assert proc.returncode == 0 - - -def test_barcode_logic_no_barcode_multi_platemaps(tmp_path, request) -> None: - """Negative case: This test expects a failed run where the user provides multiple - plate datasets, multiple plate maps (multi-experiments), and no barcode. Since - there are plate maps, this indicates that the generated datasets came from multiple - experiments. - - Checks: - ------- - non-zero return code - BarCodeRequiredError raised - """ - # PyTest module directory - test_module = str(pathlib.Path().absolute()) - - # transfer placeholder data to tmpdir - transfer_data(test_dir=tmp_path, n_plates=2, n_platemaps=2) - - # change directory to tmpdir - os.chdir(tmp_path) - - # execute CytoSnake - cmd = "cytosnake init -d *.sqlite -m metadata" - proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, check=False) - raised_error = get_raised_error(proc.stderr) - - # leave testing dir - os.chdir(test_module) - - # clean directory, - cleanup_handler = CleanUpHandler(tmp_path) - request.addfinalizer(cleanup_handler) - - # checking for success return code - assert proc.returncode == 1 - assert raised_error == errors.BarcodeRequiredError.__name__ diff --git a/cytosnake/utils/test_utils.py b/cytosnake/utils/test_utils.py new file mode 100644 index 00000000..807ff16f --- /dev/null +++ b/cytosnake/utils/test_utils.py @@ -0,0 +1,230 @@ +""" +module: test_utils.py + +test_utils.py contain additional functions that enhance testing capabilities, providing +extra functionality for conducting comprehensive and robust tests. +""" +import os +import pathlib +import shutil +from dataclasses import dataclass +from typing import Optional + +import cytosnake + +# setting test root folder path +TEST_ROOT_PATH = pathlib.Path(cytosnake.__path__[0]).parent / "tests" + + +@dataclass +class DataFiles: + """Structured datatype representation that contains all files in a selected dataset + + Attributes: + ----------- + metadata: str | list[str] + metadata directory name + plate_data: list[str] + list of plate data (parquet or sqlite files) + barcode : str + barcode file name + + Returns + ------- + DataFiles + DataStructure representation of test dataset files + """ + + # required parameters + dataset_dir: str | pathlib.Path + + # extracted files + metadata: str | list[str] = None + plate_data: str | list[str] = None + barcode: str = None + + # extracting file paths and setting into dataclass attributes + def __post_init__(self): + self._extract_content_files() + + def _extract_content_files(self): + """extracts all files within given dataset folder and sets the DataFile + dataclass attributes + + Raises + ------ + TypeError + raised if dataset_dir is not a str or pathlib.Path object. + raised if plate data is not parquet or sqlite file + """ + # get all top level files + if not isinstance(self.dataset_dir, (str, pathlib.Path)): + raise TypeError("dataset_dir must be a string or pathlib.Path object") + if isinstance(self.dataset_dir, str): + self.dataset_dir = pathlib.Path(self.dataset_dir).resolve(strict=True) + + # get all files + all_files = list(self.dataset_dir.glob("*")) + + # get data files + plate_data = [ + str(fpath.name) + for fpath in all_files + if any(fpath.suffix == ext for ext in [".parquet", ".sqlite"]) + ] + self.plate_data = plate_data + + # get metadata_dir + meta_data_path = [str(fpath.name) for fpath in all_files if fpath.is_dir()] + self.metadata = ( + meta_data_path[0] if len(meta_data_path) == 1 else meta_data_path + ) + + # get barcode + barcode_path = [ + str(fpath.name) + for fpath in all_files + if any(fpath.suffix == ext for ext in [".txt", ".csv"]) + ] + self.barcode = ( + (barcode_path[0] if len(barcode_path) == 1 else barcode_path) + if len(barcode_path) > 0 + else None + ) + + +def get_raised_error(traceback: str) -> str: + """Parses traceback and attempts to obtain raised exception error. + + Traceback is parsed in this order: + 1. split by new lines + 2. grab the last line as it contains the raised exception and message + 3. split by ":" to separate exception name and exception message + 4. grab the first element since it contains that path to exception + 5. split by "." and grab last element, which is the exception name + + Parameters + ---------- + traceback : str + complete traceback generated by executing CLI + + Returns + ------- + str + name of raised exception error + """ + + # returns exception name, refer to function documentation to understand + # the order of parsing the traceback to obtain exception name. + return traceback.splitlines()[-1].split(":")[0].split(".")[-1] + + +def get_test_data_folder(test_data_name: str) -> DataFiles: + """Gets single or multiple datasets. Users provide the name of the datasets + that will be used in their tests + + Parameters/home/axiom/Projects/CytoSnake/cytosnake/utils/datasets + name or names of datasets to be selected + + Returns + ------- + DataFiles + contains all files in a dataclass format + + Raises: + ------- + FileNotFoundError + Raised when the provided test_data_name is not a valid testing dataset. + """ + + # type checking + if not isinstance(test_data_name, str): + raise TypeError("`test_data_name` must be a string") + + # get testing dataset_path + data_dir_path = (TEST_ROOT_PATH / "datasets").resolve(strict=True) + sel_test_data = (data_dir_path / test_data_name).resolve(strict=True) + + # convert to DataFiles content + data_files = DataFiles(sel_test_data) + + return data_files + + +def prepare_dataset( + test_data_name: str, + test_dir_path: pathlib.Path, +) -> DataFiles: + """Main function to prepare dataset into testing datafolder. + + Parameters + ---------- + test_data_name : str + name of the testing dataset you want to use + + test_dir_path : pathlib.Path + path to testing directory + + Returns + ------- + DataFiles + Structured data object that contains all the files within the selected dataset + """ + # get dataset and transfer to testing directory + datafiles = get_test_data_folder(test_data_name=test_data_name) + shutil.copytree(datafiles.dataset_dir, str(test_dir_path), dirs_exist_ok=True) + + # change directory to the testing directory + os.chdir(str(test_dir_path)) + + return datafiles + + +def check_init_outputs( + paths: DataFiles, + test_dir: pathlib.Path, + plate_data_ext: Optional[str] = "sqlite", +): + """verifies if all the files are generated after executing a sucessfull `init` mode + run with CytoSnake. + + Parameters + ---------- + paths : test_utils.DataFiles + DataFile datastructure that contains all file inputs + test_dir : pathlib.Path + Path where the test is taken place + plate_data_ext : Optional[str], optional + plate data format, by default "sqlite" + """ + # typpe checking + if not isinstance(paths, DataFiles): + raise TypeError(f"`paths` must be a DataFiles type, not: {type(paths)}") + if not isinstance(test_dir, pathlib.Path): + raise TypeError( + f"`test_dir` must be a pathlib.Path type, not: {type(test_dir)}" + ) + if not isinstance(test_dir, pathlib.Path): + raise TypeError( + f"`plate_data_ext` must be a str type, not: {type(plate_data_ext)}" + ) + + # creating the paths to check + data_folder = test_dir / "data" + cytosnake_file = test_dir / ".cytosnake" + metadata_in_datafolder = data_folder / paths.metadata + all_plates = list(data_folder.glob("*.sqlite")) + + # if the barcode is not None, get the path to barcode and check + if paths.barcode is not None: + barcodes_in_datafolder = data_folder / paths.barcode + print(paths.barcode) + assert barcodes_in_datafolder.exists() + + # assert checks + assert data_folder.exists() + assert cytosnake_file.exists() + assert metadata_in_datafolder.exists() + assert all( + [str(plate_data).endswith(f".{plate_data_ext}") for plate_data in all_plates] + ) diff --git a/docs/cytosnake.cli.md b/docs/cytosnake.cli.md index f79263d3..d90ea59e 100644 --- a/docs/cytosnake.cli.md +++ b/docs/cytosnake.cli.md @@ -55,6 +55,8 @@ cytosnake.cli.exec :show-inheritance: ``` +## Module contents + ```{eval-rst} .. automodule:: cytosnake.cli :members: diff --git a/docs/cytosnake.md b/docs/cytosnake.md index 325185db..4c90ae26 100644 --- a/docs/cytosnake.md +++ b/docs/cytosnake.md @@ -8,6 +8,7 @@ cytosnake.cli cytosnake.common cytosnake.guards +cytosnake.tests cytosnake.utils ``` diff --git a/docs/cytosnake.tests.md b/docs/cytosnake.tests.md new file mode 100644 index 00000000..ee02c985 --- /dev/null +++ b/docs/cytosnake.tests.md @@ -0,0 +1,21 @@ +# cytosnake.tests package + +## Submodules + +## cytosnake.tests.test_utils module + +```{eval-rst} +.. automodule:: cytosnake.tests.test_utils + :members: + :undoc-members: + :show-inheritance: +``` + +## Module contents + +```{eval-rst} +.. automodule:: cytosnake.tests + :members: + :undoc-members: + :show-inheritance: +``` diff --git a/pyproject.toml b/pyproject.toml index 779b226e..3612181d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,4 +10,11 @@ all = true ignore = ["F821"] [tool.ruff.flake8-self] -ignore-names = ["snakemake"] \ No newline at end of file +ignore-names = ["snakemake"] + +# pytest config +[tool.pytest.ini_options] +markers = [ + "positive: marks for positive test", + "negative: marks for a negative test" +] \ No newline at end of file diff --git a/cytosnake/tests/functional/datasets/emptyfiles/barcode.txt b/tests/__init__.py similarity index 100% rename from cytosnake/tests/functional/datasets/emptyfiles/barcode.txt rename to tests/__init__.py diff --git a/tests/datasets/nf1-data-nobarcode/Plate_1_nf1_analysis.sqlite b/tests/datasets/nf1-data-nobarcode/Plate_1_nf1_analysis.sqlite new file mode 100644 index 00000000..c93d1eaa --- /dev/null +++ b/tests/datasets/nf1-data-nobarcode/Plate_1_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b78d90ac5ef1444ea14a94d97c7c9635ab3053ca6b235b7b33a5e2729c41281f +size 4956160 diff --git a/tests/datasets/nf1-data-nobarcode/metadata/platemap/platemap_NF1_plate1.csv b/tests/datasets/nf1-data-nobarcode/metadata/platemap/platemap_NF1_plate1.csv new file mode 100644 index 00000000..fe49dd9e --- /dev/null +++ b/tests/datasets/nf1-data-nobarcode/metadata/platemap/platemap_NF1_plate1.csv @@ -0,0 +1,9 @@ +WellRow,WellCol,well_position,gene_name,genotype +C,6,C6,NF1,WT +C,7,C7,NF1,Null +D,6,D6,NF1,WT +D,7,D7,NF1,Null +E,6,E6,NF1,WT +E,7,E7,NF1,Null +F,6,F6,NF1,WT +F,7,F7,NF1,Null diff --git a/tests/datasets/nf1-data/Plate_1_nf1_analysis.sqlite b/tests/datasets/nf1-data/Plate_1_nf1_analysis.sqlite new file mode 100644 index 00000000..c93d1eaa --- /dev/null +++ b/tests/datasets/nf1-data/Plate_1_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b78d90ac5ef1444ea14a94d97c7c9635ab3053ca6b235b7b33a5e2729c41281f +size 4956160 diff --git a/tests/datasets/nf1-data/Plate_2_nf1_analysis.sqlite b/tests/datasets/nf1-data/Plate_2_nf1_analysis.sqlite new file mode 100644 index 00000000..c3e0d091 --- /dev/null +++ b/tests/datasets/nf1-data/Plate_2_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8091091d350d094833362d11708e789fb34c93c161028d1eb22d66b2b11d0e7c +size 32083968 diff --git a/tests/datasets/nf1-data/Plate_3_nf1_analysis.sqlite b/tests/datasets/nf1-data/Plate_3_nf1_analysis.sqlite new file mode 100644 index 00000000..3f35632b --- /dev/null +++ b/tests/datasets/nf1-data/Plate_3_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1df7afdd2703383b0ff5b18e8d0db4d49b09ec979c6b968cd2885f5b97cbc7f0 +size 578424832 diff --git a/tests/datasets/nf1-data/Plate_3_prime_nf1_analysis.sqlite b/tests/datasets/nf1-data/Plate_3_prime_nf1_analysis.sqlite new file mode 100644 index 00000000..3ba1953b --- /dev/null +++ b/tests/datasets/nf1-data/Plate_3_prime_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac74703724e3403e4695d5df077400eb3f4c1dbf72fbfbe9ab044e5d771ff04e +size 466550784 diff --git a/tests/datasets/nf1-data/Plate_4_nf1_analysis.sqlite b/tests/datasets/nf1-data/Plate_4_nf1_analysis.sqlite new file mode 100644 index 00000000..98cebece --- /dev/null +++ b/tests/datasets/nf1-data/Plate_4_nf1_analysis.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9edaba3a3f39cb41466b518c20b484728c11bef3d66a01255d64b4453ba7d3a6 +size 233775104 diff --git a/tests/datasets/nf1-data/barcode_platemap.csv b/tests/datasets/nf1-data/barcode_platemap.csv new file mode 100644 index 00000000..641723e9 --- /dev/null +++ b/tests/datasets/nf1-data/barcode_platemap.csv @@ -0,0 +1,6 @@ +Assay_Plate_Barcode,Plate_Map_Name +Plate_1_nf1_analysis,platemap_NF1_plate1 +Plate_2_nf1_analysis,platemap_NF1_plate2 +Plate_3_nf1_analysis,platemap_NF1_plate3 +Plate_3_prime_nf1_analysis,platemap_NF1_plate3 +Plate_4_nf1_analysis,platemap_NF1_plate4 diff --git a/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate1.csv b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate1.csv new file mode 100644 index 00000000..fe49dd9e --- /dev/null +++ b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate1.csv @@ -0,0 +1,9 @@ +WellRow,WellCol,well_position,gene_name,genotype +C,6,C6,NF1,WT +C,7,C7,NF1,Null +D,6,D6,NF1,WT +D,7,D7,NF1,Null +E,6,E6,NF1,WT +E,7,E7,NF1,Null +F,6,F6,NF1,WT +F,7,F7,NF1,Null diff --git a/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate2.csv b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate2.csv new file mode 100644 index 00000000..ef724972 --- /dev/null +++ b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate2.csv @@ -0,0 +1,33 @@ +WellRow,WellCol,well_position,gene_name,genotype +A,1,A1,NF1,WT +A,6,A6,NF1,WT +A,7,A7,NF1,Null +A,12,A12,NF1,Null +B,1,B1,NF1,WT +B,6,B6,NF1,WT +B,7,B7,NF1,Null +B,12,B12,NF1,Null +C,1,C1,NF1,WT +C,6,C6,NF1,WT +C,7,C7,NF1,Null +C,12,C12,NF1,Null +D,1,D1,NF1,WT +D,6,D6,NF1,WT +D,7,D7,NF1,Null +D,12,D12,NF1,Null +E,1,E1,NF1,WT +E,6,E6,NF1,WT +E,7,E7,NF1,Null +E,12,E12,NF1,Null +F,1,F1,NF1,WT +F,6,F6,NF1,WT +F,7,F7,NF1,Null +F,12,F12,NF1,Null +G,1,G1,NF1,WT +G,6,G6,NF1,WT +G,7,G7,NF1,Null +G,12,G12,NF1,Null +H,1,H1,NF1,WT +H,6,H6,NF1,WT +H,7,H7,NF1,Null +H,12,H12,NF1,Null diff --git a/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate3.csv b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate3.csv new file mode 100644 index 00000000..a694fe9f --- /dev/null +++ b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate3.csv @@ -0,0 +1,73 @@ +WellRow,WellCol,well_position,gene_name,genotype,seed_density +B,1,B1,NF1,WT,500 +C,1,C1,NF1,WT,500 +D,1,D1,NF1,WT,500 +E,1,E1,NF1,WT,500 +F,1,F1,NF1,WT,500 +G,1,G1,NF1,WT,500 +B,2,B2,NF1,WT,1000 +C,2,C2,NF1,WT,1000 +D,2,D2,NF1,WT,1000 +E,2,E2,NF1,WT,1000 +F,2,F2,NF1,WT,1000 +G,2,G2,NF1,WT,1000 +B,3,B3,NF1,WT,2000 +C,3,C3,NF1,WT,2000 +D,3,D3,NF1,WT,2000 +E,3,E3,NF1,WT,2000 +F,3,F3,NF1,WT,2000 +G,3,G3,NF1,WT,2000 +B,4,B4,NF1,WT,4000 +C,4,C4,NF1,WT,4000 +D,4,D4,NF1,WT,4000 +E,4,E4,NF1,WT,4000 +F,4,F4,NF1,WT,4000 +G,4,G4,NF1,WT,4000 +B,5,B5,NF1,HET,500 +C,5,C5,NF1,HET,500 +D,5,D5,NF1,HET,500 +E,5,E5,NF1,HET,500 +F,5,F5,NF1,HET,500 +G,5,G5,NF1,HET,500 +B,6,B6,NF1,HET,1000 +C,6,C6,NF1,HET,1000 +D,6,D6,NF1,HET,1000 +E,6,E6,NF1,HET,1000 +F,6,F6,NF1,HET,1000 +G,6,G6,NF1,HET,1000 +B,7,B7,NF1,HET,2000 +C,7,C7,NF1,HET,2000 +D,7,D7,NF1,HET,2000 +E,7,E7,NF1,HET,2000 +F,7,F7,NF1,HET,2000 +G,7,G7,NF1,HET,2000 +B,8,B8,NF1,HET,4000 +C,8,C8,NF1,HET,4000 +D,8,D8,NF1,HET,4000 +E,8,E8,NF1,HET,4000 +F,8,F8,NF1,HET,4000 +G,8,G8,NF1,HET,4000 +B,9,B9,NF1,Null,500 +C,9,C9,NF1,Null,500 +D,9,D9,NF1,Null,500 +E,9,E9,NF1,Null,500 +F,9,F9,NF1,Null,500 +G,9,G9,NF1,Null,500 +B,10,B10,NF1,Null,1000 +C,10,C10,NF1,Null,1000 +D,10,D10,NF1,Null,1000 +E,10,E10,NF1,Null,1000 +F,10,F10,NF1,Null,1000 +G,10,G10,NF1,Null,1000 +B,11,B11,NF1,Null,2000 +C,11,C11,NF1,Null,2000 +D,11,D11,NF1,Null,2000 +E,11,E11,NF1,Null,2000 +F,11,F11,NF1,Null,2000 +G,11,G11,NF1,Null,2000 +B,12,B12,NF1,Null,4000 +C,12,C12,NF1,Null,4000 +D,12,D12,NF1,Null,4000 +E,12,E12,NF1,Null,4000 +F,12,F12,NF1,Null,4000 +G,12,G12,NF1,Null,4000 diff --git a/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate4.csv b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate4.csv new file mode 100644 index 00000000..8d80538c --- /dev/null +++ b/tests/datasets/nf1-data/metadata/platemap/platemap_NF1_plate4.csv @@ -0,0 +1,61 @@ +WellRow,WellCol,well_position,gene_name,genotype,seed_density,siRNA,RNAiMax,Concentration +B,2,B2,NF1,WT,1000,None,0,0 +B,3,B3,NF1,WT,1000,Scramble,1,0.05 +B,4,B4,NF1,WT,1000,Scramble,1,0.005 +B,5,B5,NF1,WT,1000,None,0,0 +B,6,B6,NF1,WT,1000,Scramble,1,0.005 +B,7,B7,NF1,WT,1000,Scramble,1,0.05 +B,8,B8,NF1,WT,1000,None,0,0 +B,9,B9,NF1,WT,1000,Scramble,1,0.05 +B,10,B10,NF1,WT,1000,Scramble,1,0.005 +B,11,B11,NF1,WT,1000,None,1,0 +C,2,C2,NF1,Null,1000,None,0,0 +C,3,C3,NF1,WT,1000,NF1 Target 1,1,0.05 +C,4,C4,NF1,WT,1000,NF1 Target 1,1,0.005 +C,5,C5,NF1,Null,1000,None,0,0 +C,6,C6,NF1,WT,1000,NF1 Target 1,1,0.05 +C,7,C7,NF1,WT,1000,NF1 Target 1,1,0.005 +C,8,C8,NF1,Null,1000,None,0,0 +C,9,C9,NF1,WT,1000,NF1 Target 1,1,0.05 +C,10,C10,NF1,WT,1000,NF1 Target 1,1,0.005 +C,11,C11,NF1,WT,1000,None,1,0 +D,2,D2,NF1,Null,1000,None,0,0 +D,3,D3,NF1,WT,1000,NF1 Target 2,1,0.05 +D,4,D4,NF1,WT,1000,NF1 Target 2,1,0.005 +D,5,D5,NF1,Null,1000,None,0,0 +D,6,D6,NF1,WT,1000,NF1 Target 2,1,0.05 +D,7,D7,NF1,WT,1000,NF1 Target 2,1,0.005 +D,8,D8,NF1,Null,1000,None,0,0 +D,9,D9,NF1,WT,1000,NF1 Target 2,1,0.05 +D,10,D10,NF1,WT,1000,NF1 Target 2,1,0.005 +D,11,D11,NF1,WT,1000,None,1,0 +E,2,E2,NF1,WT,1000,Scramble,1,0.1 +E,3,E3,NF1,WT,1000,Scramble,1,0.01 +E,4,E4,NF1,WT,1000,Scramble,1,0.001 +E,5,E5,NF1,WT,1000,Scramble,1,0.1 +E,6,E6,NF1,WT,1000,Scramble,1,0.01 +E,7,E7,NF1,WT,1000,Scramble,1,0.001 +E,8,E8,NF1,WT,1000,Scramble,1,0.1 +E,9,E9,NF1,WT,1000,Scramble,1,0.01 +E,10,E10,NF1,WT,1000,Scramble,1,0.001 +E,11,E11,NF1,WT,1000,None,0,0 +F,2,F2,NF1,WT,1000,NF1 Target 1,1,0.1 +F,3,F3,NF1,WT,1000,NF1 Target 1,1,0.01 +F,4,F4,NF1,WT,1000,NF1 Target 1,1,0.001 +F,5,F5,NF1,WT,1000,NF1 Target 1,1,0.1 +F,6,F6,NF1,WT,1000,NF1 Target 1,1,0.01 +F,7,F7,NF1,WT,1000,NF1 Target 1,1,0.001 +F,8,F8,NF1,WT,1000,NF1 Target 1,1,0.1 +F,9,F9,NF1,WT,1000,NF1 Target 1,1,0.01 +F,10,F10,NF1,WT,1000,NF1 Target 1,1,0.001 +F,11,F11,NF1,Null,1000,None,0,0 +G,2,G2,NF1,WT,1000,NF1 Target 2,1,0.1 +G,3,G3,NF1,WT,1000,NF1 Target 2,1,0.01 +G,4,G4,NF1,WT,1000,NF1 Target 2,1,0.001 +G,5,G5,NF1,WT,1000,NF1 Target 2,1,0.1 +G,6,G6,NF1,WT,1000,NF1 Target 2,1,0.01 +G,7,G7,NF1,WT,1000,NF1 Target 2,1,0.001 +G,8,G8,NF1,WT,1000,NF1 Target 2,1,0.1 +G,9,G9,NF1,WT,1000,NF1 Target 2,1,0.01 +G,10,G10,NF1,WT,1000,NF1 Target 2,1,0.001 +G,11,G11,NF1,Null,1000,None,0,0 diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py new file mode 100644 index 00000000..46cb6323 --- /dev/null +++ b/tests/functional/test_cli.py @@ -0,0 +1,215 @@ +""" +module: test_cli.py + +Description: + +The test_cli.py module is a crucial part of the CytoSnake project, responsible for +conducting functional tests on CytoSnake's Command Line Interface (CLI). These tests aim +to ensure that the CLI functions correctly, handling both positive and negative +scenarios effectively. + +The purpose of this module is to verify that the CytoSnake CLI operates as expected and +provides accurate results when interacting with various user parameters and modes. It +validates that the CLI can handle different inputs, execute successfully in positive +cases, and properly report errors in negative cases. + +The test scenarios are categorized into two main types: + +Positive Cases: +These test cases validate the expected behavior of the CLI when +provided with valid user parameters. The primary goal is to ensure that the CLI executes +successfully without errors and produces the correct output. + +Negative Cases: +These test cases simulate scenarios where invalid user parameters are provided. +The objective is to confirm that the CLI can identify and handle errors appropriately, +providing informative error messages to the user. +""" + +import os +import pathlib +import shutil +import subprocess + +import pytest + +from cytosnake.common import errors +from cytosnake.utils import test_utils + + +# --------------- +# PyTest Fixtures +# --------------- +@pytest.fixture +def testing_dir(tmp_path, request): + """Creates a testing directory + + Note: Pytest will tear down tmp_path per test + + Parameters + ---------- + tmp_path : pytest.fixture + pytest default fixture value to be called when creating a temp dir. + + request : pytest.fixture + Allows custom testing function to be added in the testing workflow + + returns: + -------- + pathlib.Path + Path pointing to temporary directory + """ + # setting paths + original_cwd = str(pathlib.Path(".").absolute()) + + # creating a temporary path + test_name = request.node.name + tmp_dir = (tmp_path / test_name).absolute() + tmp_dir.mkdir() + + # return the temporary directory path + yield tmp_dir + + # teardown: remove the testing + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) + + # change the current working directory to the oroginal root + os.chdir(str(original_cwd)) + + +# --------------- +# Init Mode Tests +# --------------- +@pytest.mark.negative +def test_multiplate_maps_no_barcode(testing_dir) -> None: + """Negative case test: Expects `BarcodeMissingError` and Non-zero return code + + This tests checks if the CLI raises an error when a user provides multiple platemaps + but no barcodes. + + Rational: + --------- + CytoSnake relies on a barcode file to accurately map each platemap to its + corresponding plate. If users provide multiple platemaps without a barcode, + it becomes impossible for CytoSnake to determine the correct association between + platemaps and their respective plates. + + Parameters: + ----------- + testing_dir: pytest.fixture + Testing directory + """ + + # transfer data to testing folder + test_utils.prepare_dataset(test_data_name="nf1-data", test_dir_path=testing_dir) + + # Grab raised exception + with pytest.raises(subprocess.CalledProcessError) as subproc_error: + # execute test + proc = subprocess.run( + "cytosnake init -d *.sqlite -m metadata".split(), + capture_output=True, + text=True, + check=True, + ) + + # parse trace back generated from CytoSnake + raised_exception = test_utils.get_raised_error(proc.stderr) + + # assert checking + assert subproc_error.value.returncode == 1 + assert raised_exception == errors.BarcodeMissingError.__name__ + + +@pytest.mark.postive +def test_one_plate_one_platemap(testing_dir) -> None: + """Positive case test: Expects a 0 exit code + + This test checks if the CLI can handle one plate and one plate map without barcode + as inputs. + + Rational: + --------- + As this input involves only one plate, CytoSnake does not require the process + of identifying which platemap corresponds to that plate. + + Parameters: + ----------- + test_dir: pytest.fixture + Testing directory + """ + + # prepare testing files + datafiles = test_utils.prepare_dataset( + test_data_name="nf1-data-nobarcode", test_dir_path=testing_dir + ) + + # Selecting one plate and meta data dir + plate = datafiles.plate_data[0] + metadata = datafiles.metadata + + print(datafiles.barcode) + + # execute + proc = subprocess.run( + f"cytosnake init -d {plate} -m {metadata}".split(), + capture_output=True, + text=True, + check=False, + ) + + # assert checks + assert proc.returncode == 0 + + # check generated files + test_utils.check_init_outputs( + paths=datafiles, test_dir=testing_dir, plate_data_ext="sqlite" + ) + + +@pytest.mark.postive +def test_multiplates_with_multi_platemaps(testing_dir): + """Positive case test: Expects a 0 exit code + + This test checks if the CLI can handle multiple plates and multiple plate maps + with a barcode as inputs. + + checks for: data plates, data folder, cytosnake folder, barcodes in datafolder, + return code + + Rational: + --------- + CytoSnake requires input from multiple platemaps and barcodes. To ensure + accurate mapping of plate data, it necessitates the use of a barcode as an + input. The barcode files are used by CytoSnake to correctly associate each + plate map with its corresponding plate data. + + Parameters: + ----------- + test_dir: pytest.fixture + Testing directory + """ + + # prepare testing files and select inputs + datafiles = test_utils.prepare_dataset( + test_data_name="nf1-data", test_dir_path=testing_dir + ) + metadata = datafiles.metadata + barcode = datafiles.barcode + + # execute CytoSnake in testing folder + proc = subprocess.run( + f"cytosnake init -d *.sqlite -m {metadata} -b {barcode}".split(), + capture_output=True, + text=True, + check=False, + ) + + # check subprocess execution + assert proc.returncode == 0 + + # check generated files + test_utils.check_init_outputs( + paths=datafiles, test_dir=testing_dir, plate_data_ext="sqlite" + )