Project

General

Profile

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

hydrilla-builder / tests / test_local_apt.py @ bd588eb9

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,
294
                       match='^couldnt_execute_apt-get_is_it_installed$'):
295
        with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
296
            pass
297

    
298
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(update_code=1))
299
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
300
def test_local_apt_update_fail(mock_cache_dir):
301
    """
302
    Verify that the local_apt() function raises a proper error when
303
    'apt-get update' command returns non-0.
304
    """
305
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
306

    
307
    error_regex = """^\
308
command_apt-get -c \\S+ update_failed
309

    
310
STDOUT_OUTPUT_heading
311

    
312
some output
313

    
314
STDERR_OUTPUT_heading
315

    
316
some error output\
317
$\
318
"""
319

    
320
    with pytest.raises(local_apt.AptError, match=error_regex):
321
        with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
322
            pass
323

    
324
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
325
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
326
def test_local_apt_download(mock_cache_dir):
327
    """
328
    Verify that download_apt_packages() function properly performs the download
329
    of .debs and sources.
330
    """
331
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
332
    destination = mock_cache_dir / 'destination'
333
    destination.mkdir()
334

    
335
    local_apt.download_apt_packages(sources_list, local_apt.default_keys,
336
                                    ['libjs-mathjax'], destination)
337

    
338
    libjs_mathjax_path = destination / 'libjs-mathjax_2.7.9+dfsg-1_all.deb'
339
    fonts_mathjax_path = destination / 'fonts-mathjax_2.7.9+dfsg-1_all.deb'
340

    
341
    source_paths = [
342
        destination / 'mathjax_2.7.9+dfsg-1.debian.tar.xz',
343
        destination / 'mathjax_2.7.9+dfsg-1.dsc',
344
        destination / 'mathjax_2.7.9+dfsg.orig.tar.xz'
345
    ]
346

    
347
    assert {*destination.iterdir()} == {libjs_mathjax_path, *source_paths}
348

    
349
    local_apt.download_apt_packages(sources_list, local_apt.default_keys,
350
                                    ['libjs-mathjax'], destination,
351
                                    with_deps=True)
352

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

    
356
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(install_code=1))
357
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
358
def test_local_apt_install_fail(mock_cache_dir):
359
    """
360
    Verify that the download_apt_packages() function raises a proper error when
361
    'apt-get install' command returns non-0.
362
    """
363
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
364
    destination = mock_cache_dir / 'destination'
365
    destination.mkdir()
366

    
367
    error_regex = f"""^\
368
command_apt-get -c \\S+ install --yes --just-print libjs-mathjax_failed
369

    
370
STDOUT_OUTPUT_heading
371

    
372
{re.escape(sample_install_stdout)}
373

    
374
STDERR_OUTPUT_heading
375

    
376
some error output\
377
$\
378
"""
379

    
380
    with pytest.raises(local_apt.AptError, match=error_regex):
381
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
382
                                        ['libjs-mathjax'], destination,
383
                                        with_deps=True)
384

    
385
    assert [*destination.iterdir()] == []
386

    
387
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(download_code=1))
388
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
389
def test_local_apt_download_fail(mock_cache_dir):
390
    """
391
    Verify that the download_apt_packages() function raises a proper error when
392
    'apt-get download' command returns non-0.
393
    """
394
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
395
    destination = mock_cache_dir / 'destination'
396
    destination.mkdir()
397

    
398
    error_regex = """^\
399
command_apt-get -c \\S+ download libjs-mathjax_failed
400

    
401
STDOUT_OUTPUT_heading
402

    
403
some output
404

    
405
STDERR_OUTPUT_heading
406

    
407
some error output\
408
$\
409
"""
410

    
411
    with pytest.raises(local_apt.AptError, match=error_regex):
412
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
413
                                        ['libjs-mathjax'], destination)
414

    
415
    assert [*destination.iterdir()] == []
416

    
417
@pytest.fixture
418
def mock_bad_deb_file(monkeypatch, mock_subprocess_run):
419
    """
420
    Make mocked 'apt-get download' command produce an incorrectly-named file.
421
    """
422
    old_run = local_apt.subprocess.run
423

    
424
    def twice_mocked_run(command, **kwargs):
425
        """
426
        Create an evil file if needed; then act just like the run() function
427
        that got replaced by this one.
428
        """
429
        if 'download' in command:
430
            destination = Path(kwargs.get('cwd') or Path.cwd())
431
            (destination / 'arbitrary-name').write_text('anything')
432

    
433
        return old_run(command, **kwargs)
434

    
435
    monkeypatch.setattr(local_apt.subprocess, 'run', twice_mocked_run)
436

    
437
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
438
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import',
439
                         'mock_bad_deb_file')
440
def test_local_apt_download_bad_filename(mock_cache_dir):
441
    """
442
    Verify that the download_apt_packages() function raises a proper error when
443
    'apt-get download' command produces an incorrectly-named file.
444
    """
445
    sources_list = local_apt.SourcesList([], 'nabia')
446
    destination = mock_cache_dir / 'destination'
447
    destination.mkdir()
448

    
449
    error_regex = """^\
450
apt_download_gave_bad_filename_arbitrary-name
451

    
452
STDOUT_OUTPUT_heading
453

    
454
some output
455

    
456
STDERR_OUTPUT_heading
457

    
458
some error output\
459
$\
460
"""
461

    
462
    with pytest.raises(local_apt.AptError, match=error_regex):
463
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
464
                                        ['libjs-mathjax'], destination)
465

    
466
    assert [*destination.iterdir()] == []
467

    
468
@pytest.mark.subprocess_run(local_apt, make_run_apt_get(source_code=1))
469
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
470
def test_local_apt_source_fail(mock_cache_dir):
471
    """
472
    Verify that the download_apt_packages() function raises a proper error when
473
    'apt-get source' command returns non-0.
474
    """
475
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
476
    destination = mock_cache_dir / 'destination'
477
    destination.mkdir()
478

    
479
    error_regex = """^\
480
command_apt-get -c \\S* source --download-only \\S+_failed
481

    
482
STDOUT_OUTPUT_heading
483

    
484
some output
485

    
486
STDERR_OUTPUT_heading
487

    
488
some error output\
489
$\
490
"""
491

    
492
    with pytest.raises(local_apt.AptError, match=error_regex):
493
        local_apt.download_apt_packages(sources_list, local_apt.default_keys,
494
                                        ['libjs-mathjax'], destination)
495

    
496
    assert [*destination.iterdir()] == []
497

    
498
def test_sources_list():
499
    """Verify that the SourcesList class works properly."""
500
    list = local_apt.SourcesList([], 'nabia')
501
    assert list.identity() == 'nabia'
502

    
503
    with pytest.raises(local_apt.DistroError, match='^distro_nabiał_unknown$'):
504
        local_apt.SourcesList([], 'nabiał')
505

    
506
    list = local_apt.SourcesList(['deb sth', 'deb-src sth'], 'nabia')
507
    assert list.identity() == \
508
        'ef28d408b96046eae45c8ab3094ce69b2ac0c02a887e796b1d3d1a4f06fb49f1'
509

    
510
def run_dpkg_deb(command, returncode=0, **kwargs):
511
    """
512
    Insted of running an 'dpkg-deb -x' command just create some dummy file
513
    in the destination directory.
514
    """
515
    expected = ['dpkg-deb', '-x', '<deb_path>', '<dst_path>']
516

    
517
    variables = process_run_args(command, kwargs, expected)
518
    deb_path = Path(variables['deb_path'])
519
    dst_path = Path(variables['dst_path'])
520

    
521
    package_name = re.match('^([^_]+)_.*', deb_path.name).group(1)
522
    for path in [
523
            dst_path / 'etc' / f'dummy_{package_name}_config',
524
            dst_path / 'usr/share/doc' / package_name / 'copyright'
525
    ]:
526
        path.parent.mkdir(parents=True, exist_ok=True)
527
        path.write_text(f'dummy {path.name}')
528

    
529
    return MockedCompletedProcess(command, returncode,
530
                                  text_output=kwargs.get('text'))
531

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

    
546
    with open(destination_dir / 'test_data.json', 'w') as out:
547
        json.dump({
548
            'list_identity': list.identity(),
549
            'keys': keys,
550
            'packages': packages,
551
            'with_deps': with_deps
552
        }, out)
553

    
554
@pytest.fixture
555
def mock_download_packages(monkeypatch):
556
    """Mock the download_apt_packages() function in local_apt.py."""
557
    monkeypatch.setattr(local_apt, 'download_apt_packages',
558
                        download_apt_packages)
559

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

    
598
        archive_files = dict(piggybacked.archive_files())
599

    
600
        archive_names = [
601
            'some-bin-package_1.1-2_all.deb',
602
            'another-package_1.1-2_all.deb',
603
            'some-source-package_1.1.orig.tar.gz',
604
            'some-source-package_1.1-1.dsc',
605
            'test_data.json'
606
        ]
607
        assert {*archive_files.keys()} == \
608
            {PurePosixPath('apt') / n for n in archive_names}
609

    
610
        for path in archive_files.values():
611
            if path.name == 'test_data.json':
612
                assert json.loads(path.read_text()) == {
613
                    'list_identity': params['identity'],
614
                    'keys': params['all_keys'],
615
                    'packages': ['some-bin-package', 'another-package=1.1-2'],
616
                    'with_deps': params['with_deps']
617
                }
618
            else:
619
                assert path.read_text() == f'dummy {path.name}'
620

    
621
        license_files = {*piggybacked.package_license_files}
622

    
623
        assert license_files == {
624
            PurePosixPath('.apt-root/usr/share/doc/another-package/copyright'),
625
            PurePosixPath('.apt-root/usr/share/doc/some-bin-package/copyright')
626
        }
627

    
628
        assert ['dummy copyright'] * 2 == \
629
            [piggybacked.resolve_file(p).read_text() for p in license_files]
630

    
631
        for name in ['some-bin-package', 'another-package']:
632
            path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
633
            assert piggybacked.resolve_file(path).read_text() == \
634
                f'dummy {path.name}'
635

    
636
        assert piggybacked.resolve_file(PurePosixPath('a/b/c')) == None
637
        assert piggybacked.resolve_file(PurePosixPath('')) == None
638

    
639
        output_text = 'loading_.apt-root/a/../../../b_outside_piggybacked_dir'
640
        with pytest.raises(FileReferenceError,
641
                           match=f'^{re.escape(output_text)}$'):
642
            piggybacked.resolve_file(PurePosixPath('.apt-root/a/../../../b'))
643

    
644
        root = piggybacked.resolve_file(PurePosixPath('.apt-root/dummy')).parent
645
        assert root.is_dir()
646

    
647
    assert not root.exists()
648

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

    
662
    with TemporaryDirectory() as td:
663
        td = Path(td)
664
        (td / 'apt').mkdir()
665
        for name in archive_names:
666
            (td / 'apt' / name).write_text(f'dummy {name}')
667

    
668
        with local_apt.piggybacked_system({
669
                'system': 'apt',
670
                'distribution': 'nabia',
671
                'dependencies': True,
672
                'packages': ['whatever', 'whatever2']
673
        }, td) as piggybacked:
674
            archive_files = dict(piggybacked.archive_files())
675

    
676
            assert {*archive_files.keys()} == \
677
                {PurePosixPath('apt') / name for name in archive_names}
678

    
679
            for path in archive_files.values():
680
                assert path.read_text() == f'dummy {path.name}'
681

    
682
            assert {*piggybacked.package_license_files} == {
683
                PurePosixPath('.apt-root/usr/share/doc/some-lib/copyright'),
684
                PurePosixPath('.apt-root/usr/share/doc/other-lib/copyright')
685
            }
686

    
687
            for name in ['some-lib', 'other-lib']:
688
                path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
689
                assert piggybacked.resolve_file(path).read_text() == \
690
                    f'dummy {path.name}'
691

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

    
709
@pytest.mark.subprocess_run(local_apt, lambda c, **kw: run_dpkg_deb(c, 1, **kw))
710
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
711
def test_piggybacked_system_fail():
712
    """
713
    Verify that the piggybacked_system() function raises a proper error when
714
    'dpkg-deb -x' command returns non-0.
715
    """
716
    error_regex = """^\
717
command_dpkg-deb -x \\S+\\.deb \\S+_failed
718

    
719
STDOUT_OUTPUT_heading
720

    
721
some output
722

    
723
STDERR_OUTPUT_heading
724

    
725
some error output\
726
$\
727
"""
728

    
729
    with pytest.raises(local_apt.AptError, match=error_regex):
730
        with local_apt.piggybacked_system({
731
                'system': 'apt',
732
                'distribution': 'nabia',
733
                'packages': ['some-package'],
734
                'dependencies': False
735
        }, None) as piggybacked:
736
            pass
(5-5/5)