Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 1 | Host stack test framework |
| 2 | ========================= |
| 3 | |
| 4 | Overview |
| 5 | -------- |
| 6 | |
| 7 | The goal of the Host stack test framework (**hs-test**) is to ease writing and running end-to-end tests for VPP's Host Stack. |
| 8 | End-to-end tests often want multiple VPP instances, network namespaces, different types of interfaces |
| 9 | and to execute external tools or commands. With such requirements the existing VPP test framework is not sufficient. |
| 10 | For this, ``Go`` was chosen as a high level language, allowing rapid development, with ``Docker`` and ``ip`` being the tools for creating required topology. |
| 11 | |
| 12 | Go's package `testing`_ together with `go test`_ command form the base framework upon which the *hs-test* is built and run. |
| 13 | |
| 14 | Anatomy of a test case |
| 15 | ---------------------- |
| 16 | |
| 17 | **Prerequisites**: |
| 18 | |
Filip Tehlar | 671cf51 | 2023-01-31 10:34:18 +0100 | [diff] [blame] | 19 | * Install hs-test dependencies with ``make install-deps`` |
| 20 | * Tests use *hs-test*'s own docker image, so building it before starting tests is a prerequisite. Run ``make build[-debug]`` to do so |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 21 | * Docker has to be installed and Go has to be in path of both the running user and root |
| 22 | * Root privileges are required to run tests as it uses Linux ``ip`` command for configuring topology |
| 23 | |
| 24 | **Action flow when running a test case**: |
| 25 | |
Filip Tehlar | 671cf51 | 2023-01-31 10:34:18 +0100 | [diff] [blame] | 26 | #. It starts with running ``make test``. Optional arguments are VERBOSE, PERSIST (topology configuration isn't cleaned up after test run), |
| 27 | and TEST=<test-name> to run specific test. |
| 28 | #. ``make list-tests`` (or ``make help``) shows all test names. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 29 | #. ``go test`` compiles package ``main`` along with any files with names matching the file pattern ``*_test.go`` |
| 30 | and then runs the resulting test binaries |
| 31 | #. The go test framework runs each function matching :ref:`naming convention<test-convention>`. Each of these corresponds to a `test suite`_ |
| 32 | #. Testify toolkit's ``suite.Run(t *testing.T, suite TestingSuite)`` function runs the suite and does the following: |
| 33 | |
| 34 | #. Suite is initialized. The topology is loaded and configured in this step |
| 35 | #. Test suite runs all the tests attached to it |
| 36 | #. Execute tear-down functions, which currently consists of stopping running containers |
| 37 | and clean-up of test topology |
| 38 | |
| 39 | Adding a test case |
| 40 | ------------------ |
| 41 | |
| 42 | This describes adding a new test case to an existing suite. |
| 43 | For adding a new suite, please see `Modifying the framework`_ below. |
| 44 | |
| 45 | #. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists |
| 46 | #. Declare method whose name starts with ``Test`` and specifies its receiver as a pointer to the suite's struct (defined in ``framework_test.go``) |
| 47 | #. Implement test behaviour inside the test method. This typically includes the following: |
| 48 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 49 | #. Retrieve a running container in which to run some action. Method ``getContainerByName`` |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 50 | from ``HstSuite`` struct serves this purpose |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 51 | #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``vppctl`` method to access debug CLI |
| 52 | #. Run arbitrary commands inside the containers with ``exec`` method |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 53 | #. Run other external tool with one of the preexisting functions in the ``utils.go`` file. |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 54 | For example, use ``wget`` with ``startWget`` function |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 55 | #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 56 | #. Verify results of your tests using ``assert`` methods provided by the test suite, |
| 57 | implemented by HstSuite struct |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 58 | |
| 59 | **Example test case** |
| 60 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 61 | Assumed are two docker containers, each with its own VPP instance running. One VPP then pings the other. |
| 62 | This can be put in file ``extras/hs-test/my_test.go`` and run with command ``./test -run TestMySuite/TestMyCase``. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 63 | |
| 64 | :: |
| 65 | |
| 66 | package main |
| 67 | |
| 68 | import ( |
| 69 | "fmt" |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 70 | ) |
| 71 | |
| 72 | func (s *MySuite) TestMyCase() { |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 73 | clientVpp := s.getContainerByName("client-vpp").vppInstance |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 74 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 75 | serverVethAddress := s.netInterfaces["server-iface"].AddressString() |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 76 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 77 | result := clientVpp.vppctl("ping " + serverVethAddress) |
| 78 | s.assertNotNil(result) |
| 79 | s.log(result) |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 80 | } |
| 81 | |
| 82 | Modifying the framework |
| 83 | ----------------------- |
| 84 | |
| 85 | **Adding a test suite** |
| 86 | |
| 87 | .. _test-convention: |
| 88 | |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 89 | #. Adding a new suite takes place in ``framework_test.go`` and by creating a new file for the suite. |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 90 | Naming convention for the suite files is ``suite_name_test.go`` where *name* will be replaced |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 91 | by the actual name |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 92 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 93 | #. Make a ``struct``, in the suite file, with at least ``HstSuite`` struct as its member. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 94 | HstSuite provides functionality that can be shared for all suites, like starting containers |
| 95 | |
| 96 | :: |
| 97 | |
| 98 | type MySuite struct { |
| 99 | HstSuite |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 100 | } |
| 101 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 102 | #. In suite file, implement ``SetupSuite`` method which testify runs once before starting any of the tests. |
| 103 | It's important here to call ``configureNetworkTopology`` method, |
| 104 | pass the topology name to the function in a form of file name of one of the *yaml* files in ``topo-network`` folder. |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 105 | Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo-network/myTopology.yaml`` |
| 106 | This will ensure network topology, such as network interfaces and namespaces, will be created. |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 107 | Another important method to call is ``loadContainerTopology()`` which will load |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 108 | containers and shared volumes used by the suite. This time the name passed to method corresponds |
| 109 | to file in ``extras/hs-test/topo-containers`` folder |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 110 | |
| 111 | :: |
| 112 | |
| 113 | func (s *MySuite) SetupSuite() { |
| 114 | // Add custom setup code here |
| 115 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 116 | s.configureNetworkTopology("myTopology") |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 117 | s.loadContainerTopology("2peerVeth") |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 118 | } |
| 119 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 120 | #. In suite file, implement ``SetupTest`` method which gets executed before each test. Starting containers and |
| 121 | configuring VPP is usually placed here |
| 122 | |
| 123 | :: |
| 124 | |
| 125 | func (s *MySuite) SetupTest() { |
| 126 | s.SetupVolumes() |
| 127 | s.SetupContainers() |
| 128 | } |
| 129 | |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 130 | #. In order for ``go test`` to run this suite, we need to create a normal test function and pass our suite to ``suite.Run``. |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 131 | These functions are placed at the end of ``framework_test.go`` |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 132 | |
| 133 | :: |
| 134 | |
| 135 | func TestMySuite(t *testing.T) { |
| 136 | var m MySuite |
| 137 | suite.Run(t, &m) |
| 138 | } |
| 139 | |
| 140 | #. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above |
| 141 | |
| 142 | **Adding a topology element** |
| 143 | |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 144 | Topology configuration exists as ``yaml`` files in the ``extras/hs-test/topo-network`` and |
| 145 | ``extras/hs-test/topo-containers`` folders. Processing of a network topology file for a particular test suite |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 146 | is started by the ``configureNetworkTopology`` method depending on which file's name is passed to it. |
| 147 | Specified file is loaded and converted into internal data structures which represent various elements of the topology. |
| 148 | After parsing the configuration, framework loops over the elements and configures them one by one on the host system. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 149 | |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 150 | These are currently supported types of network elements. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 151 | |
| 152 | * ``netns`` - network namespace |
| 153 | * ``veth`` - veth network interface, optionally with target network namespace or IPv4 address |
| 154 | * ``bridge`` - ethernet bridge to connect created interfaces, optionally with target network namespace |
| 155 | * ``tap`` - tap network interface with IP address |
| 156 | |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 157 | Similarly, container topology is started by ``loadContainerTopology()``, configuration file is processed |
| 158 | so that test suite retains map of defined containers and uses that to start them at the beginning |
| 159 | of each test case and stop containers after the test finishes. Container configuration can specify |
| 160 | also volumes which allow to share data between containers or between host system and containers. |
| 161 | |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 162 | Supporting a new type of topology element requires adding code to recognize the new element type during loading. |
Maros Ondrejicka | db823ed | 2022-12-14 16:30:04 +0100 | [diff] [blame] | 163 | And adding code to set up the element in the host system with some Linux tool, such as *ip*. |
| 164 | This should be implemented in ``netconfig.go`` for network and in ``container.go`` for containers and volumes. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 165 | |
| 166 | **Communicating between containers** |
| 167 | |
| 168 | When two VPP instances or other applications, each in its own Docker container, |
| 169 | want to communicate there are typically two ways this can be done within *hs-test*. |
| 170 | |
| 171 | * Network interfaces. Containers are being created with ``-d --network host`` options, |
| 172 | so they are connected with interfaces created in host system |
| 173 | * Shared folders. Containers are being created with ``-v`` option to create shared `volumes`_ between host system and containers |
| 174 | or just between containers |
| 175 | |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 176 | Host system connects to VPP instances running in containers using a shared folder |
| 177 | where binary API socket is accessible by both sides. |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 178 | |
| 179 | **Adding an external tool** |
| 180 | |
| 181 | If an external program should be executed as part of a test case, it might be useful to wrap its execution in its own function. |
| 182 | These types of functions are placed in the ``utils.go`` file. If the external program is not available by default in Docker image, |
| 183 | add its installation to ``extras/hs-test/Dockerfile.vpp`` in ``apt-get install`` command. |
| 184 | Alternatively copy the executable from host system to the Docker image, similarly how the VPP executables and libraries are being copied. |
| 185 | |
Filip Tehlar | 9418143 | 2024-01-15 13:11:28 +0100 | [diff] [blame] | 186 | **Skipping tests** |
| 187 | |
| 188 | ``HstSuite`` provides several methods that can be called in tests for skipping it conditionally or unconditionally such as: |
| 189 | ``skip()``, ``SkipIfMultiWorker()``, ``SkipUnlessExtendedTestsBuilt()``. |
| 190 | However the tests currently run under test suites which set up topology and containers before actual test is run. For the reason of saving |
| 191 | test run time it is not advisable to use aforementioned skip methods and instead prefix test name with ``Skip``: |
| 192 | |
| 193 | :: |
| 194 | |
| 195 | func (s *MySuite) SkipTest(){ |
| 196 | |
| 197 | |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 198 | **Eternal dependencies** |
| 199 | |
| 200 | * Linux tools ``ip``, ``brctl`` |
| 201 | * Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made, |
| 202 | they are reasonably up-to-date automatically |
Maros Ondrejicka | 56bfc63 | 2023-02-21 13:42:35 +0100 | [diff] [blame] | 203 | * Programs in Docker images - ``envoyproxy/envoy-contrib`` and ``nginx`` |
| 204 | * ``http_server`` - homegrown application that listens on specified port and sends a test file in response |
Maros Ondrejicka | 7943c90 | 2022-11-08 08:00:51 +0100 | [diff] [blame] | 205 | * Non-standard Go libraries - see ``extras/hs-test/go.mod`` |
| 206 | |
| 207 | Generally, these will be updated on a per-need basis, for example when a bug is discovered |
| 208 | or a new version incompatibility issue occurs. |
| 209 | |
| 210 | |
| 211 | .. _testing: https://pkg.go.dev/testing |
| 212 | .. _go test: https://pkg.go.dev/cmd/go#hdr-Test_packages |
| 213 | .. _test suite: https://github.com/stretchr/testify#suite-package |
| 214 | .. _volumes: https://docs.docker.com/storage/volumes/ |
| 215 | |