1+ import io
2+ import unittest
3+ from argparse import Namespace
4+ from unittest import mock
5+
6+ from gff3tool .bin import gff3_QC
7+
8+
9+ class TestGff3QcCli (unittest .TestCase ):
10+ def test_script_main_exits_when_gff_missing_and_no_stdin (self ):
11+ args = Namespace (
12+ gff = None ,
13+ fasta = 'ref.fa' ,
14+ noncanonical_gene = False ,
15+ initial_phase = False ,
16+ allowed_num_of_n = 0 ,
17+ check_n_feature_types = ['CDS' ],
18+ output = None ,
19+ statistic = None ,
20+ )
21+
22+ stdin = mock .Mock ()
23+ stdin .isatty .return_value = True
24+
25+ with mock .patch ('argparse.ArgumentParser.parse_args' , return_value = args ), \
26+ mock .patch ('sys.stdin' , stdin ), \
27+ mock .patch ('argparse.ArgumentParser.print_help' ) as print_help , \
28+ self .assertRaises (SystemExit ) as exc :
29+ gff3_QC .script_main ()
30+
31+ print_help .assert_called_once ()
32+ self .assertEqual (exc .exception .code , 1 )
33+
34+ def test_script_main_exits_when_fasta_missing_and_no_stdin (self ):
35+ args = Namespace (
36+ gff = 'input.gff3' ,
37+ fasta = None ,
38+ noncanonical_gene = False ,
39+ initial_phase = False ,
40+ allowed_num_of_n = 0 ,
41+ check_n_feature_types = ['CDS' ],
42+ output = None ,
43+ statistic = None ,
44+ )
45+
46+ stdin = mock .Mock ()
47+ stdin .isatty .return_value = True
48+
49+ with mock .patch ('argparse.ArgumentParser.parse_args' , return_value = args ), \
50+ mock .patch ('sys.stdin' , stdin ), \
51+ mock .patch ('argparse.ArgumentParser.print_help' ) as print_help , \
52+ self .assertRaises (SystemExit ) as exc :
53+ gff3_QC .script_main ()
54+
55+ print_help .assert_called_once ()
56+ self .assertEqual (exc .exception .code , 1 )
57+
58+ def test_script_main_noncanonical_skips_phase_and_writes_default_reports (self ):
59+ args = Namespace (
60+ gff = 'input.gff3' ,
61+ fasta = 'ref.fa' ,
62+ noncanonical_gene = True ,
63+ initial_phase = False ,
64+ allowed_num_of_n = 0 ,
65+ check_n_feature_types = ['CDS' ],
66+ output = None ,
67+ statistic = None ,
68+ )
69+ gff3 = mock .Mock ()
70+ gff3 .check_parent_boundary .return_value = True
71+ extract_errors = [{'line_num' : ['Line 2' ], 'eCode' : 'Emr0001' , 'error_level' : 'Error' , 'eTag' : 'internal' }]
72+ intra_errors = [{'line_num' : ['Line 3' ], 'eCode' : 'Ema0006' , 'error_level' : 'Warning' , 'eTag' : 'intra' }]
73+ inter_errors = [{'line_num' : ['Line 4' ], 'eCode' : 'Emr0002' , 'error_level' : 'Error' , 'eTag' : 'inter' }]
74+ single_errors = [{'line_num' : ['Line 5' ], 'eCode' : 'Esf0003' , 'error_level' : 'Error' , 'eTag' : 'single' }]
75+ report_handle = io .StringIO ()
76+ stat_handle = io .StringIO ()
77+
78+ def open_side_effect (path , mode = 'r' , * args , ** kwargs ):
79+ if path == 'report.txt' :
80+ return report_handle
81+ if path == 'statistic.txt' :
82+ return stat_handle
83+ raise AssertionError (path )
84+
85+ with mock .patch ('argparse.ArgumentParser.parse_args' , return_value = args ), \
86+ mock .patch .object (gff3_QC , 'Gff3' , autospec = True , return_value = gff3 ), \
87+ mock .patch .object (gff3_QC .function4gff , 'FIX_MISSING_ATTR' , autospec = True ) as fix_missing , \
88+ mock .patch .object (gff3_QC .function4gff , 'extract_internal_detected_errors' , autospec = True , return_value = extract_errors ), \
89+ mock .patch .object (gff3_QC .intra_model , 'main' , autospec = True , return_value = intra_errors ), \
90+ mock .patch .object (gff3_QC .inter_model , 'main' , autospec = True , return_value = inter_errors ), \
91+ mock .patch .object (gff3_QC .single_feature , 'main' , autospec = True , return_value = single_errors ), \
92+ mock .patch ('builtins.open' , side_effect = open_side_effect ):
93+ gff3_QC .script_main ()
94+
95+ gff3 .check_phase .assert_not_called ()
96+ gff3 .check_reference .assert_called_once_with (
97+ fasta_external = 'ref.fa' ,
98+ check_n = True ,
99+ allowed_num_of_n = 0 ,
100+ feature_types = ['CDS' ],
101+ )
102+ fix_missing .assert_called_once_with (gff3 , logger = mock .ANY )
103+ self .assertIn ('Line_num\t Error_code\t Error_level\t Error_tag' , report_handle .getvalue ())
104+ self .assertIn ('Emr0001' , report_handle .getvalue ())
105+ self .assertIn ('Esf0003' , report_handle .getvalue ())
106+ self .assertIn ('Error_code\t Number_of_problematic_models\t Error_level\t Error_tag' , stat_handle .getvalue ())
107+ self .assertIn ('Esf0003' , stat_handle .getvalue ())
108+
109+ def test_script_main_runs_phase_check_for_canonical_models (self ):
110+ args = Namespace (
111+ gff = 'input.gff3' ,
112+ fasta = 'ref.fa' ,
113+ noncanonical_gene = False ,
114+ initial_phase = True ,
115+ allowed_num_of_n = 0 ,
116+ check_n_feature_types = ['CDS' ],
117+ output = 'qc.txt' ,
118+ statistic = 'stats.txt' ,
119+ )
120+ gff3 = mock .Mock ()
121+ gff3 .check_parent_boundary .return_value = True
122+
123+ with mock .patch ('argparse.ArgumentParser.parse_args' , return_value = args ), \
124+ mock .patch .object (gff3_QC , 'Gff3' , autospec = True , return_value = gff3 ), \
125+ mock .patch .object (gff3_QC .function4gff , 'FIX_MISSING_ATTR' , autospec = True ), \
126+ mock .patch .object (gff3_QC .function4gff , 'extract_internal_detected_errors' , autospec = True , return_value = []), \
127+ mock .patch .object (gff3_QC .intra_model , 'main' , autospec = True , return_value = []), \
128+ mock .patch .object (gff3_QC .inter_model , 'main' , autospec = True , return_value = []), \
129+ mock .patch .object (gff3_QC .single_feature , 'main' , autospec = True , return_value = []), \
130+ mock .patch ('builtins.open' , side_effect = [io .StringIO (), io .StringIO ()]):
131+ gff3_QC .script_main ()
132+
133+ gff3 .check_phase .assert_called_once_with (True )
134+
135+ def test_script_main_exits_early_when_parent_boundary_fails (self ):
136+ args = Namespace (
137+ gff = 'input.gff3' ,
138+ fasta = 'ref.fa' ,
139+ noncanonical_gene = False ,
140+ initial_phase = False ,
141+ allowed_num_of_n = 0 ,
142+ check_n_feature_types = ['CDS' ],
143+ output = None ,
144+ statistic = None ,
145+ )
146+ gff3 = mock .Mock ()
147+ gff3 .check_parent_boundary .return_value = False
148+
149+ with mock .patch ('argparse.ArgumentParser.parse_args' , return_value = args ), \
150+ mock .patch .object (gff3_QC , 'Gff3' , autospec = True , return_value = gff3 ), \
151+ self .assertRaises (SystemExit ):
152+ gff3_QC .script_main ()
153+
154+ gff3 .check_unresolved_parents .assert_not_called ()
155+
156+
157+ if __name__ == '__main__' :
158+ unittest .main ()
0 commit comments