Project

General

Profile

Download (24.1 KB) Statistics
| Branch: | Tag: | Revision:

hydrilla-builder / tests / test_local_apt.py @ 61f0aa75

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])
(5-5/5)