Skip to content

Commit 18bc4d3

Browse files
test: expand low-risk coverage
1 parent 2f12729 commit 18bc4d3

10 files changed

Lines changed: 1048 additions & 1 deletion

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
env:
3535
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3636
run: |
37-
coverage report -m
37+
coverage report --fail-under=22 -m
3838
coveralls --service=github
3939
codecov
4040

tests/unit/test_gff3_QC_cli.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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\tError_code\tError_level\tError_tag', report_handle.getvalue())
104+
self.assertIn('Emr0001', report_handle.getvalue())
105+
self.assertIn('Esf0003', report_handle.getvalue())
106+
self.assertIn('Error_code\tNumber_of_problematic_models\tError_level\tError_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()

tests/unit/test_gff3_fix_cli.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import io
2+
import unittest
3+
from argparse import Namespace
4+
from unittest import mock
5+
6+
from gff3tool.bin import gff3_fix
7+
8+
9+
class TestGff3FixCli(unittest.TestCase):
10+
def test_script_main_exits_when_qc_report_missing(self):
11+
args = Namespace(qc_report=None, gff='input.gff3', output_gff='out.gff3')
12+
13+
with mock.patch('argparse.ArgumentParser.parse_args', return_value=args), \
14+
mock.patch('argparse.ArgumentParser.print_help') as print_help, \
15+
self.assertRaises(SystemExit):
16+
gff3_fix.script_main()
17+
18+
print_help.assert_called_once()
19+
20+
def test_script_main_exits_when_gff_missing(self):
21+
args = Namespace(qc_report='report.txt', gff=None, output_gff='out.gff3')
22+
23+
with mock.patch('argparse.ArgumentParser.parse_args', return_value=args), \
24+
mock.patch('argparse.ArgumentParser.print_help') as print_help, \
25+
self.assertRaises(SystemExit):
26+
gff3_fix.script_main()
27+
28+
print_help.assert_called_once()
29+
30+
def test_script_main_parses_qc_report_and_calls_fix_main(self):
31+
args = Namespace(qc_report='report.txt', gff='input.gff3', output_gff='out.gff3')
32+
report_content = (
33+
'Line_num\tError_code\tError_level\tError_tag\n'
34+
"['Line 2', 'Line 4']\tEmr0001\tError\ttag1\n"
35+
"['Line 3']\tEsf0003\tWarning\ttag2\n"
36+
'malformed line\n'
37+
)
38+
gff3 = object()
39+
40+
def open_side_effect(path, mode='r', *args, **kwargs):
41+
if path == 'report.txt':
42+
return io.StringIO(report_content)
43+
raise AssertionError(path)
44+
45+
with mock.patch('argparse.ArgumentParser.parse_args', return_value=args), \
46+
mock.patch('builtins.open', side_effect=open_side_effect), \
47+
mock.patch.object(gff3_fix, 'Gff3', autospec=True, return_value=gff3), \
48+
mock.patch.object(gff3_fix.gff3_fix.fix, 'main', autospec=True) as fix_main:
49+
gff3_fix.script_main()
50+
51+
fix_main.assert_called_once_with(
52+
gff3=gff3,
53+
output_gff='out.gff3',
54+
error_dict={'Emr0001': [[2, 4]], 'Esf0003': [[3]]},
55+
line_num_dict={2: {'Emr0001': 'Error'}, 4: {'Emr0001': 'Error'}, 3: {'Esf0003': 'Warning'}},
56+
logger=mock.ANY,
57+
)
58+
59+
def test_script_main_exits_when_gff_cannot_be_read(self):
60+
args = Namespace(qc_report='report.txt', gff='input.gff3', output_gff='out.gff3')
61+
62+
with mock.patch('argparse.ArgumentParser.parse_args', return_value=args), \
63+
mock.patch('builtins.open', return_value=io.StringIO('header\n')), \
64+
mock.patch.object(gff3_fix, 'Gff3', autospec=True, side_effect=OSError), \
65+
self.assertRaises(SystemExit) as exc:
66+
gff3_fix.script_main()
67+
68+
self.assertEqual(exc.exception.code, 1)
69+
70+
71+
if __name__ == '__main__':
72+
unittest.main()

0 commit comments

Comments
 (0)