From b78db92a6c6b4211c091e13d13f219d4a165f3d3 Mon Sep 17 00:00:00 2001 From: devonjkohler Date: Thu, 16 Jul 2026 11:29:41 -0400 Subject: [PATCH 1/3] new unit tests --- inst/tinytest/test_SDRFconverter.R | 64 ++++++++++++ inst/tinytest/test_dataProcessPlots.R | 83 +++++++++++++++ inst/tinytest/test_designSampleSize.R | 78 ++++++++++++++ inst/tinytest/test_groupComparisonPlots.R | 101 +++++++++++++++++++ inst/tinytest/test_modelBasedQCPlots.R | 49 +++++++++ inst/tinytest/test_plot_quality_metrics.R | 69 +++++++++++++ inst/tinytest/test_quantification.R | 36 +++++++ inst/tinytest/test_utils_anomaly_weights.R | 34 +++++++ inst/tinytest/test_utils_feature_selection.R | 42 ++++++++ inst/tinytest/test_utils_plots_common.R | 74 ++++++++++++++ 10 files changed, 630 insertions(+) create mode 100644 inst/tinytest/test_SDRFconverter.R create mode 100644 inst/tinytest/test_dataProcessPlots.R create mode 100644 inst/tinytest/test_designSampleSize.R create mode 100644 inst/tinytest/test_groupComparisonPlots.R create mode 100644 inst/tinytest/test_modelBasedQCPlots.R create mode 100644 inst/tinytest/test_plot_quality_metrics.R create mode 100644 inst/tinytest/test_quantification.R create mode 100644 inst/tinytest/test_utils_anomaly_weights.R create mode 100644 inst/tinytest/test_utils_feature_selection.R create mode 100644 inst/tinytest/test_utils_plots_common.R diff --git a/inst/tinytest/test_SDRFconverter.R b/inst/tinytest/test_SDRFconverter.R new file mode 100644 index 00000000..d5fbe7f1 --- /dev/null +++ b/inst/tinytest/test_SDRFconverter.R @@ -0,0 +1,64 @@ +# Test SDRFtoAnnotation ------------------------------------------------------ + +# Test 1: default column names on the built-in example_SDRF dataset +annot = SDRFtoAnnotation(example_SDRF) +expect_true(is.data.frame(annot)) +expect_equal(colnames(annot), c("Run", "Condition", "BioReplicate")) +expect_equal(nrow(annot), nrow(example_SDRF)) + +# Test 2: fraction column can be added when provided +annot_fraction = SDRFtoAnnotation( + example_SDRF, + fraction = "comment[fraction identifier]" +) +expect_true("Fraction" %in% colnames(annot_fraction)) +expect_equal(nrow(annot_fraction), nrow(example_SDRF)) + +# Test 3: missing column name errors +expect_error( + SDRFtoAnnotation(example_SDRF, run_name = "not a real column") +) + +# Test SDRF/extractSDRF ------------------------------------------------------- + +msstats_format_data = data.table::data.table( + Condition = c("A", "A", "B", "B"), + BioReplicate = c("1", "2", "3", "4"), + Run = c("run1", "run2", "run3", "run4"), + Fraction = c(1, 1, 1, 1), + Protein = c("P1", "P1", "P1", "P1"), + Intensity = c(10, 20, 30, 40) +) + +# Test 4: default call keeps Fraction column, and values land in the +# correctly-named columns (Run -> run_name, Condition -> condition_name, +# BioReplicate -> biological_replicate), not just column names matching +sdrf_with_fraction = extractSDRF(msstats_format_data, fraction = "Fraction") +expect_true(is.data.frame(sdrf_with_fraction)) +expect_equal(nrow(sdrf_with_fraction), 4) +expect_true("Fraction" %in% colnames(sdrf_with_fraction)) +expect_true(all(sdrf_with_fraction[["comment[data file]"]] %in% + msstats_format_data$Run)) +expect_true(all(sdrf_with_fraction[["characteristics[disease]"]] %in% + msstats_format_data$Condition)) +expect_true(all(sdrf_with_fraction[["characteristics[biological replicate]"]] %in% + msstats_format_data$BioReplicate)) + +# Test 5: fraction = NULL drops the Fraction column and renames the others +sdrf_no_fraction = extractSDRF(msstats_format_data) +expect_false("Fraction" %in% colnames(sdrf_no_fraction)) +expect_true(all(c("comment[data file]", "characteristics[disease]", + "characteristics[biological replicate]") %in% + colnames(sdrf_no_fraction))) +expect_true(all(sdrf_no_fraction[["comment[data file]"]] %in% + msstats_format_data$Run)) + +# Test 6: meta_data is merged in by run name +meta = data.frame( + `comment[data file]` = c("run1", "run2", "run3", "run4"), + instrument = c("QE", "QE", "QE", "QE"), + check.names = FALSE +) +sdrf_with_meta = extractSDRF(msstats_format_data, meta_data = meta) +expect_true("instrument" %in% colnames(sdrf_with_meta)) +expect_equal(nrow(sdrf_with_meta), 4) diff --git a/inst/tinytest/test_dataProcessPlots.R b/inst/tinytest/test_dataProcessPlots.R new file mode 100644 index 00000000..6e6352fb --- /dev/null +++ b/inst/tinytest/test_dataProcessPlots.R @@ -0,0 +1,83 @@ +# Setup ------------------------------------------------------------------ +QuantData = dataProcess(SRMRawData, use_log_file = FALSE) +protein_name = as.character(unique(QuantData$ProteinLevelData$Protein))[1] + +# Test 1: invalid type errors ------------------------------------------------- +expect_error(dataProcessPlots(QuantData, type = "INVALID")) + +# Test 2: address = FALSE with which.Protein = "all" errors ------------------- +expect_error( + dataProcessPlots(QuantData, type = "ProfilePlot", which.Protein = "all", + address = FALSE) +) + +# Test 3: address = FALSE with multiple proteins errors ----------------------- +expect_error( + dataProcessPlots(QuantData, type = "ProfilePlot", which.Protein = c(1, 2), + address = FALSE) +) + +# ggplot2 (isPlotly = FALSE) branches, saved to a tempdir --------------------- + +tmp_dir = tempfile("msstats_dataprocessplots_") +dir.create(tmp_dir) +address_prefix = paste0(tmp_dir, "/") + +# Test 4: ProfilePlot (ggplot2) creates a pdf file, and always warns ---------- +expect_warning( + dataProcessPlots(QuantData, type = "ProfilePlot", which.Protein = protein_name, + address = address_prefix, + remove_uninformative_feature_outlier = TRUE) +) +expect_true(file.exists(paste0(address_prefix, "ProfilePlot.pdf"))) + +# Test 5: QCPlot (ggplot2) creates a pdf file --------------------------------- +expect_warning( + dataProcessPlots(QuantData, type = "QCPlot", which.Protein = protein_name, + address = address_prefix) +) +expect_true(file.exists(paste0(address_prefix, "QCPlot.pdf"))) + +# Test 6: ConditionPlot (ggplot2) creates a pdf file -------------------------- +expect_warning( + dataProcessPlots(QuantData, type = "ConditionPlot", which.Protein = protein_name, + address = address_prefix) +) +expect_true(file.exists(paste0(address_prefix, "ConditionPlot.pdf"))) + +unlink(tmp_dir, recursive = TRUE) + +# plotly (isPlotly = TRUE) branches, address = FALSE (no file saving) --------- + +# Test 7: ProfilePlot (plotly) returns a list of plotly objects -------------- +plotly_profile = suppressWarnings( + dataProcessPlots(QuantData, type = "ProfilePlot", which.Protein = protein_name, + address = FALSE, isPlotly = TRUE) +) +expect_true(is.list(plotly_profile)) +expect_true(length(plotly_profile) > 0) +expect_true(inherits(plotly_profile[[1]], "plotly")) + +# Test 8: QCPlot (plotly) returns a list of plotly objects ------------------- +plotly_qc = suppressWarnings( + dataProcessPlots(QuantData, type = "QCPlot", which.Protein = protein_name, + address = FALSE, isPlotly = TRUE) +) +expect_true(is.list(plotly_qc)) +expect_true(length(plotly_qc) > 0) +expect_true(inherits(plotly_qc[[1]], "plotly")) + +# Test 9: ConditionPlot (plotly), saved as a zipped HTML file ---------------- +# Note: which.Protein must be "all" here (rather than a single protein as +# above) - selecting a subset together with isPlotly = TRUE hits a pre-existing +# bug in dataProcessPlots() where NULL placeholders for unselected proteins +# are still passed to the ggplot-to-plotly conversion step. +tmp_dir2 = tempfile("msstats_dataprocessplots_condition_") +dir.create(tmp_dir2) +address_prefix2 = paste0(tmp_dir2, "/") +invisible(capture.output(suppressWarnings( + dataProcessPlots(QuantData, type = "ConditionPlot", which.Protein = "all", + address = address_prefix2, isPlotly = TRUE) +))) +expect_true(any(grepl("ConditionPlot.*\\.zip$", list.files(tmp_dir2)))) +unlink(tmp_dir2, recursive = TRUE) diff --git a/inst/tinytest/test_designSampleSize.R b/inst/tinytest/test_designSampleSize.R new file mode 100644 index 00000000..d01adf4b --- /dev/null +++ b/inst/tinytest/test_designSampleSize.R @@ -0,0 +1,78 @@ +# Setup ------------------------------------------------------------------ +QuantData = dataProcess(SRMRawData, use_log_file = FALSE) +comparison1 = matrix(c(-1,0,1,0,0,0,0,0,0,0), nrow=1) +comparison2 = matrix(c(-1,0,0,0,0,0,1,0,0,0), nrow=1) +comparison3 = matrix(c(-1,0,0,0,0,0,0,0,1,0), nrow=1) +comparison = rbind(comparison1, comparison2, comparison3) +row.names(comparison) = c("T3-T1", "T7-T1", "T9-T1") +groups = levels(QuantData$ProteinLevelData$GROUP) +colnames(comparison) = groups[order(as.numeric(groups))] + +testResultMultiComparisons = groupComparison(contrast.matrix = comparison, + data = QuantData, + use_log_file = FALSE) + +# Test 1: designSampleSize with numSample = TRUE (calculate sample size) ----- +result_sample = designSampleSize(data = testResultMultiComparisons$FittedModel, + numSample = TRUE, + desiredFC = c(1.25, 1.75), FDR = 0.05, power = 0.8, + use_log_file = FALSE) +expect_true(is.data.frame(result_sample)) +expect_true(all(c("desiredFC", "numSample", "FDR", "power", "CV") %in% + colnames(result_sample))) +expect_true(all(result_sample$numSample > 0)) + +# Test 2: designSampleSize with power = TRUE (calculate power) --------------- +result_power = designSampleSize(data = testResultMultiComparisons$FittedModel, + numSample = 2, + desiredFC = c(1.25, 1.75), FDR = 0.05, power = TRUE, + use_log_file = FALSE) +expect_true(is.data.frame(result_power)) +expect_true(all(c("desiredFC", "numSample", "FDR", "power", "CV") %in% + colnames(result_power))) +expect_true(all(result_power$power >= 0 & result_power$power <= 1)) +expect_true(all(result_power$numSample == 2)) + +# Test 3: increasing desired fold change reduces the required sample size --- +result_sample_small_fc = designSampleSize(data = testResultMultiComparisons$FittedModel, + numSample = TRUE, + desiredFC = c(1.05, 1.075), FDR = 0.05, power = 0.8, + use_log_file = FALSE) +expect_true(max(result_sample_small_fc$numSample) >= min(result_sample$numSample)) + +# Test designSampleSizePlots (ggplot2 branch, isPlotly = FALSE) --------------- + +tmp_dir = tempfile("msstats_designsamplesize_") +dir.create(tmp_dir) +pdf(file.path(tmp_dir, "plots.pdf")) + +# Test 4: numSample plot (base graphics) does not error +expect_silent(designSampleSizePlots(data = result_sample)) + +# Test 5: power plot (base graphics) does not error +expect_silent(designSampleSizePlots(data = result_power)) + +dev.off() +unlink(tmp_dir, recursive = TRUE) + +# Test designSampleSizePlots (plotly branch, isPlotly = TRUE) ----------------- + +# Test 6: numSample plot returns a plotly object +plotly_sample = designSampleSizePlots(data = result_sample, isPlotly = TRUE) +expect_true(inherits(plotly_sample, "plotly")) + +# Test 7: power plot returns a plotly object +plotly_power = designSampleSizePlots(data = result_power, isPlotly = TRUE) +expect_true(inherits(plotly_power, "plotly")) + +# Test .getVarComponent / .getMedianSigmaSubject / .calculatePower / .getNumSample + +# Test 8: .getVarComponent extracts Error/Subject/GroupBySubject per protein +var_component = MSstats:::.getVarComponent(testResultMultiComparisons$FittedModel) +expect_true(all(c("Error", "Subject", "GroupBySubject", "Protein") %in% + colnames(var_component))) +expect_equal(nrow(var_component), length(testResultMultiComparisons$FittedModel)) + +# Test 9: .getMedianSigmaSubject returns 0 when all components are NA -------- +all_na_component = data.frame(Subject = NA_real_, GroupBySubject = NA_real_) +expect_equal(MSstats:::.getMedianSigmaSubject(all_na_component), 0) diff --git a/inst/tinytest/test_groupComparisonPlots.R b/inst/tinytest/test_groupComparisonPlots.R new file mode 100644 index 00000000..a0e500f1 --- /dev/null +++ b/inst/tinytest/test_groupComparisonPlots.R @@ -0,0 +1,101 @@ +# Setup ------------------------------------------------------------------ +QuantData = dataProcess(SRMRawData, use_log_file = FALSE) +comparison1 = matrix(c(-1,0,1,0,0,0,0,0,0,0), nrow=1) +comparison2 = matrix(c(-1,0,0,0,0,0,1,0,0,0), nrow=1) +comparison3 = matrix(c(-1,0,0,0,0,0,0,0,1,0), nrow=1) +comparison = rbind(comparison1, comparison2, comparison3) +row.names(comparison) = c("T3-T1", "T7-T1", "T9-T1") +groups = levels(QuantData$ProteinLevelData$GROUP) +colnames(comparison) = groups[order(as.numeric(groups))] + +testResultMultiComparisons = groupComparison(contrast.matrix = comparison, + data = QuantData, + use_log_file = FALSE) +comparison_result = testResultMultiComparisons$ComparisonResult + +# Test 1: invalid type errors ------------------------------------------------- +expect_error(groupComparisonPlots(data = comparison_result, type = "INVALID")) + +# Test 2: invalid which.Comparison label errors ------------------------------- +expect_error( + groupComparisonPlots(data = comparison_result, type = "VolcanoPlot", + which.Comparison = "NotARealComparison") +) + +tmp_dir = tempfile("msstats_groupcomparisonplots_") +dir.create(tmp_dir) +address_prefix = paste0(tmp_dir, "/") + +# Test 3: Heatmap (ggplot2) creates a pdf file and always warns -------------- +expect_warning( + groupComparisonPlots(data = comparison_result, type = "Heatmap", + logBase.pvalue = 2, address = address_prefix) +) +expect_true(file.exists(paste0(address_prefix, "Heatmap.pdf"))) + +# Test 4: Heatmap with FCcutoff also runs without error ---------------------- +expect_warning( + groupComparisonPlots(data = comparison_result, type = "Heatmap", + FCcutoff = 70, logBase.pvalue = 2, address = address_prefix) +) + +# Test 5: VolcanoPlot (ggplot2) creates a pdf file, no warning required ------ +invisible(capture.output( + groupComparisonPlots(data = comparison_result, type = "VolcanoPlot", + logBase.pvalue = 2, address = address_prefix) +)) +expect_true(file.exists(paste0(address_prefix, "VolcanoPlot.pdf"))) + +# Test 6: VolcanoPlot with FCcutoff and no protein names --------------------- +invisible(capture.output( + groupComparisonPlots(data = comparison_result, type = "VolcanoPlot", + FCcutoff = 70, logBase.pvalue = 2, ylimUp = 100, + ProteinName = FALSE, address = address_prefix) +)) + +# Test 7: ComparisonPlot (ggplot2) creates a pdf file and always warns ------- +expect_warning( + groupComparisonPlots(data = comparison_result, type = "ComparisonPlot", + address = address_prefix) +) +expect_true(file.exists(paste0(address_prefix, "ComparisonPlot.pdf"))) + +unlink(tmp_dir, recursive = TRUE) + +# plotly (isPlotly = TRUE) branches -------------------------------------------- + +# Test 8: VolcanoPlot (plotly), address = FALSE, returns a list of plotly objects +# (address = FALSE requires a single comparison to be selected) +plotly_volcano = invisible(capture.output( + result_volcano <- groupComparisonPlots(data = comparison_result, type = "VolcanoPlot", + which.Comparison = "T3-T1", + logBase.pvalue = 2, address = FALSE, + isPlotly = TRUE) +)) +expect_true(is.list(result_volcano)) +expect_true(length(result_volcano) > 0) +expect_true(inherits(result_volcano[[1]], "plotly")) + +# Test 9: Heatmap (plotly), saved as a zipped HTML file ---------------------- +tmp_dir2 = tempfile("msstats_groupcomparisonplots_heatmap_") +dir.create(tmp_dir2) +address_prefix2 = paste0(tmp_dir2, "/") +invisible(capture.output(suppressWarnings( + groupComparisonPlots(data = comparison_result, type = "Heatmap", + logBase.pvalue = 2, address = address_prefix2, + isPlotly = TRUE) +))) +expect_true(any(grepl("Heatmap.*\\.zip$", list.files(tmp_dir2)))) +unlink(tmp_dir2, recursive = TRUE) + +# Test 10: ComparisonPlot (plotly, all proteins), saved as a zipped HTML file +tmp_dir3 = tempfile("msstats_groupcomparisonplots_comparison_") +dir.create(tmp_dir3) +address_prefix3 = paste0(tmp_dir3, "/") +invisible(capture.output(suppressWarnings( + groupComparisonPlots(data = comparison_result, type = "ComparisonPlot", + which.Protein = "all", address = address_prefix3, + isPlotly = TRUE) +))) +expect_true(any(grepl("ComparisonPlot.*\\.zip$", list.files(tmp_dir3)))) +unlink(tmp_dir3, recursive = TRUE) diff --git a/inst/tinytest/test_modelBasedQCPlots.R b/inst/tinytest/test_modelBasedQCPlots.R new file mode 100644 index 00000000..dcce6060 --- /dev/null +++ b/inst/tinytest/test_modelBasedQCPlots.R @@ -0,0 +1,49 @@ +# These tests exercise the real modelBasedQCPlots()/groupComparisonQCPlots() +# plotting logic. The existing test_groupComparisonQCPlots.R mocks out +# modelBasedQCPlots entirely, so none of this logic was previously covered. + +QuantData = dataProcess(SRMRawData, use_log_file = FALSE) +comparison = matrix(c(-1,0,0,0,0,0,1,0,0,0), nrow=1) +row.names(comparison) = "T7-T1" +colnames(comparison) = unique(QuantData$ProteinLevelData$GROUP) +testResultOneComparison = groupComparison(contrast.matrix = comparison, data = QuantData, + use_log_file = FALSE) +protein_name = as.character(unique(testResultOneComparison$ComparisonResult$Protein))[1] + +# Test 1: invalid type errors ------------------------------------------------- +expect_error( + modelBasedQCPlots(data = testResultOneComparison, type = "INVALID", + which.Protein = protein_name, address = FALSE) +) + +tmp_dir = tempfile("msstats_modelbasedqc_") +dir.create(tmp_dir) +address_prefix = paste0(tmp_dir, "/") + +# Test 2: QQPlots creates a pdf file ------------------------------------------ +invisible(capture.output( + modelBasedQCPlots(data = testResultOneComparison, type = "QQPlots", + which.Protein = protein_name, address = address_prefix) +)) +expect_true(file.exists(paste0(address_prefix, "QQPlot.pdf"))) + +# Test 3: ResidualPlots creates a pdf file ------------------------------------ +invisible(capture.output( + modelBasedQCPlots(data = testResultOneComparison, type = "ResidualPlots", + which.Protein = protein_name, address = address_prefix) +)) +expect_true(file.exists(paste0(address_prefix, "ResidualPlot.pdf"))) + +unlink(tmp_dir, recursive = TRUE) + +# Test 4: groupComparisonQCPlots() wrapper actually calls through to +# modelBasedQCPlots and produces the same output ------------------------------ +tmp_dir2 = tempfile("msstats_groupcomparisonqcplots_") +dir.create(tmp_dir2) +address_prefix2 = paste0(tmp_dir2, "/") +invisible(capture.output( + groupComparisonQCPlots(data = testResultOneComparison, type = "QQPlots", + which.Protein = protein_name, address = address_prefix2) +)) +expect_true(file.exists(paste0(address_prefix2, "QQPlot.pdf"))) +unlink(tmp_dir2, recursive = TRUE) diff --git a/inst/tinytest/test_plot_quality_metrics.R b/inst/tinytest/test_plot_quality_metrics.R new file mode 100644 index 00000000..5228acc5 --- /dev/null +++ b/inst/tinytest/test_plot_quality_metrics.R @@ -0,0 +1,69 @@ +# Setup ------------------------------------------------------------------ +quality_data = data.frame( + ProteinName = rep(c("ProteinA", "ProteinB"), each = 12), + PeptideSequence = rep(rep(c("PEPTIDEA", "PEPTIDEB"), each = 6), 2), + PrecursorCharge = rep(2, 24), + Run = rep(paste0("Run", 1:6), 4), + AnomalyScores = c(1:6, 2:7, 3:8, 4:9) / 10, + EGDeltaRT = seq(0.1, 2.4, length.out = 24) +) + +# Test 1: which.Protein is required ------------------------------------------- +expect_error(MSstatsQualityMetricsPlot(quality_data)) + +# Test 2: missing required column errors -------------------------------------- +missing_col_data = quality_data +missing_col_data$Run = NULL +expect_error( + MSstatsQualityMetricsPlot(missing_col_data, which.Protein = "ProteinA") +) + +# Test 3: metric not present errors ------------------------------------------- +expect_error( + MSstatsQualityMetricsPlot(quality_data, metric = "NotAColumn", + which.Protein = "ProteinA") +) + +# Test 4: protein not present errors ------------------------------------------ +expect_error( + MSstatsQualityMetricsPlot(quality_data, which.Protein = "NotAProtein") +) + +# Test 5: default call (address = FALSE) returns a ggplot object ------------- +p_default = MSstatsQualityMetricsPlot(quality_data, which.Protein = "ProteinA") +expect_true(inherits(p_default, "ggplot")) + +# Test 6: a different metric column can be selected --------------------------- +p_metric = MSstatsQualityMetricsPlot(quality_data, metric = "EGDeltaRT", + which.Protein = "ProteinB") +expect_true(inherits(p_metric, "ggplot")) +expect_equal(p_metric$labels$y, "EGDeltaRT") + +# Test 7: isPlotly = TRUE returns a plotly object ----------------------------- +p_plotly = MSstatsQualityMetricsPlot(quality_data, which.Protein = "ProteinA", + isPlotly = TRUE) +expect_true(inherits(p_plotly, "plotly")) + +# Test 8: address != FALSE saves a pdf file (ggplot2) ------------------------- +tmp_dir = tempfile("msstats_qualitymetrics_") +dir.create(tmp_dir) +address_prefix = paste0(tmp_dir, "/") +invisible(MSstatsQualityMetricsPlot(quality_data, which.Protein = "ProteinA", + address = address_prefix)) +expect_true(file.exists(paste0(address_prefix, "QualityMetricsPlot.pdf"))) +unlink(tmp_dir, recursive = TRUE) + +# Test 9: address != FALSE saves an html file (plotly) ------------------------ +tmp_dir2 = tempfile("msstats_qualitymetrics_html_") +dir.create(tmp_dir2) +address_prefix2 = paste0(tmp_dir2, "/") +invisible(MSstatsQualityMetricsPlot(quality_data, which.Protein = "ProteinA", + address = address_prefix2, isPlotly = TRUE)) +expect_true(file.exists(paste0(address_prefix2, "QualityMetricsPlot.html"))) +unlink(tmp_dir2, recursive = TRUE) + +# Test 10: Run column already a factor is respected (not re-leveled) -------- +factor_data = quality_data +factor_data$Run = factor(factor_data$Run, levels = paste0("Run", 6:1)) +p_factor = MSstatsQualityMetricsPlot(factor_data, which.Protein = "ProteinA") +expect_true(inherits(p_factor, "ggplot")) diff --git a/inst/tinytest/test_quantification.R b/inst/tinytest/test_quantification.R new file mode 100644 index 00000000..7d88f181 --- /dev/null +++ b/inst/tinytest/test_quantification.R @@ -0,0 +1,36 @@ +# Setup ------------------------------------------------------------------ +QuantData = dataProcess(SRMRawData, use_log_file = FALSE) + +# Test 1: Sample quantification, matrix format (default) ------------------- +sample_matrix = quantification(QuantData, use_log_file = FALSE) +expect_true(is.data.frame(sample_matrix)) +expect_true("Protein" %in% colnames(sample_matrix)) +expect_equal(nrow(sample_matrix), length(unique(QuantData$ProteinLevelData$Protein))) + +# Test 2: Sample quantification, long format -------------------------------- +sample_long = quantification(QuantData, type = "Sample", format = "long", + use_log_file = FALSE) +expect_true(is.data.frame(sample_long)) +expect_true(all(c("Protein", "Group_Subject", "LogIntensity") %in% colnames(sample_long))) + +# Test 3: Group quantification, matrix format ------------------------------- +group_matrix = quantification(QuantData, type = "Group", use_log_file = FALSE) +expect_true(is.data.frame(group_matrix)) +expect_true("Protein" %in% colnames(group_matrix)) +expect_equal(nrow(group_matrix), length(unique(QuantData$ProteinLevelData$Protein))) + +# Test 4: Group quantification, long format --------------------------------- +group_long = quantification(QuantData, type = "Group", format = "long", + use_log_file = FALSE) +expect_true(is.data.frame(group_long)) +expect_true(all(c("Protein", "Group", "LogIntensity") %in% colnames(group_long))) + +# Test 5: type is case-insensitive ------------------------------------------ +sample_lower = quantification(QuantData, type = "sample", use_log_file = FALSE) +expect_equal(dim(sample_lower), dim(sample_matrix)) + +# Test 6: invalid type errors ----------------------------------------------- +expect_error(quantification(QuantData, type = "Invalid", use_log_file = FALSE)) + +# Test 7: invalid format errors ---------------------------------------------- +expect_error(quantification(QuantData, format = "Invalid", use_log_file = FALSE)) diff --git a/inst/tinytest/test_utils_anomaly_weights.R b/inst/tinytest/test_utils_anomaly_weights.R new file mode 100644 index 00000000..ae46c887 --- /dev/null +++ b/inst/tinytest/test_utils_anomaly_weights.R @@ -0,0 +1,34 @@ +# Test anomaly_weights_z_vec ------------------------------------------------ + +# Test 1: fewer than 3 non-NA values returns all weights of 1 for non-NA entries +s_few = c(1, NA, 3) +w_few = MSstats:::anomaly_weights_z_vec(s_few) +expect_equal(w_few, c(1, NA, 1)) + +# Test 2: all NA +s_all_na = c(NA_real_, NA_real_) +w_all_na = MSstats:::anomaly_weights_z_vec(s_all_na) +expect_true(all(is.na(w_all_na))) + +# Test 3: no anomalies - roughly homogeneous values should get similar weights near 1 +s_homog = c(10, 10.1, 9.9, 10.05, 9.95) +w_homog = MSstats:::anomaly_weights_z_vec(s_homog) +expect_equal(length(w_homog), length(s_homog)) +expect_true(all(!is.na(w_homog))) +expect_true(all(w_homog >= 0.05 & w_homog <= 5)) + +# Test 4: a clear anomaly should receive a lower weight than the rest +s_anomaly = c(10, 10.1, 9.9, 10.05, 100) +w_anomaly = MSstats:::anomaly_weights_z_vec(s_anomaly) +expect_true(w_anomaly[5] < min(w_anomaly[1:4])) + +# Test 5: normalize_sum_to_n = FALSE skips renormalization step (weights unbounded by n) +w_no_norm = MSstats:::anomaly_weights_z_vec(s_anomaly, normalize_sum_to_n = FALSE) +w_norm = MSstats:::anomaly_weights_z_vec(s_anomaly, normalize_sum_to_n = TRUE) +expect_true(!isTRUE(all.equal(w_no_norm, w_norm))) + +# Test 6: NA values are preserved as NA in output, non-NA positions filled +s_mixed = c(1, 2, NA, 4, 5, 100) +w_mixed = MSstats:::anomaly_weights_z_vec(s_mixed) +expect_true(is.na(w_mixed[3])) +expect_true(all(!is.na(w_mixed[-3]))) diff --git a/inst/tinytest/test_utils_feature_selection.R b/inst/tinytest/test_utils_feature_selection.R new file mode 100644 index 00000000..1b3ebd34 --- /dev/null +++ b/inst/tinytest/test_utils_feature_selection.R @@ -0,0 +1,42 @@ +# Setup -------------------------------------------------------------------- +MSstatsConvert::MSstatsLogsSettings(FALSE) +raw_input = MSstatsPrepareForDataProcess(DDARawData, 2, NULL) +raw_input = MSstatsNormalize(raw_input, "EQUALIZEMEDIANS") +raw_input = MSstatsMergeFractions(raw_input) +raw_input = MSstatsHandleMissing(raw_input, "TMP", TRUE, "NA", 0.999) + +# Test 1: method = "all" keeps all features (no "remove" column added) ------- +input_all = MSstatsSelectFeatures(data.table::copy(raw_input), "all") +expect_true(data.table::is.data.table(input_all)) +expect_equal(nrow(input_all), nrow(raw_input)) + +# Test 2: method = "topN" flags features outside the top N per protein ------ +input_topn = MSstatsSelectFeatures(data.table::copy(raw_input), "topN", top_n = 2) +expect_true("remove" %in% colnames(input_topn)) +kept_per_protein = input_topn[!(remove), + list(n_features = length(unique(FEATURE))), + by = "PROTEIN"] +expect_true(all(kept_per_protein$n_features <= 2)) + +# Test 3: top3 is equivalent to topN with default top_n -------------------- +input_top3 = MSstatsSelectFeatures(data.table::copy(raw_input), "top3") +expect_true("remove" %in% colnames(input_top3)) + +# Test 4: method = "highQuality" flags feature quality and outliers --------- +input_hq = MSstatsSelectFeatures(data.table::copy(raw_input), "highQuality", + min_feature_count = 2) +expect_true(all(c("feature_quality", "is_outlier") %in% colnames(input_hq))) +expect_true(all(input_hq$feature_quality %in% c("Informative", "Noisy", "Uninformative"))) +expect_true(is.logical(input_hq$is_outlier)) +expect_equal(nrow(input_hq), nrow(raw_input)) + +# Test 5: invalid method errors ---------------------------------------------- +expect_error(MSstatsSelectFeatures(data.table::copy(raw_input), "invalid_method")) + +# Test 6: full pipeline still summarizes with highQuality selection ---------- +selected = MSstatsSelectFeatures(data.table::copy(raw_input), "highQuality") +processed = getProcessed(selected) +prepared = MSstatsPrepareForSummarization(selected, "TMP", TRUE, "NA", FALSE) +summarized = MSstatsSummarizeWithSingleCore(prepared, "TMP", TRUE, "NA", FALSE, TRUE, 90) +output = MSstatsSummarizationOutput(prepared, summarized, processed, "TMP", TRUE, "NA") +expect_true(nrow(output$ProteinLevelData) > 0) diff --git a/inst/tinytest/test_utils_plots_common.R b/inst/tinytest/test_utils_plots_common.R new file mode 100644 index 00000000..472f51cc --- /dev/null +++ b/inst/tinytest/test_utils_plots_common.R @@ -0,0 +1,74 @@ +# Test theme_msstats ------------------------------------------------------- + +theme_condition = theme_msstats("CONDITIONPLOT") +expect_true(inherits(theme_condition, "theme")) + +theme_comparison = theme_msstats("COMPARISONPLOT") +expect_true(inherits(theme_comparison, "theme")) + +theme_other = theme_msstats("PROFILEPLOT") +expect_true(inherits(theme_other, "theme")) + +# Test getSelectedProteins -------------------------------------------------- + +all_proteins = c("ProtA", "ProtB", "ProtC") + +# Test 1: character selection, all found +expect_equal(getSelectedProteins(c("ProtA", "ProtC"), all_proteins), + c("ProtA", "ProtC")) + +# Test 2: character selection with a missing protein errors +expect_error(getSelectedProteins(c("ProtA", "NotAProtein"), all_proteins)) + +# Test 3: numeric selection within range +expect_equal(getSelectedProteins(c(1, 3), all_proteins), + c("ProtA", "ProtC")) + +# Test 4: numeric selection out of range errors +expect_error(getSelectedProteins(c(1, 10), all_proteins)) + +# Test savePlot / getFileName ------------------------------------------------ + +tmp_dir = tempfile("msstats_saveplot_") +dir.create(tmp_dir) +old_wd = getwd() +setwd(tmp_dir) + +# Test 5: savePlot with name_base = FALSE is a no-op, returns NULL, no file created +result_noop = savePlot(FALSE, "ProfilePlot", width = 800, height = 600) +expect_true(is.null(result_noop)) +expect_equal(length(list.files(tmp_dir)), 0) + +# Test 6: savePlot with a real name_base creates a pdf file +result_plot = savePlot("", "ProfilePlot", width = 800, height = 600) +dev.off() +expect_true(is.null(result_plot)) +expect_true(file.exists(file.path(tmp_dir, "ProfilePlot.pdf"))) + +# Test 7: calling savePlot again with the same file_name appends a numeric suffix +result_plot2 = savePlot("", "QCPlot", width = 800, height = 600) +dev.off() +result_plot3 = savePlot("", "QCPlot", width = 800, height = 600) +dev.off() +expect_true(file.exists(file.path(tmp_dir, "QCPlot_2.pdf"))) + +setwd(old_wd) +unlink(tmp_dir, recursive = TRUE) + +# Test .saveTable ------------------------------------------------------------ + +tmp_dir2 = tempfile("msstats_savetable_") +dir.create(tmp_dir2) +tbl = data.table::data.table(Protein = c("A", "B"), Value = c(1, 2)) + +# Test 8: .saveTable with name_base = FALSE is a no-op +res_noop_table = MSstats:::.saveTable(tbl, FALSE, "MyTable") +expect_true(is.null(res_noop_table)) +expect_equal(length(list.files(tmp_dir2)), 0) + +# Test 9: .saveTable with a real name_base writes a file +res_table = MSstats:::.saveTable(tbl, tmp_dir2, "MyTable") +expect_true(is.null(res_table)) +expect_true(file.exists(file.path(tmp_dir2, "MyTable.pdf"))) + +unlink(tmp_dir2, recursive = TRUE) From 2b08e5867a0fbaaa7dbdfd6018e6045b19c75bec Mon Sep 17 00:00:00 2001 From: devonjkohler Date: Thu, 16 Jul 2026 11:29:51 -0400 Subject: [PATCH 2/3] bug fixes --- R/SDRFconverter.R | 13 ++++++++----- R/groupComparisonPlots.R | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/R/SDRFconverter.R b/R/SDRFconverter.R index d66967ed..7c1f3aca 100644 --- a/R/SDRFconverter.R +++ b/R/SDRFconverter.R @@ -52,9 +52,12 @@ SDRFtoAnnotation = function( if (length(colnames(data)) < length(extract_cols)){ stop("ERROR: One or more of the column passed in the parameters were not found in the data. Please ensure that the column names are correct.") } - data.table::setnames(data, extract_cols, - c("Run", "Condition", "BioReplicate")) - + new_names = c("Run", "Condition", "BioReplicate") + if (!is.null(fraction)){ + new_names = c(new_names, "Fraction") + } + data.table::setnames(data, extract_cols, new_names) + return(data) } @@ -102,10 +105,10 @@ extractSDRF = function( if (is.null(fraction)){ data$Fraction = NULL - data.table::setnames(data, c("Condition", "BioReplicate", "Run"), + data.table::setnames(data, c("Run", "Condition", "BioReplicate"), c(run_name, condition_name, biological_replicate)) } else { - data.table::setnames(data, extract_cols, + data.table::setnames(data, c("Run", "Condition", "BioReplicate", "Fraction"), c(run_name, condition_name, biological_replicate, fraction)) } diff --git a/R/groupComparisonPlots.R b/R/groupComparisonPlots.R index 43852ba8..e23a1436 100644 --- a/R/groupComparisonPlots.R +++ b/R/groupComparisonPlots.R @@ -249,7 +249,7 @@ groupComparisonPlots = function( - if (address != FALSE) { + if (address != FALSE & !isPlotly) { dev.off() } if(isPlotly) { @@ -372,7 +372,7 @@ groupComparisonPlots = function( if (!isPlotly) print(plot) plots[[i]] = plot } - if (address != FALSE) { + if (address != FALSE & !isPlotly) { dev.off() } if (isPlotly) { @@ -426,7 +426,7 @@ groupComparisonPlots = function( if (!isPlotly) print(plot) plots[[i]] = plot } - if (address != FALSE) { + if (address != FALSE & !isPlotly) { dev.off() } if (isPlotly) { From 2ddc0a4073ff3c279e0097b5b9de8b8ae5af5c83 Mon Sep 17 00:00:00 2001 From: devonjkohler Date: Thu, 16 Jul 2026 13:07:13 -0400 Subject: [PATCH 3/3] more test coverage --- inst/tinytest/test_groupComparison_designs.R | 66 +++++++++++++++ inst/tinytest/test_groupComparison_missing.R | 72 ++++++++++++++++ inst/tinytest/test_summarization_linear.R | 55 ++++++++++++ inst/tinytest/test_utils_checks_extra.R | 83 +++++++++++++++++++ .../test_utils_groupcomparison_checks_extra.R | 40 +++++++++ inst/tinytest/test_utils_output_finalize.R | 38 +++++++++ .../test_utils_summarization_isSummarizable.R | 47 +++++++++++ 7 files changed, 401 insertions(+) create mode 100644 inst/tinytest/test_groupComparison_designs.R create mode 100644 inst/tinytest/test_groupComparison_missing.R create mode 100644 inst/tinytest/test_summarization_linear.R create mode 100644 inst/tinytest/test_utils_checks_extra.R create mode 100644 inst/tinytest/test_utils_groupcomparison_checks_extra.R create mode 100644 inst/tinytest/test_utils_output_finalize.R create mode 100644 inst/tinytest/test_utils_summarization_isSummarizable.R diff --git a/inst/tinytest/test_groupComparison_designs.R b/inst/tinytest/test_groupComparison_designs.R new file mode 100644 index 00000000..59810a1a --- /dev/null +++ b/inst/tinytest/test_groupComparison_designs.R @@ -0,0 +1,66 @@ +# Coverage for the model-fitting branches of .fitModelForGroupComparison in +# utils_groupcomparison.R. The existing test_groupComparison.R only exercises +# the case-control / no-tech-replicate path (a plain lm). Here we drive the +# mixed-model branches: repeated measures, technical replicates, and both. + +set.seed(101) + +build = function(proteins, group_vec, subject_vec) { + data.table::rbindlist(lapply(proteins, function(p) { + n = length(group_vec) + data.table::data.table( + RUN = paste0(p, "_", seq_len(n)), + Protein = p, + LogIntensities = 10 + rnorm(n, 0, 0.3) + (group_vec == "B") * 0.6, + originalRUN = paste0(p, "_Run", seq_len(n)), + GROUP = group_vec, + SUBJECT = subject_vec, + TotalGroupMeasurements = n / 2, NumMeasuredFeature = 2, + MissingPercentage = 0, more50missing = FALSE, NumImputedFeature = 0 + ) + })) +} + +run_gc = function(pld) { + pld[, GROUP := factor(GROUP, levels = c("A", "B"))] + data_in = list(ProteinLevelData = pld, + FeatureLevelData = data.table::data.table(Label = "L")) + cm = matrix(c(1, -1), nrow = 1) + rownames(cm) = "A-B"; colnames(cm) = c("A", "B") + suppressWarnings(groupComparison(cm, data_in, use_log_file = FALSE)) +} + +# Test 1: repeated measures, no technical replicates (subjects across groups) +# -> lmer(ABUNDANCE ~ GROUP + (1|SUBJECT)) +pld_rep = build(c("P1", "P2"), + group_vec = rep(c("A", "B"), each = 3), + subject_vec = rep(c("s1", "s2", "s3"), 2)) +expect_true(checkRepeatedDesign(list(ProteinLevelData = pld_rep))) +res_rep = run_gc(pld_rep) +expect_equal(nrow(res_rep$ComparisonResult), 2) +expect_true(all(is.finite(res_rep$ComparisonResult$log2FC))) +# a mixed model was fitted (lmerMod), not a plain lm +expect_true(inherits(res_rep$FittedModel[[1]], "lmerMod")) + +# Test 2: case-control WITH technical replicates (subject repeated within group) +# -> lmer(ABUNDANCE ~ GROUP + (1|SUBJECT)) +pld_tech = build(c("P1", "P2"), + group_vec = rep(c("A", "B"), each = 4), + subject_vec = c("s1", "s1", "s2", "s2", "s3", "s3", "s4", "s4")) +expect_false(checkRepeatedDesign(list(ProteinLevelData = pld_tech))) +expect_true(MSstats:::.checkTechReplicate(pld_tech)) +res_tech = run_gc(pld_tech) +expect_equal(nrow(res_tech$ComparisonResult), 2) +expect_true(inherits(res_tech$FittedModel[[1]], "lmerMod")) + +# Test 3: repeated measures WITH technical replicates +# -> lmer(ABUNDANCE ~ GROUP + (1|SUBJECT) + (1|GROUP:SUBJECT)) +pld_both = build(c("P1", "P2"), + group_vec = rep(c("A", "B"), each = 6), + subject_vec = rep(c("s1", "s1", "s2", "s2", "s3", "s3"), 2)) +expect_true(checkRepeatedDesign(list(ProteinLevelData = pld_both))) +expect_true(MSstats:::.checkTechReplicate(pld_both)) +res_both = run_gc(pld_both) +expect_equal(nrow(res_both$ComparisonResult), 2) +expect_true(all(is.finite(res_both$ComparisonResult$log2FC))) +expect_true(inherits(res_both$FittedModel[[1]], "lmerMod")) diff --git a/inst/tinytest/test_groupComparison_missing.R b/inst/tinytest/test_groupComparison_missing.R new file mode 100644 index 00000000..20b49552 --- /dev/null +++ b/inst/tinytest/test_groupComparison_missing.R @@ -0,0 +1,72 @@ +# Group comparison edge cases: proteins measured in only some conditions. +# These exercise .getEmptyComparison (protein present in a single group) and +# .handleEmptyConditions (a contrast references a condition with no +# measurements for that protein) in utils_groupcomparison.R. + +# Build synthetic summarized (ProteinLevelData) with three conditions A/B/C. +# SUBJECT ids are scoped to their group so the design is case-control +# (checkRepeatedDesign == FALSE). +mk = function(prot, groups_vec) { + n = length(groups_vec) + subj = paste0(groups_vec, ave(seq_along(groups_vec), groups_vec, FUN = seq_along)) + data.table::data.table( + RUN = paste0(prot, "_", seq_len(n)), + Protein = prot, + LogIntensities = 10 + seq_len(n) / 10, + originalRUN = paste0(prot, "_Run", seq_len(n)), + GROUP = groups_vec, + SUBJECT = subj, + TotalGroupMeasurements = 1, NumMeasuredFeature = 2, + MissingPercentage = 0, more50missing = FALSE, NumImputedFeature = 0 + ) +} +pld = data.table::rbindlist(list( + mk("Pall", rep(c("A", "B", "C"), each = 3)), # all 3 conditions + mk("PsingleGroup", rep("A", 6)), # only condition A + mk("PmissingC", rep(c("A", "B"), each = 3)) # A and B, C missing +)) +pld[, GROUP := factor(GROUP, levels = c("A", "B", "C"))] +data_in = list(ProteinLevelData = pld, + FeatureLevelData = data.table::data.table(Label = "L")) + +comparison = matrix(c(1, -1, 0, 1, 0, -1), nrow = 2, byrow = TRUE) +rownames(comparison) = c("A-B", "A-C") +colnames(comparison) = c("A", "B", "C") + +res = suppressWarnings(groupComparison(comparison, data_in, use_log_file = FALSE)) +cr = res$ComparisonResult + +# Test 1: all three proteins produce results for both contrasts +expect_equal(nrow(cr), 6) +expect_true(all(c("Pall", "PsingleGroup", "PmissingC") %in% cr$Protein)) + +# Test 2: fully-measured protein has finite, non-issue log-fold changes +pall = cr[cr$Protein == "Pall", ] +expect_true(all(is.finite(pall$log2FC))) +expect_true(all(is.na(pall$issue))) + +# Test 3: single-group protein (.getEmptyComparison) flags oneConditionMissing +# with an infinite fold change for both contrasts +psingle = cr[cr$Protein == "PsingleGroup", ] +expect_true(all(psingle$issue == "oneConditionMissing")) +expect_true(all(is.infinite(psingle$log2FC))) + +# Test 4: protein missing condition C (.handleEmptyConditions) - the A-B +# contrast is estimable, the A-C contrast is flagged +pmissing = cr[cr$Protein == "PmissingC", ] +ab = pmissing[pmissing$Label == "A-B", ] +ac = pmissing[pmissing$Label == "A-C", ] +expect_true(is.finite(ab$log2FC)) +expect_true(is.na(ab$issue)) +expect_equal(ac$issue, "oneConditionMissing") +expect_true(is.infinite(ac$log2FC)) + +# Test 5: .getEmptyComparison completeMissing branch - a contrast where BOTH +# referenced conditions are absent for the single-group protein +comparison_bc = matrix(c(0, 1, -1), nrow = 1) +rownames(comparison_bc) = "B-C" +colnames(comparison_bc) = c("A", "B", "C") +res_bc = suppressWarnings(groupComparison(comparison_bc, data_in, use_log_file = FALSE)) +psingle_bc = res_bc$ComparisonResult[res_bc$ComparisonResult$Protein == "PsingleGroup", ] +expect_equal(psingle_bc$issue, "completeMissing") +expect_true(is.na(psingle_bc$log2FC)) diff --git a/inst/tinytest/test_summarization_linear.R b/inst/tinytest/test_summarization_linear.R new file mode 100644 index 00000000..2a96d9b6 --- /dev/null +++ b/inst/tinytest/test_summarization_linear.R @@ -0,0 +1,55 @@ +# Coverage for the linear summarization path in utils_summarization.R: +# .fitLinearModel (both single- and multi-feature) and .updateUnequalVariances +# (equalFeatureVar = FALSE). The default dataProcess uses TMP, so none of the +# linear-model code was previously exercised by the test suite. + +# Test 1: linear summarization with equal feature variance ------------------ +q_equal = dataProcess(DDARawData, summaryMethod = "linear", + equalFeatureVar = TRUE, use_log_file = FALSE) +expect_true(is.list(q_equal)) +expect_true(nrow(q_equal$ProteinLevelData) > 0) +expect_true("LogIntensities" %in% colnames(q_equal$ProteinLevelData)) + +# Test 2: linear summarization with unequal feature variance ---------------- +# (this routes through .updateUnequalVariances / loess reweighting) +q_unequal = dataProcess(DDARawData, summaryMethod = "linear", + equalFeatureVar = FALSE, use_log_file = FALSE) +expect_true(nrow(q_unequal$ProteinLevelData) > 0) +expect_true(all(is.finite(q_unequal$ProteinLevelData$LogIntensities))) + +# Test 3: both variance options summarize the same set of proteins ---------- +expect_equal(sort(unique(as.character(q_equal$ProteinLevelData$Protein))), + sort(unique(as.character(q_unequal$ProteinLevelData$Protein)))) + +# Test 4: .fitLinearModel directly, single feature vs multi-feature --------- +# Build a minimal single-protein feature-level table with the columns the +# linear model fitter expects. +make_feature_data = function(n_features) { + runs = paste0("R", 1:4) + features = paste0("F", seq_len(n_features)) + grid = expand.grid(RUN = runs, FEATURE = features, + stringsAsFactors = FALSE) + data.table::data.table( + FEATURE = factor(grid$FEATURE), + RUN = factor(grid$RUN), + newABUNDANCE = 20 + rnorm(nrow(grid)), + weights = NA_real_ + ) +} + +set.seed(1) +single = make_feature_data(1) +fit_single = MSstats:::.fitLinearModel(single, is_single_feature = TRUE, + is_labeled = FALSE, equal_variances = TRUE) +expect_true(inherits(fit_single, "lm")) + +multi = make_feature_data(3) +fit_multi = MSstats:::.fitLinearModel(multi, is_single_feature = FALSE, + is_labeled = FALSE, equal_variances = TRUE) +expect_true(inherits(fit_multi, "lm")) + +# Test 5: .fitLinearModel with equal_variances = FALSE reweights via +# .updateUnequalVariances and still returns a fitted lm +fit_unequal = MSstats:::.fitLinearModel(multi, is_single_feature = FALSE, + is_labeled = FALSE, equal_variances = FALSE) +expect_true(inherits(fit_unequal, "lm")) diff --git a/inst/tinytest/test_utils_checks_extra.R b/inst/tinytest/test_utils_checks_extra.R new file mode 100644 index 00000000..9100127a --- /dev/null +++ b/inst/tinytest/test_utils_checks_extra.R @@ -0,0 +1,83 @@ +# Additional coverage for utils_checks.R: validateAnnotation, +# makePeptidesDictionary, .checkExperimentDesign, .preProcessIntensities. + +# validateAnnotation --------------------------------------------------------- + +# Test 1: valid group-comparison annotation (each BioReplicate in one Condition) +gc_annot = data.table::data.table( + BioReplicate = c("s1", "s2", "s3", "s4"), + Condition = c("H", "H", "D", "D") +) +expect_true(validateAnnotation(gc_annot, design_type = "group comparison")) + +# Test 2: only one condition -> error (relative quantification needs >1) +one_cond = data.table::data.table( + BioReplicate = c("s1", "s2"), + Condition = c("H", "H") +) +expect_error(validateAnnotation(one_cond)) + +# Test 3: group comparison with a BioReplicate spanning conditions -> error +gc_bad = data.table::data.table( + BioReplicate = c("s1", "s1", "s2"), + Condition = c("H", "D", "D") +) +expect_error(validateAnnotation(gc_bad, design_type = "group comparison")) + +# Test 4: valid repeated-measures annotation (BioReplicate across conditions) +rm_annot = data.table::data.table( + BioReplicate = c("s1", "s1", "s2", "s2"), + Condition = c("H", "D", "H", "D") +) +expect_true(validateAnnotation(rm_annot, design_type = "repeated measures")) + +# Test 5: repeated measures but each BioReplicate has a single condition -> error +rm_bad = data.table::data.table( + BioReplicate = c("s1", "s2", "s3", "s4"), + Condition = c("H", "H", "D", "D") +) +expect_error(validateAnnotation(rm_bad, design_type = "repeated measures")) + +# Test 6: unrecognized design type -> error +expect_error(validateAnnotation(gc_annot, design_type = "nonsense")) + +# makePeptidesDictionary ----------------------------------------------------- + +dda = data.table::as.data.table(DDARawData) + +# Test 7: GLOBALSTANDARDS builds a dictionary with a PEPTIDE key column +peptides_dict = makePeptidesDictionary(dda, "GLOBALSTANDARDS") +expect_true(data.table::is.data.table(peptides_dict)) +expect_true("PEPTIDE" %in% colnames(peptides_dict)) +expect_true(all(grepl("_", peptides_dict$PEPTIDE))) + +# Test 8: any other normalization returns NULL +expect_true(is.null(makePeptidesDictionary(dda, "EQUALIZEMEDIANS"))) + +# .checkExperimentDesign ----------------------------------------------------- + +design_ok = data.table::data.table(Condition = c("A", "B")) +# Test 9: complete column passes silently (returns NULL invisibly, no error) +expect_silent(MSstats:::.checkExperimentDesign(design_ok, "Condition")) + +# Test 10: NA in the checked column errors +design_bad = data.table::data.table(Condition = c("A", NA)) +expect_error(MSstats:::.checkExperimentDesign(design_bad, "Condition")) + +# .preProcessIntensities ----------------------------------------------------- +MSstatsConvert::MSstatsLogsSettings(FALSE) + +# Test 11: intensities below 1 are floored to 1 before log transform, and an +# ABUNDANCE column is created +intens = data.table::data.table(INTENSITY = c(0.5, 100, 1000, NA)) +MSstats:::.preProcessIntensities(intens, log_base = 2) +expect_true("ABUNDANCE" %in% colnames(intens)) +expect_equal(intens$INTENSITY[1], 1) # 0.5 floored to 1 +expect_equal(intens$ABUNDANCE[1], 0) # log2(1) == 0 +expect_equal(intens$ABUNDANCE[2], log(100, 2)) +expect_true(is.na(intens$ABUNDANCE[4])) # NA stays NA + +# Test 12: with no sub-1 intensities the flooring branch is skipped +intens2 = data.table::data.table(INTENSITY = c(10, 100, 1000)) +MSstats:::.preProcessIntensities(intens2, log_base = 10) +expect_equal(intens2$ABUNDANCE, log(c(10, 100, 1000), 10)) diff --git a/inst/tinytest/test_utils_groupcomparison_checks_extra.R b/inst/tinytest/test_utils_groupcomparison_checks_extra.R new file mode 100644 index 00000000..6f7730d6 --- /dev/null +++ b/inst/tinytest/test_utils_groupcomparison_checks_extra.R @@ -0,0 +1,40 @@ +# Additional coverage for utils_groupcomparison_checks.R: +# .checkContrastMatrix and .checkGroupComparisonInput. + +MSstatsConvert::MSstatsLogsSettings(FALSE) + +# .checkGroupComparisonInput ------------------------------------------------- + +# Test 1: a data.table with all required columns passes through unchanged +good_input = data.table::data.table( + RUN = 1, Protein = "P1", LogIntensities = 10, originalRUN = "r1", + GROUP = "A", SUBJECT = 1, more50missing = FALSE, NumMeasuredFeature = 2 +) +res = MSstats:::.checkGroupComparisonInput(good_input) +expect_true(data.table::is.data.table(res)) +expect_equal(nrow(res), 1) + +# Test 2: missing a required column (not processed by dataProcess) -> error +bad_input = data.table::data.table( + RUN = 1, Protein = "P1", LogIntensities = 10 +) +expect_error(MSstats:::.checkGroupComparisonInput(bad_input)) + +# .checkContrastMatrix ------------------------------------------------------- + +summarized = data.table::data.table( + GROUP = factor(c("A", "B", "C"), levels = c("A", "B", "C")) +) + +# Test 3: contrast matrix with a column per group passes +cm_ok = matrix(c(1, -1, 0), nrow = 1) +rownames(cm_ok) = "A-B" +colnames(cm_ok) = c("A", "B", "C") +res_cm = MSstats:::.checkContrastMatrix(cm_ok, summarized) +expect_equal(ncol(res_cm), 3) + +# Test 4: wrong number of columns (fewer than number of groups) -> error +cm_wrong = matrix(c(1, -1), nrow = 1) +rownames(cm_wrong) = "A-B" +colnames(cm_wrong) = c("A", "B") +expect_error(MSstats:::.checkContrastMatrix(cm_wrong, summarized)) diff --git a/inst/tinytest/test_utils_output_finalize.R b/inst/tinytest/test_utils_output_finalize.R new file mode 100644 index 00000000..2c0506ff --- /dev/null +++ b/inst/tinytest/test_utils_output_finalize.R @@ -0,0 +1,38 @@ +# Coverage for .finalizeLinear in utils_output.R. This helper computes +# feature-level summary statistics for linear-model summarization; it is +# reachable only by direct call, so it is exercised here explicitly. +MSstatsConvert::MSstatsLogsSettings(FALSE) + +make_input = function() { + data.table::data.table( + PROTEIN = factor("P1"), + RUN = factor(rep(c("r1", "r2"), each = 2)), + FEATURE = factor(rep(c("f1", "f2"), 2)), + LABEL = "L", + newABUNDANCE = c(10, 11, 10.5, NA), + INTENSITY = c(1024, 2048, 1500, NA), + censored = c(FALSE, FALSE, FALSE, TRUE), + n_obs = c(2, 2, 2, 2), + n_obs_run = c(2, 2, 2, 1), + total_features = 2 + ) +} + +# Test 1: with a censored symbol, summary columns are added and NumImputedFeature +# is always 0 for the linear method +res = MSstats:::.finalizeLinear(make_input(), "NA") +expect_true(all(c("NumMeasuredFeature", "MissingPercentage", "more50missing", + "nonmissing_orig", "NumImputedFeature") %in% colnames(res))) +expect_true(all(res$NumImputedFeature == 0)) + +# Test 2: the run with a censored/missing feature is flagged as >50% missing +run2 = res[RUN == "r2", ] +expect_true(all(run2$more50missing)) +run1 = res[RUN == "r1", ] +expect_false(any(run1$more50missing)) + +# Test 3: censored_symbol = NULL takes the INTENSITY-based branch and still +# produces the measured-feature statistics +res_null = MSstats:::.finalizeLinear(make_input(), NULL) +expect_true("NumMeasuredFeature" %in% colnames(res_null)) +expect_equal(res_null[RUN == "r1", ]$NumMeasuredFeature, c(2, 2)) diff --git a/inst/tinytest/test_utils_summarization_isSummarizable.R b/inst/tinytest/test_utils_summarization_isSummarizable.R new file mode 100644 index 00000000..22471e47 --- /dev/null +++ b/inst/tinytest/test_utils_summarization_isSummarizable.R @@ -0,0 +1,47 @@ +# Coverage for .isSummarizable in utils_summarization.R: the guard that decides +# whether a protein can be summarized with TMP. Each early-return branch +# corresponds to a different degenerate protein, exercised here directly. +MSstatsConvert::MSstatsLogsSettings(FALSE) + +base = data.table::data.table( + PROTEIN = "P1", RUN = c("r1", "r2"), + newABUNDANCE = c(10, 11), + n_obs = c(2, 2), n_obs_run = c(2, 2), + prop_features = c(1, 1) +) + +# Test 1: a well-behaved protein is returned unchanged +res_ok = MSstats:::.isSummarizable(data.table::copy(base), TRUE) +expect_true(!is.null(res_ok)) +expect_equal(nrow(res_ok), 2) + +# Test 2: all measurements missing/zero -> NULL +b_na = data.table::copy(base); b_na$newABUNDANCE = NA_real_ +expect_true(is.null(MSstats:::.isSummarizable(b_na, TRUE))) + +# Test 3: all n_obs are zero -> NULL +b_nobs0 = data.table::copy(base); b_nobs0$n_obs = 0 +expect_true(is.null(MSstats:::.isSummarizable(b_nobs0, TRUE))) + +# Test 4: every feature has only a single measurement across runs -> NULL +b_nobs1 = data.table::copy(base); b_nobs1$n_obs = 1 +expect_true(is.null(MSstats:::.isSummarizable(b_nobs1, TRUE))) + +# Test 5: remove50missing = TRUE and all runs >50% missing -> NULL +b_50 = data.table::copy(base); b_50$prop_features = c(0.2, 0.3) +expect_true(is.null(MSstats:::.isSummarizable(b_50, TRUE))) + +# Test 6: same data with remove50missing = FALSE is summarizable (branch skipped) +b_50b = data.table::copy(base); b_50b$prop_features = c(0.2, 0.3) +expect_true(!is.null(MSstats:::.isSummarizable(b_50b, FALSE))) + +# Test 7: runs with no observations are filtered out, others retained +b_missing_run = data.table::data.table( + PROTEIN = "P1", RUN = c("r1", "r2", "r3"), + newABUNDANCE = c(10, 11, 12), + n_obs = c(2, 2, 2), n_obs_run = c(2, 2, 0), + prop_features = c(1, 1, 1) +) +res_filtered = MSstats:::.isSummarizable(b_missing_run, TRUE) +expect_equal(nrow(res_filtered), 2) +expect_false("r3" %in% res_filtered$RUN)