Home | History | Annotate | Download | only in test
      1 """Test harness for the zipapp module."""
      2 
      3 import io
      4 import pathlib
      5 import stat
      6 import sys
      7 import tempfile
      8 import unittest
      9 import zipapp
     10 import zipfile
     11 
     12 from unittest.mock import patch
     13 
     14 class ZipAppTest(unittest.TestCase):
     15 
     16     """Test zipapp module functionality."""
     17 
     18     def setUp(self):
     19         tmpdir = tempfile.TemporaryDirectory()
     20         self.addCleanup(tmpdir.cleanup)
     21         self.tmpdir = pathlib.Path(tmpdir.name)
     22 
     23     def test_create_archive(self):
     24         # Test packing a directory.
     25         source = self.tmpdir / 'source'
     26         source.mkdir()
     27         (source / '__main__.py').touch()
     28         target = self.tmpdir / 'source.pyz'
     29         zipapp.create_archive(str(source), str(target))
     30         self.assertTrue(target.is_file())
     31 
     32     def test_create_archive_with_pathlib(self):
     33         # Test packing a directory using Path objects for source and target.
     34         source = self.tmpdir / 'source'
     35         source.mkdir()
     36         (source / '__main__.py').touch()
     37         target = self.tmpdir / 'source.pyz'
     38         zipapp.create_archive(source, target)
     39         self.assertTrue(target.is_file())
     40 
     41     def test_create_archive_with_subdirs(self):
     42         # Test packing a directory includes entries for subdirectories.
     43         source = self.tmpdir / 'source'
     44         source.mkdir()
     45         (source / '__main__.py').touch()
     46         (source / 'foo').mkdir()
     47         (source / 'bar').mkdir()
     48         (source / 'foo' / '__init__.py').touch()
     49         target = io.BytesIO()
     50         zipapp.create_archive(str(source), target)
     51         target.seek(0)
     52         with zipfile.ZipFile(target, 'r') as z:
     53             self.assertIn('foo/', z.namelist())
     54             self.assertIn('bar/', z.namelist())
     55 
     56     def test_create_archive_default_target(self):
     57         # Test packing a directory to the default name.
     58         source = self.tmpdir / 'source'
     59         source.mkdir()
     60         (source / '__main__.py').touch()
     61         zipapp.create_archive(str(source))
     62         expected_target = self.tmpdir / 'source.pyz'
     63         self.assertTrue(expected_target.is_file())
     64 
     65     def test_no_main(self):
     66         # Test that packing a directory with no __main__.py fails.
     67         source = self.tmpdir / 'source'
     68         source.mkdir()
     69         (source / 'foo.py').touch()
     70         target = self.tmpdir / 'source.pyz'
     71         with self.assertRaises(zipapp.ZipAppError):
     72             zipapp.create_archive(str(source), str(target))
     73 
     74     def test_main_and_main_py(self):
     75         # Test that supplying a main argument with __main__.py fails.
     76         source = self.tmpdir / 'source'
     77         source.mkdir()
     78         (source / '__main__.py').touch()
     79         target = self.tmpdir / 'source.pyz'
     80         with self.assertRaises(zipapp.ZipAppError):
     81             zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
     82 
     83     def test_main_written(self):
     84         # Test that the __main__.py is written correctly.
     85         source = self.tmpdir / 'source'
     86         source.mkdir()
     87         (source / 'foo.py').touch()
     88         target = self.tmpdir / 'source.pyz'
     89         zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
     90         with zipfile.ZipFile(str(target), 'r') as z:
     91             self.assertIn('__main__.py', z.namelist())
     92             self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
     93 
     94     def test_main_only_written_once(self):
     95         # Test that we don't write multiple __main__.py files.
     96         # The initial implementation had this bug; zip files allow
     97         # multiple entries with the same name
     98         source = self.tmpdir / 'source'
     99         source.mkdir()
    100         # Write 2 files, as the original bug wrote __main__.py
    101         # once for each file written :-(
    102         # See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
    103         # (line 67)
    104         (source / 'foo.py').touch()
    105         (source / 'bar.py').touch()
    106         target = self.tmpdir / 'source.pyz'
    107         zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
    108         with zipfile.ZipFile(str(target), 'r') as z:
    109             self.assertEqual(1, z.namelist().count('__main__.py'))
    110 
    111     def test_main_validation(self):
    112         # Test that invalid values for main are rejected.
    113         source = self.tmpdir / 'source'
    114         source.mkdir()
    115         target = self.tmpdir / 'source.pyz'
    116         problems = [
    117             '', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
    118             '.a:b', 'a:b.', 'a:.b', 'a:silly name'
    119         ]
    120         for main in problems:
    121             with self.subTest(main=main):
    122                 with self.assertRaises(zipapp.ZipAppError):
    123                     zipapp.create_archive(str(source), str(target), main=main)
    124 
    125     def test_default_no_shebang(self):
    126         # Test that no shebang line is written to the target by default.
    127         source = self.tmpdir / 'source'
    128         source.mkdir()
    129         (source / '__main__.py').touch()
    130         target = self.tmpdir / 'source.pyz'
    131         zipapp.create_archive(str(source), str(target))
    132         with target.open('rb') as f:
    133             self.assertNotEqual(f.read(2), b'#!')
    134 
    135     def test_custom_interpreter(self):
    136         # Test that a shebang line with a custom interpreter is written
    137         # correctly.
    138         source = self.tmpdir / 'source'
    139         source.mkdir()
    140         (source / '__main__.py').touch()
    141         target = self.tmpdir / 'source.pyz'
    142         zipapp.create_archive(str(source), str(target), interpreter='python')
    143         with target.open('rb') as f:
    144             self.assertEqual(f.read(2), b'#!')
    145             self.assertEqual(b'python\n', f.readline())
    146 
    147     def test_pack_to_fileobj(self):
    148         # Test that we can pack to a file object.
    149         source = self.tmpdir / 'source'
    150         source.mkdir()
    151         (source / '__main__.py').touch()
    152         target = io.BytesIO()
    153         zipapp.create_archive(str(source), target, interpreter='python')
    154         self.assertTrue(target.getvalue().startswith(b'#!python\n'))
    155 
    156     def test_read_shebang(self):
    157         # Test that we can read the shebang line correctly.
    158         source = self.tmpdir / 'source'
    159         source.mkdir()
    160         (source / '__main__.py').touch()
    161         target = self.tmpdir / 'source.pyz'
    162         zipapp.create_archive(str(source), str(target), interpreter='python')
    163         self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
    164 
    165     def test_read_missing_shebang(self):
    166         # Test that reading the shebang line of a file without one returns None.
    167         source = self.tmpdir / 'source'
    168         source.mkdir()
    169         (source / '__main__.py').touch()
    170         target = self.tmpdir / 'source.pyz'
    171         zipapp.create_archive(str(source), str(target))
    172         self.assertEqual(zipapp.get_interpreter(str(target)), None)
    173 
    174     def test_modify_shebang(self):
    175         # Test that we can change the shebang of a file.
    176         source = self.tmpdir / 'source'
    177         source.mkdir()
    178         (source / '__main__.py').touch()
    179         target = self.tmpdir / 'source.pyz'
    180         zipapp.create_archive(str(source), str(target), interpreter='python')
    181         new_target = self.tmpdir / 'changed.pyz'
    182         zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
    183         self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
    184 
    185     def test_write_shebang_to_fileobj(self):
    186         # Test that we can change the shebang of a file, writing the result to a
    187         # file object.
    188         source = self.tmpdir / 'source'
    189         source.mkdir()
    190         (source / '__main__.py').touch()
    191         target = self.tmpdir / 'source.pyz'
    192         zipapp.create_archive(str(source), str(target), interpreter='python')
    193         new_target = io.BytesIO()
    194         zipapp.create_archive(str(target), new_target, interpreter='python2.7')
    195         self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
    196 
    197     def test_read_from_pathobj(self):
    198         # Test that we can copy an archive using a pathlib.Path object
    199         # for the source.
    200         source = self.tmpdir / 'source'
    201         source.mkdir()
    202         (source / '__main__.py').touch()
    203         target1 = self.tmpdir / 'target1.pyz'
    204         target2 = self.tmpdir / 'target2.pyz'
    205         zipapp.create_archive(source, target1, interpreter='python')
    206         zipapp.create_archive(target1, target2, interpreter='python2.7')
    207         self.assertEqual(zipapp.get_interpreter(target2), 'python2.7')
    208 
    209     def test_read_from_fileobj(self):
    210         # Test that we can copy an archive using an open file object.
    211         source = self.tmpdir / 'source'
    212         source.mkdir()
    213         (source / '__main__.py').touch()
    214         target = self.tmpdir / 'source.pyz'
    215         temp_archive = io.BytesIO()
    216         zipapp.create_archive(str(source), temp_archive, interpreter='python')
    217         new_target = io.BytesIO()
    218         temp_archive.seek(0)
    219         zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
    220         self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
    221 
    222     def test_remove_shebang(self):
    223         # Test that we can remove the shebang from a file.
    224         source = self.tmpdir / 'source'
    225         source.mkdir()
    226         (source / '__main__.py').touch()
    227         target = self.tmpdir / 'source.pyz'
    228         zipapp.create_archive(str(source), str(target), interpreter='python')
    229         new_target = self.tmpdir / 'changed.pyz'
    230         zipapp.create_archive(str(target), str(new_target), interpreter=None)
    231         self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
    232 
    233     def test_content_of_copied_archive(self):
    234         # Test that copying an archive doesn't corrupt it.
    235         source = self.tmpdir / 'source'
    236         source.mkdir()
    237         (source / '__main__.py').touch()
    238         target = io.BytesIO()
    239         zipapp.create_archive(str(source), target, interpreter='python')
    240         new_target = io.BytesIO()
    241         target.seek(0)
    242         zipapp.create_archive(target, new_target, interpreter=None)
    243         new_target.seek(0)
    244         with zipfile.ZipFile(new_target, 'r') as z:
    245             self.assertEqual(set(z.namelist()), {'__main__.py'})
    246 
    247     # (Unix only) tests that archives with shebang lines are made executable
    248     @unittest.skipIf(sys.platform == 'win32',
    249                      'Windows does not support an executable bit')
    250     def test_shebang_is_executable(self):
    251         # Test that an archive with a shebang line is made executable.
    252         source = self.tmpdir / 'source'
    253         source.mkdir()
    254         (source / '__main__.py').touch()
    255         target = self.tmpdir / 'source.pyz'
    256         zipapp.create_archive(str(source), str(target), interpreter='python')
    257         self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
    258 
    259     @unittest.skipIf(sys.platform == 'win32',
    260                      'Windows does not support an executable bit')
    261     def test_no_shebang_is_not_executable(self):
    262         # Test that an archive with no shebang line is not made executable.
    263         source = self.tmpdir / 'source'
    264         source.mkdir()
    265         (source / '__main__.py').touch()
    266         target = self.tmpdir / 'source.pyz'
    267         zipapp.create_archive(str(source), str(target), interpreter=None)
    268         self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
    269 
    270 
    271 class ZipAppCmdlineTest(unittest.TestCase):
    272 
    273     """Test zipapp module command line API."""
    274 
    275     def setUp(self):
    276         tmpdir = tempfile.TemporaryDirectory()
    277         self.addCleanup(tmpdir.cleanup)
    278         self.tmpdir = pathlib.Path(tmpdir.name)
    279 
    280     def make_archive(self):
    281         # Test that an archive with no shebang line is not made executable.
    282         source = self.tmpdir / 'source'
    283         source.mkdir()
    284         (source / '__main__.py').touch()
    285         target = self.tmpdir / 'source.pyz'
    286         zipapp.create_archive(source, target)
    287         return target
    288 
    289     def test_cmdline_create(self):
    290         # Test the basic command line API.
    291         source = self.tmpdir / 'source'
    292         source.mkdir()
    293         (source / '__main__.py').touch()
    294         args = [str(source)]
    295         zipapp.main(args)
    296         target = source.with_suffix('.pyz')
    297         self.assertTrue(target.is_file())
    298 
    299     def test_cmdline_copy(self):
    300         # Test copying an archive.
    301         original = self.make_archive()
    302         target = self.tmpdir / 'target.pyz'
    303         args = [str(original), '-o', str(target)]
    304         zipapp.main(args)
    305         self.assertTrue(target.is_file())
    306 
    307     def test_cmdline_copy_inplace(self):
    308         # Test copying an archive in place fails.
    309         original = self.make_archive()
    310         target = self.tmpdir / 'target.pyz'
    311         args = [str(original), '-o', str(original)]
    312         with self.assertRaises(SystemExit) as cm:
    313             zipapp.main(args)
    314         # Program should exit with a non-zero returm code.
    315         self.assertTrue(cm.exception.code)
    316 
    317     def test_cmdline_copy_change_main(self):
    318         # Test copying an archive doesn't allow changing __main__.py.
    319         original = self.make_archive()
    320         target = self.tmpdir / 'target.pyz'
    321         args = [str(original), '-o', str(target), '-m', 'foo:bar']
    322         with self.assertRaises(SystemExit) as cm:
    323             zipapp.main(args)
    324         # Program should exit with a non-zero returm code.
    325         self.assertTrue(cm.exception.code)
    326 
    327     @patch('sys.stdout', new_callable=io.StringIO)
    328     def test_info_command(self, mock_stdout):
    329         # Test the output of the info command.
    330         target = self.make_archive()
    331         args = [str(target), '--info']
    332         with self.assertRaises(SystemExit) as cm:
    333             zipapp.main(args)
    334         # Program should exit with a zero returm code.
    335         self.assertEqual(cm.exception.code, 0)
    336         self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
    337 
    338     def test_info_error(self):
    339         # Test the info command fails when the archive does not exist.
    340         target = self.tmpdir / 'dummy.pyz'
    341         args = [str(target), '--info']
    342         with self.assertRaises(SystemExit) as cm:
    343             zipapp.main(args)
    344         # Program should exit with a non-zero returm code.
    345         self.assertTrue(cm.exception.code)
    346 
    347 
    348 if __name__ == "__main__":
    349     unittest.main()
    350