Project

General

Profile

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

hydrilla-builder / tests / test_local_apt.py @ 866922f8

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
        'Dir':                    str(apt_root),
257
        'Dir::State':             f'{apt_root}/var/lib/apt',
258
        'Dir::State::status':     f'{apt_root}/var/lib/dpkg/status',
259
        'Dir::Etc::SourceList':   f'{apt_root}/etc/apt.sources.list',
260
        'Dir::Etc::SourceParts':  '',
261
        'Dir::Cache':             f'{apt_root}/var/cache/apt',
262
        'pkgCacheGen::Essential': 'none',
263
        'Dir::Etc::Trusted':      f'{apt_root}/etc/trusted.gpg',
264
    }
265

    
266
    conf_regex = re.compile(r'^(?P<key>\S+)\s"(?P<val>\S*)";$')
267
    assert dict([(m.group('key'), m.group('val'))
268
                 for l in conf_lines if l for m in [conf_regex.match(l)]]) == \
269
        expected_conf
270

    
271
    with ZipFile(mock_cache_dir / f'apt_{sources_list.identity()}.zip') as zf:
272
        # reuse the same APT, its cached zip file should exist now
273
        with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
274
            apt_root = Path(apt.apt_conf).parent.parent
275

    
276
            expected_members = {*apt_root.rglob('*')}
277
            expected_members.remove(apt_root / 'etc' / 'apt.conf')
278
            expected_members.remove(apt_root / 'etc' / 'trusted.gpg')
279

    
280
            names = zf.namelist()
281
            assert len(names) == len(expected_members)
282

    
283
            for name in names:
284
                path = apt_root / name
285
                assert path in expected_members
286
                assert zf.read(name) == \
287
                    (b'' if path.is_dir() else path.read_bytes())
288

    
289
    assert not apt_root.exists()
290

    
291
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
292
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
293
def test_local_apt_missing(mock_cache_dir):
294
    """
295
    Verify that the local_apt() function raises a proper error when 'apt-get'
296
    command is missing.
297
    """
298
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
299

    
300
    with pytest.raises(local_apt.AptError,
301
                       match='^couldnt_execute_apt-get_is_it_installed$'):
302
        with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
303
            pass
304

    
305
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(update_code=1))
306
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
307
def test_local_apt_update_fail(mock_cache_dir):
308
    """
309
    Verify that the local_apt() function raises a proper error when
310
    'apt-get update' command returns non-0.
311
    """
312
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
313

    
314
    error_regex = """^\
315
command_apt-get -c \\S+ update_failed
316

    
317
STDOUT_OUTPUT_heading
318

    
319
some output
320

    
321
STDERR_OUTPUT_heading
322

    
323
some error output\
324
$\
325
"""
326

    
327
    with pytest.raises(local_apt.AptError, match=error_regex):
328
        with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
329
            pass
330

    
331
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
332
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
333
def test_local_apt_download(mock_cache_dir):
334
    """
335
    Verify that download_apt_packages() function properly performs the download
336
    of .debs and sources.
337
    """
338
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
339
    destination = mock_cache_dir / 'destination'
340
    destination.mkdir()
341

    
342
    local_apt.download_apt_packages(sources_list, local_apt.default_keys,
343
                                    ['libjs-mathjax'], destination)
344

    
345
    libjs_mathjax_path = destination / 'libjs-mathjax_0%3a2.7.9+dfsg-1_all.deb'
346
    fonts_mathjax_path = destination / 'fonts-mathjax_0%3a2.7.9+dfsg-1_all.deb'
347

    
348
    source_paths = [
349
        destination / 'mathjax_2.7.9+dfsg-1.debian.tar.xz',
350
        destination / 'mathjax_2.7.9+dfsg-1.dsc',
351
        destination / 'mathjax_2.7.9+dfsg.orig.tar.xz'
352
    ]
353

    
354
    assert {*destination.iterdir()} == {libjs_mathjax_path, *source_paths}
355

    
356
    local_apt.download_apt_packages(sources_list, local_apt.default_keys,
357
                                    ['libjs-mathjax'], destination,
358
                                    with_deps=True)
359

    
360
    assert {*destination.iterdir()} == \
361
        {libjs_mathjax_path, fonts_mathjax_path, *source_paths}
362

    
363
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(install_code=1))
364
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
365
def test_local_apt_install_fail(mock_cache_dir):
366
    """
367
    Verify that the download_apt_packages() function raises a proper error when
368
    'apt-get install' command returns non-0.
369
    """
370
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
371
    destination = mock_cache_dir / 'destination'
372
    destination.mkdir()
373

    
374
    error_regex = f"""^\
375
command_apt-get -c \\S+ install --yes --just-print libjs-mathjax_failed
376

    
377
STDOUT_OUTPUT_heading
378

    
379
{re.escape(sample_install_stdout)}
380

    
381
STDERR_OUTPUT_heading
382

    
383
some error output\
384
$\
385
"""
386

    
387
    with pytest.raises(local_apt.AptError, match=error_regex):
388
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
389
                                        ['libjs-mathjax'], destination,
390
                                        with_deps=True)
391

    
392
    assert [*destination.iterdir()] == []
393

    
394
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(download_code=1))
395
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
396
def test_local_apt_download_fail(mock_cache_dir):
397
    """
398
    Verify that the download_apt_packages() function raises a proper error when
399
    'apt-get download' command returns non-0.
400
    """
401
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
402
    destination = mock_cache_dir / 'destination'
403
    destination.mkdir()
404

    
405
    error_regex = """^\
406
command_apt-get -c \\S+ download libjs-mathjax_failed
407

    
408
STDOUT_OUTPUT_heading
409

    
410
some output
411

    
412
STDERR_OUTPUT_heading
413

    
414
some error output\
415
$\
416
"""
417

    
418
    with pytest.raises(local_apt.AptError, match=error_regex):
419
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
420
                                        ['libjs-mathjax'], destination)
421

    
422
    assert [*destination.iterdir()] == []
423

    
424
@pytest.fixture
425
def mock_bad_deb_file(monkeypatch, mock_subprocess_run):
426
    """
427
    Make mocked 'apt-get download' command produce an incorrectly-named file.
428
    """
429
    old_run = local_apt.subprocess.run
430

    
431
    def twice_mocked_run(command, **kwargs):
432
        """
433
        Create an evil file if needed; then act just like the run() function
434
        that got replaced by this one.
435
        """
436
        if 'download' in command:
437
            destination = Path(kwargs.get('cwd') or Path.cwd())
438
            (destination / 'arbitrary-name').write_text('anything')
439

    
440
        return old_run(command, **kwargs)
441

    
442
    monkeypatch.setattr(local_apt.subprocess, 'run', twice_mocked_run)
443

    
444
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
445
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import',
446
                         'mock_bad_deb_file')
447
def test_local_apt_download_bad_filename(mock_cache_dir):
448
    """
449
    Verify that the download_apt_packages() function raises a proper error when
450
    'apt-get download' command produces an incorrectly-named file.
451
    """
452
    sources_list = local_apt.SourcesList([], 'nabia')
453
    destination = mock_cache_dir / 'destination'
454
    destination.mkdir()
455

    
456
    error_regex = """^\
457
apt_download_gave_bad_filename_arbitrary-name
458

    
459
STDOUT_OUTPUT_heading
460

    
461
some output
462

    
463
STDERR_OUTPUT_heading
464

    
465
some error output\
466
$\
467
"""
468

    
469
    with pytest.raises(local_apt.AptError, match=error_regex):
470
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
471
                                        ['libjs-mathjax'], destination)
472

    
473
    assert [*destination.iterdir()] == []
474

    
475
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(source_code=1))
476
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
477
def test_local_apt_source_fail(mock_cache_dir):
478
    """
479
    Verify that the download_apt_packages() function raises a proper error when
480
    'apt-get source' command returns non-0.
481
    """
482
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
483
    destination = mock_cache_dir / 'destination'
484
    destination.mkdir()
485

    
486
    error_regex = """^\
487
command_apt-get -c \\S* source --download-only \\S+_failed
488

    
489
STDOUT_OUTPUT_heading
490

    
491
some output
492

    
493
STDERR_OUTPUT_heading
494

    
495
some error output\
496
$\
497
"""
498

    
499
    with pytest.raises(local_apt.AptError, match=error_regex):
500
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
501
                                        ['libjs-mathjax'], destination)
502

    
503
    assert [*destination.iterdir()] == []
504

    
505
def test_sources_list():
506
    """Verify that the SourcesList class works properly."""
507
    list = local_apt.SourcesList([], 'nabia')
508
    assert list.identity() == 'nabia'
509

    
510
    with pytest.raises(local_apt.DistroError, match='^distro_nabiał_unknown$'):
511
        local_apt.SourcesList([], 'nabiał')
512

    
513
    list = local_apt.SourcesList(['deb sth', 'deb-src sth'], 'nabia')
514
    assert list.identity() == \
515
        'ef28d408b96046eae45c8ab3094ce69b2ac0c02a887e796b1d3d1a4f06fb49f1'
516

    
517
def run_dpkg_deb(command, returncode=0, **kwargs):
518
    """
519
    Insted of running an 'dpkg-deb -x' command just create some dummy file
520
    in the destination directory.
521
    """
522
    expected = ['dpkg-deb', '-x', '<deb_path>', '<dst_path>']
523

    
524
    variables = process_run_args(command, kwargs, expected)
525
    deb_path = Path(variables['deb_path'])
526
    dst_path = Path(variables['dst_path'])
527

    
528
    package_name = re.match('^([^_]+)_.*', deb_path.name).group(1)
529
    for path in [
530
            dst_path / 'etc' / f'dummy_{package_name}_config',
531
            dst_path / 'usr/share/doc' / package_name / 'copyright'
532
    ]:
533
        path.parent.mkdir(parents=True, exist_ok=True)
534
        path.write_text(f'dummy {path.name}')
535

    
536
    return MockedCompletedProcess(command, returncode,
537
                                  text_output=kwargs.get('text'))
538

    
539
def download_apt_packages(list, keys, packages, destination_dir,
540
                          with_deps=False):
541
    """
542
    Replacement for download_apt_packages() function in local_apt.py, for
543
    unit-testing the piggybacked_system() function.
544
    """
545
    for path in [
546
            destination_dir / 'some-bin-package_1.1-2_all.deb',
547
            destination_dir / 'another-package_1.1-2_all.deb',
548
            destination_dir / 'some-source-package_1.1.orig.tar.gz',
549
            destination_dir / 'some-source-package_1.1-1.dsc'
550
    ]:
551
        path.write_text(f'dummy {path.name}')
552

    
553
    with open(destination_dir / 'test_data.json', 'w') as out:
554
        json.dump({
555
            'list_identity': list.identity(),
556
            'keys': keys,
557
            'packages': packages,
558
            'with_deps': with_deps
559
        }, out)
560

    
561
@pytest.fixture
562
def mock_download_packages(monkeypatch):
563
    """Mock the download_apt_packages() function in local_apt.py."""
564
    monkeypatch.setattr(local_apt, 'download_apt_packages',
565
                        download_apt_packages)
566

    
567
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
568
@pytest.mark.parametrize('params', [
569
    {
570
        'with_deps': False,
571
        'base_depends': True,
572
        'identity': 'nabia',
573
        'props': {'distribution': 'nabia', 'dependencies': False},
574
        'all_keys': local_apt.default_keys
575
    },
576
    {
577
        'with_deps': True,
578
        'base_depends': False,
579
        'identity': '38db0b4fa2f6610cd1398b66a2c05d9abb1285f9a055a96eb96dee0f6b72aca8',
580
        'props': {
581
            'sources_list': [f'deb{suf} http://example.com/ stable main'
582
                             for suf in ('', '-src')],
583
            'trusted_keys': ['AB' * 20],
584
            'dependencies': True,
585
            'depend_on_base_packages': False
586
        },
587
        'all_keys': [*local_apt.default_keys, 'AB' * 20],
588
    }
589
])
590
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
591
def test_piggybacked_system_download(params):
592
    """
593
    Verify that the piggybacked_system() function properly downloads and unpacks
594
    APT packages.
595
    """
596
    with local_apt.piggybacked_system({
597
            'system': 'apt',
598
            **params['props'],
599
            'packages': ['some-bin-package', 'another-package=1.1-2']
600
    }, None) as piggybacked:
601
        expected_depends = [{'identifier': 'apt-common-licenses'}] \
602
            if params['base_depends'] else []
603
        assert piggybacked.package_must_depend == expected_depends
604

    
605
        archive_files = dict(piggybacked.archive_files())
606

    
607
        archive_names = [
608
            'some-bin-package_1.1-2_all.deb',
609
            'another-package_1.1-2_all.deb',
610
            'some-source-package_1.1.orig.tar.gz',
611
            'some-source-package_1.1-1.dsc',
612
            'test_data.json'
613
        ]
614
        assert {*archive_files.keys()} == \
615
            {PurePosixPath('apt') / n for n in archive_names}
616

    
617
        for path in archive_files.values():
618
            if path.name == 'test_data.json':
619
                assert json.loads(path.read_text()) == {
620
                    'list_identity': params['identity'],
621
                    'keys': params['all_keys'],
622
                    'packages': ['some-bin-package', 'another-package=1.1-2'],
623
                    'with_deps': params['with_deps']
624
                }
625
            else:
626
                assert path.read_text() == f'dummy {path.name}'
627

    
628
        license_files = {*piggybacked.package_license_files}
629

    
630
        assert license_files == {
631
            PurePosixPath('.apt-root/usr/share/doc/another-package/copyright'),
632
            PurePosixPath('.apt-root/usr/share/doc/some-bin-package/copyright')
633
        }
634

    
635
        assert ['dummy copyright'] * 2 == \
636
            [piggybacked.resolve_file(p).read_text() for p in license_files]
637

    
638
        for name in ['some-bin-package', 'another-package']:
639
            path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
640
            assert piggybacked.resolve_file(path).read_text() == \
641
                f'dummy {path.name}'
642

    
643
        assert piggybacked.resolve_file(PurePosixPath('a/b/c')) == None
644
        assert piggybacked.resolve_file(PurePosixPath('')) == None
645

    
646
        output_text = 'loading_.apt-root/a/../../../b_outside_piggybacked_dir'
647
        with pytest.raises(FileReferenceError,
648
                           match=f'^{re.escape(output_text)}$'):
649
            piggybacked.resolve_file(PurePosixPath('.apt-root/a/../../../b'))
650

    
651
        root = piggybacked.resolve_file(PurePosixPath('.apt-root/dummy')).parent
652
        assert root.is_dir()
653

    
654
    assert not root.exists()
655

    
656
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
657
@pytest.mark.usefixtures('mock_subprocess_run')
658
def test_piggybacked_system_no_download():
659
    """
660
    Verify that the piggybacked_system() function is able to use pre-downloaded
661
    APT packages.
662
    """
663
    archive_names = {
664
        f'{package}{rest}'
665
        for package in ('some-lib_1:2.3', 'other-lib_4.45.2')
666
        for rest in ('-1_all.deb', '.orig.tar.gz', '-1.debian.tar.xz', '-1.dsc')
667
    }
668

    
669
    with TemporaryDirectory() as td:
670
        td = Path(td)
671
        (td / 'apt').mkdir()
672
        for name in archive_names:
673
            (td / 'apt' / name).write_text(f'dummy {name}')
674

    
675
        with local_apt.piggybacked_system({
676
                'system': 'apt',
677
                'distribution': 'nabia',
678
                'dependencies': True,
679
                'packages': ['whatever', 'whatever2']
680
        }, td) as piggybacked:
681
            archive_files = dict(piggybacked.archive_files())
682

    
683
            assert {*archive_files.keys()} == \
684
                {PurePosixPath('apt') / name for name in archive_names}
685

    
686
            for path in archive_files.values():
687
                assert path.read_text() == f'dummy {path.name}'
688

    
689
            assert {*piggybacked.package_license_files} == {
690
                PurePosixPath('.apt-root/usr/share/doc/some-lib/copyright'),
691
                PurePosixPath('.apt-root/usr/share/doc/other-lib/copyright')
692
            }
693

    
694
            for name in ['some-lib', 'other-lib']:
695
                path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
696
                assert piggybacked.resolve_file(path).read_text() == \
697
                    f'dummy {path.name}'
698

    
699
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
700
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
701
def test_piggybacked_system_missing():
702
    """
703
    Verify that the piggybacked_system() function raises a proper error when
704
    'dpkg-deb' is missing.
705
    """
706
    with pytest.raises(local_apt.AptError,
707
                       match='^couldnt_execute_dpkg-deb_is_it_installed$'):
708
        with local_apt.piggybacked_system({
709
                'system': 'apt',
710
                'distribution': 'nabia',
711
                'packages': ['some-package'],
712
                'dependencies': False
713
        }, None) as piggybacked:
714
            pass
715

    
716
@pytest.mark.subprocess_run(local_apt, lambda c, **kw: run_dpkg_deb(c, 1, **kw))
717
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
718
def test_piggybacked_system_fail():
719
    """
720
    Verify that the piggybacked_system() function raises a proper error when
721
    'dpkg-deb -x' command returns non-0.
722
    """
723
    error_regex = """^\
724
command_dpkg-deb -x \\S+\\.deb \\S+_failed
725

    
726
STDOUT_OUTPUT_heading
727

    
728
some output
729

    
730
STDERR_OUTPUT_heading
731

    
732
some error output\
733
$\
734
"""
735

    
736
    with pytest.raises(local_apt.AptError, match=error_regex):
737
        with local_apt.piggybacked_system({
738
                'system': 'apt',
739
                'distribution': 'nabia',
740
                'packages': ['some-package'],
741
                'dependencies': False
742
        }, None) as piggybacked:
743
            pass
(5-5/5)