github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/sharding/label_test.go (about) 1 // SPDX-License-Identifier: AGPL-3.0-only 2 3 package sharding 4 5 import ( 6 "fmt" 7 "math/rand" 8 "testing" 9 10 "github.com/prometheus/prometheus/model/labels" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 ) 14 15 func TestParseShard(t *testing.T) { 16 tests := map[string]struct { 17 input string 18 index, count uint64 19 err bool 20 }{ 21 "should return error on invalid format": { 22 input: "lsdjf", 23 err: true, 24 }, 25 "should return error on invalid index (not an integer)": { 26 input: "a_of_3", 27 err: true, 28 }, 29 "should return error on invalid index (not positive)": { 30 input: "-1_of_3", 31 err: true, 32 }, 33 "should return error on invalid count (not positive)": { 34 input: "-1_of_-3", 35 err: true, 36 }, 37 "should return error on invalid index (too large)": { 38 input: "4_of_3", 39 err: true, 40 }, 41 "should return error on invalid index (too small)": { 42 input: "0_of_3", 43 err: true, 44 }, 45 "should return error on invalid separator": { 46 input: "1_out_3", 47 err: true, 48 }, 49 "should succeed on valid first shard ID": { 50 input: "1_of_2", 51 index: 0, // 0-based 52 count: 2, 53 }, 54 "should succeed on valid last shard selector": { 55 input: "2_of_2", 56 index: 1, // 0-based 57 count: 2, 58 }, 59 } 60 61 for testName, testData := range tests { 62 t.Run(testName, func(t *testing.T) { 63 index, count, err := ParseShardIDLabelValue(testData.input) 64 if testData.err { 65 require.Error(t, err) 66 } else { 67 require.NoError(t, err) 68 require.Equal(t, testData.index, index) 69 require.Equal(t, testData.count, count) 70 } 71 }) 72 } 73 } 74 75 func TestRemoveShardFromMatchers(t *testing.T) { 76 tests := map[string]struct { 77 input []*labels.Matcher 78 expectedShard *ShardSelector 79 expectedMatchers []*labels.Matcher 80 expectedError error 81 }{ 82 "should return no shard on empty label matchers": {}, 83 "should return no shard on no shard label matcher": { 84 input: []*labels.Matcher{ 85 labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test"), 86 labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"), 87 }, 88 expectedShard: nil, 89 expectedMatchers: []*labels.Matcher{ 90 labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test"), 91 labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"), 92 }, 93 }, 94 "should return matching shard and filter out its matcher": { 95 input: []*labels.Matcher{ 96 labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test"), 97 labels.MustNewMatcher(labels.MatchEqual, ShardLabel, ShardSelector{ShardIndex: 1, ShardCount: 8}.LabelValue()), 98 labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"), 99 }, 100 expectedShard: &ShardSelector{ 101 ShardIndex: 1, 102 ShardCount: 8, 103 }, 104 expectedMatchers: []*labels.Matcher{ 105 labels.MustNewMatcher(labels.MatchEqual, labels.MetricName, "test"), 106 labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*"), 107 }, 108 }, 109 } 110 111 for testName, testData := range tests { 112 t.Run(testName, func(t *testing.T) { 113 actualShard, actualMatchers, actualError := RemoveShardFromMatchers(testData.input) 114 assert.Equal(t, testData.expectedShard, actualShard) 115 assert.Equal(t, testData.expectedError, actualError) 116 117 // Assert same matchers. We do some optimizations in mimir-prometheus which make 118 // the label matchers not comparable with reflect.DeepEqual() so we're going to 119 // compare their string representation. 120 require.Len(t, actualMatchers, len(testData.expectedMatchers)) 121 for i := 0; i < len(testData.expectedMatchers); i++ { 122 assert.Equal(t, testData.expectedMatchers[i].String(), actualMatchers[i].String()) 123 } 124 }) 125 } 126 } 127 128 func TestShardFromMatchers(t *testing.T) { 129 testExpr := []struct { 130 input []*labels.Matcher 131 shard *ShardSelector 132 idx int 133 err bool 134 }{ 135 { 136 input: []*labels.Matcher{ 137 {}, 138 { 139 Name: ShardLabel, 140 Type: labels.MatchEqual, 141 Value: ShardSelector{ 142 ShardIndex: 10, 143 ShardCount: 16, 144 }.LabelValue(), 145 }, 146 {}, 147 }, 148 shard: &ShardSelector{ 149 ShardIndex: 10, 150 ShardCount: 16, 151 }, 152 idx: 1, 153 err: false, 154 }, 155 { 156 input: []*labels.Matcher{ 157 { 158 Name: ShardLabel, 159 Type: labels.MatchEqual, 160 Value: "invalid-fmt", 161 }, 162 }, 163 shard: nil, 164 idx: 0, 165 err: true, 166 }, 167 { 168 input: []*labels.Matcher{}, 169 shard: nil, 170 idx: 0, 171 err: false, 172 }, 173 } 174 175 for i, c := range testExpr { 176 t.Run(fmt.Sprint(i), func(t *testing.T) { 177 shard, idx, err := ShardFromMatchers(c.input) 178 if c.err { 179 require.NotNil(t, err) 180 } else { 181 require.Nil(t, err) 182 require.Equal(t, c.shard, shard) 183 require.Equal(t, c.idx, idx) 184 } 185 }) 186 } 187 } 188 189 func TestFormatAndParseShardId(t *testing.T) { 190 r := rand.New(rand.NewSource(0)) 191 192 const maxTests = 1000 193 const maxShardCount = 10000 194 195 for i := 0; i < maxTests; i++ { 196 count := 1 + r.Intn(maxShardCount) 197 id := r.Intn(count) 198 199 require.True(t, id < count) 200 201 out := FormatShardIDLabelValue(uint64(id), uint64(count)) 202 nid, ncount, err := ParseShardIDLabelValue(out) 203 204 require.NoError(t, err) 205 require.Equal(t, uint64(id), nid) 206 require.Equal(t, uint64(count), ncount) 207 } 208 }