1# Copyright 2020 pydicom authors. See LICENSE file for details.
2"""Tests for command-line interface"""
3
4from argparse import ArgumentTypeError
5
6import pytest
7
8from pydicom.cli.main import (
9    filespec_parser, eval_element, main, filespec_parts
10)
11
12
13bad_elem_specs = (
14    "extra:colon",
15    "no_callable()",
16    "no_equals = ",
17    "BeamSequence[0]extra",  # must match to end of string
18    "BeamSequence[x]",  # index must be an int
19)
20
21missing_elements = (
22    "NotThere",
23    "BeamSequenceXX",
24    "BeamDose",  # valid keyword but not at top level
25)
26
27bad_indexes = (
28    "BeamSequence[42]",
29    "BeamSequence[-42]",
30)
31
32
33class TestFilespec:
34    @pytest.mark.parametrize("bad_spec", bad_elem_specs)
35    def test_syntax(self, bad_spec):
36        """Invalid syntax for for CLI file:element spec raises error"""
37        with pytest.raises(ArgumentTypeError, match=r".* syntax .*"):
38            filespec_parser(f"pydicom::rtplan.dcm::{bad_spec}")
39
40    @pytest.mark.parametrize("missing_element", missing_elements)
41    def test_elem_not_exists(self, missing_element):
42        """CLI filespec elements not in the dataset raise an error"""
43        with pytest.raises(
44            ArgumentTypeError, match=r".* is not in the dataset"
45        ):
46            filespec_parser(f"pydicom::rtplan.dcm::{missing_element}")
47
48    @pytest.mark.parametrize("bad_index", bad_indexes)
49    def test_bad_index(self, bad_index):
50        """CLI filespec elements with an invalid index raise an error"""
51        with pytest.raises(ArgumentTypeError, match=r".* index error"):
52            filespec_parser(f"pydicom::rtplan.dcm::{bad_index}")
53
54    def test_offers_pydicom_testfile(self):
55        """CLI message offers pydicom data file if file not found"""
56        with pytest.raises(
57            ArgumentTypeError, match=r".*pydicom::rtplan\.dcm.*is available.*"
58        ):
59            filespec_parser(f"rtplan.dcm")
60
61    def test_colons(self):
62        """CLI filespec with a colon in filename works correctly"""
63        expected = ("", r"c:\test.dcm", "")
64        assert expected == filespec_parts(r"c:\test.dcm")
65
66        expected = ("pydicom", r"c:\test.dcm", "")
67        assert expected == filespec_parts(r"pydicom::c:\test.dcm")
68
69        filespec = r"pydicom::c:\test.dcm::StudyDate"
70        expected = ("pydicom", r"c:\test.dcm", "StudyDate")
71        assert expected == filespec_parts(filespec)
72
73        filespec = r"c:\test.dcm::StudyDate"
74        expected = ("", r"c:\test.dcm", "StudyDate")
75        assert expected == filespec_parts(filespec)
76
77
78class TestFilespecElementEval:
79    # Load plan once
80    plan, _ = filespec_parser("pydicom::rtplan.dcm")[0]
81
82    def test_correct_values(self):
83        """CLI produces correct evaluation of requested element"""
84        # A nested data element
85        elem_str = "BeamSequence[0].ControlPointSequence[0].NominalBeamEnergy"
86        elem_val = eval_element(self.plan, elem_str)
87        assert 6.0 == elem_val
88
89        # A nested Sequence item
90        elem_str = "BeamSequence[0].ControlPointSequence[0]"
91        elem_val = eval_element(self.plan, elem_str)
92        assert 6.0 == elem_val.NominalBeamEnergy
93
94        # A nested Sequence itself
95        elem_str = "BeamSequence[0].ControlPointSequence"
96        elem_val = eval_element(self.plan, elem_str)
97        assert 6.0 == elem_val[0].NominalBeamEnergy
98
99        # A non-nested data element
100        elem_str = "PatientID"
101        elem_val = eval_element(self.plan, elem_str)
102        assert "id00001" == elem_val
103
104        # The file_meta or file_meta data element
105        elem_str = "file_meta"
106        elem_val = eval_element(self.plan, elem_str)
107        assert "RT Plan Storage" == elem_val.MediaStorageSOPClassUID.name
108
109        elem_str = "file_meta.MediaStorageSOPClassUID"
110        elem_val = eval_element(self.plan, elem_str)
111        assert "RT Plan Storage" == elem_val.name
112
113
114class TestCLIcall:
115    """Test calls to `pydicom` command-line interface"""
116
117    def test_bare_command(self, capsys):
118        """CLI `pydicom` with no arguments displays help"""
119        main([])
120        out, _ = capsys.readouterr()
121        assert out.startswith("usage: pydicom [-h] {")
122
123    def test_codify_command(self, capsys):
124        """CLI `codify` command prints correct output"""
125
126        # With private elements
127        main("codify -p pydicom::nested_priv_SQ.dcm".split())
128        out, _ = capsys.readouterr()
129        assert "add_new((0x0001, 0x0001)" in out
130
131        # Without private elements
132        main("codify pydicom::nested_priv_SQ.dcm".split())
133        out, _ = capsys.readouterr()
134        assert "add_new((0x0001, 0x0001)" not in out
135
136    def test_codify_data_element(self, capsys):
137        """CLI `codify` command raises error if not a Dataset"""
138        with pytest.raises(NotImplementedError):
139            main("codify pydicom::rtplan.dcm::RTPlanLabel".split())
140
141    def test_help(self, capsys):
142        """CLI `help` command gives expected output"""
143        # With subcommand
144        main("help show".split())
145        out, err = capsys.readouterr()
146        assert out.startswith("usage: pydicom show [-h] [")
147        assert err == ""
148
149        # No subcommand following
150        main(["help"])
151        out, _ = capsys.readouterr()
152        assert "Available subcommands:" in out
153
154        # Non-existent subcommand following
155        main("help DoesntExist".split())
156        out, _ = capsys.readouterr()
157        assert "Available subcommands:" in out
158
159    def test_show_command(self, capsys):
160        """CLI `show` command prints correct output"""
161        main("show pydicom::MR_small_RLE.dcm".split())
162        out, err = capsys.readouterr()
163
164        assert "Instance Creation Date              DA: '20040826'" in out
165        assert out.endswith("OB: Array of 126 elements\n")
166        assert err == ""
167
168        # Get a specific data element
169        main("show pydicom::MR_small_RLE.dcm::LargestImagePixelValue".split())
170        out, _ = capsys.readouterr()
171        assert "4000" == out.strip()
172
173    def test_show_options(self, capsys):
174        """CLI `show` command with options prints correct output"""
175        # Quiet option, image file
176        main("show -q pydicom::MR_small_RLE.dcm".split())
177        out, err = capsys.readouterr()
178
179        assert out.startswith("SOPClassUID: MR Image Storage")
180        assert out.endswith("Rows: 64\nColumns: 64\nSliceLocation: 0.0000\n")
181        assert err == ""
182
183        # 'Quiet' option, RTPLAN file
184        main("show -q pydicom::rtplan.dcm".split())
185        out, err = capsys.readouterr()
186        assert out.endswith(
187            "Beam 1 'Field 1' TREATMENT STATIC PHOTON energy 6.00000000000000 "
188            "gantry 0.0, coll 0.0, couch 0.0 "
189            "(0 wedges, 0 comps, 0 boli, 0 blocks)\n"
190        )
191        assert err == ""
192
193        # Top-level-only option, also different file for more variety
194        main("show -t pydicom::nested_priv_SQ.dcm".split())
195        out, err = capsys.readouterr()
196        assert "(0001, 0001)  Private Creator" in out
197        assert "UN: b'Nested SQ'" not in out
198        assert err == ""
199
200        # Exclude private option
201        main("show -x pydicom::nested_priv_SQ.dcm".split())
202        out, err = capsys.readouterr()
203        assert "(0001, 0001)  Private Creator" not in out
204        assert err == ""
205