blob: e85c93b9bec860029979e4c26ddd010646f8de05 [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
12
13class ZipAppTest(unittest.TestCase):
14
15 """Test zipapp module functionality."""
16
17 def setUp(self):
18 tmpdir = tempfile.TemporaryDirectory()
19 self.addCleanup(tmpdir.cleanup)
20 self.tmpdir = pathlib.Path(tmpdir.name)
21
22 def test_create_archive(self):
23 # Test packing a directory.
24 source = self.tmpdir / 'source'
25 source.mkdir()
26 (source / '__main__.py').touch()
27 target = self.tmpdir / 'source.pyz'
28 zipapp.create_archive(str(source), str(target))
29 self.assertTrue(target.is_file())
30
31 def test_create_archive_with_subdirs(self):
32 # Test packing a directory includes entries for subdirectories.
33 source = self.tmpdir / 'source'
34 source.mkdir()
35 (source / '__main__.py').touch()
36 (source / 'foo').mkdir()
37 (source / 'bar').mkdir()
38 (source / 'foo' / '__init__.py').touch()
39 target = io.BytesIO()
40 zipapp.create_archive(str(source), target)
41 target.seek(0)
42 with zipfile.ZipFile(target, 'r') as z:
43 self.assertIn('foo/', z.namelist())
44 self.assertIn('bar/', z.namelist())
45
46 def test_create_archive_default_target(self):
47 # Test packing a directory to the default name.
48 source = self.tmpdir / 'source'
49 source.mkdir()
50 (source / '__main__.py').touch()
51 zipapp.create_archive(str(source))
52 expected_target = self.tmpdir / 'source.pyz'
53 self.assertTrue(expected_target.is_file())
54
55 def test_no_main(self):
56 # Test that packing a directory with no __main__.py fails.
57 source = self.tmpdir / 'source'
58 source.mkdir()
59 (source / 'foo.py').touch()
60 target = self.tmpdir / 'source.pyz'
61 with self.assertRaises(zipapp.ZipAppError):
62 zipapp.create_archive(str(source), str(target))
63
64 def test_main_and_main_py(self):
65 # Test that supplying a main argument with __main__.py fails.
66 source = self.tmpdir / 'source'
67 source.mkdir()
68 (source / '__main__.py').touch()
69 target = self.tmpdir / 'source.pyz'
70 with self.assertRaises(zipapp.ZipAppError):
71 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
72
73 def test_main_written(self):
74 # Test that the __main__.py is written correctly.
75 source = self.tmpdir / 'source'
76 source.mkdir()
77 (source / 'foo.py').touch()
78 target = self.tmpdir / 'source.pyz'
79 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
80 with zipfile.ZipFile(str(target), 'r') as z:
81 self.assertIn('__main__.py', z.namelist())
82 self.assertIn(b'pkg.mod.fn()', z.read('__main__.py'))
83
84 def test_main_only_written_once(self):
85 # Test that we don't write multiple __main__.py files.
86 # The initial implementation had this bug; zip files allow
87 # multiple entries with the same name
88 source = self.tmpdir / 'source'
89 source.mkdir()
90 # Write 2 files, as the original bug wrote __main__.py
91 # once for each file written :-(
92 # See http://bugs.python.org/review/23491/diff/13982/Lib/zipapp.py#newcode67Lib/zipapp.py:67
93 # (line 67)
94 (source / 'foo.py').touch()
95 (source / 'bar.py').touch()
96 target = self.tmpdir / 'source.pyz'
97 zipapp.create_archive(str(source), str(target), main='pkg.mod:fn')
98 with zipfile.ZipFile(str(target), 'r') as z:
99 self.assertEqual(1, z.namelist().count('__main__.py'))
100
101 def test_main_validation(self):
102 # Test that invalid values for main are rejected.
103 source = self.tmpdir / 'source'
104 source.mkdir()
105 target = self.tmpdir / 'source.pyz'
106 problems = [
107 '', 'foo', 'foo:', ':bar', '12:bar', 'a.b.c.:d',
108 '.a:b', 'a:b.', 'a:.b', 'a:silly name'
109 ]
110 for main in problems:
111 with self.subTest(main=main):
112 with self.assertRaises(zipapp.ZipAppError):
113 zipapp.create_archive(str(source), str(target), main=main)
114
115 def test_default_no_shebang(self):
116 # Test that no shebang line is written to the target by default.
117 source = self.tmpdir / 'source'
118 source.mkdir()
119 (source / '__main__.py').touch()
120 target = self.tmpdir / 'source.pyz'
121 zipapp.create_archive(str(source), str(target))
122 with target.open('rb') as f:
123 self.assertNotEqual(f.read(2), b'#!')
124
125 def test_custom_interpreter(self):
126 # Test that a shebang line with a custom interpreter is written
127 # correctly.
128 source = self.tmpdir / 'source'
129 source.mkdir()
130 (source / '__main__.py').touch()
131 target = self.tmpdir / 'source.pyz'
132 zipapp.create_archive(str(source), str(target), interpreter='python')
133 with target.open('rb') as f:
134 self.assertEqual(f.read(2), b'#!')
135 self.assertEqual(b'python\n', f.readline())
136
137 def test_pack_to_fileobj(self):
138 # Test that we can pack to a file object.
139 source = self.tmpdir / 'source'
140 source.mkdir()
141 (source / '__main__.py').touch()
142 target = io.BytesIO()
143 zipapp.create_archive(str(source), target, interpreter='python')
144 self.assertTrue(target.getvalue().startswith(b'#!python\n'))
145
146 def test_read_shebang(self):
147 # Test that we can read the shebang line correctly.
148 source = self.tmpdir / 'source'
149 source.mkdir()
150 (source / '__main__.py').touch()
151 target = self.tmpdir / 'source.pyz'
152 zipapp.create_archive(str(source), str(target), interpreter='python')
153 self.assertEqual(zipapp.get_interpreter(str(target)), 'python')
154
155 def test_read_missing_shebang(self):
156 # Test that reading the shebang line of a file without one returns None.
157 source = self.tmpdir / 'source'
158 source.mkdir()
159 (source / '__main__.py').touch()
160 target = self.tmpdir / 'source.pyz'
161 zipapp.create_archive(str(source), str(target))
162 self.assertEqual(zipapp.get_interpreter(str(target)), None)
163
164 def test_modify_shebang(self):
165 # Test that we can change the shebang of a file.
166 source = self.tmpdir / 'source'
167 source.mkdir()
168 (source / '__main__.py').touch()
169 target = self.tmpdir / 'source.pyz'
170 zipapp.create_archive(str(source), str(target), interpreter='python')
171 new_target = self.tmpdir / 'changed.pyz'
172 zipapp.create_archive(str(target), str(new_target), interpreter='python2.7')
173 self.assertEqual(zipapp.get_interpreter(str(new_target)), 'python2.7')
174
175 def test_write_shebang_to_fileobj(self):
176 # Test that we can change the shebang of a file, writing the result to a
177 # file object.
178 source = self.tmpdir / 'source'
179 source.mkdir()
180 (source / '__main__.py').touch()
181 target = self.tmpdir / 'source.pyz'
182 zipapp.create_archive(str(source), str(target), interpreter='python')
183 new_target = io.BytesIO()
184 zipapp.create_archive(str(target), new_target, interpreter='python2.7')
185 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
186
187 def test_read_from_fileobj(self):
188 # Test that we can copy an archive using an open file object.
189 source = self.tmpdir / 'source'
190 source.mkdir()
191 (source / '__main__.py').touch()
192 target = self.tmpdir / 'source.pyz'
193 temp_archive = io.BytesIO()
194 zipapp.create_archive(str(source), temp_archive, interpreter='python')
195 new_target = io.BytesIO()
196 temp_archive.seek(0)
197 zipapp.create_archive(temp_archive, new_target, interpreter='python2.7')
198 self.assertTrue(new_target.getvalue().startswith(b'#!python2.7\n'))
199
200 def test_remove_shebang(self):
201 # Test that we can remove the shebang from a file.
202 source = self.tmpdir / 'source'
203 source.mkdir()
204 (source / '__main__.py').touch()
205 target = self.tmpdir / 'source.pyz'
206 zipapp.create_archive(str(source), str(target), interpreter='python')
207 new_target = self.tmpdir / 'changed.pyz'
208 zipapp.create_archive(str(target), str(new_target), interpreter=None)
209 self.assertEqual(zipapp.get_interpreter(str(new_target)), None)
210
211 def test_content_of_copied_archive(self):
212 # Test that copying an archive doesn't corrupt it.
213 source = self.tmpdir / 'source'
214 source.mkdir()
215 (source / '__main__.py').touch()
216 target = io.BytesIO()
217 zipapp.create_archive(str(source), target, interpreter='python')
218 new_target = io.BytesIO()
219 target.seek(0)
220 zipapp.create_archive(target, new_target, interpreter=None)
221 new_target.seek(0)
222 with zipfile.ZipFile(new_target, 'r') as z:
223 self.assertEqual(set(z.namelist()), {'__main__.py'})
224
225 # (Unix only) tests that archives with shebang lines are made executable
226 @unittest.skipIf(sys.platform == 'win32',
227 'Windows does not support an executable bit')
228 def test_shebang_is_executable(self):
229 # Test that an archive with a shebang line is made executable.
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 self.assertTrue(target.stat().st_mode & stat.S_IEXEC)
236
237 @unittest.skipIf(sys.platform == 'win32',
238 'Windows does not support an executable bit')
239 def test_no_shebang_is_not_executable(self):
240 # Test that an archive with no shebang line is not made executable.
241 source = self.tmpdir / 'source'
242 source.mkdir()
243 (source / '__main__.py').touch()
244 target = self.tmpdir / 'source.pyz'
245 zipapp.create_archive(str(source), str(target), interpreter=None)
246 self.assertFalse(target.stat().st_mode & stat.S_IEXEC)
247
248
249if __name__ == "__main__":
250 unittest.main()