+39
@@ -0,0 +1,39 @@
|
||||
// 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 internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ProdAddr = "logging.googleapis.com:443"
|
||||
Version = "0.2.0"
|
||||
)
|
||||
|
||||
func LogPath(parent, logID string) string {
|
||||
logID = strings.Replace(logID, "/", "%2F", -1)
|
||||
return fmt.Sprintf("%s/logs/%s", parent, logID)
|
||||
}
|
||||
|
||||
func LogIDFromPath(parent, path string) string {
|
||||
start := len(parent) + len("/logs/")
|
||||
if len(path) < start {
|
||||
return ""
|
||||
}
|
||||
logID := path[start:]
|
||||
return strings.Replace(logID, "%2F", "/", -1)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
)
|
||||
|
||||
// Compare two payloads, assuming they are both proto.Messages
|
||||
// or both strings.
|
||||
func PayloadEqual(a, b interface{}) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
switch a := a.(type) {
|
||||
case proto.Message:
|
||||
return proto.Equal(a, b.(proto.Message))
|
||||
case string:
|
||||
return a == b.(string)
|
||||
default:
|
||||
panic(fmt.Sprintf("payloadEqual: unexpected type %T", a))
|
||||
}
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
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"
|
||||
"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 IDs that WriteLogEntries will accept.
|
||||
// Important for testing Ping.
|
||||
const (
|
||||
validProjectID = "PROJECT_ID"
|
||||
validOrgID = "433637338589"
|
||||
)
|
||||
|
||||
// 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+"/") && !strings.HasPrefix(req.LogName, "organizations/"+validOrgID+"/") {
|
||||
return nil, fmt.Errorf("bad LogName: %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 := testutil.PageBounds(int(req.PageSize), req.PageToken, len(entries))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListLogEntriesResponse{
|
||||
Entries: entries[from:to],
|
||||
NextPageToken: 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
|
||||
}
|
||||
|
||||
// Lists logs.
|
||||
func (h *loggingHandler) ListLogs(_ context.Context, req *logpb.ListLogsRequest) (*logpb.ListLogsResponse, error) {
|
||||
// Return fixed, fake response.
|
||||
logNames := []string{"a", "b", "c"}
|
||||
from, to, npt, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(logNames))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var lns []string
|
||||
for _, ln := range logNames[from:to] {
|
||||
lns = append(lns, req.Parent+"/logs/"+ln)
|
||||
}
|
||||
return &logpb.ListLogsResponse{
|
||||
LogNames: lns,
|
||||
NextPageToken: npt,
|
||||
}, 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 := testutil.PageBounds(int(req.PageSize), req.PageToken, len(sinks))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListSinksResponse{
|
||||
Sinks: sinks[from:to],
|
||||
NextPageToken: nextPageToken,
|
||||
}, 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 := testutil.PageBounds(int(req.PageSize), req.PageToken, len(metrics))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &logpb.ListLogMetricsResponse{
|
||||
Metrics: metrics[from:to],
|
||||
NextPageToken: nextPageToken,
|
||||
}, 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)
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
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 (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
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 !logEntriesEqual(got, want) {
|
||||
t.Errorf("%q: got %v, want %v", test.orderBy, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logEntriesEqual(a, b []*logpb.LogEntry) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, aa := range a {
|
||||
if !proto.Equal(aa, b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user