public inbox for passt-dev@passt.top
 help / color / mirror / code / Atom feed
blob 0554fbd0ece49d5e4826dd735fba7b9d255d2ec5 7451 bytes (raw)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
 
#! /usr/bin/env python3

# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright Red Hat
# Author: David Gibson <david@gibson.dropbear.id.au>

"""
Test A Simple Socket Transport

tasst/snh.py - Simulated network hosts for testing
"""


import contextlib
import ipaddress
import json
import subprocess
import sys

import exeter


STDOUT = 1


class SnhProcess(contextlib.AbstractContextManager):
    """
    A background process running on a SimNetHost
    """

    def __init__(self, snh, *cmd, check=True, context_timeout=1.0, **kwargs):
        self.snh = snh
        self.cmd = cmd
        self.check = check
        self.context_timeout = float(context_timeout)

        self.kwargs = kwargs

    def __enter__(self):
        self.popen = subprocess.Popen(self.cmd, **self.kwargs)
        return self

    def run(self, **kwargs):
        stdout, stderr = self.popen.communicate(**kwargs)
        cp = subprocess.CompletedProcess(self.popen.args,
                                         self.popen.returncode,
                                         stdout, stderr)
        if self.check:
            cp.check_returncode()
        return cp

    def terminate(self):
        self.popen.terminate()

    def kill(self):
        self.popen.kill()

    def __exit__(self, *exc_details):
        try:
            self.popen.wait(timeout=self.context_timeout)
        except subprocess.TimeoutExpired as e:
            self.terminate()
            try:
                self.popen.wait(timeout=self.context_timeout)
            except subprocess.TimeoutExpired:
                self.kill()
            raise e


class SimNetHost(contextlib.AbstractContextManager):
    """
    A (usually virtual or simulated) location where we can execute
    commands and configure networks.

    """

    def __init__(self, name):
        self.name = name   # For debugging

    def hostify(self, *cmd, **kwargs):
        raise NotImplementedError

    def __enter__(self):
        return self

    def __exit__(self, *exc_details):
        pass

    def output(self, *cmd, **kwargs):
        proc = self.fg(*cmd, capture=STDOUT, **kwargs)
        return proc.stdout

    def fg(self, *cmd, timeout=None, **kwargs):
        # We don't use subprocess.run() because it kills without
        # attempting to terminate on timeout
        with self.bg(*cmd, **kwargs) as proc:
            res = proc.run(timeout=timeout)
        return res

    def bg(self, *cmd, capture=None, **kwargs):
        if capture == STDOUT:
            kwargs['stdout'] = subprocess.PIPE
        hostcmd, kwargs = self.hostify(*cmd, **kwargs)
        proc = SnhProcess(self, *hostcmd, **kwargs)
        print(f"SimNetHost {self.name}: Started {cmd} => {proc}",
              file=sys.stderr)
        return proc

    def ifs(self):
        info = json.loads(self.output('ip', '-j', 'link', 'show'))
        return [i['ifname'] for i in info]

    def ifup(self, ifname):
        self.fg('ip', 'link', 'set', f'{ifname}', 'up', capable=True)

    def addrinfos(self, ifname, **criteria):
        info = json.loads(self.output('ip', '-j', 'addr', 'show', f'{ifname}'))
        assert len(info) == 1  # We specified a specific interface

        ais = list(ai for ai in info[0]['addr_info'])
        for key, value in criteria.items():
            ais = [ai for ai in ais if key in ai and ai[key] == value]

        return ais

    def addrs(self, ifname, **criteria):
        # Return just the parsed, non-tentative addresses
        return [ipaddress.ip_interface(f'{ai["local"]}/{ai["prefixlen"]}')
                for ai in self.addrinfos(ifname, **criteria)
                if 'tentative' not in ai]

    def mtu(self, ifname):
        cmd = ['ip', '-j', 'link', 'show', f'{ifname}']
        (info,) = json.loads(self.output(*cmd))
        return info['mtu']

    # Internal tests
    def test_true(self):
        with self as snh:
            snh.fg('true')

    def test_false(self):
        with self as snh:
            exeter.assert_raises(subprocess.CalledProcessError,
                                 snh.fg, 'false')

    def test_echo(self):
        msg = 'Hello tasst'
        with self as snh:
            out = snh.output('echo', f'{msg}')
            exeter.assert_eq(out, msg.encode('utf-8') + b'\n')

    def test_timeout(self):
        with self as snh:
            exeter.assert_raises(subprocess.TimeoutExpired, snh.fg,
                                 'sleep', 'infinity', timeout=0.1, check=False)

    def test_bg_true(self):
        with self as snh:
            with snh.bg('true'):
                pass

    def test_bg_false(self):
        with self as snh:
            with snh.bg('false') as proc:
                exeter.assert_raises(subprocess.CalledProcessError, proc.run)

    def test_bg_echo(self):
        msg = 'Hello tasst'
        with self as snh:
            with snh.bg('echo', f'{msg}', capture=STDOUT) as proc:
                res = proc.run()
            exeter.assert_eq(res.stdout, msg.encode('utf-8') + b'\n')

    def test_bg_timeout(self):
        with self as snh:
            with snh.bg('sleep', 'infinity') as proc:
                exeter.assert_raises(subprocess.TimeoutExpired,
                                     proc.run, timeout=0.1)
                proc.terminate()

    def test_bg_context_timeout(self):
        with self as snh:
            def run_timeout():
                with snh.bg('sleep', 'infinity', context_timeout=0.1):
                    pass
            exeter.assert_raises(subprocess.TimeoutExpired, run_timeout)

    def test_has_lo(self):
        with self as snh:
            assert 'lo' in snh.ifs()

    def test_lo_addrs(self):
        expected = set(ipaddress.ip_interface(a)
                       for a in ['127.0.0.1/8', '::1/128'])
        with self as snh:
            assert set(snh.addrs('lo')) == expected

    def test_lo_mtu(self):
        with self as snh:
            exeter.assert_eq(snh.mtu('lo'), 65536)

    SELFTESTS = [test_true, test_false, test_echo, test_timeout,
                 test_bg_true, test_bg_false, test_bg_echo, test_bg_timeout,
                 test_bg_context_timeout,
                 test_has_lo, test_lo_addrs, test_lo_mtu]

    @classmethod
    def selftest(cls, setup):
        "Register standard snh tests for instance returned by setup"
        for t in cls.SELFTESTS:
            testid = f'{setup.__qualname__}|{t.__qualname__}'
            exeter.register_pipe(testid, setup, t)

    # Additional tests only valid if the snh is isolated (no outside
    # network connections)
    def test_is_isolated(self):
        with self as snh:
            exeter.assert_eq(snh.ifs(), ['lo'])

    ISOLATED_SELFTESTS = [test_is_isolated]

    @classmethod
    def selftest_isolated(cls, setup):
        "Register self tests for an isolated snh example"
        cls.selftest(setup)
        for t in cls.ISOLATED_SELFTESTS:
            testid = f'{setup.__qualname__}|{t.__qualname__}'
            exeter.register_pipe(testid, setup, t)


class RealHost(SimNetHost):
    """Represents the host on which the tests are running (as opposed
    to some simulated host created by the tests)

    """

    def __init__(self):
        super().__init__('REAL_HOST')

    def hostify(self, *cmd, capable=False, **kwargs):
        assert not capable, \
            "BUG: Shouldn't run commands with capabilities on host"
        return cmd, kwargs


SimNetHost.selftest(RealHost)

debug log:

solving 0554fbd0 ...
found 0554fbd0 in https://archives.passt.top/passt-dev/20240805123701.1720730-15-david@gibson.dropbear.id.au/
found fd8f6f13 in https://archives.passt.top/passt-dev/20240805123701.1720730-13-david@gibson.dropbear.id.au/
found 598ea979 in https://archives.passt.top/passt-dev/20240805123701.1720730-12-david@gibson.dropbear.id.au/
found 8ee9802a in https://archives.passt.top/passt-dev/20240805123701.1720730-11-david@gibson.dropbear.id.au/
found dfbe2c84 in https://archives.passt.top/passt-dev/20240805123701.1720730-10-david@gibson.dropbear.id.au/

applying [1/5] https://archives.passt.top/passt-dev/20240805123701.1720730-10-david@gibson.dropbear.id.au/
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
new file mode 100644
index 00000000..dfbe2c84


applying [2/5] https://archives.passt.top/passt-dev/20240805123701.1720730-11-david@gibson.dropbear.id.au/
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
index dfbe2c84..8ee9802a 100644


applying [3/5] https://archives.passt.top/passt-dev/20240805123701.1720730-12-david@gibson.dropbear.id.au/
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
index 8ee9802a..598ea979 100644


applying [4/5] https://archives.passt.top/passt-dev/20240805123701.1720730-13-david@gibson.dropbear.id.au/
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
index 598ea979..fd8f6f13 100644


applying [5/5] https://archives.passt.top/passt-dev/20240805123701.1720730-15-david@gibson.dropbear.id.au/
diff --git a/test/tasst/snh.py b/test/tasst/snh.py
index fd8f6f13..0554fbd0 100644

Checking patch test/tasst/snh.py...
Applied patch test/tasst/snh.py cleanly.
Checking patch test/tasst/snh.py...
Applied patch test/tasst/snh.py cleanly.
Checking patch test/tasst/snh.py...
Applied patch test/tasst/snh.py cleanly.
Checking patch test/tasst/snh.py...
Applied patch test/tasst/snh.py cleanly.
Checking patch test/tasst/snh.py...
Applied patch test/tasst/snh.py cleanly.

index at:
100644 0554fbd0ece49d5e4826dd735fba7b9d255d2ec5	test/tasst/snh.py

Code repositories for project(s) associated with this public inbox

	https://passt.top/passt

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for IMAP folder(s).