Repository: go-kratos/kratos
Pull request #2298 (2022-08-21 06:13:34)

fix(internal/host): Extract can't return the minium index ip
#### Description (what this PR does / why we need it):

- fix bug #2297 
- rename: `lowest` -> `minIndex`
- ~~use const `math.MaxInt` replace `int(^uint(0) >> 1)`~~
https://go.dev/doc/go1.17
![image](https://user-images.githubusercontent.com/25560763/185780690-4e97afdc-e366-42e7-bc12-3e2fe26920d1.png)


#### Which issue(s) this PR fixes (resolves / be part of):

https://github.com/go-kratos/kratos/issues/2297


codecov-commenter: # [Codecov](https://codecov.io/gh/go-kratos/kratos/pull/2298?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) Report
> Merging [#2298](https://codecov.io/gh/go-kratos/kratos/pull/2298?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (d9ab6dc) into [main](https://codecov.io/gh/go-kratos/kratos/commit/b1bb0bd18f10c33e20c34eadd0926aba1b90130b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) (b1bb0bd) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #2298   +/-   ##
=======================================
  Coverage   84.96%   84.96%           
=======================================
  Files          88       88           
  Lines        3771     3771           
=======================================
  Hits         3204     3204           
  Misses        399      399           
  Partials      168      168           
```


| [Impacted Files](https://codecov.io/gh/go-kratos/kratos/pull/2298?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos) | Coverage Δ | |
|---|---|---|
| [internal/host/host.go](https://codecov.io/gh/go-kratos/kratos/pull/2298/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos#diff-aW50ZXJuYWwvaG9zdC9ob3N0Lmdv) | `66.66% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=go-kratos)


Patch:
diff --git a/README.md b/README.md
index f4023812..815be2fc 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,6 @@ If you discover a security vulnerability within Kratos, please send an e-mail to
 - [Wechat Group](https://github.com/go-kratos/kratos/issues/682)
 - [Discord Group](https://discord.gg/BWzJsUJ)
 - [go-kratos.dev](https://go-kratos.dev/en)
-- QQ Group: 716486124
 
 ## Contributors
 
diff --git a/api/metadata/server.go b/api/metadata/server.go
index 3438d132..50f90f61 100644
--- a/api/metadata/server.go
+++ b/api/metadata/server.go
@@ -105,7 +105,10 @@ func (s *Server) ListServices(ctx context.Context, in *ListServicesRequest) (*Li
 	if err := s.load(); err != nil {
 		return nil, err
 	}
-	reply := new(ListServicesReply)
+	reply := &ListServicesReply{
+		Services: make([]string, 0, len(s.services)),
+		Methods:  make([]string, 0, len(s.methods)),
+	}
 	for name := range s.services {
 		reply.Services = append(reply.Services, name)
 	}
diff --git a/cmd/kratos/internal/base/repo.go b/cmd/kratos/internal/base/repo.go
index 55b12ac0..bdcb3f01 100644
--- a/cmd/kratos/internal/base/repo.go
+++ b/cmd/kratos/internal/base/repo.go
@@ -3,13 +3,15 @@ package base
 import (
 	"context"
 	"fmt"
-	stdurl "net/url"
+	"net"
 	"os"
 	"os/exec"
 	"path"
 	"strings"
 )
 
+var unExpandVarPath = []string{"~", ".", ".."}
+
 // Repo is git repository manager.
 type Repo struct {
 	url    string
@@ -18,27 +20,21 @@ type Repo struct {
 }
 
 func repoDir(url string) string {
-	if !strings.Contains(url, "//") {
-		url = "//" + url
-	}
-	if strings.HasPrefix(url, "//git@") {
-		url = "ssh:" + url
-	} else if strings.HasPrefix(url, "//") {
-		url = "https:" + url
+	vcsURL, err := ParseVCSUrl(url)
+	if err != nil {
+		return url
 	}
-	u, err := stdurl.Parse(url)
-	if err == nil {
-		url = fmt.Sprintf("%s://%s%s", u.Scheme, u.Hostname(), u.Path)
+	// check host contains port
+	host, _, err := net.SplitHostPort(vcsURL.Host)
+	if err != nil {
+		host = vcsURL.Host
 	}
-	var start int
-	start = strings.Index(url, "//")
-	if start == -1 {
-		start = strings.Index(url, ":") + 1
-	} else {
-		start += 2
+	for _, p := range unExpandVarPath {
+		host = strings.TrimLeft(host, p)
 	}
-	end := strings.LastIndex(url, "/")
-	return url[start:end]
+	dir := path.Base(path.Dir(vcsURL.Path))
+	url = fmt.Sprintf("%s/%s", host, dir)
+	return url
 }
 
 // NewRepo new a repository manager.
diff --git a/cmd/kratos/internal/base/vcs_url.go b/cmd/kratos/internal/base/vcs_url.go
new file mode 100644
index 00000000..ef76db30
--- /dev/null
+++ b/cmd/kratos/internal/base/vcs_url.go
@@ -0,0 +1,58 @@
+package base
+
+import (
+	"errors"
+	"net/url"
+	"regexp"
+	"strings"
+)
+
+var (
+	scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`)
+	scheme      = []string{"git", "https", "http", "git+ssh", "ssh", "file", "ftp", "ftps"}
+)
+
+// ParseVCSUrl ref https://github.com/golang/go/blob/master/src/cmd/go/internal/vcs/vcs.go
+// see https://go-review.googlesource.com/c/go/+/12226/
+// git url define https://git-scm.com/docs/git-clone#_git_urls
+func ParseVCSUrl(repo string) (*url.URL, error) {
+	var (
+		repoURL *url.URL
+		err     error
+	)
+
+	if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil {
+		// Match SCP-like syntax and convert it to a URL.
+		// Eg, "git@github.com:user/repo" becomes
+		// "ssh://git@github.com/user/repo".
+		repoURL = &url.URL{
+			Scheme: "ssh",
+			User:   url.User(m[1]),
+			Host:   m[2],
+			Path:   m[3],
+		}
+	} else {
+		if !strings.Contains(repo, "//") {
+			repo = "//" + repo
+		}
+		if strings.HasPrefix(repo, "//git@") {
+			repo = "ssh:" + repo
+		} else if strings.HasPrefix(repo, "//") {
+			repo = "https:" + repo
+		}
+		repoURL, err = url.Parse(repo)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// Iterate over insecure schemes too, because this function simply
+	// reports the state of the repo. If we can't see insecure schemes then
+	// we can't report the actual repo URL.
+	for _, s := range scheme {
+		if repoURL.Scheme == s {
+			return repoURL, nil
+		}
+	}
+	return nil, errors.New("unable to parse repo url")
+}
diff --git a/cmd/protoc-gen-go-http/http.go b/cmd/protoc-gen-go-http/http.go
index 3eb91c06..cebbb6b1 100644
--- a/cmd/protoc-gen-go-http/http.go
+++ b/cmd/protoc-gen-go-http/http.go
@@ -213,9 +213,9 @@ func buildPathVars(path string) (res map[string]*string) {
 	if strings.HasSuffix(path, "/") {
 		fmt.Fprintf(os.Stderr, "\u001B[31mWARN\u001B[m: Path %s should not end with \"/\" \n", path)
 	}
-	res = make(map[string]*string)
 	pattern := regexp.MustCompile(`(?i){([a-z\.0-9_\s]*)=?([^{}]*)}`)
 	matches := pattern.FindAllStringSubmatch(path, -1)
+	res = make(map[string]*string, len(matches))
 	for _, m := range matches {
 		name := strings.TrimSpace(m[1])
 		if len(name) > 1 && len(m[2]) > 0 {
diff --git a/config/value.go b/config/value.go
index 805f155e..46417e77 100644
--- a/config/value.go
+++ b/config/value.go
@@ -93,9 +93,9 @@ func (v *atomicValue) Slice() ([]Value, error) {
 
 func (v *atomicValue) Map() (map[string]Value, error) {
 	if vals, ok := v.Load().(map[string]interface{}); ok {
-		m := make(map[string]Value)
+		m := make(map[string]Value, len(vals))
 		for key, val := range vals {
-			a := &atomicValue{}
+			a := new(atomicValue)
 			a.Store(val)
 			m[key] = a
 		}
diff --git a/contrib/registry/consul/client.go b/contrib/registry/consul/client.go
index 80ea383a..1b39975a 100644
--- a/contrib/registry/consul/client.go
+++ b/contrib/registry/consul/client.go
@@ -29,6 +29,8 @@ type Client struct {
 	heartbeat bool
 	// deregisterCriticalServiceAfter time interval in seconds
 	deregisterCriticalServiceAfter int
+	// serviceChecks  user custom checks
+	serviceChecks api.AgentServiceChecks
 }
 
 // NewClient creates consul client
@@ -95,7 +97,7 @@ func (c *Client) Service(ctx context.Context, service string, index uint64, pass
 
 // Register register service instance to consul
 func (c *Client) Register(_ context.Context, svc *registry.ServiceInstance, enableHealthCheck bool) error {
-	addresses := make(map[string]api.ServiceAddress)
+	addresses := make(map[string]api.ServiceAddress, len(svc.Endpoints))
 	checkAddresses := make([]string, 0, len(svc.Endpoints))
 	for _, endpoint := range svc.Endpoints {
 		raw, err := url.Parse(endpoint)
@@ -139,6 +141,9 @@ func (c *Client) Register(_ context.Context, svc *registry.ServiceInstance, enab
 		})
 	}
 
+	// custom checks
+	asr.Checks = append(asr.Checks, c.serviceChecks...)
+
 	err := c.cli.Agent().ServiceRegister(asr)
 	if err != nil {
 		return err
diff --git a/contrib/registry/consul/registry.go b/contrib/registry/consul/registry.go
index 9da95d77..d1c47d77 100644
--- a/contrib/registry/consul/registry.go
+++ b/contrib/registry/consul/registry.go
@@ -63,6 +63,15 @@ func WithDeregisterCriticalServiceAfter(interval int) Option {
 	}
 }
 
+// WithServiceCheck with service checks
+func WithServiceCheck(checks ...*api.AgentServiceCheck) Option {
+	return func(o *Registry) {
+		if o.cli != nil {
+			o.cli.serviceChecks = checks
+		}
+	}
+}
+
 // Config is consul registry config
 type Config struct {
 	*api.Config
diff --git a/contrib/registry/discovery/discovery.go b/contrib/registry/discovery/discovery.go
index f6df56dc..dfff0a97 100644
--- a/contrib/registry/discovery/discovery.go
+++ b/contrib/registry/discovery/discovery.go
@@ -439,7 +439,7 @@ func (r *Resolve) fetch(ctx context.Context) (ins *disInstancesInfo, ok bool) {
 		ins = new(disInstancesInfo)
 		ins.LastTs = appIns.LastTs
 		ins.Scheduler = appIns.Scheduler
-		ins.Instances = make(map[string][]*discoveryInstance)
+		ins.Instances = make(map[string][]*discoveryInstance, len(appIns.Instances))
 		for zone, in := range appIns.Instances {
 			ins.Instances[zone] = in
 		}
diff --git a/internal/host/host.go b/internal/host/host.go
index d5cc7ffa..bf204eed 100644
--- a/internal/host/host.go
+++ b/internal/host/host.go
@@ -50,16 +50,13 @@ func Extract(hostPort string, lis net.Listener) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	lowest := int(^uint(0) >> 1)
+	minIndex := int(^uint(0) >> 1)
 	var result net.IP
 	for _, iface := range ifaces {
 		if (iface.Flags & net.FlagUp) == 0 {
 			continue
 		}
-		if iface.Index < lowest || result == nil {
-			lowest = iface.Index
-		}
-		if result != nil {
+		if iface.Index >= minIndex && result != nil {
 			continue
 		}
 		addrs, err := iface.Addrs()
@@ -77,7 +74,9 @@ func Extract(hostPort string, lis net.Listener) (string, error) {
 				continue
 			}
 			if isValidIP(ip.String()) {
+				minIndex = iface.Index
 				result = ip
+				break
 			}
 		}
 	}
diff --git a/middleware/tracing/tracer.go b/middleware/tracing/tracer.go
index 48d4eaa1..cf9115bb 100644
--- a/middleware/tracing/tracer.go
+++ b/middleware/tracing/tracer.go
@@ -24,6 +24,7 @@ type Tracer struct {
 func NewTracer(kind trace.SpanKind, opts ...Option) *Tracer {
 	op := options{
 		propagator: propagation.NewCompositeTextMapPropagator(Metadata{}, propagation.Baggage{}, propagation.TraceContext{}),
+		tracerName: "kratos",
 	}
 	for _, o := range opts {
 		o(&op)
@@ -34,9 +35,9 @@ func NewTracer(kind trace.SpanKind, opts ...Option) *Tracer {
 
 	switch kind {
 	case trace.SpanKindClient:
-		return &Tracer{tracer: otel.Tracer("kratos"), kind: kind, opt: &op}
+		return &Tracer{tracer: otel.Tracer(op.tracerName), kind: kind, opt: &op}
 	case trace.SpanKindServer:
-		return &Tracer{tracer: otel.Tracer("kratos"), kind: kind, opt: &op}
+		return &Tracer{tracer: otel.Tracer(op.tracerName), kind: kind, opt: &op}
 	default:
 		panic(fmt.Sprintf("unsupported span kind: %v", kind))
 	}
diff --git a/middleware/tracing/tracing.go b/middleware/tracing/tracing.go
index c04b13d0..167725f4 100644
--- a/middleware/tracing/tracing.go
+++ b/middleware/tracing/tracing.go
@@ -15,6 +15,7 @@ import (
 type Option func(*options)
 
 type options struct {
+	tracerName     string
 	tracerProvider trace.TracerProvider
 	propagator     propagation.TextMapPropagator
 }
@@ -34,6 +35,13 @@ func WithTracerProvider(provider trace.TracerProvider) Option {
 	}
 }
 
+// WithTracerName with tracer name
+func WithTracerName(tracerName string) Option {
+	return func(opts *options) {
+		opts.tracerName = tracerName
+	}
+}
+
 // Server returns a new server middleware for OpenTelemetry.
 func Server(opts ...Option) middleware.Middleware {
 	tracer := NewTracer(trace.SpanKindServer, opts...)
diff --git a/transport/grpc/balancer.go b/transport/grpc/balancer.go
index bd50f098..2047eb0d 100644
--- a/transport/grpc/balancer.go
+++ b/transport/grpc/balancer.go
@@ -40,7 +40,7 @@ func (b *balancerBuilder) Build(info base.PickerBuildInfo) balancer.Picker {
 		// Block the RPC until a new picker is available via UpdateState().
 		return base.NewErrPicker(balancer.ErrNoSubConnAvailable)
 	}
-	nodes := make([]selector.Node, 0)
+	nodes := make([]selector.Node, 0, len(info.ReadySCs))
 	for conn, info := range info.ReadySCs {
 		ins, _ := info.Address.Attributes.Value("rawServiceInstance").(*registry.ServiceInstance)
 		nodes = append(nodes, &grpcNode{


Test patch:
diff --git a/cmd/kratos/internal/base/repo_test.go b/cmd/kratos/internal/base/repo_test.go
index 15ed33c8..dfcdbb93 100644
--- a/cmd/kratos/internal/base/repo_test.go
+++ b/cmd/kratos/internal/base/repo_test.go
@@ -7,7 +7,37 @@ import (
 )
 
 func TestRepo(t *testing.T) {
-	os.RemoveAll("/tmp/test_repo")
+	urls := []string{
+		// ssh://[user@]host.xz[:port]/path/to/repo.git/
+		"ssh://git@github.com:7875/go-kratos/kratos.git",
+		// git://host.xz[:port]/path/to/repo.git/
+		"git://github.com:7875/go-kratos/kratos.git",
+		// http[s]://host.xz[:port]/path/to/repo.git/
+		"https://github.com:7875/go-kratos/kratos.git",
+		// ftp[s]://host.xz[:port]/path/to/repo.git/
+		"ftps://github.com:7875/go-kratos/kratos.git",
+		//[user@]host.xz:path/to/repo.git/
+		"git@github.com:go-kratos/kratos.git",
+		// ssh://[user@]host.xz[:port]/~[user]/path/to/repo.git/
+		"ssh://git@github.com:7875/go-kratos/kratos.git",
+		// git://host.xz[:port]/~[user]/path/to/repo.git/
+		"git://github.com:7875/go-kratos/kratos.git",
+		//[user@]host.xz:/~[user]/path/to/repo.git/
+		"git@github.com:go-kratos/kratos.git",
+		///path/to/repo.git/
+		"//github.com/go-kratos/kratos.git",
+		// file:///path/to/repo.git/
+		"file://./github.com/go-kratos/kratos.git",
+	}
+	for _, url := range urls {
+		dir := repoDir(url)
+		if dir != "github.com/go-kratos" && dir != "/go-kratos" {
+			t.Fatal(url, "repoDir test failed", dir)
+		}
+	}
+}
+
+func TestRepoClone(t *testing.T) {
 	r := NewRepo("https://github.com/go-kratos/service-layout.git", "")
 	if err := r.Clone(context.Background()); err != nil {
 		t.Fatal(err)
@@ -15,19 +45,7 @@ func TestRepo(t *testing.T) {
 	if err := r.CopyTo(context.Background(), "/tmp/test_repo", "github.com/go-kratos/kratos-layout", nil); err != nil {
 		t.Fatal(err)
 	}
-	urls := []string{
-		"ssh://git@gitlab.xxx.com:1234/foo/bar.git",
-		"ssh://gitlab.xxx.com:1234/foo/bar.git",
-		"//git@gitlab.xxx.com:1234/foo/bar.git",
-		"git@gitlab.xxx.com:1234/foo/bar.git",
-		"gitlab.xxx.com:1234/foo/bar.git",
-		"gitlab.xxx.com/foo/bar.git",
-		"gitlab.xxx.com/foo/bar",
-	}
-	for _, url := range urls {
-		dir := repoDir(url)
-		if dir != "gitlab.xxx.com/foo" {
-			t.Fatal("repoDir test failed", dir)
-		}
-	}
+	t.Cleanup(func() {
+		os.RemoveAll("/tmp/test_repo")
+	})
 }
diff --git a/cmd/kratos/internal/base/vcs_url_test.go b/cmd/kratos/internal/base/vcs_url_test.go
new file mode 100644
index 00000000..09b8d568
--- /dev/null
+++ b/cmd/kratos/internal/base/vcs_url_test.go
@@ -0,0 +1,55 @@
+package base
+
+import (
+	"net"
+	"strings"
+	"testing"
+)
+
+func TestParseVCSUrl(t *testing.T) {
+	repos := []string{
+		// ssh://[user@]host.xz[:port]/path/to/repo.git/
+		"ssh://git@github.com:7875/go-kratos/kratos.git",
+		// git://host.xz[:port]/path/to/repo.git/
+		"git://github.com:7875/go-kratos/kratos.git",
+		// http[s]://host.xz[:port]/path/to/repo.git/
+		"https://github.com:7875/go-kratos/kratos.git",
+		// ftp[s]://host.xz[:port]/path/to/repo.git/
+		"ftps://github.com:7875/go-kratos/kratos.git",
+		//[user@]host.xz:path/to/repo.git/
+		"git@github.com:go-kratos/kratos.git",
+		// ssh://[user@]host.xz[:port]/~[user]/path/to/repo.git/
+		"ssh://git@github.com:7875/go-kratos/kratos.git",
+		// git://host.xz[:port]/~[user]/path/to/repo.git/
+		"git://github.com:7875/go-kratos/kratos.git",
+		//[user@]host.xz:/~[user]/path/to/repo.git/
+		"git@github.com:go-kratos/kratos.git",
+		///path/to/repo.git/
+		"~/go-kratos/kratos.git",
+		// file:///path/to/repo.git/
+		"file://~/go-kratos/kratos.git",
+	}
+	for _, repo := range repos {
+		url, err := ParseVCSUrl(repo)
+		if err != nil {
+			t.Fatal(repo, err)
+		}
+		urlPath := strings.TrimLeft(url.Path, "/")
+		if urlPath != "go-kratos/kratos.git" {
+			t.Fatal(repo, "parse url failed", urlPath)
+		}
+	}
+}
+
+func TestParseSsh(t *testing.T) {
+	repo := "ssh://git@github.com:7875/go-kratos/kratos.git"
+	url, err := ParseVCSUrl(repo)
+	if err != nil {
+		t.Fatal(err)
+	}
+	host, _, err := net.SplitHostPort(url.Host)
+	if err != nil {
+		host = url.Host
+	}
+	t.Log(host, url.Path)
+}
diff --git a/selector/selector_test.go b/selector/selector_test.go
index f7c1f1bc..bcc08264 100644
--- a/selector/selector_test.go
+++ b/selector/selector_test.go
@@ -12,6 +12,8 @@ import (
 	"github.com/go-kratos/kratos/v2/registry"
 )
 
+var errNodeNotMatch = errors.New("node is not match")
+
 type mockWeightedNode struct {
 	Node
 
@@ -80,6 +82,18 @@ func (b *mockBalancer) Pick(ctx context.Context, nodes []WeightedNode) (selected
 	return
 }
 
+type mockMustErrorBalancerBuilder struct{}
+
+func (b *mockMustErrorBalancerBuilder) Build() Balancer {
+	return &mockMustErrorBalancer{}
+}
+
+type mockMustErrorBalancer struct{}
+
+func (b *mockMustErrorBalancer) Pick(ctx context.Context, nodes []WeightedNode) (selected WeightedNode, done DoneFunc, err error) {
+	return nil, nil, errNodeNotMatch
+}
+
 func TestDefault(t *testing.T) {
 	builder := DefaultBuilder{
 		Node:     &mockWeightedNodeBuilder{},
@@ -107,6 +121,7 @@ func TestDefault(t *testing.T) {
 			Endpoints: []string{"http://127.0.0.1:9090"},
 			Metadata:  map[string]string{"weight": "10"},
 		}))
+
 	selector.Apply(nodes)
 	n, done, err := selector.Select(context.Background(), WithNodeFilter(mockFilter("v2.0.0")))
 	if err != nil {
@@ -121,6 +136,9 @@ func TestDefault(t *testing.T) {
 	if !reflect.DeepEqual("v2.0.0", n.Version()) {
 		t.Errorf("expect %v, got %v", "v2.0.0", n.Version())
 	}
+	if n.Scheme() == "" {
+		t.Errorf("expect %v, got %v", "", n.Scheme())
+	}
 	if n.Address() == "" {
 		t.Errorf("expect %v, got %v", "", n.Address())
 	}
@@ -135,6 +153,21 @@ func TestDefault(t *testing.T) {
 	}
 	done(context.Background(), DoneInfo{})
 
+	// peer in ctx
+	ctx := NewPeerContext(context.Background(), &Peer{
+		Node: mockWeightedNode{},
+	})
+	n, done, err = selector.Select(ctx)
+	if err != nil {
+		t.Errorf("expect %v, got %v", ErrNoAvailable, err)
+	}
+	if done == nil {
+		t.Errorf("expect %v, got %v", nil, done)
+	}
+	if n == nil {
+		t.Errorf("expect %v, got %v", nil, n)
+	}
+
 	// no v3.0.0 instance
 	n, done, err = selector.Select(context.Background(), WithNodeFilter(mockFilter("v3.0.0")))
 	if !errors.Is(ErrNoAvailable, err) {
@@ -172,4 +205,87 @@ func TestDefault(t *testing.T) {
 	if n != nil {
 		t.Errorf("expect %v, got %v", nil, n)
 	}
+
+	// without node_filters
+	n, done, err = selector.Select(context.Background())
+	if !errors.Is(ErrNoAvailable, err) {
+		t.Errorf("expect %v, got %v", ErrNoAvailable, err)
+	}
+	if done != nil {
+		t.Errorf("expect %v, got %v", nil, done)
+	}
+	if n != nil {
+		t.Errorf("expect %v, got %v", nil, n)
+	}
+}
+
+func TestWithoutApply(t *testing.T) {
+	builder := DefaultBuilder{
+		Node:     &mockWeightedNodeBuilder{},
+		Balancer: &mockBalancerBuilder{},
+	}
+	selector := builder.Build()
+	n, done, err := selector.Select(context.Background())
+	if !errors.Is(ErrNoAvailable, err) {
+		t.Errorf("expect %v, got %v", ErrNoAvailable, err)
+	}
+	if done != nil {
+		t.Errorf("expect %v, got %v", nil, done)
+	}
+	if n != nil {
+		t.Errorf("expect %v, got %v", nil, n)
+	}
+}
+
+func TestNoPick(t *testing.T) {
+	builder := DefaultBuilder{
+		Node:     &mockWeightedNodeBuilder{},
+		Balancer: &mockMustErrorBalancerBuilder{},
+	}
+	var nodes []Node
+	nodes = append(nodes, NewNode(
+		"http",
+		"127.0.0.1:8080",
+		&registry.ServiceInstance{
+			ID:        "127.0.0.1:8080",
+			Name:      "helloworld",
+			Version:   "v2.0.0",
+			Endpoints: []string{"http://127.0.0.1:8080"},
+			Metadata:  map[string]string{"weight": "10"},
+		}))
+	nodes = append(nodes, NewNode(
+		"http",
+		"127.0.0.1:9090",
+		&registry.ServiceInstance{
+			ID:        "127.0.0.1:9090",
+			Name:      "helloworld",
+			Version:   "v1.0.0",
+			Endpoints: []string{"http://127.0.0.1:9090"},
+			Metadata:  map[string]string{"weight": "10"},
+		}))
+	selector := builder.Build()
+	selector.Apply(nodes)
+	n, done, err := selector.Select(context.Background())
+	if !errors.Is(errNodeNotMatch, err) {
+		t.Errorf("expect %v, got %v", errNodeNotMatch, err)
+	}
+	if done != nil {
+		t.Errorf("expect %v, got %v", nil, done)
+	}
+	if n != nil {
+		t.Errorf("expect %v, got %v", nil, n)
+	}
+}
+
+func TestGolobal(t *testing.T) {
+	builder := DefaultBuilder{
+		Node:     &mockWeightedNodeBuilder{},
+		Balancer: &mockBalancerBuilder{},
+	}
+	SetGlobalSelector(&builder)
+
+	gBuilder := GlobalSelector()
+	if gBuilder == nil {
+		t.Errorf("expect %v, got %v", nil, gBuilder)
+	}
 }