246 lines
8.9 KiB
R
246 lines
8.9 KiB
R
# Helpers
|
|
|
|
#' Get workflow_glue_r repository root directory from environment variable.
|
|
#' @return Normalized path to workflow_glue_r repository root directory.
|
|
#' @export
|
|
workflow_glue_r_repo_root <- function() {
|
|
normalizePath(Sys.getenv("WORKFLOW_GLUE_R_REPO_ROOT"))
|
|
}
|
|
|
|
|
|
#' Get path to a test fixture file within the workflow_glue_r test data directory.
|
|
#' @param ... Path components within the test data directory.
|
|
#' @return Normalized path to the specified test fixture file.
|
|
#' @export
|
|
workflow_glue_r_fixture <- function(...) {
|
|
file.path(Sys.getenv("TEST_DATA"), "workflow_glue_r", ...)
|
|
}
|
|
|
|
#' Run a system command and capture its output and status.
|
|
#' @param command Command to run.
|
|
#' @param args Arguments to the command.
|
|
#' @param stdout Whether to capture standard output (TRUE or file path).
|
|
#' @param stderr Whether to capture standard error (TRUE or file path).
|
|
#' @return A list with 'status' (exit status) and 'output' (captured output).
|
|
#' @export
|
|
run_system2 <- function(command, args, stdout = TRUE, stderr = TRUE) {
|
|
output <- system2(command, args, stdout = stdout, stderr = stderr)
|
|
if (is.character(output)) {
|
|
status <- attr(output, "status")
|
|
if (is.null(status)) {
|
|
status <- 0L
|
|
}
|
|
return(list(status = status, output = output))
|
|
}
|
|
|
|
captured <- character(0)
|
|
if (is.character(stderr) && file.exists(stderr)) {
|
|
captured <- readLines(stderr, warn = FALSE)
|
|
}
|
|
|
|
list(status = as.integer(output), output = captured)
|
|
}
|
|
#' Run an R script using Rscript.
|
|
#' @param script_name Name of the R script to run.
|
|
#' @param args Arguments to pass to the R script.
|
|
#' @return A list with 'status' (exit status) and 'output' (captured output).
|
|
#' @export
|
|
run_rscript <- function(script_name, args) {
|
|
command <- file.path(R.home("bin"), "Rscript")
|
|
script <- file.path(workflow_glue_r_repo_root(), "bin", script_name)
|
|
run_system2(command, c(script, args), stdout = TRUE, stderr = TRUE)
|
|
}
|
|
|
|
#' Create a test transcript-level SummarizedExperiment with synthetic count data.
|
|
#'
|
|
#' SummarizedExperiment is a Bioconductor container that holds:
|
|
#' - assays: matrices of counts/CPM (rows=transcripts, cols=samples)
|
|
#' - rowRanges: genomic coordinates and metadata for each transcript
|
|
#' - colData: sample metadata (not used here)
|
|
#'
|
|
#' This fixture creates realistic differential expression patterns:
|
|
#' - tx1: high in control, low in treated (downregulated)
|
|
#' - tx2: low in control, high in treated (upregulated)
|
|
#' - tx3: similar across conditions (not DE)
|
|
#' - tx4: low counts in both (filtered in real DE analysis)
|
|
#'
|
|
#' @param include_geneid Whether to include GENEID metadata column in rowRanges.
|
|
#' @param sample_names Optional vector of sample names to use as column names.
|
|
#' @return A SummarizedExperiment object with synthetic transcript-level data.
|
|
#' @export
|
|
make_test_tx_se <- function(include_geneid = TRUE, sample_names = NULL) {
|
|
if (is.null(sample_names)) {
|
|
sample_names <- c(
|
|
"control_rep1",
|
|
"control_rep2",
|
|
"control_rep3",
|
|
"treated_rep1",
|
|
"treated_rep2",
|
|
"treated_rep3"
|
|
)
|
|
}
|
|
|
|
counts <- vapply(sample_names, function(sample_name) {
|
|
condition <- sub("_rep[0-9]+$", "", sample_name)
|
|
replicate_id <- suppressWarnings(as.integer(sub("^.*_rep", "", sample_name)))
|
|
if (is.na(replicate_id)) {
|
|
replicate_id <- match(sample_name, sample_names)
|
|
}
|
|
offset <- ((replicate_id - 1L) %% 3L) - 1L
|
|
|
|
base_counts <- switch(
|
|
condition,
|
|
control = c(120, 18, 80, 15),
|
|
treated = c(25, 95, 76, 14),
|
|
treated2 = c(35, 85, 60, 40),
|
|
baseline = c(120, 18, 80, 15),
|
|
c(80, 40, 70, 20)
|
|
)
|
|
pmax(base_counts + c(offset, -offset, offset, 0), 1)
|
|
}, numeric(4))
|
|
dimnames(counts) <- list(c("tx1", "tx2", "tx3", "tx4"), sample_names)
|
|
cpm <- t(t(counts) / colSums(counts)) * 1e6
|
|
|
|
row_ranges <- GenomicRanges::GRanges(
|
|
seqnames = rep("chr1", 4),
|
|
ranges = IRanges::IRanges(start = c(1, 101, 201, 301), width = 50),
|
|
strand = rep("+", 4),
|
|
TXNAME = rownames(counts),
|
|
eqClassById = IRanges::CharacterList(list(c("1", "2"), "3", "4", "5"))
|
|
)
|
|
if (include_geneid) {
|
|
S4Vectors::mcols(row_ranges)$GENEID <- c("gene1", "gene1", "gene2", "gene2")
|
|
}
|
|
|
|
SummarizedExperiment::SummarizedExperiment(
|
|
assays = list(counts = counts, CPM = cpm),
|
|
rowRanges = row_ranges
|
|
)
|
|
}
|
|
|
|
#' Create a test gene-level SummarizedExperiment by aggregating a transcript-level SummarizedExperiment.
|
|
#' @param sample_names Optional vector of sample names to use as column names.
|
|
#' @return A SummarizedExperiment object with synthetic gene-level data.
|
|
#' @export
|
|
make_test_gene_se <- function(sample_names = NULL) {
|
|
if (is.null(sample_names)) {
|
|
sample_names <- c(
|
|
"control_rep1",
|
|
"control_rep2",
|
|
"control_rep3",
|
|
"treated_rep1",
|
|
"treated_rep2",
|
|
"treated_rep3"
|
|
)
|
|
}
|
|
|
|
tx_se <- make_test_tx_se(sample_names = sample_names)
|
|
tx_counts <- SummarizedExperiment::assays(tx_se)$counts
|
|
counts <- rbind(
|
|
gene1 = tx_counts["tx1", ] + tx_counts["tx2", ],
|
|
gene2 = tx_counts["tx3", ] + tx_counts["tx4", ]
|
|
)
|
|
cpm <- t(t(counts) / colSums(counts)) * 1e6
|
|
|
|
SummarizedExperiment::SummarizedExperiment(
|
|
assays = list(counts = counts, CPM = cpm),
|
|
rowData = S4Vectors::DataFrame(GENEID = rownames(counts))
|
|
)
|
|
}
|
|
|
|
#' Create a test DE analysis input bundle with synthetic transcript and gene
|
|
#' SummarizedExperiments and a sample sheet.
|
|
#' @param out_dir Directory to write the output files.
|
|
#' @param levels Optional vector of condition levels to use in the sample sheet.
|
|
#' @return A list with paths to the generated transcript RDS, gene RDS, and sample sheet CSV.
|
|
#' @export
|
|
write_de_fixture_bundle <- function(out_dir, levels = c("control", "treated")) {
|
|
sample_names <- unlist(lapply(levels, function(level) {
|
|
sprintf("%s_rep%d", level, seq_len(3))
|
|
}))
|
|
|
|
tx_se <- make_test_tx_se(sample_names = sample_names)
|
|
gene_se <- make_test_gene_se(sample_names = sample_names)
|
|
|
|
tx_path <- file.path(out_dir, "transcripts.rds")
|
|
gene_path <- file.path(out_dir, "genes.rds")
|
|
saveRDS(tx_se, tx_path)
|
|
saveRDS(gene_se, gene_path)
|
|
|
|
sample_sheet <- file.path(out_dir, "sample_sheet.csv")
|
|
sample_rows <- do.call(
|
|
rbind,
|
|
lapply(levels, function(level) {
|
|
data.frame(
|
|
alias = sprintf("%s_rep%d", level, seq_len(3)),
|
|
condition = level,
|
|
batch = c("b1", "b2", "b1"),
|
|
stringsAsFactors = FALSE
|
|
)
|
|
})
|
|
)
|
|
utils::write.csv(sample_rows, sample_sheet, row.names = FALSE, quote = FALSE)
|
|
|
|
list(
|
|
transcript_rds = tx_path,
|
|
gene_rds = gene_path,
|
|
sample_sheet = sample_sheet
|
|
)
|
|
}
|
|
|
|
# Expecters
|
|
|
|
#' Expect a system command to succeed.
|
|
#' @param command Command to run.
|
|
#' @param args Arguments to the command.
|
|
#' @param stdout Whether to capture standard output (TRUE or file path).
|
|
#' @param stderr Whether to capture standard error (TRUE or file path).
|
|
#' @return Invisibly returns the result of the command.
|
|
#' @export
|
|
expect_command_success <- function(command, args, stdout = TRUE, stderr = TRUE) {
|
|
result <- run_system2(command, args, stdout = stdout, stderr = stderr)
|
|
testthat::expect_equal(
|
|
result$status,
|
|
0L,
|
|
info = paste(c(command, args, result$output), collapse = "\n")
|
|
)
|
|
invisible(result)
|
|
}
|
|
|
|
#' Expect a BAM fixture can be built with minimap2 and samtools.
|
|
#' @param reference Path to the reference FASTA file.
|
|
#' @param reads Path to the FASTQ file containing reads to align.
|
|
#' @param out_dir Directory to write the output BAM file and intermediate files.
|
|
#' @param alias Sample alias to use in output file names.
|
|
#' @return Path to the generated BAM file.
|
|
#' @export
|
|
expect_bam_fixture_built <- function(reference, reads, out_dir, alias = "sampleA") {
|
|
sam_path <- file.path(out_dir, sprintf("%s.sam", alias))
|
|
bam_path <- file.path(out_dir, sprintf("%s.aligned.sorted.bam", alias))
|
|
minimap2_stderr <- file.path(out_dir, sprintf("%s.minimap2.stderr.txt", alias))
|
|
sort_stderr <- file.path(out_dir, sprintf("%s.samtools-sort.stderr.txt", alias))
|
|
index_stderr <- file.path(out_dir, sprintf("%s.samtools-index.stderr.txt", alias))
|
|
|
|
expect_command_success(
|
|
"minimap2",
|
|
c("-ax", "splice", "-uf", reference, reads),
|
|
stdout = sam_path,
|
|
stderr = minimap2_stderr
|
|
)
|
|
testthat::expect_true(file.exists(sam_path))
|
|
testthat::expect_gt(file.info(sam_path)$size, 0)
|
|
|
|
expect_command_success(
|
|
"samtools",
|
|
c("sort", "-o", bam_path, sam_path),
|
|
stderr = sort_stderr
|
|
)
|
|
expect_command_success(
|
|
"samtools",
|
|
c("index", bam_path),
|
|
stderr = index_stderr
|
|
)
|
|
|
|
bam_path
|
|
}
|