1#!/usr/bin/env python
2#
3#  Licensed under the Apache License, Version 2.0 (the "License"); you may
4#  not use this file except in compliance with the License. You may obtain
5#  a copy of the License at
6#
7#       http://www.apache.org/licenses/LICENSE-2.0
8#
9#  Unless required by applicable law or agreed to in writing, software
10#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12#  License for the specific language governing permissions and limitations
13#  under the License.
14
15import argparse
16import os
17import textwrap
18
19from six import StringIO
20from unittest import mock
21
22from cliff.formatters import table
23from cliff.tests import base
24from cliff.tests import test_columns
25
26
27class args(object):
28    def __init__(self, max_width=0, print_empty=False, fit_width=False):
29        self.fit_width = fit_width
30        if max_width > 0:
31            self.max_width = max_width
32        else:
33            # Envvar is only taken into account iff CLI parameter not given
34            self.max_width = int(os.environ.get('CLIFF_MAX_TERM_WIDTH', 0))
35        self.print_empty = print_empty
36
37
38def _table_tester_helper(tags, data, extra_args=None):
39    """Get table output as a string, formatted according to
40    CLI arguments, environment variables and terminal size
41
42    tags - tuple of strings for data tags (column headers or fields)
43    data - tuple of strings for single data row
44         - list of tuples of strings for multiple rows of data
45    extra_args - an instance of class args
46               - a list of strings for CLI arguments
47    """
48    sf = table.TableFormatter()
49
50    if extra_args is None:
51        # Default to no CLI arguments
52        parsed_args = args()
53    elif type(extra_args) == args:
54        # Use the given CLI arguments
55        parsed_args = extra_args
56    else:
57        # Parse arguments as if passed on the command-line
58        parser = argparse.ArgumentParser(description='Testing...')
59        sf.add_argument_group(parser)
60        parsed_args = parser.parse_args(extra_args)
61
62    output = StringIO()
63    emitter = sf.emit_list if type(data) is list else sf.emit_one
64    emitter(tags, data, output, parsed_args)
65    return output.getvalue()
66
67
68class TestTableFormatter(base.TestBase):
69
70    @mock.patch('cliff.utils.terminal_width')
71    def test(self, tw):
72        tw.return_value = 80
73        c = ('a', 'b', 'c', 'd')
74        d = ('A', 'B', 'C', 'test\rcarriage\r\nreturn')
75        expected = textwrap.dedent('''\
76        +-------+---------------+
77        | Field | Value         |
78        +-------+---------------+
79        | a     | A             |
80        | b     | B             |
81        | c     | C             |
82        | d     | test carriage |
83        |       | return        |
84        +-------+---------------+
85        ''')
86        self.assertEqual(expected, _table_tester_helper(c, d))
87
88
89class TestTerminalWidth(base.TestBase):
90
91    # Multi-line output when width is restricted to 42 columns
92    expected_ml_val = textwrap.dedent('''\
93    +-------+--------------------------------+
94    | Field | Value                          |
95    +-------+--------------------------------+
96    | a     | A                              |
97    | b     | B                              |
98    | c     | C                              |
99    | d     | dddddddddddddddddddddddddddddd |
100    |       | dddddddddddddddddddddddddddddd |
101    |       | ddddddddddddddddd              |
102    +-------+--------------------------------+
103    ''')
104
105    # Multi-line output when width is restricted to 80 columns
106    expected_ml_80_val = textwrap.dedent('''\
107    +-------+----------------------------------------------------------------------+
108    | Field | Value                                                                |
109    +-------+----------------------------------------------------------------------+
110    | a     | A                                                                    |
111    | b     | B                                                                    |
112    | c     | C                                                                    |
113    | d     | dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
114    |       | ddddddddd                                                            |
115    +-------+----------------------------------------------------------------------+
116    ''')  # noqa
117
118    # Single-line output, for when no line length restriction apply
119    expected_sl_val = textwrap.dedent('''\
120    +-------+-------------------------------------------------------------------------------+
121    | Field | Value                                                                         |
122    +-------+-------------------------------------------------------------------------------+
123    | a     | A                                                                             |
124    | b     | B                                                                             |
125    | c     | C                                                                             |
126    | d     | ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd |
127    +-------+-------------------------------------------------------------------------------+
128    ''')  # noqa
129
130    @mock.patch('cliff.utils.terminal_width')
131    def test_table_formatter_no_cli_param(self, tw):
132        tw.return_value = 80
133        c = ('a', 'b', 'c', 'd')
134        d = ('A', 'B', 'C', 'd' * 77)
135        self.assertEqual(
136            self.expected_ml_80_val,
137            _table_tester_helper(c, d, extra_args=args(fit_width=True)),
138        )
139
140    @mock.patch('cliff.utils.terminal_width')
141    def test_table_formatter_cli_param(self, tw):
142        tw.return_value = 80
143        c = ('a', 'b', 'c', 'd')
144        d = ('A', 'B', 'C', 'd' * 77)
145        self.assertEqual(
146            self.expected_ml_val,
147            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
148        )
149
150    @mock.patch('cliff.utils.terminal_width')
151    def test_table_formatter_no_cli_param_unlimited_tw(self, tw):
152        tw.return_value = 0
153        c = ('a', 'b', 'c', 'd')
154        d = ('A', 'B', 'C', 'd' * 77)
155        # output should not be wrapped to multiple lines
156        self.assertEqual(
157            self.expected_sl_val,
158            _table_tester_helper(c, d, extra_args=args()),
159        )
160
161    @mock.patch('cliff.utils.terminal_width')
162    def test_table_formatter_cli_param_unlimited_tw(self, tw):
163        tw.return_value = 0
164        c = ('a', 'b', 'c', 'd')
165        d = ('A', 'B', 'C', 'd' * 77)
166        self.assertEqual(
167            self.expected_ml_val,
168            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
169        )
170
171    @mock.patch('cliff.utils.terminal_width')
172    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
173    def test_table_formatter_cli_param_envvar_big(self, tw):
174        tw.return_value = 80
175        c = ('a', 'b', 'c', 'd')
176        d = ('A', 'B', 'C', 'd' * 77)
177        self.assertEqual(
178            self.expected_ml_val,
179            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
180        )
181
182    @mock.patch('cliff.utils.terminal_width')
183    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '23'})
184    def test_table_formatter_cli_param_envvar_tiny(self, tw):
185        tw.return_value = 80
186        c = ('a', 'b', 'c', 'd')
187        d = ('A', 'B', 'C', 'd' * 77)
188        self.assertEqual(
189            self.expected_ml_val,
190            _table_tester_helper(c, d, extra_args=['--max-width', '42']),
191        )
192
193
194class TestMaxWidth(base.TestBase):
195
196    expected_80 = textwrap.dedent('''\
197    +--------------------------+---------------------------------------------+
198    | Field                    | Value                                       |
199    +--------------------------+---------------------------------------------+
200    | field_name               | the value                                   |
201    | a_really_long_field_name | a value significantly longer than the field |
202    +--------------------------+---------------------------------------------+
203    ''')
204
205    @mock.patch('cliff.utils.terminal_width')
206    def test_80(self, tw):
207        tw.return_value = 80
208        c = ('field_name', 'a_really_long_field_name')
209        d = ('the value', 'a value significantly longer than the field')
210        self.assertEqual(self.expected_80, _table_tester_helper(c, d))
211
212    @mock.patch('cliff.utils.terminal_width')
213    def test_70(self, tw):
214        # resize value column
215        tw.return_value = 70
216        c = ('field_name', 'a_really_long_field_name')
217        d = ('the value', 'a value significantly longer than the field')
218        expected = textwrap.dedent('''\
219        +--------------------------+-----------------------------------------+
220        | Field                    | Value                                   |
221        +--------------------------+-----------------------------------------+
222        | field_name               | the value                               |
223        | a_really_long_field_name | a value significantly longer than the   |
224        |                          | field                                   |
225        +--------------------------+-----------------------------------------+
226        ''')
227        self.assertEqual(
228            expected,
229            _table_tester_helper(c, d, extra_args=['--fit-width']),
230        )
231
232    @mock.patch('cliff.utils.terminal_width')
233    def test_50(self, tw):
234        # resize both columns
235        tw.return_value = 50
236        c = ('field_name', 'a_really_long_field_name')
237        d = ('the value', 'a value significantly longer than the field')
238        expected = textwrap.dedent('''\
239        +-----------------------+------------------------+
240        | Field                 | Value                  |
241        +-----------------------+------------------------+
242        | field_name            | the value              |
243        | a_really_long_field_n | a value significantly  |
244        | ame                   | longer than the field  |
245        +-----------------------+------------------------+
246        ''')
247        self.assertEqual(
248            expected,
249            _table_tester_helper(c, d, extra_args=['--fit-width']),
250        )
251
252    @mock.patch('cliff.utils.terminal_width')
253    def test_10(self, tw):
254        # resize all columns limited by min_width=16
255        tw.return_value = 10
256        c = ('field_name', 'a_really_long_field_name')
257        d = ('the value', 'a value significantly longer than the field')
258        expected = textwrap.dedent('''\
259        +------------------+------------------+
260        | Field            | Value            |
261        +------------------+------------------+
262        | field_name       | the value        |
263        | a_really_long_fi | a value          |
264        | eld_name         | significantly    |
265        |                  | longer than the  |
266        |                  | field            |
267        +------------------+------------------+
268        ''')
269        self.assertEqual(
270            expected,
271            _table_tester_helper(c, d, extra_args=['--fit-width']),
272        )
273
274
275class TestListFormatter(base.TestBase):
276
277    _col_names = ('one', 'two', 'three')
278    _col_data = [(
279        'one one one one one',
280        'two two two two',
281        'three three')]
282
283    _expected_mv = {
284        80: textwrap.dedent('''\
285        +---------------------+-----------------+-------------+
286        | one                 | two             | three       |
287        +---------------------+-----------------+-------------+
288        | one one one one one | two two two two | three three |
289        +---------------------+-----------------+-------------+
290        '''),
291
292        50: textwrap.dedent('''\
293        +----------------+-----------------+-------------+
294        | one            | two             | three       |
295        +----------------+-----------------+-------------+
296        | one one one    | two two two two | three three |
297        | one one        |                 |             |
298        +----------------+-----------------+-------------+
299        '''),
300
301        47: textwrap.dedent('''\
302        +---------------+---------------+-------------+
303        | one           | two           | three       |
304        +---------------+---------------+-------------+
305        | one one one   | two two two   | three three |
306        | one one       | two           |             |
307        +---------------+---------------+-------------+
308        '''),
309
310        45: textwrap.dedent('''\
311        +--------------+--------------+-------------+
312        | one          | two          | three       |
313        +--------------+--------------+-------------+
314        | one one one  | two two two  | three three |
315        | one one      | two          |             |
316        +--------------+--------------+-------------+
317        '''),
318
319        40: textwrap.dedent('''\
320        +------------+------------+------------+
321        | one        | two        | three      |
322        +------------+------------+------------+
323        | one one    | two two    | three      |
324        | one one    | two two    | three      |
325        | one        |            |            |
326        +------------+------------+------------+
327        '''),
328
329        10: textwrap.dedent('''\
330        +----------+----------+----------+
331        | one      | two      | three    |
332        +----------+----------+----------+
333        | one one  | two two  | three    |
334        | one one  | two two  | three    |
335        | one      |          |          |
336        +----------+----------+----------+
337        '''),
338    }
339
340    @mock.patch('cliff.utils.terminal_width')
341    def test_table_list_formatter(self, tw):
342        tw.return_value = 80
343        c = ('a', 'b', 'c')
344        d1 = ('A', 'B', 'C')
345        d2 = ('D', 'E', 'test\rcarriage\r\nreturn')
346        data = [d1, d2]
347        expected = textwrap.dedent('''\
348        +---+---+---------------+
349        | a | b | c             |
350        +---+---+---------------+
351        | A | B | C             |
352        | D | E | test carriage |
353        |   |   | return        |
354        +---+---+---------------+
355        ''')
356        self.assertEqual(expected, _table_tester_helper(c, data))
357
358    @mock.patch('cliff.utils.terminal_width')
359    def test_table_formatter_formattable_column(self, tw):
360        tw.return_value = 0
361        c = ('a', 'b', 'c', 'd')
362        d = ('A', 'B', 'C', test_columns.FauxColumn(['the', 'value']))
363        expected = textwrap.dedent('''\
364        +-------+---------------------------------------------+
365        | Field | Value                                       |
366        +-------+---------------------------------------------+
367        | a     | A                                           |
368        | b     | B                                           |
369        | c     | C                                           |
370        | d     | I made this string myself: ['the', 'value'] |
371        +-------+---------------------------------------------+
372        ''')
373        self.assertEqual(expected, _table_tester_helper(c, d))
374
375    @mock.patch('cliff.utils.terminal_width')
376    def test_formattable_column(self, tw):
377        tw.return_value = 80
378        c = ('a', 'b', 'c')
379        d1 = ('A', 'B', test_columns.FauxColumn(['the', 'value']))
380        data = [d1]
381        expected = textwrap.dedent('''\
382        +---+---+---------------------------------------------+
383        | a | b | c                                           |
384        +---+---+---------------------------------------------+
385        | A | B | I made this string myself: ['the', 'value'] |
386        +---+---+---------------------------------------------+
387        ''')
388        self.assertEqual(expected, _table_tester_helper(c, data))
389
390    @mock.patch('cliff.utils.terminal_width')
391    def test_max_width_80(self, tw):
392        # no resize
393        width = tw.return_value = 80
394        self.assertEqual(
395            self._expected_mv[width],
396            _table_tester_helper(self._col_names, self._col_data),
397        )
398
399    @mock.patch('cliff.utils.terminal_width')
400    def test_max_width_50(self, tw):
401        # resize 1 column
402        width = tw.return_value = 50
403        actual = _table_tester_helper(self._col_names, self._col_data,
404                                      extra_args=['--fit-width'])
405        self.assertEqual(self._expected_mv[width], actual)
406        self.assertEqual(width, len(actual.splitlines()[0]))
407
408    @mock.patch('cliff.utils.terminal_width')
409    def test_max_width_45(self, tw):
410        # resize 2 columns
411        width = tw.return_value = 45
412        actual = _table_tester_helper(self._col_names, self._col_data,
413                                      extra_args=['--fit-width'])
414        self.assertEqual(self._expected_mv[width], actual)
415        self.assertEqual(width, len(actual.splitlines()[0]))
416
417    @mock.patch('cliff.utils.terminal_width')
418    def test_max_width_40(self, tw):
419        # resize all columns
420        width = tw.return_value = 40
421        actual = _table_tester_helper(self._col_names, self._col_data,
422                                      extra_args=['--fit-width'])
423        self.assertEqual(self._expected_mv[width], actual)
424        self.assertEqual(width, len(actual.splitlines()[0]))
425
426    @mock.patch('cliff.utils.terminal_width')
427    def test_max_width_10(self, tw):
428        # resize all columns limited by min_width=8
429        width = tw.return_value = 10
430        actual = _table_tester_helper(self._col_names, self._col_data,
431                                      extra_args=['--fit-width'])
432        self.assertEqual(self._expected_mv[width], actual)
433        # 3 columns each 8 wide, plus table spacing and borders
434        expected_width = 11 * 3 + 1
435        self.assertEqual(expected_width, len(actual.splitlines()[0]))
436
437    # Force a wide terminal by overriding its width with envvar
438    @mock.patch('cliff.utils.terminal_width')
439    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '666'})
440    def test_max_width_and_envvar_max(self, tw):
441        # no resize
442        tw.return_value = 80
443        self.assertEqual(
444            self._expected_mv[80],
445            _table_tester_helper(self._col_names, self._col_data),
446        )
447
448        # resize 1 column
449        tw.return_value = 50
450        self.assertEqual(
451            self._expected_mv[80],
452            _table_tester_helper(self._col_names, self._col_data),
453        )
454
455        # resize 2 columns
456        tw.return_value = 45
457        self.assertEqual(
458            self._expected_mv[80],
459            _table_tester_helper(self._col_names, self._col_data),
460        )
461
462        # resize all columns
463        tw.return_value = 40
464        self.assertEqual(
465            self._expected_mv[80],
466            _table_tester_helper(self._col_names, self._col_data),
467        )
468
469        # resize all columns limited by min_width=8
470        tw.return_value = 10
471        self.assertEqual(
472            self._expected_mv[80],
473            _table_tester_helper(self._col_names, self._col_data),
474        )
475
476    # Force a narrow terminal by overriding its width with envvar
477    @mock.patch('cliff.utils.terminal_width')
478    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '47'})
479    def test_max_width_and_envvar_mid(self, tw):
480        # no resize
481        tw.return_value = 80
482        self.assertEqual(
483            self._expected_mv[47],
484            _table_tester_helper(self._col_names, self._col_data),
485        )
486
487        # resize 1 column
488        tw.return_value = 50
489        actual = _table_tester_helper(self._col_names, self._col_data)
490        self.assertEqual(self._expected_mv[47], actual)
491        self.assertEqual(47, len(actual.splitlines()[0]))
492
493        # resize 2 columns
494        tw.return_value = 45
495        actual = _table_tester_helper(self._col_names, self._col_data)
496        self.assertEqual(self._expected_mv[47], actual)
497        self.assertEqual(47, len(actual.splitlines()[0]))
498
499        # resize all columns
500        tw.return_value = 40
501        actual = _table_tester_helper(self._col_names, self._col_data)
502        self.assertEqual(self._expected_mv[47], actual)
503        self.assertEqual(47, len(actual.splitlines()[0]))
504
505        # resize all columns limited by min_width=8
506        tw.return_value = 10
507        actual = _table_tester_helper(self._col_names, self._col_data)
508        self.assertEqual(self._expected_mv[47], actual)
509        self.assertEqual(47, len(actual.splitlines()[0]))
510
511    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '80'})
512    def test_env_maxwidth_noresize(self):
513        # no resize
514        self.assertEqual(
515            self._expected_mv[80],
516            _table_tester_helper(self._col_names, self._col_data),
517        )
518
519    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '50'})
520    def test_env_maxwidth_resize_one(self):
521        # resize 1 column
522        actual = _table_tester_helper(self._col_names, self._col_data)
523        self.assertEqual(self._expected_mv[50], actual)
524        self.assertEqual(50, len(actual.splitlines()[0]))
525
526    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '45'})
527    def test_env_maxwidth_resize_two(self):
528        # resize 2 columns
529        actual = _table_tester_helper(self._col_names, self._col_data)
530        self.assertEqual(self._expected_mv[45], actual)
531        self.assertEqual(45, len(actual.splitlines()[0]))
532
533    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '40'})
534    def test_env_maxwidth_resize_all(self):
535        # resize all columns
536        actual = _table_tester_helper(self._col_names, self._col_data)
537        self.assertEqual(self._expected_mv[40], actual)
538        self.assertEqual(40, len(actual.splitlines()[0]))
539
540    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '8'})
541    def test_env_maxwidth_resize_all_tiny(self):
542        # resize all columns limited by min_width=8
543        actual = _table_tester_helper(self._col_names, self._col_data)
544        self.assertEqual(self._expected_mv[10], actual)
545        # 3 columns each 8 wide, plus table spacing and borders
546        expected_width = 11 * 3 + 1
547        self.assertEqual(expected_width, len(actual.splitlines()[0]))
548
549    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
550    def test_env_maxwidth_args_big(self):
551        self.assertEqual(
552            self._expected_mv[80],
553            _table_tester_helper(self._col_names, self._col_data,
554                                 extra_args=args(666)),
555        )
556
557    @mock.patch.dict(os.environ, {'CLIFF_MAX_TERM_WIDTH': '42'})
558    def test_env_maxwidth_args_tiny(self):
559        self.assertEqual(
560            self._expected_mv[40],
561            _table_tester_helper(self._col_names, self._col_data,
562                                 extra_args=args(40)),
563        )
564
565    @mock.patch('cliff.utils.terminal_width')
566    def test_empty(self, tw):
567        tw.return_value = 80
568        c = ('a', 'b', 'c')
569        data = []
570        expected = '\n'
571        self.assertEqual(expected, _table_tester_helper(c, data))
572
573    @mock.patch('cliff.utils.terminal_width')
574    def test_empty_table(self, tw):
575        tw.return_value = 80
576        c = ('a', 'b', 'c')
577        data = []
578        expected = textwrap.dedent('''\
579        +---+---+---+
580        | a | b | c |
581        +---+---+---+
582        +---+---+---+
583        ''')
584        self.assertEqual(
585            expected,
586            _table_tester_helper(c, data,
587                                 extra_args=['--print-empty']),
588        )
589
590
591class TestFieldWidths(base.TestBase):
592
593    def test(self):
594        tf = table.TableFormatter
595        self.assertEqual(
596            {
597                'a': 1,
598                'b': 2,
599                'c': 3,
600                'd': 10
601            },
602            tf._field_widths(
603                ('a', 'b', 'c', 'd'),
604                '+---+----+-----+------------+'),
605        )
606
607    def test_zero(self):
608        tf = table.TableFormatter
609        self.assertEqual(
610            {
611                'a': 0,
612                'b': 0,
613                'c': 0
614            },
615            tf._field_widths(
616                ('a', 'b', 'c'),
617                '+--+-++'),
618        )
619
620    def test_info(self):
621        tf = table.TableFormatter
622        self.assertEqual((49, 4), (tf._width_info(80, 10)))
623        self.assertEqual((76, 76), (tf._width_info(80, 1)))
624        self.assertEqual((79, 0), (tf._width_info(80, 0)))
625        self.assertEqual((0, 0), (tf._width_info(0, 80)))
626