Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: tests/test_packagerWebExt.py

Issue 29501558: Issue 5383 - Add tests for the Chrome and Firefox packagers (Closed)
Patch Set: Addressing comments, handling Edge devenv Created Oct. 18, 2017, 11:19 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « tests/test_packagerEdge.py ('k') | tox.ini » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5 import os
6 import shutil
7 import zipfile
8 import json
9 import re
10 from struct import unpack
11 import difflib
12
13 import pytest
14 from Crypto.Hash import SHA
15 from Crypto.PublicKey import RSA
16 from Crypto.Signature import PKCS1_v1_5
17 from xml.etree import ElementTree
18
19 from buildtools import packager
20 from buildtools.packagerChrome import defaultLocale
21 from buildtools.build import processArgs
22
23 LOCALES_MODULE = {
24 'test.Foobar': {
25 'message': 'Ensuring dict-copy from modules for $domain$',
26 'description': 'test description',
27 'placeholders': {'content': '$1', 'example': 'www.adblockplus.org'}
28 }
29 }
30
31 ALL_LANGUAGES = ['en_US', 'de', 'it']
32
33 MESSAGES_EN_US = json.dumps({
34 'name': {'message': 'Adblock Plus'},
35 'name_devbuild': {'message': 'devbuild-marker'},
36 'description': {
37 'message': 'Adblock Plus is the most popular ad blocker ever, '
38 'and also supports websites by not blocking '
39 'unobstrusive ads by default (configurable).'
40 },
41 })
42
43
44 class Content(object):
45 """Base class for a unified ZipFile / Directory interface.
46
47 Base class for providing a unified context manager interface for
48 accessing files. This class is subclassed by ZipContent and DirContent,
49 which provide the additional methods "namelist()" and "read(path)".
50 """
51
52 def __enter__(self):
53 return self
54
55 def __exit__(self, exc_type, exc_value, exc_tb):
56 self._close()
57
58
59 class ZipContent(Content):
60 """Provide a unified context manager for ZipFile access.
61
62 Inherits the context manager API from Content.
63 If desired, the specified ZipFile is deleted on exiting the manager.
64 """
65
66 def __init__(self, zip_path, delete_on_close=True):
67 """Construct ZipContent object handling the file <zip_path>.
68
69 The parameter 'delete_on_close' causes the context manager to
70 delete the handled ZipFile (specified by zip_path) if set to
71 True (default).
72 """
73 self._zip_path = zip_path
74 self._zip_file = zipfile.ZipFile(zip_path)
75 self._delete_on_close = delete_on_close
76 super(ZipContent, self).__init__()
77
78 def _close(self):
79 self._zip_file.close()
80 if self._delete_on_close:
81 # if specified, delete the handled file
82 os.remove(self._zip_path)
83
84 def namelist(self):
85 return self._zip_file.namelist()
86
87 def read(self, path):
88 return self._zip_file.read(path)
89
90
91 class DirContent(Content):
92 """Provides a unified context manager for directory access.
93
94 Inherits the context managet API from Content.
95 """
96
97 def __init__(self, path):
98 """Construct a DirContent object handling <path>."""
99 self._path = path
100 super(DirContent, self).__init__()
101
102 def _close(self):
103 pass
104
105 def namelist(self):
106 """Generate a list of filenames."""
107 result = []
108 for parent, directories, files in os.walk(self._path):
109 for filename in files:
110 file_path = os.path.join(parent, filename)
111 rel_path = os.path.relpath(file_path, self._path)
112 result.append(rel_path)
113 return result
114
115 def read(self, path):
116 content = None
117 with open(os.path.join(self._path, path)) as fp:
118 content = fp.read()
119 return content
120
121
122 def copy_metadata(filename, tmpdir):
123 """Copy the used metadata to the used temporary directory."""
124 path = os.path.join(os.path.dirname(__file__), filename)
125 destination = str(tmpdir.join(filename))
126 shutil.copy(path, destination)
127
128
129 def run_webext_build(ext_type, build_opt, srcdir, buildnum=None, keyfile=None):
130 """Run a build process."""
131 if build_opt == 'build':
132 build_args = ['build']
133 elif build_opt == 'release':
134 build_args = ['build', '-r']
135 else:
136 build_args = ['devenv']
137
138 args = ['build.py', '-t', ext_type] + build_args
139
140 if keyfile:
141 args += ['-k', keyfile]
142 if buildnum:
143 args += ['-b', buildnum]
144
145 processArgs(str(srcdir), args)
146
147
148 def locale_files(languages, rootfolder, srcdir):
149 """Generate example locales.
150
151 languages: tuple, list or set of languages to include
152 rootdir: folder-name to create the locale-folders in
153 """
154 for lang in languages:
155 subfolders = rootfolder.split(os.pathsep) + [lang, 'messages.json']
156 json_file = srcdir.ensure(*subfolders)
157 if lang == defaultLocale:
158 json_file.write(MESSAGES_EN_US)
159 else:
160 json_file.write('{}')
161
162
163 def assert_all_locales_present(package, prefix):
164 names = {x for x in package.namelist() if
165 x.startswith(os.path.join(prefix, '_locales'))}
166 assert names == {
167 os.path.join(prefix, '_locales', lang, 'messages.json')
168 for lang in ALL_LANGUAGES
169 }
170
171
172 @pytest.fixture
173 def srcdir(tmpdir):
174 """Source directory for building the package."""
175 for size in ['44', '50', '150']:
176 path = tmpdir.join('chrome', 'icons', 'abp-{}.png'.format(size))
177 path.write(size, ensure=True)
178
179 tmpdir.join('bar.json').write(json.dumps({}))
180 return tmpdir
181
182
183 @pytest.fixture
184 def locale_modules(tmpdir):
185 mod_dir = tmpdir.mkdir('_modules')
186 lang_dir = mod_dir.mkdir('en_US')
187 lang_dir.join('module.json').write(json.dumps(LOCALES_MODULE))
188
189
190 @pytest.fixture
191 def icons(srcdir):
192 icons_dir = srcdir.mkdir('icons')
193 for filename in ['abp-16.png', 'abp-19.png', 'abp-53.png']:
194 shutil.copy(
195 os.path.join(os.path.dirname(__file__), filename),
196 os.path.join(str(icons_dir), filename),
197 )
198
199
200 @pytest.fixture
201 def all_lang_locales(tmpdir):
202 return locale_files(ALL_LANGUAGES, '_locales', tmpdir)
203
204
205 @pytest.fixture
206 def chrome_metadata(tmpdir):
207 filename = 'metadata.chrome'
208 copy_metadata(filename, tmpdir)
209
210
211 @pytest.fixture
212 def gecko_webext_metadata(tmpdir, chrome_metadata):
213 filename = 'metadata.gecko'
214 copy_metadata(filename, tmpdir)
215
216
217 @pytest.fixture
218 def edge_metadata(tmpdir):
219 filename = 'metadata.edge'
220 copy_metadata(filename, tmpdir)
221
222 return packager.readMetadata(str(tmpdir), 'edge')
223
224
225 @pytest.fixture
226 def keyfile(tmpdir):
227 """Test-privatekey for signing chrome release-package."""
228 return os.path.join(os.path.dirname(__file__), 'chrome_rsa.pem')
229
230
231 @pytest.fixture
232 def lib_files(tmpdir):
233 files = packager.Files(['lib'], set())
234 files['ext/a.js'] = 'require("./c.js");\nvar bar;'
235 files['lib/b.js'] = 'var foo;'
236 files['ext/c.js'] = 'var this_is_c;'
237
238 tmpdir.mkdir('lib').join('b.js').write(files['lib/b.js'])
239 ext_dir = tmpdir.mkdir('ext')
240 ext_dir.join('a.js').write(files['ext/a.js'])
241 ext_dir.join('c.js').write(files['ext/c.js'])
242
243 return files
244
245
246 def comparable_xml(xml):
247 """Create a nonambiguous representation of a given XML tree."""
248 def get_leafs_string(tree):
249 """Recursively build a string representing all xml leaf-nodes."""
250 root_str = '{}|{}|{}'.format(tree.tag, tree.tail, tree.text).strip()
251 result = []
252
253 if len(tree) > 0:
254 for subtree in tree:
255 for leaf in get_leafs_string(subtree):
256 result.append('{}__{}'.format(root_str, leaf))
257 for k, v in tree.attrib.items():
258 result.append('{}__{}:{}'.format(root_str, k, v))
259 else:
260 result.append(root_str)
261 return result
262
263 # XML data itself shall not be sorted, hence we can safely sort
264 # our string representations in order to produce a valid unified diff.
265 return sorted(get_leafs_string(ElementTree.fromstring(xml)))
266
267
268 def comparable_json(json_string):
269 """Create a nonambiguous representation of a given JSON string."""
270 return json.dumps(
271 json.loads(json_string), sort_keys=True, indent=0
272 ).split('\n')
273
274
275 def assert_manifest_content(manifest, expected_path):
276 extension = os.path.basename(expected_path).split('.')[-1]
277
278 with open(expected_path, 'r') as fp:
279 if extension == 'xml':
280 generated = comparable_xml(manifest)
281 expected = comparable_xml(fp.read())
282 else:
283 generated = comparable_json(manifest)
284 expected = comparable_json(fp.read())
285
286 diff = list(difflib.unified_diff(generated, expected, n=0))
287 assert len(diff) == 0, '\n'.join(diff)
288
289
290 def assert_webpack_bundle(package, prefix, excluded=False):
291 libfoo = package.read(os.path.join(prefix, 'lib/foo.js'))
292 libfoomap = package.read(os.path.join(prefix, 'lib/foo.js.map'))
293
294 assert 'var bar;' in libfoo
295 assert 'webpack:///./ext/a.js' in libfoomap
296
297 assert 'var this_is_c;' in libfoo
298 assert 'webpack:///./ext/c.js' in libfoomap
299
300 assert ('var foo;' in libfoo) != excluded
301 assert ('webpack:///./lib/b.js' in libfoomap) != excluded
302
303
304 def assert_devenv_scripts(package, prefix, devenv):
305 manifest = json.loads(package.read(os.path.join(prefix, 'manifest.json')))
306 filenames = package.namelist()
307 scripts = [
308 'ext/common.js',
309 'ext/background.js',
310 ]
311
312 assert (os.path.join(prefix, 'qunit/index.html') in filenames) == devenv
313 assert (os.path.join(prefix, 'devenvPoller__.js') in filenames) == devenv
314 assert (os.path.join(prefix, 'devenvVersion__') in filenames) == devenv
315
316 if devenv:
317 quint_index = package.read(os.path.join(prefix, 'qunit/index.html'))
318 assert '../ext/common.js' in quint_index
319 assert '../ext/background.js' in quint_index
320
321 assert set(manifest['background']['scripts']) == set(
322 scripts + ['devenvPoller__.js']
323 )
324 else:
325 assert set(manifest['background']['scripts']) == set(scripts)
326
327
328 def assert_base_files(package, platform):
329 filenames = set(package.namelist())
330
331 if platform in {'chrome', 'gecko'}:
Sebastian Noack 2017/10/19 05:37:54 While, the files checked in the else-clause are ex
tlucas 2017/10/20 12:05:19 No, changed it.
332 assert 'bar.json' in filenames
333 assert 'manifest.json' in filenames
334 assert 'lib/foo.js' in filenames
335 assert 'foo/logo_50.png' in filenames
336 assert 'icons/logo_150.png' in filenames
337 else:
338 assert 'AppxManifest.xml' in filenames
339 assert 'AppxBlockMap.xml' in filenames
340 assert '[Content_Types].xml' in filenames
341
342 assert 'Extension/bar.json' in filenames
343 assert 'Extension/lib/foo.js' in filenames
344
345 assert package.read('Assets/logo_44.png') == '44'
346 assert package.read('Extension/icons/abp-44.png') == '44'
347
348
349 def assert_chrome_signature(filename, keyfile):
350 with open(filename, 'r') as fp:
351 content = fp.read()
352
353 _, _, l_pubkey, l_signature = unpack('<4sIII', content[:16])
354 signature = content[16 + l_pubkey: 16 + l_pubkey + l_signature]
355
356 digest = SHA.new()
357 with open(keyfile, 'r') as fp:
358 rsa_key = RSA.importKey(fp.read())
359
360 signer = PKCS1_v1_5.new(rsa_key)
361
362 digest.update(content[16 + l_pubkey + l_signature:])
363 assert signer.verify(digest, signature)
364
365
366 def assert_locale_upfix(package):
367 translations = [
368 json.loads(package.read('_locales/{}/messages.json'.format(lang)))
369 for lang in ALL_LANGUAGES
370 ]
371
372 manifest = package.read('manifest.json')
373
374 # Chrome Web Store requires descriptive translations to be present in
375 # every language.
376 for match in re.finditer(r'__MSG_(\S+)__', manifest):
377 name = match.group(1)
378
379 for other in translations[1:]:
380 assert translations[0][name]['message'] == other[name]['message']
381
382
383 @pytest.mark.usefixtures(
384 'all_lang_locales',
385 'locale_modules',
386 'icons',
387 'lib_files',
388 'chrome_metadata',
389 'gecko_webext_metadata',
390 'edge_metadata',
391 )
392 @pytest.mark.parametrize('platform,command,buildnum', [
393 ('chrome', 'build', '1337'),
394 ('chrome', 'devenv', None),
395 ('chrome', 'release', None),
396 ('gecko', 'build', '1337'),
397 ('gecko', 'devenv', None),
398 ('gecko', 'release', None),
399 ('edge', 'build', '1337'),
400 ('edge', 'devenv', None),
401 ('edge', 'release', None),
402 ])
403 def test_build_webext(platform, command, keyfile, tmpdir, srcdir,
404 buildnum, capsys):
405 release = command == 'release'
406 devenv = command == 'devenv'
407
408 if platform == 'chrome' and release:
409 key = keyfile
410 else:
411 key = None
412
413 manifests = {
414 'gecko': [('', 'manifest', 'json')],
415 'chrome': [('', 'manifest', 'json')],
416 'edge': [('', 'AppxManifest', 'xml'),
417 ('Extension', 'manifest', 'json')],
418 }
419
420 filenames = {
421 'gecko': 'adblockplusfirefox-1.2.3{}.xpi',
422 'chrome': 'adblockpluschrome-1.2.3{{}}.{}'.format(
423 {True: 'crx', False: 'zip'}[release]
424 ),
425 'edge': 'adblockplusedge-1.2.3{}.appx',
426 }
427
428 if platform == 'edge':
429 prefix = 'Extension'
430 else:
431 prefix = ''
432
433 run_webext_build(platform, command, srcdir, buildnum=buildnum,
434 keyfile=key)
435
436 # The makeIcons() in packagerChrome.py should warn about non-square
437 # icons via stderr.
438 out, err = capsys.readouterr()
439 assert 'icon should be square' in err
440
441 if devenv:
442 content_class = DirContent
443 out_file_path = os.path.join(str(srcdir), 'devenv.' + platform)
444 else:
445 content_class = ZipContent
446
447 if release:
448 add_version = ''
449 else:
450 add_version = '.' + buildnum
451
452 out_file = filenames[platform].format(add_version)
453
454 out_file_path = os.path.abspath(os.path.join(
455 os.path.dirname(__file__), os.pardir, out_file))
456 assert os.path.exists(out_file_path)
457
458 if release and platform == 'chrome':
459 assert_chrome_signature(out_file_path, keyfile)
460
461 with content_class(out_file_path) as package:
462 assert_base_files(package, platform)
463 assert_all_locales_present(package, prefix)
464 assert_webpack_bundle(package, prefix, platform == 'gecko')
465
466 if platform == 'chrome':
467 assert_locale_upfix(package)
468
469 assert_devenv_scripts(package, prefix, devenv)
470
471 for folder, name, ext in manifests[platform]:
472 filename = '{{}}_{}_{}.{{}}'.format(platform, command)
473 expected = os.path.join(
474 os.path.dirname(__file__),
475 'expecteddata',
476 filename.format(name, ext),
477 )
478
479 assert_manifest_content(
480 package.read(os.path.join(folder, '{}.{}'.format(name, ext))),
481 expected,
482 )
OLDNEW
« no previous file with comments | « tests/test_packagerEdge.py ('k') | tox.ini » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld