Repository: envoyproxy/gateway
Pull request #3503 (2024-05-30 08:48:53)

chore: improve extension server unit tests
When running unit tests that touch the Extension Server, go through the gRPC code and the regular extension manager 
instead of creating a testing-only variant. 

This PR enables using an in-memory gRPC server implementation as the extension server, instead of bypassing the manager code during tests.

This improves coverage and removes testing code from the built binary.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/3503?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
Attention: Patch coverage is `82.60870%` with `4 lines` in your changes are missing coverage. Please review.
> Project coverage is 67.79%. Comparing base [(`6f30397`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/6f30397e23ed06218896b7e5a0a8dad3368775f3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) to head [(`3135997`)](https://app.codecov.io/gh/envoyproxy/gateway/commit/313599730dacd58de001d8a9d919ad4906233bce?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

| [Files](https://app.codecov.io/gh/envoyproxy/gateway/pull/3503?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) | Patch % | Lines |
|---|---|---|
| [internal/extension/registry/extension\_manager.go](https://app.codecov.io/gh/envoyproxy/gateway/pull/3503?src=pr&el=tree&filepath=internal%2Fextension%2Fregistry%2Fextension_manager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy#diff-aW50ZXJuYWwvZXh0ZW5zaW9uL3JlZ2lzdHJ5L2V4dGVuc2lvbl9tYW5hZ2VyLmdv) | 82.60% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/envoyproxy/gateway/pull/3503?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3503      +/-   ##
==========================================
+ Coverage   67.53%   67.79%   +0.25%     
==========================================
  Files         168      166       -2     
  Lines       20154    20039     -115     
==========================================
- Hits        13612    13586      -26     
+ Misses       5558     5464      -94     
- Partials      984      989       +5     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/envoyproxy/gateway/pull/3503?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy).

liorokman: /retest
liorokman: /retest

Patch:
diff --git a/internal/extension/registry/extension_manager.go b/internal/extension/registry/extension_manager.go
index d00e51ce4..568864047 100644
--- a/internal/extension/registry/extension_manager.go
+++ b/internal/extension/registry/extension_manager.go
@@ -10,10 +10,12 @@ import (
 	"crypto/x509"
 	"errors"
 	"fmt"
+	"net"
 
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/test/bufconn"
 	corev1 "k8s.io/api/core/v1"
 	k8scli "sigs.k8s.io/controller-runtime/pkg/client"
 	k8sclicfg "sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -73,6 +75,37 @@ func NewManager(cfg *config.Server) (extTypes.Manager, error) {
 	}, nil
 }
 
+func NewInMemoryManager(cfg v1alpha1.ExtensionManager, server extension.EnvoyGatewayExtensionServer) (extTypes.Manager, func(), error) {
+	if server == nil {
+		return nil, nil, fmt.Errorf("in-memory manager must be passed a server")
+	}
+
+	buffer := 101024 * 1024
+	lis := bufconn.Listen(buffer)
+
+	baseServer := grpc.NewServer()
+	extension.RegisterEnvoyGatewayExtensionServer(baseServer, server)
+	go func() {
+		_ = baseServer.Serve(lis)
+	}()
+	conn, err := grpc.DialContext(context.Background(), "",
+		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
+			return lis.Dial()
+		}), grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		return nil, nil, err
+	}
+	c := func() {
+		lis.Close()
+		baseServer.Stop()
+	}
+
+	return &Manager{
+		extensionConnCache: conn,
+		extension:          cfg,
+	}, c, nil
+}
+
 // HasExtension checks to see whether a given Group and Kind has an
 // associated extension registered for it.
 func (m *Manager) HasExtension(g v1.Group, k v1.Kind) bool {
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 791b7fcbf..58b5e4fca 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -1216,7 +1216,6 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 
 	// Watch Secret CRUDs and process affected EG CRs (Gateway, SecurityPolicy, more in the future).
 	secretPredicates := []predicate.TypedPredicate[*corev1.Secret]{
-		predicate.TypedGenerationChangedPredicate[*corev1.Secret]{},
 		predicate.NewTypedPredicateFuncs(func(s *corev1.Secret) bool {
 			return r.validateSecretForReconcile(s)
 		}),
@@ -1237,7 +1236,6 @@ func (r *gatewayAPIReconciler) watchResources(ctx context.Context, mgr manager.M
 
 	// Watch ConfigMap CRUDs and process affected ClienTraffiPolicies and BackendTLSPolicies.
 	configMapPredicates := []predicate.TypedPredicate[*corev1.ConfigMap]{
-		predicate.TypedGenerationChangedPredicate[*corev1.ConfigMap]{},
 		predicate.NewTypedPredicateFuncs[*corev1.ConfigMap](func(cm *corev1.ConfigMap) bool {
 			return r.validateConfigMapForReconcile(cm)
 		}),


Test patch:
diff --git a/internal/extension/testutils/manager.go b/internal/extension/testutils/manager.go
deleted file mode 100644
index 7185d5873..000000000
--- a/internal/extension/testutils/manager.go
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Envoy Gateway Authors
-// SPDX-License-Identifier: Apache-2.0
-// The full text of the Apache license is available in the LICENSE file at
-// the root of the repo.
-
-package testutils
-
-import (
-	v1 "sigs.k8s.io/gateway-api/apis/v1"
-
-	"github.com/envoyproxy/gateway/api/v1alpha1"
-	extType "github.com/envoyproxy/gateway/internal/extension/types"
-)
-
-var _ extType.Manager = (*Manager)(nil)
-
-type Manager struct {
-	extension v1alpha1.ExtensionManager
-}
-
-func NewManager(ext v1alpha1.ExtensionManager) extType.Manager {
-	return &Manager{
-		extension: ext,
-	}
-}
-
-func (m *Manager) HasExtension(g v1.Group, k v1.Kind) bool {
-	extension := m.extension
-	// TODO: not currently checking the version since extensionRef only supports group and kind.
-	for _, gvk := range extension.Resources {
-		if g == v1.Group(gvk.Group) && k == v1.Kind(gvk.Kind) {
-			return true
-		}
-	}
-	return false
-}
-
-func (m *Manager) GetPreXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) extType.XDSHookClient {
-	if m.extension.Hooks == nil {
-		return nil
-	}
-
-	if m.extension.Hooks.XDSTranslator == nil {
-		return nil
-	}
-
-	for _, hook := range m.extension.Hooks.XDSTranslator.Pre {
-		if xdsHookType == hook {
-			return &XDSHookClient{}
-		}
-	}
-	return nil
-}
-
-func (m *Manager) GetPostXDSHookClient(xdsHookType v1alpha1.XDSTranslatorHook) extType.XDSHookClient {
-	if m.extension.Hooks == nil {
-		return nil
-	}
-
-	if m.extension.Hooks.XDSTranslator == nil {
-		return nil
-	}
-
-	for _, hook := range m.extension.Hooks.XDSTranslator.Post {
-		if xdsHookType == hook {
-			return &XDSHookClient{}
-		}
-	}
-	return nil
-}
diff --git a/internal/gatewayapi/runner/runner_test.go b/internal/gatewayapi/runner/runner_test.go
index 204d4e843..5f3bc2a65 100644
--- a/internal/gatewayapi/runner/runner_test.go
+++ b/internal/gatewayapi/runner/runner_test.go
@@ -19,9 +19,10 @@ import (
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/envoygateway/config"
-	"github.com/envoyproxy/gateway/internal/extension/testutils"
+	"github.com/envoyproxy/gateway/internal/extension/registry"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/message"
+	pb "github.com/envoyproxy/gateway/proto/extension"
 )
 
 func TestRunner(t *testing.T) {
@@ -31,12 +32,15 @@ func TestRunner(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 	// Start
@@ -115,12 +119,15 @@ func TestDeleteStatusKeys(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 
@@ -205,12 +212,15 @@ func TestDeleteAllStatusKeys(t *testing.T) {
 	infraIR := new(message.InfraIR)
 	cfg, err := config.New()
 	require.NoError(t, err)
+	extMgr, closeFunc, err := registry.NewInMemoryManager(egv1a1.ExtensionManager{}, &pb.UnimplementedEnvoyGatewayExtensionServer{})
+	require.NoError(t, err)
+	defer closeFunc()
 	r := New(&Config{
 		Server:            *cfg,
 		ProviderResources: pResources,
 		XdsIR:             xdsIR,
 		InfraIR:           infraIR,
-		ExtensionManager:  testutils.NewManager(egv1a1.ExtensionManager{}),
+		ExtensionManager:  extMgr,
 	})
 	ctx := context.Background()
 
diff --git a/internal/extension/testutils/hooks.go b/internal/xds/translator/extensionserver_test.go
similarity index 60%
rename from internal/extension/testutils/hooks.go
rename to internal/xds/translator/extensionserver_test.go
index b7b38c166..9a65c9954 100644
--- a/internal/extension/testutils/hooks.go
+++ b/internal/xds/translator/extensionserver_test.go
@@ -3,9 +3,10 @@
 // The full text of the Apache license is available in the LICENSE file at
 // the root of the repo.
 
-package testutils
+package translator
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"strings"
@@ -16,24 +17,36 @@ import (
 	listenerV3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
 	routeV3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
 	tlsV3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
-	"github.com/golang/protobuf/proto"
+	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/types/known/durationpb"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	pb "github.com/envoyproxy/gateway/proto/extension"
 )
 
-type XDSHookClient struct{}
+type testingExtensionServer struct {
+	pb.UnimplementedEnvoyGatewayExtensionServer
+}
 
 // PostRouteModifyHook returns a modified version of the route using context info and the passed in extensionResources
-func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames []string, extensionResources []*unstructured.Unstructured) (*routeV3.Route, error) {
+func (t *testingExtensionServer) PostRouteModify(_ context.Context, req *pb.PostRouteModifyRequest) (*pb.PostRouteModifyResponse, error) {
 	// Simulate an error an extension may return
-	if route.Name == "extension-post-xdsroute-hook-error" {
-		return nil, errors.New("route hook resource error")
+	if req.Route.Name == "extension-post-xdsroute-hook-error" {
+		return &pb.PostRouteModifyResponse{
+			Route: req.Route,
+		}, errors.New("route hook resource error")
 	}
 
 	// Setup a new route to avoid operating directly on the passed in pointer for better test coverage that the
 	// route we are returning gets used properly
-	modifiedRoute := proto.Clone(route).(*routeV3.Route)
-	for _, extensionResource := range extensionResources {
+	modifiedRoute := proto.Clone(req.Route).(*routeV3.Route)
+	for _, extensionResourceBytes := range req.PostRouteContext.ExtensionResources {
+		extensionResource := unstructured.Unstructured{}
+		if err := extensionResource.UnmarshalJSON(extensionResourceBytes.UnstructuredBytes); err != nil {
+			return &pb.PostRouteModifyResponse{
+				Route: req.Route,
+			}, err
+		}
 		modifiedRoute.ResponseHeadersToAdd = append(modifiedRoute.ResponseHeadersToAdd,
 			&coreV3.HeaderValueOption{
 				Header: &coreV3.HeaderValue{
@@ -44,7 +57,7 @@ func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames
 			&coreV3.HeaderValueOption{
 				Header: &coreV3.HeaderValue{
 					Key:   "mock-extension-was-here-route-hostnames",
-					Value: strings.Join(routeHostnames, ", "),
+					Value: strings.Join(req.PostRouteContext.Hostnames, ", "),
 				},
 			},
 			&coreV3.HeaderValueOption{
@@ -73,19 +86,23 @@ func (c *XDSHookClient) PostRouteModifyHook(route *routeV3.Route, routeHostnames
 			},
 		)
 	}
-	return modifiedRoute, nil
+	return &pb.PostRouteModifyResponse{
+		Route: modifiedRoute,
+	}, nil
 }
 
 // PostVirtualHostModifyHook returns a modified version of the virtualhost with a new route injected
-func (c *XDSHookClient) PostVirtualHostModifyHook(vh *routeV3.VirtualHost) (*routeV3.VirtualHost, error) {
+func (t *testingExtensionServer) PostVirtualHostModify(_ context.Context, req *pb.PostVirtualHostModifyRequest) (*pb.PostVirtualHostModifyResponse, error) {
 	// Only make the change when the VirtualHost's name matches the expected testdata
 	// This prevents us from having to update every single testfile.out
-	if vh.Name == "extension-post-xdsvirtualhost-hook-error/*" {
-		return nil, fmt.Errorf("extension post xds virtual host hook error")
-	} else if vh.Name == "extension-listener" {
+	if req.VirtualHost.Name == "extension-post-xdsvirtualhost-hook-error/*" {
+		return &pb.PostVirtualHostModifyResponse{
+			VirtualHost: req.VirtualHost,
+		}, fmt.Errorf("extension post xds virtual host hook error")
+	} else if req.VirtualHost.Name == "extension-listener" {
 		// Setup a new VirtualHost to avoid operating directly on the passed in pointer for better test coverage that the
 		// VirtualHost we are returning gets used properly
-		modifiedVH := proto.Clone(vh).(*routeV3.VirtualHost)
+		modifiedVH := proto.Clone(req.VirtualHost).(*routeV3.VirtualHost)
 		modifiedVH.Routes = append(modifiedVH.Routes, &routeV3.Route{
 			Name: "mock-extension-inserted-route",
 			Action: &routeV3.Route_DirectResponse{
@@ -94,31 +111,41 @@ func (c *XDSHookClient) PostVirtualHostModifyHook(vh *routeV3.VirtualHost) (*rou
 				},
 			},
 		})
-		return modifiedVH, nil
+		return &pb.PostVirtualHostModifyResponse{
+			VirtualHost: modifiedVH,
+		}, nil
 	}
-	return vh, nil
+	return &pb.PostVirtualHostModifyResponse{
+		VirtualHost: req.VirtualHost,
+	}, nil
 }
 
 // PostHTTPListenerModifyHook returns a modified version of the listener with a changed statprefix of the listener
 // A more useful use-case for an extension would be looping through the FilterChains to find the
 // HTTPConnectionManager(s) and inject a custom HTTPFilter, but that for testing purposes we don't need to make a complex change
-func (c *XDSHookClient) PostHTTPListenerModifyHook(l *listenerV3.Listener) (*listenerV3.Listener, error) {
+func (t *testingExtensionServer) PostHTTPListenerModify(_ context.Context, req *pb.PostHTTPListenerModifyRequest) (*pb.PostHTTPListenerModifyResponse, error) {
 	// Only make the change when the listener's name matches the expected testdata
 	// This prevents us from having to update every single testfile.out
-	if l.Name == "extension-post-xdslistener-hook-error" {
-		return nil, fmt.Errorf("extension post xds listener hook error")
-	} else if l.Name == "extension-listener" {
+	if req.Listener.Name == "extension-post-xdslistener-hook-error" {
+		return &pb.PostHTTPListenerModifyResponse{
+			Listener: req.Listener,
+		}, fmt.Errorf("extension post xds listener hook error")
+	} else if req.Listener.Name == "extension-listener" {
 		// Setup a new Listener to avoid operating directly on the passed in pointer for better test coverage that the
 		// Listener we are returning gets used properly
-		modifiedListener := proto.Clone(l).(*listenerV3.Listener)
+		modifiedListener := proto.Clone(req.Listener).(*listenerV3.Listener)
 		modifiedListener.StatPrefix = "mock-extension-inserted-prefix"
-		return modifiedListener, nil
+		return &pb.PostHTTPListenerModifyResponse{
+			Listener: modifiedListener,
+		}, nil
 	}
-	return l, nil
+	return &pb.PostHTTPListenerModifyResponse{
+		Listener: req.Listener,
+	}, nil
 }
 
 // PostTranslateModifyHook inserts and overrides some clusters/secrets
-func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, secrets []*tlsV3.Secret) ([]*clusterV3.Cluster, []*tlsV3.Secret, error) {
+func (t *testingExtensionServer) PostTranslateModify(_ context.Context, req *pb.PostTranslateModifyRequest) (*pb.PostTranslateModifyResponse, error) {
 	extensionSvcEndpoint := &endpointV3.LbEndpoint_Endpoint{
 		Endpoint: &endpointV3.Endpoint{
 			Address: &coreV3.Address{
@@ -135,13 +162,18 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	}
 
-	for idx, cluster := range clusters {
+	response := &pb.PostTranslateModifyResponse{
+		Clusters: make([]*clusterV3.Cluster, len(req.Clusters)),
+		Secrets:  make([]*tlsV3.Secret, len(req.Secrets)),
+	}
+	for idx, cluster := range req.Clusters {
+		response.Clusters[idx] = proto.Clone(cluster).(*clusterV3.Cluster)
 		if cluster.Name == "first-route" {
-			clusters[idx].ConnectTimeout = &durationpb.Duration{Seconds: 30}
+			response.Clusters[idx].ConnectTimeout = &durationpb.Duration{Seconds: 30}
 		}
 	}
 
-	clusters = append(clusters, &clusterV3.Cluster{
+	response.Clusters = append(response.Clusters, &clusterV3.Cluster{
 		Name: "mock-extension-injected-cluster",
 		LoadAssignment: &endpointV3.ClusterLoadAssignment{
 			ClusterName: "mock-extension-injected-cluster",
@@ -157,7 +189,10 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	})
 
-	secrets = append(secrets, &tlsV3.Secret{
+	for idx, secret := range req.Secrets {
+		response.Secrets[idx] = proto.Clone(secret).(*tlsV3.Secret)
+	}
+	response.Secrets = append(response.Secrets, &tlsV3.Secret{
 		Name: "mock-extension-injected-secret",
 		Type: &tlsV3.Secret_GenericSecret{
 			GenericSecret: &tlsV3.GenericSecret{
@@ -170,5 +205,5 @@ func (c *XDSHookClient) PostTranslateModifyHook(clusters []*clusterV3.Cluster, s
 		},
 	})
 
-	return clusters, secrets, nil
+	return response, nil
 }
diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go
index 60dcf8636..c77145d5b 100644
--- a/internal/xds/translator/translator_test.go
+++ b/internal/xds/translator/translator_test.go
@@ -23,7 +23,7 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"github.com/envoyproxy/gateway/api/v1alpha1"
-	"github.com/envoyproxy/gateway/internal/extension/testutils"
+	"github.com/envoyproxy/gateway/internal/extension/registry"
 	"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/utils/field"
@@ -199,13 +199,13 @@ func TestTranslateRateLimitConfig(t *testing.T) {
 func TestTranslateXdsWithExtension(t *testing.T) {
 	testConfigs := map[string]testFileConfig{
 		"http-route-extension-route-error": {
-			errMsg: "route hook resource error",
+			errMsg: "rpc error: code = Unknown desc = route hook resource error",
 		},
 		"http-route-extension-virtualhost-error": {
-			errMsg: "extension post xds virtual host hook error",
+			errMsg: "rpc error: code = Unknown desc = extension post xds virtual host hook error",
 		},
 		"http-route-extension-listener-error": {
-			errMsg: "extension post xds listener hook error",
+			errMsg: "rpc error: code = Unknown desc = extension post xds listener hook error",
 		},
 	}
 
@@ -248,7 +248,10 @@ func TestTranslateXdsWithExtension(t *testing.T) {
 					},
 				},
 			}
-			extMgr := testutils.NewManager(ext)
+
+			extMgr, closeFunc, err := registry.NewInMemoryManager(ext, &testingExtensionServer{})
+			require.NoError(t, err)
+			defer closeFunc()
 			tr.ExtensionManager = &extMgr
 
 			tCtx, err := tr.Translate(x)