Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/humanfriendly/tests.py @ 1:56ad4e20f292 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
| author | guerler |
|---|---|
| date | Fri, 31 Jul 2020 00:32:28 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 0:d30785e31577 | 1:56ad4e20f292 |
|---|---|
| 1 #!/usr/bin/env python | |
| 2 # vim: fileencoding=utf-8 : | |
| 3 | |
| 4 # Tests for the `humanfriendly' package. | |
| 5 # | |
| 6 # Author: Peter Odding <peter.odding@paylogic.eu> | |
| 7 # Last Change: April 19, 2020 | |
| 8 # URL: https://humanfriendly.readthedocs.io | |
| 9 | |
| 10 """Test suite for the `humanfriendly` package.""" | |
| 11 | |
| 12 # Standard library modules. | |
| 13 import datetime | |
| 14 import math | |
| 15 import os | |
| 16 import random | |
| 17 import re | |
| 18 import subprocess | |
| 19 import sys | |
| 20 import time | |
| 21 import types | |
| 22 import unittest | |
| 23 import warnings | |
| 24 | |
| 25 # Modules included in our package. | |
| 26 from humanfriendly import ( | |
| 27 InvalidDate, | |
| 28 InvalidLength, | |
| 29 InvalidSize, | |
| 30 InvalidTimespan, | |
| 31 Timer, | |
| 32 coerce_boolean, | |
| 33 coerce_pattern, | |
| 34 format_length, | |
| 35 format_number, | |
| 36 format_path, | |
| 37 format_size, | |
| 38 format_timespan, | |
| 39 parse_date, | |
| 40 parse_length, | |
| 41 parse_path, | |
| 42 parse_size, | |
| 43 parse_timespan, | |
| 44 prompts, | |
| 45 round_number, | |
| 46 ) | |
| 47 from humanfriendly.case import CaseInsensitiveDict, CaseInsensitiveKey | |
| 48 from humanfriendly.cli import main | |
| 49 from humanfriendly.compat import StringIO | |
| 50 from humanfriendly.decorators import cached | |
| 51 from humanfriendly.deprecation import DeprecationProxy, define_aliases, deprecated_args, get_aliases | |
| 52 from humanfriendly.prompts import ( | |
| 53 TooManyInvalidReplies, | |
| 54 prompt_for_confirmation, | |
| 55 prompt_for_choice, | |
| 56 prompt_for_input, | |
| 57 ) | |
| 58 from humanfriendly.sphinx import ( | |
| 59 deprecation_note_callback, | |
| 60 man_role, | |
| 61 pypi_role, | |
| 62 setup, | |
| 63 special_methods_callback, | |
| 64 usage_message_callback, | |
| 65 ) | |
| 66 from humanfriendly.tables import ( | |
| 67 format_pretty_table, | |
| 68 format_robust_table, | |
| 69 format_rst_table, | |
| 70 format_smart_table, | |
| 71 ) | |
| 72 from humanfriendly.terminal import ( | |
| 73 ANSI_CSI, | |
| 74 ANSI_ERASE_LINE, | |
| 75 ANSI_HIDE_CURSOR, | |
| 76 ANSI_RESET, | |
| 77 ANSI_SGR, | |
| 78 ANSI_SHOW_CURSOR, | |
| 79 ansi_strip, | |
| 80 ansi_style, | |
| 81 ansi_width, | |
| 82 ansi_wrap, | |
| 83 clean_terminal_output, | |
| 84 connected_to_terminal, | |
| 85 find_terminal_size, | |
| 86 get_pager_command, | |
| 87 message, | |
| 88 output, | |
| 89 show_pager, | |
| 90 terminal_supports_colors, | |
| 91 warning, | |
| 92 ) | |
| 93 from humanfriendly.terminal.html import html_to_ansi | |
| 94 from humanfriendly.terminal.spinners import AutomaticSpinner, Spinner | |
| 95 from humanfriendly.testing import ( | |
| 96 CallableTimedOut, | |
| 97 CaptureOutput, | |
| 98 MockedProgram, | |
| 99 PatchedAttribute, | |
| 100 PatchedItem, | |
| 101 TemporaryDirectory, | |
| 102 TestCase, | |
| 103 retry, | |
| 104 run_cli, | |
| 105 skip_on_raise, | |
| 106 touch, | |
| 107 ) | |
| 108 from humanfriendly.text import ( | |
| 109 compact, | |
| 110 compact_empty_lines, | |
| 111 concatenate, | |
| 112 dedent, | |
| 113 generate_slug, | |
| 114 pluralize, | |
| 115 random_string, | |
| 116 trim_empty_lines, | |
| 117 ) | |
| 118 from humanfriendly.usage import ( | |
| 119 find_meta_variables, | |
| 120 format_usage, | |
| 121 parse_usage, | |
| 122 render_usage, | |
| 123 ) | |
| 124 | |
| 125 # Test dependencies. | |
| 126 from mock import MagicMock | |
| 127 | |
| 128 | |
| 129 class HumanFriendlyTestCase(TestCase): | |
| 130 | |
| 131 """Container for the `humanfriendly` test suite.""" | |
| 132 | |
| 133 def test_case_insensitive_dict(self): | |
| 134 """Test the CaseInsensitiveDict class.""" | |
| 135 # Test support for the dict(iterable) signature. | |
| 136 assert len(CaseInsensitiveDict([('key', True), ('KEY', False)])) == 1 | |
| 137 # Test support for the dict(iterable, **kw) signature. | |
| 138 assert len(CaseInsensitiveDict([('one', True), ('ONE', False)], one=False, two=True)) == 2 | |
| 139 # Test support for the dict(mapping) signature. | |
| 140 assert len(CaseInsensitiveDict(dict(key=True, KEY=False))) == 1 | |
| 141 # Test support for the dict(mapping, **kw) signature. | |
| 142 assert len(CaseInsensitiveDict(dict(one=True, ONE=False), one=False, two=True)) == 2 | |
| 143 # Test support for the dict(**kw) signature. | |
| 144 assert len(CaseInsensitiveDict(one=True, ONE=False, two=True)) == 2 | |
| 145 # Test support for dict.fromkeys(). | |
| 146 obj = CaseInsensitiveDict.fromkeys(["One", "one", "ONE", "Two", "two", "TWO"]) | |
| 147 assert len(obj) == 2 | |
| 148 # Test support for dict.get(). | |
| 149 obj = CaseInsensitiveDict(existing_key=42) | |
| 150 assert obj.get('Existing_Key') == 42 | |
| 151 # Test support for dict.pop(). | |
| 152 obj = CaseInsensitiveDict(existing_key=42) | |
| 153 assert obj.pop('Existing_Key') == 42 | |
| 154 assert len(obj) == 0 | |
| 155 # Test support for dict.setdefault(). | |
| 156 obj = CaseInsensitiveDict(existing_key=42) | |
| 157 assert obj.setdefault('Existing_Key') == 42 | |
| 158 obj.setdefault('other_key', 11) | |
| 159 assert obj['Other_Key'] == 11 | |
| 160 # Test support for dict.__contains__(). | |
| 161 obj = CaseInsensitiveDict(existing_key=42) | |
| 162 assert 'Existing_Key' in obj | |
| 163 # Test support for dict.__delitem__(). | |
| 164 obj = CaseInsensitiveDict(existing_key=42) | |
| 165 del obj['Existing_Key'] | |
| 166 assert len(obj) == 0 | |
| 167 # Test support for dict.__getitem__(). | |
| 168 obj = CaseInsensitiveDict(existing_key=42) | |
| 169 assert obj['Existing_Key'] == 42 | |
| 170 # Test support for dict.__setitem__(). | |
| 171 obj = CaseInsensitiveDict(existing_key=42) | |
| 172 obj['Existing_Key'] = 11 | |
| 173 assert obj['existing_key'] == 11 | |
| 174 | |
| 175 def test_case_insensitive_key(self): | |
| 176 """Test the CaseInsensitiveKey class.""" | |
| 177 # Test the __eq__() special method. | |
| 178 polite = CaseInsensitiveKey("Please don't shout") | |
| 179 rude = CaseInsensitiveKey("PLEASE DON'T SHOUT") | |
| 180 assert polite == rude | |
| 181 # Test the __hash__() special method. | |
| 182 mapping = {} | |
| 183 mapping[polite] = 1 | |
| 184 mapping[rude] = 2 | |
| 185 assert len(mapping) == 1 | |
| 186 | |
| 187 def test_capture_output(self): | |
| 188 """Test the CaptureOutput class.""" | |
| 189 with CaptureOutput() as capturer: | |
| 190 sys.stdout.write("Something for stdout.\n") | |
| 191 sys.stderr.write("And for stderr.\n") | |
| 192 assert capturer.stdout.get_lines() == ["Something for stdout."] | |
| 193 assert capturer.stderr.get_lines() == ["And for stderr."] | |
| 194 | |
| 195 def test_skip_on_raise(self): | |
| 196 """Test the skip_on_raise() decorator.""" | |
| 197 def test_fn(): | |
| 198 raise NotImplementedError() | |
| 199 decorator_fn = skip_on_raise(NotImplementedError) | |
| 200 decorated_fn = decorator_fn(test_fn) | |
| 201 self.assertRaises(NotImplementedError, test_fn) | |
| 202 self.assertRaises(unittest.SkipTest, decorated_fn) | |
| 203 | |
| 204 def test_retry_raise(self): | |
| 205 """Test :func:`~humanfriendly.testing.retry()` based on assertion errors.""" | |
| 206 # Define a helper function that will raise an assertion error on the | |
| 207 # first call and return a string on the second call. | |
| 208 def success_helper(): | |
| 209 if not hasattr(success_helper, 'was_called'): | |
| 210 setattr(success_helper, 'was_called', True) | |
| 211 assert False | |
| 212 else: | |
| 213 return 'yes' | |
| 214 assert retry(success_helper) == 'yes' | |
| 215 | |
| 216 # Define a helper function that always raises an assertion error. | |
| 217 def failure_helper(): | |
| 218 assert False | |
| 219 with self.assertRaises(AssertionError): | |
| 220 retry(failure_helper, timeout=1) | |
| 221 | |
| 222 def test_retry_return(self): | |
| 223 """Test :func:`~humanfriendly.testing.retry()` based on return values.""" | |
| 224 # Define a helper function that will return False on the first call and | |
| 225 # return a number on the second call. | |
| 226 def success_helper(): | |
| 227 if not hasattr(success_helper, 'was_called'): | |
| 228 # On the first call we return False. | |
| 229 setattr(success_helper, 'was_called', True) | |
| 230 return False | |
| 231 else: | |
| 232 # On the second call we return a number. | |
| 233 return 42 | |
| 234 assert retry(success_helper) == 42 | |
| 235 with self.assertRaises(CallableTimedOut): | |
| 236 retry(lambda: False, timeout=1) | |
| 237 | |
| 238 def test_mocked_program(self): | |
| 239 """Test :class:`humanfriendly.testing.MockedProgram`.""" | |
| 240 name = random_string() | |
| 241 script = dedent(''' | |
| 242 # This goes to stdout. | |
| 243 tr a-z A-Z | |
| 244 # This goes to stderr. | |
| 245 echo Fake warning >&2 | |
| 246 ''') | |
| 247 with MockedProgram(name=name, returncode=42, script=script) as directory: | |
| 248 assert os.path.isdir(directory) | |
| 249 assert os.path.isfile(os.path.join(directory, name)) | |
| 250 program = subprocess.Popen(name, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| 251 stdout, stderr = program.communicate(input=b'hello world\n') | |
| 252 assert program.returncode == 42 | |
| 253 assert stdout == b'HELLO WORLD\n' | |
| 254 assert stderr == b'Fake warning\n' | |
| 255 | |
| 256 def test_temporary_directory(self): | |
| 257 """Test :class:`humanfriendly.testing.TemporaryDirectory`.""" | |
| 258 with TemporaryDirectory() as directory: | |
| 259 assert os.path.isdir(directory) | |
| 260 temporary_file = os.path.join(directory, 'some-file') | |
| 261 with open(temporary_file, 'w') as handle: | |
| 262 handle.write("Hello world!") | |
| 263 assert not os.path.exists(temporary_file) | |
| 264 assert not os.path.exists(directory) | |
| 265 | |
| 266 def test_touch(self): | |
| 267 """Test :func:`humanfriendly.testing.touch()`.""" | |
| 268 with TemporaryDirectory() as directory: | |
| 269 # Create a file in the temporary directory. | |
| 270 filename = os.path.join(directory, random_string()) | |
| 271 assert not os.path.isfile(filename) | |
| 272 touch(filename) | |
| 273 assert os.path.isfile(filename) | |
| 274 # Create a file in a subdirectory. | |
| 275 filename = os.path.join(directory, random_string(), random_string()) | |
| 276 assert not os.path.isfile(filename) | |
| 277 touch(filename) | |
| 278 assert os.path.isfile(filename) | |
| 279 | |
| 280 def test_patch_attribute(self): | |
| 281 """Test :class:`humanfriendly.testing.PatchedAttribute`.""" | |
| 282 class Subject(object): | |
| 283 my_attribute = 42 | |
| 284 instance = Subject() | |
| 285 assert instance.my_attribute == 42 | |
| 286 with PatchedAttribute(instance, 'my_attribute', 13) as return_value: | |
| 287 assert return_value is instance | |
| 288 assert instance.my_attribute == 13 | |
| 289 assert instance.my_attribute == 42 | |
| 290 | |
| 291 def test_patch_item(self): | |
| 292 """Test :class:`humanfriendly.testing.PatchedItem`.""" | |
| 293 instance = dict(my_item=True) | |
| 294 assert instance['my_item'] is True | |
| 295 with PatchedItem(instance, 'my_item', False) as return_value: | |
| 296 assert return_value is instance | |
| 297 assert instance['my_item'] is False | |
| 298 assert instance['my_item'] is True | |
| 299 | |
| 300 def test_run_cli_intercepts_exit(self): | |
| 301 """Test that run_cli() intercepts SystemExit.""" | |
| 302 returncode, output = run_cli(lambda: sys.exit(42)) | |
| 303 self.assertEqual(returncode, 42) | |
| 304 | |
| 305 def test_run_cli_intercepts_error(self): | |
| 306 """Test that run_cli() intercepts exceptions.""" | |
| 307 returncode, output = run_cli(self.run_cli_raise_other) | |
| 308 self.assertEqual(returncode, 1) | |
| 309 | |
| 310 def run_cli_raise_other(self): | |
| 311 """run_cli() sample that raises an exception.""" | |
| 312 raise ValueError() | |
| 313 | |
| 314 def test_run_cli_intercepts_output(self): | |
| 315 """Test that run_cli() intercepts output.""" | |
| 316 expected_output = random_string() + "\n" | |
| 317 returncode, output = run_cli(lambda: sys.stdout.write(expected_output)) | |
| 318 self.assertEqual(returncode, 0) | |
| 319 self.assertEqual(output, expected_output) | |
| 320 | |
| 321 def test_caching_decorator(self): | |
| 322 """Test the caching decorator.""" | |
| 323 # Confirm that the caching decorator works. | |
| 324 a = cached(lambda: random.random()) | |
| 325 b = cached(lambda: random.random()) | |
| 326 assert a() == a() | |
| 327 assert b() == b() | |
| 328 # Confirm that functions have their own cache. | |
| 329 assert a() != b() | |
| 330 | |
| 331 def test_compact(self): | |
| 332 """Test :func:`humanfriendly.text.compact()`.""" | |
| 333 assert compact(' a \n\n b ') == 'a b' | |
| 334 assert compact(''' | |
| 335 %s template notation | |
| 336 ''', 'Simple') == 'Simple template notation' | |
| 337 assert compact(''' | |
| 338 More {type} template notation | |
| 339 ''', type='readable') == 'More readable template notation' | |
| 340 | |
| 341 def test_compact_empty_lines(self): | |
| 342 """Test :func:`humanfriendly.text.compact_empty_lines()`.""" | |
| 343 # Simple strings pass through untouched. | |
| 344 assert compact_empty_lines('foo') == 'foo' | |
| 345 # Horizontal whitespace remains untouched. | |
| 346 assert compact_empty_lines('\tfoo') == '\tfoo' | |
| 347 # Line breaks should be preserved. | |
| 348 assert compact_empty_lines('foo\nbar') == 'foo\nbar' | |
| 349 # Vertical whitespace should be preserved. | |
| 350 assert compact_empty_lines('foo\n\nbar') == 'foo\n\nbar' | |
| 351 # Vertical whitespace should be compressed. | |
| 352 assert compact_empty_lines('foo\n\n\nbar') == 'foo\n\nbar' | |
| 353 assert compact_empty_lines('foo\n\n\n\nbar') == 'foo\n\nbar' | |
| 354 assert compact_empty_lines('foo\n\n\n\n\nbar') == 'foo\n\nbar' | |
| 355 | |
| 356 def test_dedent(self): | |
| 357 """Test :func:`humanfriendly.text.dedent()`.""" | |
| 358 assert dedent('\n line 1\n line 2\n\n') == 'line 1\n line 2\n' | |
| 359 assert dedent(''' | |
| 360 Dedented, %s text | |
| 361 ''', 'interpolated') == 'Dedented, interpolated text\n' | |
| 362 assert dedent(''' | |
| 363 Dedented, {op} text | |
| 364 ''', op='formatted') == 'Dedented, formatted text\n' | |
| 365 | |
| 366 def test_pluralization(self): | |
| 367 """Test :func:`humanfriendly.text.pluralize()`.""" | |
| 368 assert pluralize(1, 'word') == '1 word' | |
| 369 assert pluralize(2, 'word') == '2 words' | |
| 370 assert pluralize(1, 'box', 'boxes') == '1 box' | |
| 371 assert pluralize(2, 'box', 'boxes') == '2 boxes' | |
| 372 | |
| 373 def test_generate_slug(self): | |
| 374 """Test :func:`humanfriendly.text.generate_slug()`.""" | |
| 375 # Test the basic functionality. | |
| 376 self.assertEqual('some-random-text', generate_slug('Some Random Text!')) | |
| 377 # Test that previous output doesn't change. | |
| 378 self.assertEqual('some-random-text', generate_slug('some-random-text')) | |
| 379 # Test that inputs which can't be converted to a slug raise an exception. | |
| 380 with self.assertRaises(ValueError): | |
| 381 generate_slug(' ') | |
| 382 with self.assertRaises(ValueError): | |
| 383 generate_slug('-') | |
| 384 | |
| 385 def test_boolean_coercion(self): | |
| 386 """Test :func:`humanfriendly.coerce_boolean()`.""" | |
| 387 for value in [True, 'TRUE', 'True', 'true', 'on', 'yes', '1']: | |
| 388 self.assertEqual(True, coerce_boolean(value)) | |
| 389 for value in [False, 'FALSE', 'False', 'false', 'off', 'no', '0']: | |
| 390 self.assertEqual(False, coerce_boolean(value)) | |
| 391 with self.assertRaises(ValueError): | |
| 392 coerce_boolean('not a boolean') | |
| 393 | |
| 394 def test_pattern_coercion(self): | |
| 395 """Test :func:`humanfriendly.coerce_pattern()`.""" | |
| 396 empty_pattern = re.compile('') | |
| 397 # Make sure strings are converted to compiled regular expressions. | |
| 398 assert isinstance(coerce_pattern('foobar'), type(empty_pattern)) | |
| 399 # Make sure compiled regular expressions pass through untouched. | |
| 400 assert empty_pattern is coerce_pattern(empty_pattern) | |
| 401 # Make sure flags are respected. | |
| 402 pattern = coerce_pattern('foobar', re.IGNORECASE) | |
| 403 assert pattern.match('FOOBAR') | |
| 404 # Make sure invalid values raise the expected exception. | |
| 405 with self.assertRaises(ValueError): | |
| 406 coerce_pattern([]) | |
| 407 | |
| 408 def test_format_timespan(self): | |
| 409 """Test :func:`humanfriendly.format_timespan()`.""" | |
| 410 minute = 60 | |
| 411 hour = minute * 60 | |
| 412 day = hour * 24 | |
| 413 week = day * 7 | |
| 414 year = week * 52 | |
| 415 assert '1 nanosecond' == format_timespan(0.000000001, detailed=True) | |
| 416 assert '500 nanoseconds' == format_timespan(0.0000005, detailed=True) | |
| 417 assert '1 microsecond' == format_timespan(0.000001, detailed=True) | |
| 418 assert '500 microseconds' == format_timespan(0.0005, detailed=True) | |
| 419 assert '1 millisecond' == format_timespan(0.001, detailed=True) | |
| 420 assert '500 milliseconds' == format_timespan(0.5, detailed=True) | |
| 421 assert '0.5 seconds' == format_timespan(0.5, detailed=False) | |
| 422 assert '0 seconds' == format_timespan(0) | |
| 423 assert '0.54 seconds' == format_timespan(0.54321) | |
| 424 assert '1 second' == format_timespan(1) | |
| 425 assert '3.14 seconds' == format_timespan(math.pi) | |
| 426 assert '1 minute' == format_timespan(minute) | |
| 427 assert '1 minute and 20 seconds' == format_timespan(80) | |
| 428 assert '2 minutes' == format_timespan(minute * 2) | |
| 429 assert '1 hour' == format_timespan(hour) | |
| 430 assert '2 hours' == format_timespan(hour * 2) | |
| 431 assert '1 day' == format_timespan(day) | |
| 432 assert '2 days' == format_timespan(day * 2) | |
| 433 assert '1 week' == format_timespan(week) | |
| 434 assert '2 weeks' == format_timespan(week * 2) | |
| 435 assert '1 year' == format_timespan(year) | |
| 436 assert '2 years' == format_timespan(year * 2) | |
| 437 assert '6 years, 5 weeks, 4 days, 3 hours, 2 minutes and 500 milliseconds' == \ | |
| 438 format_timespan(year * 6 + week * 5 + day * 4 + hour * 3 + minute * 2 + 0.5, detailed=True) | |
| 439 assert '1 year, 2 weeks and 3 days' == \ | |
| 440 format_timespan(year + week * 2 + day * 3 + hour * 12) | |
| 441 # Make sure milliseconds are never shown separately when detailed=False. | |
| 442 # https://github.com/xolox/python-humanfriendly/issues/10 | |
| 443 assert '1 minute, 1 second and 100 milliseconds' == format_timespan(61.10, detailed=True) | |
| 444 assert '1 minute and 1.1 second' == format_timespan(61.10, detailed=False) | |
| 445 # Test for loss of precision as reported in issue 11: | |
| 446 # https://github.com/xolox/python-humanfriendly/issues/11 | |
| 447 assert '1 minute and 0.3 seconds' == format_timespan(60.300) | |
| 448 assert '5 minutes and 0.3 seconds' == format_timespan(300.300) | |
| 449 assert '1 second and 15 milliseconds' == format_timespan(1.015, detailed=True) | |
| 450 assert '10 seconds and 15 milliseconds' == format_timespan(10.015, detailed=True) | |
| 451 assert '1 microsecond and 50 nanoseconds' == format_timespan(0.00000105, detailed=True) | |
| 452 # Test the datetime.timedelta support: | |
| 453 # https://github.com/xolox/python-humanfriendly/issues/27 | |
| 454 now = datetime.datetime.now() | |
| 455 then = now - datetime.timedelta(hours=23) | |
| 456 assert '23 hours' == format_timespan(now - then) | |
| 457 | |
| 458 def test_parse_timespan(self): | |
| 459 """Test :func:`humanfriendly.parse_timespan()`.""" | |
| 460 self.assertEqual(0, parse_timespan('0')) | |
| 461 self.assertEqual(0, parse_timespan('0s')) | |
| 462 self.assertEqual(0.000000001, parse_timespan('1ns')) | |
| 463 self.assertEqual(0.000000051, parse_timespan('51ns')) | |
| 464 self.assertEqual(0.000001, parse_timespan('1us')) | |
| 465 self.assertEqual(0.000052, parse_timespan('52us')) | |
| 466 self.assertEqual(0.001, parse_timespan('1ms')) | |
| 467 self.assertEqual(0.001, parse_timespan('1 millisecond')) | |
| 468 self.assertEqual(0.5, parse_timespan('500 milliseconds')) | |
| 469 self.assertEqual(0.5, parse_timespan('0.5 seconds')) | |
| 470 self.assertEqual(5, parse_timespan('5s')) | |
| 471 self.assertEqual(5, parse_timespan('5 seconds')) | |
| 472 self.assertEqual(60 * 2, parse_timespan('2m')) | |
| 473 self.assertEqual(60 * 2, parse_timespan('2 minutes')) | |
| 474 self.assertEqual(60 * 3, parse_timespan('3 min')) | |
| 475 self.assertEqual(60 * 3, parse_timespan('3 mins')) | |
| 476 self.assertEqual(60 * 60 * 3, parse_timespan('3 h')) | |
| 477 self.assertEqual(60 * 60 * 3, parse_timespan('3 hours')) | |
| 478 self.assertEqual(60 * 60 * 24 * 4, parse_timespan('4d')) | |
| 479 self.assertEqual(60 * 60 * 24 * 4, parse_timespan('4 days')) | |
| 480 self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 w')) | |
| 481 self.assertEqual(60 * 60 * 24 * 7 * 5, parse_timespan('5 weeks')) | |
| 482 with self.assertRaises(InvalidTimespan): | |
| 483 parse_timespan('1z') | |
| 484 | |
| 485 def test_parse_date(self): | |
| 486 """Test :func:`humanfriendly.parse_date()`.""" | |
| 487 self.assertEqual((2013, 6, 17, 0, 0, 0), parse_date('2013-06-17')) | |
| 488 self.assertEqual((2013, 6, 17, 2, 47, 42), parse_date('2013-06-17 02:47:42')) | |
| 489 self.assertEqual((2016, 11, 30, 0, 47, 17), parse_date(u'2016-11-30 00:47:17')) | |
| 490 with self.assertRaises(InvalidDate): | |
| 491 parse_date('2013-06-XY') | |
| 492 | |
| 493 def test_format_size(self): | |
| 494 """Test :func:`humanfriendly.format_size()`.""" | |
| 495 self.assertEqual('0 bytes', format_size(0)) | |
| 496 self.assertEqual('1 byte', format_size(1)) | |
| 497 self.assertEqual('42 bytes', format_size(42)) | |
| 498 self.assertEqual('1 KB', format_size(1000 ** 1)) | |
| 499 self.assertEqual('1 MB', format_size(1000 ** 2)) | |
| 500 self.assertEqual('1 GB', format_size(1000 ** 3)) | |
| 501 self.assertEqual('1 TB', format_size(1000 ** 4)) | |
| 502 self.assertEqual('1 PB', format_size(1000 ** 5)) | |
| 503 self.assertEqual('1 EB', format_size(1000 ** 6)) | |
| 504 self.assertEqual('1 ZB', format_size(1000 ** 7)) | |
| 505 self.assertEqual('1 YB', format_size(1000 ** 8)) | |
| 506 self.assertEqual('1 KiB', format_size(1024 ** 1, binary=True)) | |
| 507 self.assertEqual('1 MiB', format_size(1024 ** 2, binary=True)) | |
| 508 self.assertEqual('1 GiB', format_size(1024 ** 3, binary=True)) | |
| 509 self.assertEqual('1 TiB', format_size(1024 ** 4, binary=True)) | |
| 510 self.assertEqual('1 PiB', format_size(1024 ** 5, binary=True)) | |
| 511 self.assertEqual('1 EiB', format_size(1024 ** 6, binary=True)) | |
| 512 self.assertEqual('1 ZiB', format_size(1024 ** 7, binary=True)) | |
| 513 self.assertEqual('1 YiB', format_size(1024 ** 8, binary=True)) | |
| 514 self.assertEqual('45 KB', format_size(1000 * 45)) | |
| 515 self.assertEqual('2.9 TB', format_size(1000 ** 4 * 2.9)) | |
| 516 | |
| 517 def test_parse_size(self): | |
| 518 """Test :func:`humanfriendly.parse_size()`.""" | |
| 519 self.assertEqual(0, parse_size('0B')) | |
| 520 self.assertEqual(42, parse_size('42')) | |
| 521 self.assertEqual(42, parse_size('42B')) | |
| 522 self.assertEqual(1000, parse_size('1k')) | |
| 523 self.assertEqual(1024, parse_size('1k', binary=True)) | |
| 524 self.assertEqual(1000, parse_size('1 KB')) | |
| 525 self.assertEqual(1000, parse_size('1 kilobyte')) | |
| 526 self.assertEqual(1024, parse_size('1 kilobyte', binary=True)) | |
| 527 self.assertEqual(1000 ** 2 * 69, parse_size('69 MB')) | |
| 528 self.assertEqual(1000 ** 3, parse_size('1 GB')) | |
| 529 self.assertEqual(1000 ** 4, parse_size('1 TB')) | |
| 530 self.assertEqual(1000 ** 5, parse_size('1 PB')) | |
| 531 self.assertEqual(1000 ** 6, parse_size('1 EB')) | |
| 532 self.assertEqual(1000 ** 7, parse_size('1 ZB')) | |
| 533 self.assertEqual(1000 ** 8, parse_size('1 YB')) | |
| 534 self.assertEqual(1000 ** 3 * 1.5, parse_size('1.5 GB')) | |
| 535 self.assertEqual(1024 ** 8 * 1.5, parse_size('1.5 YiB')) | |
| 536 with self.assertRaises(InvalidSize): | |
| 537 parse_size('1q') | |
| 538 with self.assertRaises(InvalidSize): | |
| 539 parse_size('a') | |
| 540 | |
| 541 def test_format_length(self): | |
| 542 """Test :func:`humanfriendly.format_length()`.""" | |
| 543 self.assertEqual('0 metres', format_length(0)) | |
| 544 self.assertEqual('1 metre', format_length(1)) | |
| 545 self.assertEqual('42 metres', format_length(42)) | |
| 546 self.assertEqual('1 km', format_length(1 * 1000)) | |
| 547 self.assertEqual('15.3 cm', format_length(0.153)) | |
| 548 self.assertEqual('1 cm', format_length(1e-02)) | |
| 549 self.assertEqual('1 mm', format_length(1e-03)) | |
| 550 self.assertEqual('1 nm', format_length(1e-09)) | |
| 551 | |
| 552 def test_parse_length(self): | |
| 553 """Test :func:`humanfriendly.parse_length()`.""" | |
| 554 self.assertEqual(0, parse_length('0m')) | |
| 555 self.assertEqual(42, parse_length('42')) | |
| 556 self.assertEqual(1.5, parse_length('1.5')) | |
| 557 self.assertEqual(42, parse_length('42m')) | |
| 558 self.assertEqual(1000, parse_length('1km')) | |
| 559 self.assertEqual(0.153, parse_length('15.3 cm')) | |
| 560 self.assertEqual(1e-02, parse_length('1cm')) | |
| 561 self.assertEqual(1e-03, parse_length('1mm')) | |
| 562 self.assertEqual(1e-09, parse_length('1nm')) | |
| 563 with self.assertRaises(InvalidLength): | |
| 564 parse_length('1z') | |
| 565 with self.assertRaises(InvalidLength): | |
| 566 parse_length('a') | |
| 567 | |
| 568 def test_format_number(self): | |
| 569 """Test :func:`humanfriendly.format_number()`.""" | |
| 570 self.assertEqual('1', format_number(1)) | |
| 571 self.assertEqual('1.5', format_number(1.5)) | |
| 572 self.assertEqual('1.56', format_number(1.56789)) | |
| 573 self.assertEqual('1.567', format_number(1.56789, 3)) | |
| 574 self.assertEqual('1,000', format_number(1000)) | |
| 575 self.assertEqual('1,000', format_number(1000.12, 0)) | |
| 576 self.assertEqual('1,000,000', format_number(1000000)) | |
| 577 self.assertEqual('1,000,000.42', format_number(1000000.42)) | |
| 578 | |
| 579 def test_round_number(self): | |
| 580 """Test :func:`humanfriendly.round_number()`.""" | |
| 581 self.assertEqual('1', round_number(1)) | |
| 582 self.assertEqual('1', round_number(1.0)) | |
| 583 self.assertEqual('1.00', round_number(1, keep_width=True)) | |
| 584 self.assertEqual('3.14', round_number(3.141592653589793)) | |
| 585 | |
| 586 def test_format_path(self): | |
| 587 """Test :func:`humanfriendly.format_path()`.""" | |
| 588 friendly_path = os.path.join('~', '.vimrc') | |
| 589 absolute_path = os.path.join(os.environ['HOME'], '.vimrc') | |
| 590 self.assertEqual(friendly_path, format_path(absolute_path)) | |
| 591 | |
| 592 def test_parse_path(self): | |
| 593 """Test :func:`humanfriendly.parse_path()`.""" | |
| 594 friendly_path = os.path.join('~', '.vimrc') | |
| 595 absolute_path = os.path.join(os.environ['HOME'], '.vimrc') | |
| 596 self.assertEqual(absolute_path, parse_path(friendly_path)) | |
| 597 | |
| 598 def test_pretty_tables(self): | |
| 599 """Test :func:`humanfriendly.tables.format_pretty_table()`.""" | |
| 600 # The simplest case possible :-). | |
| 601 data = [['Just one column']] | |
| 602 assert format_pretty_table(data) == dedent(""" | |
| 603 ------------------- | |
| 604 | Just one column | | |
| 605 ------------------- | |
| 606 """).strip() | |
| 607 # A bit more complex: two rows, three columns, varying widths. | |
| 608 data = [['One', 'Two', 'Three'], ['1', '2', '3']] | |
| 609 assert format_pretty_table(data) == dedent(""" | |
| 610 --------------------- | |
| 611 | One | Two | Three | | |
| 612 | 1 | 2 | 3 | | |
| 613 --------------------- | |
| 614 """).strip() | |
| 615 # A table including column names. | |
| 616 column_names = ['One', 'Two', 'Three'] | |
| 617 data = [['1', '2', '3'], ['a', 'b', 'c']] | |
| 618 assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" | |
| 619 --------------------- | |
| 620 | One | Two | Three | | |
| 621 --------------------- | |
| 622 | 1 | 2 | 3 | | |
| 623 | a | b | c | | |
| 624 --------------------- | |
| 625 """).strip() | |
| 626 # A table that contains a column with only numeric data (will be right aligned). | |
| 627 column_names = ['Just a label', 'Important numbers'] | |
| 628 data = [['Row one', '15'], ['Row two', '300']] | |
| 629 assert ansi_strip(format_pretty_table(data, column_names)) == dedent(""" | |
| 630 ------------------------------------ | |
| 631 | Just a label | Important numbers | | |
| 632 ------------------------------------ | |
| 633 | Row one | 15 | | |
| 634 | Row two | 300 | | |
| 635 ------------------------------------ | |
| 636 """).strip() | |
| 637 | |
| 638 def test_robust_tables(self): | |
| 639 """Test :func:`humanfriendly.tables.format_robust_table()`.""" | |
| 640 column_names = ['One', 'Two', 'Three'] | |
| 641 data = [['1', '2', '3'], ['a', 'b', 'c']] | |
| 642 assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" | |
| 643 -------- | |
| 644 One: 1 | |
| 645 Two: 2 | |
| 646 Three: 3 | |
| 647 -------- | |
| 648 One: a | |
| 649 Two: b | |
| 650 Three: c | |
| 651 -------- | |
| 652 """).strip() | |
| 653 column_names = ['One', 'Two', 'Three'] | |
| 654 data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] | |
| 655 assert ansi_strip(format_robust_table(data, column_names)) == dedent(""" | |
| 656 ------------------ | |
| 657 One: 1 | |
| 658 Two: 2 | |
| 659 Three: 3 | |
| 660 ------------------ | |
| 661 One: a | |
| 662 Two: b | |
| 663 Three: | |
| 664 Here comes a | |
| 665 multi line column! | |
| 666 ------------------ | |
| 667 """).strip() | |
| 668 | |
| 669 def test_smart_tables(self): | |
| 670 """Test :func:`humanfriendly.tables.format_smart_table()`.""" | |
| 671 column_names = ['One', 'Two', 'Three'] | |
| 672 data = [['1', '2', '3'], ['a', 'b', 'c']] | |
| 673 assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" | |
| 674 --------------------- | |
| 675 | One | Two | Three | | |
| 676 --------------------- | |
| 677 | 1 | 2 | 3 | | |
| 678 | a | b | c | | |
| 679 --------------------- | |
| 680 """).strip() | |
| 681 column_names = ['One', 'Two', 'Three'] | |
| 682 data = [['1', '2', '3'], ['a', 'b', 'Here comes a\nmulti line column!']] | |
| 683 assert ansi_strip(format_smart_table(data, column_names)) == dedent(""" | |
| 684 ------------------ | |
| 685 One: 1 | |
| 686 Two: 2 | |
| 687 Three: 3 | |
| 688 ------------------ | |
| 689 One: a | |
| 690 Two: b | |
| 691 Three: | |
| 692 Here comes a | |
| 693 multi line column! | |
| 694 ------------------ | |
| 695 """).strip() | |
| 696 | |
| 697 def test_rst_tables(self): | |
| 698 """Test :func:`humanfriendly.tables.format_rst_table()`.""" | |
| 699 # Generate a table with column names. | |
| 700 column_names = ['One', 'Two', 'Three'] | |
| 701 data = [['1', '2', '3'], ['a', 'b', 'c']] | |
| 702 self.assertEqual( | |
| 703 format_rst_table(data, column_names), | |
| 704 dedent(""" | |
| 705 === === ===== | |
| 706 One Two Three | |
| 707 === === ===== | |
| 708 1 2 3 | |
| 709 a b c | |
| 710 === === ===== | |
| 711 """).rstrip(), | |
| 712 ) | |
| 713 # Generate a table without column names. | |
| 714 data = [['1', '2', '3'], ['a', 'b', 'c']] | |
| 715 self.assertEqual( | |
| 716 format_rst_table(data), | |
| 717 dedent(""" | |
| 718 = = = | |
| 719 1 2 3 | |
| 720 a b c | |
| 721 = = = | |
| 722 """).rstrip(), | |
| 723 ) | |
| 724 | |
| 725 def test_concatenate(self): | |
| 726 """Test :func:`humanfriendly.text.concatenate()`.""" | |
| 727 assert concatenate([]) == '' | |
| 728 assert concatenate(['one']) == 'one' | |
| 729 assert concatenate(['one', 'two']) == 'one and two' | |
| 730 assert concatenate(['one', 'two', 'three']) == 'one, two and three' | |
| 731 | |
| 732 def test_split(self): | |
| 733 """Test :func:`humanfriendly.text.split()`.""" | |
| 734 from humanfriendly.text import split | |
| 735 self.assertEqual(split(''), []) | |
| 736 self.assertEqual(split('foo'), ['foo']) | |
| 737 self.assertEqual(split('foo, bar'), ['foo', 'bar']) | |
| 738 self.assertEqual(split('foo, bar, baz'), ['foo', 'bar', 'baz']) | |
| 739 self.assertEqual(split('foo,bar,baz'), ['foo', 'bar', 'baz']) | |
| 740 | |
| 741 def test_timer(self): | |
| 742 """Test :func:`humanfriendly.Timer`.""" | |
| 743 for seconds, text in ((1, '1 second'), | |
| 744 (2, '2 seconds'), | |
| 745 (60, '1 minute'), | |
| 746 (60 * 2, '2 minutes'), | |
| 747 (60 * 60, '1 hour'), | |
| 748 (60 * 60 * 2, '2 hours'), | |
| 749 (60 * 60 * 24, '1 day'), | |
| 750 (60 * 60 * 24 * 2, '2 days'), | |
| 751 (60 * 60 * 24 * 7, '1 week'), | |
| 752 (60 * 60 * 24 * 7 * 2, '2 weeks')): | |
| 753 t = Timer(time.time() - seconds) | |
| 754 self.assertEqual(round_number(t.elapsed_time, keep_width=True), '%i.00' % seconds) | |
| 755 self.assertEqual(str(t), text) | |
| 756 # Test rounding to seconds. | |
| 757 t = Timer(time.time() - 2.2) | |
| 758 self.assertEqual(t.rounded, '2 seconds') | |
| 759 # Test automatic timer. | |
| 760 automatic_timer = Timer() | |
| 761 time.sleep(1) | |
| 762 # XXX The following normalize_timestamp(ndigits=0) calls are intended | |
| 763 # to compensate for unreliable clock sources in virtual machines | |
| 764 # like those encountered on Travis CI, see also: | |
| 765 # https://travis-ci.org/xolox/python-humanfriendly/jobs/323944263 | |
| 766 self.assertEqual(normalize_timestamp(automatic_timer.elapsed_time, 0), '1.00') | |
| 767 # Test resumable timer. | |
| 768 resumable_timer = Timer(resumable=True) | |
| 769 for i in range(2): | |
| 770 with resumable_timer: | |
| 771 time.sleep(1) | |
| 772 self.assertEqual(normalize_timestamp(resumable_timer.elapsed_time, 0), '2.00') | |
| 773 # Make sure Timer.__enter__() returns the timer object. | |
| 774 with Timer(resumable=True) as timer: | |
| 775 assert timer is not None | |
| 776 | |
| 777 def test_spinner(self): | |
| 778 """Test :func:`humanfriendly.Spinner`.""" | |
| 779 stream = StringIO() | |
| 780 spinner = Spinner(label='test spinner', total=4, stream=stream, interactive=True) | |
| 781 for progress in [1, 2, 3, 4]: | |
| 782 spinner.step(progress=progress) | |
| 783 time.sleep(0.2) | |
| 784 spinner.clear() | |
| 785 output = stream.getvalue() | |
| 786 output = (output.replace(ANSI_SHOW_CURSOR, '') | |
| 787 .replace(ANSI_HIDE_CURSOR, '')) | |
| 788 lines = [line for line in output.split(ANSI_ERASE_LINE) if line] | |
| 789 self.assertTrue(len(lines) > 0) | |
| 790 self.assertTrue(all('test spinner' in l for l in lines)) | |
| 791 self.assertTrue(all('%' in l for l in lines)) | |
| 792 self.assertEqual(sorted(set(lines)), sorted(lines)) | |
| 793 | |
| 794 def test_automatic_spinner(self): | |
| 795 """ | |
| 796 Test :func:`humanfriendly.AutomaticSpinner`. | |
| 797 | |
| 798 There's not a lot to test about the :class:`.AutomaticSpinner` class, | |
| 799 but by at least running it here we are assured that the code functions | |
| 800 on all supported Python versions. :class:`.AutomaticSpinner` is built | |
| 801 on top of the :class:`.Spinner` class so at least we also have the | |
| 802 tests for the :class:`.Spinner` class to back us up. | |
| 803 """ | |
| 804 with AutomaticSpinner(label='test spinner'): | |
| 805 time.sleep(1) | |
| 806 | |
| 807 def test_prompt_for_choice(self): | |
| 808 """Test :func:`humanfriendly.prompts.prompt_for_choice()`.""" | |
| 809 # Choice selection without any options should raise an exception. | |
| 810 with self.assertRaises(ValueError): | |
| 811 prompt_for_choice([]) | |
| 812 # If there's only one option no prompt should be rendered so we expect | |
| 813 # the following code to not raise an EOFError exception (despite | |
| 814 # connecting standard input to /dev/null). | |
| 815 with open(os.devnull) as handle: | |
| 816 with PatchedAttribute(sys, 'stdin', handle): | |
| 817 only_option = 'only one option (shortcut)' | |
| 818 assert prompt_for_choice([only_option]) == only_option | |
| 819 # Choice selection by full string match. | |
| 820 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'foo'): | |
| 821 assert prompt_for_choice(['foo', 'bar']) == 'foo' | |
| 822 # Choice selection by substring input. | |
| 823 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'f'): | |
| 824 assert prompt_for_choice(['foo', 'bar']) == 'foo' | |
| 825 # Choice selection by number. | |
| 826 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: '2'): | |
| 827 assert prompt_for_choice(['foo', 'bar']) == 'bar' | |
| 828 # Choice selection by going with the default. | |
| 829 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): | |
| 830 assert prompt_for_choice(['foo', 'bar'], default='bar') == 'bar' | |
| 831 # Invalid substrings are refused. | |
| 832 replies = ['', 'q', 'z'] | |
| 833 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): | |
| 834 assert prompt_for_choice(['foo', 'bar', 'baz']) == 'baz' | |
| 835 # Choice selection by substring input requires an unambiguous substring match. | |
| 836 replies = ['a', 'q'] | |
| 837 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): | |
| 838 assert prompt_for_choice(['foo', 'bar', 'baz', 'qux']) == 'qux' | |
| 839 # Invalid numbers are refused. | |
| 840 replies = ['42', '2'] | |
| 841 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): | |
| 842 assert prompt_for_choice(['foo', 'bar', 'baz']) == 'bar' | |
| 843 # Test that interactive prompts eventually give up on invalid replies. | |
| 844 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): | |
| 845 with self.assertRaises(TooManyInvalidReplies): | |
| 846 prompt_for_choice(['a', 'b', 'c']) | |
| 847 | |
| 848 def test_prompt_for_confirmation(self): | |
| 849 """Test :func:`humanfriendly.prompts.prompt_for_confirmation()`.""" | |
| 850 # Test some (more or less) reasonable replies that indicate agreement. | |
| 851 for reply in 'yes', 'Yes', 'YES', 'y', 'Y': | |
| 852 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply): | |
| 853 assert prompt_for_confirmation("Are you sure?") is True | |
| 854 # Test some (more or less) reasonable replies that indicate disagreement. | |
| 855 for reply in 'no', 'No', 'NO', 'n', 'N': | |
| 856 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: reply): | |
| 857 assert prompt_for_confirmation("Are you sure?") is False | |
| 858 # Test that empty replies select the default choice. | |
| 859 for default_choice in True, False: | |
| 860 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): | |
| 861 assert prompt_for_confirmation("Are you sure?", default=default_choice) is default_choice | |
| 862 # Test that a warning is shown when no input nor a default is given. | |
| 863 replies = ['', 'y'] | |
| 864 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: replies.pop(0)): | |
| 865 with CaptureOutput(merged=True) as capturer: | |
| 866 assert prompt_for_confirmation("Are you sure?") is True | |
| 867 assert "there's no default choice" in capturer.get_text() | |
| 868 # Test that the default reply is shown in uppercase. | |
| 869 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: 'y'): | |
| 870 for default_value, expected_text in (True, 'Y/n'), (False, 'y/N'), (None, 'y/n'): | |
| 871 with CaptureOutput(merged=True) as capturer: | |
| 872 assert prompt_for_confirmation("Are you sure?", default=default_value) is True | |
| 873 assert expected_text in capturer.get_text() | |
| 874 # Test that interactive prompts eventually give up on invalid replies. | |
| 875 with PatchedAttribute(prompts, 'interactive_prompt', lambda p: ''): | |
| 876 with self.assertRaises(TooManyInvalidReplies): | |
| 877 prompt_for_confirmation("Are you sure?") | |
| 878 | |
| 879 def test_prompt_for_input(self): | |
| 880 """Test :func:`humanfriendly.prompts.prompt_for_input()`.""" | |
| 881 with open(os.devnull) as handle: | |
| 882 with PatchedAttribute(sys, 'stdin', handle): | |
| 883 # If standard input isn't connected to a terminal the default value should be returned. | |
| 884 default_value = "To seek the holy grail!" | |
| 885 assert prompt_for_input("What is your quest?", default=default_value) == default_value | |
| 886 # If standard input isn't connected to a terminal and no default value | |
| 887 # is given the EOFError exception should be propagated to the caller. | |
| 888 with self.assertRaises(EOFError): | |
| 889 prompt_for_input("What is your favorite color?") | |
| 890 | |
| 891 def test_cli(self): | |
| 892 """Test the command line interface.""" | |
| 893 # Test that the usage message is printed by default. | |
| 894 returncode, output = run_cli(main) | |
| 895 assert 'Usage:' in output | |
| 896 # Test that the usage message can be requested explicitly. | |
| 897 returncode, output = run_cli(main, '--help') | |
| 898 assert 'Usage:' in output | |
| 899 # Test handling of invalid command line options. | |
| 900 returncode, output = run_cli(main, '--unsupported-option') | |
| 901 assert returncode != 0 | |
| 902 # Test `humanfriendly --format-number'. | |
| 903 returncode, output = run_cli(main, '--format-number=1234567') | |
| 904 assert output.strip() == '1,234,567' | |
| 905 # Test `humanfriendly --format-size'. | |
| 906 random_byte_count = random.randint(1024, 1024 * 1024) | |
| 907 returncode, output = run_cli(main, '--format-size=%i' % random_byte_count) | |
| 908 assert output.strip() == format_size(random_byte_count) | |
| 909 # Test `humanfriendly --format-size --binary'. | |
| 910 random_byte_count = random.randint(1024, 1024 * 1024) | |
| 911 returncode, output = run_cli(main, '--format-size=%i' % random_byte_count, '--binary') | |
| 912 assert output.strip() == format_size(random_byte_count, binary=True) | |
| 913 # Test `humanfriendly --format-length'. | |
| 914 random_len = random.randint(1024, 1024 * 1024) | |
| 915 returncode, output = run_cli(main, '--format-length=%i' % random_len) | |
| 916 assert output.strip() == format_length(random_len) | |
| 917 random_len = float(random_len) / 12345.6 | |
| 918 returncode, output = run_cli(main, '--format-length=%f' % random_len) | |
| 919 assert output.strip() == format_length(random_len) | |
| 920 # Test `humanfriendly --format-table'. | |
| 921 returncode, output = run_cli(main, '--format-table', '--delimiter=\t', input='1\t2\t3\n4\t5\t6\n7\t8\t9') | |
| 922 assert output.strip() == dedent(''' | |
| 923 ------------- | |
| 924 | 1 | 2 | 3 | | |
| 925 | 4 | 5 | 6 | | |
| 926 | 7 | 8 | 9 | | |
| 927 ------------- | |
| 928 ''').strip() | |
| 929 # Test `humanfriendly --format-timespan'. | |
| 930 random_timespan = random.randint(5, 600) | |
| 931 returncode, output = run_cli(main, '--format-timespan=%i' % random_timespan) | |
| 932 assert output.strip() == format_timespan(random_timespan) | |
| 933 # Test `humanfriendly --parse-size'. | |
| 934 returncode, output = run_cli(main, '--parse-size=5 KB') | |
| 935 assert int(output) == parse_size('5 KB') | |
| 936 # Test `humanfriendly --parse-size'. | |
| 937 returncode, output = run_cli(main, '--parse-size=5 YiB') | |
| 938 assert int(output) == parse_size('5 YB', binary=True) | |
| 939 # Test `humanfriendly --parse-length'. | |
| 940 returncode, output = run_cli(main, '--parse-length=5 km') | |
| 941 assert int(output) == parse_length('5 km') | |
| 942 returncode, output = run_cli(main, '--parse-length=1.05 km') | |
| 943 assert float(output) == parse_length('1.05 km') | |
| 944 # Test `humanfriendly --run-command'. | |
| 945 returncode, output = run_cli(main, '--run-command', 'bash', '-c', 'sleep 2 && exit 42') | |
| 946 assert returncode == 42 | |
| 947 # Test `humanfriendly --demo'. The purpose of this test is | |
| 948 # to ensure that the demo runs successfully on all versions | |
| 949 # of Python and outputs the expected sections (recognized by | |
| 950 # their headings) without triggering exceptions. This was | |
| 951 # written as a regression test after issue #28 was reported: | |
| 952 # https://github.com/xolox/python-humanfriendly/issues/28 | |
| 953 returncode, output = run_cli(main, '--demo') | |
| 954 assert returncode == 0 | |
| 955 lines = [ansi_strip(l) for l in output.splitlines()] | |
| 956 assert "Text styles:" in lines | |
| 957 assert "Foreground colors:" in lines | |
| 958 assert "Background colors:" in lines | |
| 959 assert "256 color mode (standard colors):" in lines | |
| 960 assert "256 color mode (high-intensity colors):" in lines | |
| 961 assert "256 color mode (216 colors):" in lines | |
| 962 assert "256 color mode (gray scale colors):" in lines | |
| 963 | |
| 964 def test_ansi_style(self): | |
| 965 """Test :func:`humanfriendly.terminal.ansi_style()`.""" | |
| 966 assert ansi_style(bold=True) == '%s1%s' % (ANSI_CSI, ANSI_SGR) | |
| 967 assert ansi_style(faint=True) == '%s2%s' % (ANSI_CSI, ANSI_SGR) | |
| 968 assert ansi_style(italic=True) == '%s3%s' % (ANSI_CSI, ANSI_SGR) | |
| 969 assert ansi_style(underline=True) == '%s4%s' % (ANSI_CSI, ANSI_SGR) | |
| 970 assert ansi_style(inverse=True) == '%s7%s' % (ANSI_CSI, ANSI_SGR) | |
| 971 assert ansi_style(strike_through=True) == '%s9%s' % (ANSI_CSI, ANSI_SGR) | |
| 972 assert ansi_style(color='blue') == '%s34%s' % (ANSI_CSI, ANSI_SGR) | |
| 973 assert ansi_style(background='blue') == '%s44%s' % (ANSI_CSI, ANSI_SGR) | |
| 974 assert ansi_style(color='blue', bright=True) == '%s94%s' % (ANSI_CSI, ANSI_SGR) | |
| 975 assert ansi_style(color=214) == '%s38;5;214%s' % (ANSI_CSI, ANSI_SGR) | |
| 976 assert ansi_style(background=214) == '%s39;5;214%s' % (ANSI_CSI, ANSI_SGR) | |
| 977 assert ansi_style(color=(0, 0, 0)) == '%s38;2;0;0;0%s' % (ANSI_CSI, ANSI_SGR) | |
| 978 assert ansi_style(color=(255, 255, 255)) == '%s38;2;255;255;255%s' % (ANSI_CSI, ANSI_SGR) | |
| 979 assert ansi_style(background=(50, 100, 150)) == '%s48;2;50;100;150%s' % (ANSI_CSI, ANSI_SGR) | |
| 980 with self.assertRaises(ValueError): | |
| 981 ansi_style(color='unknown') | |
| 982 | |
| 983 def test_ansi_width(self): | |
| 984 """Test :func:`humanfriendly.terminal.ansi_width()`.""" | |
| 985 text = "Whatever" | |
| 986 # Make sure ansi_width() works as expected on strings without ANSI escape sequences. | |
| 987 assert len(text) == ansi_width(text) | |
| 988 # Wrap a text in ANSI escape sequences and make sure ansi_width() treats it as expected. | |
| 989 wrapped = ansi_wrap(text, bold=True) | |
| 990 # Make sure ansi_wrap() changed the text. | |
| 991 assert wrapped != text | |
| 992 # Make sure ansi_wrap() added additional bytes. | |
| 993 assert len(wrapped) > len(text) | |
| 994 # Make sure the result of ansi_width() stays the same. | |
| 995 assert len(text) == ansi_width(wrapped) | |
| 996 | |
| 997 def test_ansi_wrap(self): | |
| 998 """Test :func:`humanfriendly.terminal.ansi_wrap()`.""" | |
| 999 text = "Whatever" | |
| 1000 # Make sure ansi_wrap() does nothing when no keyword arguments are given. | |
| 1001 assert text == ansi_wrap(text) | |
| 1002 # Make sure ansi_wrap() starts the text with the CSI sequence. | |
| 1003 assert ansi_wrap(text, bold=True).startswith(ANSI_CSI) | |
| 1004 # Make sure ansi_wrap() ends the text by resetting the ANSI styles. | |
| 1005 assert ansi_wrap(text, bold=True).endswith(ANSI_RESET) | |
| 1006 | |
| 1007 def test_html_to_ansi(self): | |
| 1008 """Test the :func:`humanfriendly.terminal.html_to_ansi()` function.""" | |
| 1009 assert html_to_ansi("Just some plain text") == "Just some plain text" | |
| 1010 # Hyperlinks. | |
| 1011 assert html_to_ansi('<a href="https://python.org">python.org</a>') == \ | |
| 1012 '\x1b[0m\x1b[4;94mpython.org\x1b[0m (\x1b[0m\x1b[4;94mhttps://python.org\x1b[0m)' | |
| 1013 # Make sure `mailto:' prefixes are stripped (they're not at all useful in a terminal). | |
| 1014 assert html_to_ansi('<a href="mailto:peter@peterodding.com">peter@peterodding.com</a>') == \ | |
| 1015 '\x1b[0m\x1b[4;94mpeter@peterodding.com\x1b[0m' | |
| 1016 # Bold text. | |
| 1017 assert html_to_ansi("Let's try <b>bold</b>") == "Let's try \x1b[0m\x1b[1mbold\x1b[0m" | |
| 1018 assert html_to_ansi("Let's try <span style=\"font-weight: bold\">bold</span>") == \ | |
| 1019 "Let's try \x1b[0m\x1b[1mbold\x1b[0m" | |
| 1020 # Italic text. | |
| 1021 assert html_to_ansi("Let's try <i>italic</i>") == \ | |
| 1022 "Let's try \x1b[0m\x1b[3mitalic\x1b[0m" | |
| 1023 assert html_to_ansi("Let's try <span style=\"font-style: italic\">italic</span>") == \ | |
| 1024 "Let's try \x1b[0m\x1b[3mitalic\x1b[0m" | |
| 1025 # Underlined text. | |
| 1026 assert html_to_ansi("Let's try <ins>underline</ins>") == \ | |
| 1027 "Let's try \x1b[0m\x1b[4munderline\x1b[0m" | |
| 1028 assert html_to_ansi("Let's try <span style=\"text-decoration: underline\">underline</span>") == \ | |
| 1029 "Let's try \x1b[0m\x1b[4munderline\x1b[0m" | |
| 1030 # Strike-through text. | |
| 1031 assert html_to_ansi("Let's try <s>strike-through</s>") == \ | |
| 1032 "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m" | |
| 1033 assert html_to_ansi("Let's try <span style=\"text-decoration: line-through\">strike-through</span>") == \ | |
| 1034 "Let's try \x1b[0m\x1b[9mstrike-through\x1b[0m" | |
| 1035 # Pre-formatted text. | |
| 1036 assert html_to_ansi("Let's try <code>pre-formatted</code>") == \ | |
| 1037 "Let's try \x1b[0m\x1b[33mpre-formatted\x1b[0m" | |
| 1038 # Text colors (with a 6 digit hexadecimal color value). | |
| 1039 assert html_to_ansi("Let's try <span style=\"color: #AABBCC\">text colors</s>") == \ | |
| 1040 "Let's try \x1b[0m\x1b[38;2;170;187;204mtext colors\x1b[0m" | |
| 1041 # Background colors (with an rgb(N, N, N) expression). | |
| 1042 assert html_to_ansi("Let's try <span style=\"background-color: rgb(50, 50, 50)\">background colors</s>") == \ | |
| 1043 "Let's try \x1b[0m\x1b[48;2;50;50;50mbackground colors\x1b[0m" | |
| 1044 # Line breaks. | |
| 1045 assert html_to_ansi("Let's try some<br>line<br>breaks") == \ | |
| 1046 "Let's try some\nline\nbreaks" | |
| 1047 # Check that decimal entities are decoded. | |
| 1048 assert html_to_ansi("&") == "&" | |
| 1049 # Check that named entities are decoded. | |
| 1050 assert html_to_ansi("&") == "&" | |
| 1051 assert html_to_ansi(">") == ">" | |
| 1052 assert html_to_ansi("<") == "<" | |
| 1053 # Check that hexadecimal entities are decoded. | |
| 1054 assert html_to_ansi("&") == "&" | |
| 1055 # Check that the text callback is actually called. | |
| 1056 | |
| 1057 def callback(text): | |
| 1058 return text.replace(':wink:', ';-)') | |
| 1059 | |
| 1060 assert ':wink:' not in html_to_ansi('<b>:wink:</b>', callback=callback) | |
| 1061 # Check that the text callback doesn't process preformatted text. | |
| 1062 assert ':wink:' in html_to_ansi('<code>:wink:</code>', callback=callback) | |
| 1063 # Try a somewhat convoluted but nevertheless real life example from my | |
| 1064 # personal chat archives that causes humanfriendly releases 4.15 and | |
| 1065 # 4.15.1 to raise an exception. | |
| 1066 assert html_to_ansi(u''' | |
| 1067 Tweakers zit er idd nog steeds:<br><br> | |
| 1068 peter@peter-work> curl -s <a href="tweakers.net">tweakers.net</a> | grep -i hosting<br> | |
| 1069 <a href="<a href="http://www.true.nl/webhosting/">http://www.true.nl/webhosting/</a>" | |
| 1070 rel="external" id="true" title="Hosting door True"></a><br> | |
| 1071 Hosting door <a href="<a href="http://www.true.nl/vps/">http://www.true.nl/vps/</a>" | |
| 1072 title="VPS hosting" rel="external">True</a> | |
| 1073 ''') | |
| 1074 | |
| 1075 def test_generate_output(self): | |
| 1076 """Test the :func:`humanfriendly.terminal.output()` function.""" | |
| 1077 text = "Standard output generated by output()" | |
| 1078 with CaptureOutput(merged=False) as capturer: | |
| 1079 output(text) | |
| 1080 self.assertEqual([text], capturer.stdout.get_lines()) | |
| 1081 self.assertEqual([], capturer.stderr.get_lines()) | |
| 1082 | |
| 1083 def test_generate_message(self): | |
| 1084 """Test the :func:`humanfriendly.terminal.message()` function.""" | |
| 1085 text = "Standard error generated by message()" | |
| 1086 with CaptureOutput(merged=False) as capturer: | |
| 1087 message(text) | |
| 1088 self.assertEqual([], capturer.stdout.get_lines()) | |
| 1089 self.assertEqual([text], capturer.stderr.get_lines()) | |
| 1090 | |
| 1091 def test_generate_warning(self): | |
| 1092 """Test the :func:`humanfriendly.terminal.warning()` function.""" | |
| 1093 from capturer import CaptureOutput | |
| 1094 text = "Standard error generated by warning()" | |
| 1095 with CaptureOutput(merged=False) as capturer: | |
| 1096 warning(text) | |
| 1097 self.assertEqual([], capturer.stdout.get_lines()) | |
| 1098 self.assertEqual([ansi_wrap(text, color='red')], self.ignore_coverage_warning(capturer.stderr)) | |
| 1099 | |
| 1100 def ignore_coverage_warning(self, stream): | |
| 1101 """ | |
| 1102 Filter out coverage.py warning from standard error. | |
| 1103 | |
| 1104 This is intended to remove the following line from the lines captured | |
| 1105 on the standard error stream: | |
| 1106 | |
| 1107 Coverage.py warning: No data was collected. (no-data-collected) | |
| 1108 """ | |
| 1109 return [line for line in stream.get_lines() if 'no-data-collected' not in line] | |
| 1110 | |
| 1111 def test_clean_output(self): | |
| 1112 """Test :func:`humanfriendly.terminal.clean_terminal_output()`.""" | |
| 1113 # Simple output should pass through unharmed (single line). | |
| 1114 assert clean_terminal_output('foo') == ['foo'] | |
| 1115 # Simple output should pass through unharmed (multiple lines). | |
| 1116 assert clean_terminal_output('foo\nbar') == ['foo', 'bar'] | |
| 1117 # Carriage returns and preceding substrings are removed. | |
| 1118 assert clean_terminal_output('foo\rbar\nbaz') == ['bar', 'baz'] | |
| 1119 # Carriage returns move the cursor to the start of the line without erasing text. | |
| 1120 assert clean_terminal_output('aaa\rab') == ['aba'] | |
| 1121 # Backspace moves the cursor one position back without erasing text. | |
| 1122 assert clean_terminal_output('aaa\b\bb') == ['aba'] | |
| 1123 # Trailing empty lines should be stripped. | |
| 1124 assert clean_terminal_output('foo\nbar\nbaz\n\n\n') == ['foo', 'bar', 'baz'] | |
| 1125 | |
| 1126 def test_find_terminal_size(self): | |
| 1127 """Test :func:`humanfriendly.terminal.find_terminal_size()`.""" | |
| 1128 lines, columns = find_terminal_size() | |
| 1129 # We really can't assert any minimum or maximum values here because it | |
| 1130 # simply doesn't make any sense; it's impossible for me to anticipate | |
| 1131 # on what environments this test suite will run in the future. | |
| 1132 assert lines > 0 | |
| 1133 assert columns > 0 | |
| 1134 # The find_terminal_size_using_ioctl() function is the default | |
| 1135 # implementation and it will likely work fine. This makes it hard to | |
| 1136 # test the fall back code paths though. However there's an easy way to | |
| 1137 # make find_terminal_size_using_ioctl() fail ... | |
| 1138 saved_stdin = sys.stdin | |
| 1139 saved_stdout = sys.stdout | |
| 1140 saved_stderr = sys.stderr | |
| 1141 try: | |
| 1142 # What do you mean this is brute force?! ;-) | |
| 1143 sys.stdin = StringIO() | |
| 1144 sys.stdout = StringIO() | |
| 1145 sys.stderr = StringIO() | |
| 1146 # Now find_terminal_size_using_ioctl() should fail even though | |
| 1147 # find_terminal_size_using_stty() might work fine. | |
| 1148 lines, columns = find_terminal_size() | |
| 1149 assert lines > 0 | |
| 1150 assert columns > 0 | |
| 1151 # There's also an ugly way to make `stty size' fail: The | |
| 1152 # subprocess.Popen class uses os.execvp() underneath, so if we | |
| 1153 # clear the $PATH it will break. | |
| 1154 saved_path = os.environ['PATH'] | |
| 1155 try: | |
| 1156 os.environ['PATH'] = '' | |
| 1157 # Now find_terminal_size_using_stty() should fail. | |
| 1158 lines, columns = find_terminal_size() | |
| 1159 assert lines > 0 | |
| 1160 assert columns > 0 | |
| 1161 finally: | |
| 1162 os.environ['PATH'] = saved_path | |
| 1163 finally: | |
| 1164 sys.stdin = saved_stdin | |
| 1165 sys.stdout = saved_stdout | |
| 1166 sys.stderr = saved_stderr | |
| 1167 | |
| 1168 def test_terminal_capabilities(self): | |
| 1169 """Test the functions that check for terminal capabilities.""" | |
| 1170 from capturer import CaptureOutput | |
| 1171 for test_stream in connected_to_terminal, terminal_supports_colors: | |
| 1172 # This test suite should be able to run interactively as well as | |
| 1173 # non-interactively, so we can't expect or demand that standard streams | |
| 1174 # will always be connected to a terminal. Fortunately Capturer enables | |
| 1175 # us to fake it :-). | |
| 1176 for stream in sys.stdout, sys.stderr: | |
| 1177 with CaptureOutput(): | |
| 1178 assert test_stream(stream) | |
| 1179 # Test something that we know can never be a terminal. | |
| 1180 with open(os.devnull) as handle: | |
| 1181 assert not test_stream(handle) | |
| 1182 # Verify that objects without isatty() don't raise an exception. | |
| 1183 assert not test_stream(object()) | |
| 1184 | |
| 1185 def test_show_pager(self): | |
| 1186 """Test :func:`humanfriendly.terminal.show_pager()`.""" | |
| 1187 original_pager = os.environ.get('PAGER', None) | |
| 1188 try: | |
| 1189 # We specifically avoid `less' because it would become awkward to | |
| 1190 # run the test suite in an interactive terminal :-). | |
| 1191 os.environ['PAGER'] = 'cat' | |
| 1192 # Generate a significant amount of random text spread over multiple | |
| 1193 # lines that we expect to be reported literally on the terminal. | |
| 1194 random_text = "\n".join(random_string(25) for i in range(50)) | |
| 1195 # Run the pager command and validate the output. | |
| 1196 with CaptureOutput() as capturer: | |
| 1197 show_pager(random_text) | |
| 1198 assert random_text in capturer.get_text() | |
| 1199 finally: | |
| 1200 if original_pager is not None: | |
| 1201 # Restore the original $PAGER value. | |
| 1202 os.environ['PAGER'] = original_pager | |
| 1203 else: | |
| 1204 # Clear the custom $PAGER value. | |
| 1205 os.environ.pop('PAGER') | |
| 1206 | |
| 1207 def test_get_pager_command(self): | |
| 1208 """Test :func:`humanfriendly.terminal.get_pager_command()`.""" | |
| 1209 # Make sure --RAW-CONTROL-CHARS isn't used when it's not needed. | |
| 1210 assert '--RAW-CONTROL-CHARS' not in get_pager_command("Usage message") | |
| 1211 # Make sure --RAW-CONTROL-CHARS is used when it's needed. | |
| 1212 assert '--RAW-CONTROL-CHARS' in get_pager_command(ansi_wrap("Usage message", bold=True)) | |
| 1213 # Make sure that less-specific options are only used when valid. | |
| 1214 options_specific_to_less = ['--no-init', '--quit-if-one-screen'] | |
| 1215 for pager in 'cat', 'less': | |
| 1216 original_pager = os.environ.get('PAGER', None) | |
| 1217 try: | |
| 1218 # Set $PAGER to `cat' or `less'. | |
| 1219 os.environ['PAGER'] = pager | |
| 1220 # Get the pager command line. | |
| 1221 command_line = get_pager_command() | |
| 1222 # Check for less-specific options. | |
| 1223 if pager == 'less': | |
| 1224 assert all(opt in command_line for opt in options_specific_to_less) | |
| 1225 else: | |
| 1226 assert not any(opt in command_line for opt in options_specific_to_less) | |
| 1227 finally: | |
| 1228 if original_pager is not None: | |
| 1229 # Restore the original $PAGER value. | |
| 1230 os.environ['PAGER'] = original_pager | |
| 1231 else: | |
| 1232 # Clear the custom $PAGER value. | |
| 1233 os.environ.pop('PAGER') | |
| 1234 | |
| 1235 def test_find_meta_variables(self): | |
| 1236 """Test :func:`humanfriendly.usage.find_meta_variables()`.""" | |
| 1237 assert sorted(find_meta_variables(""" | |
| 1238 Here's one example: --format-number=VALUE | |
| 1239 Here's another example: --format-size=BYTES | |
| 1240 A final example: --format-timespan=SECONDS | |
| 1241 This line doesn't contain a META variable. | |
| 1242 """)) == sorted(['VALUE', 'BYTES', 'SECONDS']) | |
| 1243 | |
| 1244 def test_parse_usage_simple(self): | |
| 1245 """Test :func:`humanfriendly.usage.parse_usage()` (a simple case).""" | |
| 1246 introduction, options = self.preprocess_parse_result(""" | |
| 1247 Usage: my-fancy-app [OPTIONS] | |
| 1248 | |
| 1249 Boring description. | |
| 1250 | |
| 1251 Supported options: | |
| 1252 | |
| 1253 -h, --help | |
| 1254 | |
| 1255 Show this message and exit. | |
| 1256 """) | |
| 1257 # The following fragments are (expected to be) part of the introduction. | |
| 1258 assert "Usage: my-fancy-app [OPTIONS]" in introduction | |
| 1259 assert "Boring description." in introduction | |
| 1260 assert "Supported options:" in introduction | |
| 1261 # The following fragments are (expected to be) part of the documented options. | |
| 1262 assert "-h, --help" in options | |
| 1263 assert "Show this message and exit." in options | |
| 1264 | |
| 1265 def test_parse_usage_tricky(self): | |
| 1266 """Test :func:`humanfriendly.usage.parse_usage()` (a tricky case).""" | |
| 1267 introduction, options = self.preprocess_parse_result(""" | |
| 1268 Usage: my-fancy-app [OPTIONS] | |
| 1269 | |
| 1270 Here's the introduction to my-fancy-app. Some of the lines in the | |
| 1271 introduction start with a command line option just to confuse the | |
| 1272 parsing algorithm :-) | |
| 1273 | |
| 1274 For example | |
| 1275 --an-awesome-option | |
| 1276 is still part of the introduction. | |
| 1277 | |
| 1278 Supported options: | |
| 1279 | |
| 1280 -a, --an-awesome-option | |
| 1281 | |
| 1282 Explanation why this is an awesome option. | |
| 1283 | |
| 1284 -b, --a-boring-option | |
| 1285 | |
| 1286 Explanation why this is a boring option. | |
| 1287 """) | |
| 1288 # The following fragments are (expected to be) part of the introduction. | |
| 1289 assert "Usage: my-fancy-app [OPTIONS]" in introduction | |
| 1290 assert any('still part of the introduction' in p for p in introduction) | |
| 1291 assert "Supported options:" in introduction | |
| 1292 # The following fragments are (expected to be) part of the documented options. | |
| 1293 assert "-a, --an-awesome-option" in options | |
| 1294 assert "Explanation why this is an awesome option." in options | |
| 1295 assert "-b, --a-boring-option" in options | |
| 1296 assert "Explanation why this is a boring option." in options | |
| 1297 | |
| 1298 def test_parse_usage_commas(self): | |
| 1299 """Test :func:`humanfriendly.usage.parse_usage()` against option labels containing commas.""" | |
| 1300 introduction, options = self.preprocess_parse_result(""" | |
| 1301 Usage: my-fancy-app [OPTIONS] | |
| 1302 | |
| 1303 Some introduction goes here. | |
| 1304 | |
| 1305 Supported options: | |
| 1306 | |
| 1307 -f, --first-option | |
| 1308 | |
| 1309 Explanation of first option. | |
| 1310 | |
| 1311 -s, --second-option=WITH,COMMA | |
| 1312 | |
| 1313 This should be a separate option's description. | |
| 1314 """) | |
| 1315 # The following fragments are (expected to be) part of the introduction. | |
| 1316 assert "Usage: my-fancy-app [OPTIONS]" in introduction | |
| 1317 assert "Some introduction goes here." in introduction | |
| 1318 assert "Supported options:" in introduction | |
| 1319 # The following fragments are (expected to be) part of the documented options. | |
| 1320 assert "-f, --first-option" in options | |
| 1321 assert "Explanation of first option." in options | |
| 1322 assert "-s, --second-option=WITH,COMMA" in options | |
| 1323 assert "This should be a separate option's description." in options | |
| 1324 | |
| 1325 def preprocess_parse_result(self, text): | |
| 1326 """Ignore leading/trailing whitespace in usage parsing tests.""" | |
| 1327 return tuple([p.strip() for p in r] for r in parse_usage(dedent(text))) | |
| 1328 | |
| 1329 def test_format_usage(self): | |
| 1330 """Test :func:`humanfriendly.usage.format_usage()`.""" | |
| 1331 # Test that options are highlighted. | |
| 1332 usage_text = "Just one --option" | |
| 1333 formatted_text = format_usage(usage_text) | |
| 1334 assert len(formatted_text) > len(usage_text) | |
| 1335 assert formatted_text.startswith("Just one ") | |
| 1336 # Test that the "Usage: ..." line is highlighted. | |
| 1337 usage_text = "Usage: humanfriendly [OPTIONS]" | |
| 1338 formatted_text = format_usage(usage_text) | |
| 1339 assert len(formatted_text) > len(usage_text) | |
| 1340 assert usage_text in formatted_text | |
| 1341 assert not formatted_text.startswith(usage_text) | |
| 1342 # Test that meta variables aren't erroneously highlighted. | |
| 1343 usage_text = ( | |
| 1344 "--valid-option=VALID_METAVAR\n" | |
| 1345 "VALID_METAVAR is bogus\n" | |
| 1346 "INVALID_METAVAR should not be highlighted\n" | |
| 1347 ) | |
| 1348 formatted_text = format_usage(usage_text) | |
| 1349 formatted_lines = formatted_text.splitlines() | |
| 1350 # Make sure the meta variable in the second line is highlighted. | |
| 1351 assert ANSI_CSI in formatted_lines[1] | |
| 1352 # Make sure the meta variable in the third line isn't highlighted. | |
| 1353 assert ANSI_CSI not in formatted_lines[2] | |
| 1354 | |
| 1355 def test_render_usage(self): | |
| 1356 """Test :func:`humanfriendly.usage.render_usage()`.""" | |
| 1357 assert render_usage("Usage: some-command WITH ARGS") == "**Usage:** `some-command WITH ARGS`" | |
| 1358 assert render_usage("Supported options:") == "**Supported options:**" | |
| 1359 assert 'code-block' in render_usage(dedent(""" | |
| 1360 Here comes a shell command: | |
| 1361 | |
| 1362 $ echo test | |
| 1363 test | |
| 1364 """)) | |
| 1365 assert all(token in render_usage(dedent(""" | |
| 1366 Supported options: | |
| 1367 | |
| 1368 -n, --dry-run | |
| 1369 | |
| 1370 Don't change anything. | |
| 1371 """)) for token in ('`-n`', '`--dry-run`')) | |
| 1372 | |
| 1373 def test_deprecated_args(self): | |
| 1374 """Test the deprecated_args() decorator function.""" | |
| 1375 @deprecated_args('foo', 'bar') | |
| 1376 def test_function(**options): | |
| 1377 assert options['foo'] == 'foo' | |
| 1378 assert options.get('bar') in (None, 'bar') | |
| 1379 return 42 | |
| 1380 fake_fn = MagicMock() | |
| 1381 with PatchedAttribute(warnings, 'warn', fake_fn): | |
| 1382 assert test_function('foo', 'bar') == 42 | |
| 1383 with self.assertRaises(TypeError): | |
| 1384 test_function('foo', 'bar', 'baz') | |
| 1385 assert fake_fn.was_called | |
| 1386 | |
| 1387 def test_alias_proxy_deprecation_warning(self): | |
| 1388 """Test that the DeprecationProxy class emits deprecation warnings.""" | |
| 1389 fake_fn = MagicMock() | |
| 1390 with PatchedAttribute(warnings, 'warn', fake_fn): | |
| 1391 module = sys.modules[__name__] | |
| 1392 aliases = dict(concatenate='humanfriendly.text.concatenate') | |
| 1393 proxy = DeprecationProxy(module, aliases) | |
| 1394 assert proxy.concatenate == concatenate | |
| 1395 assert fake_fn.was_called | |
| 1396 | |
| 1397 def test_alias_proxy_sphinx_compensation(self): | |
| 1398 """Test that the DeprecationProxy class emits deprecation warnings.""" | |
| 1399 with PatchedItem(sys.modules, 'sphinx', types.ModuleType('sphinx')): | |
| 1400 define_aliases(__name__, concatenate='humanfriendly.text.concatenate') | |
| 1401 assert "concatenate" in dir(sys.modules[__name__]) | |
| 1402 assert "concatenate" in get_aliases(__name__) | |
| 1403 | |
| 1404 def test_alias_proxy_sphinx_integration(self): | |
| 1405 """Test that aliases can be injected into generated documentation.""" | |
| 1406 module = sys.modules[__name__] | |
| 1407 define_aliases(__name__, concatenate='humanfriendly.text.concatenate') | |
| 1408 lines = module.__doc__.splitlines() | |
| 1409 deprecation_note_callback(app=None, what=None, name=None, obj=module, options=None, lines=lines) | |
| 1410 # Check that something was injected. | |
| 1411 assert "\n".join(lines) != module.__doc__ | |
| 1412 | |
| 1413 def test_sphinx_customizations(self): | |
| 1414 """Test the :mod:`humanfriendly.sphinx` module.""" | |
| 1415 class FakeApp(object): | |
| 1416 | |
| 1417 def __init__(self): | |
| 1418 self.callbacks = {} | |
| 1419 self.roles = {} | |
| 1420 | |
| 1421 def __documented_special_method__(self): | |
| 1422 """Documented unofficial special method.""" | |
| 1423 pass | |
| 1424 | |
| 1425 def __undocumented_special_method__(self): | |
| 1426 # Intentionally not documented :-). | |
| 1427 pass | |
| 1428 | |
| 1429 def add_role(self, name, callback): | |
| 1430 self.roles[name] = callback | |
| 1431 | |
| 1432 def connect(self, event, callback): | |
| 1433 self.callbacks.setdefault(event, []).append(callback) | |
| 1434 | |
| 1435 def bogus_usage(self): | |
| 1436 """Usage: This is not supposed to be reformatted!""" | |
| 1437 pass | |
| 1438 | |
| 1439 # Test event callback registration. | |
| 1440 fake_app = FakeApp() | |
| 1441 setup(fake_app) | |
| 1442 assert man_role == fake_app.roles['man'] | |
| 1443 assert pypi_role == fake_app.roles['pypi'] | |
| 1444 assert deprecation_note_callback in fake_app.callbacks['autodoc-process-docstring'] | |
| 1445 assert special_methods_callback in fake_app.callbacks['autodoc-skip-member'] | |
| 1446 assert usage_message_callback in fake_app.callbacks['autodoc-process-docstring'] | |
| 1447 # Test that `special methods' which are documented aren't skipped. | |
| 1448 assert special_methods_callback( | |
| 1449 app=None, what=None, name=None, | |
| 1450 obj=FakeApp.__documented_special_method__, | |
| 1451 skip=True, options=None, | |
| 1452 ) is False | |
| 1453 # Test that `special methods' which are undocumented are skipped. | |
| 1454 assert special_methods_callback( | |
| 1455 app=None, what=None, name=None, | |
| 1456 obj=FakeApp.__undocumented_special_method__, | |
| 1457 skip=True, options=None, | |
| 1458 ) is True | |
| 1459 # Test formatting of usage messages. obj/lines | |
| 1460 from humanfriendly import cli, sphinx | |
| 1461 # We expect the docstring in the `cli' module to be reformatted | |
| 1462 # (because it contains a usage message in the expected format). | |
| 1463 assert self.docstring_is_reformatted(cli) | |
| 1464 # We don't expect the docstring in the `sphinx' module to be | |
| 1465 # reformatted (because it doesn't contain a usage message). | |
| 1466 assert not self.docstring_is_reformatted(sphinx) | |
| 1467 # We don't expect the docstring of the following *method* to be | |
| 1468 # reformatted because only *module* docstrings should be reformatted. | |
| 1469 assert not self.docstring_is_reformatted(fake_app.bogus_usage) | |
| 1470 | |
| 1471 def docstring_is_reformatted(self, entity): | |
| 1472 """Check whether :func:`.usage_message_callback()` reformats a module's docstring.""" | |
| 1473 lines = trim_empty_lines(entity.__doc__).splitlines() | |
| 1474 saved_lines = list(lines) | |
| 1475 usage_message_callback( | |
| 1476 app=None, what=None, name=None, | |
| 1477 obj=entity, options=None, lines=lines, | |
| 1478 ) | |
| 1479 return lines != saved_lines | |
| 1480 | |
| 1481 | |
| 1482 def normalize_timestamp(value, ndigits=1): | |
| 1483 """ | |
| 1484 Round timestamps to the given number of digits. | |
| 1485 | |
| 1486 This helps to make the test suite less sensitive to timing issues caused by | |
| 1487 multitasking, processor scheduling, etc. | |
| 1488 """ | |
| 1489 return '%.2f' % round(float(value), ndigits=ndigits) |
