1# -*- coding: utf-8 -*-
2"""
3    Command line test
4    ~~~~~~~~~~~~~~~~~
5
6    :copyright: Copyright 2006-2020 by the Pygments team, see AUTHORS.
7    :license: BSD, see LICENSE for details.
8"""
9
10import io
11import os
12import re
13import sys
14import tempfile
15from io import BytesIO
16from os import path
17
18from pytest import raises
19
20from pygments import cmdline, highlight
21
22TESTDIR = path.dirname(path.abspath(__file__))
23TESTFILE = path.join(TESTDIR, 'test_cmdline.py')
24
25TESTCODE = '''\
26def func(args):
27    pass
28'''
29
30
31def _decode_output(text):
32    try:
33        return text.decode('utf-8')
34    except UnicodeEncodeError:  # implicit encode on Python 2 with data loss
35        return text
36
37
38def run_cmdline(*args, **kwds):
39    saved_stdin = sys.stdin
40    saved_stdout = sys.stdout
41    saved_stderr = sys.stderr
42    stdin_buffer = BytesIO()
43    stdout_buffer = BytesIO()
44    stderr_buffer = BytesIO()
45    new_stdin = sys.stdin = io.TextIOWrapper(stdin_buffer, 'utf-8')
46    new_stdout = sys.stdout = io.TextIOWrapper(stdout_buffer, 'utf-8')
47    new_stderr = sys.stderr = io.TextIOWrapper(stderr_buffer, 'utf-8')
48    new_stdin.write(kwds.get('stdin', ''))
49    new_stdin.seek(0, 0)
50    try:
51        ret = cmdline.main(['pygmentize'] + list(args))
52    finally:
53        sys.stdin = saved_stdin
54        sys.stdout = saved_stdout
55        sys.stderr = saved_stderr
56    new_stdout.flush()
57    new_stderr.flush()
58    out, err = stdout_buffer.getvalue(), \
59        stderr_buffer.getvalue()
60    return (ret, _decode_output(out), _decode_output(err))
61
62
63def check_success(*cmdline, **kwds):
64    code, out, err = run_cmdline(*cmdline, **kwds)
65    assert code == 0
66    assert err == ''
67    return out
68
69
70def check_failure(*cmdline, **kwds):
71    expected_code = kwds.pop('code', 1)
72    code, out, err = run_cmdline(*cmdline, **kwds)
73    assert code == expected_code
74    assert out == ''
75    return err
76
77
78def test_normal():
79    # test that cmdline gives the same output as library api
80    from pygments.lexers import PythonLexer
81    from pygments.formatters import HtmlFormatter
82    filename = TESTFILE
83    with open(filename, 'rb') as fp:
84        code = fp.read()
85
86    output = highlight(code, PythonLexer(), HtmlFormatter())
87
88    o = check_success('-lpython', '-fhtml', filename)
89    assert o == output
90
91
92def test_stdin():
93    o = check_success('-lpython', '-fhtml', stdin=TESTCODE)
94    o = re.sub('<[^>]*>', '', o)
95    # rstrip is necessary since HTML inserts a \n after the last </div>
96    assert o.rstrip() == TESTCODE.rstrip()
97
98    # guess if no lexer given
99    o = check_success('-fhtml', stdin=TESTCODE)
100    o = re.sub('<[^>]*>', '', o)
101    # rstrip is necessary since HTML inserts a \n after the last </div>
102    assert o.rstrip() == TESTCODE.rstrip()
103
104
105def test_outfile():
106    # test that output file works with and without encoding
107    fd, name = tempfile.mkstemp()
108    os.close(fd)
109    for opts in [['-fhtml', '-o', name, TESTFILE],
110                 ['-flatex', '-o', name, TESTFILE],
111                 ['-fhtml', '-o', name, '-O', 'encoding=utf-8', TESTFILE]]:
112        try:
113            check_success(*opts)
114        finally:
115            os.unlink(name)
116
117
118def test_load_from_file():
119    lexer_file = os.path.join(TESTDIR, 'support', 'python_lexer.py')
120    formatter_file = os.path.join(TESTDIR, 'support', 'html_formatter.py')
121
122    # By default, use CustomLexer
123    o = check_success('-l', lexer_file, '-f', 'html', '-x', stdin=TESTCODE)
124    o = re.sub('<[^>]*>', '', o)
125    # rstrip is necessary since HTML inserts a \n after the last </div>
126    assert o.rstrip() == TESTCODE.rstrip()
127
128    # If user specifies a name, use it
129    o = check_success('-f', 'html', '-x', '-l',
130                      lexer_file + ':LexerWrapper', stdin=TESTCODE)
131    o = re.sub('<[^>]*>', '', o)
132    # rstrip is necessary since HTML inserts a \n after the last </div>
133    assert o.rstrip() == TESTCODE.rstrip()
134
135    # Should also work for formatters
136    o = check_success('-lpython', '-f',
137                      formatter_file + ':HtmlFormatterWrapper',
138                      '-x', stdin=TESTCODE)
139    o = re.sub('<[^>]*>', '', o)
140    # rstrip is necessary since HTML inserts a \n after the last </div>
141    assert o.rstrip() == TESTCODE.rstrip()
142
143
144def test_stream_opt():
145    o = check_success('-lpython', '-s', '-fterminal', stdin=TESTCODE)
146    o = re.sub(r'\x1b\[.*?m', '', o)
147    assert o.replace('\r\n', '\n') == TESTCODE
148
149
150def test_h_opt():
151    o = check_success('-h')
152    assert 'Usage:' in o
153
154
155def test_L_opt():
156    o = check_success('-L')
157    assert 'Lexers' in o and 'Formatters' in o and 'Filters' in o and 'Styles' in o
158    o = check_success('-L', 'lexer')
159    assert 'Lexers' in o and 'Formatters' not in o
160    check_success('-L', 'lexers')
161
162
163def test_O_opt():
164    filename = TESTFILE
165    o = check_success('-Ofull=1,linenos=true,foo=bar', '-fhtml', filename)
166    assert '<html' in o
167    assert 'class="linenos"' in o
168
169    # "foobar" is invalid for a bool option
170    e = check_failure('-Ostripnl=foobar', TESTFILE)
171    assert 'Error: Invalid value' in e
172    e = check_failure('-Ostripnl=foobar', '-lpy')
173    assert 'Error: Invalid value' in e
174
175
176def test_P_opt():
177    filename = TESTFILE
178    o = check_success('-Pfull', '-Ptitle=foo, bar=baz=,', '-fhtml', filename)
179    assert '<title>foo, bar=baz=,</title>' in o
180
181
182def test_F_opt():
183    filename = TESTFILE
184    o = check_success('-Fhighlight:tokentype=Name.Blubb,'
185                      'names=TESTFILE filename', '-fhtml', filename)
186    assert '<span class="n n-Blubb' in o
187
188
189def test_H_opt():
190    o = check_success('-H', 'formatter', 'html')
191    assert 'HTML' in o
192    o = check_success('-H', 'lexer', 'python')
193    assert 'Python' in o
194    o = check_success('-H', 'filter', 'raiseonerror')
195    assert 'raiseonerror' in o
196    e = check_failure('-H', 'lexer', 'foobar')
197    assert 'not found' in e
198
199
200def test_S_opt():
201    o = check_success('-S', 'default', '-f', 'html', '-O', 'linenos=1')
202    lines = o.splitlines()
203    for line in lines[5:]:
204        # every line is for a token class, except for the first 5 lines,
205        # which define styles for `pre` and line numbers
206        parts = line.split()
207        assert parts[0].startswith('.')
208        assert parts[1] == '{'
209        if parts[0] != '.hll':
210            assert parts[-4] == '}'
211            assert parts[-3] == '/*'
212            assert parts[-1] == '*/'
213    check_failure('-S', 'default', '-f', 'foobar')
214
215
216def test_N_opt():
217    o = check_success('-N', 'test.py')
218    assert 'python' == o.strip()
219    o = check_success('-N', 'test.unknown')
220    assert 'text' == o.strip()
221
222
223def test_invalid_opts():
224    for opts in [
225        ('-X',),
226        ('-L', '-lpy'),
227        ('-L', '-fhtml'),
228        ('-L', '-Ox'),
229        ('-S', 'default', '-l', 'py', '-f', 'html'),
230        ('-S', 'default'),
231        ('-a', 'arg'),
232        ('-H',),
233        (TESTFILE, TESTFILE),
234        ('-H', 'formatter'),
235        ('-H', 'foo', 'bar'),
236        ('-s',),
237        ('-s', TESTFILE),
238    ]:
239        check_failure(*opts, code=2)
240
241
242def test_errors():
243    # input file not found
244    e = check_failure('-lpython', 'nonexistent.py')
245    assert 'Error: cannot read infile' in e
246    assert 'nonexistent.py' in e
247
248    # lexer not found
249    e = check_failure('-lfooo', TESTFILE)
250    assert 'Error: no lexer for alias' in e
251
252    # cannot load .py file without load_from_file flag
253    e = check_failure('-l', 'nonexistent.py', TESTFILE)
254    assert 'Error: no lexer for alias' in e
255
256    # lexer file is missing/unreadable
257    e = check_failure('-l', 'nonexistent.py', '-x', TESTFILE)
258    assert 'Error: cannot read' in e
259
260    # lexer file is malformed
261    e = check_failure('-l', path.join(TESTDIR, 'support', 'empty.py'),
262                      '-x', TESTFILE)
263    assert 'Error: no valid CustomLexer class found' in e
264
265    # formatter not found
266    e = check_failure('-lpython', '-ffoo', TESTFILE)
267    assert 'Error: no formatter found for name' in e
268
269    # formatter for outfile not found
270    e = check_failure('-ofoo.foo', TESTFILE)
271    assert 'Error: no formatter found for file name' in e
272
273    # cannot load .py file without load_from_file flag
274    e = check_failure('-f', 'nonexistent.py', TESTFILE)
275    assert 'Error: no formatter found for name' in e
276
277    # formatter file is missing/unreadable
278    e = check_failure('-f', 'nonexistent.py', '-x', TESTFILE)
279    assert 'Error: cannot read' in e
280
281    # formatter file is malformed
282    e = check_failure('-f', path.join(TESTDIR, 'support', 'empty.py'),
283                      '-x', TESTFILE)
284    assert 'Error: no valid CustomFormatter class found' in e
285
286    # output file not writable
287    e = check_failure('-o', os.path.join('nonexistent', 'dir', 'out.html'),
288                      '-lpython', TESTFILE)
289    assert 'Error: cannot open outfile' in e
290    assert 'out.html' in e
291
292    # unknown filter
293    e = check_failure('-F', 'foo', TESTFILE)
294    assert 'Error: filter \'foo\' not found' in e
295
296
297def test_exception():
298    cmdline.highlight = None  # override callable to provoke TypeError
299    try:
300        # unexpected exception while highlighting
301        e = check_failure('-lpython', TESTFILE)
302        assert '*** Error while highlighting:' in e
303        assert 'TypeError' in e
304
305        # same with -v: should reraise the exception
306        assert raises(Exception, check_failure, '-lpython', '-v', TESTFILE)
307    finally:
308        cmdline.highlight = highlight
309
310
311def test_parse_opts():
312    assert cmdline._parse_options(['  ', 'keyonly,key = value ']) == \
313        {'keyonly': True, 'key': 'value'}
314