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  }