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 | |
| 19 | * Tests use *hs-test*'s own docker image, so building it before starting tests is a prerequisite. Run ``sudo make`` to do so |
| 20 | * Docker has to be installed and Go has to be in path of both the running user and root |
| 21 | * Root privileges are required to run tests as it uses Linux ``ip`` command for configuring topology |
| 22 | |
| 23 | **Action flow when running a test case**: |
| 24 | |
| 25 | #. It starts with running ``./test``. This script is basically a wrapper for ``go test`` and accepts its parameters, |
| 26 | for example following runs a specific test: ``./test -run TestNs/TestHttpTps`` |
| 27 | #. ``go test`` compiles package ``main`` along with any files with names matching the file pattern ``*_test.go`` |
| 28 | and then runs the resulting test binaries |
| 29 | #. The go test framework runs each function matching :ref:`naming convention<test-convention>`. Each of these corresponds to a `test suite`_ |
| 30 | #. Testify toolkit's ``suite.Run(t *testing.T, suite TestingSuite)`` function runs the suite and does the following: |
| 31 | |
| 32 | #. Suite is initialized. The topology is loaded and configured in this step |
| 33 | #. Test suite runs all the tests attached to it |
| 34 | #. Execute tear-down functions, which currently consists of stopping running containers |
| 35 | and clean-up of test topology |
| 36 | |
| 37 | Adding a test case |
| 38 | ------------------ |
| 39 | |
| 40 | This describes adding a new test case to an existing suite. |
| 41 | For adding a new suite, please see `Modifying the framework`_ below. |
| 42 | |
| 43 | #. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists |
| 44 | #. Declare method whose name starts with ``Test`` and specifies its receiver as a pointer to the suite's struct (defined in ``framework_test.go``) |
| 45 | #. Implement test behaviour inside the test method. This typically includes the following: |
| 46 | |
| 47 | #. Start docker container(s) as needed. Function ``dockerRun(instance, args string)`` |
| 48 | from ``utils.go`` serves this purpose. Alternatively use suite struct's ``NewContainer(name string)`` method |
| 49 | #. Execute *hs-test* action(s) inside any of the running containers. |
| 50 | Function ``hstExec`` from ``utils.go`` does this by using ``docker exec`` command to run ``hs-test`` executable. |
| 51 | For starting an VPP instance inside a container, the ``Vpp`` struct can be used as a forward-looking alternative |
| 52 | #. Run arbitrary commands inside the containers with ``dockerExec(cmd string, instance string)`` |
| 53 | #. Run other external tool with one of the preexisting functions in the ``utils.go`` file. |
| 54 | For example, use ``wget`` with ``startWget(..)`` function |
| 55 | #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else |
| 56 | #. ``defer func() { exechelper.Run("docker stop <container-name>) }()`` inside the method body, |
| 57 | to stop the running container(s). It's not necessary to do this if containers were created |
| 58 | with suite's ``NewContainer(..)`` method |
| 59 | |
| 60 | **Example test case** |
| 61 | |
| 62 | Two docker containers, each with its own VPP instance running. One VPP then pings the other. |
| 63 | This can be put in file ``extras/hs-test/my_test.go`` and run with command ``./test -run TestMySuite``. |
| 64 | |
| 65 | :: |
| 66 | |
| 67 | package main |
| 68 | |
| 69 | import ( |
| 70 | "fmt" |
| 71 | "github.com/edwarnicke/exechelper" |
| 72 | ) |
| 73 | |
| 74 | func (s *MySuite) TestMyCase() { |
| 75 | t := s.T() |
| 76 | |
| 77 | vpp1Instance := "vpp-1" |
| 78 | vpp2Instance := "vpp-2" |
| 79 | |
| 80 | err := dockerRun(vpp1Instance, "") |
| 81 | if err != nil { |
| 82 | t.Errorf("%v", err) |
| 83 | return |
| 84 | } |
| 85 | defer func() { exechelper.Run("docker stop " + vpp1Instance) }() |
| 86 | |
| 87 | err = dockerRun(vpp2Instance, "") |
| 88 | if err != nil { |
| 89 | t.Errorf("%v", err) |
| 90 | return |
| 91 | } |
| 92 | defer func() { exechelper.Run("docker stop " + vpp2Instance) }() |
| 93 | |
| 94 | _, err = hstExec("Configure2Veths srv", vpp1Instance) |
| 95 | if err != nil { |
| 96 | t.Errorf("%v", err) |
| 97 | return |
| 98 | } |
| 99 | |
| 100 | _, err = hstExec("Configure2Veths cln", vpp2Instance) |
| 101 | if err != nil { |
| 102 | t.Errorf("%v", err) |
| 103 | return |
| 104 | } |
| 105 | |
| 106 | // ping one VPP from the other |
| 107 | // |
| 108 | // not using dockerExec because it executes in detached mode |
| 109 | // and we want to capture output from ping and show it |
| 110 | command := "docker exec --detach=false vpp-1 vppctl -s /tmp/2veths/var/run/vpp/cli.sock ping 10.10.10.2" |
| 111 | output, err := exechelper.CombinedOutput(command) |
| 112 | if err != nil { |
| 113 | t.Errorf("ping failed: %v", err) |
| 114 | } |
| 115 | fmt.Println(string(output)) |
| 116 | } |
| 117 | |
| 118 | Modifying the framework |
| 119 | ----------------------- |
| 120 | |
| 121 | **Adding a test suite** |
| 122 | |
| 123 | .. _test-convention: |
| 124 | |
| 125 | #. Adding a new suite takes place in ``framework_test.go`` |
| 126 | |
| 127 | #. Make a ``struct`` with at least ``HstSuite`` struct and a ``teardownSuite`` function as its members. |
| 128 | HstSuite provides functionality that can be shared for all suites, like starting containers |
| 129 | |
| 130 | :: |
| 131 | |
| 132 | type MySuite struct { |
| 133 | HstSuite |
| 134 | teardownSuite func() |
| 135 | } |
| 136 | |
| 137 | #. Implement SetupSuite method which testify runs before running the tests. |
| 138 | It's important here to call ``setupSuite(s *suite.Suite, topologyName string)`` and assign its result to the suite's ``teardownSuite`` member. |
| 139 | Pass the topology name to the function in the form of file name of one of the *yaml* files in ``topo`` folder. |
| 140 | Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo/myTopology.yaml`` |
| 141 | |
| 142 | :: |
| 143 | |
| 144 | func (s *MySuite) SetupSuite() { |
| 145 | // Add custom setup code here |
| 146 | |
| 147 | s.teardownSuite = setupSuite(&s.Suite, "myTopology") |
| 148 | } |
| 149 | |
| 150 | #. Implement TearDownSuite method which testify runs after the tests, to clean-up. |
| 151 | It's good idea to add at least the suite's own ``teardownSuite()`` |
| 152 | and HstSuite upper suite's ``stopContainers()`` methods |
| 153 | |
| 154 | :: |
| 155 | |
| 156 | func (s *MySuite) TearDownSuite() { |
| 157 | s.teardownSuite() |
| 158 | s.StopContainers() |
| 159 | } |
| 160 | |
| 161 | #. In order for ``go test`` to run this suite, we need to create a normal test function and pass our suite to ``suite.Run`` |
| 162 | |
| 163 | :: |
| 164 | |
| 165 | func TestMySuite(t *testing.T) { |
| 166 | var m MySuite |
| 167 | suite.Run(t, &m) |
| 168 | } |
| 169 | |
| 170 | #. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above |
| 171 | |
| 172 | **Adding a topology element** |
| 173 | |
| 174 | Topology configuration exists as ``yaml`` files in the ``extras/hs-test/topo`` folder. |
| 175 | Processing of a file for a particular test suite is started by the ``setupSuite`` function depending on which file's name is passed to it. |
| 176 | Specified file is loaded by ``LoadTopology()`` function and converted into internal data structures which represent various elements of the topology. |
| 177 | After parsing the configuration, ``Configure()`` method loops over array of topology elements and configures them one by one. |
| 178 | |
| 179 | These are currently supported types of elements. |
| 180 | |
| 181 | * ``netns`` - network namespace |
| 182 | * ``veth`` - veth network interface, optionally with target network namespace or IPv4 address |
| 183 | * ``bridge`` - ethernet bridge to connect created interfaces, optionally with target network namespace |
| 184 | * ``tap`` - tap network interface with IP address |
| 185 | |
| 186 | Supporting a new type of topology element requires adding code to recognize the new element type during loading. |
| 187 | And adding code to set up the element in the host system with some Linux tool, such as *ip*. This should be implemented in ``netconfig.go``. |
| 188 | |
| 189 | **Communicating between containers** |
| 190 | |
| 191 | When two VPP instances or other applications, each in its own Docker container, |
| 192 | want to communicate there are typically two ways this can be done within *hs-test*. |
| 193 | |
| 194 | * Network interfaces. Containers are being created with ``-d --network host`` options, |
| 195 | so they are connected with interfaces created in host system |
| 196 | * Shared folders. Containers are being created with ``-v`` option to create shared `volumes`_ between host system and containers |
| 197 | or just between containers |
| 198 | |
| 199 | **Adding a hs-test action** |
| 200 | |
| 201 | Executing more complex or long running jobs is made easier by *hs-test* actions. |
| 202 | These are functions that compartmentalize configuration and execution together for a specific task. |
| 203 | For example, starting up VPP or running VCL echo client. |
| 204 | |
| 205 | The actions are located in ``extras/hs-test/actions.go``. To add one, create a new method that has its receiver as a pointer to ``Actions`` struct. |
| 206 | |
| 207 | Run it from test case with ``hstExec(args, instance)`` where ``args`` is the action method's name and ``instance`` is target Docker container's name. |
| 208 | This then executes the ``hs-test`` binary inside of the container and it then runs selected action. |
| 209 | Action is specified by its name as first argument for the binary. |
| 210 | |
| 211 | *Note*: When ``hstExec(..)`` runs some action from a test case, the execution of ``hs-test`` inside the container |
| 212 | is asynchronous. The action might take many seconds to finish, while the test case execution context continues to run. |
| 213 | To mitigate this, ``hstExec(..)`` waits pre-defined arbitrary number of seconds for a *sync file* to be written by ``hs-test`` |
| 214 | at the end of its run. The test case context and container use Docker volume to share the file. |
| 215 | |
| 216 | **Adding an external tool** |
| 217 | |
| 218 | 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. |
| 219 | These types of functions are placed in the ``utils.go`` file. If the external program is not available by default in Docker image, |
| 220 | add its installation to ``extras/hs-test/Dockerfile.vpp`` in ``apt-get install`` command. |
| 221 | Alternatively copy the executable from host system to the Docker image, similarly how the VPP executables and libraries are being copied. |
| 222 | |
| 223 | **Eternal dependencies** |
| 224 | |
| 225 | * Linux tools ``ip``, ``brctl`` |
| 226 | * Standalone programs ``wget``, ``iperf3`` - since these are downloaded when Docker image is made, |
| 227 | they are reasonably up-to-date automatically |
| 228 | * Programs in Docker images - see ``envoyproxy/envoy-contrib`` in ``utils.go`` |
| 229 | * ``http_server`` - homegrown application that listens on specified address and sends a test file in response |
| 230 | * Non-standard Go libraries - see ``extras/hs-test/go.mod`` |
| 231 | |
| 232 | Generally, these will be updated on a per-need basis, for example when a bug is discovered |
| 233 | or a new version incompatibility issue occurs. |
| 234 | |
| 235 | |
| 236 | .. _testing: https://pkg.go.dev/testing |
| 237 | .. _go test: https://pkg.go.dev/cmd/go#hdr-Test_packages |
| 238 | .. _test suite: https://github.com/stretchr/testify#suite-package |
| 239 | .. _volumes: https://docs.docker.com/storage/volumes/ |
| 240 | |