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