go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/internal/partition/partition_test.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package partition 16 17 import ( 18 "encoding/json" 19 "math/big" 20 "testing" 21 22 . "github.com/smartystreets/goconvey/convey" 23 ) 24 25 func TestPartition(t *testing.T) { 26 t.Parallel() 27 28 Convey("Partition", t, func() { 29 Convey("Universe", func() { 30 u1 := Universe(1) // 1 byte 31 So(u1.Low.Int64(), ShouldEqual, 0) 32 So(u1.High.Int64(), ShouldEqual, 256) 33 u4 := Universe(4) // 4 bytes 34 So(u4.Low.Int64(), ShouldEqual, 0) 35 So(u4.High.Int64(), ShouldEqual, int64(1)<<32) 36 }) 37 38 Convey("To/From string", func() { 39 u1 := Universe(1) 40 So(u1.String(), ShouldEqual, "0_100") 41 42 p, err := FromString("f0_ff") 43 So(err, ShouldBeNil) 44 So(p.Low.Int64(), ShouldEqual, 240) 45 So(p.High.Int64(), ShouldEqual, 255) 46 47 _, err = FromString("_") 48 So(err, ShouldNotBeNil) 49 _, err = FromString("10_") 50 So(err, ShouldNotBeNil) 51 _, err = FromString("_1") 52 So(err, ShouldNotBeNil) 53 _, err = FromString("10_-1") 54 So(err, ShouldNotBeNil) 55 }) 56 57 Convey("To/From JSON", func() { 58 Convey("works", func() { 59 in := FromInts(5, 64) 60 bytes, err := json.Marshal(in) 61 So(err, ShouldBeNil) 62 So(string(bytes), ShouldResemble, `"5_40"`) 63 out := &Partition{} 64 So(json.Unmarshal(bytes, out), ShouldBeNil) 65 So(out, ShouldResemble, in) 66 }) 67 Convey("null", func() { 68 p := Partition{} 69 So(json.Unmarshal([]byte(`null`), &p), ShouldBeNil) 70 So(p, ShouldResemble, Partition{}) 71 }) 72 Convey("partial error doesn't mutate passed object", func() { 73 p := Partition{} 74 err := json.Unmarshal([]byte(`"10_badhighvalue"`), &p) 75 So(err, ShouldNotBeNil) 76 So(p, ShouldResemble, Partition{}) 77 }) 78 }) 79 80 Convey("Span", func() { 81 p, err := SpanInclusive("05", "10") 82 So(err, ShouldBeNil) 83 So(p.Low.Int64(), ShouldEqual, 5) 84 So(p.High.Int64(), ShouldEqual, 0x10+1) 85 86 _, err = SpanInclusive("Not hex", "10") 87 So(err, ShouldNotBeNil) 88 }) 89 90 Convey("Copy doesn't share bigInts", func() { 91 var a, b *Partition 92 a = FromInts(1, 10) 93 b = a.Copy() 94 a.Low.SetInt64(100) 95 So(b, ShouldResemble, FromInts(1, 10)) 96 }) 97 98 Convey("ApplyToQuery", func() { 99 Convey("inKeySpace", func() { 100 So(inKeySpace(big.NewInt(1), 1), ShouldBeTrue) 101 So(inKeySpace(big.NewInt(254), 1), ShouldBeTrue) 102 So(inKeySpace(big.NewInt(255), 1), ShouldBeTrue) 103 So(inKeySpace(big.NewInt(256), 1), ShouldBeFalse) 104 So(inKeySpace(big.NewInt(256), 2), ShouldBeTrue) 105 }) 106 107 u := Universe(1) 108 l, h := u.QueryBounds(1) 109 So(l, ShouldEqual, "00") 110 So(h, ShouldEqual, "g") 111 l, h = u.QueryBounds(2) 112 So(l, ShouldEqual, "0000") 113 So(h, ShouldEqual, "0100") 114 115 p := FromInts(10, 255) 116 l, h = p.QueryBounds(1) 117 So(l, ShouldEqual, "0a") 118 So(h, ShouldEqual, "ff") 119 l, h = p.QueryBounds(2) 120 So(l, ShouldEqual, "000a") 121 So(h, ShouldEqual, "00ff") 122 }) 123 124 Convey("Split", func() { 125 Convey("Exact", func() { 126 u1 := Universe(1) 127 ps := u1.Split(2) 128 So(len(ps), ShouldEqual, 2) 129 So(ps[0].Low.Int64(), ShouldEqual, 0) 130 So(ps[0].High.Int64(), ShouldEqual, 128) 131 So(ps[1].Low.Int64(), ShouldEqual, 128) 132 So(ps[1].High.Int64(), ShouldEqual, 256) 133 }) 134 135 Convey("Rounding", func() { 136 ps := FromInts(0, 10).Split(3) 137 So(len(ps), ShouldEqual, 3) 138 So(ps[0].Low.Int64(), ShouldEqual, 0) 139 So(ps[0].High.Int64(), ShouldEqual, 4) 140 So(ps[1].Low.Int64(), ShouldEqual, 4) 141 So(ps[1].High.Int64(), ShouldEqual, 8) 142 So(ps[2].Low.Int64(), ShouldEqual, 8) 143 So(ps[2].High.Int64(), ShouldEqual, 10) 144 }) 145 146 Convey("Degenerate", func() { 147 ps := FromInts(0, 1).Split(2) 148 So(len(ps), ShouldEqual, 1) 149 So(ps[0].Low.Int64(), ShouldEqual, 0) 150 So(ps[0].High.Int64(), ShouldEqual, 1) 151 }) 152 }) 153 154 Convey("EducatedSplitAfter", func() { 155 u1 := Universe(1) // 0..256 156 Convey("Ideal", func() { 157 ps := u1.EducatedSplitAfter( 158 "3f", // cutoff, covers 0..64 159 8, // items before the cutoff 160 8, // target per shard 161 100, // maxShards 162 ) 163 So(len(ps), ShouldEqual, 3) 164 So(ps[0].Low.Int64(), ShouldEqual, 64) 165 So(ps[0].High.Int64(), ShouldEqual, 128) 166 So(ps[1].Low.Int64(), ShouldEqual, 128) 167 So(ps[1].High.Int64(), ShouldEqual, 192) 168 So(ps[2].Low.Int64(), ShouldEqual, 192) 169 So(ps[2].High.Int64(), ShouldEqual, 256) 170 }) 171 Convey("MaxShards", func() { 172 ps := u1.EducatedSplitAfter( 173 "3f", // cutoff, covers 0..64 174 8, // items before the cutoff 175 8, // target per shard 176 2, // maxShards 177 ) 178 So(len(ps), ShouldEqual, 2) 179 So(ps[0].Low.Int64(), ShouldEqual, 64) 180 So(ps[0].High.Int64(), ShouldEqual, 160) 181 So(ps[1].Low.Int64(), ShouldEqual, 160) 182 So(ps[1].High.Int64(), ShouldEqual, 256) 183 }) 184 Convey("Rounding", func() { 185 ps := u1.EducatedSplitAfter( 186 "3f", // cutoff, covers 0..64 187 8, // items before the cutoff => (1/8 density) 188 10, // target per shard => range of 80 per shard is ideal. 189 100, // maxShards 190 ) 191 So(len(ps), ShouldEqual, 3) 192 So(ps[0].Low.Int64(), ShouldEqual, 64) 193 So(ps[0].High.Int64(), ShouldEqual, 128) 194 So(ps[1].Low.Int64(), ShouldEqual, 128) 195 So(ps[1].High.Int64(), ShouldEqual, 192) 196 So(ps[2].Low.Int64(), ShouldEqual, 192) 197 So(ps[2].High.Int64(), ShouldEqual, 256) 198 }) 199 }) 200 }) 201 } 202 203 func TestSortedPartitionsBuilder(t *testing.T) { 204 t.Parallel() 205 206 Convey("SortedPartitionsBuilder", t, func() { 207 b := NewSortedPartitionsBuilder(FromInts(0, 300)) 208 So(b.IsEmpty(), ShouldBeFalse) 209 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 300)}) 210 211 b.Exclude(FromInts(100, 200)) 212 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 100), FromInts(200, 300)}) 213 214 b.Exclude(FromInts(150, 175)) // noop 215 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 100), FromInts(200, 300)}) 216 217 b.Exclude(FromInts(150, 250)) // cut front 218 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 100), FromInts(250, 300)}) 219 220 b.Exclude(FromInts(275, 400)) // cut back 221 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 100), FromInts(250, 275)}) 222 223 b.Exclude(FromInts(40, 80)) // another split. 224 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 40), FromInts(80, 100), FromInts(250, 275)}) 225 226 b.Exclude(FromInts(10, 270)) 227 So(b.Result(), ShouldResemble, SortedPartitions{FromInts(0, 10), FromInts(270, 275)}) 228 229 b.Exclude(FromInts(0, 4000)) 230 So(b.Result(), ShouldResemble, SortedPartitions{}) 231 So(b.IsEmpty(), ShouldBeTrue) 232 }) 233 } 234 235 func TestOnlyIn(t *testing.T) { 236 t.Parallel() 237 238 Convey("SortedPartition.OnlyIn", t, func() { 239 var sp SortedPartitions 240 241 const keySpaceBytes = 1 242 copyIn := func(sorted ...string) []string { 243 // This is actually the intended use of the OnlyIn function. 244 reuse := sorted[:] // re-use existing sorted slice. 245 l := 0 246 key := func(i int) string { 247 return sorted[i] 248 } 249 use := func(i, j int) { 250 l += copy(reuse[l:], sorted[i:j]) 251 } 252 sp.OnlyIn(len(sorted), key, use, keySpaceBytes) 253 return reuse[:l] 254 } 255 256 sp = SortedPartitions{FromInts(1, 3), FromInts(9, 16), FromInts(241, 242)} 257 So(copyIn("00"), ShouldResemble, []string{}) 258 So(copyIn("02"), ShouldResemble, []string{"02"}) 259 260 So(copyIn( 261 "00", 262 "02", 263 "03", 264 "0f", // 15 265 "10", // 16 266 "40", // 64 267 "f0", // 240 268 "f1", // 241 269 ), 270 ShouldResemble, []string{"02", "0f", "f1"}) 271 }) 272 }