e2e-tests (3/4): Add gNMI integration tests with envtest#409
Conversation
78654a7 to
d200e98
Compare
envtest mode for faster in-process e2e testing
82d714b to
829e761
Compare
d200e98 to
823e208
Compare
823e208 to
f735c68
Compare
be16d2f to
e20ae93
Compare
51d400f to
ace1a56
Compare
8b82070 to
e0403cc
Compare
ace1a56 to
bce5bb0
Compare
bce5bb0 to
2cf82d6
Compare
envtest mode for faster in-process e2e testingenvtest
dd3f643 to
5b100a6
Compare
f81d9b2 to
47fda5a
Compare
268ae9b to
625b160
Compare
e22db40 to
6a91fa7
Compare
This commit introduces envtest-based integration tests for gNMI functionality using the OpenConfig provider. The tests verify the end-to-end workflow from Kubernetes resources to provider gNMI payloads without requiring a full cluster deployment. The test architecture uses a per-process gNMI test server to enable parallel test execution with Ginkgo's --procs flag. Each Ginkgo process creates its own suite-level server in BeforeSuite, shares it across all tests within that process, and cleans it up in AfterSuite. Tests within each process run serially, ensuring proper isolation through per-test namespaces and state clearing between tests. Signed-off-by: Pujol <enric.pujol@sap.com>
…lacks VLAN support) The OpenConfig provider does not yet support VLAN interface configuration, causing the interface_routed_vlan test to fail. Skip this test by renaming the fixture file to .skip extension until VLAN support is implemented. Signed-off-by: Pujol <enric.pujol@sap.com>
Add a dedicated GitHub Actions workflow for gNMI integration tests using the OpenConfig provider. The workflow runs in parallel with other test jobs, executing tests with 4 processes via Ginkgo's --procs flag. Signed-off-by: Pujol <enric.pujol@sap.com>
6a91fa7 to
c06cf19
Compare
Merging this branch will not change overall coverage
Coverage by fileChanged files (no unit tests)
Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code. Changed unit test files
|
| paths-ignore: | ||
| - 'docs/**' | ||
| - '**/*.md' |
There was a problem hiding this comment.
Proposal for a bit more aggressive filtering. This should only trigger e2e on code or dependency changes.
| paths-ignore: | |
| - 'docs/**' | |
| - '**/*.md' | |
| paths: | |
| - '**.go' | |
| - 'hack/**' | |
| - 'config/**' | |
| - 'go.mod' | |
| - 'go.sum' | |
| - 'Makefile' |
| if envProvider == "" { | ||
| It("requires PROVIDER to be set", func() { | ||
| Fail(ProviderEnvVar + " not set. Please set " + ProviderEnvVar + " to one of: " + strings.Join(supportedProviders, ", ")) | ||
| }) | ||
| return | ||
| } |
There was a problem hiding this comment.
This can be rewritten using only gomega matchers:
| if envProvider == "" { | |
| It("requires PROVIDER to be set", func() { | |
| Fail(ProviderEnvVar + " not set. Please set " + ProviderEnvVar + " to one of: " + strings.Join(supportedProviders, ", ")) | |
| }) | |
| return | |
| } | |
| Expect(envProvider).NotTo(BeEmpty(), "PROVIDER environment variable must be set to one of: %s", strings.Join(supportedProviders, ", ")) |
Same for some other if checks in this file
| // For comparison, it unmarshals both into interface{} and uses reflect.DeepEqual | ||
| // after sorting any arrays and removing empty arrays/objects to ignore ordering | ||
| // and cleanup artifacts. | ||
| func CompareJSON(got, want string) error { |
There was a problem hiding this comment.
Have you tried the gomega build-in matcher for JSON? https://onsi.github.io/gomega/#matchjsonjson-any
| # SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and IronCore contributors | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| name: Test gNMI integration |
There was a problem hiding this comment.
| name: Test gNMI integration | |
| name: Test gNMI integration |
why does this file have a two space indentation by default?
| - name: Install ginkgo | ||
| run: go install github.com/onsi/ginkgo/v2/ginkgo@latest |
There was a problem hiding this comment.
Is this install step required if the 'make test-gnmi' already takes care of installing ginkgo if it's not present?
|
|
||
| jobs: | ||
| test-gnmi: | ||
| name: Run gNMI Integration Tests (OpenConfig) |
There was a problem hiding this comment.
| name: Run gNMI Integration Tests (OpenConfig) | |
| name: gNMI Integration (${{ matrix.provider }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| provider: | |
| - openconfig |
Should we already integrate a matrix build, such that it is easier to integrate more providers later?
| - name: Install ginkgo | ||
| run: go install github.com/onsi/ginkgo/v2/ginkgo@latest | ||
| - name: Running gNMI tests | ||
| run: make test-gnmi TESTGNMI_PROVIDER=openconfig GINKGO_PROCS=4 |
There was a problem hiding this comment.
| run: make test-gnmi TESTGNMI_PROVIDER=openconfig GINKGO_PROCS=4 | |
| run: make test-gnmi TESTGNMI_PROVIDER=openconfig GINKGO_PROCS=$(nproc) |
why not let the platform use all available?
| if s.grpcServer != nil { | ||
| s.grpcServer.GracefulStop() | ||
| } |
There was a problem hiding this comment.
| if s.grpcServer != nil { | |
| s.grpcServer.GracefulStop() | |
| } | |
| if s.grpcServer != nil { | |
| const timeout = 5 * time.Second | |
| t := time.AfterFunc(timeout, func() { | |
| log.Printf("Graceful shutdown timed out, forcing stop") | |
| s.grpcServer.Stop() | |
| }) | |
| s.grpcServer.GracefulStop() | |
| t.Stop() | |
| } |
What about this? This completely removes the need to create a done chan and spawing a gorountine in a goroutine. So the go func in the NewTestServer can just become:
go func() {
<-ctx.Done()
server.Close()
}()| // Delete and wait for resources with finalizers - controller needs Device alive to clean up gNMI state | ||
| for _, gvk := range Resources { | ||
| list := &unstructured.UnstructuredList{} | ||
| list.SetGroupVersionKind(schema.GroupVersionKind{ | ||
| Group: gvk.Group, | ||
| Version: gvk.Version, | ||
| Kind: gvk.Kind + "List", | ||
| }) | ||
|
|
||
| if err := c.List(cleanupCtx, list, client.InNamespace(namespace)); err != nil { | ||
| if apimeta.IsNoMatchError(err) { | ||
| continue | ||
| } | ||
| Expect(err).NotTo(HaveOccurred()) | ||
| } | ||
|
|
||
| for _, item := range list.Items { | ||
| Expect(client.IgnoreNotFound(c.Delete(cleanupCtx, &item))).To(Succeed()) | ||
| } | ||
|
|
||
| if len(list.Items) > 0 { | ||
| Eventually(func(g Gomega) { | ||
| var remaining unstructured.UnstructuredList | ||
| remaining.SetGroupVersionKind(list.GroupVersionKind()) | ||
| g.Expect(c.List(cleanupCtx, &remaining, client.InNamespace(namespace))).To(Succeed()) | ||
| g.Expect(remaining.Items).To(BeEmpty()) | ||
| }).WithContext(cleanupCtx).Should(Succeed()) | ||
| } | ||
| } |
There was a problem hiding this comment.
I guess a
c.Delete(cleanupCtx, device, client.PropagationPolicy(metav1.DeletePropagationForeground))
should already work for deleting in the right order.
If that is not the case, then we clearly have some internal errors elsewhere.
| controllerCancel() | ||
| } | ||
|
|
||
| if gnmiServerCancel != nil { |
There was a problem hiding this comment.
I think this is a race, as ggnmiServerCancel()triggers a context cancelation, which means the the goroutine inside ofNewTestServerwill continue to callGracefulStop(), and Close()later also callsGracefulStop(). I believe this is also the reason why sync.Once` was introduced as a quickfix, but I feel like that shows that the implementation is a bit muddy here.
| gnmiServer *testserver.Server | ||
| ) | ||
|
|
||
| // Lifecycle — cancel functions called in AfterSuite |
There was a problem hiding this comment.
Why two separate contexts? Isn't this just one from t.Context()?
| } | ||
|
|
||
| func (s *State) Set(path *gpb.Path, raw []byte) { | ||
| func (s *State) Set(path *gpb.Path, raw []byte) error { |
There was a problem hiding this comment.
| func (s *State) Set(path *gpb.Path, raw []byte) error { | |
| func (s *State) Set(path *gpb.Path, raw []byte) (err error) { |
nit: avoid having multiple var err error later.
| elems := path.GetElem() | ||
| if len(elems) == 0 { | ||
| s.Buf = buf | ||
| return nil | ||
| } | ||
|
|
||
| for k, v := range elems[len(elems)-1].GetKey() { | ||
| buf, err = sjson.SetBytes(buf, sb.String()+"."+k, v) | ||
| if err != nil { | ||
| return fmt.Errorf("sjson.SetBytes final key %s: %w", k, err) | ||
| } | ||
| } |
There was a problem hiding this comment.
| elems := path.GetElem() | |
| if len(elems) == 0 { | |
| s.Buf = buf | |
| return nil | |
| } | |
| for k, v := range elems[len(elems)-1].GetKey() { | |
| buf, err = sjson.SetBytes(buf, sb.String()+"."+k, v) | |
| if err != nil { | |
| return fmt.Errorf("sjson.SetBytes final key %s: %w", k, err) | |
| } | |
| } | |
| if elems := path.GetElem(); len(elems) > 0 { | |
| for k, v := range elems[len(elems)-1].GetKey() { | |
| buf, err = sjson.SetBytes(buf, sb.String()+"."+k, v) | |
| if err != nil { | |
| return fmt.Errorf("sjson.SetBytes final key %s: %w", k, err) | |
| } | |
| } | |
| } |
This PR introduces envtest-based gNMI integration tests for the OpenConfig provider, enabling faster validation of the Kubernetes resources to gNMI sequence without requiring a full cluster deployment.
The test architecture uses a per-process gNMI server design that enables parallel test execution with Ginkgo's --procs flag. Each process creates its own suite-level server in BeforeSuite and shares it across tests running serially within that process.