hs-test: transition to ginkgo test framework

Type: test

Change-Id: Ia38bf5549d20b22876f6082085b69a52a03d0142
Signed-off-by: Adrian Villin <avillin@cisco.com>
Signed-off-by: Matus Fabian <matfabia@cisco.com>
diff --git a/extras/hs-test/Makefile b/extras/hs-test/Makefile
index f0ec755..9c4d345 100644
--- a/extras/hs-test/Makefile
+++ b/extras/hs-test/Makefile
@@ -23,6 +23,10 @@
 CPUS=1
 endif
 
+ifeq ($(PARALLEL),)
+PARALLEL=1
+endif
+
 ifeq ($(VPPSRC),)
 VPPSRC=$(shell pwd)/../..
 endif
@@ -35,8 +39,8 @@
 ARCH=$(shell dpkg --print-architecture)
 endif
 
-list_tests = @(grep -r ') Test' *_test.go | cut -d '*' -f2 | cut -d '(' -f1 | \
-		tr -d ' ' | tr ')' '/' | sed 's/Suite//')
+list_tests = @go run github.com/onsi/ginkgo/v2/ginkgo --dry-run -v --no-color --seed=2 | head -n -1 | grep 'Test' | \
+		sed 's/^/* /; s/\(Suite\) /\1\//g'
 
 .PHONY: help
 help:
@@ -60,6 +64,7 @@
 	@echo " TEST=[test-name]         - specific test to run"
 	@echo " CPUS=[n-cpus]            - number of cpus to run with vpp"
 	@echo " VPPSRC=[path-to-vpp-src] - path to vpp source files (for gdb)"
+	@echo " PARALLEL=[n-cpus]"		 - number of test processes to spawn to run in parallel
 	@echo
 	@echo "List of all tests:"
 	$(call list_tests)
@@ -78,7 +83,7 @@
 test: .deps.ok .build.vpp
 	@bash ./test --persist=$(PERSIST) --verbose=$(VERBOSE) \
 		--unconfigure=$(UNCONFIGURE) --debug=$(DEBUG) --test=$(TEST) --cpus=$(CPUS) \
-		--vppsrc=$(VPPSRC)
+		--vppsrc=$(VPPSRC) --parallel=$(PARALLEL)
 
 build-go:
 	go build ./tools/http_server
diff --git a/extras/hs-test/README.rst b/extras/hs-test/README.rst
index 6db832b..1dc1039 100644
--- a/extras/hs-test/README.rst
+++ b/extras/hs-test/README.rst
@@ -9,7 +9,10 @@
 and to execute external tools or commands. With such requirements the existing VPP test framework is not sufficient.
 For this, ``Go`` was chosen as a high level language, allowing rapid development, with ``Docker`` and ``ip`` being the tools for creating required topology.
 
-Go's package `testing`_ together with `go test`_ command form the base framework upon which the *hs-test* is built and run.
+`Ginkgo`_ forms the base framework upon which the *hs-test* is built and run.
+All tests are technically in a single suite because we are only using ``package main``. We simulate suite behavior by grouping tests by the topology they require.
+This allows us to run those mentioned groups in parallel, but not individual tests in parallel.
+
 
 Anatomy of a test case
 ----------------------
@@ -24,15 +27,16 @@
 **Action flow when running a test case**:
 
 #. It starts with running ``make test``. Optional arguments are VERBOSE, PERSIST (topology configuration isn't cleaned up after test run),
-   and TEST=<test-name> to run specific test.
-#. ``make list-tests`` (or ``make help``) shows all test names.
-#. ``go test`` compiles package ``main`` along with any files with names matching the file pattern ``*_test.go``
-   and then runs the resulting test binaries
-#. The go test framework runs each function matching :ref:`naming convention<test-convention>`. Each of these corresponds to a `test suite`_
-#. Testify toolkit's ``suite.Run(t *testing.T, suite TestingSuite)`` function runs the suite and does the following:
+   TEST=<test-name> to run a specific test and PARALLEL=[n-cpus].
+#. ``make list-tests`` (or ``make help``) shows all tests. The current `list of tests`_ is at the bottom of this document.
+#. ``Ginkgo`` looks for a spec suite in the current directory and then compiles it to a .test binary
+#. The Ginkgo test framework runs each function that was registered manually using ``registerMySuiteTest(s *MySuite)``. Each of these functions correspond to a suite
+#. Ginkgo's ``RunSpecs(t, "Suite description")`` function is the entry point and does the following:
 
+  #. Ginkgo compiles the spec, builds a spec tree
+  #. ``Describe`` container nodes in suite\_\*_test.go files are run (in series by default, or in parallel with the argument PARALLEL=[n-cpus])
   #. Suite is initialized. The topology is loaded and configured in this step
-  #. Test suite runs all the tests attached to it
+  #. Registered tests are run in generated ``It`` subject nodes
   #. Execute tear-down functions, which currently consists of stopping running containers
      and clean-up of test topology
 
@@ -43,23 +47,25 @@
 For adding a new suite, please see `Modifying the framework`_ below.
 
 #. To write a new test case, create a file whose name ends with ``_test.go`` or pick one that already exists
-#. Declare method whose name starts with ``Test`` and specifies its receiver as a pointer to the suite's struct (defined in ``framework_test.go``)
+#. Declare method whose name ends with ``Test`` and specifies its parameter as a pointer to the suite's struct (defined in ``suite_*_test.go``)
 #. Implement test behaviour inside the test method. This typically includes the following:
 
-  #. Retrieve a running container in which to run some action. Method ``getContainerByName``
-     from ``HstSuite`` struct serves this purpose
-  #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``vppctl`` method to access debug CLI
-  #. Run arbitrary commands inside the containers with ``exec`` method
-  #. Run other external tool with one of the preexisting functions in the ``utils.go`` file.
-     For example, use ``wget`` with ``startWget`` function
-  #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else
-  #. Verify results of your tests using ``assert`` methods provided by the test suite,
-     implemented by HstSuite struct
+   #. Retrieve a running container in which to run some action. Method ``getContainerByName``
+      from ``HstSuite`` struct serves this purpose
+   #. Interact with VPP through the ``VppInstance`` struct embedded in container. It provides ``vppctl`` method to access debug CLI
+   #. Run arbitrary commands inside the containers with ``exec`` method
+   #. Run other external tool with one of the preexisting functions in the ``utils.go`` file.
+      For example, use ``wget`` with ``startWget`` function
+   #. Use ``exechelper`` or just plain ``exec`` packages to run whatever else
+   #. Verify results of your tests using ``assert`` methods provided by the test suite, implemented by HstSuite struct or use ``Gomega`` assert functions.
+
+#. Create an ``init()`` function and register the test using ``register*SuiteTests(testCaseFunction)``
+
 
 **Example test case**
 
 Assumed are two docker containers, each with its own VPP instance running. One VPP then pings the other.
-This can be put in file ``extras/hs-test/my_test.go`` and run with command ``./test -run TestMySuite/TestMyCase``.
+This can be put in file ``extras/hs-test/my_test.go`` and run with command ``make test TEST=MyTest`` or ``ginkgo -v --trace --focus MyTest``.
 
 ::
 
@@ -69,7 +75,11 @@
                 "fmt"
         )
 
-        func (s *MySuite) TestMyCase() {
+        func init(){
+                registerMySuiteTest(MyTest)
+        }
+
+        func MyTest(s *MySuite) {
                 clientVpp := s.getContainerByName("client-vpp").vppInstance
 
                 serverVethAddress := s.netInterfaces["server-iface"].AddressString()
@@ -86,8 +96,7 @@
 
 .. _test-convention:
 
-#. Adding a new suite takes place in ``framework_test.go`` and by creating a new file for the suite.
-   Naming convention for the suite files is ``suite_name_test.go`` where *name* will be replaced
+#. To add a new suite, create a new file. Naming convention for the suite files is ``suite_name_test.go`` where *name* will be replaced
    by the actual name
 
 #. Make a ``struct``, in the suite file, with at least ``HstSuite`` struct as its member.
@@ -99,7 +108,17 @@
                         HstSuite
                 }
 
-#. In suite file, implement ``SetupSuite`` method which testify runs once before starting any of the tests.
+#. Create a new slice that will contain test functions with a pointer to the suite's struct: ``var myTests = []func(s *MySuite){}``
+
+#. Then create a new function that will append test functions to that slice:
+
+        ::
+
+                func registerMySuiteTests(tests ...func(s *MySuite)) {
+	                nginxTests = append(myTests, tests...)
+                }
+
+#. In suite file, implement ``SetupSuite`` method which Ginkgo runs once before starting any of the tests.
    It's important here to call ``configureNetworkTopology`` method,
    pass the topology name to the function in a form of file name of one of the *yaml* files in ``topo-network`` folder.
    Without the extension. In this example, *myTopology* corresponds to file ``extras/hs-test/topo-network/myTopology.yaml``
@@ -111,6 +130,8 @@
         ::
 
                 func (s *MySuite) SetupSuite() {
+                        s.HstSuite.SetupSuite()
+
                         // Add custom setup code here
 
                         s.configureNetworkTopology("myTopology")
@@ -123,19 +144,62 @@
         ::
 
                 func (s *MySuite) SetupTest() {
+                        s.HstSuite.setupTest()
                         s.SetupVolumes()
                         s.SetupContainers()
                 }
 
-#. In order for ``go test`` to run this suite, we need to create a normal test function and pass our suite to ``suite.Run``.
-   These functions are placed at the end of ``framework_test.go``
+#. In order for ``Ginkgo`` to run this suite, we need to create a ``Describe`` container node with setup nodes and an ``It`` subject node.
+   Place them at the end of the suite file
+
+   * Declare a suite struct variable before anything else
+   * To use ``BeforeAll()`` and ``AfterAll()``, the container has to be marked as ``Ordered``
+   * Because the container is now marked as Ordered, if a test fails, all the subsequent tests are skipped.
+     To override this behavior, decorate the container node with ``ContinueOnFailure``
 
         ::
 
-                func TestMySuite(t *testing.T) {
-                        var m MySuite
-                        suite.Run(t, &m)
-                }
+                var _ = Describe("MySuite", Ordered, ContinueOnFailure, func() {
+	        var s MySuite
+	        BeforeAll(func() {
+		        s.SetupSuite()
+	        })
+	        BeforeEach(func() {
+		        s.SetupTest()
+	        })
+	        AfterAll(func() {
+		        s.TearDownSuite()
+	        })
+	        AfterEach(func() {
+		        s.TearDownTest()
+        	})
+	        for _, test := range mySuiteTests {
+		        test := test
+		        pc := reflect.ValueOf(test).Pointer()
+		        funcValue := runtime.FuncForPC(pc)
+		        It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			        test(&s)
+		        }, SpecTimeout(time.Minute*5))
+	        }
+                })
+
+#. Notice the loop - it will generate multiple ``It`` nodes, each running a different test.
+   ``test := test`` is necessary, otherwise only the last test in a suite will run.
+   For a more detailed description, check Ginkgo's documentation: https://onsi.github.io/ginkgo/#dynamically-generating-specs\.
+
+#. ``funcValue.Name()`` returns the full name of a function (e.g. ``fd.io/hs-test.MyTest``), however, we only need the test name (``MyTest``).
+
+#. To run certain tests solo, create a new slice that will only contain tests that have to run solo and a new register function.
+   Add a ``Serial`` decorator to the container node and ``Label("SOLO")`` to the ``It`` subject node:
+
+        ::
+
+                var _ = Describe("MySuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+                        ...
+                        It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		        }, SpecTimeout(time.Minute*5))
+                })
 
 #. Next step is to add test cases to the suite. For that, see section `Adding a test case`_ above
 
@@ -186,14 +250,9 @@
 **Skipping tests**
 
 ``HstSuite`` provides several methods that can be called in tests for skipping it conditionally or unconditionally such as:
-``skip()``, ``SkipIfMultiWorker()``, ``SkipUnlessExtendedTestsBuilt()``.
+``skip()``, ``SkipIfMultiWorker()``, ``SkipUnlessExtendedTestsBuilt()``. You can also use Ginkgo's ``Skip()``.
 However the tests currently run under test suites which set up topology and containers before actual test is run. For the reason of saving
-test run time it is not advisable to use aforementioned skip methods and instead prefix test name with ``Skip``:
-
-::
-
-    func (s *MySuite) SkipTest(){
-
+test run time it is not advisable to use aforementioned skip methods and instead, just don't register the test.
 
 **Debugging a test**
 
@@ -201,11 +260,11 @@
 
 ::
 
-    $ make test TEST=TestVeths/TestLDPreloadIperfVpp DEBUG=true
+    $ make test TEST=LDPreloadIperfVppTest DEBUG=true
     ...
     run following command in different terminal:
-    docker exec -it server-vpp gdb -ex "attach $(docker exec server-vpp pidof vpp)"
-    Afterwards press CTRL+C to continue
+    docker exec -it server-vpp2456109 gdb -ex "attach $(docker exec server-vpp2456109 pidof vpp)"
+    Afterwards press CTRL+\ to continue
 
 If a test consists of more VPP instances then this is done for each of them.
 
@@ -223,8 +282,38 @@
 or a new version incompatibility issue occurs.
 
 
-.. _testing: https://pkg.go.dev/testing
-.. _go test: https://pkg.go.dev/cmd/go#hdr-Test_packages
-.. _test suite: https://github.com/stretchr/testify#suite-package
+.. _ginkgo: https://onsi.github.io/ginkgo/
 .. _volumes: https://docs.docker.com/storage/volumes/
 
+**List of tests**
+
+.. _list of tests:
+
+Please update this list whenever you add a new test by pasting the output below.
+
+* NsSuite/HttpTpsTest
+* NsSuite/VppProxyHttpTcpTest
+* NsSuite/VppProxyHttpTlsTest
+* NsSuite/EnvoyProxyHttpTcpTest
+* NginxSuite/MirroringTest
+* VethsSuiteSolo TcpWithLossTest [SOLO]
+* NoTopoSuiteSolo HttpStaticPromTest [SOLO]
+* TapSuite/LinuxIperfTest
+* NoTopoSuite/NginxHttp3Test
+* NoTopoSuite/NginxAsServerTest
+* NoTopoSuite/NginxPerfCpsTest
+* NoTopoSuite/NginxPerfRpsTest
+* NoTopoSuite/NginxPerfWrkTest
+* VethsSuite/EchoBuiltinTest
+* VethsSuite/HttpCliTest
+* VethsSuite/LDPreloadIperfVppTest
+* VethsSuite/VppEchoQuicTest
+* VethsSuite/VppEchoTcpTest
+* VethsSuite/VppEchoUdpTest
+* VethsSuite/XEchoVclClientUdpTest
+* VethsSuite/XEchoVclClientTcpTest
+* VethsSuite/XEchoVclServerUdpTest
+* VethsSuite/XEchoVclServerTcpTest
+* VethsSuite/VclEchoTcpTest
+* VethsSuite/VclEchoUdpTest
+* VethsSuite/VclRetryAttachTest
diff --git a/extras/hs-test/address_allocator.go b/extras/hs-test/address_allocator.go
index 72bc298..e05ea76 100644
--- a/extras/hs-test/address_allocator.go
+++ b/extras/hs-test/address_allocator.go
@@ -12,7 +12,7 @@
 type AddressCounter = int
 
 type Ip4AddressAllocator struct {
-	networks map[int]AddressCounter
+	networks    map[int]AddressCounter
 	chosenOctet int
 	assignedIps []string
 }
@@ -47,7 +47,7 @@
 // Creates a file every time an IP is assigned: used to keep track of addresses in use.
 // If an address is not in use, 'counter' is then copied to 'chosenOctet' and it is used for the remaining tests.
 // Also checks host IP addresses.
-func (a *Ip4AddressAllocator) createIpAddress(networkNumber int, numberOfAddresses int) (string, error){
+func (a *Ip4AddressAllocator) createIpAddress(networkNumber int, numberOfAddresses int) (string, error) {
 	hostIps, _ := exechelper.CombinedOutput("ip a")
 	counter := 10
 	var address string
@@ -56,7 +56,7 @@
 		if a.chosenOctet != 0 {
 			address = fmt.Sprintf("10.%v.%v.%v", a.chosenOctet, networkNumber, numberOfAddresses)
 			file, err := os.Create(address)
-			if err != nil{
+			if err != nil {
 				return "", errors.New("unable to create file: " + fmt.Sprint(err))
 			}
 			file.Close()
@@ -68,14 +68,14 @@
 				counter++
 			} else if os.IsNotExist(err) {
 				file, err := os.Create(address)
-					if err != nil{
+				if err != nil {
 					return "", errors.New("unable to create file: " + fmt.Sprint(err))
-					}
+				}
 				file.Close()
 				a.chosenOctet = counter
 				break
 			} else {
-				return "", errors.New("an error occured while checking if a file exists: " + fmt.Sprint(err))
+				return "", errors.New("an error occurred while checking if a file exists: " + fmt.Sprint(err))
 			}
 		}
 	}
@@ -84,8 +84,8 @@
 	return address, nil
 }
 
-func (a *Ip4AddressAllocator) deleteIpAddresses(){
-	for ip := range a.assignedIps{
+func (a *Ip4AddressAllocator) deleteIpAddresses() {
+	for ip := range a.assignedIps {
 		os.Remove(a.assignedIps[ip])
 	}
 }
diff --git a/extras/hs-test/container.go b/extras/hs-test/container.go
index 87e8aa3..c82f1fc 100644
--- a/extras/hs-test/container.go
+++ b/extras/hs-test/container.go
@@ -9,6 +9,7 @@
 	"time"
 
 	"github.com/edwarnicke/exechelper"
+	. "github.com/onsi/ginkgo/v2"
 )
 
 const (
@@ -38,7 +39,7 @@
 	vppInstance      *VppInstance
 }
 
-func newContainer(suite *HstSuite, yamlInput ContainerConfig, pid string) (*Container, error) {
+func newContainer(suite *HstSuite, yamlInput ContainerConfig) (*Container, error) {
 	containerName := yamlInput["name"].(string)
 	if len(containerName) == 0 {
 		err := fmt.Errorf("container name must not be blank")
@@ -48,7 +49,7 @@
 	var container = new(Container)
 	container.volumes = make(map[string]Volume)
 	container.envVars = make(map[string]string)
-	container.name = containerName + pid
+	container.name = containerName
 	container.suite = suite
 
 	if image, ok := yamlInput["image"]; ok {
@@ -76,7 +77,7 @@
 	}
 
 	if _, ok := yamlInput["volumes"]; ok {
-		workingVolumeDir := logDir + container.suite.T().Name() + pid + volumeDir
+		workingVolumeDir := logDir + CurrentSpecReport().LeafNodeText + container.suite.pid + volumeDir
 		workDirReplacer := strings.NewReplacer("$HST_DIR", workDir)
 		volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
 		for _, volu := range yamlInput["volumes"].([]interface{}) {
@@ -249,7 +250,7 @@
 }
 
 func (c *Container) createFile(destFileName string, content string) error {
-	f, err := os.CreateTemp("/tmp", "hst-config" + c.suite.pid)
+	f, err := os.CreateTemp("/tmp", "hst-config"+c.suite.pid)
 	if err != nil {
 		return err
 	}
@@ -273,7 +274,7 @@
 	serverCommand := fmt.Sprintf(command, arguments...)
 	containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
 		" " + c.name + " " + serverCommand
-	c.suite.T().Helper()
+	GinkgoHelper()
 	c.suite.log(containerExecCommand)
 	c.suite.assertNil(exechelper.Run(containerExecCommand))
 }
@@ -282,7 +283,7 @@
 	cliCommand := fmt.Sprintf(command, arguments...)
 	containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
 		" " + c.name + " " + cliCommand
-	c.suite.T().Helper()
+	GinkgoHelper()
 	c.suite.log(containerExecCommand)
 	byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
 	c.suite.assertNil(err, err)
@@ -291,12 +292,12 @@
 
 func (c *Container) getLogDirPath() string {
 	testId := c.suite.getTestId()
-	testName := c.suite.T().Name()
+	testName := CurrentSpecReport().LeafNodeText
 	logDirPath := logDir + testName + "/" + testId + "/"
 
 	cmd := exec.Command("mkdir", "-p", logDirPath)
 	if err := cmd.Run(); err != nil {
-		c.suite.T().Fatalf("mkdir error: %v", err)
+		Fail("mkdir error: " + fmt.Sprint(err))
 	}
 
 	return logDirPath
@@ -313,12 +314,12 @@
 	cmd = exec.Command("docker", "logs", "--details", "-t", c.name)
 	output, err := cmd.CombinedOutput()
 	if err != nil {
-		c.suite.T().Fatalf("fetching logs error: %v", err)
+		Fail("fetching logs error: " + fmt.Sprint(err))
 	}
 
 	f, err := os.Create(testLogFilePath)
 	if err != nil {
-		c.suite.T().Fatalf("file create error: %v", err)
+		Fail("file create error: " + fmt.Sprint(err))
 	}
 	fmt.Fprint(f, string(output))
 	f.Close()
diff --git a/extras/hs-test/cpu.go b/extras/hs-test/cpu.go
index e17bc11..9a034ed 100644
--- a/extras/hs-test/cpu.go
+++ b/extras/hs-test/cpu.go
@@ -36,7 +36,7 @@
 	return &cpuCtx, nil
 }
 
-func (c *CpuAllocatorT) readCpus(fname string) error {
+func (c *CpuAllocatorT) readCpus() error {
 	var first, last int
 	file, err := os.Open(CPU_PATH)
 	if err != nil {
@@ -60,7 +60,7 @@
 func CpuAllocator() (*CpuAllocatorT, error) {
 	if cpuAllocator == nil {
 		cpuAllocator = new(CpuAllocatorT)
-		err := cpuAllocator.readCpus(CPU_PATH)
+		err := cpuAllocator.readCpus()
 		if err != nil {
 			return nil, err
 		}
diff --git a/extras/hs-test/docker/Dockerfile.vpp b/extras/hs-test/docker/Dockerfile.vpp
index 6b05758..a8c9847 100644
--- a/extras/hs-test/docker/Dockerfile.vpp
+++ b/extras/hs-test/docker/Dockerfile.vpp
@@ -16,6 +16,7 @@
    $DIR/unittest_plugin.so \
    $DIR/quic_plugin.so \
    $DIR/http_static_plugin.so \
+   $DIR/ping_plugin.so \
    $DIR/prom_plugin.so \
    $DIR/tlsopenssl_plugin.so \
    /usr/lib/x86_64-linux-gnu/vpp_plugins/
diff --git a/extras/hs-test/echo_test.go b/extras/hs-test/echo_test.go
index 690f6d1..710163c 100644
--- a/extras/hs-test/echo_test.go
+++ b/extras/hs-test/echo_test.go
@@ -1,6 +1,11 @@
 package main
 
-func (s *VethsSuite) TestEchoBuiltin() {
+func init() {
+	registerVethTests(EchoBuiltinTest)
+	registerSoloVethTests(TcpWithLossTest)
+}
+
+func EchoBuiltinTest(s *VethsSuite) {
 	serverVpp := s.getContainerByName("server-vpp").vppInstance
 	serverVeth := s.getInterfaceByName(serverInterfaceName)
 
@@ -16,7 +21,7 @@
 	s.assertNotContains(o, "failed:")
 }
 
-func (s *VethsSuite) TestTcpWithLoss() {
+func TcpWithLossTest(s *VethsSuite) {
 	serverVpp := s.getContainerByName("server-vpp").vppInstance
 
 	serverVeth := s.getInterfaceByName(serverInterfaceName)
@@ -25,9 +30,12 @@
 
 	clientVpp := s.getContainerByName("client-vpp").vppInstance
 
+	// TODO: investigate why this ping was here:
+	// ---------
 	// Ensure that VPP doesn't abort itself with NSIM enabled
 	// Warning: Removing this ping will make the test fail!
-	clientVpp.vppctl("ping %s", serverVeth.ip4AddressString())
+	// clientVpp.vppctl("ping %s", serverVeth.ip4AddressString())
+	// ---------
 
 	// Add loss of packets with Network Delay Simulator
 	clientVpp.vppctl("set nsim poll-main-thread delay 0.01 ms bandwidth 40 gbit" +
diff --git a/extras/hs-test/framework_test.go b/extras/hs-test/framework_test.go
index 84aa570..8773fa2 100644
--- a/extras/hs-test/framework_test.go
+++ b/extras/hs-test/framework_test.go
@@ -3,30 +3,11 @@
 import (
 	"testing"
 
-	"github.com/stretchr/testify/suite"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 )
 
-func TestTapSuite(t *testing.T) {
-	var m TapSuite
-	suite.Run(t, &m)
-}
-
-func TestNs(t *testing.T) {
-	var m NsSuite
-	suite.Run(t, &m)
-}
-
-func TestVeths(t *testing.T) {
-	var m VethsSuite
-	suite.Run(t, &m)
-}
-
-func TestNoTopo(t *testing.T) {
-	var m NoTopoSuite
-	suite.Run(t, &m)
-}
-
-func TestNginx(t *testing.T) {
-	var m NginxSuite
-	suite.Run(t, &m)
+func TestHst(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "HST")
 }
diff --git a/extras/hs-test/go.mod b/extras/hs-test/go.mod
index 00e1213..50d83a4 100644
--- a/extras/hs-test/go.mod
+++ b/extras/hs-test/go.mod
@@ -4,19 +4,28 @@
 
 require (
 	github.com/edwarnicke/exechelper v1.0.3
-	github.com/stretchr/testify v1.8.4
 	go.fd.io/govpp v0.9.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect
+	golang.org/x/net v0.20.0 // indirect
+	golang.org/x/text v0.14.0 // indirect
+	golang.org/x/tools v0.17.0 // indirect
+)
+
+require (
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
+	github.com/onsi/ginkgo/v2 v2.16.0
+	github.com/onsi/gomega v1.30.0
 	github.com/pkg/errors v0.9.1 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/vishvananda/netns v0.0.4 // indirect
 	golang.org/x/sys v0.16.0 // indirect
diff --git a/extras/hs-test/go.sum b/extras/hs-test/go.sum
index df59673..0070725 100644
--- a/extras/hs-test/go.sum
+++ b/extras/hs-test/go.sum
@@ -1,3 +1,6 @@
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -6,10 +9,19 @@
 github.com/edwarnicke/exechelper v1.0.3/go.mod h1:R65OUPKns4bgeHkCmfSHbmqLBU8aHZxTgLmEyUBUk4U=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
+github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
 github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -18,6 +30,8 @@
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
+github.com/onsi/ginkgo/v2 v2.16.0 h1:7q1w9frJDzninhXxjZd+Y/x54XNjG/UlRLIYPZafsPM=
+github.com/onsi/ginkgo/v2 v2.16.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
 github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
 github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -28,22 +42,27 @@
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
 github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
 github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
 go.fd.io/govpp v0.9.0 h1:EHUXhQ+dph2K2An4YMqmd/WBE3Fcqsg97KVmdLJoSoU=
 go.fd.io/govpp v0.9.0/go.mod h1:9QoqjEbvfuuXNfjHS0A7YS+7QQVVaQ9cMioOWpSM4rY=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
 golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
 golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
+golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/extras/hs-test/hst_suite.go b/extras/hs-test/hst_suite.go
index c5c8edb..4c6d5b2 100644
--- a/extras/hs-test/hst_suite.go
+++ b/extras/hs-test/hst_suite.go
@@ -5,14 +5,15 @@
 	"errors"
 	"flag"
 	"fmt"
+	"log/slog"
 	"os"
 	"os/exec"
 	"strings"
 	"time"
 
 	"github.com/edwarnicke/exechelper"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/suite"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
 	"gopkg.in/yaml.v3"
 )
 
@@ -28,7 +29,6 @@
 var vppSourceFileDir = flag.String("vppsrc", "", "vpp source file directory")
 
 type HstSuite struct {
-	suite.Suite
 	containers       map[string]*Container
 	volumes          []string
 	netConfigs       []NetConfig
@@ -38,7 +38,7 @@
 	cpuAllocator     *CpuAllocatorT
 	cpuContexts      []*CpuContext
 	cpuPerVpp        int
-	pid				 string
+	pid              string
 }
 
 func (s *HstSuite) SetupSuite() {
@@ -46,7 +46,7 @@
 	s.pid = fmt.Sprint(os.Getpid())
 	s.cpuAllocator, err = CpuAllocator()
 	if err != nil {
-		s.FailNow("failed to init cpu allocator: %v", err)
+		Fail("failed to init cpu allocator: " + fmt.Sprint(err))
 	}
 	s.cpuPerVpp = *nConfiguredCpus
 }
@@ -85,6 +85,10 @@
 }
 
 func (s *HstSuite) SetupTest() {
+	RegisterFailHandler(func(message string, callerSkip ...int) {
+		s.hstFail()
+		Fail(message, callerSkip...)
+	})
 	s.skipIfUnconfiguring()
 	s.setupVolumes()
 	s.setupContainers()
@@ -106,15 +110,15 @@
 	}
 }
 
-func logVppInstance(container *Container, maxLines int){
-	if container.vppInstance == nil{
+func logVppInstance(container *Container, maxLines int) {
+	if container.vppInstance == nil {
 		return
 	}
 
 	logSource := container.getHostWorkDir() + defaultLogFilePath
 	file, err := os.Open(logSource)
 
-	if err != nil{
+	if err != nil {
 		return
 	}
 	defer file.Close()
@@ -123,7 +127,7 @@
 	var lines []string
 	var counter int
 
-	for scanner.Scan(){
+	for scanner.Scan() {
 		lines = append(lines, scanner.Text())
 		counter++
 		if counter > maxLines {
@@ -133,7 +137,7 @@
 	}
 
 	fmt.Println("vvvvvvvvvvvvvvv " + container.name + " [VPP instance]:")
-	for _, line := range lines{
+	for _, line := range lines {
 		fmt.Println(line)
 	}
 	fmt.Printf("^^^^^^^^^^^^^^^\n\n")
@@ -141,73 +145,56 @@
 
 func (s *HstSuite) hstFail() {
 	fmt.Println("Containers: " + fmt.Sprint(s.containers))
-	for _, container := range s.containers{
+	for _, container := range s.containers {
 		out, err := container.log(20)
-		if err != nil{
+		if err != nil {
 			fmt.Printf("An error occured while obtaining '%s' container logs: %s\n", container.name, fmt.Sprint(err))
 			break
 		}
 		fmt.Printf("\nvvvvvvvvvvvvvvv " +
-					container.name + ":\n" +
-					out +
-					"^^^^^^^^^^^^^^^\n\n")
+			container.name + ":\n" +
+			out +
+			"^^^^^^^^^^^^^^^\n\n")
 		logVppInstance(container, 20)
 	}
-	s.T().FailNow()
 }
 
 func (s *HstSuite) assertNil(object interface{}, msgAndArgs ...interface{}) {
-	if !assert.Nil(s.T(), object, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(object).To(BeNil(), msgAndArgs...)
 }
 
 func (s *HstSuite) assertNotNil(object interface{}, msgAndArgs ...interface{}) {
-	if !assert.NotNil(s.T(), object, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(object).ToNot(BeNil(), msgAndArgs...)
 }
 
 func (s *HstSuite) assertEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
-	if !assert.Equal(s.T(), expected, actual, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(actual).To(Equal(expected), msgAndArgs...)
 }
 
 func (s *HstSuite) assertNotEqual(expected, actual interface{}, msgAndArgs ...interface{}) {
-	if !assert.NotEqual(s.T(), expected, actual, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(actual).ToNot(Equal(expected), msgAndArgs...)
 }
 
 func (s *HstSuite) assertContains(testString, contains interface{}, msgAndArgs ...interface{}) {
-	if !assert.Contains(s.T(), testString, contains, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(testString).To(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
 }
 
 func (s *HstSuite) assertNotContains(testString, contains interface{}, msgAndArgs ...interface{}) {
-	if !assert.NotContains(s.T(), testString, contains, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(testString).ToNot(ContainSubstring(fmt.Sprint(contains)), msgAndArgs...)
 }
 
 func (s *HstSuite) assertNotEmpty(object interface{}, msgAndArgs ...interface{}) {
-	if !assert.NotEmpty(s.T(), object, msgAndArgs...) {
-		s.hstFail()
-	}
+	Expect(object).ToNot(BeEmpty(), msgAndArgs...)
 }
 
-func (s *HstSuite) log(args ...any) {
+func (s *HstSuite) log(arg any) {
 	if *isVerbose {
-		s.T().Helper()
-		s.T().Log(args...)
+		slog.Info(fmt.Sprint(arg))
 	}
 }
 
-func (s *HstSuite) skip(args ...any) {
-	s.log(args...)
-	s.T().SkipNow()
+func (s *HstSuite) skip(args string) {
+	Skip(args)
 }
 
 func (s *HstSuite) SkipIfMultiWorker(args ...any) {
@@ -249,11 +236,11 @@
 }
 
 func (s *HstSuite) getInterfaceByName(name string) *NetInterface {
-	return s.netInterfaces[name + s.pid]
+	return s.netInterfaces[name+s.pid]
 }
 
 func (s *HstSuite) getContainerByName(name string) *Container {
-	return s.containers[name + s.pid]
+	return s.containers[name+s.pid]
 }
 
 /*
@@ -261,25 +248,25 @@
  * are not able to modify the original container and affect other tests by doing that
  */
 func (s *HstSuite) getTransientContainerByName(name string) *Container {
-	containerCopy := *s.containers[name + s.pid]
+	containerCopy := *s.containers[name+s.pid]
 	return &containerCopy
 }
 
 func (s *HstSuite) loadContainerTopology(topologyName string) {
 	data, err := os.ReadFile(containerTopologyDir + topologyName + ".yaml")
 	if err != nil {
-		s.T().Fatalf("read error: %v", err)
+		Fail("read error: " + fmt.Sprint(err))
 	}
 	var yamlTopo YamlTopology
 	err = yaml.Unmarshal(data, &yamlTopo)
 	if err != nil {
-		s.T().Fatalf("unmarshal error: %v", err)
+		Fail("unmarshal error: " + fmt.Sprint(err))
 	}
 
 	for _, elem := range yamlTopo.Volumes {
 		volumeMap := elem["volume"].(VolumeConfig)
 		hostDir := volumeMap["host-dir"].(string)
-		workingVolumeDir := logDir + s.T().Name() + s.pid + volumeDir
+		workingVolumeDir := logDir + CurrentSpecReport().LeafNodeText + s.pid + volumeDir
 		volDirReplacer := strings.NewReplacer("$HST_VOLUME_DIR", workingVolumeDir)
 		hostDir = volDirReplacer.Replace(hostDir)
 		s.volumes = append(s.volumes, hostDir)
@@ -287,10 +274,11 @@
 
 	s.containers = make(map[string]*Container)
 	for _, elem := range yamlTopo.Containers {
-		newContainer, err := newContainer(s, elem, s.pid)
+		newContainer, err := newContainer(s, elem)
 		newContainer.suite = s
+		newContainer.name += newContainer.suite.pid
 		if err != nil {
-			s.T().Fatalf("container config error: %v", err)
+			Fail("container config error: " + fmt.Sprint(err))
 		}
 		s.containers[newContainer.name] = newContainer
 	}
@@ -299,12 +287,12 @@
 func (s *HstSuite) loadNetworkTopology(topologyName string) {
 	data, err := os.ReadFile(networkTopologyDir + topologyName + ".yaml")
 	if err != nil {
-		s.T().Fatalf("read error: %v", err)
+		Fail("read error: " + fmt.Sprint(err))
 	}
 	var yamlTopo YamlTopology
 	err = yaml.Unmarshal(data, &yamlTopo)
 	if err != nil {
-		s.T().Fatalf("unmarshal error: %v", err)
+		Fail("unmarshal error: " + fmt.Sprint(err))
 	}
 
 	s.ip4AddrAllocator = NewIp4AddressAllocator()
@@ -316,10 +304,10 @@
 		}
 
 		if peer, ok := elem["peer"].(NetDevConfig); ok {
-			if peer["name"].(string) != ""{
+			if peer["name"].(string) != "" {
 				peer["name"] = peer["name"].(string) + s.pid
 			}
-			if _, ok := peer["netns"]; ok{
+			if _, ok := peer["netns"]; ok {
 				peer["netns"] = peer["netns"].(string) + s.pid
 			}
 		}
@@ -341,7 +329,7 @@
 				if namespace, err := newNetNamespace(elem); err == nil {
 					s.netConfigs = append(s.netConfigs, &namespace)
 				} else {
-					s.T().Fatalf("network config error: %v", err)
+					Fail("network config error: " + fmt.Sprint(err))
 				}
 			}
 		case Veth, Tap:
@@ -350,7 +338,7 @@
 					s.netConfigs = append(s.netConfigs, netIf)
 					s.netInterfaces[netIf.Name()] = netIf
 				} else {
-					s.T().Fatalf("network config error: %v", err)
+					Fail("network config error: " + fmt.Sprint(err))
 				}
 			}
 		case Bridge:
@@ -358,7 +346,7 @@
 				if bridge, err := newBridge(elem); err == nil {
 					s.netConfigs = append(s.netConfigs, &bridge)
 				} else {
-					s.T().Fatalf("network config error: %v", err)
+					Fail("network config error: " + fmt.Sprint(err))
 				}
 			}
 		}
@@ -374,7 +362,7 @@
 
 	for _, nc := range s.netConfigs {
 		if err := nc.configure(); err != nil {
-			s.T().Fatalf("network config error: %v", err)
+			Fail("Network config error: " + fmt.Sprint(err))
 		}
 	}
 }
@@ -389,7 +377,7 @@
 }
 
 func (s *HstSuite) getTestId() string {
-	testName := s.T().Name()
+	testName := CurrentSpecReport().LeafNodeText
 
 	if s.testIds == nil {
 		s.testIds = map[string]string{}
diff --git a/extras/hs-test/http_test.go b/extras/hs-test/http_test.go
index 943c8a5..4277d43 100644
--- a/extras/hs-test/http_test.go
+++ b/extras/hs-test/http_test.go
@@ -4,9 +4,20 @@
 	"fmt"
 	"os"
 	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
 )
 
-func (s *NsSuite) TestHttpTps() {
+func init() {
+	registerNsTests(HttpTpsTest)
+	registerVethTests(HttpCliTest)
+	registerNoTopoTests(NginxHttp3Test, NginxAsServerTest,
+		NginxPerfCpsTest, NginxPerfRpsTest, NginxPerfWrkTest)
+	registerNoTopoSoloTests(HttpStaticPromTest)
+}
+
+func HttpTpsTest(s *NsSuite) {
 	iface := s.getInterfaceByName(clientInterface)
 	client_ip := iface.ip4AddressString()
 	port := "8080"
@@ -18,13 +29,16 @@
 	// configure vpp in the container
 	container.vppInstance.vppctl("http tps uri tcp://0.0.0.0/8080")
 
-	go s.startWget(finished, client_ip, port, "test_file_10M", clientNetns)
+	go func() {
+		defer GinkgoRecover()
+		s.startWget(finished, client_ip, port, "test_file_10M", clientNetns)
+	}()
 	// wait for client
 	err := <-finished
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 }
 
-func (s *VethsSuite) TestHttpCli() {
+func HttpCliTest(s *VethsSuite) {
 	serverContainer := s.getContainerByName("server-vpp")
 	clientContainer := s.getContainerByName("client-vpp")
 
@@ -41,7 +55,7 @@
 	s.assertContains(o, "<html>", "<html> not found in the result!")
 }
 
-func (s *NoTopoSuite) TestNginxHttp3() {
+func NginxHttp3Test(s *NoTopoSuite) {
 	s.SkipUnlessExtendedTestsBuilt()
 
 	query := "index.html"
@@ -57,23 +71,27 @@
 	args := fmt.Sprintf("curl --noproxy '*' --local-port 55444 --http3-only -k https://%s:8443/%s", serverAddress, query)
 	curlCont.extraRunningArgs = args
 	o, err := curlCont.combinedOutput()
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.assertContains(o, "<http>", "<http> not found in the result!")
 }
 
-func (s *NoTopoSuite) TestHttpStaticProm() {
+func HttpStaticPromTest(s *NoTopoSuite) {
 	finished := make(chan error, 1)
 	query := "stats.prom"
 	vpp := s.getContainerByName("vpp").vppInstance
 	serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
 	s.log(vpp.vppctl("http static server uri tcp://" + serverAddress + "/80 url-handlers"))
 	s.log(vpp.vppctl("prom enable"))
-	go s.startWget(finished, serverAddress, "80", query, "")
+	time.Sleep(time.Second * 5)
+	go func() {
+		defer GinkgoRecover()
+		s.startWget(finished, serverAddress, "80", query, "")
+	}()
 	err := <-finished
-	s.assertNil(err, err)
+	s.assertNil(err)
 }
 
-func (s *NoTopoSuite) TestNginxAsServer() {
+func NginxAsServerTest(s *NoTopoSuite) {
 	query := "return_ok"
 	finished := make(chan error, 1)
 
@@ -86,7 +104,10 @@
 	serverAddress := s.getInterfaceByName(tapInterfaceName).peer.ip4AddressString()
 
 	defer func() { os.Remove(query) }()
-	go s.startWget(finished, serverAddress, "80", query, "")
+	go func() {
+		defer GinkgoRecover()
+		s.startWget(finished, serverAddress, "80", query, "")
+	}()
 	s.assertNil(<-finished)
 }
 
@@ -124,9 +145,11 @@
 		args += " -r"
 		args += " http://" + serverAddress + ":80/64B.json"
 		abCont.extraRunningArgs = args
+		time.Sleep(time.Second * 10)
 		o, err := abCont.combinedOutput()
 		rps := parseString(o, "Requests per second:")
-		s.log(rps, err)
+		s.log(rps)
+		s.log(err)
 		s.assertNil(err, "err: '%s', output: '%s'", err, o)
 	} else {
 		wrkCont := s.getContainerByName("wrk")
@@ -135,20 +158,21 @@
 		wrkCont.extraRunningArgs = args
 		o, err := wrkCont.combinedOutput()
 		rps := parseString(o, "requests")
-		s.log(rps, err)
+		s.log(rps)
+		s.log(err)
 		s.assertNil(err, "err: '%s', output: '%s'", err, o)
 	}
 	return nil
 }
 
-func (s *NoTopoSuite) TestNginxPerfCps() {
+func NginxPerfCpsTest(s *NoTopoSuite) {
 	s.assertNil(runNginxPerf(s, "cps", "ab"))
 }
 
-func (s *NoTopoSuite) TestNginxPerfRps() {
+func NginxPerfRpsTest(s *NoTopoSuite) {
 	s.assertNil(runNginxPerf(s, "rps", "ab"))
 }
 
-func (s *NoTopoSuite) TestNginxPerfWrk() {
+func NginxPerfWrkTest(s *NoTopoSuite) {
 	s.assertNil(runNginxPerf(s, "", "wrk"))
 }
diff --git a/extras/hs-test/ldp_test.go b/extras/hs-test/ldp_test.go
index 8d9168d..24d2de3 100644
--- a/extras/hs-test/ldp_test.go
+++ b/extras/hs-test/ldp_test.go
@@ -3,9 +3,15 @@
 import (
 	"fmt"
 	"os"
+
+	. "github.com/onsi/ginkgo/v2"
 )
 
-func (s *VethsSuite) TestLDPreloadIperfVpp() {
+func init() {
+	registerVethTests(LDPreloadIperfVppTest)
+}
+
+func LDPreloadIperfVppTest(s *VethsSuite) {
 	var clnVclConf, srvVclConf Stanza
 
 	serverContainer := s.getContainerByName("server-vpp")
@@ -14,10 +20,7 @@
 	clientContainer := s.getContainerByName("client-vpp")
 	clientVclFileName := clientContainer.getHostWorkDir() + "/vcl_cln.conf"
 
-	ldpreload := os.Getenv("HST_LDPRELOAD")
-	s.assertNotEqual("", ldpreload)
-
-	ldpreload = "LD_PRELOAD=" + ldpreload
+	ldpreload := "LD_PRELOAD=../../build-root/build-vpp-native/vpp/lib/x86_64-linux-gnu/libvcl_ldpreload.so"
 
 	stopServerCh := make(chan struct{}, 1)
 	srvCh := make(chan error, 1)
@@ -36,7 +39,7 @@
 		append("use-mq-eventfd").
 		append(clientAppSocketApi).close().
 		saveToFile(clientVclFileName)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 
 	serverAppSocketApi := fmt.Sprintf("app-socket-api %s/var/run/app_ns_sockets/default",
 		serverContainer.getHostWorkDir())
@@ -49,26 +52,32 @@
 		append("use-mq-eventfd").
 		append(serverAppSocketApi).close().
 		saveToFile(serverVclFileName)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 
 	s.log("attaching server to vpp")
 
 	srvEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+serverVclFileName)
-	go s.startServerApp(srvCh, stopServerCh, srvEnv)
+	go func() {
+		defer GinkgoRecover()
+		s.startServerApp(srvCh, stopServerCh, srvEnv)
+	}()
 
 	err = <-srvCh
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 
 	s.log("attaching client to vpp")
 	var clnRes = make(chan string, 1)
 	clnEnv := append(os.Environ(), ldpreload, "VCL_CONFIG="+clientVclFileName)
 	serverVethAddress := s.getInterfaceByName(serverInterfaceName).ip4AddressString()
-	go s.startClientApp(serverVethAddress, clnEnv, clnCh, clnRes)
+	go func() {
+		defer GinkgoRecover()
+		s.startClientApp(serverVethAddress, clnEnv, clnCh, clnRes)
+	}()
 	s.log(<-clnRes)
 
 	// wait for client's result
 	err = <-clnCh
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 
 	// stop server
 	stopServerCh <- struct{}{}
diff --git a/extras/hs-test/linux_iperf_test.go b/extras/hs-test/linux_iperf_test.go
index 06247e4..e323f7f 100644
--- a/extras/hs-test/linux_iperf_test.go
+++ b/extras/hs-test/linux_iperf_test.go
@@ -1,6 +1,16 @@
 package main
 
-func (s *TapSuite) TestLinuxIperf() {
+import (
+	"fmt"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
+func init() {
+	registerTapTests(LinuxIperfTest)
+}
+
+func LinuxIperfTest(s *TapSuite) {
 	clnCh := make(chan error)
 	stopServerCh := make(chan struct{})
 	srvCh := make(chan error, 1)
@@ -9,13 +19,19 @@
 		stopServerCh <- struct{}{}
 	}()
 
-	go s.startServerApp(srvCh, stopServerCh, nil)
+	go func() {
+		defer GinkgoRecover()
+		s.startServerApp(srvCh, stopServerCh, nil)
+	}()
 	err := <-srvCh
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.log("server running")
 
 	ipAddress := s.getInterfaceByName(tapInterfaceName).ip4AddressString()
-	go s.startClientApp(ipAddress, nil, clnCh, clnRes)
+	go func() {
+		defer GinkgoRecover()
+		s.startClientApp(ipAddress, nil, clnCh, clnRes)
+	}()
 	s.log("client running")
 	s.log(<-clnRes)
 	err = <-clnCh
diff --git a/extras/hs-test/mirroring_test.go b/extras/hs-test/mirroring_test.go
index 91f43f4..6c5a860 100644
--- a/extras/hs-test/mirroring_test.go
+++ b/extras/hs-test/mirroring_test.go
@@ -4,7 +4,11 @@
 	"github.com/edwarnicke/exechelper"
 )
 
-func (s *NginxSuite) TestMirroring() {
+func init() {
+	registerNginxTests(MirroringTest)
+}
+
+func MirroringTest(s *NginxSuite) {
 	proxyAddress := s.getInterfaceByName(mirroringClientInterfaceName).peer.ip4AddressString()
 
 	path := "/64B.json"
diff --git a/extras/hs-test/proxy_test.go b/extras/hs-test/proxy_test.go
index c2f9b6f..ac5f94c 100644
--- a/extras/hs-test/proxy_test.go
+++ b/extras/hs-test/proxy_test.go
@@ -5,8 +5,13 @@
 	"os"
 
 	"github.com/edwarnicke/exechelper"
+	. "github.com/onsi/ginkgo/v2"
 )
 
+func init() {
+	registerNsTests(VppProxyHttpTcpTest, VppProxyHttpTlsTest, EnvoyProxyHttpTcpTest)
+}
+
 func testProxyHttpTcp(s *NsSuite, proto string) error {
 	var outputFile string = "test" + s.pid + ".data"
 	var srcFilePid string = "httpTestFile" + s.pid
@@ -19,12 +24,15 @@
 
 	// create test file
 	err := exechelper.Run(fmt.Sprintf("ip netns exec %s truncate -s %s %s", serverNetns, fileSize, srcFilePid))
-	s.assertNil(err, "failed to run truncate command: " + fmt.Sprint(err))
+	s.assertNil(err, "failed to run truncate command: "+fmt.Sprint(err))
 	defer func() { os.Remove(srcFilePid) }()
 
 	s.log("test file created...")
 
-	go s.startHttpServer(serverRunning, stopServer, ":666", serverNetns)
+	go func() {
+		defer GinkgoRecover()
+		s.startHttpServer(serverRunning, stopServer, ":666", serverNetns)
+	}()
 	// TODO better error handling and recovery
 	<-serverRunning
 
@@ -64,21 +72,21 @@
 		clientVeth.ip4AddressString(),
 		serverVeth.peer.ip4AddressString(),
 	)
-	s.log("proxy configured...", output)
+	s.log("proxy configured: " + output)
 }
 
-func (s *NsSuite) TestVppProxyHttpTcp() {
+func VppProxyHttpTcpTest(s *NsSuite) {
 	proto := "tcp"
 	configureVppProxy(s, proto)
 	err := testProxyHttpTcp(s, proto)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 }
 
-func (s *NsSuite) TestVppProxyHttpTls() {
+func VppProxyHttpTlsTest(s *NsSuite) {
 	proto := "tls"
 	configureVppProxy(s, proto)
 	err := testProxyHttpTcp(s, proto)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 }
 
 func configureEnvoyProxy(s *NsSuite) {
@@ -100,8 +108,8 @@
 	s.assertNil(envoyContainer.start())
 }
 
-func (s *NsSuite) TestEnvoyProxyHttpTcp() {
+func EnvoyProxyHttpTcpTest(s *NsSuite) {
 	configureEnvoyProxy(s)
 	err := testProxyHttpTcp(s, "tcp")
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 }
diff --git a/extras/hs-test/raw_session_test.go b/extras/hs-test/raw_session_test.go
index 670ed58..cf74c62 100644
--- a/extras/hs-test/raw_session_test.go
+++ b/extras/hs-test/raw_session_test.go
@@ -1,15 +1,20 @@
 package main
 
-func (s *VethsSuite) TestVppEchoQuic() {
+func init() {
+	registerVethTests(VppEchoQuicTest, VppEchoTcpTest, VppEchoUdpTest)
+}
+
+func VppEchoQuicTest(s *VethsSuite) {
 	s.testVppEcho("quic")
 }
 
 // udp echo currently broken in vpp, skipping
-func (s *VethsSuite) SkipTestVppEchoUdp() {
+func VppEchoUdpTest(s *VethsSuite) {
+	s.skip("Broken")
 	s.testVppEcho("udp")
 }
 
-func (s *VethsSuite) TestVppEchoTcp() {
+func VppEchoTcpTest(s *VethsSuite) {
 	s.testVppEcho("tcp")
 }
 
diff --git a/extras/hs-test/suite_nginx_test.go b/extras/hs-test/suite_nginx_test.go
index 8f40590..2d1caf1 100644
--- a/extras/hs-test/suite_nginx_test.go
+++ b/extras/hs-test/suite_nginx_test.go
@@ -1,18 +1,37 @@
 package main
 
+import (
+	"reflect"
+	"runtime"
+	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
 // These correspond to names used in yaml config
 const (
-	vppProxyContainerName		 = "vpp-proxy"
-	nginxProxyContainerName 	 = "nginx-proxy"
-	nginxServerContainerName 	 = "nginx-server"
+	vppProxyContainerName        = "vpp-proxy"
+	nginxProxyContainerName      = "nginx-proxy"
+	nginxServerContainerName     = "nginx-server"
 	mirroringClientInterfaceName = "hstcln"
 	mirroringServerInterfaceName = "hstsrv"
 )
 
+var nginxTests = []func(s *NginxSuite){}
+var nginxSoloTests = []func(s *NginxSuite){}
+
 type NginxSuite struct {
 	HstSuite
 }
 
+func registerNginxTests(tests ...func(s *NginxSuite)) {
+	nginxTests = append(nginxTests, tests...)
+}
+func registerNginxSoloTests(tests ...func(s *NginxSuite)) {
+	nginxSoloTests = append(nginxSoloTests, tests...)
+}
+
 func (s *NginxSuite) SetupSuite() {
 	s.HstSuite.SetupSuite()
 	s.loadNetworkTopology("2taps")
@@ -60,3 +79,51 @@
 
 	proxyVpp.waitForApp("nginx-", 5)
 }
+
+var _ = Describe("NginxSuite", Ordered, ContinueOnFailure, func() {
+	var s NginxSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+	for _, test := range nginxTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
+
+var _ = Describe("NginxSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+	var s NginxSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+	for _, test := range nginxSoloTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
diff --git a/extras/hs-test/suite_no_topo_test.go b/extras/hs-test/suite_no_topo_test.go
index bbf0cfd..6df06c7 100644
--- a/extras/hs-test/suite_no_topo_test.go
+++ b/extras/hs-test/suite_no_topo_test.go
@@ -1,15 +1,34 @@
 package main
 
-const (
-	singleTopoContainerVpp 		= "vpp"
-	singleTopoContainerNginx 	= "nginx"
-	tapInterfaceName			= "htaphost"
+import (
+	"reflect"
+	"runtime"
+	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
 )
 
+const (
+	singleTopoContainerVpp   = "vpp"
+	singleTopoContainerNginx = "nginx"
+	tapInterfaceName         = "htaphost"
+)
+
+var noTopoTests = []func(s *NoTopoSuite){}
+var noTopoSoloTests = []func(s *NoTopoSuite){}
+
 type NoTopoSuite struct {
 	HstSuite
 }
 
+func registerNoTopoTests(tests ...func(s *NoTopoSuite)) {
+	noTopoTests = append(noTopoTests, tests...)
+}
+func registerNoTopoSoloTests(tests ...func(s *NoTopoSuite)) {
+	noTopoSoloTests = append(noTopoSoloTests, tests...)
+}
+
 func (s *NoTopoSuite) SetupSuite() {
 	s.HstSuite.SetupSuite()
 	s.loadNetworkTopology("tap")
@@ -35,3 +54,53 @@
 
 	s.assertNil(vpp.createTap(tapInterface), "failed to create tap interface")
 }
+
+var _ = Describe("NoTopoSuite", Ordered, ContinueOnFailure, func() {
+	var s NoTopoSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range noTopoTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
+
+var _ = Describe("NoTopoSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+	var s NoTopoSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range noTopoSoloTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
diff --git a/extras/hs-test/suite_ns_test.go b/extras/hs-test/suite_ns_test.go
index 46d5bef..86d9b78 100644
--- a/extras/hs-test/suite_ns_test.go
+++ b/extras/hs-test/suite_ns_test.go
@@ -1,15 +1,35 @@
 package main
 
+import (
+	"fmt"
+	"reflect"
+	"runtime"
+	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+)
+
 // These correspond to names used in yaml config
 const (
 	clientInterface = "hclnvpp"
 	serverInterface = "hsrvvpp"
 )
 
+var nsTests = []func(s *NsSuite){}
+var nsSoloTests = []func(s *NsSuite){}
+
 type NsSuite struct {
 	HstSuite
 }
 
+func registerNsTests(tests ...func(s *NsSuite)) {
+	nsTests = append(nsTests, tests...)
+}
+func registerNsSoloTests(tests ...func(s *NsSuite)) {
+	nsSoloTests = append(nsSoloTests, tests...)
+}
+
 func (s *NsSuite) SetupSuite() {
 	s.HstSuite.SetupSuite()
 	s.configureNetworkTopology("ns")
@@ -34,12 +54,62 @@
 	s.assertNil(vpp.start())
 
 	idx, err := vpp.createAfPacket(s.getInterfaceByName(serverInterface))
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.assertNotEqual(0, idx)
 
 	idx, err = vpp.createAfPacket(s.getInterfaceByName(clientInterface))
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.assertNotEqual(0, idx)
 
 	container.exec("chmod 777 -R %s", container.getContainerWorkDir())
 }
+
+var _ = Describe("NsSuite", Ordered, ContinueOnFailure, func() {
+	var s NsSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range nsTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
+
+var _ = Describe("NsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+	var s NsSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range nsSoloTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
diff --git a/extras/hs-test/suite_tap_test.go b/extras/hs-test/suite_tap_test.go
index 8b0950a..25c1e25 100644
--- a/extras/hs-test/suite_tap_test.go
+++ b/extras/hs-test/suite_tap_test.go
@@ -1,15 +1,80 @@
 package main
 
 import (
+	"reflect"
+	"runtime"
+	"strings"
 	"time"
+
+	. "github.com/onsi/ginkgo/v2"
 )
 
 type TapSuite struct {
 	HstSuite
 }
 
+var tapTests = []func(s *TapSuite){}
+var tapSoloTests = []func(s *TapSuite){}
+
+func registerTapTests(tests ...func(s *TapSuite)) {
+	tapTests = append(tapTests, tests...)
+}
+func registerTapSoloTests(tests ...func(s *TapSuite)) {
+	tapSoloTests = append(tapSoloTests, tests...)
+}
+
 func (s *TapSuite) SetupSuite() {
 	time.Sleep(1 * time.Second)
 	s.HstSuite.SetupSuite()
 	s.configureNetworkTopology("tap")
 }
+
+var _ = Describe("TapSuite", Ordered, ContinueOnFailure, func() {
+	var s TapSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range tapTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
+
+var _ = Describe("TapSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+	var s TapSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	for _, test := range tapSoloTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
diff --git a/extras/hs-test/suite_veth_test.go b/extras/hs-test/suite_veth_test.go
index 061eee0..f4c3684 100644
--- a/extras/hs-test/suite_veth_test.go
+++ b/extras/hs-test/suite_veth_test.go
@@ -1,7 +1,13 @@
 package main
 
 import (
+	"fmt"
+	"reflect"
+	"runtime"
+	"strings"
 	"time"
+
+	. "github.com/onsi/ginkgo/v2"
 )
 
 // These correspond to names used in yaml config
@@ -10,10 +16,20 @@
 	clientInterfaceName = "cln"
 )
 
+var vethTests = []func(s *VethsSuite){}
+var vethSoloTests = []func(s *VethsSuite){}
+
 type VethsSuite struct {
 	HstSuite
 }
 
+func registerVethTests(tests ...func(s *VethsSuite)) {
+	vethTests = append(vethTests, tests...)
+}
+func registerSoloVethTests(tests ...func(s *VethsSuite)) {
+	vethSoloTests = append(vethSoloTests, tests...)
+}
+
 func (s *VethsSuite) SetupSuite() {
 	time.Sleep(1 * time.Second)
 	s.HstSuite.SetupSuite()
@@ -36,7 +52,7 @@
 
 	cpus := s.AllocateCpus()
 	serverVpp, err := serverContainer.newVppInstance(cpus, sessionConfig)
-	s.assertNotNil(serverVpp, err)
+	s.assertNotNil(serverVpp, fmt.Sprint(err))
 
 	s.setupServerVpp()
 
@@ -45,7 +61,7 @@
 
 	cpus = s.AllocateCpus()
 	clientVpp, err := clientContainer.newVppInstance(cpus, sessionConfig)
-	s.assertNotNil(clientVpp, err)
+	s.assertNotNil(clientVpp, fmt.Sprint(err))
 
 	s.setupClientVpp()
 }
@@ -56,7 +72,7 @@
 
 	serverVeth := s.getInterfaceByName(serverInterfaceName)
 	idx, err := serverVpp.createAfPacket(serverVeth)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.assertNotEqual(0, idx)
 }
 
@@ -66,6 +82,60 @@
 
 	clientVeth := s.getInterfaceByName(clientInterfaceName)
 	idx, err := clientVpp.createAfPacket(clientVeth)
-	s.assertNil(err, err)
+	s.assertNil(err, fmt.Sprint(err))
 	s.assertNotEqual(0, idx)
 }
+
+var _ = Describe("VethsSuite", Ordered, ContinueOnFailure, func() {
+	var s VethsSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	// https://onsi.github.io/ginkgo/#dynamically-generating-specs
+	for _, test := range vethTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
+
+var _ = Describe("VethsSuiteSolo", Ordered, ContinueOnFailure, Serial, func() {
+	var s VethsSuite
+	BeforeAll(func() {
+		s.SetupSuite()
+	})
+	BeforeEach(func() {
+		s.SetupTest()
+	})
+	AfterAll(func() {
+		s.TearDownSuite()
+
+	})
+	AfterEach(func() {
+		s.TearDownTest()
+	})
+
+	// https://onsi.github.io/ginkgo/#dynamically-generating-specs
+	for _, test := range vethSoloTests {
+		test := test
+		pc := reflect.ValueOf(test).Pointer()
+		funcValue := runtime.FuncForPC(pc)
+		It(strings.Split(funcValue.Name(), ".")[2], Label("SOLO"), func(ctx SpecContext) {
+			test(&s)
+		}, SpecTimeout(time.Minute*5))
+	}
+})
diff --git a/extras/hs-test/test b/extras/hs-test/test
index c3b9eae..9b18e1b 100755
--- a/extras/hs-test/test
+++ b/extras/hs-test/test
@@ -8,6 +8,8 @@
 unconfigure_set=0
 debug_set=0
 vppsrc=
+ginkgo_args=
+parallel=
 
 for i in "$@"
 do
@@ -49,8 +51,14 @@
         tc_name="${i#*=}"
         if [ $tc_name != "all" ]; then
             single_test=1
-            args="$args -run $tc_name -verbose"
+            ginkgo_args="$ginkgo_args --focus $tc_name -vv"
+            args="$args -verbose"
+        else
+            ginkgo_args="$ginkgo_args -v"
         fi
+        ;;
+    --parallel=*)
+        ginkgo_args="$ginkgo_args -procs=${i#*=}"
 esac
 done
 
@@ -74,4 +82,4 @@
     exit 1
 fi
 
-sudo -E go test -timeout=20m -buildvcs=false -v $args
+sudo -E go run github.com/onsi/ginkgo/v2/ginkgo --trace $ginkgo_args -- $args
diff --git a/extras/hs-test/vcl_test.go b/extras/hs-test/vcl_test.go
index cb6aaa4..fdcd60a 100644
--- a/extras/hs-test/vcl_test.go
+++ b/extras/hs-test/vcl_test.go
@@ -5,6 +5,11 @@
 	"time"
 )
 
+func init() {
+	registerVethTests(XEchoVclClientUdpTest, XEchoVclClientTcpTest, XEchoVclServerUdpTest,
+		XEchoVclServerTcpTest, VclEchoTcpTest, VclEchoUdpTest, VclRetryAttachTest)
+}
+
 func getVclConfig(c *Container, ns_id_optional ...string) string {
 	var s Stanza
 	ns_id := "default"
@@ -23,11 +28,11 @@
 	return s.close().toString()
 }
 
-func (s *VethsSuite) TestXEchoVclClientUdp() {
+func XEchoVclClientUdpTest(s *VethsSuite) {
 	s.testXEchoVclClient("udp")
 }
 
-func (s *VethsSuite) TestXEchoVclClientTcp() {
+func XEchoVclClientTcpTest(s *VethsSuite) {
 	s.testXEchoVclClient("tcp")
 }
 
@@ -49,11 +54,11 @@
 	s.assertContains(o, "CLIENT RESULTS")
 }
 
-func (s *VethsSuite) TestXEchoVclServerUdp() {
+func XEchoVclServerUdpTest(s *VethsSuite) {
 	s.testXEchoVclServer("udp")
 }
 
-func (s *VethsSuite) TestXEchoVclServerTcp() {
+func XEchoVclServerTcpTest(s *VethsSuite) {
 	s.testXEchoVclServer("tcp")
 }
 
@@ -97,16 +102,15 @@
 	s.log(o)
 }
 
-func (s *VethsSuite) TestVclEchoTcp() {
+func VclEchoTcpTest(s *VethsSuite) {
 	s.testVclEcho("tcp")
 }
 
-func (s *VethsSuite) TestVclEchoUdp() {
+func VclEchoUdpTest(s *VethsSuite) {
 	s.testVclEcho("udp")
 }
 
-// this test takes too long, for now it's being skipped
-func (s *VethsSuite) SkipTestVclRetryAttach() {
+func VclRetryAttachTest(s *VethsSuite) {
 	s.testRetryAttach("tcp")
 }
 
diff --git a/extras/hs-test/vppinstance.go b/extras/hs-test/vppinstance.go
index 1a058c8..d78e6c5 100644
--- a/extras/hs-test/vppinstance.go
+++ b/extras/hs-test/vppinstance.go
@@ -11,6 +11,7 @@
 	"time"
 
 	"github.com/edwarnicke/exechelper"
+	. "github.com/onsi/ginkgo/v2"
 
 	"go.fd.io/govpp"
 	"go.fd.io/govpp/api"
@@ -59,6 +60,7 @@
   plugin http_static_plugin.so { enable }
   plugin prom_plugin.so { enable }
   plugin tlsopenssl_plugin.so { enable }
+  plugin ping_plugin.so { enable }
 }
 
 logging {
@@ -131,9 +133,10 @@
 	vpp.container.createFile(vppcliFileName, cliContent)
 	vpp.container.exec("chmod 0755 " + vppcliFileName)
 
+	vpp.getSuite().log("starting vpp")
 	if *isVppDebug {
 		sig := make(chan os.Signal, 1)
-		signal.Notify(sig, syscall.SIGINT)
+		signal.Notify(sig, syscall.SIGQUIT)
 		cont := make(chan bool, 1)
 		go func() {
 			<-sig
@@ -143,7 +146,7 @@
 		vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
 		fmt.Println("run following command in different terminal:")
 		fmt.Println("docker exec -it " + vpp.container.name + " gdb -ex \"attach $(docker exec " + vpp.container.name + " pidof vpp)\"")
-		fmt.Println("Afterwards press CTRL+C to continue")
+		fmt.Println("Afterwards press CTRL+\\ to continue")
 		<-cont
 		fmt.Println("continuing...")
 	} else {
@@ -151,6 +154,7 @@
 		vpp.container.execServer("su -c \"vpp -c " + startupFileName + " &> /proc/1/fd/1\"")
 	}
 
+	vpp.getSuite().log("connecting to vpp")
 	// Connect to VPP and store the connection
 	sockAddress := vpp.container.getHostWorkDir() + defaultApiSocketFilePath
 	conn, connEv, err := govpp.AsyncConnect(
@@ -207,7 +211,7 @@
 			tokens := strings.Split(strings.TrimSpace(line), " ")
 			val, err := strconv.Atoi(tokens[0])
 			if err != nil {
-				vpp.getSuite().FailNow("failed to parse stat value %s", err)
+				Fail("failed to parse stat value %s" + fmt.Sprint(err))
 				return 0
 			}
 			return val
@@ -217,6 +221,7 @@
 }
 
 func (vpp *VppInstance) waitForApp(appName string, timeout int) {
+	vpp.getSuite().log("waiting for app " + appName)
 	for i := 0; i < timeout; i++ {
 		o := vpp.vppctl("show app")
 		if strings.Contains(o, appName) {
@@ -240,6 +245,7 @@
 	}
 	createReply := &af_packet.AfPacketCreateV2Reply{}
 
+	vpp.getSuite().log("create af-packet interface " + veth.Name())
 	if err := vpp.apiChannel.SendRequest(createReq).ReceiveReply(createReply); err != nil {
 		return 0, err
 	}
@@ -252,6 +258,7 @@
 	}
 	upReply := &interfaces.SwInterfaceSetFlagsReply{}
 
+	vpp.getSuite().log("set af-packet interface " + veth.Name() + " up")
 	if err := vpp.apiChannel.SendRequest(upReq).ReceiveReply(upReply); err != nil {
 		return 0, err
 	}
@@ -273,6 +280,7 @@
 	}
 	addressReply := &interfaces.SwInterfaceAddDelAddressReply{}
 
+	vpp.getSuite().log("af-packet interface " + veth.Name() + " add address " + veth.ip4Address)
 	if err := vpp.apiChannel.SendRequest(addressReq).ReceiveReply(addressReply); err != nil {
 		return 0, err
 	}
@@ -292,6 +300,7 @@
 	}
 	reply := &session.AppNamespaceAddDelV2Reply{}
 
+	vpp.getSuite().log("add app namespace " + namespaceId)
 	if err := vpp.apiChannel.SendRequest(req).ReceiveReply(reply); err != nil {
 		return err
 	}
@@ -301,6 +310,7 @@
 	}
 	sessionReply := &session.SessionEnableDisableReply{}
 
+	vpp.getSuite().log("enable app namespace " + namespaceId)
 	if err := vpp.apiChannel.SendRequest(sessionReq).ReceiveReply(sessionReply); err != nil {
 		return err
 	}
@@ -325,6 +335,7 @@
 	}
 	createTapReply := &tapv2.TapCreateV2Reply{}
 
+	vpp.getSuite().log("create tap interface " + tap.Name())
 	// Create tap interface
 	if err := vpp.apiChannel.SendRequest(createTapReq).ReceiveReply(createTapReply); err != nil {
 		return err
@@ -338,6 +349,7 @@
 	}
 	addAddressReply := &interfaces.SwInterfaceAddDelAddressReply{}
 
+	vpp.getSuite().log("tap interface " + tap.Name() + " add address " + tap.peer.ip4Address)
 	if err := vpp.apiChannel.SendRequest(addAddressReq).ReceiveReply(addAddressReply); err != nil {
 		return err
 	}
@@ -349,6 +361,7 @@
 	}
 	upReply := &interfaces.SwInterfaceSetFlagsReply{}
 
+	vpp.getSuite().log("set tap interface " + tap.Name() + " up")
 	if err := vpp.apiChannel.SendRequest(upReq).ReceiveReply(upReply); err != nil {
 		return err
 	}
@@ -360,7 +373,6 @@
 	logTarget := vpp.container.getLogDirPath() + "vppinstance-" + vpp.container.name + ".log"
 	logSource := vpp.container.getHostWorkDir() + defaultLogFilePath
 	cmd := exec.Command("cp", logSource, logTarget)
-	vpp.getSuite().T().Helper()
 	vpp.getSuite().log(cmd.String())
 	cmd.Run()
 }