Left: | ||
Right: |
LEFT | RIGHT |
---|---|
1 # This Source Code Form is subject to the terms of the Mozilla Public | 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 | 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/. | 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
4 | 4 |
5 import os | |
6 import io | |
5 | 7 |
6 import pytest | 8 import pytest |
7 | 9 |
8 import json | 10 import json |
9 | 11 |
10 from zipfile import ZipFile | 12 from zipfile import ZipFile |
11 | 13 |
12 from xml.etree import ElementTree | 14 from xml.etree import ElementTree |
13 from itertools import product | |
14 | 15 |
15 from buildtools import packagerGecko | 16 from buildtools import packagerGecko |
16 from buildtools import localeTools | 17 from buildtools import localeTools |
17 | 18 |
18 from buildtools.packager import readMetadata, getBuildVersion, Files | 19 from buildtools.packager import getBuildVersion, Files, readMetadata |
19 from functools import reduce | 20 from buildtools.tests.tools import copy_metadata |
20 | |
21 TR_FA = [True, False] | |
22 | 21 |
23 MESSAGES = '\n'.join(( | 22 MESSAGES = '\n'.join(( |
24 'name=Name {0}', | 23 'name=Name {0}', |
25 'description=Awesome description {0}', | 24 'description=Awesome description {0}', |
26 )) | 25 )) |
27 | 26 |
28 | 27 |
29 @pytest.fixture | 28 @pytest.fixture |
30 def scripts(tmp_dir): | 29 def gecko_metadata(tmpdir): |
31 """Examplary scripts for testing addMissingFiles""" | 30 filename = 'metadata.gecko' |
Vasily Kuznetsov
2017/08/03 16:52:32
AFAIK "examplary" nowadays is usually spelled "exe
tlucas
2017/08/03 21:26:02
Your are right - but i like the "outstanding" part
tlucas
2017/08/04 14:52:00
Done.
| |
32 lib_dir = tmp_dir.mkdir('lib') | 31 copy_metadata(filename, tmpdir) |
32 | |
33 return readMetadata(str(tmpdir), 'gecko') | |
34 | |
35 | |
36 @pytest.fixture | |
37 def scripts(tmpdir): | |
38 """Example scripts for testing addMissingFiles""" | |
39 lib_dir = tmpdir.mkdir('lib') | |
33 lib_dir.join('ext.js').write('require("hooks");') | 40 lib_dir.join('ext.js').write('require("hooks");') |
34 | 41 |
35 content_dir = tmp_dir.mkdir('chrome').mkdir('content') | 42 content_dir = tmpdir.mkdir('chrome').mkdir('content') |
36 content_dir.join('common.js').write('require("hooks");') | 43 content_dir.join('common.js').write('require("hooks");') |
37 | 44 |
38 | 45 |
39 @pytest.fixture | 46 @pytest.fixture |
40 def prefs_json(tmp_dir): | 47 def prefs_json(tmpdir): |
41 """Minimal .json file for testing processJSONFiles""" | 48 """Minimal .json file for testing processJSONFiles""" |
42 lib_dir = tmp_dir.mkdir('lib') | 49 lib_dir = tmpdir.mkdir('lib') |
43 lib_dir.join('prefs.json').write(json.dumps( | 50 lib_dir.join('prefs.json').write(json.dumps( |
44 {'foo': 'bar'} | 51 {'foo': 'bar'} |
45 )) | 52 )) |
46 | 53 |
47 | 54 |
48 @pytest.fixture | 55 @pytest.fixture |
49 def locales(tmp_dir): | 56 def locales(tmpdir): |
50 """Minimal locales for testing locale-processing""" | 57 """Minimal locales for testing locale-processing""" |
51 chrome_dir = tmp_dir.mkdir('chrome') | 58 chrome_dir = tmpdir.mkdir('chrome') |
52 locale_dir = chrome_dir.mkdir('locale') | 59 locale_dir = chrome_dir.mkdir('locale') |
53 | 60 |
54 data = { | 61 data = { |
55 'name': {'message': 'Name translated'}, | 62 'name': {'message': 'Name translated'}, |
56 'description': {'message': 'Description translated'} | 63 'description': {'message': 'Description translated'}, |
Vasily Kuznetsov
2017/08/03 16:52:32
When you use multiline layout for lists, sets and
tlucas
2017/08/03 21:26:03
As you can see in other collection-defintions, i n
tlucas
2017/08/04 14:51:59
Done.
| |
57 } | 64 } |
58 | 65 |
59 for locale in ['en-US', 'de', 'kn']: | 66 for locale in ['en-US', 'de', 'kn']: |
60 new_dir = locale_dir.mkdir(locale) | 67 new_dir = locale_dir.mkdir(locale) |
61 new_dir.join('meta.properties').write(MESSAGES.format(locale)) | 68 new_dir.join('meta.properties').write(MESSAGES.format(locale)) |
62 new_dir.join('test.json').write(json.dumps(data)) | 69 new_dir.join('test.json').write(json.dumps(data)) |
63 if locale == 'kn': | 70 if locale == 'kn': |
64 new_dir.join('.incomplete').write('') | 71 new_dir.join('.incomplete').write('') |
65 | 72 |
66 | 73 |
67 @pytest.fixture | 74 @pytest.fixture |
68 def subscriptions(tmp_dir): | 75 def subscriptions(tmpdir): |
69 """Examplary sbuscription-configuration""" | 76 """Examplary sbuscription-configuration""" |
70 tmp_dir.join('subs.xml').write('\n'.join(( | 77 tmpdir.join('subs.xml').write( |
Vasily Kuznetsov
2017/08/03 16:52:32
Wouldn't it be easier to just use a string literal
tlucas
2017/08/03 21:26:03
Acknowledged.
tlucas
2017/08/04 14:52:00
Done.
| |
71 '<subscriptions>', | 78 '<subscriptions>' |
72 '<subscription title="EasyList"', | 79 '<subscription title="EasyList"' |
73 'specialization="English"', | 80 ' specialization="English"' |
74 'url="https://easylist-downloads.adblockplus.org/easylist.txt"', | 81 ' url="https://easylist-downloads.adblockplus.org/easylist.txt"' |
75 'homepage="https://easylist.adblockplus.org/"', | 82 ' homepage="https://easylist.adblockplus.org/"' |
76 'prefixes="en"', | 83 ' prefixes="en"' |
77 'author="fanboy, MonztA, Famlam, Khrin"', | 84 ' author="fanboy, MonztA, Famlam, Khrin"' |
78 'type="ads"/>', | 85 ' type="ads"/>' |
79 '</subscriptions>', | 86 '</subscriptions>' |
80 ))) | 87 ) |
81 | 88 |
82 | 89 |
83 def test_package_files(tmpdir): | 90 def test_package_files(tmpdir): |
84 tmpdir.join('foo.xml').write('') | 91 tmpdir.join('foo.xml').write('') |
85 tmpdir.join('foo.txt').write('') | 92 tmpdir.join('foo.txt').write('') |
86 tmpdir.join('foo.js').write('') | 93 tmpdir.join('foo.js').write('') |
87 | 94 |
88 params = { | 95 params = { |
89 'baseDir': str(tmpdir) | 96 'baseDir': str(tmpdir) |
90 } | 97 } |
91 | 98 |
92 files = packagerGecko.getPackageFiles(params) | 99 files = packagerGecko.getPackageFiles(params) |
93 assert 'foo.xml' in files | 100 assert 'foo.xml' in files |
94 assert 'foo.js' in files | 101 assert 'foo.js' in files |
95 assert 'foo.txt' not in files | 102 assert 'foo.txt' not in files |
96 | 103 |
97 | 104 |
98 @pytest.mark.usefixtures('locales') | 105 @pytest.mark.usefixtures('locales') |
99 def test_get_locales(tmp_dir): | 106 @pytest.mark.parametrize('incomplete', [True, False]) |
100 for incomplete in [True, False]: | 107 def test_get_locales(tmpdir, incomplete): |
Vasily Kuznetsov
2017/08/03 16:52:32
This could also be done via parametrize perhaps.
tlucas
2017/08/03 21:26:02
Acknowledged.
tlucas
2017/08/04 14:52:00
Done.
| |
101 locales = packagerGecko.getLocales(str(tmp_dir), incomplete) | 108 locales = packagerGecko.getLocales(str(tmpdir), incomplete) |
102 | 109 |
103 assert 'de' in locales | 110 assert 'de' in locales |
104 assert 'en-US' in locales | 111 assert 'en-US' in locales |
105 assert ('kn' in locales) == incomplete | 112 assert ('kn' in locales) == incomplete |
106 | 113 |
107 | 114 |
108 @pytest.mark.parametrize('metadata_files', ['metadata.gecko'], indirect=True) | 115 @pytest.mark.usefixtures('locales', 'subscriptions') |
109 @pytest.mark.usefixtures('locales', 'metadata_files', 'subscriptions') | 116 @pytest.mark.parametrize('release', [True, False]) |
110 def test_create_manifest(tmp_dir): | 117 @pytest.mark.parametrize('multicompartment', [True, False]) |
Vasily Kuznetsov
2017/08/03 16:52:32
All this code is pretty cool and clever, but it ma
tlucas
2017/08/03 21:26:02
The cool- and cleverness could be a result of foll
tlucas
2017/08/04 14:51:59
Done.
Vasily Kuznetsov
2017/08/10 19:48:27
Acknowledged.
| |
111 def first(elem): | 118 def test_create_manifest(tmpdir, release, multicompartment, gecko_metadata): |
112 return elem[0] | 119 locales = packagerGecko.getLocales(str(tmpdir)) |
113 | 120 contributors = packagerGecko.getContributors(gecko_metadata) |
114 def text(elem): | 121 version = getBuildVersion(str(tmpdir), gecko_metadata, release, None) |
115 return elem.text | 122 |
116 | 123 rdfpath = os.path.join( |
117 def iteritems(func=None): | 124 os.path.dirname(__file__), |
118 def wrapper(elements): | 125 'expecteddata', |
119 for elem in elements: | 126 'manifest_gecko_{}_{}.rdf'.format(release, multicompartment) |
Vasily Kuznetsov
2017/08/10 19:48:28
Nit: comma at the end of the line.
tlucas
2017/08/11 12:17:09
Done.
| |
120 if func: | 127 ) |
121 yield func(elem) | 128 |
122 else: | 129 with io.open(rdfpath, 'r') as fp: |
123 yield elem | 130 expected = ElementTree.fromstring(fp.read()) |
124 return wrapper | 131 |
125 | 132 params = { |
126 metadata = readMetadata(str(tmp_dir), 'gecko') | 133 'baseDir': str(tmpdir), |
127 locales = packagerGecko.getLocales(str(tmp_dir)) | 134 'locales': locales, |
128 contributors = packagerGecko.getContributors(metadata) | 135 'metadata': gecko_metadata, |
129 | 136 'version': version.encode('utf-8'), |
130 namespaces = { | 137 'multicompartment': multicompartment, |
131 'em': 'http://www.mozilla.org/2004/em-rdf#', | 138 'contributors': contributors |
Vasily Kuznetsov
2017/08/10 19:48:28
Nit: comma at the end of the line.
tlucas
2017/08/11 12:17:09
Done.
| |
132 'ns': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', | 139 } |
133 } | 140 manifest = packagerGecko.createManifest(params) |
134 | 141 |
135 base = [ | 142 tree = ElementTree.fromstring(manifest) |
136 ('.//*', [len], 54), | 143 |
137 ('./ns:Description/em:id', [first, text], | 144 from buildtools.tests.tools import get_leafs_string |
138 '{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}'), | 145 assert set(get_leafs_string(tree)) == set(get_leafs_string(expected)) |
139 ('./ns:Description/em:optionsURL', [first, text], | 146 |
140 'chrome://adblockplus/content/ui/settings.xul'), | 147 |
141 ('./ns:Description/em:optionsType', [first, text], '2'), | 148 @pytest.mark.usefixtures('locales') |
142 ('./ns:Description/em:bootstrap', [first, text], 'true'), | 149 def test_fixup_import_locales(files, tmpdir, gecko_metadata): |
143 ('./ns:Description/em:multiprocessCompatible', [first, text], 'true'), | 150 locale_dir = tmpdir.dirpath(tmpdir.basename, 'chrome', 'locale') |
144 ('./ns:Description/em:homepageURL', [first, text], | |
145 'http://adblockplus.org/'), | |
146 ('./ns:Description/em:creator', [first, text], 'Wladimir Palant'), | |
147 ('./ns:Description/em:contributor', [iteritems(text)], | |
148 ['Pety Pete', 'Neil Armstrong', 'Famlam', 'fanboy', 'Khrin', | |
149 'MonztA']), | |
150 ] | |
151 | |
152 base += [ | |
153 ('./ns:Description/em:localized/ns:Description[em:locale="{}"]/em:{}' | |
154 .format(locale, tag), | |
155 [first, text], | |
156 value.format(locale)) | |
157 for locale in locales | |
158 for tag, value in [ | |
159 ('name', 'Name {}'), | |
160 ('description', 'Awesome description {}') | |
161 ] | |
162 ] | |
163 | |
164 tags = ['minVersion', 'maxVersion'] | |
165 apps = metadata.items('compat') | |
166 comp = [ | |
167 ( | |
168 packagerGecko.KNOWN_APPS.get(app[0]), | |
169 tags[i], | |
170 app[1].split('/')[i] | |
171 ) for app in apps for i in range(2) | |
172 ] | |
173 | |
174 base += [ | |
175 (''.join(( | |
176 './ns:Description/em:targetApplication/', | |
177 'ns:Description[em:id="{}"]/em:{}' | |
178 )) | |
179 .format(mapped_id, tag), | |
180 [first, text], | |
181 value) | |
182 for mapped_id, tag, value in comp | |
183 ] | |
184 | |
185 for release, multicompartment in product(TR_FA, TR_FA): | |
Vasily Kuznetsov
2017/08/03 16:52:32
Maybe better do this with parametrization?
tlucas
2017/08/03 21:26:02
Acknowledged.
tlucas
2017/08/04 14:52:00
Done.
| |
186 version = getBuildVersion(str(tmp_dir), metadata, release, None) | |
187 expected = base + [ | |
188 ('./ns:Description/em:version', [first, text], version), | |
189 ] | |
190 params = { | |
191 'baseDir': str(tmp_dir), | |
192 'locales': locales, | |
193 'metadata': metadata, | |
194 'version': version.encode('utf-8'), | |
195 'multicompartment': multicompartment, | |
196 'contributors': contributors | |
197 } | |
198 manifest = packagerGecko.createManifest(params) | |
199 | |
200 tree = ElementTree.fromstring(manifest) | |
201 | |
202 with open('/tmp/test.xml', 'w') as fp: | |
Vasily Kuznetsov
2017/08/03 16:52:32
Why use '/tmp' instead of tmpdir fixture? Also kee
tlucas
2017/08/03 21:26:02
Acknowledged.
tlucas
2017/08/04 14:52:00
Done (removed).
| |
203 fp.write(manifest) | |
204 | |
205 for expression, modifiers, value in expected: | |
206 res = reduce( | |
207 lambda val, func: func(val), | |
208 modifiers, | |
209 tree.findall(expression, namespaces=namespaces)) | |
210 | |
211 from collections import Iterable | |
212 | |
213 if isinstance(res, Iterable) and not isinstance(res, str): | |
214 res = list(res) | |
215 for x in res: | |
Vasily Kuznetsov
2017/08/03 16:52:32
Seems like you just want to compare `res` to `valu
tlucas
2017/08/03 21:26:02
I agree with the assert set() == set() part, but o
tlucas
2017/08/04 14:52:00
I totally missed something. Done.
| |
216 assert x in value | |
217 for x in value: | |
218 assert x in res | |
219 assert res == value | |
220 | |
221 | |
222 @pytest.mark.parametrize('metadata_files', ['metadata.gecko'], indirect=True) | |
223 @pytest.mark.usefixtures('locales', 'metadata_files') | |
224 def test_fixup_import_locales(files, tmp_dir): | |
225 locale_dir = tmp_dir.dirpath(tmp_dir.basename, 'chrome', 'locale') | |
226 locale_dir.mkdir('fr').join('meta.properties')\ | 151 locale_dir.mkdir('fr').join('meta.properties')\ |
Vasily Kuznetsov
2017/08/10 19:48:28
You don't need to break this line, it actually fit
tlucas
2017/08/11 12:17:09
Done.
| |
227 .write(MESSAGES.format('fr')) | 152 .write(MESSAGES.format('fr')) |
228 | 153 |
229 metadata = readMetadata(str(tmp_dir), 'gecko') | 154 locales = packagerGecko.getLocales(str(tmpdir), False) |
230 locales = packagerGecko.getLocales(str(tmp_dir), False) | 155 |
231 | 156 params = { |
232 params = { | 157 'metadata': gecko_metadata, |
233 'metadata': metadata, | |
234 'locales': locales, | 158 'locales': locales, |
235 'baseDir': str(tmp_dir) | 159 'baseDir': str(tmpdir) |
236 } | 160 } |
237 | 161 |
238 # Should add missing fr/test.properties to files | 162 # Should add missing fr/test.properties to files |
239 packagerGecko.fixupLocales(params, files) | 163 packagerGecko.fixupLocales(params, files) |
240 | 164 |
241 packagerGecko.importLocales(params, files) | 165 packagerGecko.importLocales(params, files) |
242 for locale in locales: | 166 for locale in locales: |
243 properties = files['chrome/locale/{}/test.properties'.format(locale)] | 167 properties = files['chrome/locale/{}/test.properties'.format(locale)] |
244 translation_data = list( | 168 translation_data = list( |
245 localeTools.parsePropertiesString(properties, '')) | 169 localeTools.parsePropertiesString(properties, '')) |
246 | 170 |
247 for trans in [ | 171 for trans in [ |
248 ('name', None, 'Name translated'), | 172 ('name', None, 'Name translated'), |
249 ('description', None, 'Description translated')]: | 173 ('description', None, 'Description translated')]: |
250 assert trans in translation_data | 174 assert trans in translation_data |
251 | 175 |
252 | 176 |
253 def test_process_json_files(tmp_dir, prefs_json): | 177 def test_process_json_files(tmpdir, prefs_json): |
254 params = { | 178 params = { |
255 'baseDir': str(tmp_dir), | 179 'baseDir': str(tmpdir), |
256 'jsonRequires': {}, | 180 'jsonRequires': {}, |
257 } | 181 } |
258 | 182 |
259 files = Files(packagerGecko.getPackageFiles(params), set()) | 183 files = Files(packagerGecko.getPackageFiles(params), set()) |
260 files.read(str(tmp_dir)) | 184 files.read(str(tmpdir)) |
261 | 185 |
262 packagerGecko.processJSONFiles(params, files) | 186 packagerGecko.processJSONFiles(params, files) |
263 | 187 |
264 assert params['jsonRequires'] == {'prefs.json': {'foo': 'bar'}} | 188 assert params['jsonRequires'] == {'prefs.json': {'foo': 'bar'}} |
265 | 189 |
266 | 190 |
267 @pytest.mark.parametrize('metadata_files', ['metadata.gecko'], indirect=True) | 191 @pytest.mark.usefixtures('scripts') |
268 @pytest.mark.usefixtures('scripts', 'metadata_files') | 192 def test_add_missing_files(tmpdir, gecko_metadata): |
269 def test_add_missing_files(tmp_dir): | 193 params = { |
270 metadata = readMetadata(str(tmp_dir), 'gecko') | 194 'baseDir': str(tmpdir), |
271 | 195 'metadata': gecko_metadata, |
272 params = { | |
273 'baseDir': str(tmp_dir), | |
274 'metadata': metadata, | |
275 'jsonRequires': {}, | 196 'jsonRequires': {}, |
276 'multicompartment': True, | 197 'multicompartment': True, |
277 'hasWebExtension': False, | 198 'hasWebExtension': False, |
278 } | 199 } |
279 | 200 |
280 files = Files(packagerGecko.getPackageFiles(params), set()) | 201 files = Files(packagerGecko.getPackageFiles(params), set()) |
281 files.read(str(tmp_dir)) | 202 files.read(str(tmpdir)) |
282 | 203 |
283 packagerGecko.addMissingFiles(params, files) | 204 packagerGecko.addMissingFiles(params, files) |
284 | 205 |
285 assert 'let shutdownHandlers = [];' in files['bootstrap.js'] | 206 assert 'let shutdownHandlers = [];' in files['bootstrap.js'] |
286 assert 'Services.obs.addObserver(' in files['bootstrap.js'] | 207 assert 'Services.obs.addObserver(' in files['bootstrap.js'] |
287 for filename in ['bootstrap.js', 'chrome/content/common.js', | 208 for filename in ['bootstrap.js', 'chrome/content/common.js', |
288 'lib/ext.js', 'lib/hooks.js']: | 209 'lib/ext.js', 'lib/hooks.js']: |
289 assert filename in files | 210 assert filename in files |
290 | 211 |
291 | 212 |
292 @pytest.mark.parametrize('metadata_files', ['metadata.gecko'], indirect=True) | 213 @pytest.mark.usefixtures('gecko_metadata', 'locales', 'subscriptions') |
293 @pytest.mark.usefixtures('metadata_files', 'locales', 'subscriptions') | 214 @pytest.mark.parametrize('release', [True, False]) |
294 def test_create_build(tmp_dir, capsys): | 215 @pytest.mark.parametrize('multicompartment', [True, False]) |
216 @pytest.mark.parametrize('all_locales', ['all', None]) | |
217 def test_create_build(tmpdir, capsys, release, multicompartment, all_locales): | |
295 base_files = [ | 218 base_files = [ |
296 'bootstrap.js', | 219 'bootstrap.js', |
297 'chrome/locale/de/test.json', | 220 'chrome/locale/de/test.json', |
298 'chrome/locale/de/test.properties', | 221 'chrome/locale/de/test.properties', |
299 'chrome/locale/en-US/test.json', | 222 'chrome/locale/en-US/test.json', |
300 'chrome/locale/en-US/test.properties', | 223 'chrome/locale/en-US/test.properties', |
301 'install.rdf', | 224 'install.rdf', |
302 'subs.xml' | 225 'subs.xml' |
303 ] | 226 ] |
304 | 227 |
305 for all_locales, release, multicompartment in product( | 228 if all_locales is None: |
Vasily Kuznetsov
2017/08/03 16:52:32
Also maybe parametrization?
tlucas
2017/08/03 21:26:03
Acknowledged.
tlucas
2017/08/04 14:51:59
Done.
| |
306 ['all', None], TR_FA, TR_FA): | 229 expected = base_files |
307 | 230 else: |
308 if all_locales is None: | 231 expected = base_files + [ |
309 expected = base_files | 232 'chrome/locale/kn/test.json', |
310 else: | 233 'chrome/locale/kn/test.properties' |
311 expected = base_files + [ | 234 ] |
312 'chrome/locale/kn/test.json', | 235 |
313 'chrome/locale/kn/test.properties' | 236 out_file = tmpdir.join('{}_{}_{}.zip'.format( |
314 ] | 237 all_locales, release, multicompartment)) |
315 | 238 |
316 out_file = tmp_dir.join('{}_{}_{}.zip'.format( | 239 out_file = str(out_file) |
317 all_locales, release, multicompartment)) | 240 |
318 | 241 packagerGecko.createBuild( |
319 out_file = str(out_file) | 242 str(tmpdir), |
320 | 243 locales=all_locales, |
321 packagerGecko.createBuild( | 244 outFile=out_file, |
322 str(tmp_dir), | 245 releaseBuild=release, |
323 locales=all_locales, | 246 multicompartment=multicompartment) |
324 outFile=out_file, | 247 |
325 releaseBuild=release, | 248 out, err = capsys.readouterr() |
326 multicompartment=multicompartment) | 249 |
327 | 250 assert err ==\ |
328 out, err = capsys.readouterr() | 251 "Warning: Mapped file adblockplusui/firstRun.html doesn't exist\n" |
329 | 252 |
330 assert err ==\ | 253 with ZipFile(out_file, 'r') as zipfp: |
331 "Warning: Mapped file adblockplusui/firstRun.html doesn't exist\n" | 254 zipfp.testzip() |
332 | 255 |
333 with ZipFile(out_file, 'r') as zipfp: | 256 filenames = [zipinfo.filename for zipinfo in zipfp.infolist()] |
334 zipfp.testzip() | 257 |
335 | 258 for name in expected: |
336 filenames = [zipinfo.filename for zipinfo in zipfp.infolist()] | 259 assert name in filenames |
337 | |
338 for name in expected: | |
339 assert name in filenames | |
LEFT | RIGHT |