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 (2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
132
|
Inst libjs-mathjax (2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
133
|
Conf fonts-mathjax (2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
|
134
|
Conf libjs-mathjax (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', 'libjs-mathjax']
|
157
|
if 'fonts-mathjax' in command:
|
158
|
expected.insert(-1, 'fonts-mathjax')
|
159
|
|
160
|
conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
|
161
|
|
162
|
destination = Path(kwargs.get('cwd') or Path.cwd())
|
163
|
|
164
|
for word in expected:
|
165
|
if word.endswith('-mathjax'):
|
166
|
deb_path = destination / f'{word}_2.7.9+dfsg-1_all.deb'
|
167
|
deb_path.write_text(f'dummy {deb_path.name}')
|
168
|
|
169
|
return MockedCompletedProcess(command, returncode,
|
170
|
text_output=kwargs.get('text'))
|
171
|
|
172
|
def run_apt_get_source(command, returncode=0, **kwargs):
|
173
|
"""
|
174
|
Instead of running an 'apt-get source' command just write some dummy
|
175
|
"tarballs" to the appropriate directory.
|
176
|
"""
|
177
|
expected = ['apt-get', '-c', '<conf_path>', 'source',
|
178
|
'--download-only', 'libjs-mathjax=2.7.9+dfsg-1']
|
179
|
if 'fonts-mathjax=2.7.9+dfsg-1' in command:
|
180
|
if command[-1] == 'fonts-mathjax=2.7.9+dfsg-1':
|
181
|
expected.append('fonts-mathjax=2.7.9+dfsg-1')
|
182
|
else:
|
183
|
expected.insert(-1, 'fonts-mathjax=2.7.9+dfsg-1')
|
184
|
|
185
|
destination = Path(kwargs.get('cwd') or Path.cwd())
|
186
|
for filename in [
|
187
|
'mathjax_2.7.9+dfsg-1.debian.tar.xz',
|
188
|
'mathjax_2.7.9+dfsg-1.dsc',
|
189
|
'mathjax_2.7.9+dfsg.orig.tar.xz'
|
190
|
]:
|
191
|
(destination / filename).write_text(f'dummy {filename}')
|
192
|
|
193
|
return MockedCompletedProcess(command, returncode,
|
194
|
text_output=kwargs.get('text'))
|
195
|
|
196
|
def make_run_apt_get(**returncodes):
|
197
|
"""
|
198
|
Produce a function that chooses and runs the appropriate one of
|
199
|
subprocess_run_apt_get_*() mock functions.
|
200
|
"""
|
201
|
def mock_run(command, **kwargs):
|
202
|
"""
|
203
|
Chooses and runs the appropriate one of subprocess_run_apt_get_*() mock
|
204
|
functions.
|
205
|
"""
|
206
|
for subcommand, run in [
|
207
|
('update', run_apt_get_update),
|
208
|
('install', run_apt_get_install),
|
209
|
('download', run_apt_get_download),
|
210
|
('source', run_apt_get_source)
|
211
|
]:
|
212
|
if subcommand in command:
|
213
|
returncode = returncodes.get(f'{subcommand}_code', 0)
|
214
|
return run(command, returncode, **kwargs)
|
215
|
|
216
|
raise Exception('Unknown command: {}'.format(' '.join(command)))
|
217
|
|
218
|
return mock_run
|
219
|
|
220
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
|
221
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
222
|
def test_local_apt_contextmanager(mock_cache_dir):
|
223
|
"""
|
224
|
Verify that the local_apt() function creates a proper apt environment and
|
225
|
that it also properly restores it from cache.
|
226
|
"""
|
227
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
228
|
|
229
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
230
|
apt_root = Path(apt.apt_conf).parent.parent
|
231
|
|
232
|
assert (apt_root / 'etc' / 'trusted.gpg').read_bytes() == \
|
233
|
b'<dummy keys export>'
|
234
|
|
235
|
assert (apt_root / 'etc' / 'update_called').exists()
|
236
|
|
237
|
assert (apt_root / 'etc' / 'apt.sources.list').read_text() == \
|
238
|
'deb-src sth\ndeb sth'
|
239
|
|
240
|
conf_lines = (apt_root / 'etc' / 'apt.conf').read_text().split('\n')
|
241
|
|
242
|
# check mocked keyring
|
243
|
assert {*local_apt.default_keys} == \
|
244
|
{*(mock_cache_dir / 'master_keyring.gpg').read_text().split('\n')}
|
245
|
|
246
|
assert not apt_root.exists()
|
247
|
|
248
|
expected_conf = {
|
249
|
'Dir': str(apt_root),
|
250
|
'Dir::State': f'{apt_root}/var/lib/apt',
|
251
|
'Dir::State::status': f'{apt_root}/var/lib/dpkg/status',
|
252
|
'Dir::Etc::SourceList': f'{apt_root}/etc/apt.sources.list',
|
253
|
'Dir::Etc::SourceParts': '',
|
254
|
'Dir::Cache': f'{apt_root}/var/cache/apt',
|
255
|
'pkgCacheGen::Essential': 'none',
|
256
|
'Dir::Etc::Trusted': f'{apt_root}/etc/trusted.gpg',
|
257
|
}
|
258
|
|
259
|
conf_regex = re.compile(r'^(?P<key>\S+)\s"(?P<val>\S*)";$')
|
260
|
assert dict([(m.group('key'), m.group('val'))
|
261
|
for l in conf_lines if l for m in [conf_regex.match(l)]]) == \
|
262
|
expected_conf
|
263
|
|
264
|
with ZipFile(mock_cache_dir / f'apt_{sources_list.identity()}.zip') as zf:
|
265
|
# reuse the same APT, its cached zip file should exist now
|
266
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
267
|
apt_root = Path(apt.apt_conf).parent.parent
|
268
|
|
269
|
expected_members = {*apt_root.rglob('*')}
|
270
|
expected_members.remove(apt_root / 'etc' / 'apt.conf')
|
271
|
expected_members.remove(apt_root / 'etc' / 'trusted.gpg')
|
272
|
|
273
|
names = zf.namelist()
|
274
|
assert len(names) == len(expected_members)
|
275
|
|
276
|
for name in names:
|
277
|
path = apt_root / name
|
278
|
assert path in expected_members
|
279
|
assert zf.read(name) == \
|
280
|
(b'' if path.is_dir() else path.read_bytes())
|
281
|
|
282
|
assert not apt_root.exists()
|
283
|
|
284
|
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
|
285
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
286
|
def test_local_apt_missing(mock_cache_dir):
|
287
|
"""
|
288
|
Verify that the local_apt() function raises a proper error when 'apt-get'
|
289
|
command is missing.
|
290
|
"""
|
291
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
292
|
|
293
|
with pytest.raises(local_apt.AptError) as excinfo:
|
294
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
295
|
pass
|
296
|
|
297
|
assert len(excinfo.value.args) == 1
|
298
|
assert isinstance(excinfo.value.args[0], str)
|
299
|
assert '\n' not in excinfo.value.args[0]
|
300
|
|
301
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(update_code=1))
|
302
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
303
|
def test_local_apt_update_fail(mock_cache_dir):
|
304
|
"""
|
305
|
Verify that the local_apt() function raises a proper error when
|
306
|
'apt-get update' command returns non-0.
|
307
|
"""
|
308
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
309
|
|
310
|
with pytest.raises(local_apt.AptError) as excinfo:
|
311
|
with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
|
312
|
pass
|
313
|
|
314
|
assert len(excinfo.value.args) == 1
|
315
|
|
316
|
assert re.match(r'.*\n\n.*\n\nsome output\n\n.*\n\nsome error output',
|
317
|
excinfo.value.args[0])
|
318
|
|
319
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
|
320
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
321
|
def test_local_apt_download(mock_cache_dir):
|
322
|
"""
|
323
|
Verify that download_apt_packages() function properly performs the download
|
324
|
of .debs and sources.
|
325
|
"""
|
326
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
327
|
destination = mock_cache_dir / 'destination'
|
328
|
destination.mkdir()
|
329
|
|
330
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
331
|
['libjs-mathjax'], destination)
|
332
|
|
333
|
libjs_mathjax_path = destination / 'libjs-mathjax_2.7.9+dfsg-1_all.deb'
|
334
|
fonts_mathjax_path = destination / 'fonts-mathjax_2.7.9+dfsg-1_all.deb'
|
335
|
|
336
|
source_paths = [
|
337
|
destination / 'mathjax_2.7.9+dfsg-1.debian.tar.xz',
|
338
|
destination / 'mathjax_2.7.9+dfsg-1.dsc',
|
339
|
destination / 'mathjax_2.7.9+dfsg.orig.tar.xz'
|
340
|
]
|
341
|
|
342
|
assert {*destination.iterdir()} == {libjs_mathjax_path, *source_paths}
|
343
|
|
344
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
345
|
['libjs-mathjax'], destination,
|
346
|
with_deps=True)
|
347
|
|
348
|
assert {*destination.iterdir()} == \
|
349
|
{libjs_mathjax_path, fonts_mathjax_path, *source_paths}
|
350
|
|
351
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(install_code=1))
|
352
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
353
|
def test_local_apt_install_fail(mock_cache_dir):
|
354
|
"""
|
355
|
Verify that the download_apt_packages() function raises a proper error when
|
356
|
'apt-get install' command returns non-0.
|
357
|
"""
|
358
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
359
|
destination = mock_cache_dir / 'destination'
|
360
|
destination.mkdir()
|
361
|
|
362
|
with pytest.raises(local_apt.AptError) as excinfo:
|
363
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
364
|
['libjs-mathjax'], destination,
|
365
|
with_deps=True)
|
366
|
|
367
|
assert len(excinfo.value.args) == 1
|
368
|
|
369
|
assert re.match(r'^.*\n\n.*\n\n', excinfo.value.args[0])
|
370
|
assert re.search(r'\n\nsome error output$', excinfo.value.args[0])
|
371
|
assert sample_install_stdout in excinfo.value.args[0]
|
372
|
|
373
|
assert [*destination.iterdir()] == []
|
374
|
|
375
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(download_code=1))
|
376
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
377
|
def test_local_apt_download_fail(mock_cache_dir):
|
378
|
"""
|
379
|
Verify that the download_apt_packages() function raises a proper error when
|
380
|
'apt-get download' command returns non-0.
|
381
|
"""
|
382
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
383
|
destination = mock_cache_dir / 'destination'
|
384
|
destination.mkdir()
|
385
|
|
386
|
with pytest.raises(local_apt.AptError) as excinfo:
|
387
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
388
|
['libjs-mathjax'], destination)
|
389
|
|
390
|
assert len(excinfo.value.args) == 1
|
391
|
|
392
|
assert re.match(r'.*\n\n.*\n\nsome output\n\n.*\n\nsome error output',
|
393
|
excinfo.value.args[0])
|
394
|
|
395
|
assert [*destination.iterdir()] == []
|
396
|
|
397
|
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(source_code=1))
|
398
|
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
|
399
|
def test_local_apt_source_fail(mock_cache_dir):
|
400
|
"""
|
401
|
Verify that the download_apt_packages() function raises a proper error when
|
402
|
'apt-get source' command returns non-0.
|
403
|
"""
|
404
|
sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
|
405
|
destination = mock_cache_dir / 'destination'
|
406
|
destination.mkdir()
|
407
|
|
408
|
with pytest.raises(local_apt.AptError) as excinfo:
|
409
|
local_apt.download_apt_packages(sources_list, local_apt.default_keys,
|
410
|
['libjs-mathjax'], destination)
|
411
|
|
412
|
assert len(excinfo.value.args) == 1
|
413
|
|
414
|
assert re.match(r'.*\n\n.*\n\nsome output\n\n.*\n\nsome error output',
|
415
|
excinfo.value.args[0])
|
416
|
|
417
|
assert [*destination.iterdir()] == []
|
418
|
|
419
|
def test_sources_list():
|
420
|
"""Verify that the SourcesList class works properly."""
|
421
|
list = local_apt.SourcesList([], 'nabia')
|
422
|
assert list.identity() == 'nabia'
|
423
|
|
424
|
with pytest.raises(local_apt.DistroError):
|
425
|
local_apt.SourcesList([], 'nabiał')
|
426
|
|
427
|
list = local_apt.SourcesList(['deb sth', 'deb-src sth'], 'nabia')
|
428
|
assert list.identity() == \
|
429
|
'ef28d408b96046eae45c8ab3094ce69b2ac0c02a887e796b1d3d1a4f06fb49f1'
|
430
|
|
431
|
def run_dpkg_deb(command, returncode=0, **kwargs):
|
432
|
"""
|
433
|
Insted of running an 'dpkg-deb -x' command just create some dummy file
|
434
|
in the destination directory.
|
435
|
"""
|
436
|
expected = ['dpkg-deb', '-x', '<deb_path>', '<dst_path>']
|
437
|
|
438
|
variables = process_run_args(command, kwargs, expected)
|
439
|
deb_path = Path(variables['deb_path'])
|
440
|
dst_path = Path(variables['dst_path'])
|
441
|
|
442
|
package_name = re.match('^([^_]+)_.*', deb_path.name).group(1)
|
443
|
for path in [
|
444
|
dst_path / 'etc' / f'dummy_{package_name}_config',
|
445
|
dst_path / 'usr/share/doc' / package_name / 'copyright'
|
446
|
]:
|
447
|
path.parent.mkdir(parents=True, exist_ok=True)
|
448
|
path.write_text(f'dummy {path.name}')
|
449
|
|
450
|
return MockedCompletedProcess(command, returncode,
|
451
|
text_output=kwargs.get('text'))
|
452
|
|
453
|
def download_apt_packages(list, keys, packages, destination_dir,
|
454
|
with_deps=False):
|
455
|
"""
|
456
|
Replacement for download_apt_packages() function in local_apt.py, for
|
457
|
unit-testing the piggybacked_system() function.
|
458
|
"""
|
459
|
for path in [
|
460
|
destination_dir / 'some-bin-package_1.1-2_all.deb',
|
461
|
destination_dir / 'another-package_1.1-2_all.deb',
|
462
|
destination_dir / 'some-source-package_1.1.orig.tar.gz',
|
463
|
destination_dir / 'some-source-package_1.1-1.dsc'
|
464
|
]:
|
465
|
path.write_text(f'dummy {path.name}')
|
466
|
|
467
|
with open(destination_dir / 'test_data.json', 'w') as out:
|
468
|
json.dump({
|
469
|
'list_identity': list.identity(),
|
470
|
'keys': keys,
|
471
|
'packages': packages,
|
472
|
'with_deps': with_deps
|
473
|
}, out)
|
474
|
|
475
|
@pytest.fixture
|
476
|
def mock_download_packages(monkeypatch):
|
477
|
"""Mock the download_apt_packages() function in local_apt.py."""
|
478
|
monkeypatch.setattr(local_apt, 'download_apt_packages',
|
479
|
download_apt_packages)
|
480
|
|
481
|
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
|
482
|
@pytest.mark.parametrize('params', [
|
483
|
{
|
484
|
'with_deps': False,
|
485
|
'base_depends': True,
|
486
|
'identity': 'nabia',
|
487
|
'props': {'distribution': 'nabia', 'dependencies': False},
|
488
|
'all_keys': local_apt.default_keys
|
489
|
},
|
490
|
{
|
491
|
'with_deps': True,
|
492
|
'base_depends': False,
|
493
|
'identity': '38db0b4fa2f6610cd1398b66a2c05d9abb1285f9a055a96eb96dee0f6b72aca8',
|
494
|
'props': {
|
495
|
'sources_list': [f'deb{suf} http://example.com/ stable main'
|
496
|
for suf in ('', '-src')],
|
497
|
'trusted_keys': ['AB' * 20],
|
498
|
'dependencies': True,
|
499
|
'depend_on_base_packages': False
|
500
|
},
|
501
|
'all_keys': [*local_apt.default_keys, 'AB' * 20],
|
502
|
}
|
503
|
])
|
504
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
505
|
def test_piggybacked_system_download(params):
|
506
|
"""
|
507
|
Verify that the piggybacked_system() function properly downloads and unpacks
|
508
|
APT packages.
|
509
|
"""
|
510
|
with local_apt.piggybacked_system({
|
511
|
'system': 'apt',
|
512
|
**params['props'],
|
513
|
'packages': ['some-bin-package', 'another-package=1.1-2']
|
514
|
}, None) as piggybacked:
|
515
|
expected_depends = [{'identifier': 'apt-common-licenses'}] \
|
516
|
if params['base_depends'] else []
|
517
|
assert piggybacked.package_must_depend == expected_depends
|
518
|
|
519
|
archive_files = dict(piggybacked.archive_files())
|
520
|
|
521
|
archive_names = [
|
522
|
'some-bin-package_1.1-2_all.deb',
|
523
|
'another-package_1.1-2_all.deb',
|
524
|
'some-source-package_1.1.orig.tar.gz',
|
525
|
'some-source-package_1.1-1.dsc',
|
526
|
'test_data.json'
|
527
|
]
|
528
|
assert {*archive_files.keys()} == \
|
529
|
{PurePosixPath('apt') / n for n in archive_names}
|
530
|
|
531
|
for path in archive_files.values():
|
532
|
if path.name == 'test_data.json':
|
533
|
assert json.loads(path.read_text()) == {
|
534
|
'list_identity': params['identity'],
|
535
|
'keys': params['all_keys'],
|
536
|
'packages': ['some-bin-package', 'another-package=1.1-2'],
|
537
|
'with_deps': params['with_deps']
|
538
|
}
|
539
|
else:
|
540
|
assert path.read_text() == f'dummy {path.name}'
|
541
|
|
542
|
license_files = {*piggybacked.package_license_files}
|
543
|
|
544
|
assert license_files == {
|
545
|
PurePosixPath('.apt-root/usr/share/doc/another-package/copyright'),
|
546
|
PurePosixPath('.apt-root/usr/share/doc/some-bin-package/copyright')
|
547
|
}
|
548
|
|
549
|
assert ['dummy copyright'] * 2 == \
|
550
|
[piggybacked.resolve_file(p).read_text() for p in license_files]
|
551
|
|
552
|
for name in ['some-bin-package', 'another-package']:
|
553
|
path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
|
554
|
assert piggybacked.resolve_file(path).read_text() == \
|
555
|
f'dummy {path.name}'
|
556
|
|
557
|
assert piggybacked.resolve_file(PurePosixPath('a/b/c')) == None
|
558
|
assert piggybacked.resolve_file(PurePosixPath('')) == None
|
559
|
|
560
|
with pytest.raises(FileReferenceError):
|
561
|
piggybacked.resolve_file(PurePosixPath('.apt-root/a/../../../b'))
|
562
|
|
563
|
root = piggybacked.resolve_file(PurePosixPath('.apt-root/dummy')).parent
|
564
|
assert root.is_dir()
|
565
|
|
566
|
assert not root.exists()
|
567
|
|
568
|
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
|
569
|
@pytest.mark.usefixtures('mock_subprocess_run')
|
570
|
def test_piggybacked_system_no_download():
|
571
|
"""
|
572
|
Verify that the piggybacked_system() function is able to use pre-downloaded
|
573
|
APT packages.
|
574
|
"""
|
575
|
archive_names = {
|
576
|
f'{package}{rest}'
|
577
|
for package in ('some-lib_1:2.3', 'other-lib_4.45.2')
|
578
|
for rest in ('-1_all.deb', '.orig.tar.gz', '-1.debian.tar.xz', '-1.dsc')
|
579
|
}
|
580
|
|
581
|
with TemporaryDirectory() as td:
|
582
|
td = Path(td)
|
583
|
(td / 'apt').mkdir()
|
584
|
for name in archive_names:
|
585
|
(td / 'apt' / name).write_text(f'dummy {name}')
|
586
|
|
587
|
with local_apt.piggybacked_system({
|
588
|
'system': 'apt',
|
589
|
'distribution': 'nabia',
|
590
|
'dependencies': True,
|
591
|
'packages': ['whatever', 'whatever2']
|
592
|
}, td) as piggybacked:
|
593
|
archive_files = dict(piggybacked.archive_files())
|
594
|
|
595
|
assert {*archive_files.keys()} == \
|
596
|
{PurePosixPath('apt') / name for name in archive_names}
|
597
|
|
598
|
for path in archive_files.values():
|
599
|
assert path.read_text() == f'dummy {path.name}'
|
600
|
|
601
|
assert {*piggybacked.package_license_files} == {
|
602
|
PurePosixPath('.apt-root/usr/share/doc/some-lib/copyright'),
|
603
|
PurePosixPath('.apt-root/usr/share/doc/other-lib/copyright')
|
604
|
}
|
605
|
|
606
|
for name in ['some-lib', 'other-lib']:
|
607
|
path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
|
608
|
assert piggybacked.resolve_file(path).read_text() == \
|
609
|
f'dummy {path.name}'
|
610
|
|
611
|
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
|
612
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
613
|
def test_piggybacked_system_missing():
|
614
|
"""
|
615
|
Verify that the piggybacked_system() function raises a proper error when
|
616
|
'dpkg-deb' is missing.
|
617
|
"""
|
618
|
with pytest.raises(local_apt.AptError) as excinfo:
|
619
|
with local_apt.piggybacked_system({
|
620
|
'system': 'apt',
|
621
|
'distribution': 'nabia',
|
622
|
'packages': ['some-package'],
|
623
|
'dependencies': False
|
624
|
}, None) as piggybacked:
|
625
|
pass
|
626
|
|
627
|
assert len(excinfo.value.args) == 1
|
628
|
|
629
|
assert '\n' not in excinfo.value.args[0]
|
630
|
|
631
|
|
632
|
@pytest.mark.subprocess_run(local_apt, lambda c, **kw: run_dpkg_deb(c, 1, **kw))
|
633
|
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
|
634
|
def test_piggybacked_system_fail():
|
635
|
"""
|
636
|
Verify that the piggybacked_system() function raises a proper error when
|
637
|
'dpkg-deb -x' command returns non-0.
|
638
|
"""
|
639
|
with pytest.raises(local_apt.AptError) as excinfo:
|
640
|
with local_apt.piggybacked_system({
|
641
|
'system': 'apt',
|
642
|
'distribution': 'nabia',
|
643
|
'packages': ['some-package'],
|
644
|
'dependencies': False
|
645
|
}, None) as piggybacked:
|
646
|
pass
|
647
|
|
648
|
assert len(excinfo.value.args) == 1
|
649
|
|
650
|
assert re.match(r'.*\n\n.*\n\nsome output\n\n.*\n\nsome error output',
|
651
|
excinfo.value.args[0])
|