OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright 2016 The LUCI Authors. All rights reserved. |
| 3 # Use of this source code is governed under the Apache License, Version 2.0 |
| 4 # that can be found in the LICENSE file. |
| 5 |
| 6 # pylint: disable=relative-import |
| 7 |
| 8 import doctest |
| 9 import io |
| 10 import os |
| 11 import shutil |
| 12 import subprocess |
| 13 import sys |
| 14 import tempfile |
| 15 import unittest |
| 16 |
| 17 import arfile |
| 18 import cli |
| 19 |
| 20 |
| 21 ARFILE_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 22 sys.path.insert(0, ARFILE_DIR) |
| 23 |
| 24 |
| 25 if not hasattr(subprocess, 'DEVNULL'): |
| 26 subprocess.DEVNULL = file(os.devnull, 'wb') |
| 27 |
| 28 |
| 29 def filesystem_supports_unicode(): |
| 30 try: |
| 31 u'\u2603'.encode(sys.getfilesystemencoding()) |
| 32 return True |
| 33 except UnicodeEncodeError: |
| 34 return False |
| 35 |
| 36 |
| 37 class ClosesSaveIOBytes(io.BytesIO): |
| 38 |
| 39 def close(self): |
| 40 _value = self.getvalue() |
| 41 self.getvalue = lambda: _value |
| 42 io.BytesIO.close(self) |
| 43 |
| 44 |
| 45 AR_TEST_SIMPLE1 = ( |
| 46 # ar file header |
| 47 '!<arch>\n' |
| 48 # File 1 |
| 49 # ---------------------- |
| 50 # (16 bytes) simple file |
| 51 'filename1 ' |
| 52 # (12 bytes) modification time |
| 53 '123 ' |
| 54 # (6 bytes) user id |
| 55 '1000 ' |
| 56 # (6 bytes) group id |
| 57 '1000 ' |
| 58 # (8 bytes) file mode |
| 59 '100640 ' |
| 60 # (10 bytes) data size |
| 61 '6 ' |
| 62 # (2 bytes) file magic |
| 63 '\x60\n' |
| 64 # File data |
| 65 'abc123' |
| 66 # Finished |
| 67 '') |
| 68 |
| 69 AR_TEST_SIMPLE_UTF = ( |
| 70 # ar file header |
| 71 '!<arch>\n' |
| 72 # File 1 |
| 73 # ---------------------- |
| 74 # (16 bytes) simple file |
| 75 '\xe2\x98\x83 ' |
| 76 # (12 bytes) modification time |
| 77 '123 ' |
| 78 # (6 bytes) user id |
| 79 '1000 ' |
| 80 # (6 bytes) group id |
| 81 '1000 ' |
| 82 # (8 bytes) file mode |
| 83 '100640 ' |
| 84 # (10 bytes) data size |
| 85 '4 ' |
| 86 # (2 bytes) file magic |
| 87 '\x60\n' |
| 88 # (4 bytes) File data |
| 89 '\xf0\x9f\x92\xa9' |
| 90 # Finished |
| 91 '') |
| 92 |
| 93 AR_TEST_BSD1 = ( |
| 94 # ar file header |
| 95 '!<arch>\n' |
| 96 # File 1 |
| 97 # ---------------------- |
| 98 # (16 bytes) BSD style filename length |
| 99 '#1/9 ' |
| 100 # (12 bytes) modification time |
| 101 '1234 ' |
| 102 # (6 bytes) user id |
| 103 '1001 ' |
| 104 # (6 bytes) group id |
| 105 '1001 ' |
| 106 # (8 bytes) file mode |
| 107 '100644 ' |
| 108 # (10 bytes) data size |
| 109 '15 ' |
| 110 # (2 bytes) file magic |
| 111 '\x60\n' |
| 112 # BSD style filename |
| 113 'filename1' |
| 114 # File data |
| 115 'abc123' |
| 116 # Padding |
| 117 '\n' |
| 118 # Finished |
| 119 '') |
| 120 |
| 121 AR_TEST_BSD2 = ( |
| 122 # ar file header |
| 123 '!<arch>\n' |
| 124 |
| 125 # File 1 |
| 126 # ---------------------- |
| 127 # (16 bytes) filename len |
| 128 '#1/5 ' |
| 129 # (12 bytes) mtime |
| 130 '1447140471 ' |
| 131 # (6 bytes) owner id |
| 132 '1000 ' |
| 133 # (6 bytes) group id |
| 134 '1000 ' |
| 135 # (8 bytes) file mode |
| 136 '100640 ' |
| 137 # (10 bytes) Data size |
| 138 '13 ' |
| 139 # (2 bytes) File magic |
| 140 '\x60\n' |
| 141 # (9 bytes) File name |
| 142 'file1' |
| 143 # (6 bytes) File data |
| 144 'contents' |
| 145 # (1 byte) Padding |
| 146 '\n' |
| 147 |
| 148 # File 2 |
| 149 # ---------------------- |
| 150 # (16 bytes) filename len |
| 151 '#1/7 ' |
| 152 # (12 bytes) mtime |
| 153 '1447140471 ' |
| 154 # (6 bytes) owner id |
| 155 '1000 ' |
| 156 # (6 bytes) group id |
| 157 '1000 ' |
| 158 # (8 bytes) file mode |
| 159 '100640 ' |
| 160 # (10 bytes) Data size |
| 161 '10 ' |
| 162 # (2 bytes) File magic |
| 163 '\x60\n' |
| 164 # (9 bytes) File name |
| 165 'fileabc' |
| 166 # (6 bytes) File data |
| 167 '123' |
| 168 # (0 byte) No padding |
| 169 '' |
| 170 |
| 171 # File 3 |
| 172 # ---------------------- |
| 173 # (16 bytes) filename len |
| 174 '#1/10 ' |
| 175 # (12 bytes) mtime |
| 176 '1447140471 ' |
| 177 # (6 bytes) owner id |
| 178 '1000 ' |
| 179 # (6 bytes) group id |
| 180 '1000 ' |
| 181 # (8 bytes) file mode |
| 182 '100640 ' |
| 183 # (10 bytes) Data size |
| 184 '16 ' |
| 185 # (2 bytes) File magic |
| 186 '\x60\n' |
| 187 # (9 bytes) File name |
| 188 'dir1/file1' |
| 189 # (6 bytes) File data |
| 190 '123abc' |
| 191 # (0 byte) No padding |
| 192 '' |
| 193 |
| 194 # Finished |
| 195 '') |
| 196 |
| 197 AR_TEST_BSD_UTF = ( |
| 198 # ar file header |
| 199 '!<arch>\n' |
| 200 # File 1 |
| 201 # ---------------------- |
| 202 # (16 bytes) BSD style filename length |
| 203 '#1/3 ' |
| 204 # (12 bytes) modification time |
| 205 '1234 ' |
| 206 # (6 bytes) user id |
| 207 '1001 ' |
| 208 # (6 bytes) group id |
| 209 '1001 ' |
| 210 # (8 bytes) file mode |
| 211 '100644 ' |
| 212 # (10 bytes) data size |
| 213 '7 ' |
| 214 # (2 bytes) file magic |
| 215 '\x60\n' |
| 216 # (3 bytes) BSD style filename |
| 217 '\xe2\x98\x83' |
| 218 # (4 bytes) File data |
| 219 '\xf0\x9f\x92\xa9' |
| 220 # Padding |
| 221 '\n' |
| 222 # Finished |
| 223 '') |
| 224 |
| 225 |
| 226 class TestArFileReader(unittest.TestCase): |
| 227 |
| 228 def testSimple1(self): |
| 229 fileobj = io.BytesIO(AR_TEST_SIMPLE1) |
| 230 |
| 231 afri = iter(arfile.ArFileReader(fileobj)) |
| 232 ai, af = afri.next() |
| 233 self.assertIs(arfile.AR_FORMAT_SIMPLE, ai.format) |
| 234 self.assertEqual('filename1', ai.name) |
| 235 self.assertEqual(6, ai.size) |
| 236 self.assertEqual(123, ai.mtime) |
| 237 self.assertEqual(1000, ai.uid) |
| 238 self.assertEqual(1000, ai.gid) |
| 239 self.assertEqual('0100640', oct(ai.mode)) |
| 240 self.assertEqual('abc123', af.read(ai.size)) |
| 241 |
| 242 def testSimpleUTF(self): |
| 243 fileobj = io.BytesIO(AR_TEST_SIMPLE_UTF) |
| 244 |
| 245 afri = iter(arfile.ArFileReader(fileobj)) |
| 246 ai, af = afri.next() |
| 247 self.assertIs(arfile.AR_FORMAT_SIMPLE, ai.format) |
| 248 self.assertEqual(u'\u2603', ai.name) |
| 249 self.assertEqual(4, ai.size) |
| 250 self.assertEqual(123, ai.mtime) |
| 251 self.assertEqual(1000, ai.uid) |
| 252 self.assertEqual(1000, ai.gid) |
| 253 self.assertEqual('0100640', oct(ai.mode)) |
| 254 self.assertEqual(u'\U0001f4a9', af.read(ai.size).decode('utf-8')) |
| 255 |
| 256 def testBSD1(self): |
| 257 fileobj = io.BytesIO(AR_TEST_BSD1) |
| 258 |
| 259 afri = iter(arfile.ArFileReader(fileobj)) |
| 260 ai, af = afri.next() |
| 261 self.assertIs(arfile.AR_FORMAT_BSD, ai.format) |
| 262 self.assertEqual('filename1', ai.name) |
| 263 self.assertEqual(6, ai.size) |
| 264 self.assertEqual(1234, ai.mtime) |
| 265 self.assertEqual(1001, ai.uid) |
| 266 self.assertEqual(1001, ai.gid) |
| 267 self.assertEqual('0100644', oct(ai.mode)) |
| 268 self.assertEqual('abc123', af.read(ai.size)) |
| 269 |
| 270 def testBSD2(self): |
| 271 fileobj = io.BytesIO(AR_TEST_BSD2) |
| 272 |
| 273 afri = iter(arfile.ArFileReader(fileobj)) |
| 274 ai, af = afri.next() |
| 275 self.assertIs(arfile.AR_FORMAT_BSD, ai.format) |
| 276 self.assertEqual('file1', ai.name) |
| 277 self.assertEqual(8, ai.size) |
| 278 self.assertEqual(1447140471, ai.mtime) |
| 279 self.assertEqual(1000, ai.uid) |
| 280 self.assertEqual(1000, ai.gid) |
| 281 self.assertEqual('0100640', oct(ai.mode)) |
| 282 self.assertEqual('contents', af.read(ai.size)) |
| 283 |
| 284 ai, af = afri.next() |
| 285 self.assertIs(arfile.AR_FORMAT_BSD, ai.format) |
| 286 self.assertEqual('fileabc', ai.name) |
| 287 self.assertEqual(3, ai.size) |
| 288 self.assertEqual(1447140471, ai.mtime) |
| 289 self.assertEqual(1000, ai.uid) |
| 290 self.assertEqual(1000, ai.gid) |
| 291 self.assertEqual('0100640', oct(ai.mode)) |
| 292 self.assertEqual('123', af.read(ai.size)) |
| 293 |
| 294 ai, af = afri.next() |
| 295 self.assertIs(arfile.AR_FORMAT_BSD, ai.format) |
| 296 self.assertEqual('dir1/file1', ai.name) |
| 297 self.assertEqual(6, ai.size) |
| 298 self.assertEqual(1447140471, ai.mtime) |
| 299 self.assertEqual(1000, ai.uid) |
| 300 self.assertEqual(1000, ai.gid) |
| 301 self.assertEqual('0100640', oct(ai.mode)) |
| 302 self.assertEqual('123abc', af.read(ai.size)) |
| 303 |
| 304 def testBSDUTF(self): |
| 305 fileobj = io.BytesIO(AR_TEST_BSD_UTF) |
| 306 |
| 307 afri = iter(arfile.ArFileReader(fileobj)) |
| 308 ai, af = afri.next() |
| 309 self.assertIs(arfile.AR_FORMAT_BSD, ai.format) |
| 310 self.assertEqual(u'\u2603', ai.name) |
| 311 self.assertEqual(4, ai.size) |
| 312 self.assertEqual(1234, ai.mtime) |
| 313 self.assertEqual(1001, ai.uid) |
| 314 self.assertEqual(1001, ai.gid) |
| 315 self.assertEqual('0100644', oct(ai.mode)) |
| 316 self.assertEqual(u'\U0001f4a9', af.read(ai.size).decode('utf-8')) |
| 317 |
| 318 |
| 319 class TestArFileWriter(unittest.TestCase): |
| 320 |
| 321 def testSimple1(self): |
| 322 fileobj = ClosesSaveIOBytes() |
| 323 |
| 324 afw = arfile.ArFileWriter(fileobj) |
| 325 ai = arfile.ArInfo( |
| 326 arfile.AR_FORMAT_SIMPLE, 'filename1', 6, 123, 1000, 1000, 0100640) |
| 327 afw.addfile(ai, io.BytesIO('abc123')) |
| 328 afw.close() |
| 329 |
| 330 self.assertMultiLineEqual(AR_TEST_SIMPLE1, fileobj.getvalue()) |
| 331 |
| 332 def testSimpleUTF(self): |
| 333 fileobj = ClosesSaveIOBytes() |
| 334 |
| 335 afw = arfile.ArFileWriter(fileobj) |
| 336 ai = arfile.ArInfo( |
| 337 arfile.AR_FORMAT_SIMPLE, u'\u2603', 4, 123, 1000, 1000, 0100640) |
| 338 afw.addfile(ai, io.BytesIO(u'\U0001f4a9'.encode('utf-8'))) |
| 339 afw.close() |
| 340 |
| 341 self.assertMultiLineEqual(AR_TEST_SIMPLE_UTF, fileobj.getvalue()) |
| 342 |
| 343 def testBSD1(self): |
| 344 fileobj = ClosesSaveIOBytes() |
| 345 |
| 346 afw = arfile.ArFileWriter(fileobj) |
| 347 ai = arfile.ArInfo( |
| 348 arfile.AR_FORMAT_BSD, 'filename1', 6, 1234, 1001, 1001, 0100644) |
| 349 afw.addfile(ai, io.BytesIO('abc123')) |
| 350 afw.close() |
| 351 |
| 352 self.assertMultiLineEqual(AR_TEST_BSD1, fileobj.getvalue()) |
| 353 |
| 354 def testBSD2(self): |
| 355 fileobj = ClosesSaveIOBytes() |
| 356 |
| 357 afw = arfile.ArFileWriter(fileobj) |
| 358 afw.addfile( |
| 359 arfile.ArInfo.fromdefault( |
| 360 'file1', 8, arformat=arfile.AR_FORMAT_BSD), |
| 361 io.BytesIO('contents')) |
| 362 afw.addfile( |
| 363 arfile.ArInfo.fromdefault( |
| 364 'fileabc', 3, arformat=arfile.AR_FORMAT_BSD), |
| 365 io.BytesIO('123')) |
| 366 afw.addfile( |
| 367 arfile.ArInfo.fromdefault( |
| 368 'dir1/file1', 6, arformat=arfile.AR_FORMAT_BSD), |
| 369 io.BytesIO('123abc')) |
| 370 afw.close() |
| 371 |
| 372 self.assertMultiLineEqual(AR_TEST_BSD2, fileobj.getvalue()) |
| 373 |
| 374 def testBSDUTF(self): |
| 375 fileobj = ClosesSaveIOBytes() |
| 376 |
| 377 afw = arfile.ArFileWriter(fileobj) |
| 378 ai = arfile.ArInfo( |
| 379 arfile.AR_FORMAT_BSD, u'\u2603', 4, 1234, 1001, 1001, 0100644) |
| 380 afw.addfile(ai, io.BytesIO(u'\U0001f4a9'.encode('utf-8'))) |
| 381 afw.close() |
| 382 |
| 383 self.assertMultiLineEqual(AR_TEST_BSD_UTF, fileobj.getvalue()) |
| 384 |
| 385 |
| 386 class BaseTestSuite(object): |
| 387 |
| 388 def testSimple1(self): |
| 389 self.assertWorking( |
| 390 ( |
| 391 arfile.ArInfo( |
| 392 arfile.AR_FORMAT_SIMPLE, 'filename1', |
| 393 6, 123, 1000, 1000, 0100640), |
| 394 'abc123')) |
| 395 |
| 396 def testSimpleUTF(self): |
| 397 self.assertWorking( |
| 398 ( |
| 399 arfile.ArInfo( |
| 400 arfile.AR_FORMAT_SIMPLE, u'\u2603', |
| 401 4, 123, 1000, 1000, 0100640), |
| 402 u'\U0001f4a9'.encode('utf-8'))) |
| 403 |
| 404 def testBSD1(self): |
| 405 self.assertWorking( |
| 406 ( |
| 407 arfile.ArInfo( |
| 408 arfile.AR_FORMAT_BSD, 'filename1', |
| 409 6, 123, 1000, 1000, 0100640), |
| 410 'abc123')) |
| 411 |
| 412 def testBSD2(self): |
| 413 self.assertWorking( |
| 414 ( |
| 415 arfile.ArInfo.fromdefault( |
| 416 'file1', 8, arformat=arfile.AR_FORMAT_BSD), |
| 417 'contents'), |
| 418 ( |
| 419 arfile.ArInfo.fromdefault( |
| 420 'fileabc', 3, arformat=arfile.AR_FORMAT_BSD), |
| 421 '123'), |
| 422 ( |
| 423 arfile.ArInfo.fromdefault( |
| 424 'dir1/file1', 6, arformat=arfile.AR_FORMAT_BSD), |
| 425 '123abc')) |
| 426 |
| 427 def testBSDUTF(self): |
| 428 self.assertWorking( |
| 429 ( |
| 430 arfile.ArInfo( |
| 431 arfile.AR_FORMAT_BSD, u'\u2603', |
| 432 4, 123, 1000, 1000, 0100640), |
| 433 u'\U0001f4a9'.encode('utf-8'))) |
| 434 |
| 435 def testMixed(self): |
| 436 self.assertWorking( |
| 437 (arfile.ArInfo.fromdefault('file1', 0), ''), |
| 438 (arfile.ArInfo.fromdefault('f f', 1), 'a'), |
| 439 (arfile.ArInfo.fromdefault('123456789abcedefa', 1), 'a')) |
| 440 |
| 441 |
| 442 class TestArRoundTrip(BaseTestSuite, unittest.TestCase): |
| 443 |
| 444 def assertWorking(self, *initems): |
| 445 outfile = ClosesSaveIOBytes() |
| 446 |
| 447 afw = arfile.ArFileWriter(outfile) |
| 448 for ai, data in initems: |
| 449 assert ai.size == len(data) |
| 450 afw.addfile(ai, io.BytesIO(data)) |
| 451 afw.close() |
| 452 |
| 453 infile = io.BytesIO(outfile.getvalue()) |
| 454 afr = arfile.ArFileReader(infile) |
| 455 |
| 456 outitems = [] |
| 457 for ai, fd in afr: |
| 458 data = fd.read(ai.size) |
| 459 outitems.append((ai, data)) |
| 460 |
| 461 self.assertSequenceEqual(initems, outitems) |
| 462 |
| 463 |
| 464 def system_has_ar(): |
| 465 retcode = subprocess.call( |
| 466 'ar', stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
| 467 return retcode == 1 |
| 468 |
| 469 |
| 470 @unittest.skipIf(not system_has_ar(), 'no ar binary found.') |
| 471 class TestArExternal(BaseTestSuite, unittest.TestCase): |
| 472 |
| 473 def assertWorking(self, *initems): |
| 474 tf = tempfile.NamedTemporaryFile(mode='wb') |
| 475 afw = arfile.ArFileWriter(tf) |
| 476 |
| 477 files = [] |
| 478 for ai, data in initems: |
| 479 files.append(ai.name) |
| 480 assert ai.size == len(data) |
| 481 afw.addfile(ai, io.BytesIO(data)) |
| 482 afw.flush() |
| 483 |
| 484 output = subprocess.check_output(['ar', 't', tf.name]) |
| 485 self.assertMultiLineEqual('\n'.join(files), output.decode('utf-8').strip()) |
| 486 tf.close() |
| 487 |
| 488 |
| 489 class TestCLI(unittest.TestCase): |
| 490 |
| 491 def runCLI(self, args): |
| 492 orig_stdout = sys.stdout |
| 493 orig_stderr = sys.stderr |
| 494 try: |
| 495 sys.stdout = io.StringIO() |
| 496 sys.stderr = io.StringIO() |
| 497 cli.main('artool', args) |
| 498 return sys.stdout.getvalue(), sys.stderr.getvalue() |
| 499 finally: |
| 500 sys.stdout = orig_stdout |
| 501 sys.stderr = orig_stderr |
| 502 |
| 503 def assertCLI(self, *initems, **kw): |
| 504 extra_args = kw.get('extra_args', []) |
| 505 |
| 506 indir = None |
| 507 ardir = None |
| 508 outdir = None |
| 509 try: |
| 510 indir = tempfile.mkdtemp().decode(sys.getfilesystemencoding()) |
| 511 ardir = tempfile.mkdtemp().decode(sys.getfilesystemencoding()) |
| 512 outdir = tempfile.mkdtemp().decode(sys.getfilesystemencoding()) |
| 513 |
| 514 arp = os.path.join(ardir, 'out.ar') |
| 515 assert not os.path.exists(arp) |
| 516 |
| 517 # Write out a directory tree |
| 518 files = [] |
| 519 for fp, contents in initems: |
| 520 fn = os.path.join(indir, fp) |
| 521 dn = os.path.dirname(fn) |
| 522 if not os.path.exists(dn): |
| 523 os.makedirs(dn) |
| 524 |
| 525 with file(fn, 'wb') as f: |
| 526 f.write(contents) |
| 527 |
| 528 files.append(fp) |
| 529 |
| 530 files.sort() |
| 531 fileslist = '\n'.join(files) |
| 532 |
| 533 # Create an archive from a directory |
| 534 self.runCLI(['create', '--filename', arp, indir] + extra_args) |
| 535 self.assertTrue( |
| 536 os.path.exists(arp), '%s file should exists' % arp) |
| 537 |
| 538 # List the archive contents |
| 539 output, _ = self.runCLI(['list', '--filename', arp]) |
| 540 filesoutput = '\n'.join(sorted(output[:-1].split('\n'))) |
| 541 self.assertMultiLineEqual(fileslist, filesoutput) |
| 542 |
| 543 # Extract the archive |
| 544 os.chdir(outdir) |
| 545 self.runCLI(['extract', '--filename', arp] + extra_args) |
| 546 |
| 547 # Walk the directory tree and collect the extracted output |
| 548 outitems = [] |
| 549 for root, _, files in os.walk(outdir): |
| 550 for fn in files: |
| 551 fp = os.path.join(root, fn) |
| 552 outitems.append([fp[len(outdir)+1:], file(fp, 'rb').read()]) |
| 553 |
| 554 # Check the two are equal |
| 555 self.assertSequenceEqual(sorted(initems), sorted(outitems)) |
| 556 |
| 557 finally: |
| 558 if indir: |
| 559 shutil.rmtree(indir, ignore_errors=True) |
| 560 if ardir: |
| 561 shutil.rmtree(ardir, ignore_errors=True) |
| 562 if outdir: |
| 563 shutil.rmtree(outdir, ignore_errors=True) |
| 564 |
| 565 def testSimple1(self): |
| 566 self.assertCLI(['file1', 'contents1']) |
| 567 |
| 568 def testFullStat(self): |
| 569 self.assertCLI( |
| 570 ['file1', 'contents1'], |
| 571 extra_args=['--dont-use-defaults']) |
| 572 |
| 573 def testMultiple(self): |
| 574 self.assertCLI( |
| 575 ['file1', 'contents1'], |
| 576 ['dir1/file2', 'contents2'], |
| 577 ['dir2/dir3/file3', 'contents3'], |
| 578 ['file4', 'contents4'], |
| 579 ) |
| 580 |
| 581 def testUnicodeContents(self): |
| 582 self.assertCLI(['file1', u'\u2603'.encode('utf-8')]) |
| 583 |
| 584 def testFilenameSpaces(self): |
| 585 self.assertCLI( |
| 586 ['f f1', 'contents1'], |
| 587 ['d d1/file2', 'contents2'], |
| 588 ['d d1/f f3', 'contents3'], |
| 589 ['file4', 'contents4'], |
| 590 ) |
| 591 |
| 592 def testBigFile(self): |
| 593 self.assertCLI(['bigfile', 'data'*1024*1024*10]) |
| 594 |
| 595 @unittest.skipIf( |
| 596 not filesystem_supports_unicode(), 'no unicode file support') |
| 597 def testUnicode(self): |
| 598 self.assertCLI([u'\u2603', u'\U0001f4a9'.encode('utf-8')]) |
| 599 |
| 600 |
| 601 if __name__ == '__main__': |
| 602 doctest.testmod(arfile) |
| 603 unittest.main() |
OLD | NEW |