blob: 56cf37c9afa03b457b8194403994333501f64202 [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
11
Paul Moorea4d4dd32015-03-22 15:32:36 +000012from unittest.mock import patch
Brett Cannoncc4dfc12015-03-13 10:40:49 -040013
14class 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
Paul Moorea4d4dd32015-03-22 15:32:36 +000032 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
Brett Cannoncc4dfc12015-03-13 10:40:49 -040041 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
Paul Moore0780bf72017-08-26 18:04:12 +010056 def test_create_archive_with_filter(self):
57 # Test packing a directory and using filter to specify
58 # which files to include.
59 def skip_pyc_files(path):
60 return path.suffix != '.pyc'
Jeffrey Rackauckasb811d662017-08-09 06:37:17 -070061 source = self.tmpdir / 'source'
62 source.mkdir()
63 (source / '__main__.py').touch()
64 (source / 'test.py').touch()
65 (source / 'test.pyc').touch()
66 target = self.tmpdir / 'source.pyz'
67
Paul Moore0780bf72017-08-26 18:04:12 +010068 zipapp.create_archive(source, target, filter=skip_pyc_files)
Jeffrey Rackauckasb811d662017-08-09 06:37:17 -070069 with zipfile.ZipFile(target, 'r') as z:
70 self.assertIn('__main__.py', z.namelist())
71 self.assertIn('test.py', z.namelist())
72 self.assertNotIn('test.pyc', z.namelist())
73
Paul Moore0780bf72017-08-26 18:04:12 +010074 def test_create_archive_filter_exclude_dir(self):
75 # Test packing a directory and using a filter to exclude a
76 # subdirectory (ensures that the path supplied to include
77 # is relative to the source location, as expected).
78 def skip_dummy_dir(path):
79 return path.parts[0] != 'dummy'
80 source = self.tmpdir / 'source'
81 source.mkdir()
82 (source / '__main__.py').touch()
83 (source / 'test.py').touch()
84 (source / 'dummy').mkdir()
85 (source / 'dummy' / 'test2.py').touch()
86 target = self.tmpdir / 'source.pyz'
87
88 zipapp.create_archive(source, target, filter=skip_dummy_dir)
89 with zipfile.ZipFile(target, 'r') as z:
90 self.assertEqual(len(z.namelist()), 2)
91 self.assertIn('__main__.py', z.namelist())
92 self.assertIn('test.py', z.namelist())
93
Brett Cannoncc4dfc12015-03-13 10:40:49 -040094 def test_create_archive_default_target(self):
95 # Test packing a directory to the default name.
96 source = self.tmpdir / 'source'
97 source.mkdir()
98 (source / '__main__.py').touch()
99 zipapp.create_archive(str(source))
100 expected_target = self.tmpdir / 'source.pyz'
101 self.assertTrue(expected_target.is_file())
102
103 def test_no_main(self):
104 # Test that packing a directory with no __main__.py fails.
105 source = self.tmpdir / 'source'
106 source.mkdir()
107 (source / 'foo.py').touch()
108 target = self.tmpdir / 'source.pyz'
109 with self.assertRaises(zipapp.ZipAppError):
110 zipapp.create_archive(str(source), str(target))
111
112 def test_main_and_main_py(self):
113 # Test that supplying a main argument with __main__.py fails.
114 source = self.tmpdir / 'source'
115 source.mkdir()
116 (source / '__main__.py').touch()
117 target = self.tmpdir / 'source.pyz'
118 with self.assertRaises(zipapp.ZipAppError):
119 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
120
121 def test_main_written(self):
122 # Test that the __main__.py is written correctly.
123 source = self.tmpdir / 'source'
124 source.mkdir()
125 (source / 'foo.py').touch()
126 target = self.tmpdir / 'source.pyz'
127 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
128 with zipfile.ZipFile(str(target), 'r') as z:
129 self.assertIn('__main__.py', z.namelist())
130 self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
131
132 def test_main_only_written_once(self):
133 # Test that we don't write multiple __main__.py files.
134 # The initial implementation had this bug; zip files allow
135 # multiple entries with the same name
136 source = self.tmpdir / 'source'
137 source.mkdir()
138 # Write 2 files, as the original bug wrote __main__.py
139 # once for each file written :-(
140 # See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
141 # (line 67)
142 (source / 'foo.py').touch()
143 (source / 'bar.py').touch()
144 target = self.tmpdir / 'source.pyz'
145 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
146 with zipfile.ZipFile(str(target), 'r') as z:
147 self.assertEqual(1, z.namelist().count('__main__.py'))
148
149 def test_main_validation(self):
150 # Test that invalid values for main are rejected.
151 source = self.tmpdir / 'source'
152 source.mkdir()
153 target = self.tmpdir / 'source.pyz'
154 problems = [
155 '', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
156 '.a:b', 'a:b.', 'a:.b', 'a:silly name'
157 ]
158 for main in problems:
159 with self.subTest(main=main):
160 with self.assertRaises(zipapp.ZipAppError):
161 zipapp.create_archive(str(source), str(target), main=main)
162
163 def test_default_no_shebang(self):
164 # Test that no shebang line is written to the target by default.
165 source = self.tmpdir / 'source'
166 source.mkdir()
167 (source / '__main__.py').touch()
168 target = self.tmpdir / 'source.pyz'
169 zipapp.create_archive(str(source), str(target))
170 with target.open('rb') as f:
171 self.assertNotEqual(f.read(2), b'#!')
172
173 def test_custom_interpreter(self):
174 # Test that a shebang line with a custom interpreter is written
175 # correctly.
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 with target.open('rb') as f:
182 self.assertEqual(f.read(2), b'#!')
183 self.assertEqual(b'python\n', f.readline())
184
185 def test_pack_to_fileobj(self):
186 # Test that we can pack to a file object.
187 source = self.tmpdir / 'source'
188 source.mkdir()
189 (source / '__main__.py').touch()
190 target = io.BytesIO()
191 zipapp.create_archive(str(source), target, interpreter='python')
192 self.assertTrue(target.getvalue().startswith(b'#!python\n'))
193
194 def test_read_shebang(self):
195 # Test that we can read the shebang line correctly.
196 source = self.tmpdir / 'source'
197 source.mkdir()
198 (source / '__main__.py').touch()
199 target = self.tmpdir / 'source.pyz'
200 zipapp.create_archive(str(source), str(target), interpreter='python')
201 self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
202
203 def test_read_missing_shebang(self):
204 # Test that reading the shebang line of a file without one returns None.
205 source = self.tmpdir / 'source'
206 source.mkdir()
207 (source / '__main__.py').touch()
208 target = self.tmpdir / 'source.pyz'
209 zipapp.create_archive(str(source), str(target))
210 self.assertEqual(zipapp.get_interpreter(str(target)), None)
211
212 def test_modify_shebang(self):
213 # Test that we can change the shebang of a file.
214 source = self.tmpdir / 'source'
215 source.mkdir()
216 (source / '__main__.py').touch()
217 target = self.tmpdir / 'source.pyz'
218 zipapp.create_archive(str(source), str(target), interpreter='python')
219 new_target = self.tmpdir / 'changed.pyz'
220 zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
221 self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
222
223 def test_write_shebang_to_fileobj(self):
224 # Test that we can change the shebang of a file, writing the result to a
225 # file object.
226 source = self.tmpdir / 'source'
227 source.mkdir()
228 (source / '__main__.py').touch()
229 target = self.tmpdir / 'source.pyz'
230 zipapp.create_archive(str(source), str(target), interpreter='python')
231 new_target = io.BytesIO()
232 zipapp.create_archive(str(target), new_target, interpreter='python2.7')
233 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
234
Paul Moorea4d4dd32015-03-22 15:32:36 +0000235 def test_read_from_pathobj(self):
Serhiy Storchaka6a7b3a72016-04-17 08:32:47 +0300236 # Test that we can copy an archive using a pathlib.Path object
Paul Moorea4d4dd32015-03-22 15:32:36 +0000237 # for the source.
238 source = self.tmpdir / 'source'
239 source.mkdir()
240 (source / '__main__.py').touch()
241 target1 = self.tmpdir / 'target1.pyz'
242 target2 = self.tmpdir / 'target2.pyz'
243 zipapp.create_archive(source, target1, interpreter='python')
244 zipapp.create_archive(target1, target2, interpreter='python2.7')
245 self.assertEqual(zipapp.get_interpreter(target2), 'python2.7')
246
Brett Cannoncc4dfc12015-03-13 10:40:49 -0400247 def test_read_from_fileobj(self):
248 # Test that we can copy an archive using an open file object.
249 source = self.tmpdir / 'source'
250 source.mkdir()
251 (source / '__main__.py').touch()
252 target = self.tmpdir / 'source.pyz'
253 temp_archive = io.BytesIO()
254 zipapp.create_archive(str(source), temp_archive, interpreter='python')
255 new_target = io.BytesIO()
256 temp_archive.seek(0)
257 zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
258 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
259
260 def test_remove_shebang(self):
261 # Test that we can remove the shebang from a file.
262 source = self.tmpdir / 'source'
263 source.mkdir()
264 (source / '__main__.py').touch()
265 target = self.tmpdir / 'source.pyz'
266 zipapp.create_archive(str(source), str(target), interpreter='python')
267 new_target = self.tmpdir / 'changed.pyz'
268 zipapp.create_archive(str(target), str(new_target), interpreter=None)
269 self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
270
271 def test_content_of_copied_archive(self):
272 # Test that copying an archive doesn't corrupt it.
273 source = self.tmpdir / 'source'
274 source.mkdir()
275 (source / '__main__.py').touch()
276 target = io.BytesIO()
277 zipapp.create_archive(str(source), target, interpreter='python')
278 new_target = io.BytesIO()
279 target.seek(0)
280 zipapp.create_archive(target, new_target, interpreter=None)
281 new_target.seek(0)
282 with zipfile.ZipFile(new_target, 'r') as z:
283 self.assertEqual(set(z.namelist()), {'__main__.py'})
284
285 # (Unix only) tests that archives with shebang lines are made executable
286 @unittest.skipIf(sys.platform == 'win32',
287 'Windows does not support an executable bit')
288 def test_shebang_is_executable(self):
289 # Test that an archive with a shebang line is made executable.
290 source = self.tmpdir / 'source'
291 source.mkdir()
292 (source / '__main__.py').touch()
293 target = self.tmpdir / 'source.pyz'
294 zipapp.create_archive(str(source), str(target), interpreter='python')
295 self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
296
297 @unittest.skipIf(sys.platform == 'win32',
298 'Windows does not support an executable bit')
299 def test_no_shebang_is_not_executable(self):
300 # Test that an archive with no shebang line is not made executable.
301 source = self.tmpdir / 'source'
302 source.mkdir()
303 (source / '__main__.py').touch()
304 target = self.tmpdir / 'source.pyz'
305 zipapp.create_archive(str(source), str(target), interpreter=None)
306 self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
307
308
Paul Moorea4d4dd32015-03-22 15:32:36 +0000309class ZipAppCmdlineTest(unittest.TestCase):
310
311 """Test zipapp module command line API."""
312
313 def setUp(self):
314 tmpdir = tempfile.TemporaryDirectory()
315 self.addCleanup(tmpdir.cleanup)
316 self.tmpdir = pathlib.Path(tmpdir.name)
317
318 def make_archive(self):
319 # Test that an archive with no shebang line is not made executable.
320 source = self.tmpdir / 'source'
321 source.mkdir()
322 (source / '__main__.py').touch()
323 target = self.tmpdir / 'source.pyz'
324 zipapp.create_archive(source, target)
325 return target
326
327 def test_cmdline_create(self):
328 # Test the basic command line API.
329 source = self.tmpdir / 'source'
330 source.mkdir()
331 (source / '__main__.py').touch()
332 args = [str(source)]
333 zipapp.main(args)
334 target = source.with_suffix('.pyz')
335 self.assertTrue(target.is_file())
336
337 def test_cmdline_copy(self):
338 # Test copying an archive.
339 original = self.make_archive()
340 target = self.tmpdir / 'target.pyz'
341 args = [str(original), '-o', str(target)]
342 zipapp.main(args)
343 self.assertTrue(target.is_file())
344
345 def test_cmdline_copy_inplace(self):
346 # Test copying an archive in place fails.
347 original = self.make_archive()
348 target = self.tmpdir / 'target.pyz'
349 args = [str(original), '-o', str(original)]
350 with self.assertRaises(SystemExit) as cm:
351 zipapp.main(args)
352 # Program should exit with a non-zero returm code.
353 self.assertTrue(cm.exception.code)
354
355 def test_cmdline_copy_change_main(self):
356 # Test copying an archive doesn't allow changing __main__.py.
357 original = self.make_archive()
358 target = self.tmpdir / 'target.pyz'
359 args = [str(original), '-o', str(target), '-m', 'foo:bar']
360 with self.assertRaises(SystemExit) as cm:
361 zipapp.main(args)
362 # Program should exit with a non-zero returm code.
363 self.assertTrue(cm.exception.code)
364
365 @patch('sys.stdout', new_callable=io.StringIO)
366 def test_info_command(self, mock_stdout):
367 # Test the output of the info command.
368 target = self.make_archive()
369 args = [str(target), '--info']
370 with self.assertRaises(SystemExit) as cm:
371 zipapp.main(args)
372 # Program should exit with a zero returm code.
373 self.assertEqual(cm.exception.code, 0)
374 self.assertEqual(mock_stdout.getvalue(), "Interpreter: <none>\n")
375
376 def test_info_error(self):
377 # Test the info command fails when the archive does not exist.
378 target = self.tmpdir / 'dummy.pyz'
379 args = [str(target), '--info']
380 with self.assertRaises(SystemExit) as cm:
381 zipapp.main(args)
382 # Program should exit with a non-zero returm code.
383 self.assertTrue(cm.exception.code)
384
385
Brett Cannoncc4dfc12015-03-13 10:40:49 -0400386if __name__ == "__main__":
387 unittest.main()