api: creates versioned package for prometheus v1 api
This commit creates a new package to hold the prometheus v1 API interface. This interface will contain all the funcionality exposed by Prometheus v1 HTTP API. The underlying http client is kept on the api package since it may be reused across diferent API versions and also by the Alertmanager api package (to come.)
This commit is contained in:
parent
7b5f0fdaa9
commit
349922b38c
|
@ -0,0 +1,136 @@
|
||||||
|
// Copyright 2015 The Prometheus Authors
|
||||||
|
// 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 api provides clients the HTTP API's.
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/context"
|
||||||
|
"golang.org/x/net/context/ctxhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CancelableTransport is like net.Transport but provides
|
||||||
|
// per-request cancelation functionality.
|
||||||
|
type CancelableTransport interface {
|
||||||
|
http.RoundTripper
|
||||||
|
CancelRequest(req *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultTransport is used if no Transport is set in Config.
|
||||||
|
var DefaultTransport CancelableTransport = &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
Dial: (&net.Dialer{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
KeepAlive: 30 * time.Second,
|
||||||
|
}).Dial,
|
||||||
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config defines configuration parameters for a new client.
|
||||||
|
type Config struct {
|
||||||
|
// The address of the Prometheus to connect to.
|
||||||
|
Address string
|
||||||
|
|
||||||
|
// Transport is used by the Client to drive HTTP requests. If not
|
||||||
|
// provided, DefaultTransport will be used.
|
||||||
|
Transport CancelableTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) transport() CancelableTransport {
|
||||||
|
if cfg.Transport == nil {
|
||||||
|
return DefaultTransport
|
||||||
|
}
|
||||||
|
return cfg.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the interface for an API client.
|
||||||
|
type Client interface {
|
||||||
|
URL(ep string, args map[string]string) *url.URL
|
||||||
|
Do(context.Context, *http.Request) (*http.Response, []byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Client.
|
||||||
|
//
|
||||||
|
// It is safe to use the returned Client from multiple goroutines.
|
||||||
|
func New(cfg Config) (Client, error) {
|
||||||
|
u, err := url.Parse(cfg.Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
u.Path = strings.TrimRight(u.Path, "/")
|
||||||
|
|
||||||
|
return &httpClient{
|
||||||
|
endpoint: u,
|
||||||
|
transport: cfg.transport(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpClient struct {
|
||||||
|
endpoint *url.URL
|
||||||
|
transport CancelableTransport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpClient) URL(ep string, args map[string]string) *url.URL {
|
||||||
|
p := path.Join(c.endpoint.Path, ep)
|
||||||
|
|
||||||
|
for arg, val := range args {
|
||||||
|
arg = ":" + arg
|
||||||
|
p = strings.Replace(p, arg, val, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
u := *c.endpoint
|
||||||
|
u.Path = p
|
||||||
|
|
||||||
|
return &u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *httpClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
||||||
|
resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if resp != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
body, err = ioutil.ReadAll(resp.Body)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
err = resp.Body.Close()
|
||||||
|
<-done
|
||||||
|
if err == nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, body, err
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright 2015 The Prometheus Authors
|
||||||
|
// 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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
c := Config{}
|
||||||
|
if c.transport() != DefaultTransport {
|
||||||
|
t.Fatalf("expected default transport for nil Transport field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
address string
|
||||||
|
endpoint string
|
||||||
|
args map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "/test",
|
||||||
|
expected: "http://localhost:9090/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost",
|
||||||
|
endpoint: "/test",
|
||||||
|
expected: "http://localhost/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "test",
|
||||||
|
expected: "http://localhost:9090/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090/prefix",
|
||||||
|
endpoint: "/test",
|
||||||
|
expected: "http://localhost:9090/prefix/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "https://localhost:9090/",
|
||||||
|
endpoint: "/test/",
|
||||||
|
expected: "https://localhost:9090/test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "/test/:param",
|
||||||
|
args: map[string]string{
|
||||||
|
"param": "content",
|
||||||
|
},
|
||||||
|
expected: "http://localhost:9090/test/content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "/test/:param/more/:param",
|
||||||
|
args: map[string]string{
|
||||||
|
"param": "content",
|
||||||
|
},
|
||||||
|
expected: "http://localhost:9090/test/content/more/content",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "/test/:param/more/:foo",
|
||||||
|
args: map[string]string{
|
||||||
|
"param": "content",
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
expected: "http://localhost:9090/test/content/more/bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
address: "http://localhost:9090",
|
||||||
|
endpoint: "/test/:param",
|
||||||
|
args: map[string]string{
|
||||||
|
"nonexistant": "content",
|
||||||
|
},
|
||||||
|
expected: "http://localhost:9090/test/:param",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
ep, err := url.Parse(test.address)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hclient := &httpClient{
|
||||||
|
endpoint: ep,
|
||||||
|
transport: DefaultTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
u := hclient.URL(test.endpoint, test.args)
|
||||||
|
if u.String() != test.expected {
|
||||||
|
t.Errorf("unexpected result: got %s, want %s", u, test.expected)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2015 The Prometheus Authors
|
// Copyright 2017 The Prometheus Authors
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
|
@ -11,35 +11,32 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
// Package prometheus provides bindings to the Prometheus HTTP API:
|
// Package v1 provides bindings to the Prometheus HTTP API v1:
|
||||||
// http://prometheus.io/docs/querying/api/
|
// http://prometheus.io/docs/querying/api/
|
||||||
package prometheus
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"golang.org/x/net/context/ctxhttp"
|
|
||||||
|
"github.com/prometheus/client_golang/api"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
statusAPIError = 422
|
statusAPIError = 422
|
||||||
apiPrefix = "/api/v1"
|
|
||||||
|
|
||||||
epQuery = "/query"
|
apiPrefix = "/api/v1"
|
||||||
epQueryRange = "/query_range"
|
|
||||||
epLabelValues = "/label/:name/values"
|
epQuery = apiPrefix + "/query"
|
||||||
epSeries = "/series"
|
epQueryRange = apiPrefix + "/query_range"
|
||||||
|
epLabelValues = apiPrefix + "/label/:name/values"
|
||||||
|
epSeries = apiPrefix + "/series"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrorType models the different API error types.
|
// ErrorType models the different API error types.
|
||||||
|
@ -64,168 +61,6 @@ func (e *Error) Error() string {
|
||||||
return fmt.Sprintf("%s: %s", e.Type, e.Msg)
|
return fmt.Sprintf("%s: %s", e.Type, e.Msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CancelableTransport is like net.Transport but provides
|
|
||||||
// per-request cancelation functionality.
|
|
||||||
type CancelableTransport interface {
|
|
||||||
http.RoundTripper
|
|
||||||
CancelRequest(req *http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultTransport is used if no Transport is set in Config.
|
|
||||||
var DefaultTransport CancelableTransport = &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: (&net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).Dial,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config defines configuration parameters for a new client.
|
|
||||||
type Config struct {
|
|
||||||
// The address of the Prometheus to connect to.
|
|
||||||
Address string
|
|
||||||
|
|
||||||
// Transport is used by the Client to drive HTTP requests. If not
|
|
||||||
// provided, DefaultTransport will be used.
|
|
||||||
Transport CancelableTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) transport() CancelableTransport {
|
|
||||||
if cfg.Transport == nil {
|
|
||||||
return DefaultTransport
|
|
||||||
}
|
|
||||||
return cfg.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client is the interface for an API client.
|
|
||||||
type Client interface {
|
|
||||||
url(ep string, args map[string]string) *url.URL
|
|
||||||
do(context.Context, *http.Request) (*http.Response, []byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New returns a new Client.
|
|
||||||
//
|
|
||||||
// It is safe to use the returned Client from multiple goroutines.
|
|
||||||
func New(cfg Config) (Client, error) {
|
|
||||||
u, err := url.Parse(cfg.Address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
u.Path = strings.TrimRight(u.Path, "/") + apiPrefix
|
|
||||||
|
|
||||||
return &httpClient{
|
|
||||||
endpoint: u,
|
|
||||||
transport: cfg.transport(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type httpClient struct {
|
|
||||||
endpoint *url.URL
|
|
||||||
transport CancelableTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *httpClient) url(ep string, args map[string]string) *url.URL {
|
|
||||||
p := path.Join(c.endpoint.Path, ep)
|
|
||||||
|
|
||||||
for arg, val := range args {
|
|
||||||
arg = ":" + arg
|
|
||||||
p = strings.Replace(p, arg, val, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
u := *c.endpoint
|
|
||||||
u.Path = p
|
|
||||||
|
|
||||||
return &u
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *httpClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
|
||||||
resp, err := ctxhttp.Do(ctx, &http.Client{Transport: c.transport}, req)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if resp != nil {
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var body []byte
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
body, err = ioutil.ReadAll(resp.Body)
|
|
||||||
close(done)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
err = resp.Body.Close()
|
|
||||||
<-done
|
|
||||||
if err == nil {
|
|
||||||
err = ctx.Err()
|
|
||||||
}
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// apiClient wraps a regular client and processes successful API responses.
|
|
||||||
// Successful also includes responses that errored at the API level.
|
|
||||||
type apiClient struct {
|
|
||||||
Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Data json.RawMessage `json:"data"`
|
|
||||||
ErrorType ErrorType `json:"errorType"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c apiClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
|
||||||
resp, body, err := c.Client.do(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return resp, body, err
|
|
||||||
}
|
|
||||||
|
|
||||||
code := resp.StatusCode
|
|
||||||
|
|
||||||
if code/100 != 2 && code != statusAPIError {
|
|
||||||
return resp, body, &Error{
|
|
||||||
Type: ErrBadResponse,
|
|
||||||
Msg: fmt.Sprintf("bad response code %d", resp.StatusCode),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result apiResponse
|
|
||||||
|
|
||||||
if err = json.Unmarshal(body, &result); err != nil {
|
|
||||||
return resp, body, &Error{
|
|
||||||
Type: ErrBadResponse,
|
|
||||||
Msg: err.Error(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code == statusAPIError) != (result.Status == "error") {
|
|
||||||
err = &Error{
|
|
||||||
Type: ErrBadResponse,
|
|
||||||
Msg: "inconsistent body for response code",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if code == statusAPIError && result.Status == "error" {
|
|
||||||
err = &Error{
|
|
||||||
Type: result.ErrorType,
|
|
||||||
Msg: result.Error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, []byte(result.Data), err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range represents a sliced time range.
|
// Range represents a sliced time range.
|
||||||
type Range struct {
|
type Range struct {
|
||||||
// The boundaries of the time range.
|
// The boundaries of the time range.
|
||||||
|
@ -234,6 +69,16 @@ type Range struct {
|
||||||
Step time.Duration
|
Step time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API provides bindings the Prometheus's v1 API.
|
||||||
|
type API interface {
|
||||||
|
// Query performs a query for the given time.
|
||||||
|
Query(ctx context.Context, query string, ts time.Time) (model.Value, error)
|
||||||
|
// QueryRange performs a query for the given range.
|
||||||
|
QueryRange(ctx context.Context, query string, r Range) (model.Value, error)
|
||||||
|
// LabelValues performs a query for the values of the given label.
|
||||||
|
LabelValues(ctx context.Context, label string) (model.LabelValues, error)
|
||||||
|
}
|
||||||
|
|
||||||
// queryResult contains result data for a query.
|
// queryResult contains result data for a query.
|
||||||
type queryResult struct {
|
type queryResult struct {
|
||||||
Type model.ValueType `json:"resultType"`
|
Type model.ValueType `json:"resultType"`
|
||||||
|
@ -276,29 +121,19 @@ func (qr *queryResult) UnmarshalJSON(b []byte) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryAPI provides bindings the Prometheus's query API.
|
// NewAPI returns a new API for the client.
|
||||||
type QueryAPI interface {
|
|
||||||
// Query performs a query for the given time.
|
|
||||||
Query(ctx context.Context, query string, ts time.Time) (model.Value, error)
|
|
||||||
// QueryRange performs a query for the given range.
|
|
||||||
QueryRange(ctx context.Context, query string, r Range) (model.Value, error)
|
|
||||||
// LabelValues performs a query for the values of the given label.
|
|
||||||
LabelValues(ctx context.Context, label string) (model.LabelValues, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewQueryAPI returns a new QueryAPI for the client.
|
|
||||||
//
|
//
|
||||||
// It is safe to use the returned QueryAPI from multiple goroutines.
|
// It is safe to use the returned API from multiple goroutines.
|
||||||
func NewQueryAPI(c Client) QueryAPI {
|
func NewAPI(c api.Client) API {
|
||||||
return &httpQueryAPI{client: apiClient{c}}
|
return &httpAPI{client: apiClient{c}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpQueryAPI struct {
|
type httpAPI struct {
|
||||||
client Client
|
client api.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) {
|
func (h *httpAPI) Query(ctx context.Context, query string, ts time.Time) (model.Value, error) {
|
||||||
u := h.client.url(epQuery, nil)
|
u := h.client.URL(epQuery, nil)
|
||||||
q := u.Query()
|
q := u.Query()
|
||||||
|
|
||||||
q.Set("query", query)
|
q.Set("query", query)
|
||||||
|
@ -311,7 +146,7 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, body, err := h.client.do(ctx, req)
|
_, body, err := h.client.Do(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -322,8 +157,8 @@ func (h *httpQueryAPI) Query(ctx context.Context, query string, ts time.Time) (m
|
||||||
return model.Value(qres.v), err
|
return model.Value(qres.v), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) {
|
func (h *httpAPI) QueryRange(ctx context.Context, query string, r Range) (model.Value, error) {
|
||||||
u := h.client.url(epQueryRange, nil)
|
u := h.client.URL(epQueryRange, nil)
|
||||||
q := u.Query()
|
q := u.Query()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -344,7 +179,7 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, body, err := h.client.do(ctx, req)
|
_, body, err := h.client.Do(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -355,13 +190,13 @@ func (h *httpQueryAPI) QueryRange(ctx context.Context, query string, r Range) (m
|
||||||
return model.Value(qres.v), err
|
return model.Value(qres.v), err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *httpQueryAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) {
|
func (h *httpAPI) LabelValues(ctx context.Context, label string) (model.LabelValues, error) {
|
||||||
u := h.client.url(epLabelValues, map[string]string{"name": label})
|
u := h.client.URL(epLabelValues, map[string]string{"name": label})
|
||||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, body, err := h.client.do(ctx, req)
|
_, body, err := h.client.Do(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -369,3 +204,57 @@ func (h *httpQueryAPI) LabelValues(ctx context.Context, label string) (model.Lab
|
||||||
err = json.Unmarshal(body, &labelValues)
|
err = json.Unmarshal(body, &labelValues)
|
||||||
return labelValues, err
|
return labelValues, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apiClient wraps a regular client and processes successful API responses.
|
||||||
|
// Successful also includes responses that errored at the API level.
|
||||||
|
type apiClient struct {
|
||||||
|
api.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
ErrorType ErrorType `json:"errorType"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c apiClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
||||||
|
resp, body, err := c.Client.Do(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return resp, body, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := resp.StatusCode
|
||||||
|
|
||||||
|
if code/100 != 2 && code != statusAPIError {
|
||||||
|
return resp, body, &Error{
|
||||||
|
Type: ErrBadResponse,
|
||||||
|
Msg: fmt.Sprintf("bad response code %d", resp.StatusCode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result apiResponse
|
||||||
|
|
||||||
|
if err = json.Unmarshal(body, &result); err != nil {
|
||||||
|
return resp, body, &Error{
|
||||||
|
Type: ErrBadResponse,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == statusAPIError) != (result.Status == "error") {
|
||||||
|
err = &Error{
|
||||||
|
Type: ErrBadResponse,
|
||||||
|
Msg: "inconsistent body for response code",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == statusAPIError && result.Status == "error" {
|
||||||
|
err = &Error{
|
||||||
|
Type: result.ErrorType,
|
||||||
|
Msg: result.Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, []byte(result.Data), err
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2015 The Prometheus Authors
|
// Copyright 2017 The Prometheus Authors
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package prometheus
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
@ -23,107 +23,190 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/common/model"
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfig(t *testing.T) {
|
type apiTest struct {
|
||||||
c := Config{}
|
do func() (interface{}, error)
|
||||||
if c.transport() != DefaultTransport {
|
inErr error
|
||||||
t.Fatalf("expected default transport for nil Transport field")
|
inRes interface{}
|
||||||
}
|
|
||||||
|
reqPath string
|
||||||
|
reqParam url.Values
|
||||||
|
reqMethod string
|
||||||
|
res interface{}
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClientURL(t *testing.T) {
|
type apiTestClient struct {
|
||||||
tests := []struct {
|
*testing.T
|
||||||
address string
|
curTest apiTest
|
||||||
endpoint string
|
}
|
||||||
args map[string]string
|
|
||||||
expected string
|
func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL {
|
||||||
}{
|
path := ep
|
||||||
|
for k, v := range args {
|
||||||
|
path = strings.Replace(path, ":"+k, v, -1)
|
||||||
|
}
|
||||||
|
u := &url.URL{
|
||||||
|
Host: "test:9090",
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
||||||
|
|
||||||
|
test := c.curTest
|
||||||
|
|
||||||
|
if req.URL.Path != test.reqPath {
|
||||||
|
c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path)
|
||||||
|
}
|
||||||
|
if req.Method != test.reqMethod {
|
||||||
|
c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(test.inRes)
|
||||||
|
if err != nil {
|
||||||
|
c.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &http.Response{}
|
||||||
|
if test.inErr != nil {
|
||||||
|
resp.StatusCode = statusAPIError
|
||||||
|
} else {
|
||||||
|
resp.StatusCode = http.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, b, test.inErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPIs(t *testing.T) {
|
||||||
|
|
||||||
|
testTime := time.Now()
|
||||||
|
|
||||||
|
client := &apiTestClient{T: t}
|
||||||
|
|
||||||
|
queryAPI := &httpAPI{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
doQuery := func(q string, ts time.Time) func() (interface{}, error) {
|
||||||
|
return func() (interface{}, error) {
|
||||||
|
return queryAPI.Query(context.Background(), q, ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doQueryRange := func(q string, rng Range) func() (interface{}, error) {
|
||||||
|
return func() (interface{}, error) {
|
||||||
|
return queryAPI.QueryRange(context.Background(), q, rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doLabelValues := func(label string) func() (interface{}, error) {
|
||||||
|
return func() (interface{}, error) {
|
||||||
|
return queryAPI.LabelValues(context.Background(), label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queryTests := []apiTest{
|
||||||
{
|
{
|
||||||
address: "http://localhost:9090",
|
do: doQuery("2", testTime),
|
||||||
endpoint: "/test",
|
inRes: &queryResult{
|
||||||
expected: "http://localhost:9090/test",
|
Type: model.ValScalar,
|
||||||
},
|
Result: &model.Scalar{
|
||||||
{
|
Value: 2,
|
||||||
address: "http://localhost",
|
Timestamp: model.TimeFromUnix(testTime.Unix()),
|
||||||
endpoint: "/test",
|
},
|
||||||
expected: "http://localhost/test",
|
},
|
||||||
},
|
|
||||||
{
|
reqMethod: "GET",
|
||||||
address: "http://localhost:9090",
|
reqPath: "/api/v1/query",
|
||||||
endpoint: "test",
|
reqParam: url.Values{
|
||||||
expected: "http://localhost:9090/test",
|
"query": []string{"2"},
|
||||||
},
|
"time": []string{testTime.Format(time.RFC3339Nano)},
|
||||||
{
|
},
|
||||||
address: "http://localhost:9090/prefix",
|
res: &model.Scalar{
|
||||||
endpoint: "/test",
|
Value: 2,
|
||||||
expected: "http://localhost:9090/prefix/test",
|
Timestamp: model.TimeFromUnix(testTime.Unix()),
|
||||||
},
|
|
||||||
{
|
|
||||||
address: "https://localhost:9090/",
|
|
||||||
endpoint: "/test/",
|
|
||||||
expected: "https://localhost:9090/test",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
address: "http://localhost:9090",
|
|
||||||
endpoint: "/test/:param",
|
|
||||||
args: map[string]string{
|
|
||||||
"param": "content",
|
|
||||||
},
|
},
|
||||||
expected: "http://localhost:9090/test/content",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
address: "http://localhost:9090",
|
do: doQuery("2", testTime),
|
||||||
endpoint: "/test/:param/more/:param",
|
inErr: fmt.Errorf("some error"),
|
||||||
args: map[string]string{
|
|
||||||
"param": "content",
|
reqMethod: "GET",
|
||||||
|
reqPath: "/api/v1/query",
|
||||||
|
reqParam: url.Values{
|
||||||
|
"query": []string{"2"},
|
||||||
|
"time": []string{testTime.Format(time.RFC3339Nano)},
|
||||||
},
|
},
|
||||||
expected: "http://localhost:9090/test/content/more/content",
|
err: fmt.Errorf("some error"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
address: "http://localhost:9090",
|
do: doQueryRange("2", Range{
|
||||||
endpoint: "/test/:param/more/:foo",
|
Start: testTime.Add(-time.Minute),
|
||||||
args: map[string]string{
|
End: testTime,
|
||||||
"param": "content",
|
Step: time.Minute,
|
||||||
"foo": "bar",
|
}),
|
||||||
|
inErr: fmt.Errorf("some error"),
|
||||||
|
|
||||||
|
reqMethod: "GET",
|
||||||
|
reqPath: "/api/v1/query_range",
|
||||||
|
reqParam: url.Values{
|
||||||
|
"query": []string{"2"},
|
||||||
|
"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
|
||||||
|
"end": []string{testTime.Format(time.RFC3339Nano)},
|
||||||
|
"step": []string{time.Minute.String()},
|
||||||
},
|
},
|
||||||
expected: "http://localhost:9090/test/content/more/bar",
|
err: fmt.Errorf("some error"),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
address: "http://localhost:9090",
|
do: doLabelValues("mylabel"),
|
||||||
endpoint: "/test/:param",
|
inRes: []string{"val1", "val2"},
|
||||||
args: map[string]string{
|
reqMethod: "GET",
|
||||||
"nonexistant": "content",
|
reqPath: "/api/v1/label/mylabel/values",
|
||||||
},
|
res: model.LabelValues{"val1", "val2"},
|
||||||
expected: "http://localhost:9090/test/:param",
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
do: doLabelValues("mylabel"),
|
||||||
|
inErr: fmt.Errorf("some error"),
|
||||||
|
reqMethod: "GET",
|
||||||
|
reqPath: "/api/v1/label/mylabel/values",
|
||||||
|
err: fmt.Errorf("some error"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tests []apiTest
|
||||||
|
tests = append(tests, queryTests...)
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
ep, err := url.Parse(test.address)
|
client.curTest = test
|
||||||
|
|
||||||
|
res, err := test.do()
|
||||||
|
|
||||||
|
if test.err != nil {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error %q but got none", test.err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err.Error() != test.err.Error() {
|
||||||
|
t.Errorf("unexpected error: want %s, got %s", test.err, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Errorf("unexpected error: %s", err)
|
||||||
}
|
|
||||||
|
|
||||||
hclient := &httpClient{
|
|
||||||
endpoint: ep,
|
|
||||||
transport: DefaultTransport,
|
|
||||||
}
|
|
||||||
|
|
||||||
u := hclient.url(test.endpoint, test.args)
|
|
||||||
if u.String() != test.expected {
|
|
||||||
t.Errorf("unexpected result: got %s, want %s", u, test.expected)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// The apiClient must return exactly the same result as the httpClient.
|
if !reflect.DeepEqual(res, test.res) {
|
||||||
aclient := &apiClient{hclient}
|
t.Errorf("unexpected result: want %v, got %v", test.res, res)
|
||||||
|
|
||||||
u = aclient.url(test.endpoint, test.args)
|
|
||||||
if u.String() != test.expected {
|
|
||||||
t.Errorf("unexpected result: got %s, want %s", u, test.expected)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,11 +225,11 @@ type apiClientTest struct {
|
||||||
err *Error
|
err *Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testClient) url(ep string, args map[string]string) *url.URL {
|
func (c *testClient) URL(ep string, args map[string]string) *url.URL {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
c.Fatalf("context was not passed down")
|
c.Fatalf("context was not passed down")
|
||||||
}
|
}
|
||||||
|
@ -272,7 +355,7 @@ func TestAPIClientDo(t *testing.T) {
|
||||||
|
|
||||||
tc.ch <- test
|
tc.ch <- test
|
||||||
|
|
||||||
_, body, err := client.do(context.Background(), tc.req)
|
_, body, err := client.Do(context.Background(), tc.req)
|
||||||
|
|
||||||
if test.err != nil {
|
if test.err != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -295,186 +378,3 @@ func TestAPIClientDo(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiTestClient struct {
|
|
||||||
*testing.T
|
|
||||||
curTest apiTest
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiTest struct {
|
|
||||||
do func() (interface{}, error)
|
|
||||||
inErr error
|
|
||||||
inRes interface{}
|
|
||||||
|
|
||||||
reqPath string
|
|
||||||
reqParam url.Values
|
|
||||||
reqMethod string
|
|
||||||
res interface{}
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *apiTestClient) url(ep string, args map[string]string) *url.URL {
|
|
||||||
path := apiPrefix + ep
|
|
||||||
for k, v := range args {
|
|
||||||
path = strings.Replace(path, ":"+k, v, -1)
|
|
||||||
}
|
|
||||||
u := &url.URL{
|
|
||||||
Host: "test:9090",
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *apiTestClient) do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) {
|
|
||||||
|
|
||||||
test := c.curTest
|
|
||||||
|
|
||||||
if req.URL.Path != test.reqPath {
|
|
||||||
c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path)
|
|
||||||
}
|
|
||||||
if req.Method != test.reqMethod {
|
|
||||||
c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method)
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := json.Marshal(test.inRes)
|
|
||||||
if err != nil {
|
|
||||||
c.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &http.Response{}
|
|
||||||
if test.inErr != nil {
|
|
||||||
resp.StatusCode = statusAPIError
|
|
||||||
} else {
|
|
||||||
resp.StatusCode = http.StatusOK
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, b, test.inErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAPIs(t *testing.T) {
|
|
||||||
|
|
||||||
testTime := time.Now()
|
|
||||||
|
|
||||||
client := &apiTestClient{T: t}
|
|
||||||
|
|
||||||
queryAPI := &httpQueryAPI{
|
|
||||||
client: client,
|
|
||||||
}
|
|
||||||
|
|
||||||
doQuery := func(q string, ts time.Time) func() (interface{}, error) {
|
|
||||||
return func() (interface{}, error) {
|
|
||||||
return queryAPI.Query(context.Background(), q, ts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doQueryRange := func(q string, rng Range) func() (interface{}, error) {
|
|
||||||
return func() (interface{}, error) {
|
|
||||||
return queryAPI.QueryRange(context.Background(), q, rng)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
doLabelValues := func(label string) func() (interface{}, error) {
|
|
||||||
return func() (interface{}, error) {
|
|
||||||
return queryAPI.LabelValues(context.Background(), label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queryTests := []apiTest{
|
|
||||||
{
|
|
||||||
do: doQuery("2", testTime),
|
|
||||||
inRes: &queryResult{
|
|
||||||
Type: model.ValScalar,
|
|
||||||
Result: &model.Scalar{
|
|
||||||
Value: 2,
|
|
||||||
Timestamp: model.TimeFromUnix(testTime.Unix()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
reqMethod: "GET",
|
|
||||||
reqPath: "/api/v1/query",
|
|
||||||
reqParam: url.Values{
|
|
||||||
"query": []string{"2"},
|
|
||||||
"time": []string{testTime.Format(time.RFC3339Nano)},
|
|
||||||
},
|
|
||||||
res: &model.Scalar{
|
|
||||||
Value: 2,
|
|
||||||
Timestamp: model.TimeFromUnix(testTime.Unix()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
do: doQuery("2", testTime),
|
|
||||||
inErr: fmt.Errorf("some error"),
|
|
||||||
|
|
||||||
reqMethod: "GET",
|
|
||||||
reqPath: "/api/v1/query",
|
|
||||||
reqParam: url.Values{
|
|
||||||
"query": []string{"2"},
|
|
||||||
"time": []string{testTime.Format(time.RFC3339Nano)},
|
|
||||||
},
|
|
||||||
err: fmt.Errorf("some error"),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
do: doQueryRange("2", Range{
|
|
||||||
Start: testTime.Add(-time.Minute),
|
|
||||||
End: testTime,
|
|
||||||
Step: time.Minute,
|
|
||||||
}),
|
|
||||||
inErr: fmt.Errorf("some error"),
|
|
||||||
|
|
||||||
reqMethod: "GET",
|
|
||||||
reqPath: "/api/v1/query_range",
|
|
||||||
reqParam: url.Values{
|
|
||||||
"query": []string{"2"},
|
|
||||||
"start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)},
|
|
||||||
"end": []string{testTime.Format(time.RFC3339Nano)},
|
|
||||||
"step": []string{time.Minute.String()},
|
|
||||||
},
|
|
||||||
err: fmt.Errorf("some error"),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
do: doLabelValues("mylabel"),
|
|
||||||
inRes: []string{"val1", "val2"},
|
|
||||||
reqMethod: "GET",
|
|
||||||
reqPath: "/api/v1/label/mylabel/values",
|
|
||||||
res: model.LabelValues{"val1", "val2"},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
do: doLabelValues("mylabel"),
|
|
||||||
inErr: fmt.Errorf("some error"),
|
|
||||||
reqMethod: "GET",
|
|
||||||
reqPath: "/api/v1/label/mylabel/values",
|
|
||||||
err: fmt.Errorf("some error"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var tests []apiTest
|
|
||||||
tests = append(tests, queryTests...)
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
client.curTest = test
|
|
||||||
|
|
||||||
res, err := test.do()
|
|
||||||
|
|
||||||
if test.err != nil {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error %q but got none", test.err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err.Error() != test.err.Error() {
|
|
||||||
t.Errorf("unexpected error: want %s, got %s", test.err, err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unexpected error: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(res, test.res) {
|
|
||||||
t.Errorf("unexpected result: want %v, got %v", test.res, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue