Commit f940d704 authored by Paolo Abeni's avatar Paolo Abeni
Browse files

Merge branch 'selftests-tc-testing-parallel-tdc'

Pedro Tammela says:

====================
selftests/tc-testing: parallel tdc

As the number of tdc tests is growing, so is our completion wall time.
One of the ideas to improve this is to run tests in parallel, as they
are self contained.

This series allows for tests to run in parallel, in batches of 32 tests.
Not all tests can run in parallel as they might conflict with each other.
The code will still honor this requirement even when trying to run the
tests over the worker pool.

In order to make this happen we had to localize the test resources
(patches 1 and 2), where instead of having all tests sharing one single
namespace and veths devices each test now gets it's own local namespace and devices.

Even though the tests serialize over rtnl_lock in the kernel, we
measured a speedup of about 3x in a test VM.
====================

Link: https://lore.kernel.org/r/20230919135404.1778595-1-pctammela@mojatatu.com


Signed-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
parents d387e34f d3fc4eea
Loading
Loading
Loading
Loading
+10 −55
Original line number Diff line number Diff line
@@ -9,8 +9,7 @@ execute them inside a network namespace dedicated to the task.
REQUIREMENTS
------------

*  Minimum Python version of 3.4. Earlier 3.X versions may work but are not
   guaranteed.
*  Minimum Python version of 3.8.

*  The kernel must have network namespace support if using nsPlugin

@@ -96,6 +95,15 @@ the stdout with a regular expression.

Each of the commands in any stage will run in a shell instance.

Each test is an atomic unit. A test that for whatever reason spans multiple test
definitions is a bug.

A test that runs inside a namespace (requires "nsPlugin") will run in parallel
with other tests.

Tests that use netdevsim or don't run inside a namespace run serially with regards
to each other.


USER-DEFINED CONSTANTS
----------------------
@@ -116,59 +124,6 @@ COMMAND LINE ARGUMENTS

Run tdc.py -h to see the full list of available arguments.

usage: tdc.py [-h] [-p PATH] [-D DIR [DIR ...]] [-f FILE [FILE ...]]
              [-c [CATG [CATG ...]]] [-e ID [ID ...]] [-l] [-s] [-i] [-v] [-N]
              [-d DEVICE] [-P] [-n] [-V]

Linux TC unit tests

optional arguments:
  -h, --help            show this help message and exit
  -p PATH, --path PATH  The full path to the tc executable to use
  -v, --verbose         Show the commands that are being run
  -N, --notap           Suppress tap results for command under test
  -d DEVICE, --device DEVICE
                        Execute test cases that use a physical device, where
                        DEVICE is its name. (If not defined, tests that require
                        a physical device will be skipped)
  -P, --pause           Pause execution just before post-suite stage

selection:
  select which test cases: files plus directories; filtered by categories
  plus testids

  -D DIR [DIR ...], --directory DIR [DIR ...]
                        Collect tests from the specified directory(ies)
                        (default [tc-tests])
  -f FILE [FILE ...], --file FILE [FILE ...]
                        Run tests from the specified file(s)
  -c [CATG [CATG ...]], --category [CATG [CATG ...]]
                        Run tests only from the specified category/ies, or if
                        no category/ies is/are specified, list known
                        categories.
  -e ID [ID ...], --execute ID [ID ...]
                        Execute the specified test cases with specified IDs

action:
  select action to perform on selected test cases

  -l, --list            List all test cases, or those only within the
                        specified category
  -s, --show            Display the selected test cases
  -i, --id              Generate ID numbers for new test cases

netns:
  options for nsPlugin (run commands in net namespace)

  -N, --no-namespace
                        Do not run commands in a network namespace.

valgrind:
  options for valgrindPlugin (run command under test under Valgrind)

  -V, --valgrind        Run commands under valgrind


PLUGIN ARCHITECTURE
-------------------

+2 −2
Original line number Diff line number Diff line
@@ -5,10 +5,10 @@ class TdcPlugin:
        super().__init__()
        print(' -- {}.__init__'.format(self.sub_class))

    def pre_suite(self, testcount, testidlist):
    def pre_suite(self, testcount, testlist):
        '''run commands before test_runner goes into a test loop'''
        self.testcount = testcount
        self.testidlist = testidlist
        self.testlist = testlist
        if self.args.verbose > 1:
            print(' -- {}.pre_suite'.format(self.sub_class))

+2 −1
Original line number Diff line number Diff line
@@ -59,7 +59,8 @@ class TestResult:
        return self.steps

class TestSuiteReport():
    _testsuite = []
    def __init__(self):
        self._testsuite = []

    def add_resultdata(self, result_data):
        if isinstance(result_data, TestResult):
+143 −51
Original line number Diff line number Diff line
@@ -3,35 +3,96 @@ import signal
from string import Template
import subprocess
import time
from multiprocessing import Pool
from functools import cached_property
from TdcPlugin import TdcPlugin

from tdc_config import *

def prepare_suite(obj, test):
    original = obj.args.NAMES

    if 'skip' in test and test['skip'] == 'yes':
        return

    if 'nsPlugin' not in test['plugins']:
        return

    shadow = {}
    shadow['IP'] = original['IP']
    shadow['TC'] = original['TC']
    shadow['NS'] = '{}-{}'.format(original['NS'], test['random'])
    shadow['DEV0'] = '{}id{}'.format(original['DEV0'], test['id'])
    shadow['DEV1'] = '{}id{}'.format(original['DEV1'], test['id'])
    shadow['DUMMY'] = '{}id{}'.format(original['DUMMY'], test['id'])
    shadow['DEV2'] = original['DEV2']
    obj.args.NAMES = shadow

    if obj.args.namespace:
        obj._ns_create()
    else:
        obj._ports_create()

    # Make sure the netns is visible in the fs
    while True:
        obj._proc_check()
        try:
            ns = obj.args.NAMES['NS']
            f = open('/run/netns/{}'.format(ns))
            f.close()
            break
        except:
            time.sleep(0.1)
            continue

    obj.args.NAMES = original

class SubPlugin(TdcPlugin):
    def __init__(self):
        self.sub_class = 'ns/SubPlugin'
        super().__init__()

    def pre_suite(self, testcount, testidlist):
        '''run commands before test_runner goes into a test loop'''
        super().pre_suite(testcount, testidlist)
    def pre_suite(self, testcount, testlist):
        from itertools import cycle

        if self.args.namespace:
            self._ns_create()
        else:
            self._ports_create()
        super().pre_suite(testcount, testlist)

    def post_suite(self, index):
        '''run commands after test_runner goes into a test loop'''
        super().post_suite(index)
        print("Setting up namespaces and devices...")

        with Pool(self.args.mp) as p:
            it = zip(cycle([self]), testlist)
            p.starmap(prepare_suite, it)

    def pre_case(self, caseinfo, test_skip):
        if self.args.verbose:
            print('{}.post_suite'.format(self.sub_class))
            print('{}.pre_case'.format(self.sub_class))

        if test_skip:
            return


    def post_case(self):
        if self.args.verbose:
            print('{}.post_case'.format(self.sub_class))

        if self.args.namespace:
            self._ns_destroy()
        else:
            self._ports_destroy()

    def post_suite(self, index):
        if self.args.verbose:
            print('{}.post_suite'.format(self.sub_class))

        # Make sure we don't leak resources
        for f in os.listdir('/run/netns/'):
            cmd = self._replace_keywords("$IP netns del {}".format(f))

            if self.args.verbose > 3:
                print('_exec_cmd:  command "{}"'.format(cmd))

            subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    def add_args(self, parser):
        super().add_args(parser)
        self.argparser_group = self.argparser.add_argument_group(
@@ -77,18 +138,43 @@ class SubPlugin(TdcPlugin):
            print('adjust_command:  return command [{}]'.format(command))
        return command

    def _ports_create(self):
        cmd = '$IP link add $DEV0 type veth peer name $DEV1'
        self._exec_cmd('pre', cmd)
        cmd = '$IP link set $DEV0 up'
        self._exec_cmd('pre', cmd)
    def _ports_create_cmds(self):
        cmds = []

        cmds.append(self._replace_keywords('link add $DEV0 type veth peer name $DEV1'))
        cmds.append(self._replace_keywords('link set $DEV0 up'))
        cmds.append(self._replace_keywords('link add $DUMMY type dummy'))
        if not self.args.namespace:
            cmd = '$IP link set $DEV1 up'
            self._exec_cmd('pre', cmd)
            cmds.append(self._replace_keywords('link set $DEV1 up'))

        return cmds

    def _ports_create(self):
        self._exec_cmd_batched('pre', self._ports_create_cmds())

    def _ports_destroy_cmd(self):
        return self._replace_keywords('link del $DEV0')

    def _ports_destroy(self):
        cmd = '$IP link del $DEV0'
        self._exec_cmd('post', cmd)
        self._exec_cmd('post', self._ports_destroy_cmd())

    def _ns_create_cmds(self):
        cmds = []

        if self.args.namespace:
            ns = self.args.NAMES['NS']

            cmds.append(self._replace_keywords('netns add {}'.format(ns)))
            cmds.append(self._replace_keywords('link set $DEV1 netns {}'.format(ns)))
            cmds.append(self._replace_keywords('link set $DUMMY netns {}'.format(ns)))
            cmds.append(self._replace_keywords('netns exec {} $IP link set $DEV1 up'.format(ns)))
            cmds.append(self._replace_keywords('netns exec {} $IP link set $DUMMY up'.format(ns)))

            if self.args.device:
                cmds.append(self._replace_keywords('link set $DEV2 netns {}'.format(ns)))
                cmds.append(self._replace_keywords('netns exec {} $IP link set $DEV2 up'.format(ns)))

        return cmds

    def _ns_create(self):
        '''
@@ -96,18 +182,10 @@ class SubPlugin(TdcPlugin):
        the required network devices for it.
        '''
        self._ports_create()
        if self.args.namespace:
            cmd = '$IP netns add {}'.format(self.args.NAMES['NS'])
            self._exec_cmd('pre', cmd)
            cmd = '$IP link set $DEV1 netns {}'.format(self.args.NAMES['NS'])
            self._exec_cmd('pre', cmd)
            cmd = '$IP -n {} link set $DEV1 up'.format(self.args.NAMES['NS'])
            self._exec_cmd('pre', cmd)
            if self.args.device:
                cmd = '$IP link set $DEV2 netns {}'.format(self.args.NAMES['NS'])
                self._exec_cmd('pre', cmd)
                cmd = '$IP -n {} link set $DEV2 up'.format(self.args.NAMES['NS'])
                self._exec_cmd('pre', cmd)
        self._exec_cmd_batched('pre', self._ns_create_cmds())

    def _ns_destroy_cmd(self):
        return self._replace_keywords('netns delete {}'.format(self.args.NAMES['NS']))

    def _ns_destroy(self):
        '''
@@ -115,35 +193,49 @@ class SubPlugin(TdcPlugin):
        devices as well)
        '''
        if self.args.namespace:
            cmd = '$IP netns delete {}'.format(self.args.NAMES['NS'])
            self._exec_cmd('post', cmd)
            self._exec_cmd('post', self._ns_destroy_cmd())
            self._ports_destroy()

    @cached_property
    def _proc(self):
        ip = self._replace_keywords("$IP -b -")
        proc = subprocess.Popen(ip,
            shell=True,
            stdin=subprocess.PIPE,
            env=ENVIR)

        return proc

    def _proc_check(self):
        proc = self._proc

        proc.poll()

        if proc.returncode is not None and proc.returncode != 0:
            raise RuntimeError("iproute2 exited with an error code")

    def _exec_cmd(self, stage, command):
        '''
        Perform any required modifications on an executable command, then run
        it in a subprocess and return the results.
        '''
        if '$' in command:
            command = self._replace_keywords(command)

        self.adjust_command(stage, command)
        if self.args.verbose:
        if self.args.verbose > 3:
            print('_exec_cmd:  command "{}"'.format(command))
        proc = subprocess.Popen(command,
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            env=ENVIR)
        (rawout, serr) = proc.communicate()

        if proc.returncode != 0 and len(serr) > 0:
            foutput = serr.decode("utf-8")
        else:
            foutput = rawout.decode("utf-8")
        proc = self._proc

        proc.stdin.write((command + '\n').encode())
        proc.stdin.flush()

        if self.args.verbose > 3:
            print('_exec_cmd proc: {}'.format(proc))

        self._proc_check()

        proc.stdout.close()
        proc.stderr.close()
        return proc, foutput
    def _exec_cmd_batched(self, stage, commands):
        for cmd in commands:
            self._exec_cmd(stage, cmd)

    def _replace_keywords(self, cmd):
        """
+2 −2
Original line number Diff line number Diff line
@@ -10,9 +10,9 @@ class SubPlugin(TdcPlugin):
        self.sub_class = 'root/SubPlugin'
        super().__init__()

    def pre_suite(self, testcount, testidlist):
    def pre_suite(self, testcount, testlist):
        # run commands before test_runner goes into a test loop
        super().pre_suite(testcount, testidlist)
        super().pre_suite(testcount, testlist)

        if os.geteuid():
            print('This script must be run with root privileges', file=sys.stderr)
Loading