+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
var (
|
||||
alwaysEqual = cmp.Comparer(func(_, _ interface{}) bool { return true })
|
||||
|
||||
defaultCmpOptions = []cmp.Option{
|
||||
// Use proto.Equal for protobufs
|
||||
cmp.Comparer(proto.Equal),
|
||||
// NaNs compare equal
|
||||
cmp.FilterValues(func(x, y float64) bool {
|
||||
return math.IsNaN(x) && math.IsNaN(y)
|
||||
}, alwaysEqual),
|
||||
cmp.FilterValues(func(x, y float32) bool {
|
||||
return math.IsNaN(float64(x)) && math.IsNaN(float64(y))
|
||||
}, alwaysEqual),
|
||||
}
|
||||
)
|
||||
|
||||
// Equal tests two values for equality.
|
||||
func Equal(x, y interface{}, opts ...cmp.Option) bool {
|
||||
// Put default options at the end. Order doesn't matter.
|
||||
opts = append(opts[:len(opts):len(opts)], defaultCmpOptions...)
|
||||
return cmp.Equal(x, y, opts...)
|
||||
}
|
||||
|
||||
// Diff reports the differences between two values.
|
||||
// Diff(x, y) == "" iff Equal(x, y).
|
||||
func Diff(x, y interface{}, opts ...cmp.Option) string {
|
||||
// Put default options at the end. Order doesn't matter.
|
||||
opts = append(opts[:len(opts):len(opts)], defaultCmpOptions...)
|
||||
return cmp.Diff(x, y, opts...)
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package testutil contains helper functions for writing tests.
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"golang.org/x/oauth2/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
envProjID = "GCLOUD_TESTS_GOLANG_PROJECT_ID"
|
||||
envPrivateKey = "GCLOUD_TESTS_GOLANG_KEY"
|
||||
)
|
||||
|
||||
// ProjID returns the project ID to use in integration tests, or the empty
|
||||
// string if none is configured.
|
||||
func ProjID() string {
|
||||
return os.Getenv(envProjID)
|
||||
}
|
||||
|
||||
// TokenSource returns the OAuth2 token source to use in integration tests,
|
||||
// or nil if none is configured. It uses the standard environment variable
|
||||
// for tests in this repo.
|
||||
func TokenSource(ctx context.Context, scopes ...string) oauth2.TokenSource {
|
||||
return TokenSourceEnv(ctx, envPrivateKey, scopes...)
|
||||
}
|
||||
|
||||
// TokenSourceEnv returns the OAuth2 token source to use in integration tests. or nil
|
||||
// if none is configured. It tries to get credentials from the filename in the
|
||||
// environment variable envVar. If the environment variable is unset, TokenSourceEnv
|
||||
// will try to find 'Application Default Credentials'. Else, TokenSourceEnv will
|
||||
// return nil. TokenSourceEnv will log.Fatal if the token source is specified but
|
||||
// missing or invalid.
|
||||
func TokenSourceEnv(ctx context.Context, envVar string, scopes ...string) oauth2.TokenSource {
|
||||
key := os.Getenv(envVar)
|
||||
if key == "" { // Try for application default credentials.
|
||||
ts, err := google.DefaultTokenSource(ctx, scopes...)
|
||||
if err != nil {
|
||||
log.Println("No 'Application Default Credentials' found.")
|
||||
return nil
|
||||
}
|
||||
return ts
|
||||
}
|
||||
conf, err := jwtConfigFromFile(key, scopes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return conf.TokenSource(ctx)
|
||||
}
|
||||
|
||||
// JWTConfig reads the JSON private key file whose name is in the default
|
||||
// environment variable, and returns the jwt.Config it contains. It ignores
|
||||
// scopes.
|
||||
// If the environment variable is empty, it returns (nil, nil).
|
||||
func JWTConfig() (*jwt.Config, error) {
|
||||
return jwtConfigFromFile(os.Getenv(envPrivateKey), nil)
|
||||
}
|
||||
|
||||
// jwtConfigFromFile reads the given JSON private key file, and returns the
|
||||
// jwt.Config it contains.
|
||||
// If the filename is empty, it returns (nil, nil).
|
||||
func jwtConfigFromFile(filename string, scopes []string) (*jwt.Config, error) {
|
||||
if filename == "" {
|
||||
return nil, nil
|
||||
}
|
||||
jsonKey, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot read the JSON key file, err: %v", err)
|
||||
}
|
||||
conf, err := google.JWTConfigFromJSON(jsonKey, scopes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google.JWTConfigFromJSON: %v", err)
|
||||
}
|
||||
return conf, nil
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"go.opencensus.io/stats/view"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
type TestExporter struct {
|
||||
Spans []*trace.SpanData
|
||||
Stats chan *view.Data
|
||||
}
|
||||
|
||||
func NewTestExporter() *TestExporter {
|
||||
te := &TestExporter{Stats: make(chan *view.Data)}
|
||||
|
||||
view.RegisterExporter(te)
|
||||
view.SetReportingPeriod(time.Millisecond)
|
||||
if err := view.Register(ocgrpc.ClientRequestCountView); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
trace.RegisterExporter(te)
|
||||
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
|
||||
|
||||
return te
|
||||
}
|
||||
|
||||
func (te *TestExporter) ExportSpan(s *trace.SpanData) {
|
||||
te.Spans = append(te.Spans, s)
|
||||
}
|
||||
|
||||
func (te *TestExporter) ExportView(vd *view.Data) {
|
||||
if len(vd.Rows) > 0 {
|
||||
select {
|
||||
case te.Stats <- vd:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (te *TestExporter) Unregister() {
|
||||
view.UnregisterExporter(te)
|
||||
trace.UnregisterExporter(te)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// A Server is an in-process gRPC server, listening on a system-chosen port on
|
||||
// the local loopback interface. Servers are for testing only and are not
|
||||
// intended to be used in production code.
|
||||
//
|
||||
// To create a server, make a new Server, register your handlers, then call
|
||||
// Start:
|
||||
//
|
||||
// srv, err := NewServer()
|
||||
// ...
|
||||
// mypb.RegisterMyServiceServer(srv.Gsrv, &myHandler)
|
||||
// ....
|
||||
// srv.Start()
|
||||
//
|
||||
// Clients should connect to the server with no security:
|
||||
//
|
||||
// conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
// ...
|
||||
type Server struct {
|
||||
Addr string
|
||||
l net.Listener
|
||||
Gsrv *grpc.Server
|
||||
}
|
||||
|
||||
// NewServer creates a new Server. The Server will be listening for gRPC connections
|
||||
// at the address named by the Addr field, without TLS.
|
||||
func NewServer(opts ...grpc.ServerOption) (*Server, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := &Server{
|
||||
Addr: l.Addr().String(),
|
||||
l: l,
|
||||
Gsrv: grpc.NewServer(opts...),
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Start causes the server to start accepting incoming connections.
|
||||
// Call Start after registering handlers.
|
||||
func (s *Server) Start() {
|
||||
go s.Gsrv.Serve(s.l)
|
||||
}
|
||||
|
||||
// Close shuts down the server.
|
||||
func (s *Server) Close() {
|
||||
s.Gsrv.Stop()
|
||||
s.l.Close()
|
||||
}
|
||||
|
||||
// PageBounds converts an incoming page size and token from an RPC request into
|
||||
// slice bounds and the outgoing next-page token.
|
||||
//
|
||||
// PageBounds assumes that the complete, unpaginated list of items exists as a
|
||||
// single slice. In addition to the page size and token, PageBounds needs the
|
||||
// length of that slice.
|
||||
//
|
||||
// PageBounds's first two return values should be used to construct a sub-slice of
|
||||
// the complete, unpaginated slice. E.g. if the complete slice is s, then
|
||||
// s[from:to] is the desired page. Its third return value should be set as the
|
||||
// NextPageToken field of the RPC response.
|
||||
func PageBounds(pageSize int, pageToken string, length int) (from, to int, nextPageToken string, err error) {
|
||||
from, to = 0, length
|
||||
if pageToken != "" {
|
||||
from, err = strconv.Atoi(pageToken)
|
||||
if err != nil {
|
||||
return 0, 0, "", status.Errorf(codes.InvalidArgument, "bad page token: %v", err)
|
||||
}
|
||||
if from >= length {
|
||||
return length, length, "", nil
|
||||
}
|
||||
}
|
||||
if pageSize > 0 && from+pageSize < length {
|
||||
to = from + pageSize
|
||||
nextPageToken = strconv.Itoa(to)
|
||||
}
|
||||
return from, to, nextPageToken, nil
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
srv, err := NewServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.Start()
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Close()
|
||||
srv.Close()
|
||||
}
|
||||
|
||||
func TestPageBounds(t *testing.T) {
|
||||
const length = 10
|
||||
for _, test := range []struct {
|
||||
size int
|
||||
tok string
|
||||
wantFrom int
|
||||
wantTo int
|
||||
wantTok string
|
||||
}{
|
||||
{5, "",
|
||||
0, 5, "5"},
|
||||
{11, "",
|
||||
0, 10, ""},
|
||||
{5, "2",
|
||||
2, 7, "7"},
|
||||
{5, "8",
|
||||
8, 10, ""},
|
||||
{11, "8",
|
||||
8, 10, ""},
|
||||
{1, "11",
|
||||
10, 10, ""},
|
||||
} {
|
||||
gotFrom, gotTo, gotTok, err := PageBounds(test.size, test.tok, length)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := gotFrom, test.wantFrom; got != want {
|
||||
t.Errorf("%+v: from: got %d, want %d", test, got, want)
|
||||
}
|
||||
if got, want := gotTo, test.wantTo; got != want {
|
||||
t.Errorf("%+v: to: got %d, want %d", test, got, want)
|
||||
}
|
||||
if got, want := gotTok, test.wantTok; got != want {
|
||||
t.Errorf("%+v: got %q, want %q", test, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
_, _, _, err := PageBounds(4, "xyz", 5)
|
||||
if grpc.Code(err) != codes.InvalidArgument {
|
||||
t.Errorf("want invalid argument, got <%v>", err)
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file supports generating unique IDs so that multiple test executions
|
||||
// don't interfere with each other, and cleaning up old entities that may
|
||||
// remain if tests exit early.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var startTime = time.Now().UTC()
|
||||
|
||||
// A UIDSpace manages a set of unique IDs distinguished by a prefix.
|
||||
type UIDSpace struct {
|
||||
Prefix string
|
||||
Sep rune
|
||||
re *regexp.Regexp
|
||||
mu sync.Mutex
|
||||
count int
|
||||
}
|
||||
|
||||
func NewUIDSpace(prefix string) *UIDSpace {
|
||||
return NewUIDSpaceSep(prefix, '-')
|
||||
}
|
||||
|
||||
func NewUIDSpaceSep(prefix string, sep rune) *UIDSpace {
|
||||
re := fmt.Sprintf(`^%s%[2]c(\d{4})(\d{2})(\d{2})%[2]c(\d+)%[2]c\d+$`,
|
||||
regexp.QuoteMeta(prefix), sep)
|
||||
return &UIDSpace{
|
||||
Prefix: prefix,
|
||||
Sep: sep,
|
||||
re: regexp.MustCompile(re),
|
||||
}
|
||||
}
|
||||
|
||||
// New generates a new unique ID . The ID consists of the UIDSpace's prefix, a
|
||||
// timestamp, and a counter value. All unique IDs generated in the same test
|
||||
// execution will have the same timestamp.
|
||||
//
|
||||
// Aside from the characters in the prefix, IDs contain only letters, numbers
|
||||
// and sep.
|
||||
func (s *UIDSpace) New() string { return s.newID(startTime) }
|
||||
|
||||
func (s *UIDSpace) newID(t time.Time) string {
|
||||
s.mu.Lock()
|
||||
c := s.count
|
||||
s.count++
|
||||
s.mu.Unlock()
|
||||
// Write the time as a date followed by nanoseconds from midnight of that date.
|
||||
// That makes it easier to see the approximate time of the ID when it is displayed.
|
||||
y, m, d := t.Date()
|
||||
ns := t.Sub(time.Date(y, m, d, 0, 0, 0, 0, time.UTC))
|
||||
// Zero-pad the counter for lexical sort order for IDs with the same timestamp.
|
||||
return fmt.Sprintf("%s%c%04d%02d%02d%c%d%c%04d",
|
||||
s.Prefix, s.Sep, y, m, d, s.Sep, ns, s.Sep, c)
|
||||
}
|
||||
|
||||
// Timestamp extracts the timestamp of uid, which must have been generated by
|
||||
// s. The second return value is true on success, false if there was a problem.
|
||||
func (s *UIDSpace) Timestamp(uid string) (time.Time, bool) {
|
||||
subs := s.re.FindStringSubmatch(uid)
|
||||
if subs == nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
y, err1 := strconv.Atoi(subs[1])
|
||||
m, err2 := strconv.Atoi(subs[2])
|
||||
d, err3 := strconv.Atoi(subs[3])
|
||||
ns, err4 := strconv.Atoi(subs[4])
|
||||
if err1 != nil || err2 != nil || err3 != nil || err4 != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Date(y, time.Month(m), d, 0, 0, 0, ns, time.UTC), true
|
||||
}
|
||||
|
||||
// Older reports whether uid was created by m and has a timestamp older than
|
||||
// the current time by at least d.
|
||||
func (s *UIDSpace) Older(uid string, d time.Duration) bool {
|
||||
ts, ok := s.Timestamp(uid)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return time.Since(ts) > d
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
s := NewUIDSpace("prefix")
|
||||
tm := time.Date(2017, 1, 6, 0, 0, 0, 21, time.UTC)
|
||||
got := s.newID(tm)
|
||||
want := "prefix-20170106-21-0000"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
s2 := NewUIDSpaceSep("prefix2", '_')
|
||||
got = s2.newID(tm)
|
||||
want = "prefix2_20170106_21_0000"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestamp(t *testing.T) {
|
||||
s := NewUIDSpace("unique-ID")
|
||||
uid := s.New()
|
||||
got, ok := s.Timestamp(uid)
|
||||
if !ok {
|
||||
t.Fatal("got ok = false, want true")
|
||||
}
|
||||
if !startTime.Equal(got) {
|
||||
t.Errorf("got %s, want %s", got, startTime)
|
||||
}
|
||||
|
||||
got, ok = s.Timestamp("unique-ID-20160308-123-8")
|
||||
if !ok {
|
||||
t.Fatal("got false, want true")
|
||||
}
|
||||
if want := time.Date(2016, 3, 8, 0, 0, 0, 123, time.UTC); !want.Equal(got) {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
if _, ok = s.Timestamp("invalid-time-1234"); ok {
|
||||
t.Error("got true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOlder(t *testing.T) {
|
||||
s := NewUIDSpace("uid")
|
||||
// A non-matching ID returns false.
|
||||
id2 := NewUIDSpace("different-prefix").New()
|
||||
if got, want := s.Older(id2, time.Second), false; got != want {
|
||||
t.Errorf("got %t, want %t", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user