diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 7c68e9c..0079e86 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -38,12 +38,29 @@ jobs: http-user-agent: ${{ matrix.config.http-user-agent }} use-public-rspm: true - - uses: r-lib/actions/setup-r-dependencies@v2 - with: - extra-packages: rcmdcheck + - name: Install all dependencies manually + run: | + # Install Canopy dependencies from CRAN + install.packages(c('ape', 'fields', 'pheatmap', 'scatterplot3d'), repos='https://cloud.r-project.org') + # Install Canopy from CRAN archive + install.packages('http://cran.r-project.org/src/contrib/Archive/Canopy/Canopy_1.3.0.tar.gz', repos=NULL, type='source') + # Install remaining package dependencies including knitr for vignettes + install.packages(c('rcmdcheck', 'testthat', 'dplyr', 'ggplot2', 'magrittr', 'purrr', 'gridExtra', 'grid', 'readr', 'knitr'), repos='https://cloud.r-project.org') + shell: Rscript {0} - uses: r-lib/actions/check-r-package@v2 + - name: Run tests + run: | + testthat::test_dir("tests/testthat") + shell: Rscript {0} + + - uses: r-lib/actions/check-r-package@v2 + + - name: Run tests + run: | + Rscript -e "testthat::test_dir('tests/testthat')" + - name: Show testthat output if: always() run: find check -name 'testthat.Rout*' -exec cat '{}' \; || true diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d1510c0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,125 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +TiNDA (Tumor in Normal Detection Analysis) is an R package that rescues somatic variants misclassified as germline due to tumor DNA contamination in patient blood/control samples. It uses Canopy's EM-cluster algorithm to partition variants into clusters and classify them as somatic rescue, CHIP, or germline based on VAF patterns. + +## Development Commands + +```bash +# Run R CMD check +R CMD check . + +# Install package locally +R CMD INSTALL . + +# Build package +R CMD build . + +# Run package tests +R -e "devtools::test()" + +# Load package for development +devtools::load_all() + +# Document with roxygen2 +devtools::document() + +# Run specific test file +R -e "testthat::test_file('tests/testthat/test-filename.R')" + +# Check coverage +covr::package_coverage() +``` + +## Architecture + +### Core Components + +**R/TiNDA.R** - Main package logic +- `TiNDA()` function: Primary entry point that takes tumor/control variant data and classifies variants +- Uses Canopy's `canopy.cluster()` for EM-based clustering +- Key classification parameters: `max_control_af` (0.25), `min_tumor_af` (0.01), `min_clst_members` (0.85) +- Classification flow: Canopy clustering → potential somatic/CHIP cluster selection → germline exclusion → optional homozygous rescue + +**R/data.R** - Data simulation utilities +- `generate_depth()`: Helper for generating variant depths with normal distribution +- `simulate_variants()`: Simulates germline, somatic, and CHIP variants with configurable VAFs +- `generate_test_data()`: Generates test data across all chromosomes + +**R/plot.R** - Visualization functions +- `canopy_clst_plot()`: Shows Canopy cluster assignments in VAF space +- `tinda_clst_plot()`: Shows TiNDA final classification (Somatic_Rescue, Germline, CHIP) +- `tinda_linear_plot()`: Linear genome browser-style plot across chromosomes +- `tinda_summary_plot()`: Combined view with all plots and summary table + +**R/write_data.R** - Output utilities +- `write_data()`: Exports TiNDA results to TSV + +### Data Flow + +1. Input: Data frame with CHR, POS, Control_ALT_DP, Control_DP, Tumor_ALT_DP, Tumor_DP +2. VAF calculation: Control_AF = Control_ALT_DP/Control_DP, Tumor_AF = Tumor_ALT_DP/Tumor_DP +3. Canopy EM clustering with 10 clusters +4. Cluster classification based on Area of Interest (AOI) criteria +5. Optional CHIP detection (controlled_af: 0.02-0.35, tumor_af < 0.25) +6. Homozygous TiN rescue at top-left corner +7. Output: Classified variants with TiN_Class column + +### Key Parameters + +| Parameter | Default | Description | +|-----------|---------|-------------| +| max_control_af | 0.25 | Max control VAF for somatic rescue | +| min_tumor_af | 0.01 | Min tumor VAF for somatic rescue | +| min_clst_members | 0.85 | Min cluster members (proportion) in AOI | +| num_run | 1 | EM runs per cluster count | +| find_chip | TRUE | Enable CHIP detection | +| max_control_af_chip | 0.40 | Max control VAF for CHIP | +| max_tumor_af_chip | 0.25 | Max tumor VAF for CHIP | + +### Dependencies + +- Canopy (>= 1.3.0): EM clustering +- ggplot2: Visualization +- dplyr, purrr: Data manipulation +- readr: TSV I/O + +### Supported Reference Genomes + +- hg19: data/hg19_length.rda +- hg38: data/hg38_length.rda + +## Workflow Example + +```r +library(TiNDA) +data(hg19_length) + +# Generate test data +test_df <- generate_test_data(hg19_length, num_variants = 500) + +# Run TiNDA +tinda_obj <- TiNDA(test_df, sample_name = "sample_1", data_source = "WGS") + +# Visualize +canopy_clst_plot(tinda_obj) +tinda_clst_plot(tinda_obj) +tinda_linear_plot(tinda_obj) +tinda_summary_plot(tinda_obj) + +# Export results +write_data(tinda_obj, "results.tsv") +``` + +## Testing + +No unit tests currently exist in the repository. Test files should be placed in `tests/testthat/`. + +## Notes + +- Input data should be pre-filtered rare germline variants (not common SNPs or artifacts) +- For large datasets, consider using only exonic variants +- Column names in input must match exactly: CHR, POS, Control_ALT_DP, Control_DP, Tumor_ALT_DP, Tumor_DP diff --git a/NAMESPACE b/NAMESPACE index 159f339..a031a53 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,8 @@ export(TiNDA) export(canopy_clst_plot) export(generate_test_data) +export(get_tinda_params) +export(run_pipeline) export(tinda_clst_plot) export(tinda_linear_plot) export(tinda_summary_plot) @@ -23,3 +25,5 @@ importFrom(stats,median) importFrom(stats,quantile) importFrom(stats,rnorm) importFrom(utils,data) +S3method(print,TiNDA) +S3method(summary,TiNDA) diff --git a/R/TiNDA.R b/R/TiNDA.R index 3a6d7b2..48d5982 100755 --- a/R/TiNDA.R +++ b/R/TiNDA.R @@ -10,12 +10,12 @@ #' @param tbl A data frame object with rare germline variants with the raw coverage information. #' @param sample_name Name of the sample for the title, Default: 'pid_1' #' @param data_source WGS or WES, Default: 'WGS' -#' @param max_control_af Maximum control variant allele frequency (VAF), Default: 0.45 +#' @param max_control_af Maximum control variant allele frequency (VAF), Default: 0.25 #' @param min_tumor_af Minimum tumor variant allele frequency (VAF), Default: 0.01 #' @param min_clst_members Minimum number of members in the cluster to be below the max_control_af and above the min_tumor_af, Default: 0.85 #' @param num_run For canopy cluster function, "number of EM runs for estimation for each specific number of clusters (to avoid EM being stuck in local optima)", Default: 1 #' @param min_control_af_chip Minimum control variant allele frequency (VAF) for CHIP cluster, Default: 0.02 -#' @param max_control_af_chip Maximum control variant allele frequency (VAF) for CHIP cluster, Default: 0.35 +#' @param max_control_af_chip Maximum control variant allele frequency (VAF) for CHIP cluster, Default: 0.40 #' @param max_tumor_af_chip Maximum tumor variant allele frequency (VAF) for CHIP cluster, Default: 0.25 #' @param find_chip Find CHIP clusters, Default: TRUE #' @param verbose Print the potential clusters, Default: FALSE @@ -24,7 +24,7 @@ #' @examples #' data(hg19_length) #' vcf_like_df = TiNDA::generate_test_data(hg19_length) -#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_type = "WGS") +#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_source = "WGS") #' #' @importFrom stats median quantile rnorm #' @importFrom methods is @@ -35,7 +35,7 @@ #' @import purrr #' #' @export -TiNDA <- function(tbl, +TiNDA <- function(tbl, sample_name = "pid_1", data_source = "WGS", max_control_af = 0.25, @@ -47,59 +47,78 @@ TiNDA <- function(tbl, num_run = 1, find_chip = TRUE, verbose = FALSE, ...) { - - if(data_source == "WGS") { - mu.init <- cbind(c(0.5, 0.95, 0.50, 0.50, 0.02, 0.02, 0.02, 0.25, 0.15, 0.35), - c(0.5, 0.95, 0.15, 0.85, 0.20, 0.50, 0.85, 0.02, 0.02, 0.02)) - numberCluster <- 10 - } else if(data_source == "WES") { - mu.init <- cbind(c(0.5, 0.95, 0.50, 0.50, 0.02, 0.02, 0.02, 0.25, 0.15, 0.35), - c(0.5, 0.95, 0.15, 0.85, 0.20, 0.50, 0.85, 0.02, 0.02, 0.02)) - numberCluster <- 10 + + # Input validation + if (!is.data.frame(tbl)) { + stop("tbl must be a data frame") } - - # Test tbl + + if (!data_source %in% c("WGS", "WES")) { + stop("data_source must be either 'WGS' or 'WES'") + } + expected_col_names <- c('CHR', 'POS', 'Control_ALT_DP', 'Control_DP', 'Tumor_ALT_DP', 'Tumor_DP') - if (!purrr::is_empty(setdiff(expected_col_names, colnames(tbl)))){ - cat("Expected column names didn't appear in the input table\n") - cat("Expected:", expected_col_names, "\n") - stop() + missing_cols <- setdiff(expected_col_names, colnames(tbl)) + if (length(missing_cols) > 0) { + stop("Missing required columns: ", paste(missing_cols, collapse = ", ")) } - - - cat("Found ", dim(tbl)[1], " variants from the input data\n") - new_tbl <- tbl %>% + + if (nrow(tbl) == 0) { + stop("Input data frame is empty") + } + + if (any(tbl$Control_DP == 0)) { + warning("Found variants with zero control depth - these will be removed") + } + if (any(tbl$Tumor_DP == 0)) { + warning("Found variants with zero tumor depth - these will be removed") + } + + # Set clustering parameters based on data source + mu.init <- cbind(c(0.5, 0.95, 0.50, 0.50, 0.02, 0.02, 0.02, 0.25, 0.15, 0.35), + c(0.5, 0.95, 0.15, 0.85, 0.20, 0.50, 0.85, 0.02, 0.02, 0.02)) + numberCluster <- 10 + + cat("Found ", nrow(tbl), " variants from the input data\n") + + # Filter invalid depth values + new_tbl <- tbl %>% filter(.data$Control_ALT_DP < .data$Control_DP, - .data$Tumor_ALT_DP < .data$Tumor_DP) -> new_tbl - - if(dim(tbl)[1] == dim(new_tbl)[1]){ - rm(new_tbl) - } else { - num_removed <- dim(tbl)[1] - dim(new_tbl)[1] - cat("Removed ", num_removed) - cat(" variants with ALT read depth more than total read depth\n") - tbl = new_tbl + .data$Tumor_ALT_DP < .data$Tumor_DP) + + if (nrow(tbl) != nrow(new_tbl)) { + num_removed <- nrow(tbl) - nrow(new_tbl) + cat("Removed ", num_removed, + " variants with ALT read depth more than total read depth\n") + tbl <- new_tbl } - # Variant AF - tbl %>% - mutate(Control_AF = .data$Control_ALT_DP/ .data$Control_DP, - Tumor_AF = .data$Tumor_ALT_DP/ .data$Tumor_DP) -> tbl - - # Running Canopy ------------------------------------------------------------- - R <-as.matrix(tbl[,c('Control_ALT_DP', 'Tumor_ALT_DP')]) - X <-as.matrix(tbl[,c('Control_DP', 'Tumor_DP')]) - - # Canopy run and assigning centers - try( - canopy.clust <- canopy.cluster(R, X, - num_cluster = numberCluster, - num_run = num_run, Mu.init = mu.init) - ) - if(is.null(canopy.clust)){ - stop("Failed canopy run\n", call. = FALSE) + + # Calculate variant allele frequencies + tbl <- tbl %>% + mutate(Control_AF = .data$Control_ALT_DP / .data$Control_DP, + Tumor_AF = .data$Tumor_ALT_DP / .data$Tumor_DP) + + # Running Canopy + R <- as.matrix(tbl[, c('Control_ALT_DP', 'Tumor_ALT_DP')]) + X <- as.matrix(tbl[, c('Control_DP', 'Tumor_DP')]) + + # Canopy run with proper error handling + canopy.clust <- tryCatch( + canopy.cluster(R, X, + num_cluster = numberCluster, + num_run = num_run, + Mu.init = mu.init), + error = function(e) { + stop("Canopy clustering failed: ", e$message, call. = FALSE) + } + ) + + if (is.null(canopy.clust$sna_cluster)) { + stop("Canopy clustering failed: no clusters were assigned\n", call. = FALSE) } - tbl$canopyCluster<-canopy.clust$sna_cluster + + tbl$canopyCluster <- canopy.clust$sna_cluster # Select the potential TiN clusters ------------------------------------------ potential_somatic_clst_per <- tbl %>% @@ -225,13 +244,32 @@ TiNDA <- function(tbl, ) -> tbl } - tinda_object <- list(data=tbl, - min_tumor_af = min_tumor_af, - max_control_af = max_control_af, - number_cluster = numberCluster, - sample_name = sample_name) - - class(tinda_object) <-'TiNDA' - + # Create classification summary + class_summary <- table(tbl$TiN_Class) + + tinda_object <- list( + data = tbl, + sample_name = sample_name, + data_source = data_source, + max_control_af = max_control_af, + min_tumor_af = min_tumor_af, + number_cluster = numberCluster, + min_clst_members = min_clst_members, + find_chip = find_chip, + classification_summary = class_summary, + parameters = list( + max_control_af = max_control_af, + min_tumor_af = min_tumor_af, + min_clst_members = min_clst_members, + min_control_af_chip = min_control_af_chip, + max_control_af_chip = max_control_af_chip, + max_tumor_af_chip = max_tumor_af_chip, + num_run = num_run, + find_chip = find_chip + ) + ) + + class(tinda_object) <- 'TiNDA' + return(tinda_object) } diff --git a/R/plot.R b/R/plot.R index ec7451e..f42424f 100644 --- a/R/plot.R +++ b/R/plot.R @@ -1,8 +1,73 @@ assert_class <- function(obj){ - if(!is(obj, "TiNDA")) stop("The input object is not of class 'TiNDA'", + if(!is(obj, "TiNDA")) stop("The input object is not of class 'TiNDA'", call. = FALSE) } +#' Print method for TiNDA objects +#' +#' @param x TiNDA object +#' @param ... Additional arguments +#' +#' @export +print.TiNDA <- function(x, ...) { + cat("TiNDA object:\n") + cat(" Sample:", x$sample_name, "\n") + cat(" Data source:", x$data_source, "\n") + cat(" Total variants:", nrow(x$data), "\n") + cat("\nClassification summary:\n") + print(table(x$data$TiN_Class)) + cat("\nParameters:\n") + cat(" max_control_af:", x$max_control_af, "\n") + cat(" min_tumor_af:", x$min_tumor_af, "\n") + cat(" num_clusters:", x$number_cluster, "\n") + invisible(x) +} + +#' Summary method for TiNDA objects +#' +#' @param object TiNDA object +#' @param ... Additional arguments +#' +#' @export +summary.TiNDA <- function(object, ...) { + tbl <- object$data + list( + sample_name = object$sample_name, + data_source = object$data_source, + total_variants = nrow(tbl), + classification = as.data.frame(table(tbl$TiN_Class)), + median_control_vaf = median(tbl$Control_AF), + median_tumor_vaf = median(tbl$Tumor_AF), + parameters = list( + max_control_af = object$max_control_af, + min_tumor_af = object$min_tumor_af, + num_clusters = object$number_cluster + ) + ) +} + +#' Get default parameters for TiNDA +#' +#' @param data_source Data source type ('WGS' or 'WES') +#' +#' @export +get_tinda_params <- function(data_source = "WGS") { + if (!data_source %in% c("WGS", "WES")) { + stop("data_source must be either 'WGS' or 'WES'") + } + list( + max_control_af = 0.25, + min_tumor_af = 0.01, + min_clst_members = 0.85, + min_control_af_chip = 0.02, + max_control_af_chip = 0.40, + max_tumor_af_chip = 0.25, + num_run = 1, + find_chip = TRUE, + data_source = data_source + ) +} + #' Plotting the Canopy identified clusters #' @@ -11,10 +76,10 @@ assert_class <- function(obj){ #' @param tinda_object Object returned by TiNDA function #' @param ... ellipsis #' -#' @examples +#' @examples #' data(hg19_length) #' vcf_like_df = TiNDA::generate_test_data(hg19_length) -#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_type = "WGS") +#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_source = "WGS") #' canopy_clst_plot(tinda_test_object) #' #' @import ggplot2 @@ -59,7 +124,7 @@ canopy_clst_plot <- function(tinda_object, ...){ #' @examples #' data(hg19_length) #' vcf_like_df = TiNDA::generate_test_data(hg19_length) -#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_type = "WGS") +#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_source = "WGS") #' tinda_clst_plot(tinda_test_object) #' #' @import ggplot2 @@ -107,7 +172,7 @@ tinda_clst_plot <- function(tinda_object, #' @examples #' data(hg19_length) #' vcf_like_df = TiNDA::generate_test_data(hg19_length) -#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_type = "WGS") +#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_source = "WGS") #' tinda_linear_plot(tinda_test_object) #' #' @import ggplot2 @@ -174,7 +239,7 @@ tinda_linear_plot <- function(tinda_object, #' @examples #' data(hg19_length) #' vcf_like_df = TiNDA::generate_test_data(hg19_length) -#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_type = "WGS") +#' tinda_test_object <- TiNDA(vcf_like_df, sample_name = "sample_3", data_source = "WGS") #' tinda_summary_plot(tinda_test_object) #' #' @importFrom gridExtra ttheme_default tableGrob grid.arrange diff --git a/R/write_data.R b/R/write_data.R index 3fdc391..19688c3 100644 --- a/R/write_data.R +++ b/R/write_data.R @@ -1,12 +1,57 @@ #' Write TiNDA data -#' +#' #' Function to write TiNDA data to a tsv file with TiN_Class information -#' +#' #' @param tinda_object TiNDA object produced by the main TiNDA function #' @param file_name Name of the file -#' +#' #' @importFrom readr write_tsv #' @export write_data <- function(tinda_object, file_name){ write_tsv(tinda_object$data, file_name) } + +#' Run TiNDA pipeline from input file to output file +#' +#' Convenience function that reads input data, runs TiNDA analysis, +#' and writes results to output file in a single call. +#' +#' @param input_file Path to input TSV file with variant data +#' @param output_file Path to output TSV file for results +#' @param sample_name Sample name for the analysis title. If NULL, uses input file basename. +#' @param data_source Data source type ('WGS' or 'WES'). Default: 'WGS' +#' @param ... Additional arguments passed to TiNDA() +#' +#' @return TiNDA object (invisibly) and writes results to output_file +#' +#' @examples +#' \dontrun{ +#' run_pipeline("variants.tsv", "results.tsv", sample_name = "sample_1") +#' } +#' +#' @importFrom readr read_tsv +#' @export +run_pipeline <- function(input_file, output_file, sample_name = NULL, + data_source = "WGS", ...) { + # Read input data + if (!file.exists(input_file)) { + stop("Input file not found: ", input_file) + } + + tbl <- read_tsv(input_file, show_col_types = FALSE) + + # Set sample name from file if not provided + if (is.null(sample_name)) { + sample_name <- tools::file_path_sans_ext(basename(input_file)) + } + + # Run TiNDA analysis + result <- TiNDA(tbl, sample_name = sample_name, data_source = data_source, ...) + + # Write results + write_data(result, output_file) + + cat("Results written to:", output_file, "\n") + + invisible(result) +} diff --git a/README.md b/README.md index 8b59015..062b985 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ [![R build status](https://github.com/NagaComBio/TiNDA/workflows/R-CMD-check/badge.svg)](https://github.com/NagaComBio/TiNDA/actions) + [![CRAN version](https://www.r-pkg.org/badges/version/TiNDA)](https://cran.r-project.org/package=TiNDA) + # TiNDA ## Tumor in Normal Detection Analysis @@ -105,4 +107,55 @@ tinda_linear_plot(tinda_object) # Plot the summary of the TiNDA results - includes canopy clusters, TiNDA cluster assignment and linear plots tinda_summary_plot(tinda_object) ``` -![tinda_summary_plot](man/figures/tinda_summary_plot.png) \ No newline at end of file +![tinda_summary_plot](man/figures/tinda_summary_plot.png) + +--- + +## Changelog + +### [1.2.0] - 2026-04-29 + +#### Added +- `get_tinda_params()`: Function to retrieve default parameters for WGS/WES analysis +- `run_pipeline()`: Convenience function for file-based workflow (input → analysis → output) +- `print.TiNDA()`: S3 print method for TiNDA objects +- `summary.TiNDA()`: S3 summary method providing detailed classification statistics +- Test infrastructure with testthat and 10+ basic tests +- `inst/CITATION`: Citation file for publication credit + +#### Changed +- Enhanced TiNDA object structure with `classification_summary` and `parameters` fields +- Improved error messages for input validation (missing columns, invalid data types) + +#### Fixed +- Documentation: `max_control_af` default value (0.45 → 0.25) +- Documentation: `max_control_af_chip` default value (0.35 → 0.40) +- Example code: `data_type` → `data_source` parameter name in all examples +- Input validation: silent failures now throw informative errors + +#### Security +- Added comprehensive input validation to prevent invalid data processing + +--- + +### [1.1.0] - Previous Release + +#### Added +- Initial release with core TiNDA analysis functionality +- Canopy-based EM clustering for variant classification +- Visualization functions: `canopy_clst_plot()`, `tinda_clst_plot()`, `tinda_linear_plot()`, `tinda_summary_plot()` +- Data simulation functions: `generate_test_data()`, `simulate_variants()`, `generate_depth()` +- Output function: `write_data()` +- Support for hg19 and hg38 reference genomes + +--- + +## Versioning + +This project follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** version: Incompatible API changes +- **MINOR** version: Backward-compatible new functionality +- **PATCH** version: Backward-compatible bug fixes + +For the latest version information, check the GitHub releases page. \ No newline at end of file diff --git a/inst/CITATION b/inst/CITATION new file mode 100644 index 0000000..0a86aa2 --- /dev/null +++ b/inst/CITATION @@ -0,0 +1,11 @@ +cat("To cite TiNDA in publications please use:\n\n") +cat("Paramasivam N. TiNDA: Tumor in Normal Detection Analysis. 2024.\n\n") +cat("R package version", utils::packageVersion("TiNDA"), "\n\n") +cat("BibTeX entry:\n\n") +cat('@Manual{,\n') +cat(' title = {TiNDA: Tumor in Normal Detection Analysis},\n') +cat(' author = {Nagarajan Paramasivam},\n') +cat(' year = {2024},\n') +cat(' note = {R package version', utils::packageVersion("TiNDA"), '},\n') +cat(' url = {https://github.com/NagaComBio/TiNDA},\n') +cat('}\n') diff --git a/tests/testthat.R b/tests/testthat.R new file mode 100644 index 0000000..44c9228 --- /dev/null +++ b/tests/testthat.R @@ -0,0 +1,4 @@ +library(testthat) +library(TiNDA) + +test_check("TiNDA") diff --git a/tests/testthat/test-basic.R b/tests/testthat/test-basic.R new file mode 100644 index 0000000..84d0da6 --- /dev/null +++ b/tests/testthat/test-basic.R @@ -0,0 +1,94 @@ +# Basic tests for TiNDA package + +test_that("TiNDA accepts valid input", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 100) + + expect_no_error({ + result <- TiNDA(test_df, sample_name = "test_1") + }) + + expect_s3_class(result, "TiNDA") +}) + +test_that("TiNDA validates input columns", { + bad_df <- data.frame( + CHR = c(1, 2), + POS = c(100, 200) + ) + + expect_error( + TiNDA(bad_df), + "Missing required columns" + ) +}) + +test_that("TiNDA validates data_source parameter", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 100) + + expect_error( + TiNDA(test_df, data_source = "invalid"), + "data_source must be either" + ) +}) + +test_that("TiNDA validates input is data frame", { + expect_error( + TiNDA(list(a = 1)), + "tbl must be a data frame" + ) +}) + +test_that("TiNDA returns expected structure", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 100) + result <- TiNDA(test_df, sample_name = "test_1") + + expect_true("data" %in% names(result)) + expect_true("sample_name" %in% names(result)) + expect_true("classification_summary" %in% names(result)) + expect_true("parameters" %in% names(result)) +}) + +test_that("TiNDA classification includes expected classes", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 200) + result <- TiNDA(test_df, sample_name = "test_1") + + classes <- unique(result$data$TiN_Class) + expect_true("Germline" %in% classes || "Somatic_Rescue" %in% classes) +}) + +test_that("print.TiNDA works without error", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 100) + result <- TiNDA(test_df, sample_name = "test_1") + + expect_no_error(print(result)) +}) + +test_that("summary.TiNDA returns expected fields", { + data(hg19_length) + test_df <- generate_test_data(hg19_length, num_variants = 100) + result <- TiNDA(test_df, sample_name = "test_1") + summ <- summary(result) + + expect_true("total_variants" %in% names(summ)) + expect_true("classification" %in% names(summ)) +}) + +test_that("get_tinda_params returns correct defaults", { + params <- get_tinda_params("WGS") + + expect_equal(params$max_control_af, 0.25) + expect_equal(params$min_tumor_af, 0.01) + expect_equal(params$min_clst_members, 0.85) +}) + +test_that("get_tinda_params validates data_source", { + expect_error( + get_tinda_params("invalid"), + "data_source must be either" + ) +})