1
|
# SPDX-License-Identifier: CC0-1.0
|
2
|
|
3
|
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
|
4
|
#
|
5
|
# Available under the terms of Creative Commons Zero v1.0 Universal.
|
6
|
|
7
|
import pytest
|
8
|
import tempfile
|
9
|
import re
|
10
|
import json
|
11
|
from pathlib import Path, PurePosixPath
|
12
|
from zipfile import ZipFile
|
13
|
from tempfile import TemporaryDirectory
|
14
|
|
15
|
from hydrilla.builder import local_apt
|
16
|
from hydrilla.builder.common_errors import *
|
17
|
|
18
|
here = Path(__file__).resolve().parent
|
19
|
|
20
|
from .helpers import *
|
21
|
|
22
|
@pytest.fixture
|
23
|
def mock_cache_dir(monkeypatch):
|
24
|
"""Make local_apt.py cache files to a temporary directory."""
|
25
|
with tempfile.TemporaryDirectory() as td:
|
26
|
td_path = Path(td)
|
27
|
monkeypatch.setattr(local_apt, 'default_apt_cache_dir', td_path)
|
28
|
yield td_path
|
29
|
|
30
|
@pytest.fixture
|
31
|
def mock_gnupg_import(monkeypatch, mock_cache_dir):
|
32
|
"""Mock gnupg library when imported dynamically."""
|
33
|
|
34
|
gnupg_mock_dir = mock_cache_dir / 'gnupg_mock'
|
35
|
gnupg_mock_dir.mkdir()
|
36
|
(gnupg_mock_dir / 'gnupg.py').write_text('GPG = None\n')
|
37
|
|
38
|
monkeypatch.syspath_prepend(str(gnupg_mock_dir))
|
39
|
|
40
|
import gnupg
|
41
|
|
42
|
keyring_path = mock_cache_dir / 'master_keyring.gpg'
|
43
|
|
44
|
class MockedImportResult:
|
45
|
"""gnupg.ImportResult replacement"""
|
46
|
def __init__(self):
|
47
|
"""Initialize MockedImportResult object."""
|
48
|
self.imported = 1
|
49
|
|
50
|
class MockedGPG:
|
51
|
"""GPG replacement that does not really invoke GPG."""
|
52
|
def __init__(self, keyring):
|
53
|
"""Verify the keyring path and initialize MockedGPG."""
|
54
|
assert keyring == str(keyring_path)
|
55
|
|
56
|
self.known_keys = {*keyring_path.read_text().split('\n')} \
|
57
|
if keyring_path.exists() else set()
|
58
|
|
59
|
def recv_keys(self, keyserver, key):
|
60
|
"""Mock key receiving - record requested key as received."""
|
61
|
assert keyserver == local_apt.default_keyserver
|
62
|
assert key not in self.known_keys
|
63
|
|
64
|
self.known_keys.add(key)
|
65
|
keyring_path.write_text('\n'.join(self.known_keys))
|
66
|
|
67
|
return MockedImportResult()
|
68
|
|
69
|
def list_keys(self, keys=None):
|
70
|
"""Mock key listing - return a list with dummy items."""
|
71
|
if keys is None:
|
72
|
return ['dummy'] * len(self.known_keys)
|
73
|
else:
|
74
|
return ['dummy' for k in keys if k in self.known_keys]
|
75
|
|
76
|
def export_keys(self, keys, **kwargs):
|
77
|
"""
|
78
|
Mock key export - check that the call has the expected arguments and
|
79
|
return a dummy bytes array.
|
80
|
"""
|
81
|
assert kwargs['armor'] == False
|
82
|
assert kwargs['minimal'] == True
|
83
|
assert {*keys} == self.known_keys
|
84
|
|
85
|
return b'<dummy keys export>'
|
86
|
|
87
|
monkeypatch.setattr(gnupg, 'GPG', MockedGPG)
|
88
|
|
89
|
def process_run_args(command, kwargs, expected_command):
|
90
|
"""
|
91
|
Perform assertions common to all mocked subprocess.run() invocations and
|
92
|
extract variable parts of the command line (if any).
|
93
|
"""
|
94
|
assert kwargs['env'] == {'LANG': 'en_US'}
|
95
|
assert kwargs['capture_output'] == True
|
96
|
|
97
|
return process_command(command, expected_command)
|
98
|
|
99
|
def run_apt_get_update(command, returncode=0, **kwargs):
|
100
|
"""
|
101
|
Instead of running an 'apt-get update' command just touch some file in apt
|
102
|
root to indicate that the call was made.
|
103
|
"""
|
104
|
expected = ['apt-get', '-c', '<conf_path>', 'update']
|
105
|
conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
|
106
|
|
107
|
(conf_path.parent / 'update_called').touch()
|
108
|
|
109
|
return MockedCompletedProcess(command, returncode,
|
110
|
text_output=kwargs.get('text'))
|
111
|
|
112
|
"""
|
113
|
Output of 'apt-get install --yes --just-print libjs-mathjax' on some APT-based
|
114
|
system.
|
115
|
"""
|
116
|
sample_install_stdout = '''\
|
117
|
NOTE: This is only a simulation!
|
118
|
apt-get needs root privileges for real execution.
|
119
|
Keep also in mind that locking is deactivated,
|
120
|
so don't depend on the relevance to the real current situation!
|
121
|
Reading package lists...
|
122
|
Building dependency tree...
|
123
|
Reading state information...
|
124
|
The following additional packages will be installed:
|
125
|
fonts-mathjax
|
126
|
Suggested packages:
|
127
|
fonts-mathjax-extras fonts-stix libjs-mathjax-doc
|
128
|
The following NEW packages will be installed:
|
129
|
fonts-mathjax libjs-mathjax
|
130
|
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
|
131
|
Inst fonts-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
132
|
Inst libjs-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
133
|
Conf fonts-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
134
|
Conf libjs-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
135
|
'''
|
136
|
|
137
|
def run_apt_get_install(command, returncode=0, **kwargs):
|
138
|
"""
|
139
|
Instead of running an 'apt-get install' command just print a possible
|
140
|
output of one.
|
141
|
"""
|
142
|
expected = ['apt-get', '-c', '<conf_path>', 'install',
|
143
|
'--yes', '--just-print', 'libjs-mathjax']
|
144
|
|
145
|
conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
|
146
|
|
147
|
return MockedCompletedProcess(command, returncode,
|
148
|
stdout=sample_install_stdout,
|
149
|
text_output=kwargs.get('text'))
|
150
|
|
151
|
def run_apt_get_download(command, returncode=0, **kwargs):
|
152
|
"""
|
153
|
Instead of running an 'apt-get download' command just write some dummy
|
154
|
.deb to the appropriate directory.
|
155
|
"""
|
156
|
expected = ['apt-get', '-c', '<conf_path>', 'download']
|
157
|
if 'libjs-mathjax' in command:
|
158
|
expected.append('libjs-mathjax')
|
159
|
else:
|
160
|
expected.append('fonts-mathjax=0:2.7.9+dfsg-1')
|
161
|
expected.append('libjs-mathjax=0:2.7.9+dfsg-1')
|
162
|
|
163
|
conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
|
164
|
|
165
|
destination = Path(kwargs.get('cwd') or Path.cwd())
|
166
|
|
167
|
package_name_regex = re.compile(r'^[^=]+-mathjax')
|
168
|
|
169
|
for word in expected:
|
170
|
match = package_name_regex.match(word)
|
171
|
if match:
|
172
|
filename = f'{match.group(0)}_0%3a2.7.9+dfsg-1_all.deb'
|
173
|
deb_path = destination / filename
|
174
|
deb_path.write_text(f'dummy {deb_path.name}')
|
175
|
|
176
|
return MockedCompletedProcess(command, returncode,
|
177
|
text_output=kwargs.get('text'))
|
178
|
|
179
|
def run_apt_get_source(command, returncode=0, **kwargs):
|
180
|
"""
|
181
|
Instead of running an 'apt-get source' command just write some dummy
|
182
|
"tarballs" to the appropriate directory.
|
183
|
"""
|
184
|
expected = ['apt-get', '-c', '<conf_path>', 'source',
|
185
|
'--download-only', 'libjs-mathjax=0:2.7.9+dfsg-1']
|
186
|
if 'fonts-mathjax=0:2.7.9+dfsg-1' in command:
|
187
|
if command[-1] == 'fonts-mathjax=0:2.7.9+dfsg-1':
|
188
|
expected.append('fonts-mathjax=0:2.7.9+dfsg-1')
|
189
|
else:
|
190
|
expected.insert(-1, 'fonts-mathjax=0:2.7.9+dfsg-1')
|
191
|
|
192
|
destination = Path(kwargs.get('cwd') or Path.cwd())
|
193
|
for filename in [
|
194
|
'mathjax_2.7.9+dfsg-1.debian.tar.xz',
|
195
|
'mathjax_2.7.9+dfsg-1.dsc',
|
196
|
'mathjax_2.7.9+dfsg.orig.tar.xz'
|
197
|
]:
|
198
|
(destination / filename).write_text(f'dummy {filename}')
|
199
|
|
200
|
return MockedCompletedProcess(command, returncode,
|
201
|
text_output=kwargs.get('text'))
|
202
|
|
203
|
def make_run_apt_get(**returncodes):
|
204
|
"""
|
205
|
Produce a function that chooses and runs the appropriate one of
|
206
|
subprocess_run_apt_get_*() mock functions.
|
207
|
"""
|
208
|
def mock_run(command, **kwargs):
|
209
|
"""
|
210
|
Chooses and runs the appropriate one of subprocess_run_apt_get_*() mock
|
211
|
functions.
|
212
|
"""
|
213
|
for subcommand, run in [
|
214
|
('update', run_apt_get_update),
|
215
|
('install', run_apt_get_install),
|
216
|
('download', run_apt_get_download),
|
217
|
('source', run_apt_get_source)
|
218
|
]:
|
219
|
if subcommand in command:
|
220
|
returncode = returncodes.get(f'{subcommand}_code', 0)
|
221
|
return run(command, returncode, **kwargs)
|
222
|
|
223
|
raise Exception('Unknown command: {}'.format(' '.join(command)))
|
224
|
|
225
|
return mock_run
|
226
|
|
227
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
|
228
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
229
|
def test_local_apt_contextmanager(mock_cache_dir):
|
230
|
"""
|
231
|
Verify that the local_apt() function creates a proper apt environment and
|
232
|
that it also properly restores it from cache.
|
233
|
"""
|
234
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
235
|
|
236
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
237
|
apt_root = Path(apt.apt_conf).parent.parent
|
238
|
|
239
|
assert (apt_root / 'etc' / 'trusted.gpg').read_bytes() == \
|
240
|
b'<dummy keys export>'
|
241
|
|
242
|
assert (apt_root / 'etc' / 'update_called').exists()
|
243
|
|
244
|
assert (apt_root / 'etc' / 'apt.sources.list').read_text() == \
|
245
|
'deb-src sth\ndeb sth'
|
246
|
|
247
|
conf_lines = (apt_root / 'etc' / 'apt.conf').read_text().split('\n')
|
248
|
|
249
|
# check mocked keyring
|
250
|
assert {*local_apt.default_keys} == \
|
251
|
{*(mock_cache_dir / 'master_keyring.gpg').read_text().split('\n')}
|
252
|
|
253
|
assert not apt_root.exists()
|
254
|
|
255
|
expected_conf = {
|
256
|
'Architecture': 'amd64',
|
257
|
'Dir': str(apt_root),
|
258
|
'Dir::State': f'{apt_root}/var/lib/apt',
|
259
|
'Dir::State::status': f'{apt_root}/var/lib/dpkg/status',
|
260
|
'Dir::Etc::SourceList': f'{apt_root}/etc/apt.sources.list',
|
261
|
'Dir::Etc::SourceParts': '',
|
262
|
'Dir::Cache': f'{apt_root}/var/cache/apt',
|
263
|
'pkgCacheGen::Essential': 'none',
|
264
|
'Dir::Etc::Trusted': f'{apt_root}/etc/trusted.gpg',
|
265
|
}
|
266
|
|
267
|
conf_regex = re.compile(r'^(?P<key>\S+)\s"(?P<val>\S*)";$')
|
268
|
assert dict([(m.group('key'), m.group('val'))
|
269
|
for l in conf_lines if l for m in [conf_regex.match(l)]]) == \
|
270
|
expected_conf
|
271
|
|
272
|
with ZipFile(mock_cache_dir / f'apt_{sources_list.identity()}.zip') as zf:
|
273
|
# reuse the same APT, its cached zip file should exist now
|
274
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
275
|
apt_root = Path(apt.apt_conf).parent.parent
|
276
|
|
277
|
expected_members = {*apt_root.rglob('*')}
|
278
|
expected_members.remove(apt_root / 'etc' / 'apt.conf')
|
279
|
expected_members.remove(apt_root / 'etc' / 'trusted.gpg')
|
280
|
|
281
|
names = zf.namelist()
|
282
|
assert len(names) == len(expected_members)
|
283
|
|
284
|
for name in names:
|
285
|
path = apt_root / name
|
286
|
assert path in expected_members
|
287
|
assert zf.read(name) == \
|
288
|
(b'' if path.is_dir() else path.read_bytes())
|
289
|
|
290
|
assert not apt_root.exists()
|
291
|
|
292
|
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
|
293
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
294
|
def test_local_apt_missing(mock_cache_dir):
|
295
|
"""
|
296
|
Verify that the local_apt() function raises a proper error when 'apt-get'
|
297
|
command is missing.
|
298
|
"""
|
299
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
300
|
|
301
|
with pytest.raises(local_apt.AptError,
|
302
|
match='^couldnt_execute_apt-get_is_it_installed$'):
|
303
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
304
|
pass
|
305
|
|
306
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(update_code=1))
|
307
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
308
|
def test_local_apt_update_fail(mock_cache_dir):
|
309
|
"""
|
310
|
Verify that the local_apt() function raises a proper error when
|
311
|
'apt-get update' command returns non-0.
|
312
|
"""
|
313
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
314
|
|
315
|
error_regex = """^\
|
316
|
command_apt-get -c \\S+ update_failed
|
317
|
|
318
|
STDOUT_OUTPUT_heading
|
319
|
|
320
|
some output
|
321
|
|
322
|
STDERR_OUTPUT_heading
|
323
|
|
324
|
some error output\
|
325
|
$\
|
326
|
"""
|
327
|
|
328
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
329
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
330
|
pass
|
331
|
|
332
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
|
333
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
334
|
def test_local_apt_download(mock_cache_dir):
|
335
|
"""
|
336
|
Verify that download_apt_packages() function properly performs the download
|
337
|
of .debs and sources.
|
338
|
"""
|
339
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
340
|
destination = mock_cache_dir / 'destination'
|
341
|
destination.mkdir()
|
342
|
|
343
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
344
|
['libjs-mathjax'], destination, False)
|
345
|
|
346
|
libjs_mathjax_path = destination / 'libjs-mathjax_0%3a2.7.9+dfsg-1_all.deb'
|
347
|
fonts_mathjax_path = destination / 'fonts-mathjax_0%3a2.7.9+dfsg-1_all.deb'
|
348
|
|
349
|
source_paths = [
|
350
|
destination / 'mathjax_2.7.9+dfsg-1.debian.tar.xz',
|
351
|
destination / 'mathjax_2.7.9+dfsg-1.dsc',
|
352
|
destination / 'mathjax_2.7.9+dfsg.orig.tar.xz'
|
353
|
]
|
354
|
|
355
|
assert {*destination.iterdir()} == {libjs_mathjax_path, *source_paths}
|
356
|
|
357
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
358
|
['libjs-mathjax'], destination,
|
359
|
with_deps=True)
|
360
|
|
361
|
assert {*destination.iterdir()} == \
|
362
|
{libjs_mathjax_path, fonts_mathjax_path, *source_paths}
|
363
|
|
364
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(install_code=1))
|
365
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
366
|
def test_local_apt_install_fail(mock_cache_dir):
|
367
|
"""
|
368
|
Verify that the download_apt_packages() function raises a proper error when
|
369
|
'apt-get install' command returns non-0.
|
370
|
"""
|
371
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
372
|
destination = mock_cache_dir / 'destination'
|
373
|
destination.mkdir()
|
374
|
|
375
|
error_regex = f"""^\
|
376
|
command_apt-get -c \\S+ install --yes --just-print libjs-mathjax_failed
|
377
|
|
378
|
STDOUT_OUTPUT_heading
|
379
|
|
380
|
{re.escape(sample_install_stdout)}
|
381
|
|
382
|
STDERR_OUTPUT_heading
|
383
|
|
384
|
some error output\
|
385
|
$\
|
386
|
"""
|
387
|
|
388
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
389
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
390
|
['libjs-mathjax'], destination,
|
391
|
with_deps=True)
|
392
|
|
393
|
assert [*destination.iterdir()] == []
|
394
|
|
395
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(download_code=1))
|
396
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
397
|
def test_local_apt_download_fail(mock_cache_dir):
|
398
|
"""
|
399
|
Verify that the download_apt_packages() function raises a proper error when
|
400
|
'apt-get download' command returns non-0.
|
401
|
"""
|
402
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
403
|
destination = mock_cache_dir / 'destination'
|
404
|
destination.mkdir()
|
405
|
|
406
|
error_regex = """^\
|
407
|
command_apt-get -c \\S+ download libjs-mathjax_failed
|
408
|
|
409
|
STDOUT_OUTPUT_heading
|
410
|
|
411
|
some output
|
412
|
|
413
|
STDERR_OUTPUT_heading
|
414
|
|
415
|
some error output\
|
416
|
$\
|
417
|
"""
|
418
|
|
419
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
420
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
421
|
['libjs-mathjax'], destination, False)
|
422
|
|
423
|
assert [*destination.iterdir()] == []
|
424
|
|
425
|
@pytest.fixture
|
426
|
def mock_bad_deb_file(monkeypatch, mock_subprocess_run):
|
427
|
"""
|
428
|
Make mocked 'apt-get download' command produce an incorrectly-named file.
|
429
|
"""
|
430
|
old_run = local_apt.subprocess.run
|
431
|
|
432
|
def twice_mocked_run(command, **kwargs):
|
433
|
"""
|
434
|
Create an evil file if needed; then act just like the run() function
|
435
|
that got replaced by this one.
|
436
|
"""
|
437
|
if 'download' in command:
|
438
|
destination = Path(kwargs.get('cwd') or Path.cwd())
|
439
|
(destination / 'arbitrary-name').write_text('anything')
|
440
|
|
441
|
return old_run(command, **kwargs)
|
442
|
|
443
|
monkeypatch.setattr(local_apt.subprocess, 'run', twice_mocked_run)
|
444
|
|
445
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
|
446
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import',
|
447
|
'mock_bad_deb_file')
|
448
|
def test_local_apt_download_bad_filename(mock_cache_dir):
|
449
|
"""
|
450
|
Verify that the download_apt_packages() function raises a proper error when
|
451
|
'apt-get download' command produces an incorrectly-named file.
|
452
|
"""
|
453
|
sources_list = local_apt.SourcesList([], 'nabia')
|
454
|
destination = mock_cache_dir / 'destination'
|
455
|
destination.mkdir()
|
456
|
|
457
|
error_regex = """^\
|
458
|
apt_download_gave_bad_filename_arbitrary-name
|
459
|
|
460
|
STDOUT_OUTPUT_heading
|
461
|
|
462
|
some output
|
463
|
|
464
|
STDERR_OUTPUT_heading
|
465
|
|
466
|
some error output\
|
467
|
$\
|
468
|
"""
|
469
|
|
470
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
471
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
472
|
['libjs-mathjax'], destination, False)
|
473
|
|
474
|
assert [*destination.iterdir()] == []
|
475
|
|
476
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(source_code=1))
|
477
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
478
|
def test_local_apt_source_fail(mock_cache_dir):
|
479
|
"""
|
480
|
Verify that the download_apt_packages() function raises a proper error when
|
481
|
'apt-get source' command returns non-0.
|
482
|
"""
|
483
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
484
|
destination = mock_cache_dir / 'destination'
|
485
|
destination.mkdir()
|
486
|
|
487
|
error_regex = """^\
|
488
|
command_apt-get -c \\S* source --download-only \\S+_failed
|
489
|
|
490
|
STDOUT_OUTPUT_heading
|
491
|
|
492
|
some output
|
493
|
|
494
|
STDERR_OUTPUT_heading
|
495
|
|
496
|
some error output\
|
497
|
$\
|
498
|
"""
|
499
|
|
500
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
501
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
502
|
['libjs-mathjax'], destination, False)
|
503
|
|
504
|
assert [*destination.iterdir()] == []
|
505
|
|
506
|
def test_sources_list():
|
507
|
"""Verify that the SourcesList class works properly."""
|
508
|
list = local_apt.SourcesList([], 'nabia')
|
509
|
assert list.identity() == 'nabia'
|
510
|
|
511
|
with pytest.raises(local_apt.DistroError, match='^distro_nabiał_unknown$'):
|
512
|
local_apt.SourcesList([], 'nabiał')
|
513
|
|
514
|
list = local_apt.SourcesList(['deb sth', 'deb-src sth'], 'nabia')
|
515
|
assert list.identity() == \
|
516
|
'ef28d408b96046eae45c8ab3094ce69b2ac0c02a887e796b1d3d1a4f06fb49f1'
|
517
|
|
518
|
def run_dpkg_deb(command, returncode=0, **kwargs):
|
519
|
"""
|
520
|
Insted of running an 'dpkg-deb -x' command just create some dummy file
|
521
|
in the destination directory.
|
522
|
"""
|
523
|
expected = ['dpkg-deb', '-x', '<deb_path>', '<dst_path>']
|
524
|
|
525
|
variables = process_run_args(command, kwargs, expected)
|
526
|
deb_path = Path(variables['deb_path'])
|
527
|
dst_path = Path(variables['dst_path'])
|
528
|
|
529
|
package_name = re.match('^([^_]+)_.*', deb_path.name).group(1)
|
530
|
for path in [
|
531
|
dst_path / 'etc' / f'dummy_{package_name}_config',
|
532
|
dst_path / 'usr/share/doc' / package_name / 'copyright'
|
533
|
]:
|
534
|
path.parent.mkdir(parents=True, exist_ok=True)
|
535
|
path.write_text(f'dummy {path.name}')
|
536
|
|
537
|
return MockedCompletedProcess(command, returncode,
|
538
|
text_output=kwargs.get('text'))
|
539
|
|
540
|
def download_apt_packages(list, keys, packages, destination_dir,
|
541
|
with_deps=False):
|
542
|
"""
|
543
|
Replacement for download_apt_packages() function in local_apt.py, for
|
544
|
unit-testing the piggybacked_system() function.
|
545
|
"""
|
546
|
for path in [
|
547
|
destination_dir / 'some-bin-package_1.1-2_all.deb',
|
548
|
destination_dir / 'another-package_1.1-2_all.deb',
|
549
|
destination_dir / 'some-source-package_1.1.orig.tar.gz',
|
550
|
destination_dir / 'some-source-package_1.1-1.dsc'
|
551
|
]:
|
552
|
path.write_text(f'dummy {path.name}')
|
553
|
|
554
|
with open(destination_dir / 'test_data.json', 'w') as out:
|
555
|
json.dump({
|
556
|
'list_identity': list.identity(),
|
557
|
'keys': keys,
|
558
|
'packages': packages,
|
559
|
'with_deps': with_deps
|
560
|
}, out)
|
561
|
|
562
|
@pytest.fixture
|
563
|
def mock_download_packages(monkeypatch):
|
564
|
"""Mock the download_apt_packages() function in local_apt.py."""
|
565
|
monkeypatch.setattr(local_apt, 'download_apt_packages',
|
566
|
download_apt_packages)
|
567
|
|
568
|
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
|
569
|
@pytest.mark.parametrize('params', [
|
570
|
{
|
571
|
'with_deps': False,
|
572
|
'base_depends': True,
|
573
|
'identity': 'nabia',
|
574
|
'props': {'distribution': 'nabia', 'dependencies': False},
|
575
|
'all_keys': local_apt.default_keys,
|
576
|
'prepared_directory': False
|
577
|
},
|
578
|
{
|
579
|
'with_deps': True,
|
580
|
'base_depends': False,
|
581
|
'identity': '38db0b4fa2f6610cd1398b66a2c05d9abb1285f9a055a96eb96dee0f6b72aca8',
|
582
|
'props': {
|
583
|
'sources_list': [f'deb{suf} http://example.com/ stable main'
|
584
|
for suf in ('', '-src')],
|
585
|
'trusted_keys': ['AB' * 20],
|
586
|
'dependencies': True,
|
587
|
'depend_on_base_packages': False
|
588
|
},
|
589
|
'all_keys': [*local_apt.default_keys, 'AB' * 20],
|
590
|
'prepared_directory': True
|
591
|
}
|
592
|
])
|
593
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
594
|
def test_piggybacked_system_download(params, tmpdir):
|
595
|
"""
|
596
|
Verify that the piggybacked_system() function properly downloads and unpacks
|
597
|
APT packages.
|
598
|
"""
|
599
|
foreign_packages_dir = tmpdir if params['prepared_directory'] else None
|
600
|
|
601
|
with local_apt.piggybacked_system({
|
602
|
'system': 'apt',
|
603
|
**params['props'],
|
604
|
'packages': ['some-bin-package', 'another-package=1.1-2']
|
605
|
}, foreign_packages_dir) as piggybacked:
|
606
|
expected_depends = [{'identifier': 'apt-common-licenses'}] \
|
607
|
if params['base_depends'] else []
|
608
|
assert piggybacked.resource_must_depend == expected_depends
|
609
|
|
610
|
archive_files = dict(piggybacked.archive_files())
|
611
|
|
612
|
archive_names = [
|
613
|
'some-bin-package_1.1-2_all.deb',
|
614
|
'another-package_1.1-2_all.deb',
|
615
|
'some-source-package_1.1.orig.tar.gz',
|
616
|
'some-source-package_1.1-1.dsc',
|
617
|
'test_data.json'
|
618
|
]
|
619
|
assert {*archive_files.keys()} == \
|
620
|
{PurePosixPath('apt') / n for n in archive_names}
|
621
|
|
622
|
for path in archive_files.values():
|
623
|
if path.name == 'test_data.json':
|
624
|
assert json.loads(path.read_text()) == {
|
625
|
'list_identity': params['identity'],
|
626
|
'keys': params['all_keys'],
|
627
|
'packages': ['some-bin-package', 'another-package=1.1-2'],
|
628
|
'with_deps': params['with_deps']
|
629
|
}
|
630
|
else:
|
631
|
assert path.read_text() == f'dummy {path.name}'
|
632
|
|
633
|
if foreign_packages_dir is not None:
|
634
|
assert path.parent == foreign_packages_dir / 'apt'
|
635
|
|
636
|
license_files = {*piggybacked.package_license_files}
|
637
|
|
638
|
assert license_files == {
|
639
|
PurePosixPath('.apt-root/usr/share/doc/another-package/copyright'),
|
640
|
PurePosixPath('.apt-root/usr/share/doc/some-bin-package/copyright')
|
641
|
}
|
642
|
|
643
|
assert ['dummy copyright'] * 2 == \
|
644
|
[piggybacked.resolve_file(p).read_text() for p in license_files]
|
645
|
|
646
|
for name in ['some-bin-package', 'another-package']:
|
647
|
path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
|
648
|
assert piggybacked.resolve_file(path).read_text() == \
|
649
|
f'dummy {path.name}'
|
650
|
|
651
|
assert piggybacked.resolve_file(PurePosixPath('a/b/c')) == None
|
652
|
assert piggybacked.resolve_file(PurePosixPath('')) == None
|
653
|
|
654
|
output_text = 'loading_.apt-root/a/../../../b_outside_piggybacked_dir'
|
655
|
with pytest.raises(FileReferenceError,
|
656
|
match=f'^{re.escape(output_text)}$'):
|
657
|
piggybacked.resolve_file(PurePosixPath('.apt-root/a/../../../b'))
|
658
|
|
659
|
root = piggybacked.resolve_file(PurePosixPath('.apt-root/dummy')).parent
|
660
|
assert root.is_dir()
|
661
|
|
662
|
assert not root.exists()
|
663
|
|
664
|
if foreign_packages_dir:
|
665
|
assert [*tmpdir.iterdir()] == [tmpdir / 'apt']
|
666
|
|
667
|
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
|
668
|
@pytest.mark.usefixtures('mock_subprocess_run')
|
669
|
def test_piggybacked_system_no_download():
|
670
|
"""
|
671
|
Verify that the piggybacked_system() function is able to use pre-downloaded
|
672
|
APT packages.
|
673
|
"""
|
674
|
archive_names = {
|
675
|
f'{package}{rest}'
|
676
|
for package in ('some-lib_1:2.3', 'other-lib_4.45.2')
|
677
|
for rest in ('-1_all.deb', '.orig.tar.gz', '-1.debian.tar.xz', '-1.dsc')
|
678
|
}
|
679
|
|
680
|
with TemporaryDirectory() as td:
|
681
|
td = Path(td)
|
682
|
(td / 'apt').mkdir()
|
683
|
for name in archive_names:
|
684
|
(td / 'apt' / name).write_text(f'dummy {name}')
|
685
|
|
686
|
with local_apt.piggybacked_system({
|
687
|
'system': 'apt',
|
688
|
'distribution': 'nabia',
|
689
|
'dependencies': True,
|
690
|
'packages': ['whatever', 'whatever2']
|
691
|
}, td) as piggybacked:
|
692
|
archive_files = dict(piggybacked.archive_files())
|
693
|
|
694
|
assert {*archive_files.keys()} == \
|
695
|
{PurePosixPath('apt') / name for name in archive_names}
|
696
|
|
697
|
for path in archive_files.values():
|
698
|
assert path.read_text() == f'dummy {path.name}'
|
699
|
|
700
|
assert {*piggybacked.package_license_files} == {
|
701
|
PurePosixPath('.apt-root/usr/share/doc/some-lib/copyright'),
|
702
|
PurePosixPath('.apt-root/usr/share/doc/other-lib/copyright')
|
703
|
}
|
704
|
|
705
|
for name in ['some-lib', 'other-lib']:
|
706
|
path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
|
707
|
assert piggybacked.resolve_file(path).read_text() == \
|
708
|
f'dummy {path.name}'
|
709
|
|
710
|
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
|
711
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
712
|
def test_piggybacked_system_missing():
|
713
|
"""
|
714
|
Verify that the piggybacked_system() function raises a proper error when
|
715
|
'dpkg-deb' is missing.
|
716
|
"""
|
717
|
with pytest.raises(local_apt.AptError,
|
718
|
match='^couldnt_execute_dpkg-deb_is_it_installed$'):
|
719
|
with local_apt.piggybacked_system({
|
720
|
'system': 'apt',
|
721
|
'distribution': 'nabia',
|
722
|
'packages': ['some-package'],
|
723
|
'dependencies': False
|
724
|
}, None) as piggybacked:
|
725
|
pass
|
726
|
|
727
|
@pytest.mark.subprocess_run(local_apt, lambda c, **kw: run_dpkg_deb(c, 1, **kw))
|
728
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
729
|
def test_piggybacked_system_fail():
|
730
|
"""
|
731
|
Verify that the piggybacked_system() function raises a proper error when
|
732
|
'dpkg-deb -x' command returns non-0.
|
733
|
"""
|
734
|
error_regex = """^\
|
735
|
command_dpkg-deb -x \\S+\\.deb \\S+_failed
|
736
|
|
737
|
STDOUT_OUTPUT_heading
|
738
|
|
739
|
some output
|
740
|
|
741
|
STDERR_OUTPUT_heading
|
742
|
|
743
|
some error output\
|
744
|
$\
|
745
|
"""
|
746
|
|
747
|
with pytest.raises(local_apt.AptError, match=error_regex):
|
748
|
with local_apt.piggybacked_system({
|
749
|
'system': 'apt',
|
750
|
'distribution': 'nabia',
|
751
|
'packages': ['some-package'],
|
752
|
'dependencies': False
|
753
|
}, None) as piggybacked:
|
754
|
pass
|