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  }