2020-12-10 12:26:40 +00:00
|
|
|
package searchsvc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
2021-05-18 08:12:51 +00:00
|
|
|
"errors"
|
2020-12-10 12:26:40 +00:00
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"testing"
|
|
|
|
|
2023-03-07 13:38:26 +00:00
|
|
|
clientcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/client"
|
|
|
|
netmapcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/util"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object_manager/placement"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
|
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
|
|
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
|
|
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
2020-12-10 12:26:40 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
type idsErr struct {
|
2022-05-31 17:00:41 +00:00
|
|
|
ids []oid.ID
|
2020-12-10 12:26:40 +00:00
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
type testStorage struct {
|
|
|
|
items map[string]idsErr
|
|
|
|
}
|
|
|
|
|
|
|
|
type testTraverserGenerator struct {
|
2022-06-28 07:01:05 +00:00
|
|
|
c container.Container
|
2021-01-12 14:55:02 +00:00
|
|
|
b map[uint64]placement.Builder
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type testPlacementBuilder struct {
|
2022-06-08 23:18:26 +00:00
|
|
|
vectors map[string][][]netmap.NodeInfo
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type testClientCache struct {
|
|
|
|
clients map[string]*testStorage
|
|
|
|
}
|
|
|
|
|
|
|
|
type simpleIDWriter struct {
|
2022-05-31 17:00:41 +00:00
|
|
|
ids []oid.ID
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
2021-01-12 14:55:02 +00:00
|
|
|
type testEpochReceiver uint64
|
|
|
|
|
2022-12-30 11:06:47 +00:00
|
|
|
func (e testEpochReceiver) Epoch() (uint64, error) {
|
2021-01-12 14:55:02 +00:00
|
|
|
return uint64(e), nil
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func (s *simpleIDWriter) WriteIDs(ids []oid.ID) error {
|
2020-12-10 12:26:40 +00:00
|
|
|
s.ids = append(s.ids, ids...)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func newTestStorage() *testStorage {
|
|
|
|
return &testStorage{
|
|
|
|
items: make(map[string]idsErr),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func (g *testTraverserGenerator) generateTraverser(_ cid.ID, epoch uint64) (*placement.Traverser, error) {
|
2020-12-10 12:26:40 +00:00
|
|
|
return placement.NewTraverser(
|
|
|
|
placement.ForContainer(g.c),
|
2021-01-12 14:55:02 +00:00
|
|
|
placement.UseBuilder(g.b[epoch]),
|
2020-12-10 12:26:40 +00:00
|
|
|
placement.WithoutSuccessTracking(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
func (p *testPlacementBuilder) BuildPlacement(cnr cid.ID, obj *oid.ID, _ netmap.PlacementPolicy) ([][]netmap.NodeInfo, error) {
|
2022-05-31 17:00:41 +00:00
|
|
|
var addr oid.Address
|
|
|
|
addr.SetContainer(cnr)
|
|
|
|
|
|
|
|
if obj != nil {
|
|
|
|
addr.SetObject(*obj)
|
|
|
|
}
|
|
|
|
|
|
|
|
vs, ok := p.vectors[addr.EncodeToString()]
|
2020-12-10 12:26:40 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("vectors for address not found")
|
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
res := make([][]netmap.NodeInfo, len(vs))
|
2021-01-12 14:55:02 +00:00
|
|
|
copy(res, vs)
|
|
|
|
|
|
|
|
return res, nil
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
2021-09-28 04:46:10 +00:00
|
|
|
func (c *testClientCache) get(info clientcore.NodeInfo) (searchClient, error) {
|
|
|
|
v, ok := c.clients[network.StringifyGroup(info.AddressGroup())]
|
2020-12-10 12:26:40 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("could not construct client")
|
|
|
|
}
|
|
|
|
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func (s *testStorage) search(exec *execCtx) ([]oid.ID, error) {
|
2022-12-30 11:00:01 +00:00
|
|
|
v, ok := s.items[exec.prm.cnr.EncodeToString()]
|
2020-12-10 12:26:40 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return v.ids, v.err
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func (c *testStorage) searchObjects(exec *execCtx, _ clientcore.NodeInfo) ([]oid.ID, error) {
|
2022-12-30 11:00:01 +00:00
|
|
|
v, ok := c.items[exec.prm.cnr.EncodeToString()]
|
2020-12-10 12:26:40 +00:00
|
|
|
if !ok {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return v.ids, v.err
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func (c *testStorage) addResult(addr cid.ID, ids []oid.ID, err error) {
|
|
|
|
c.items[addr.EncodeToString()] = idsErr{
|
2020-12-10 12:26:40 +00:00
|
|
|
ids: ids,
|
|
|
|
err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func testSHA256() (cs [sha256.Size]byte) {
|
|
|
|
rand.Read(cs[:])
|
|
|
|
return cs
|
|
|
|
}
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
func generateIDs(num int) []oid.ID {
|
|
|
|
res := make([]oid.ID, num)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
for i := 0; i < num; i++ {
|
|
|
|
res[i].SetSHA256(testSHA256())
|
|
|
|
}
|
|
|
|
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGetLocalOnly(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
newSvc := func(storage *testStorage) *Service {
|
|
|
|
svc := &Service{cfg: new(cfg)}
|
|
|
|
svc.log = test.NewLogger(false)
|
|
|
|
svc.localStorage = storage
|
|
|
|
|
|
|
|
return svc
|
|
|
|
}
|
|
|
|
|
2022-05-12 16:37:46 +00:00
|
|
|
newPrm := func(cnr cid.ID, w IDListWriter) Prm {
|
2020-12-10 12:26:40 +00:00
|
|
|
p := Prm{}
|
2022-05-31 17:00:41 +00:00
|
|
|
p.WithContainerID(cnr)
|
2020-12-10 12:26:40 +00:00
|
|
|
p.SetWriter(w)
|
|
|
|
p.common = new(util.CommonPrm).WithLocalOnly(true)
|
|
|
|
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
|
|
|
storage := newTestStorage()
|
|
|
|
svc := newSvc(storage)
|
|
|
|
|
2022-05-12 16:37:46 +00:00
|
|
|
cnr := cidtest.ID()
|
2020-12-10 12:26:40 +00:00
|
|
|
ids := generateIDs(10)
|
2022-05-12 16:37:46 +00:00
|
|
|
storage.addResult(cnr, ids, nil)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
w := new(simpleIDWriter)
|
2022-05-12 16:37:46 +00:00
|
|
|
p := newPrm(cnr, w)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
err := svc.Search(ctx, p)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, ids, w.ids)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("FAIL", func(t *testing.T) {
|
|
|
|
storage := newTestStorage()
|
|
|
|
svc := newSvc(storage)
|
|
|
|
|
2022-05-12 16:37:46 +00:00
|
|
|
cnr := cidtest.ID()
|
2020-12-10 12:26:40 +00:00
|
|
|
testErr := errors.New("any error")
|
2022-05-12 16:37:46 +00:00
|
|
|
storage.addResult(cnr, nil, testErr)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
w := new(simpleIDWriter)
|
2022-05-12 16:37:46 +00:00
|
|
|
p := newPrm(cnr, w)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
err := svc.Search(ctx, p)
|
2022-04-29 10:22:29 +00:00
|
|
|
require.ErrorIs(t, err, testErr)
|
2020-12-10 12:26:40 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
func testNodeMatrix(t testing.TB, dim []int) ([][]netmap.NodeInfo, [][]string) {
|
|
|
|
mNodes := make([][]netmap.NodeInfo, len(dim))
|
2020-12-10 12:26:40 +00:00
|
|
|
mAddr := make([][]string, len(dim))
|
|
|
|
|
|
|
|
for i := range dim {
|
|
|
|
ns := make([]netmap.NodeInfo, dim[i])
|
|
|
|
as := make([]string, dim[i])
|
|
|
|
|
|
|
|
for j := 0; j < dim[i]; j++ {
|
|
|
|
a := fmt.Sprintf("/ip4/192.168.0.%s/tcp/%s",
|
|
|
|
strconv.Itoa(i),
|
|
|
|
strconv.Itoa(60000+j),
|
|
|
|
)
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
var ni netmap.NodeInfo
|
|
|
|
ni.SetNetworkEndpoints(a)
|
2021-06-18 06:00:21 +00:00
|
|
|
|
2021-06-22 12:19:30 +00:00
|
|
|
var na network.AddressGroup
|
2020-12-10 12:26:40 +00:00
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
err := na.FromIterator(netmapcore.Node(ni))
|
2021-06-22 12:19:30 +00:00
|
|
|
require.NoError(t, err)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
2021-06-22 12:19:30 +00:00
|
|
|
as[j] = network.StringifyGroup(na)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
ns[j] = ni
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
mNodes[i] = ns
|
2020-12-10 12:26:40 +00:00
|
|
|
mAddr[i] = as
|
|
|
|
}
|
|
|
|
|
|
|
|
return mNodes, mAddr
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestGetRemoteSmall(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
placementDim := []int{2}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
rs := make([]netmap.ReplicaDescriptor, len(placementDim))
|
2020-12-10 12:26:40 +00:00
|
|
|
for i := range placementDim {
|
2022-06-08 23:18:26 +00:00
|
|
|
rs[i].SetNumberOfObjects(uint32(placementDim[i]))
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
var pp netmap.PlacementPolicy
|
|
|
|
pp.AddReplicas(rs...)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
2022-06-28 07:01:05 +00:00
|
|
|
var cnr container.Container
|
|
|
|
cnr.SetPlacementPolicy(pp)
|
|
|
|
|
|
|
|
var id cid.ID
|
|
|
|
container.CalculateID(&id, cnr)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
newSvc := func(b *testPlacementBuilder, c *testClientCache) *Service {
|
|
|
|
svc := &Service{cfg: new(cfg)}
|
|
|
|
svc.log = test.NewLogger(false)
|
|
|
|
svc.localStorage = newTestStorage()
|
|
|
|
|
2021-01-12 14:55:02 +00:00
|
|
|
const curEpoch = 13
|
|
|
|
|
2020-12-10 12:26:40 +00:00
|
|
|
svc.traverserGenerator = &testTraverserGenerator{
|
|
|
|
c: cnr,
|
2021-01-12 14:55:02 +00:00
|
|
|
b: map[uint64]placement.Builder{
|
|
|
|
curEpoch: b,
|
|
|
|
},
|
2020-12-10 12:26:40 +00:00
|
|
|
}
|
2021-03-23 18:40:36 +00:00
|
|
|
svc.clientConstructor = c
|
2022-12-30 11:06:47 +00:00
|
|
|
svc.epochSource = testEpochReceiver(curEpoch)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
return svc
|
|
|
|
}
|
|
|
|
|
2022-05-12 16:37:46 +00:00
|
|
|
newPrm := func(id cid.ID, w IDListWriter) Prm {
|
2020-12-10 12:26:40 +00:00
|
|
|
p := Prm{}
|
2022-05-31 17:00:41 +00:00
|
|
|
p.WithContainerID(id)
|
2020-12-10 12:26:40 +00:00
|
|
|
p.SetWriter(w)
|
|
|
|
p.common = new(util.CommonPrm).WithLocalOnly(false)
|
|
|
|
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("OK", func(t *testing.T) {
|
2022-05-31 17:00:41 +00:00
|
|
|
var addr oid.Address
|
|
|
|
addr.SetContainer(id)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
ns, as := testNodeMatrix(t, placementDim)
|
|
|
|
|
|
|
|
builder := &testPlacementBuilder{
|
2022-06-08 23:18:26 +00:00
|
|
|
vectors: map[string][][]netmap.NodeInfo{
|
2022-05-31 17:00:41 +00:00
|
|
|
addr.EncodeToString(): ns,
|
2020-12-10 12:26:40 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
c1 := newTestStorage()
|
|
|
|
ids1 := generateIDs(10)
|
2021-05-31 11:03:17 +00:00
|
|
|
c1.addResult(id, ids1, nil)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
c2 := newTestStorage()
|
|
|
|
ids2 := generateIDs(10)
|
2021-05-31 11:03:17 +00:00
|
|
|
c2.addResult(id, ids2, nil)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
svc := newSvc(builder, &testClientCache{
|
|
|
|
clients: map[string]*testStorage{
|
|
|
|
as[0][0]: c1,
|
|
|
|
as[0][1]: c2,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
w := new(simpleIDWriter)
|
|
|
|
|
2021-05-31 11:03:17 +00:00
|
|
|
p := newPrm(id, w)
|
2020-12-10 12:26:40 +00:00
|
|
|
|
|
|
|
err := svc.Search(ctx, p)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, w.ids, len(ids1)+len(ids2))
|
|
|
|
|
|
|
|
for _, id := range append(ids1, ids2...) {
|
|
|
|
require.Contains(t, w.ids, id)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
func TestGetFromPastEpoch(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
placementDim := []int{2, 2}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
rs := make([]netmap.ReplicaDescriptor, len(placementDim))
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
for i := range placementDim {
|
2022-06-08 23:18:26 +00:00
|
|
|
rs[i].SetNumberOfObjects(uint32(placementDim[i]))
|
2021-01-12 14:55:02 +00:00
|
|
|
}
|
|
|
|
|
2022-06-08 23:18:26 +00:00
|
|
|
var pp netmap.PlacementPolicy
|
|
|
|
pp.AddReplicas(rs...)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
2022-06-28 07:01:05 +00:00
|
|
|
var cnr container.Container
|
|
|
|
cnr.SetPlacementPolicy(pp)
|
|
|
|
|
|
|
|
var idCnr cid.ID
|
|
|
|
container.CalculateID(&idCnr, cnr)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
var addr oid.Address
|
|
|
|
addr.SetContainer(idCnr)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
ns, as := testNodeMatrix(t, placementDim)
|
|
|
|
|
|
|
|
c11 := newTestStorage()
|
|
|
|
ids11 := generateIDs(10)
|
2022-05-31 17:00:41 +00:00
|
|
|
c11.addResult(idCnr, ids11, nil)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
c12 := newTestStorage()
|
|
|
|
ids12 := generateIDs(10)
|
2022-05-31 17:00:41 +00:00
|
|
|
c12.addResult(idCnr, ids12, nil)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
c21 := newTestStorage()
|
|
|
|
ids21 := generateIDs(10)
|
2022-05-31 17:00:41 +00:00
|
|
|
c21.addResult(idCnr, ids21, nil)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
c22 := newTestStorage()
|
|
|
|
ids22 := generateIDs(10)
|
2022-05-31 17:00:41 +00:00
|
|
|
c22.addResult(idCnr, ids22, nil)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
svc := &Service{cfg: new(cfg)}
|
|
|
|
svc.log = test.NewLogger(false)
|
|
|
|
svc.localStorage = newTestStorage()
|
|
|
|
|
|
|
|
const curEpoch = 13
|
|
|
|
|
|
|
|
svc.traverserGenerator = &testTraverserGenerator{
|
|
|
|
c: cnr,
|
|
|
|
b: map[uint64]placement.Builder{
|
|
|
|
curEpoch: &testPlacementBuilder{
|
2022-06-08 23:18:26 +00:00
|
|
|
vectors: map[string][][]netmap.NodeInfo{
|
2022-05-31 17:00:41 +00:00
|
|
|
addr.EncodeToString(): ns[:1],
|
2021-01-12 14:55:02 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
curEpoch - 1: &testPlacementBuilder{
|
2022-06-08 23:18:26 +00:00
|
|
|
vectors: map[string][][]netmap.NodeInfo{
|
2022-05-31 17:00:41 +00:00
|
|
|
addr.EncodeToString(): ns[1:],
|
2021-01-12 14:55:02 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-03-23 18:40:36 +00:00
|
|
|
svc.clientConstructor = &testClientCache{
|
2021-01-12 14:55:02 +00:00
|
|
|
clients: map[string]*testStorage{
|
|
|
|
as[0][0]: c11,
|
|
|
|
as[0][1]: c12,
|
|
|
|
as[1][0]: c21,
|
|
|
|
as[1][1]: c22,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2022-12-30 11:06:47 +00:00
|
|
|
svc.epochSource = testEpochReceiver(curEpoch)
|
2021-01-12 14:55:02 +00:00
|
|
|
|
|
|
|
w := new(simpleIDWriter)
|
|
|
|
|
|
|
|
p := Prm{}
|
2022-05-31 17:00:41 +00:00
|
|
|
p.WithContainerID(idCnr)
|
2021-01-12 14:55:02 +00:00
|
|
|
p.SetWriter(w)
|
|
|
|
|
|
|
|
commonPrm := new(util.CommonPrm)
|
|
|
|
p.SetCommonParameters(commonPrm)
|
|
|
|
|
2022-05-31 17:00:41 +00:00
|
|
|
assertContains := func(idsList ...[]oid.ID) {
|
2021-01-12 14:55:02 +00:00
|
|
|
var sz int
|
|
|
|
|
|
|
|
for _, ids := range idsList {
|
|
|
|
sz += len(ids)
|
|
|
|
|
|
|
|
for _, id := range ids {
|
|
|
|
require.Contains(t, w.ids, id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
require.Len(t, w.ids, sz)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := svc.Search(ctx, p)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assertContains(ids11, ids12)
|
|
|
|
|
|
|
|
commonPrm.SetNetmapLookupDepth(1)
|
|
|
|
w = new(simpleIDWriter)
|
|
|
|
p.SetWriter(w)
|
|
|
|
|
|
|
|
err = svc.Search(ctx, p)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assertContains(ids11, ids12, ids21, ids22)
|
|
|
|
}
|