Switch to using the dep tool and update all the dependencies
This commit is contained in:
+408
@@ -0,0 +1,408 @@
|
||||
/*
|
||||
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 testing provides support for testing the logging client.
|
||||
package testing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
emptypb "github.com/golang/protobuf/ptypes/empty"
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
context "golang.org/x/net/context"
|
||||
lpb "google.golang.org/genproto/googleapis/api/label"
|
||||
mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
|
||||
logpb "google.golang.org/genproto/googleapis/logging/v2"
|
||||
)
|
||||
|
||||
type loggingHandler struct {
|
||||
logpb.LoggingServiceV2Server
|
||||
|
||||
mu sync.Mutex
|
||||
logs map[string][]*logpb.LogEntry // indexed by log name
|
||||
}
|
||||
|
||||
type configHandler struct {
|
||||
logpb.ConfigServiceV2Server
|
||||
|
||||
mu sync.Mutex
|
||||
sinks map[string]*logpb.LogSink // indexed by (full) sink name
|
||||
}
|
||||
|
||||
type metricHandler struct {
|
||||
logpb.MetricsServiceV2Server
|
||||
|
||||
mu sync.Mutex
|
||||
metrics map[string]*logpb.LogMetric // indexed by (full) metric name
|
||||
}
|
||||
|
||||
// NewServer creates a new in-memory fake server implementing the logging service.
|
||||
// It returns the address of the server.
|
||||
func NewServer() (string, error) {
|
||||
srv, err := testutil.NewServer()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logpb.RegisterLoggingServiceV2Server(srv.Gsrv, &loggingHandler{
|
||||
logs: make(map[string][]*logpb.LogEntry),
|
||||
})
|
||||
logpb.RegisterConfigServiceV2Server(srv.Gsrv, &configHandler{
|
||||
sinks: make(map[string]*logpb.LogSink),
|
||||
})
|
||||
logpb.RegisterMetricsServiceV2Server(srv.Gsrv, &metricHandler{
|
||||
metrics: make(map[string]*logpb.LogMetric),
|
||||
})
|
||||
srv.Start()
|
||||
return srv.Addr, nil
|
||||
}
|
||||
|
||||
// DeleteLog deletes a log and all its log entries. The log will reappear if it
|
||||
// receives new entries.
|
||||
func (h *loggingHandler) DeleteLog(_ context.Context, req *logpb.DeleteLogRequest) (*emptypb.Empty, error) {
|
||||
// TODO(jba): return NotFound if log isn't there?
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.logs, req.LogName)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// The only project ID that WriteLogEntries will accept.
|
||||
// Important for testing Ping.
|
||||
const validProjectID = "PROJECT_ID"
|
||||
|
||||
// WriteLogEntries writes log entries to Stackdriver Logging. All log entries in
|
||||
// Stackdriver Logging are written by this method.
|
||||
func (h *loggingHandler) WriteLogEntries(_ context.Context, req *logpb.WriteLogEntriesRequest) (*logpb.WriteLogEntriesResponse, error) {
|
||||
if !strings.HasPrefix(req.LogName, "projects/"+validProjectID+"/") {
|
||||
return nil, fmt.Errorf("bad project ID: %q", req.LogName)
|
||||
}
|
||||
// TODO(jba): support insertId?
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for _, e := range req.Entries {
|
||||
// Assign timestamp if missing.
|
||||
if e.Timestamp == nil {
|
||||
e.Timestamp = &tspb.Timestamp{Seconds: time.Now().Unix(), Nanos: 0}
|
||||
}
|
||||
// Fill from common fields in request.
|
||||
if e.LogName == "" {
|
||||
e.LogName = req.LogName
|
||||
}
|
||||
if e.Resource == nil {
|
||||
// TODO(jba): use a global one if nil?
|
||||
e.Resource = req.Resource
|
||||
}
|
||||
for k, v := range req.Labels {
|
||||
if _, ok := e.Labels[k]; !ok {
|
||||
e.Labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Store by log name.
|
||||
h.logs[e.LogName] = append(h.logs[e.LogName], e)
|
||||
}
|
||||
return &logpb.WriteLogEntriesResponse{}, nil
|
||||
}
|
||||
|
||||
// ListLogEntries lists log entries. Use this method to retrieve log entries
|
||||
// from Stackdriver Logging.
|
||||
//
|
||||
// This fake implementation ignores project IDs. It does not support full filtering, only
|
||||
// expressions of the form "logName = NAME".
|
||||
func (h *loggingHandler) ListLogEntries(_ context.Context, req *logpb.ListLogEntriesRequest) (*logpb.ListLogEntriesResponse, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
entries, err := h.filterEntries(req.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = sortEntries(entries, req.OrderBy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
from, to, nextPageToken, err := getPage(int(req.PageSize), req.PageToken, len(entries))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListLogEntriesResponse{
|
||||
Entries: entries[from:to],
|
||||
NextPageToken: nextPageToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getPage converts an incoming page size and token from an RPC request into
|
||||
// slice bounds and the outgoing next-page token.
|
||||
//
|
||||
// getPage assumes that the complete, unpaginated list of items exists as a
|
||||
// single slice. In addition to the page size and token, getPage needs the
|
||||
// length of that slice.
|
||||
//
|
||||
// getPage'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 getPage(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, "", invalidArgument("bad page token")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (h *loggingHandler) filterEntries(filter string) ([]*logpb.LogEntry, error) {
|
||||
logName, err := parseFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logName != "" {
|
||||
return h.logs[logName], nil
|
||||
}
|
||||
var entries []*logpb.LogEntry
|
||||
for _, es := range h.logs {
|
||||
entries = append(entries, es...)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
var filterRegexp = regexp.MustCompile(`^logName\s*=\s*"?([-_/.%\w]+)"?$`)
|
||||
|
||||
// returns the log name, or "" for the empty filter
|
||||
func parseFilter(filter string) (string, error) {
|
||||
if filter == "" {
|
||||
return "", nil
|
||||
}
|
||||
subs := filterRegexp.FindStringSubmatch(filter)
|
||||
if subs == nil {
|
||||
return "", invalidArgument("bad filter")
|
||||
}
|
||||
return subs[1], nil // cannot panic by construction of regexp
|
||||
}
|
||||
|
||||
func sortEntries(entries []*logpb.LogEntry, orderBy string) error {
|
||||
switch orderBy {
|
||||
case "", "timestamp asc":
|
||||
sort.Sort(byTimestamp(entries))
|
||||
return nil
|
||||
|
||||
case "timestamp desc":
|
||||
sort.Sort(sort.Reverse(byTimestamp(entries)))
|
||||
return nil
|
||||
|
||||
default:
|
||||
return invalidArgument("bad order_by")
|
||||
}
|
||||
}
|
||||
|
||||
type byTimestamp []*logpb.LogEntry
|
||||
|
||||
func (s byTimestamp) Len() int { return len(s) }
|
||||
func (s byTimestamp) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byTimestamp) Less(i, j int) bool {
|
||||
c := compareTimestamps(s[i].Timestamp, s[j].Timestamp)
|
||||
switch {
|
||||
case c < 0:
|
||||
return true
|
||||
case c > 0:
|
||||
return false
|
||||
default:
|
||||
return s[i].InsertId < s[j].InsertId
|
||||
}
|
||||
}
|
||||
|
||||
func compareTimestamps(ts1, ts2 *tspb.Timestamp) int64 {
|
||||
if ts1.Seconds != ts2.Seconds {
|
||||
return ts1.Seconds - ts2.Seconds
|
||||
}
|
||||
return int64(ts1.Nanos - ts2.Nanos)
|
||||
}
|
||||
|
||||
// Lists monitored resource descriptors that are used by Stackdriver Logging.
|
||||
func (h *loggingHandler) ListMonitoredResourceDescriptors(context.Context, *logpb.ListMonitoredResourceDescriptorsRequest) (*logpb.ListMonitoredResourceDescriptorsResponse, error) {
|
||||
return &logpb.ListMonitoredResourceDescriptorsResponse{
|
||||
ResourceDescriptors: []*mrpb.MonitoredResourceDescriptor{
|
||||
{
|
||||
Type: "global",
|
||||
DisplayName: "Global",
|
||||
Description: "... a log is not associated with any specific resource.",
|
||||
Labels: []*lpb.LabelDescriptor{
|
||||
{Key: "project_id", Description: "The identifier of the GCP project..."},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Gets a sink.
|
||||
func (h *configHandler) GetSink(_ context.Context, req *logpb.GetSinkRequest) (*logpb.LogSink, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if s, ok := h.sinks[req.SinkName]; ok {
|
||||
return s, nil
|
||||
}
|
||||
// TODO(jba): use error codes
|
||||
return nil, fmt.Errorf("sink %q not found", req.SinkName)
|
||||
}
|
||||
|
||||
// Creates a sink.
|
||||
func (h *configHandler) CreateSink(_ context.Context, req *logpb.CreateSinkRequest) (*logpb.LogSink, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
fullName := fmt.Sprintf("%s/sinks/%s", req.Parent, req.Sink.Name)
|
||||
if _, ok := h.sinks[fullName]; ok {
|
||||
return nil, fmt.Errorf("sink with name %q already exists", fullName)
|
||||
}
|
||||
h.sinks[fullName] = req.Sink
|
||||
return req.Sink, nil
|
||||
}
|
||||
|
||||
// Creates or updates a sink.
|
||||
func (h *configHandler) UpdateSink(_ context.Context, req *logpb.UpdateSinkRequest) (*logpb.LogSink, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
// Update of a non-existent sink will create it.
|
||||
h.sinks[req.SinkName] = req.Sink
|
||||
return req.Sink, nil
|
||||
}
|
||||
|
||||
// Deletes a sink.
|
||||
func (h *configHandler) DeleteSink(_ context.Context, req *logpb.DeleteSinkRequest) (*emptypb.Empty, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.sinks, req.SinkName)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Lists sinks. This fake implementation ignores the Parent field of
|
||||
// ListSinksRequest. All sinks are listed, regardless of their project.
|
||||
func (h *configHandler) ListSinks(_ context.Context, req *logpb.ListSinksRequest) (*logpb.ListSinksResponse, error) {
|
||||
h.mu.Lock()
|
||||
var sinks []*logpb.LogSink
|
||||
for _, s := range h.sinks {
|
||||
sinks = append(sinks, s)
|
||||
}
|
||||
h.mu.Unlock() // safe because no *logpb.LogSink is ever modified
|
||||
// Since map iteration varies, sort the sinks.
|
||||
sort.Sort(sinksByName(sinks))
|
||||
from, to, nextPageToken, err := getPage(int(req.PageSize), req.PageToken, len(sinks))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListSinksResponse{
|
||||
Sinks: sinks[from:to],
|
||||
NextPageToken: nextPageToken,
|
||||
}, nil
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type sinksByName []*logpb.LogSink
|
||||
|
||||
func (s sinksByName) Len() int { return len(s) }
|
||||
func (s sinksByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s sinksByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
// Gets a metric.
|
||||
func (h *metricHandler) GetLogMetric(_ context.Context, req *logpb.GetLogMetricRequest) (*logpb.LogMetric, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if s, ok := h.metrics[req.MetricName]; ok {
|
||||
return s, nil
|
||||
}
|
||||
// TODO(jba): use error codes
|
||||
return nil, fmt.Errorf("metric %q not found", req.MetricName)
|
||||
}
|
||||
|
||||
// Creates a metric.
|
||||
func (h *metricHandler) CreateLogMetric(_ context.Context, req *logpb.CreateLogMetricRequest) (*logpb.LogMetric, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
fullName := fmt.Sprintf("%s/metrics/%s", req.Parent, req.Metric.Name)
|
||||
if _, ok := h.metrics[fullName]; ok {
|
||||
return nil, fmt.Errorf("metric with name %q already exists", fullName)
|
||||
}
|
||||
h.metrics[fullName] = req.Metric
|
||||
return req.Metric, nil
|
||||
}
|
||||
|
||||
// Creates or updates a metric.
|
||||
func (h *metricHandler) UpdateLogMetric(_ context.Context, req *logpb.UpdateLogMetricRequest) (*logpb.LogMetric, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
// Update of a non-existent metric will create it.
|
||||
h.metrics[req.MetricName] = req.Metric
|
||||
return req.Metric, nil
|
||||
}
|
||||
|
||||
// Deletes a metric.
|
||||
func (h *metricHandler) DeleteLogMetric(_ context.Context, req *logpb.DeleteLogMetricRequest) (*emptypb.Empty, error) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.metrics, req.MetricName)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// Lists metrics. This fake implementation ignores the Parent field of
|
||||
// ListMetricsRequest. All metrics are listed, regardless of their project.
|
||||
func (h *metricHandler) ListLogMetrics(_ context.Context, req *logpb.ListLogMetricsRequest) (*logpb.ListLogMetricsResponse, error) {
|
||||
h.mu.Lock()
|
||||
var metrics []*logpb.LogMetric
|
||||
for _, s := range h.metrics {
|
||||
metrics = append(metrics, s)
|
||||
}
|
||||
h.mu.Unlock() // safe because no *logpb.LogMetric is ever modified
|
||||
// Since map iteration varies, sort the metrics.
|
||||
sort.Sort(metricsByName(metrics))
|
||||
from, to, nextPageToken, err := getPage(int(req.PageSize), req.PageToken, len(metrics))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListLogMetricsResponse{
|
||||
Metrics: metrics[from:to],
|
||||
NextPageToken: nextPageToken,
|
||||
}, nil
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type metricsByName []*logpb.LogMetric
|
||||
|
||||
func (s metricsByName) Len() int { return len(s) }
|
||||
func (s metricsByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s metricsByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
func invalidArgument(msg string) error {
|
||||
// TODO(jba): status codes
|
||||
return errors.New(msg)
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// This file contains only basic checks. The fake is effectively tested by the
|
||||
// logging client unit tests.
|
||||
|
||||
package testing
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
logpb "google.golang.org/genproto/googleapis/logging/v2"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestNewServer(t *testing.T) {
|
||||
// Confirm that we can create and use a working gRPC server.
|
||||
addr, err := NewServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn, err := grpc.Dial(addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Avoid "connection is closing; please retry" message from gRPC.
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestParseFilter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
filter string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{"", "", false},
|
||||
{"logName = syslog", "syslog", false},
|
||||
{"logname = syslog", "", true},
|
||||
{"logName = 'syslog'", "", true},
|
||||
{"logName == syslog", "", true},
|
||||
} {
|
||||
got, err := parseFilter(test.filter)
|
||||
if err != nil {
|
||||
if !test.wantErr {
|
||||
t.Errorf("%q: got %v, want no error", test.filter, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if test.wantErr {
|
||||
t.Errorf("%q: got no error, want one", test.filter)
|
||||
continue
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("%q: got %q, want %q", test.filter, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortEntries(t *testing.T) {
|
||||
entries := []*logpb.LogEntry{
|
||||
/* 0 */ {Timestamp: &tspb.Timestamp{Seconds: 30}},
|
||||
/* 1 */ {Timestamp: &tspb.Timestamp{Seconds: 10}},
|
||||
/* 2 */ {Timestamp: &tspb.Timestamp{Seconds: 20}, InsertId: "b"},
|
||||
/* 3 */ {Timestamp: &tspb.Timestamp{Seconds: 20}, InsertId: "a"},
|
||||
/* 4 */ {Timestamp: &tspb.Timestamp{Seconds: 20}, InsertId: "c"},
|
||||
}
|
||||
for _, test := range []struct {
|
||||
orderBy string
|
||||
want []int // slice of index into entries; nil == error
|
||||
}{
|
||||
{"", []int{1, 3, 2, 4, 0}},
|
||||
{"timestamp asc", []int{1, 3, 2, 4, 0}},
|
||||
{"timestamp desc", []int{0, 4, 2, 3, 1}},
|
||||
{"something else", nil},
|
||||
} {
|
||||
got := make([]*logpb.LogEntry, len(entries))
|
||||
copy(got, entries)
|
||||
err := sortEntries(got, test.orderBy)
|
||||
if err != nil {
|
||||
if test.want != nil {
|
||||
t.Errorf("%q: got %v, want nil error", test.orderBy, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
want := make([]*logpb.LogEntry, len(entries))
|
||||
for i, j := range test.want {
|
||||
want[i] = entries[j]
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%q: got %v, want %v", test.orderBy, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// 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.
|
||||
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
startTime = time.Now()
|
||||
uniqueIDCounter int
|
||||
// Items older than expiredAge are remnants from previous tests and can be deleted.
|
||||
expiredAge = 24 * time.Hour
|
||||
)
|
||||
|
||||
// UniqueID generates unique IDs so tests don't interfere with each other.
|
||||
// All unique IDs generated in the same test execution will have the same timestamp.
|
||||
func UniqueID(prefix string) string {
|
||||
uniqueIDCounter++
|
||||
// Zero-pad the counter for lexical sort order.
|
||||
return fmt.Sprintf("%s-t%d-%04d", prefix, startTime.UnixNano(), uniqueIDCounter)
|
||||
}
|
||||
|
||||
// ExpiredUniqueIDs returns a subset of ids that are unique IDs as generated by
|
||||
// UniqueID(prefix) and are older than expiredAge.
|
||||
func ExpiredUniqueIDs(ids []string, prefix string) []string {
|
||||
var expired []string
|
||||
for _, id := range ids {
|
||||
t, ok := extractTime(id, prefix)
|
||||
if ok && time.Since(t) > expiredAge {
|
||||
expired = append(expired, id)
|
||||
}
|
||||
}
|
||||
return expired
|
||||
}
|
||||
|
||||
// extractTime extracts the timestamp of s, which must begin with prefix and
|
||||
// match the form generated by uniqueID. The second return value is true on
|
||||
// success, false if there was a problem.
|
||||
func extractTime(s, prefix string) (time.Time, bool) {
|
||||
if !strings.HasPrefix(s, prefix+"-t") {
|
||||
return time.Time{}, false
|
||||
}
|
||||
s = s[len(prefix)+2:]
|
||||
i := strings.Index(s, "-")
|
||||
if i < 0 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(s[:i], 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
return time.Unix(0, nanos), true
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// 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.
|
||||
|
||||
// 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 testing
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExtractTime(t *testing.T) {
|
||||
uid := UniqueID("unique-ID")
|
||||
got, ok := extractTime(uid, "unique-ID")
|
||||
if !ok {
|
||||
t.Fatal("got ok = false, want true")
|
||||
}
|
||||
if !startTime.Equal(got) {
|
||||
t.Errorf("got %s, want %s", got, startTime)
|
||||
}
|
||||
|
||||
got, ok = extractTime("p-t0-doesnotmatter", "p")
|
||||
if !ok {
|
||||
t.Fatal("got false, want true")
|
||||
}
|
||||
if want := time.Unix(0, 0); !want.Equal(got) {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
if _, ok = extractTime("invalid-time-1234", "invalid"); ok {
|
||||
t.Error("got true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiredUniqueIDs(t *testing.T) {
|
||||
const prefix = "uid"
|
||||
// The freshly unique IDs will have startTime as their timestamp.
|
||||
uids := []string{UniqueID(prefix), "uid-tinvalid-1234", UniqueID(prefix), "uid-t0-1111"}
|
||||
|
||||
// This test hasn't been running for very long, so only the last ID is expired.
|
||||
got := ExpiredUniqueIDs(uids, prefix)
|
||||
want := []string{uids[3]}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
prev := expiredAge
|
||||
expiredAge = 10 * time.Millisecond
|
||||
defer func() { expiredAge = prev }()
|
||||
// This test has been running for at least 10ms, so all but the invalid ID have expired.
|
||||
got = ExpiredUniqueIDs(uids, prefix)
|
||||
want = []string{uids[0], uids[2], uids[3]}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user