NOISSUE - Add searchable Channels name (#754)
* NOISSUE - Add searchable Things name Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix typo Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add postgres schema validation and tests Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Add namme tests in requests_test Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * NOISSUE - Add searchable Channels name Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix test description Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix bootstrap mocks Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com> * Fix reviews Signed-off-by: Manuel Imperiale <manuel.imperiale@gmail.com>
This commit is contained in:
parent
44cc20b9ca
commit
63de955a7c
|
@ -186,7 +186,7 @@ func (svc *mainfluxThings) UpdateChannel(string, things.Channel) error {
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (svc *mainfluxThings) ListChannels(string, uint64, uint64) (things.ChannelsPage, error) {
|
||||
func (svc *mainfluxThings) ListChannels(string, uint64, uint64, string) (things.ChannelsPage, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ var cmdChannels = []cobra.Command{
|
|||
}
|
||||
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit))
|
||||
l, err := sdk.Channels(args[1], uint64(Offset), uint64(Limit), Name)
|
||||
if err != nil {
|
||||
logError(err)
|
||||
return
|
||||
|
|
|
@ -50,8 +50,8 @@ func (sdk mfSDK) CreateChannel(channel Channel, token string) (string, error) {
|
|||
return id, nil
|
||||
}
|
||||
|
||||
func (sdk mfSDK) Channels(token string, offset, limit uint64) (ChannelsPage, error) {
|
||||
endpoint := fmt.Sprintf("%s?offset=%d&limit=%d", channelsEndpoint, offset, limit)
|
||||
func (sdk mfSDK) Channels(token string, offset, limit uint64, name string) (ChannelsPage, error) {
|
||||
endpoint := fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", channelsEndpoint, offset, limit, name)
|
||||
url := createURL(sdk.baseURL, sdk.thingsPrefix, endpoint)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
|
|
|
@ -162,6 +162,7 @@ func TestChannels(t *testing.T) {
|
|||
token string
|
||||
offset uint64
|
||||
limit uint64
|
||||
name string
|
||||
err error
|
||||
response []sdk.Channel
|
||||
}{
|
||||
|
@ -223,7 +224,7 @@ func TestChannels(t *testing.T) {
|
|||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit)
|
||||
page, err := mainfluxSDK.Channels(tc.token, tc.offset, tc.limit, tc.name)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected error %s, got %s", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.response, page.Channels, fmt.Sprintf("%s: expected response channel %s, got %s", tc.desc, tc.response, page.Channels))
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ type SDK interface {
|
|||
CreateChannel(channel Channel, token string) (string, error)
|
||||
|
||||
// Channels returns page of channels.
|
||||
Channels(token string, offset, limit uint64) (ChannelsPage, error)
|
||||
Channels(token string, offset, limit uint64, name string) (ChannelsPage, error)
|
||||
|
||||
// ChannelsByThing returns page of channels that are connected to specified
|
||||
// thing.
|
||||
|
|
|
@ -276,7 +276,7 @@ func listChannelsEndpoint(svc things.Service) endpoint.Endpoint {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
page, err := svc.ListChannels(req.token, req.offset, req.limit)
|
||||
page, err := svc.ListChannels(req.token, req.offset, req.limit, req.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -876,6 +876,10 @@ func TestCreateChannel(t *testing.T) {
|
|||
|
||||
data := toJSON(channel)
|
||||
|
||||
th := channel
|
||||
th.Name = invalidName
|
||||
invalidData := toJSON(th)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
|
@ -940,6 +944,14 @@ func TestCreateChannel(t *testing.T) {
|
|||
status: http.StatusUnsupportedMediaType,
|
||||
location: "",
|
||||
},
|
||||
{
|
||||
desc: "create new channel with invalid name",
|
||||
req: invalidData,
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusBadRequest,
|
||||
location: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
@ -965,9 +977,15 @@ func TestUpdateChannel(t *testing.T) {
|
|||
ts := newServer(svc)
|
||||
defer ts.Close()
|
||||
|
||||
updateData := toJSON(map[string]string{"name": "updated_channel"})
|
||||
sch, _ := svc.CreateChannel(token, channel)
|
||||
|
||||
ch := channel
|
||||
ch.Name = "updated_channel"
|
||||
updateData := toJSON(ch)
|
||||
|
||||
ch.Name = invalidName
|
||||
invalidData := toJSON(ch)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
req string
|
||||
|
@ -1048,6 +1066,13 @@ func TestUpdateChannel(t *testing.T) {
|
|||
auth: token,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
},
|
||||
{
|
||||
desc: "update channel with invalid name",
|
||||
req: invalidData,
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusBadRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
@ -1270,6 +1295,13 @@ func TestListChannels(t *testing.T) {
|
|||
url: fmt.Sprintf("%s%s", channelURL, "?offset=5&limit=e"),
|
||||
res: nil,
|
||||
},
|
||||
{
|
||||
desc: "get a list of channels with invalid name",
|
||||
auth: token,
|
||||
status: http.StatusBadRequest,
|
||||
url: fmt.Sprintf("%s?offset=%d&limit=%d&name=%s", channelURL, 0, 10, invalidName),
|
||||
res: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
|
|
@ -162,9 +162,13 @@ func (lm *loggingMiddleware) ViewChannel(token, id string) (channel things.Chann
|
|||
return lm.svc.ViewChannel(token, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64) (_ things.ChannelsPage, err error) {
|
||||
func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64, name string) (_ things.ChannelsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
message := fmt.Sprintf("Method list_channels for token %s took %s to complete", token, time.Since(begin))
|
||||
nlog := ""
|
||||
if name != "" {
|
||||
nlog = fmt.Sprintf("with name %s ", name)
|
||||
}
|
||||
message := fmt.Sprintf("Method list_channels %sfor token %s took %s to complete", nlog, token, time.Since(begin))
|
||||
if err != nil {
|
||||
lm.logger.Warn(fmt.Sprintf("%s with error: %s.", message, err))
|
||||
return
|
||||
|
@ -172,7 +176,7 @@ func (lm *loggingMiddleware) ListChannels(token string, offset, limit uint64) (_
|
|||
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ListChannels(token, offset, limit)
|
||||
return lm.svc.ListChannels(token, offset, limit, name)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListChannelsByThing(token, id string, offset, limit uint64) (_ things.ChannelsPage, err error) {
|
||||
|
|
|
@ -124,13 +124,13 @@ func (ms *metricsMiddleware) ViewChannel(token, id string) (things.Channel, erro
|
|||
return ms.svc.ViewChannel(token, id)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannels(token string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
func (ms *metricsMiddleware) ListChannels(token string, offset, limit uint64, name string) (things.ChannelsPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "list_channels").Add(1)
|
||||
ms.latency.With("method", "list_channels").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return ms.svc.ListChannels(token, offset, limit)
|
||||
return ms.svc.ListChannels(token, offset, limit, name)
|
||||
}
|
||||
|
||||
func (ms *metricsMiddleware) ListChannelsByThing(token, id string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
|
|
|
@ -39,7 +39,7 @@ type ChannelRepository interface {
|
|||
RetrieveByID(string, string) (Channel, error)
|
||||
|
||||
// RetrieveAll retrieves the subset of channels owned by the specified user.
|
||||
RetrieveAll(string, uint64, uint64) (ChannelsPage, error)
|
||||
RetrieveAll(string, uint64, uint64, string) (ChannelsPage, error)
|
||||
|
||||
// RetrieveByThing retrieves the subset of channels owned by the specified
|
||||
// user and have specified thing connected to them.
|
||||
|
|
|
@ -79,7 +79,7 @@ func (crm *channelRepositoryMock) RetrieveByID(owner, id string) (things.Channel
|
|||
return things.Channel{}, things.ErrNotFound
|
||||
}
|
||||
|
||||
func (crm *channelRepositoryMock) RetrieveAll(owner string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
func (crm *channelRepositoryMock) RetrieveAll(owner string, offset, limit uint64, name string) (things.ChannelsPage, error) {
|
||||
channels := make([]things.Channel, 0)
|
||||
|
||||
if offset < 0 || limit <= 0 {
|
||||
|
|
|
@ -10,6 +10,7 @@ package postgres
|
|||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
@ -42,8 +43,11 @@ func (cr channelRepository) Save(channel things.Channel) (string, error) {
|
|||
|
||||
if _, err := cr.db.NamedExec(q, dbch); err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok && errInvalid == pqErr.Code.Name() {
|
||||
return "", things.ErrMalformedEntity
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return "", things.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
return "", err
|
||||
|
@ -63,8 +67,11 @@ func (cr channelRepository) Update(channel things.Channel) error {
|
|||
res, err := cr.db.NamedExec(q, dbch)
|
||||
if err != nil {
|
||||
pqErr, ok := err.(*pq.Error)
|
||||
if ok && errInvalid == pqErr.Code.Name() {
|
||||
return things.ErrMalformedEntity
|
||||
if ok {
|
||||
switch pqErr.Code.Name() {
|
||||
case errInvalid, errTruncation:
|
||||
return things.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
@ -100,13 +107,21 @@ func (cr channelRepository) RetrieveByID(owner, id string) (things.Channel, erro
|
|||
return toChannel(dbch)
|
||||
}
|
||||
|
||||
func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
q := `SELECT id, name, metadata FROM channels WHERE owner = :owner ORDER BY id LIMIT :limit OFFSET :offset;`
|
||||
func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64, name string) (things.ChannelsPage, error) {
|
||||
nq := ""
|
||||
if name != "" {
|
||||
name = fmt.Sprintf(`%%%s%%`, name)
|
||||
nq = `AND name LIKE :name`
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT id, name, metadata FROM channels
|
||||
WHERE owner = :owner %s ORDER BY id LIMIT :limit OFFSET :offset;`, nq)
|
||||
|
||||
params := map[string]interface{}{
|
||||
"owner": owner,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"name": name,
|
||||
}
|
||||
rows, err := cr.db.NamedQuery(q, params)
|
||||
if err != nil {
|
||||
|
@ -128,11 +143,23 @@ func (cr channelRepository) RetrieveAll(owner string, offset, limit uint64) (thi
|
|||
items = append(items, ch)
|
||||
}
|
||||
|
||||
q = `SELECT COUNT(*) FROM channels WHERE owner = $1;`
|
||||
cq := ""
|
||||
if name != "" {
|
||||
cq = `AND name = $2`
|
||||
}
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.Get(&total, q, owner); err != nil {
|
||||
return things.ChannelsPage{}, err
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM channels WHERE owner = $1 %s;`, cq)
|
||||
|
||||
total := uint64(0)
|
||||
switch name {
|
||||
case "":
|
||||
if err := cr.db.Get(&total, q, owner); err != nil {
|
||||
return things.ChannelsPage{}, err
|
||||
}
|
||||
default:
|
||||
if err := cr.db.Get(&total, q, owner, name); err != nil {
|
||||
return things.ChannelsPage{}, err
|
||||
}
|
||||
}
|
||||
|
||||
page := things.ChannelsPage{
|
||||
|
|
|
@ -25,6 +25,7 @@ func TestChannelSave(t *testing.T) {
|
|||
|
||||
id, err := uuid.New().ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
||||
channel := things.Channel{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
|
@ -41,13 +42,22 @@ func TestChannelSave(t *testing.T) {
|
|||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "create invalid channel",
|
||||
desc: "create channel with invalid ID",
|
||||
channel: things.Channel{
|
||||
ID: "invalid",
|
||||
Owner: email,
|
||||
},
|
||||
err: things.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "create channel with invalid name",
|
||||
channel: things.Channel{
|
||||
ID: id,
|
||||
Owner: email,
|
||||
Name: invalidName,
|
||||
},
|
||||
err: things.ErrMalformedEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
@ -182,9 +192,9 @@ func TestSingleChannelRetrieval(t *testing.T) {
|
|||
func TestMultiChannelRetrieval(t *testing.T) {
|
||||
email := "channel-multi-retrieval@example.com"
|
||||
chanRepo := postgres.NewChannelRepository(db)
|
||||
channelName := "channel_name"
|
||||
|
||||
n := uint64(10)
|
||||
|
||||
for i := uint64(0); i < n; i++ {
|
||||
chid, err := uuid.New().ID()
|
||||
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
||||
|
@ -193,6 +203,11 @@ func TestMultiChannelRetrieval(t *testing.T) {
|
|||
ID: chid,
|
||||
Owner: email,
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
c.Name = channelName
|
||||
}
|
||||
|
||||
chanRepo.Save(c)
|
||||
}
|
||||
|
||||
|
@ -200,6 +215,7 @@ func TestMultiChannelRetrieval(t *testing.T) {
|
|||
owner string
|
||||
offset uint64
|
||||
limit uint64
|
||||
name string
|
||||
size uint64
|
||||
}{
|
||||
"retrieve all channels with existing owner": {
|
||||
|
@ -220,10 +236,24 @@ func TestMultiChannelRetrieval(t *testing.T) {
|
|||
limit: n,
|
||||
size: 0,
|
||||
},
|
||||
"retrieve all channels with existing name": {
|
||||
owner: email,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
name: channelName,
|
||||
size: 1,
|
||||
},
|
||||
"retrieve all channels with non-existing name": {
|
||||
owner: email,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
name: "wrong",
|
||||
size: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := chanRepo.RetrieveAll(tc.owner, tc.offset, tc.limit)
|
||||
page, err := chanRepo.RetrieveAll(tc.owner, tc.offset, tc.limit, tc.name)
|
||||
size := uint64(len(page.Channels))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: expected no error got %d\n", desc, err))
|
||||
|
|
|
@ -63,7 +63,7 @@ func migrateDB(db *sqlx.DB) error {
|
|||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
id UUID,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
name VARCHAR(1024),
|
||||
metadata JSON,
|
||||
PRIMARY KEY (id, owner)
|
||||
)`,
|
||||
|
|
|
@ -412,14 +412,14 @@ func TestMultiThingRetrieval(t *testing.T) {
|
|||
limit: n,
|
||||
size: 0,
|
||||
},
|
||||
"retrieve things with non-existing name": {
|
||||
"retrieve things with existing name": {
|
||||
owner: email,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
name: name,
|
||||
size: 1,
|
||||
},
|
||||
"retrieve things with existing name": {
|
||||
"retrieve things with non-existing name": {
|
||||
owner: email,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
|
|
|
@ -158,8 +158,8 @@ func (es eventStore) ViewChannel(token, id string) (things.Channel, error) {
|
|||
return es.svc.ViewChannel(token, id)
|
||||
}
|
||||
|
||||
func (es eventStore) ListChannels(token string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
return es.svc.ListChannels(token, offset, limit)
|
||||
func (es eventStore) ListChannels(token string, offset, limit uint64, name string) (things.ChannelsPage, error) {
|
||||
return es.svc.ListChannels(token, offset, limit, name)
|
||||
}
|
||||
|
||||
func (es eventStore) ListChannelsByThing(token, id string, offset, limit uint64) (things.ChannelsPage, error) {
|
||||
|
|
|
@ -417,8 +417,8 @@ func TestListChannels(t *testing.T) {
|
|||
require.Nil(t, err, fmt.Sprintf("unexpected error %s", err))
|
||||
|
||||
essvc := redis.NewEventStoreMiddleware(svc, redisClient)
|
||||
eschs, eserr := essvc.ListChannels(token, 0, 10)
|
||||
chs, err := svc.ListChannels(token, 0, 10)
|
||||
eschs, eserr := essvc.ListChannels(token, 0, 10, "")
|
||||
chs, err := svc.ListChannels(token, 0, 10, "")
|
||||
assert.Equal(t, chs, eschs, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", chs, eschs))
|
||||
assert.Equal(t, err, eserr, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", err, eserr))
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ type Service interface {
|
|||
|
||||
// ListChannels retrieves data about subset of channels that belongs to the
|
||||
// user identified by the provided key.
|
||||
ListChannels(string, uint64, uint64) (ChannelsPage, error)
|
||||
ListChannels(string, uint64, uint64, string) (ChannelsPage, error)
|
||||
|
||||
// ListChannelsByThing retrieves data about subset of channels that have
|
||||
// specified thing connected to them and belong to the user identified by
|
||||
|
@ -106,6 +106,7 @@ type PageMetadata struct {
|
|||
Total uint64
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
Name string
|
||||
}
|
||||
|
||||
var _ Service = (*thingsService)(nil)
|
||||
|
@ -299,7 +300,7 @@ func (ts *thingsService) ViewChannel(token, id string) (Channel, error) {
|
|||
return ts.channels.RetrieveByID(res.GetValue(), id)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListChannels(token string, offset, limit uint64) (ChannelsPage, error) {
|
||||
func (ts *thingsService) ListChannels(token string, offset, limit uint64, name string) (ChannelsPage, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
@ -308,7 +309,7 @@ func (ts *thingsService) ListChannels(token string, offset, limit uint64) (Chann
|
|||
return ChannelsPage{}, ErrUnauthorizedAccess
|
||||
}
|
||||
|
||||
return ts.channels.RetrieveAll(res.GetValue(), offset, limit)
|
||||
return ts.channels.RetrieveAll(res.GetValue(), offset, limit, name)
|
||||
}
|
||||
|
||||
func (ts *thingsService) ListChannelsByThing(token, thing string, offset, limit uint64) (ChannelsPage, error) {
|
||||
|
|
|
@ -491,6 +491,7 @@ func TestListChannels(t *testing.T) {
|
|||
offset uint64
|
||||
limit uint64
|
||||
size uint64
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
"list all channels": {
|
||||
|
@ -535,10 +536,26 @@ func TestListChannels(t *testing.T) {
|
|||
size: 0,
|
||||
err: things.ErrUnauthorizedAccess,
|
||||
},
|
||||
"list with existing name": {
|
||||
token: token,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: n,
|
||||
name: "chanel_name",
|
||||
err: nil,
|
||||
},
|
||||
"list with non-existent name": {
|
||||
token: token,
|
||||
offset: 0,
|
||||
limit: n,
|
||||
size: n,
|
||||
name: "wrong",
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for desc, tc := range cases {
|
||||
page, err := svc.ListChannels(tc.token, tc.offset, tc.limit)
|
||||
page, err := svc.ListChannels(tc.token, tc.offset, tc.limit, tc.name)
|
||||
size := uint64(len(page.Channels))
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
|
||||
|
|
Loading…
Reference in New Issue