Project

General

Profile

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

hydrilla-builder / tests / test_local_apt.py @ 117bf196

1
# SPDX-License-Identifier: CC0-1.0
2

    
3
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
4
#
5
# Available under the terms of Creative Commons Zero v1.0 Universal.
6

    
7
import pytest
8
import tempfile
9
import re
10
import json
11
from pathlib import Path, PurePosixPath
12
from zipfile import ZipFile
13
from tempfile import TemporaryDirectory
14

    
15
from hydrilla.builder import local_apt
16
from hydrilla.builder.common_errors import *
17

    
18
here = Path(__file__).resolve().parent
19

    
20
from .helpers import *
21

    
22
@pytest.fixture
23
def mock_cache_dir(monkeypatch):
24
    """Make local_apt.py cache files to a temporary directory."""
25
    with tempfile.TemporaryDirectory() as td:
26
        td_path = Path(td)
27
        monkeypatch.setattr(local_apt, 'default_apt_cache_dir', td_path)
28
        yield td_path
29

    
30
@pytest.fixture
31
def mock_gnupg_import(monkeypatch, mock_cache_dir):
32
    """Mock gnupg library when imported dynamically."""
33

    
34
    gnupg_mock_dir = mock_cache_dir / 'gnupg_mock'
35
    gnupg_mock_dir.mkdir()
36
    (gnupg_mock_dir / 'gnupg.py').write_text('GPG = None\n')
37

    
38
    monkeypatch.syspath_prepend(str(gnupg_mock_dir))
39

    
40
    import gnupg
41

    
42
    keyring_path = mock_cache_dir / 'master_keyring.gpg'
43

    
44
    class MockedImportResult:
45
        """gnupg.ImportResult replacement"""
46
        def __init__(self):
47
            """Initialize MockedImportResult object."""
48
            self.imported = 1
49

    
50
    class MockedGPG:
51
        """GPG replacement that does not really invoke GPG."""
52
        def __init__(self, keyring):
53
            """Verify the keyring path and initialize MockedGPG."""
54
            assert keyring == str(keyring_path)
55

    
56
            self.known_keys = {*keyring_path.read_text().split('\n')} \
57
                if keyring_path.exists() else set()
58

    
59
        def recv_keys(self, keyserver, key):
60
            """Mock key receiving - record requested key as received."""
61
            assert keyserver == local_apt.default_keyserver
62
            assert key not in self.known_keys
63

    
64
            self.known_keys.add(key)
65
            keyring_path.write_text('\n'.join(self.known_keys))
66

    
67
            return MockedImportResult()
68

    
69
        def list_keys(self, keys=None):
70
            """Mock key listing - return a list with dummy items."""
71
            if keys is None:
72
                return ['dummy'] * len(self.known_keys)
73
            else:
74
                return ['dummy' for k in keys if k in self.known_keys]
75

    
76
        def export_keys(self, keys, **kwargs):
77
            """
78
            Mock key export - check that the call has the expected arguments and
79
            return a dummy bytes array.
80
            """
81
            assert kwargs['armor']   == False
82
            assert kwargs['minimal'] == True
83
            assert {*keys} == self.known_keys
84

    
85
            return b'<dummy keys export>'
86

    
87
    monkeypatch.setattr(gnupg, 'GPG', MockedGPG)
88

    
89
def process_run_args(command, kwargs, expected_command):
90
    """
91
    Perform assertions common to all mocked subprocess.run() invocations and
92
    extract variable parts of the command line (if any).
93
    """
94
    assert kwargs['env'] == {'LANG': 'en_US'}
95
    assert kwargs['capture_output'] == True
96

    
97
    return process_command(command, expected_command)
98

    
99
def run_apt_get_update(command, returncode=0, **kwargs):
100
    """
101
    Instead of running an 'apt-get update' command just touch some file in apt
102
    root to indicate that the call was made.
103
    """
104
    expected = ['apt-get', '-c', '<conf_path>', 'update']
105
    conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
106

    
107
    (conf_path.parent / 'update_called').touch()
108

    
109
    return MockedCompletedProcess(command, returncode,
110
                                  text_output=kwargs.get('text'))
111

    
112
"""
113
Output of 'apt-get install --yes --just-print libjs-mathjax' on some APT-based
114
system.
115
"""
116
sample_install_stdout = '''\
117
NOTE: This is only a simulation!
118
      apt-get needs root privileges for real execution.
119
      Keep also in mind that locking is deactivated,
120
      so don't depend on the relevance to the real current situation!
121
Reading package lists...
122
Building dependency tree...
123
Reading state information...
124
The following additional packages will be installed:
125
  fonts-mathjax
126
Suggested packages:
127
  fonts-mathjax-extras fonts-stix libjs-mathjax-doc
128
The following NEW packages will be installed:
129
  fonts-mathjax libjs-mathjax
130
0 upgraded, 2 newly installed, 0 to remove and 0 not upgraded.
131
Inst fonts-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
132
Inst libjs-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
133
Conf fonts-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
134
Conf libjs-mathjax (0:2.7.9+dfsg-1 Devuan:4.0/stable, Devuan:1.0.0/unstable [all])
135
'''
136

    
137
def run_apt_get_install(command, returncode=0, **kwargs):
138
    """
139
    Instead of running an 'apt-get install' command just print a possible
140
    output of one.
141
    """
142
    expected = ['apt-get', '-c', '<conf_path>', 'install',
143
                '--yes', '--just-print', 'libjs-mathjax']
144

    
145
    conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
146

    
147
    return MockedCompletedProcess(command, returncode,
148
                                  stdout=sample_install_stdout,
149
                                  text_output=kwargs.get('text'))
150

    
151
def run_apt_get_download(command, returncode=0, **kwargs):
152
    """
153
    Instead of running an 'apt-get download' command just write some dummy
154
    .deb to the appropriate directory.
155
    """
156
    expected = ['apt-get', '-c', '<conf_path>', 'download']
157
    if 'libjs-mathjax' in command:
158
        expected.append('libjs-mathjax')
159
    else:
160
        expected.append('fonts-mathjax=0:2.7.9+dfsg-1')
161
        expected.append('libjs-mathjax=0:2.7.9+dfsg-1')
162

    
163
    conf_path = Path(process_run_args(command, kwargs, expected)['conf_path'])
164

    
165
    destination = Path(kwargs.get('cwd') or Path.cwd())
166

    
167
    package_name_regex = re.compile(r'^[^=]+-mathjax')
168

    
169
    for word in expected:
170
        match = package_name_regex.match(word)
171
        if match:
172
            filename = f'{match.group(0)}_0%3a2.7.9+dfsg-1_all.deb'
173
            deb_path = destination / filename
174
            deb_path.write_text(f'dummy {deb_path.name}')
175

    
176
    return MockedCompletedProcess(command, returncode,
177
                                  text_output=kwargs.get('text'))
178

    
179
def run_apt_get_source(command, returncode=0, **kwargs):
180
    """
181
    Instead of running an 'apt-get source' command just write some dummy
182
    "tarballs" to the appropriate directory.
183
    """
184
    expected = ['apt-get', '-c', '<conf_path>', 'source',
185
                '--download-only', 'libjs-mathjax=0:2.7.9+dfsg-1']
186
    if 'fonts-mathjax=0:2.7.9+dfsg-1' in command:
187
        if command[-1] == 'fonts-mathjax=0:2.7.9+dfsg-1':
188
            expected.append('fonts-mathjax=0:2.7.9+dfsg-1')
189
        else:
190
            expected.insert(-1, 'fonts-mathjax=0:2.7.9+dfsg-1')
191

    
192
    destination = Path(kwargs.get('cwd') or Path.cwd())
193
    for filename in [
194
        'mathjax_2.7.9+dfsg-1.debian.tar.xz',
195
        'mathjax_2.7.9+dfsg-1.dsc',
196
        'mathjax_2.7.9+dfsg.orig.tar.xz'
197
    ]:
198
        (destination / filename).write_text(f'dummy {filename}')
199

    
200
    return MockedCompletedProcess(command, returncode,
201
                                  text_output=kwargs.get('text'))
202

    
203
def make_run_apt_get(**returncodes):
204
    """
205
    Produce a function that chooses and runs the appropriate one of
206
    subprocess_run_apt_get_*() mock functions.
207
    """
208
    def mock_run(command, **kwargs):
209
        """
210
        Chooses and runs the appropriate one of subprocess_run_apt_get_*() mock
211
        functions.
212
        """
213
        for subcommand, run in [
214
            ('update',   run_apt_get_update),
215
            ('install',  run_apt_get_install),
216
            ('download', run_apt_get_download),
217
            ('source',   run_apt_get_source)
218
        ]:
219
            if subcommand in command:
220
                returncode = returncodes.get(f'{subcommand}_code', 0)
221
                return run(command, returncode, **kwargs)
222

    
223
        raise Exception('Unknown command: {}'.format(' '.join(command)))
224

    
225
    return mock_run
226

    
227
@pytest.mark.subprocess_run(local_apt, make_run_apt_get())
228
@pytest.mark.usefixtures('mock_subprocess_run', 'mock_gnupg_import')
229
def test_local_apt_contextmanager(mock_cache_dir):
230
    """
231
    Verify that the local_apt() function creates a proper apt environment and
232
    that it also properly restores it from cache.
233
    """
234
    sources_list = local_apt.SourcesList(['deb-src sth', 'deb sth'])
235

    
236
    with local_apt.local_apt(sources_list, local_apt.default_keys) as apt:
237
        apt_root = Path(apt.apt_conf).parent.parent
238

    
239
        assert (apt_root / 'etc' / 'trusted.gpg').read_bytes() == \
240
            b'<dummy keys export>'
241

    
242
        assert (apt_root / 'etc' / 'update_called').exists()
243

    
244
        assert (apt_root / 'etc' / 'apt.sources.list').read_text() == \
245
            'deb-src sth\ndeb sth'
246

    
247
        conf_lines = (apt_root / 'etc' / 'apt.conf').read_text().split('\n')
248

    
249
        # check mocked keyring
250
        assert {*local_apt.default_keys} == \
251
            {*(mock_cache_dir / 'master_keyring.gpg').read_text().split('\n')}
252

    
253
    assert not apt_root.exists()
254

    
255
    expected_conf = {
256
        'Architecture':           'amd64',
257
        'Dir':                    str(apt_root),
258
        'Dir::State':             f'{apt_root}/var/lib/apt',
259
        'Dir::State::status':     f'{apt_root}/var/lib/dpkg/status',
260
        'Dir::Etc::SourceList':   f'{apt_root}/etc/apt.sources.list',
261
        'Dir::Etc::SourceParts':  '',
262
        'Dir::Cache':             f'{apt_root}/var/cache/apt',
263
        'pkgCacheGen::Essential': 'none',
264
        'Dir::Etc::Trusted':      f'{apt_root}/etc/trusted.gpg',
265
    }
266

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

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

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

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

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

    
290
    assert not apt_root.exists()
291

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

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

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

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

    
318
STDOUT_OUTPUT_heading
319

    
320
some output
321

    
322
STDERR_OUTPUT_heading
323

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

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

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

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

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

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

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

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

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

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

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

    
378
STDOUT_OUTPUT_heading
379

    
380
{re.escape(sample_install_stdout)}
381

    
382
STDERR_OUTPUT_heading
383

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

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

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

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

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

    
409
STDOUT_OUTPUT_heading
410

    
411
some output
412

    
413
STDERR_OUTPUT_heading
414

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

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

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

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

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

    
441
        return old_run(command, **kwargs)
442

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

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

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

    
460
STDOUT_OUTPUT_heading
461

    
462
some output
463

    
464
STDERR_OUTPUT_heading
465

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

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

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

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

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

    
490
STDOUT_OUTPUT_heading
491

    
492
some output
493

    
494
STDERR_OUTPUT_heading
495

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
601
    with local_apt.piggybacked_system({
602
            'system': 'apt',
603
            **params['props'],
604
            'packages': ['some-bin-package', 'another-package=1.1-2']
605
    }, foreign_packages_dir) as piggybacked:
606
        expected_depends = [{'identifier': 'apt-common-licenses'}] \
607
            if params['base_depends'] else []
608
        assert piggybacked.resource_must_depend == expected_depends
609

    
610
        archive_files = dict(piggybacked.archive_files())
611

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

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

    
633
            if foreign_packages_dir is not None:
634
                assert path.parent == foreign_packages_dir / 'apt'
635

    
636
        license_files = {*piggybacked.package_license_files}
637

    
638
        assert license_files == {
639
            PurePosixPath('.apt-root/usr/share/doc/another-package/copyright'),
640
            PurePosixPath('.apt-root/usr/share/doc/some-bin-package/copyright')
641
        }
642

    
643
        assert ['dummy copyright'] * 2 == \
644
            [piggybacked.resolve_file(p).read_text() for p in license_files]
645

    
646
        for name in ['some-bin-package', 'another-package']:
647
            path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
648
            assert piggybacked.resolve_file(path).read_text() == \
649
                f'dummy {path.name}'
650

    
651
        assert piggybacked.resolve_file(PurePosixPath('a/b/c')) == None
652
        assert piggybacked.resolve_file(PurePosixPath('')) == None
653

    
654
        output_text = 'loading_.apt-root/a/../../../b_outside_piggybacked_dir'
655
        with pytest.raises(FileReferenceError,
656
                           match=f'^{re.escape(output_text)}$'):
657
            piggybacked.resolve_file(PurePosixPath('.apt-root/a/../../../b'))
658

    
659
        root = piggybacked.resolve_file(PurePosixPath('.apt-root/dummy')).parent
660
        assert root.is_dir()
661

    
662
    assert not root.exists()
663

    
664
    if foreign_packages_dir:
665
        assert [*tmpdir.iterdir()] == [tmpdir / 'apt']
666

    
667
@pytest.mark.subprocess_run(local_apt, run_dpkg_deb)
668
@pytest.mark.usefixtures('mock_subprocess_run')
669
def test_piggybacked_system_no_download():
670
    """
671
    Verify that the piggybacked_system() function is able to use pre-downloaded
672
    APT packages.
673
    """
674
    archive_names = {
675
        f'{package}{rest}'
676
        for package in ('some-lib_1:2.3', 'other-lib_4.45.2')
677
        for rest in ('-1_all.deb', '.orig.tar.gz', '-1.debian.tar.xz', '-1.dsc')
678
    }
679

    
680
    with TemporaryDirectory() as td:
681
        td = Path(td)
682
        (td / 'apt').mkdir()
683
        for name in archive_names:
684
            (td / 'apt' / name).write_text(f'dummy {name}')
685

    
686
        with local_apt.piggybacked_system({
687
                'system': 'apt',
688
                'distribution': 'nabia',
689
                'dependencies': True,
690
                'packages': ['whatever', 'whatever2']
691
        }, td) as piggybacked:
692
            archive_files = dict(piggybacked.archive_files())
693

    
694
            assert {*archive_files.keys()} == \
695
                {PurePosixPath('apt') / name for name in archive_names}
696

    
697
            for path in archive_files.values():
698
                assert path.read_text() == f'dummy {path.name}'
699

    
700
            assert {*piggybacked.package_license_files} == {
701
                PurePosixPath('.apt-root/usr/share/doc/some-lib/copyright'),
702
                PurePosixPath('.apt-root/usr/share/doc/other-lib/copyright')
703
            }
704

    
705
            for name in ['some-lib', 'other-lib']:
706
                path = PurePosixPath(f'.apt-root/etc/dummy_{name}_config')
707
                assert piggybacked.resolve_file(path).read_text() == \
708
                    f'dummy {path.name}'
709

    
710
@pytest.mark.subprocess_run(local_apt, run_missing_executable)
711
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
712
def test_piggybacked_system_missing():
713
    """
714
    Verify that the piggybacked_system() function raises a proper error when
715
    'dpkg-deb' is missing.
716
    """
717
    with pytest.raises(local_apt.AptError,
718
                       match='^couldnt_execute_dpkg-deb_is_it_installed$'):
719
        with local_apt.piggybacked_system({
720
                'system': 'apt',
721
                'distribution': 'nabia',
722
                'packages': ['some-package'],
723
                'dependencies': False
724
        }, None) as piggybacked:
725
            pass
726

    
727
@pytest.mark.subprocess_run(local_apt, lambda c, **kw: run_dpkg_deb(c, 1, **kw))
728
@pytest.mark.usefixtures('mock_download_packages', 'mock_subprocess_run')
729
def test_piggybacked_system_fail():
730
    """
731
    Verify that the piggybacked_system() function raises a proper error when
732
    'dpkg-deb -x' command returns non-0.
733
    """
734
    error_regex = """^\
735
command_dpkg-deb -x \\S+\\.deb \\S+_failed
736

    
737
STDOUT_OUTPUT_heading
738

    
739
some output
740

    
741
STDERR_OUTPUT_heading
742

    
743
some error output\
744
$\
745
"""
746

    
747
    with pytest.raises(local_apt.AptError, match=error_regex):
748
        with local_apt.piggybacked_system({
749
                'system': 'apt',
750
                'distribution': 'nabia',
751
                'packages': ['some-package'],
752
                'dependencies': False
753
        }, None) as piggybacked:
754
            pass
(5-5/5)