go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/state/span_test.go (about) 1 // Copyright 2022 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 state 16 17 import ( 18 "context" 19 "sort" 20 "strings" 21 "testing" 22 "time" 23 24 "go.chromium.org/luci/server/span" 25 26 "go.chromium.org/luci/analysis/internal/clustering" 27 "go.chromium.org/luci/analysis/internal/testutil" 28 29 . "github.com/smartystreets/goconvey/convey" 30 . "go.chromium.org/luci/common/testing/assertions" 31 ) 32 33 func TestSpanner(t *testing.T) { 34 Convey(`With Spanner Test Database`, t, func() { 35 ctx := testutil.SpannerTestContext(t) 36 Convey(`Create`, func() { 37 testCreate := func(e *Entry) (time.Time, error) { 38 commitTime, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 39 return Create(ctx, e) 40 }) 41 return commitTime, err 42 } 43 e := NewEntry(100).Build() 44 Convey(`Valid`, func() { 45 commitTime, err := testCreate(e) 46 So(err, ShouldBeNil) 47 e.LastUpdated = commitTime.In(time.UTC) 48 49 txn := span.Single(ctx) 50 actual, err := Read(txn, e.Project, e.ChunkID) 51 So(err, ShouldBeNil) 52 So(actual, ShouldResemble, e) 53 }) 54 Convey(`Invalid`, func() { 55 Convey(`Project missing`, func() { 56 e.Project = "" 57 _, err := testCreate(e) 58 So(err, ShouldErrLike, `project: unspecified`) 59 }) 60 Convey(`Chunk ID missing`, func() { 61 e.ChunkID = "" 62 _, err := testCreate(e) 63 So(err, ShouldErrLike, `chunk ID "" is not valid`) 64 }) 65 Convey(`Partition Time missing`, func() { 66 var t time.Time 67 e.PartitionTime = t 68 _, err := testCreate(e) 69 So(err, ShouldErrLike, "partition time must be specified") 70 }) 71 Convey(`Object ID missing`, func() { 72 e.ObjectID = "" 73 _, err := testCreate(e) 74 So(err, ShouldErrLike, "object ID must be specified") 75 }) 76 Convey(`Config Version missing`, func() { 77 var t time.Time 78 e.Clustering.ConfigVersion = t 79 _, err := testCreate(e) 80 So(err, ShouldErrLike, "config version must be valid") 81 }) 82 Convey(`Rules Version missing`, func() { 83 var t time.Time 84 e.Clustering.RulesVersion = t 85 _, err := testCreate(e) 86 So(err, ShouldErrLike, "rules version must be valid") 87 }) 88 Convey(`Algorithms Version missing`, func() { 89 e.Clustering.AlgorithmsVersion = 0 90 _, err := testCreate(e) 91 So(err, ShouldErrLike, "algorithms version must be specified") 92 }) 93 Convey(`Clusters missing`, func() { 94 e.Clustering.Clusters = nil 95 _, err := testCreate(e) 96 So(err, ShouldErrLike, "there must be clustered test results in the chunk") 97 }) 98 Convey(`Algorithms invalid`, func() { 99 Convey(`Empty algorithm`, func() { 100 e.Clustering.Algorithms[""] = struct{}{} 101 _, err := testCreate(e) 102 So(err, ShouldErrLike, `algorithm "" is not valid`) 103 }) 104 Convey("Algorithm invalid", func() { 105 e.Clustering.Algorithms["!!!"] = struct{}{} 106 _, err := testCreate(e) 107 So(err, ShouldErrLike, `algorithm "!!!" is not valid`) 108 }) 109 }) 110 Convey(`Clusters invalid`, func() { 111 Convey("Algorithm not in algorithms set", func() { 112 e.Clustering.Algorithms = map[string]struct{}{} 113 _, err := testCreate(e) 114 So(err, ShouldErrLike, `clusters: test result 0: cluster 0: algorithm not in algorithms list`) 115 }) 116 Convey("ID missing", func() { 117 e.Clustering.Clusters[1][1].ID = "" 118 _, err := testCreate(e) 119 So(err, ShouldErrLike, `clusters: test result 1: cluster 1: cluster ID is not valid: ID is empty`) 120 }) 121 }) 122 }) 123 }) 124 Convey(`UpdateClustering`, func() { 125 Convey(`Valid`, func() { 126 entry := NewEntry(0).Build() 127 entries := []*Entry{ 128 entry, 129 } 130 commitTime, err := CreateEntriesForTesting(ctx, entries) 131 So(err, ShouldBeNil) 132 entry.LastUpdated = commitTime.In(time.UTC) 133 134 expected := NewEntry(0).Build() 135 136 test := func(update clustering.ClusterResults, expected *Entry) { 137 // Apply the update. 138 commitTime, err = span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 139 err := UpdateClustering(ctx, entry, &update) 140 return err 141 }) 142 So(err, ShouldEqual, nil) 143 expected.LastUpdated = commitTime.In(time.UTC) 144 145 // Assert the update was applied. 146 actual, err := Read(span.Single(ctx), expected.Project, expected.ChunkID) 147 So(err, ShouldBeNil) 148 So(actual, ShouldResemble, expected) 149 } 150 Convey(`Full update`, func() { 151 // Prepare an update. 152 newClustering := NewEntry(1).Build().Clustering 153 expected.Clustering = newClustering 154 155 So(clustering.AlgorithmsAndClustersEqual(&entry.Clustering, &newClustering), ShouldBeFalse) 156 test(newClustering, expected) 157 }) 158 Convey(`Minor update`, func() { 159 // Update only algorithms + rules + config version, without changing clustering content. 160 newClustering := NewEntry(0). 161 WithAlgorithmsVersion(10). 162 WithConfigVersion(time.Date(2024, time.July, 5, 4, 3, 2, 1, time.UTC)). 163 WithRulesVersion(time.Date(2024, time.June, 5, 4, 3, 2, 1000, time.UTC)). 164 Build().Clustering 165 166 expected.Clustering = newClustering 167 So(clustering.AlgorithmsAndClustersEqual(&entries[0].Clustering, &newClustering), ShouldBeTrue) 168 test(newClustering, expected) 169 }) 170 Convey(`No-op update`, func() { 171 test(entry.Clustering, expected) 172 }) 173 }) 174 Convey(`Invalid`, func() { 175 originalEntry := NewEntry(0).Build() 176 newClustering := &NewEntry(0).Build().Clustering 177 178 // Try an invalid algorithm. We do not repeat all the same 179 // validation test cases as create, as the underlying 180 // implementation is the same. 181 newClustering.Algorithms["!!!"] = struct{}{} 182 183 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 184 err := UpdateClustering(ctx, originalEntry, newClustering) 185 return err 186 }) 187 So(err, ShouldErrLike, `algorithm "!!!" is not valid`) 188 }) 189 }) 190 Convey(`ReadLastUpdated`, func() { 191 // Create two entries at different times to give them different LastUpdated times. 192 entryOne := NewEntry(0).Build() 193 lastUpdatedOne, err := CreateEntriesForTesting(ctx, []*Entry{entryOne}) 194 So(err, ShouldBeNil) 195 196 entryTwo := NewEntry(1).Build() 197 lastUpdatedTwo, err := CreateEntriesForTesting(ctx, []*Entry{entryTwo}) 198 So(err, ShouldBeNil) 199 200 chunkKeys := []ChunkKey{ 201 {Project: testProject, ChunkID: entryOne.ChunkID}, 202 {Project: "otherproject", ChunkID: entryOne.ChunkID}, 203 {Project: testProject, ChunkID: "1234567890abcdef1234567890abcdef"}, 204 {Project: testProject, ChunkID: entryTwo.ChunkID}, 205 } 206 207 actual, err := ReadLastUpdated(span.Single(ctx), chunkKeys) 208 So(err, ShouldBeNil) 209 So(len(actual), ShouldEqual, len(chunkKeys)) 210 So(actual[0], ShouldEqual, lastUpdatedOne) 211 So(actual[1], ShouldEqual, time.Time{}) 212 So(actual[2], ShouldEqual, time.Time{}) 213 So(actual[3], ShouldEqual, lastUpdatedTwo) 214 }) 215 Convey(`ReadNextN`, func() { 216 targetRulesVersion := time.Date(2024, 1, 1, 1, 1, 1, 0, time.UTC) 217 targetConfigVersion := time.Date(2024, 2, 1, 1, 1, 1, 0, time.UTC) 218 targetAlgorithmsVersion := 10 219 entries := []*Entry{ 220 // Should not be read. 221 NewEntry(0).WithChunkIDPrefix("11"). 222 WithAlgorithmsVersion(10). 223 WithConfigVersion(targetConfigVersion). 224 WithRulesVersion(targetRulesVersion).Build(), 225 226 // Should be read (rulesVersion < targetRulesVersion). 227 NewEntry(1).WithChunkIDPrefix("11"). 228 WithAlgorithmsVersion(10). 229 WithConfigVersion(targetConfigVersion). 230 WithRulesVersion(targetRulesVersion.Add(-1 * time.Hour)).Build(), 231 NewEntry(2).WithChunkIDPrefix("11"). 232 WithRulesVersion(targetRulesVersion.Add(-1 * time.Hour)).Build(), 233 234 // Should be read (configVersion < targetConfigVersion). 235 NewEntry(3).WithChunkIDPrefix("11"). 236 WithAlgorithmsVersion(10). 237 WithConfigVersion(targetConfigVersion.Add(-1 * time.Hour)). 238 WithRulesVersion(targetRulesVersion).Build(), 239 NewEntry(4).WithChunkIDPrefix("11"). 240 WithConfigVersion(targetConfigVersion.Add(-1 * time.Hour)).Build(), 241 242 // Should be read (algorithmsVersion < targetAlgorithmsVersion). 243 NewEntry(5).WithChunkIDPrefix("11"). 244 WithAlgorithmsVersion(9). 245 WithConfigVersion(targetConfigVersion). 246 WithRulesVersion(targetRulesVersion).Build(), 247 NewEntry(6).WithChunkIDPrefix("11"). 248 WithAlgorithmsVersion(2).Build(), 249 250 // Should not be read (other project). 251 NewEntry(7).WithChunkIDPrefix("11"). 252 WithAlgorithmsVersion(2). 253 WithProject("other").Build(), 254 255 // Check handling of EndChunkID as an inclusive upper-bound. 256 NewEntry(8).WithChunkIDPrefix("11" + strings.Repeat("ff", 15)).WithAlgorithmsVersion(2).Build(), // Should be read. 257 NewEntry(9).WithChunkIDPrefix("12" + strings.Repeat("00", 15)).WithAlgorithmsVersion(2).Build(), // Should not be read. 258 } 259 260 commitTime, err := CreateEntriesForTesting(ctx, entries) 261 for _, e := range entries { 262 e.LastUpdated = commitTime.In(time.UTC) 263 } 264 So(err, ShouldBeNil) 265 266 expectedEntries := []*Entry{ 267 entries[1], 268 entries[2], 269 entries[3], 270 entries[4], 271 entries[5], 272 entries[6], 273 entries[8], 274 } 275 sort.Slice(expectedEntries, func(i, j int) bool { 276 return expectedEntries[i].ChunkID < expectedEntries[j].ChunkID 277 }) 278 279 readOpts := ReadNextOptions{ 280 StartChunkID: "11" + strings.Repeat("00", 15), 281 EndChunkID: "11" + strings.Repeat("ff", 15), 282 AlgorithmsVersion: int64(targetAlgorithmsVersion), 283 ConfigVersion: targetConfigVersion, 284 RulesVersion: targetRulesVersion, 285 } 286 // Reads first page. 287 rows, err := ReadNextN(span.Single(ctx), testProject, readOpts, 4) 288 So(err, ShouldBeNil) 289 So(rows, ShouldResemble, expectedEntries[0:4]) 290 291 // Read second page. 292 readOpts.StartChunkID = rows[3].ChunkID 293 rows, err = ReadNextN(span.Single(ctx), testProject, readOpts, 4) 294 So(err, ShouldBeNil) 295 So(rows, ShouldResemble, expectedEntries[4:]) 296 297 // Read empty last page. 298 readOpts.StartChunkID = rows[2].ChunkID 299 rows, err = ReadNextN(span.Single(ctx), testProject, readOpts, 4) 300 So(err, ShouldBeNil) 301 So(rows, ShouldBeEmpty) 302 }) 303 Convey(`EstimateChunks`, func() { 304 Convey(`Less than 100 chunks`, func() { 305 est, err := EstimateChunks(span.Single(ctx), testProject) 306 So(err, ShouldBeNil) 307 So(est, ShouldBeLessThan, 100) 308 }) 309 Convey(`At least 100 chunks`, func() { 310 var entries []*Entry 311 for i := 0; i < 200; i++ { 312 entries = append(entries, NewEntry(i).Build()) 313 } 314 _, err := CreateEntriesForTesting(ctx, entries) 315 So(err, ShouldBeNil) 316 317 count, err := EstimateChunks(span.Single(ctx), testProject) 318 So(err, ShouldBeNil) 319 So(count, ShouldBeGreaterThan, 190) 320 So(count, ShouldBeLessThan, 210) 321 }) 322 }) 323 }) 324 Convey(`estimateChunksFromID`, t, func() { 325 // Extremely full table. This is the minimum that the 100th ID 326 // could be (considering 0x63 = 99). 327 count, err := estimateChunksFromID("00000000000000000000000000000063") 328 So(err, ShouldBeNil) 329 // The maximum estimate. 330 So(count, ShouldEqual, 1000*1000*1000) 331 332 // The 100th ID is right in the middle of the keyspace. 333 count, err = estimateChunksFromID("7fffffffffffffffffffffffffffffff") 334 So(err, ShouldBeNil) 335 So(count, ShouldEqual, 200) 336 337 // The 100th ID is right at the end of the keyspace. 338 count, err = estimateChunksFromID("ffffffffffffffffffffffffffffffff") 339 So(err, ShouldBeNil) 340 So(count, ShouldEqual, 100) 341 }) 342 }