Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate ES client code from ES output code #16150

Merged
merged 91 commits into from
Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
5cffba8
Basic extraction of ES client code from ES output code
ycombinator Feb 6, 2020
0b1603e
Move test
ycombinator Feb 7, 2020
6d544b9
Removing duplicate function in monitoring reporter
ycombinator Feb 7, 2020
84dff7a
Break import cycle
ycombinator Feb 7, 2020
2f9b881
Guard onConnect callback execution
ycombinator Feb 7, 2020
5c2e535
Replace use of field with getter
ycombinator Feb 7, 2020
805469b
Moving common bulk API response processing into esclientleg
ycombinator Feb 7, 2020
cdb59c1
Moving API integration tests
ycombinator Feb 7, 2020
f0946c2
Fixing references in tests
ycombinator Feb 7, 2020
ce11c70
Adding developer CHANGELOG entry
ycombinator Feb 7, 2020
6c29542
Move LoadJSON method
ycombinator Feb 7, 2020
b75c8c4
Move callbacks to own file
ycombinator Feb 7, 2020
f185aaa
Move client-related constructors into client.go file
ycombinator Feb 7, 2020
d9ea8da
Reducing global logging usage
ycombinator Feb 7, 2020
a9c606b
Use new constructor in test
ycombinator Feb 7, 2020
cc7e93e
Passing logger in test
ycombinator Feb 7, 2020
6dc8b2b
Use logger in test
ycombinator Feb 7, 2020
57864df
Use struct fieldnames when initializing
ycombinator Feb 7, 2020
d7f02af
Use constructor in test
ycombinator Feb 7, 2020
13bbe68
Fixing typos
ycombinator Feb 7, 2020
9969cd0
Replace esclient.ParseProxyURL with generic function in common
ycombinator Feb 15, 2020
8446cd0
Imports formatting
ycombinator Feb 15, 2020
c0693e6
Moving more fields from ES output client to esclientleg.Connection
ycombinator Feb 24, 2020
5ef6784
Moving more fields
ycombinator Feb 24, 2020
254b5e9
Update test code
ycombinator Feb 24, 2020
b4ec366
Use new TLS package
ycombinator Feb 25, 2020
3a85b3a
Extracting common test code into eslegtest package
ycombinator Feb 26, 2020
fe0f221
Replace uses of elasticsearch output client with esclientleg.NewConne…
ycombinator Feb 26, 2020
eec5149
Replacing uses of ES output client struct with esclientleg.Connection
ycombinator Feb 26, 2020
1c55cd2
Handle callbacks
ycombinator Feb 26, 2020
b0d3767
Fixing formatting
ycombinator Feb 26, 2020
16069d7
Fixing import cycle
ycombinator Feb 26, 2020
32c338d
Fixing import and package name
ycombinator Feb 26, 2020
f83d0c1
Fixing imports
ycombinator Feb 26, 2020
b3ded80
More fixes
ycombinator Feb 26, 2020
f4746aa
Breaking import cycle
ycombinator Feb 26, 2020
7b27826
Removing unused function
ycombinator Feb 26, 2020
cc4bf48
Adding back missing statement
ycombinator Feb 26, 2020
1af576b
Fixing param
ycombinator Feb 26, 2020
f55621c
Fixing package name
ycombinator Feb 26, 2020
7b37e1f
Include ES output plugin so it's registered
ycombinator Feb 26, 2020
c37ee06
Proxy handling
ycombinator Feb 26, 2020
6b32d9b
Let Connection handle ProxyDisable setting
ycombinator Feb 26, 2020
2f39d6f
Only parse proxy field from config if set
ycombinator Feb 26, 2020
6acbb8d
Cast timeout ints
ycombinator Feb 26, 2020
32836d5
Parse proxy URL
ycombinator Feb 26, 2020
b6edfc4
Fixing proxy integration test
ycombinator Feb 26, 2020
1e3a37a
Fixing ILM test
ycombinator Feb 27, 2020
bc1d1a7
Updating expected request count in test
ycombinator Feb 27, 2020
144e7c4
Fixing package names
ycombinator Feb 28, 2020
76f2ddb
Lots more refactoring!!!
ycombinator Feb 28, 2020
7be3905
Move timeout field
ycombinator Feb 28, 2020
7f9b52d
More fixes
ycombinator Feb 28, 2020
9a0549e
Adding missing files
ycombinator Feb 28, 2020
f10972e
No need to pass HTTP any more
ycombinator Feb 28, 2020
95f75b0
Simplifying Bulk API usage
ycombinator Feb 28, 2020
9915ab3
Removing unused code
ycombinator Feb 28, 2020
9114f96
Remove bulk state from Connection
ycombinator Feb 28, 2020
c3ee320
Removing empty file
ycombinator Feb 28, 2020
8fbcbce
Moving Bulk API response streaming parsing code back into ES output p…
ycombinator Feb 28, 2020
c0c42e0
Don't make monitoring bulk parsing code use streaming parser
ycombinator Feb 28, 2020
fd4ccbf
Replacing old HTTP struct passing
ycombinator Feb 28, 2020
0b870bd
Removing HTTP use
ycombinator Feb 28, 2020
8f2f34b
Adding build tag
ycombinator Feb 29, 2020
1cfd53b
Fixing up tests
ycombinator Feb 29, 2020
a0144ea
Allow default scheme to be configurable
ycombinator Mar 3, 2020
1139429
Adding versions to import paths
ycombinator Mar 3, 2020
3da06a3
Remove redundant check
ycombinator Mar 4, 2020
82c8c53
Undoing unnecessary heartbeat import change
ycombinator Mar 4, 2020
f899879
Forgot to resolve conflicts
ycombinator Mar 4, 2020
4d2a867
Fixing imports
ycombinator Mar 4, 2020
9443e04
Running go mod tidy
ycombinator Mar 4, 2020
fdd2131
Revert "Remove redundant check"
ycombinator Mar 4, 2020
f5c7ac6
Fixing args order
ycombinator Mar 5, 2020
b0e3dfb
Removing extraneous parameter
ycombinator Mar 5, 2020
29b817c
Removing wrong errors package import
ycombinator Mar 5, 2020
c77351c
Fixing order of arguments
ycombinator Mar 5, 2020
9d3869d
Fixing package name
ycombinator Mar 5, 2020
0c2a8b1
Instantiating logger for tests
ycombinator Mar 5, 2020
f74cc1a
Making streaming JSON parser private to ES output package
ycombinator Mar 5, 2020
0234092
Detect and try to fix scheme before parsing URL
ycombinator Mar 5, 2020
32f0411
Making Connection private to ES output Client
ycombinator Mar 5, 2020
be64ba4
Update test
ycombinator Mar 5, 2020
26c28f2
Replace client.Ping() calls with client.Connect() calls in test code
ycombinator Mar 5, 2020
addcfd4
Updating tests
ycombinator Mar 5, 2020
6788cfd
Removing usage of ES output from monitoring code!
ycombinator Mar 5, 2020
9de9b87
Using strings.Index instead of strings.SplitN
ycombinator Mar 5, 2020
63664e5
Return default config via function call
ycombinator Mar 5, 2020
bfa05b7
Removing "escape hatch" method to expose underlying connection from E…
ycombinator Mar 5, 2020
a61779d
Using client connection in tests
ycombinator Mar 5, 2020
a416cfd
Re-implement Test() method for ES output client
ycombinator Mar 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-developer.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ The list below covers the major changes between 7.0.0-rc2 and master only.
- The `libbeat/outputs/transport` package has been moved to `libbeat/common/transport`. {pull}16734[16734]
- The `libbeat/outputs/tls.go` file has been removed. All exported symbols in that file (`libbeat/outputs.*`) are now available as `libbeat/common/tlscommon.*`. {pull}16734[16734]
- The newly generated Beats are using go modules to manage dependencies. {pull}16288[16288]
- Extract Elasticsearch client logic from `outputs/elasticsearch` package into new `esclientleg` package. {pull}16150[16150]
ycombinator marked this conversation as resolved.
Show resolved Hide resolved

==== Bugfixes

Expand Down
26 changes: 12 additions & 14 deletions filebeat/beater/filebeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,27 @@ import (
"fmt"
"strings"

"github.com/elastic/beats/v7/libbeat/common/reload"

"github.com/pkg/errors"

fbautodiscover "github.com/elastic/beats/v7/filebeat/autodiscover"
"github.com/elastic/beats/v7/filebeat/channel"
cfg "github.com/elastic/beats/v7/filebeat/config"
"github.com/elastic/beats/v7/filebeat/fileset"
_ "github.com/elastic/beats/v7/filebeat/include"
"github.com/elastic/beats/v7/filebeat/input"
"github.com/elastic/beats/v7/filebeat/registrar"
"github.com/elastic/beats/v7/libbeat/autodiscover"
"github.com/elastic/beats/v7/libbeat/beat"
"github.com/elastic/beats/v7/libbeat/cfgfile"
"github.com/elastic/beats/v7/libbeat/common"
"github.com/elastic/beats/v7/libbeat/common/cfgwarn"
"github.com/elastic/beats/v7/libbeat/common/reload"
"github.com/elastic/beats/v7/libbeat/esleg/eslegclient"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/libbeat/management"
"github.com/elastic/beats/v7/libbeat/monitoring"
"github.com/elastic/beats/v7/libbeat/outputs/elasticsearch"

fbautodiscover "github.com/elastic/beats/v7/filebeat/autodiscover"
"github.com/elastic/beats/v7/filebeat/channel"
cfg "github.com/elastic/beats/v7/filebeat/config"
"github.com/elastic/beats/v7/filebeat/fileset"
"github.com/elastic/beats/v7/filebeat/input"
"github.com/elastic/beats/v7/filebeat/registrar"

_ "github.com/elastic/beats/v7/filebeat/include"

// Add filebeat level processors
_ "github.com/elastic/beats/v7/filebeat/processor/add_kubernetes_metadata"
_ "github.com/elastic/beats/v7/libbeat/processors/decode_csv_fields"
Expand Down Expand Up @@ -150,7 +148,7 @@ func (fb *Filebeat) setupPipelineLoaderCallback(b *beat.Beat) error {

overwritePipelines := true
b.OverwritePipelinesCallback = func(esConfig *common.Config) error {
esClient, err := elasticsearch.NewConnectedClient(esConfig)
esClient, err := eslegclient.NewConnectedClient(esConfig)
if err != nil {
return err
}
Expand Down Expand Up @@ -184,7 +182,7 @@ func (fb *Filebeat) loadModulesPipelines(b *beat.Beat) error {

// register pipeline loading to happen every time a new ES connection is
// established
callback := func(esClient *elasticsearch.Client) error {
callback := func(esClient *eslegclient.Connection) error {
return fb.moduleRegistry.LoadPipelines(esClient, overwritePipelines)
}
_, err := elasticsearch.RegisterConnectCallback(callback)
Expand Down Expand Up @@ -358,7 +356,7 @@ func (fb *Filebeat) Stop() {
// Create a new pipeline loader (es client) factory
func newPipelineLoaderFactory(esConfig *common.Config) fileset.PipelineLoaderFactory {
pipelineLoaderFactory := func() (fileset.PipelineLoader, error) {
esClient, err := elasticsearch.NewConnectedClient(esConfig)
esClient, err := eslegclient.NewConnectedClient(esConfig)
if err != nil {
return nil, errors.Wrap(err, "Error creating Elasticsearch client")
}
Expand Down
3 changes: 2 additions & 1 deletion filebeat/fileset/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/elastic/beats/v7/libbeat/beat"
"github.com/elastic/beats/v7/libbeat/cfgfile"
"github.com/elastic/beats/v7/libbeat/common"
"github.com/elastic/beats/v7/libbeat/esleg/eslegclient"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/libbeat/monitoring"
"github.com/elastic/beats/v7/libbeat/outputs/elasticsearch"
Expand Down Expand Up @@ -134,7 +135,7 @@ func (p *inputsRunner) Start() {
}

// Register callback to try to load pipelines when connecting to ES.
callback := func(esClient *elasticsearch.Client) error {
callback := func(esClient *eslegclient.Connection) error {
return p.moduleRegistry.LoadPipelines(esClient, p.overwritePipelines)
}
p.pipelineCallbackID, err = elasticsearch.RegisterConnectCallback(callback)
Expand Down
41 changes: 31 additions & 10 deletions filebeat/fileset/modules_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import (
"github.com/stretchr/testify/assert"

"github.com/elastic/beats/v7/libbeat/beat"
"github.com/elastic/beats/v7/libbeat/outputs/elasticsearch"
"github.com/elastic/beats/v7/libbeat/outputs/elasticsearch/estest"
"github.com/elastic/beats/v7/libbeat/esleg/eslegclient"
"github.com/elastic/beats/v7/libbeat/esleg/eslegtest"
)

func makeTestInfo(version string) beat.Info {
Expand All @@ -39,7 +39,7 @@ func makeTestInfo(version string) beat.Info {
}

func TestLoadPipeline(t *testing.T) {
client := estest.GetTestingElasticsearch(t)
client := getTestingElasticsearch(t)
if !hasIngest(client) {
t.Skip("Skip tests because ingest is missing in this elasticsearch version: ", client.GetVersion())
}
Expand Down Expand Up @@ -77,7 +77,7 @@ func TestLoadPipeline(t *testing.T) {
checkUploadedPipeline(t, client, "describe pipeline 2")
}

func checkUploadedPipeline(t *testing.T, client *elasticsearch.Client, expectedDescription string) {
func checkUploadedPipeline(t *testing.T, client *eslegclient.Connection, expectedDescription string) {
status, response, err := client.Request("GET", "/_ingest/pipeline/my-pipeline-id", "", nil, nil)
assert.NoError(t, err)
assert.Equal(t, 200, status)
Expand All @@ -90,7 +90,7 @@ func checkUploadedPipeline(t *testing.T, client *elasticsearch.Client, expectedD
}

func TestSetupNginx(t *testing.T) {
client := estest.GetTestingElasticsearch(t)
client := getTestingElasticsearch(t)
if !hasIngest(client) {
t.Skip("Skip tests because ingest is missing in this elasticsearch version: ", client.GetVersion())
}
Expand Down Expand Up @@ -122,7 +122,7 @@ func TestSetupNginx(t *testing.T) {
}

func TestAvailableProcessors(t *testing.T) {
client := estest.GetTestingElasticsearch(t)
client := getTestingElasticsearch(t)
if !hasIngest(client) {
t.Skip("Skip tests because ingest is missing in this elasticsearch version: ", client.GetVersion())
}
Expand All @@ -147,18 +147,18 @@ func TestAvailableProcessors(t *testing.T) {
assert.Contains(t, err.Error(), "ingest-hello")
}

func hasIngest(client *elasticsearch.Client) bool {
func hasIngest(client *eslegclient.Connection) bool {
v := client.GetVersion()
return v.Major >= 5
}

func hasIngestPipelineProcessor(client *elasticsearch.Client) bool {
func hasIngestPipelineProcessor(client *eslegclient.Connection) bool {
v := client.GetVersion()
return v.Major > 6 || (v.Major == 6 && v.Minor >= 5)
}

func TestLoadMultiplePipelines(t *testing.T) {
client := estest.GetTestingElasticsearch(t)
client := getTestingElasticsearch(t)
if !hasIngest(client) {
t.Skip("Skip tests because ingest is missing in this elasticsearch version: ", client.GetVersion())
}
Expand Down Expand Up @@ -203,7 +203,7 @@ func TestLoadMultiplePipelines(t *testing.T) {
}

func TestLoadMultiplePipelinesWithRollback(t *testing.T) {
client := estest.GetTestingElasticsearch(t)
client := getTestingElasticsearch(t)
if !hasIngest(client) {
t.Skip("Skip tests because ingest is missing in this elasticsearch version: ", client.GetVersion())
}
Expand Down Expand Up @@ -245,3 +245,24 @@ func TestLoadMultiplePipelinesWithRollback(t *testing.T) {
status, _, _ = client.Request("GET", "/_ingest/pipeline/filebeat-6.6.0-foo-multibad-plain_logs_bad", "", nil, nil)
assert.Equal(t, 404, status)
}

func getTestingElasticsearch(t eslegtest.TestLogger) *eslegclient.Connection {
conn, err := eslegclient.NewConnection(eslegclient.ConnectionSettings{
URL: eslegtest.GetURL(),
Timeout: 0,
})
if err != nil {
t.Fatal(err)
panic(err) // panic in case TestLogger did not stop test
}

conn.Encoder = eslegclient.NewJSONEncoder(nil, false)

err = conn.Connect()
if err != nil {
t.Fatal(err)
panic(err) // panic in case TestLogger did not stop test
}

return conn
}
14 changes: 8 additions & 6 deletions filebeat/fileset/pipelines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"time"

"github.com/elastic/beats/v7/libbeat/common"
"github.com/elastic/beats/v7/libbeat/outputs/elasticsearch"
"github.com/elastic/beats/v7/libbeat/esleg/eslegclient"

"github.com/stretchr/testify/assert"
)

func TestLoadPipelinesWithMultiPipelineFileset(t *testing.T) {
Expand Down Expand Up @@ -87,9 +88,10 @@ func TestLoadPipelinesWithMultiPipelineFileset(t *testing.T) {
}))
defer testESServer.Close()

testESClient, err := elasticsearch.NewClient(elasticsearch.ClientSettings{
URL: testESServer.URL,
}, nil)
testESClient, err := eslegclient.NewConnection(eslegclient.ConnectionSettings{
URL: testESServer.URL,
Timeout: 90 * time.Second,
})
assert.NoError(t, err)

err = testESClient.Connect()
Expand Down
17 changes: 8 additions & 9 deletions libbeat/cmd/instance/beat.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,10 @@ import (
"strings"
"time"

"github.com/elastic/beats/v7/libbeat/kibana"

"github.com/gofrs/uuid"
errw "github.com/pkg/errors"
"go.uber.org/zap"

sysinfo "github.com/elastic/go-sysinfo"
"github.com/elastic/go-sysinfo/types"
ucfg "github.com/elastic/go-ucfg"

"github.com/elastic/beats/v7/libbeat/api"
"github.com/elastic/beats/v7/libbeat/asset"
"github.com/elastic/beats/v7/libbeat/beat"
Expand All @@ -53,8 +47,10 @@ import (
"github.com/elastic/beats/v7/libbeat/common/reload"
"github.com/elastic/beats/v7/libbeat/common/seccomp"
"github.com/elastic/beats/v7/libbeat/dashboards"
"github.com/elastic/beats/v7/libbeat/esleg/eslegclient"
"github.com/elastic/beats/v7/libbeat/idxmgmt"
"github.com/elastic/beats/v7/libbeat/keystore"
"github.com/elastic/beats/v7/libbeat/kibana"
"github.com/elastic/beats/v7/libbeat/logp"
"github.com/elastic/beats/v7/libbeat/logp/configure"
"github.com/elastic/beats/v7/libbeat/management"
Expand All @@ -70,6 +66,9 @@ import (
"github.com/elastic/beats/v7/libbeat/publisher/processing"
svc "github.com/elastic/beats/v7/libbeat/service"
"github.com/elastic/beats/v7/libbeat/version"
sysinfo "github.com/elastic/go-sysinfo"
"github.com/elastic/go-sysinfo/types"
ucfg "github.com/elastic/go-ucfg"
)

// Beat provides the runnable and configurable instance of a beat.
Expand Down Expand Up @@ -496,7 +495,7 @@ func (b *Beat) Setup(settings Settings, bt beat.Creator, setup SetupSettings) er
if outCfg.Name() != "elasticsearch" {
return fmt.Errorf("Index management requested but the Elasticsearch output is not configured/enabled")
}
esClient, err := elasticsearch.NewConnectedClient(outCfg.Config())
esClient, err := eslegclient.NewConnectedClient(outCfg.Config())
if err != nil {
return err
}
Expand Down Expand Up @@ -799,7 +798,7 @@ func (b *Beat) registerESIndexManagement() error {
}

func (b *Beat) indexSetupCallback() elasticsearch.ConnectCallback {
return func(esClient *elasticsearch.Client) error {
return func(esClient *eslegclient.Connection) error {
m := b.IdxSupporter.Manager(idxmgmt.NewESClientHandler(esClient), idxmgmt.BeatsAssets(b.Fields))
return m.Setup(idxmgmt.LoadModeEnabled, idxmgmt.LoadModeEnabled)
}
Expand Down Expand Up @@ -845,7 +844,7 @@ func (b *Beat) clusterUUIDFetchingCallback() (elasticsearch.ConnectCallback, err
elasticsearchRegistry := stateRegistry.NewRegistry("outputs.elasticsearch")
clusterUUIDRegVar := monitoring.NewString(elasticsearchRegistry, "cluster_uuid")

callback := func(esClient *elasticsearch.Client) error {
callback := func(esClient *eslegclient.Connection) error {
var response struct {
ClusterUUID string `json:"cluster_uuid"`
}
Expand Down
4 changes: 3 additions & 1 deletion libbeat/common/transport/wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

package transport

import "net"
import (
"net"
)

func ConnWrapper(d Dialer, w func(net.Conn) net.Conn) Dialer {
return DialerFunc(func(network, addr string) (net.Conn, error) {
Expand Down
30 changes: 30 additions & 0 deletions libbeat/common/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,33 @@ func EncodeURLParams(url string, params url.Values) string {

return strings.Join([]string{url, "?", params.Encode()}, "")
}

type ParseHint func(raw string) string

// ParseURL tries to parse a URL and return the parsed result.
func ParseURL(raw string, hints ...ParseHint) (*url.URL, error) {
if raw == "" {
return nil, nil
}

if len(hints) == 0 {
hints = append(hints, WithDefaultScheme("http"))
}

if strings.Index(raw, "://") == -1 {
for _, hint := range hints {
raw = hint(raw)
}
}

return url.Parse(raw)
}

func WithDefaultScheme(scheme string) ParseHint {
return func(raw string) string {
if !strings.Contains(raw, "://") {
return scheme + "://" + raw
}
return raw
}
}
53 changes: 53 additions & 0 deletions libbeat/common/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetUrl(t *testing.T) {
Expand Down Expand Up @@ -114,3 +115,55 @@ func TestURLParamsEncode(t *testing.T) {
assert.Equal(t, output, urlWithParams)
}
}

func TestParseURL(t *testing.T) {
tests := map[string]struct {
input string
hints []ParseHint
expected string
errorAssertFunc require.ErrorAssertionFunc
}{
"http": {
"http://host:1234/path",
nil,
"http://host:1234/path",
require.NoError,
},
"https": {
"https://host:1234/path",
nil,
"https://host:1234/path",
require.NoError,
},
"no_scheme": {
"host:1234/path",
nil,
"http://host:1234/path",
require.NoError,
},
"default_scheme_https": {
"host:1234/path",
[]ParseHint{WithDefaultScheme("https")},
"https://host:1234/path",
require.NoError,
},
"invalid": {
"foobar:port",
nil,
"",
require.Error,
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
u, err := ParseURL(test.input, test.hints...)
test.errorAssertFunc(t, err)
if test.expected != "" {
require.Equal(t, test.expected, u.String())
} else {
require.Nil(t, u)
}
})
}
}