diff deseq2.xml @ 32:2275d0688bca draft default tip

planemo upload for repository https://github.com/galaxyproject/tools-iuc/tree/main/tools/deseq2 commit 2bcb448af8311320be0a970be37aa8ea4eed66b9
author iuc
date Fri, 19 Dec 2025 08:09:52 +0000
parents 9a882d108833
children
line wrap: on
line diff
--- a/deseq2.xml	Tue Jul 18 14:58:52 2023 +0000
+++ b/deseq2.xml	Fri Dec 19 08:09:52 2025 +0000
@@ -7,6 +7,7 @@
     <expand macro='xrefs'/>
     <expand macro='requirements'/>
     <stdio>
+        <exit_code range="1:" level="fatal" />
         <regex match="Execution halted"
            source="both"
            level="fatal"
@@ -37,6 +38,7 @@
 
 #import json
 #import os
+#import base64
 Rscript '${__tool_directory__}/deseq2.R'
     --cores \${GALAXY_SLOTS:-1}
     -o '$deseq_out'
@@ -58,33 +60,59 @@
     #end if
     #set $filename_to_element_identifiers = {}
     #set $temp_factor_names = list()
-    #for $factor in $select_data.rep_factorName:
-        #set $temp_factor = list()
-        #for $level in $factor.rep_factorLevel:
-            #set $count_files = list()
-            #if $select_data.how == 'group_tags':
-                #for $group in $level.groups.value:
-                    #for $file in $select_data.countsFile.get_datasets_for_group($group):
-                        $count_files.append(str($file))
-                        $filename_to_element_identifiers.__setitem__(os.path.basename(str($file)),  $file.element_identifier)
+    #if $select_data.how == 'sample_sheet_contrasts':
+        ## Pass collection file paths and element identifiers for sample sheet mode
+        #set $count_files = list()
+        #for $file in $select_data.countsFile:
+            $count_files.append(str($file))
+            $filename_to_element_identifiers.__setitem__(os.path.basename(str($file)), $file.element_identifier)
+        #end for
+        --sample_sheet_mode
+        --sample_sheet '$select_data.sample_sheet'
+        --collection_files '#echo ",".join($count_files)#'
+        #if $select_data.design_formula_mode.mode == 'custom':
+            --custom_design_formula
+            --design_formula '$select_data.design_formula_mode.design_formula'
+        #else:
+            --factor_columns '$select_data.design_formula_mode.factor'
+        #end if
+        #if str($select_data.design_formula_mode.reference_level).strip():
+            --reference_level '${select_data.design_formula_mode.reference_level.strip()}'
+        #end if
+        #if str($select_data.design_formula_mode.target_level).strip():
+            --target_level '${select_data.design_formula_mode.target_level.strip()}'
+        #end if
+    #else:
+        #for $factor in $select_data.rep_factorName:
+            #set $temp_factor = list()
+            #for $level in $factor.rep_factorLevel:
+                #set $count_files = list()
+                #if $select_data.how == 'group_tags':
+                    #for $group in $level.groups.value:
+                        #for $file in $select_data.countsFile.get_datasets_for_group($group):
+                            $count_files.append(str($file))
+                            $filename_to_element_identifiers.__setitem__(os.path.basename(str($file)), $file.element_identifier)
+                        #end for
                     #end for
-                #end for
-            #else:
-                #for $file in $level.countsFile:
-                    $count_files.append(str($file))
-                    $filename_to_element_identifiers.__setitem__(os.path.basename(str($file)),  $file.element_identifier)
-                #end for
-            #end if
-            $temp_factor.append( {str($level.factorLevel): $count_files} )
+                #else:
+                    #for $file in $level.countsFile:
+                        $count_files.append(str($file))
+                        $filename_to_element_identifiers.__setitem__(os.path.basename(str($file)), $file.element_identifier)
+                    #end for
+                #end if
+                $temp_factor.append( {str($level.factorLevel): $count_files} )
+            #end for
+            $temp_factor.reverse()
+            $temp_factor_names.append([str($factor.factorName), $temp_factor])
         #end for
-        $temp_factor.reverse()
-        $temp_factor_names.append([str($factor.factorName), $temp_factor])
-    #end for
+    #end if
 
     $header
 
-    -f '#echo json.dumps(temp_factor_names)#'
-    -l '#echo json.dumps(filename_to_element_identifiers)#'
+    #if $select_data.how != 'sample_sheet_contrasts':
+        -f '#echo base64.b64encode(json.dumps(temp_factor_names).encode()).decode()#'
+    #end if
+    -l '#echo base64.b64encode(json.dumps(filename_to_element_identifiers).encode()).decode()#'
     #if $advanced_options.esf_cond.esf:
         #if $advanced_options.esf_cond.esf == "user":
             -e $advanced_options.esf_cond.size_factor_input
@@ -100,7 +128,7 @@
         $advanced_options.prefilter_conditional.prefilter
         -V $advanced_options.prefilter_conditional.prefilter_value
     #end if
-    
+
     $advanced_options.outlier_replace_off
     $advanced_options.outlier_filter_off
     $advanced_options.auto_mean_filter_off
@@ -124,9 +152,10 @@
             <param name="how" type="select">
                 <option value="datasets_per_level">Select datasets per level</option>
                 <option value="group_tags">Select group tags corresponding to levels</option>
+                <option value="sample_sheet_contrasts">Select contrasts from sample sheet columns</option>
             </param>
             <when value="group_tags">
-                <param name="countsFile" type="data_collection" format="tabular" label="Count file(s) collection" multiple="true"/>
+                <param name="countsFile" type="data_collection" format="tabular" label="Count file(s) collection" />
                 <expand macro="factor_repeat">
                     <param name="groups" type="group_tag" data_ref="countsFile" multiple="true" label="Select groups that correspond to this factor level"/>
                 </expand>
@@ -136,6 +165,37 @@
                     <param name="countsFile" type="data" format="tabular" multiple="true" label="Counts file(s)"/>
                 </expand>
             </when>
+            <when value="sample_sheet_contrasts">
+                <param name="countsFile" type="data_collection" collection_type="list" format="tabular,tsv" label="Count file(s) collection" />
+                <param name="sample_sheet" type="data" format="tabular" label="Sample sheet file" help="A tabular file containing at least two columns: one with the element identifiers (matching those in the collection) and one or more columns with factor levels."/>
+                <conditional name="design_formula_mode">
+                    <param name="mode" type="select" label="Design formula specification">
+                        <option value="automatic" selected="true">Automatic (select factors from columns)</option>
+                        <option value="custom">Custom design formula</option>
+                    </param>
+                    <when value="automatic">
+                        <param name="factor" type="data_column" use_header_names="true" data_ref="sample_sheet" multiple="true" optional="false" label="Factor levels column(s)" help="Select one or more columns from the sample sheet to define factor levels for DESeq2 analysis. First selected factor is the primary factor"/>
+                        <expand macro="reference_target_levels"/>
+                     </when>
+                    <when value="custom">
+                        <param name="design_formula" type="text" label="Design formula" help="Specify the design formula using R formula syntax (e.g., '~ condition' or '~ batch + condition'). Column names must match those in the sample sheet. The last factor in the formula is used as the primary factor for contrasts.">
+                            <validator type="regex" message="Design formula must start with ~ and contain valid R variable names">^~\s*[a-zA-Z][a-zA-Z0-9_.]*(\s*[+*:]\s*[a-zA-Z][a-zA-Z0-9_.]*)*$</validator>
+                            <sanitizer>
+                                <valid initial="string.letters,string.digits">
+                                    <add value="~"/>
+                                    <add value=":"/>
+                                    <add value="+"/>
+                                    <add value="*"/>
+                                    <add value="_"/>
+                                    <add value="."/>
+                                    <add value=" "/>
+                                </valid>
+                            </sanitizer>
+                        </param>
+                        <expand macro="reference_target_levels"/>
+                    </when>
+                </conditional>
+            </when>
         </conditional>
 
         <param name="batch_factors" type="data" format="tabular" optional="true" label="(Optional) provide a tabular file with additional batch factors to include in the model." help="You can produce this file using RUVSeq or svaseq."/>
@@ -209,9 +269,9 @@
                 label="Turn off independent filtering"
                 help=" DESeq2 performs independent filtering by default using the mean of normalized counts as a filter statistic"/>
             <conditional name="prefilter_conditional">
-                <param name="prefilter" type="select" label="Perform pre-filtering" help="While it is not necessary to pre-filter 
-                    low count genes before running the DESeq2 functions, there are two reasons which make pre-filtering useful: 
-                    by removing rows in which there are very few reads, we reduce the required memory, and we increase the speed. 
+                <param name="prefilter" type="select" label="Perform pre-filtering" help="While it is not necessary to pre-filter
+                    low count genes before running the DESeq2 functions, there are two reasons which make pre-filtering useful:
+                    by removing rows in which there are very few reads, we reduce the required memory, and we increase the speed.
                     It can also improve visualizations, as features with no information for differential expression are not plotted.">
                     <option value="-P">Enabled</option>
                     <option value="" selected="true">Disabled</option>
@@ -265,7 +325,7 @@
         </data>
     </outputs>
     <tests>
-        <!--Ensure counts files with header works -->
+        <!-- Ensure counts files with header works -->
         <test expect_num_outputs="4">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -309,7 +369,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure additional batch factor correction works -->
+        <!-- Ensure additional batch factor correction works -->
         <test expect_num_outputs="2">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -361,7 +421,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure counts files without header works -->
+        <!-- Ensure counts files without header works -->
         <test expect_num_outputs="4">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -380,7 +440,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value="normCounts,normRLog,normVST"/>
-            </section>        
+            </section>
             <output name="counts_out">
                 <assert_contents>
                     <has_text_matching expression="GSM461176_untreat_single.counts.noheader\tGSM461177_untreat_paired.counts.noheader\tGSM461178_untreat_paired.counts.noheader\tGSM461182_untreat_single.counts.noheader\tGSM461179_treat_single.counts.noheader\tGSM461180_treat_paired.counts.noheader\tGSM461181_treat_paired.counts.noheader"/>
@@ -405,7 +465,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure Sailfish/Salmon input with tx2gene table works-->
+        <!-- Ensure Sailfish/Salmon input with tx2gene table works -->
         <test expect_num_outputs="1">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -423,7 +483,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value=""/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="tabular"/>
@@ -434,7 +494,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure Sailfish/Salmon input with GFF3 annotation from NCBI works-->
+        <!-- Ensure Sailfish/Salmon input with GFF3 annotation from NCBI works -->
         <test expect_num_outputs="1">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -452,7 +512,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value=""/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="gtf"/>
@@ -463,7 +523,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure Sailfish/Salmon input with GTF annotation from Ensembl works-->
+        <!-- Ensure Sailfish/Salmon input with GTF annotation from Ensembl works -->
         <test expect_num_outputs="1">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -481,7 +541,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value=""/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="gtf"/>
@@ -492,7 +552,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure group tags can be used to select factor levels -->
+        <!-- Ensure group tags can be used to select factor levels -->
         <test expect_num_outputs="1">
             <param name="select_data|how" value="group_tags"/>
             <param name="select_data|countsFile">
@@ -521,7 +581,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value=""/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="tabular"/>
@@ -532,7 +592,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Ensure many_contrasts produces output collection -->
+        <!-- Ensure many_contrasts produces output collection -->
         <test expect_num_outputs="1">
             <param name="select_data|how" value="group_tags"/>
             <param name="select_data|countsFile">
@@ -561,7 +621,7 @@
             </section>
             <section name="output_options">
                 <param name="output_selector" value="many_contrasts"/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="tabular"/>
@@ -574,7 +634,7 @@
                 </element>
             </output_collection>
         </test>
-        <!--Test alpha_ma option-->
+        <!-- Test alpha_ma option -->
         <test expect_num_outputs="1">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -593,7 +653,7 @@
             <section name="output_options">
                 <param name="output_selector" value=""/>
                 <param name="alpha_ma" value="0.05"/>
-            </section>            
+            </section>
             <param name="tximport_selector" value="tximport"/>
             <param name="txtype" value="sailfish"/>
             <param name="mapping_format_selector" value="gtf"/>
@@ -632,7 +692,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Test alpha_ma option, but with user-provided size factors -->
+        <!-- Test alpha_ma option, but with user-provided size factors -->
         <test expect_num_outputs="1">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -666,7 +726,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!-- Same as above alpha_ma size factor test, but with a non-default estimator-->
+        <!-- Same as above alpha_ma size factor test, but with a non-default estimator -->
         <test expect_num_outputs="2">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -697,7 +757,7 @@
                 </assert_contents>
             </output>
         </test>
-        <!--Test prefilter parameter -->
+        <!-- Test prefilter parameter -->
         <test expect_num_outputs="2">
             <repeat name="rep_factorName">
                 <param name="factorName" value="Treatment"/>
@@ -730,6 +790,199 @@
                 </assert_contents>
             </output>
         </test>
+
+        <!--Test sample_sheet_contrasts mode -->
+        <test expect_num_outputs="2">
+            <param name="select_data|how" value="sample_sheet_contrasts"/>
+            <param name="select_data|countsFile">
+                <collection type="list">
+                    <element name="GSM461179_treat_single.counts" value="GSM461179_treat_single.counts"/>
+                    <element name="GSM461180_treat_paired.counts" value="GSM461180_treat_paired.counts"/>
+                    <element name="GSM461181_treat_paired.counts" value="GSM461181_treat_paired.counts"/>
+                    <element name="GSM461176_untreat_single.counts" value="GSM461176_untreat_single.counts"/>
+                    <element name="GSM461177_untreat_paired.counts" value="GSM461177_untreat_paired.counts"/>
+                    <element name="GSM461178_untreat_paired.counts" value="GSM461178_untreat_paired.counts"/>
+                    <element name="GSM461182_untreat_single.counts" value="GSM461182_untreat_single.counts"/>
+                </collection>
+            </param>
+            <param name="select_data|sample_sheet" value="sample_sheet.tab"/>
+            <param name="select_data|design_formula_mode|mode" value="automatic"/>
+            <param name="select_data|design_formula_mode|factor" value="2"/>
+            <section name="advanced_options">
+                <param name="use_beta_priors" value="1"/>
+            </section>
+            <section name="output_options">
+                <param name="output_selector" value="normCounts"/>
+            </section>
+            <output name="counts_out">
+                <assert_contents>
+                    <has_text_matching expression="GSM461179_treat_single.counts\tGSM461180_treat_paired.counts\tGSM461181_treat_paired.counts\tGSM461176_untreat_single.counts\tGSM461177_untreat_paired.counts\tGSM461178_untreat_paired.counts\tGSM461182_untreat_single.counts"/>
+                    <has_text_matching expression="FBgn0000003\t0\t0\t0\t0\t0\t0\t0"/>
+                </assert_contents>
+            </output>
+            <output name="deseq_out">
+                <assert_contents>
+                    <!-- Contrast direction: first level (Treated) is reference, comparing Untreated vs Treated -->
+                    <has_text_matching expression="FBgn0003360\t1933\.9504.*\t2\.8399.*\t0\.1309.*\t21\.68.*\t.*e-104\t.*e-101"/>
+                    <has_n_lines n="3999"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!--Test sample_sheet_contrasts mode with explicit reference_level -->
+        <test expect_num_outputs="2">
+            <param name="select_data|how" value="sample_sheet_contrasts"/>
+            <param name="select_data|countsFile">
+                <collection type="list">
+                    <element name="GSM461179_treat_single.counts" value="GSM461179_treat_single.counts"/>
+                    <element name="GSM461180_treat_paired.counts" value="GSM461180_treat_paired.counts"/>
+                    <element name="GSM461181_treat_paired.counts" value="GSM461181_treat_paired.counts"/>
+                    <element name="GSM461176_untreat_single.counts" value="GSM461176_untreat_single.counts"/>
+                    <element name="GSM461177_untreat_paired.counts" value="GSM461177_untreat_paired.counts"/>
+                    <element name="GSM461178_untreat_paired.counts" value="GSM461178_untreat_paired.counts"/>
+                    <element name="GSM461182_untreat_single.counts" value="GSM461182_untreat_single.counts"/>
+                </collection>
+            </param>
+            <param name="select_data|sample_sheet" value="sample_sheet.tab"/>
+            <param name="select_data|design_formula_mode|mode" value="automatic"/>
+            <param name="select_data|design_formula_mode|factor" value="2"/>
+            <param name="select_data|design_formula_mode|reference_level" value="Untreated"/>
+            <section name="advanced_options">
+                <param name="use_beta_priors" value="1"/>
+            </section>
+            <section name="output_options">
+                <param name="output_selector" value="normCounts"/>
+            </section>
+            <output name="counts_out">
+                <assert_contents>
+                    <has_text_matching expression="GSM461176_untreat_single.counts\tGSM461177_untreat_paired.counts\tGSM461178_untreat_paired.counts\tGSM461182_untreat_single.counts\tGSM461179_treat_single.counts\tGSM461180_treat_paired.counts\tGSM461181_treat_paired.counts"/>
+                    <has_text_matching expression="FBgn0000003\t0\t0\t0\t0\t0\t0\t0"/>
+                </assert_contents>
+            </output>
+            <output name="deseq_out">
+                <assert_contents>
+                    <!-- With reference_level=Untreated, compares Treated vs Untreated = log2(Treated/Untreated) -->
+                    <has_text_matching expression="FBgn0003360\t1933\.9504.*\t-2\.8399.*\t0\.1309.*\t-21\.68.*\t.*e-104\t.*e-101"/>
+                    <has_n_lines n="3999"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!--Test sample_sheet_contrasts mode with reference_level and target_level -->
+        <test expect_num_outputs="2">
+            <param name="select_data|how" value="sample_sheet_contrasts"/>
+            <param name="select_data|countsFile">
+                <collection type="list">
+                    <element name="GSM461179_treat_single.counts" value="GSM461179_treat_single.counts"/>
+                    <element name="GSM461180_treat_paired.counts" value="GSM461180_treat_paired.counts"/>
+                    <element name="GSM461181_treat_paired.counts" value="GSM461181_treat_paired.counts"/>
+                    <element name="GSM461176_untreat_single.counts" value="GSM461176_untreat_single.counts"/>
+                    <element name="GSM461177_untreat_paired.counts" value="GSM461177_untreat_paired.counts"/>
+                    <element name="GSM461178_untreat_paired.counts" value="GSM461178_untreat_paired.counts"/>
+                    <element name="GSM461182_untreat_single.counts" value="GSM461182_untreat_single.counts"/>
+                </collection>
+            </param>
+            <param name="select_data|sample_sheet" value="sample_sheet.tab"/>
+            <param name="select_data|design_formula_mode|mode" value="automatic"/>
+            <param name="select_data|design_formula_mode|factor" value="2"/>
+            <param name="select_data|design_formula_mode|reference_level" value="Untreated"/>
+            <param name="select_data|design_formula_mode|target_level" value="Treated"/>
+            <section name="advanced_options">
+                <param name="use_beta_priors" value="1"/>
+            </section>
+            <section name="output_options">
+                <param name="output_selector" value="normCounts"/>
+            </section>
+            <output name="counts_out">
+                <assert_contents>
+                    <has_text_matching expression="GSM461176_untreat_single.counts\tGSM461177_untreat_paired.counts\tGSM461178_untreat_paired.counts\tGSM461182_untreat_single.counts\tGSM461179_treat_single.counts\tGSM461180_treat_paired.counts\tGSM461181_treat_paired.counts"/>
+                    <has_text_matching expression="FBgn0000003\t0\t0\t0\t0\t0\t0\t0"/>
+                </assert_contents>
+            </output>
+            <output name="deseq_out">
+                <assert_contents>
+                    <!-- With reference=Untreated and target=Treated, compares Treated vs Untreated -->
+                    <has_text_matching expression="FBgn0003360\t1933\.9504.*\t-2\.8399.*\t0\.1309.*\t-21\.68.*\t.*e-104\t.*e-101"/>
+                    <has_n_lines n="3999"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!--Test sample_sheet_contrasts with custom design formula -->
+        <test expect_num_outputs="2">
+            <param name="select_data|how" value="sample_sheet_contrasts"/>
+            <param name="select_data|countsFile">
+                <collection type="list">
+                    <element name="GSM461179_treat_single.counts" value="GSM461179_treat_single.counts"/>
+                    <element name="GSM461180_treat_paired.counts" value="GSM461180_treat_paired.counts"/>
+                    <element name="GSM461181_treat_paired.counts" value="GSM461181_treat_paired.counts"/>
+                    <element name="GSM461176_untreat_single.counts" value="GSM461176_untreat_single.counts"/>
+                    <element name="GSM461177_untreat_paired.counts" value="GSM461177_untreat_paired.counts"/>
+                    <element name="GSM461178_untreat_paired.counts" value="GSM461178_untreat_paired.counts"/>
+                    <element name="GSM461182_untreat_single.counts" value="GSM461182_untreat_single.counts"/>
+                </collection>
+            </param>
+            <param name="select_data|sample_sheet" value="sample_sheet.tab"/>
+            <param name="select_data|design_formula_mode|mode" value="custom"/>
+            <param name="select_data|design_formula_mode|design_formula" value="~ treatment"/>
+            <param name="select_data|design_formula_mode|reference_level" value="Untreated"/>
+            <section name="advanced_options">
+                <param name="use_beta_priors" value="1"/>
+            </section>
+            <section name="output_options">
+                <param name="output_selector" value="normCounts"/>
+            </section>
+            <output name="counts_out">
+                <assert_contents>
+                    <has_text_matching expression="GSM461176_untreat_single.counts\tGSM461177_untreat_paired.counts\tGSM461178_untreat_paired.counts\tGSM461182_untreat_single.counts\tGSM461179_treat_single.counts\tGSM461180_treat_paired.counts\tGSM461181_treat_paired.counts"/>
+                    <has_text_matching expression="FBgn0000003\t0\t0\t0\t0\t0\t0\t0"/>
+                </assert_contents>
+            </output>
+            <output name="deseq_out">
+                <assert_contents>
+                    <!-- Custom formula ~treatment with reference=Untreated, compares Treated vs Untreated -->
+                    <has_text_matching expression="FBgn0003360\t1933\.9504.*\t-2\.8399.*\t0\.1309.*\t-21\.68.*\t.*e-104\t.*e-101"/>
+                    <has_n_lines n="3999"/>
+                </assert_contents>
+            </output>
+        </test>
+        <!--Test sample_sheet_contrasts with multi-factor custom design formula -->
+        <test expect_num_outputs="2">
+            <param name="select_data|how" value="sample_sheet_contrasts"/>
+            <param name="select_data|countsFile">
+                <collection type="list">
+                    <element name="GSM461179_treat_single.counts" value="GSM461179_treat_single.counts"/>
+                    <element name="GSM461180_treat_paired.counts" value="GSM461180_treat_paired.counts"/>
+                    <element name="GSM461181_treat_paired.counts" value="GSM461181_treat_paired.counts"/>
+                    <element name="GSM461176_untreat_single.counts" value="GSM461176_untreat_single.counts"/>
+                    <element name="GSM461177_untreat_paired.counts" value="GSM461177_untreat_paired.counts"/>
+                    <element name="GSM461178_untreat_paired.counts" value="GSM461178_untreat_paired.counts"/>
+                    <element name="GSM461182_untreat_single.counts" value="GSM461182_untreat_single.counts"/>
+                </collection>
+            </param>
+            <param name="select_data|sample_sheet" value="sample_sheet.tab"/>
+            <param name="select_data|design_formula_mode|mode" value="custom"/>
+            <param name="select_data|design_formula_mode|design_formula" value="~ sequencing + treatment"/>
+            <param name="select_data|design_formula_mode|reference_level" value="Untreated"/>
+            <section name="advanced_options">
+                <param name="use_beta_priors" value="1"/>
+            </section>
+            <section name="output_options">
+                <param name="output_selector" value="normCounts"/>
+            </section>
+            <output name="counts_out">
+                <assert_contents>
+                    <!-- Verify all 7 samples present with correct values (column order may vary) -->
+                    <has_text_matching expression="FBgn0000003\t0\t0\t0\t0\t0\t0\t0"/>
+                    <has_n_columns n="8"/>
+                </assert_contents>
+            </output>
+            <output name="deseq_out">
+                <assert_contents>
+                    <!-- Multi-factor design ~sequencing + treatment controls for sequencing type -->
+                    <!-- Effect estimates differ from single-factor design due to covariate adjustment -->
+                    <has_text_matching expression="FBgn0003360\t1933\.950.*\t-2\.88.*\t0\.111.*\t-25\.8.*\t.*e-14[0-9]\t.*e-14[0-9]"/>
+                    <has_n_lines n="3999"/>
+                </assert_contents>
+            </output>
+        </test>
     </tests>
     <help><![CDATA[
 .. class:: infomark