github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/api/query/cache/index/index_test.go (about)

     1  //go:build small
     2  // +build small
     3  
     4  // Copyright 2018 The WPT Dashboard Project. All rights reserved.
     5  // Use of this source code is governed by a BSD-style license that can be
     6  // found in the LICENSE file.
     7  
     8  package index
     9  
    10  import (
    11  	"errors"
    12  	"strconv"
    13  	"sync"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/web-platform-tests/wpt.fyi/api/query"
    18  
    19  	"github.com/golang/mock/gomock"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/web-platform-tests/wpt.fyi/shared"
    22  	metrics "github.com/web-platform-tests/wpt.fyi/shared/metrics"
    23  )
    24  
    25  func TestInvalidNumShards(t *testing.T) {
    26  	ctrl := gomock.NewController(t)
    27  	defer ctrl.Finish()
    28  	loader := NewMockReportLoader(ctrl)
    29  	_, err := NewShardedWPTIndex(loader, 0)
    30  	assert.NotNil(t, err)
    31  	_, err = NewShardedWPTIndex(loader, -1)
    32  }
    33  
    34  func TestEvictEmpty(t *testing.T) {
    35  	ctrl := gomock.NewController(t)
    36  	defer ctrl.Finish()
    37  	loader := NewMockReportLoader(ctrl)
    38  	i, err := NewShardedWPTIndex(loader, 1)
    39  	assert.Nil(t, err)
    40  	_, err = i.EvictRuns(0.0)
    41  	assert.NotNil(t, err)
    42  }
    43  
    44  func TestIngestRun_zeroID(t *testing.T) {
    45  	ctrl := gomock.NewController(t)
    46  	defer ctrl.Finish()
    47  	loader := NewMockReportLoader(ctrl)
    48  	i, err := NewShardedWPTIndex(loader, 1)
    49  	assert.Nil(t, err)
    50  	assert.NotNil(t, i.IngestRun(shared.TestRun{ID: 0}))
    51  }
    52  
    53  func TestIngestRun_double(t *testing.T) {
    54  	ctrl := gomock.NewController(t)
    55  	defer ctrl.Finish()
    56  	loader := NewMockReportLoader(ctrl)
    57  	i, err := NewShardedWPTIndex(loader, 1)
    58  	assert.Nil(t, err)
    59  	run := shared.TestRun{
    60  		ID:            1,
    61  		RawResultsURL: "http://example.com/results.json",
    62  	}
    63  	results := &metrics.TestResultsReport{}
    64  	loader.EXPECT().Load(run).Return(results, nil)
    65  	assert.Nil(t, i.IngestRun(run))
    66  	assert.NotNil(t, i.IngestRun(run))
    67  }
    68  
    69  func TestIngestRun_concurrent(t *testing.T) {
    70  	ctrl := gomock.NewController(t)
    71  	defer ctrl.Finish()
    72  	loader := NewMockReportLoader(ctrl)
    73  	i, err := NewShardedWPTIndex(loader, 1)
    74  	assert.Nil(t, err)
    75  	run := shared.TestRun{
    76  		ID:            1,
    77  		RawResultsURL: "http://example.com/results.json",
    78  	}
    79  
    80  	// Wait for 2 goroutines to finish. Gate second goroutine's IngestRun()
    81  	// invocation with channel that receives value after first goroutine has
    82  	// started to ingest the run.
    83  	var wg sync.WaitGroup
    84  	startSecondIngestRun := make(chan bool)
    85  	wg.Add(2)
    86  	go func() {
    87  		defer wg.Done()
    88  		loader.EXPECT().Load(run).DoAndReturn(func(shared.TestRun) (*metrics.TestResultsReport, error) {
    89  			// Now that Load(run) has been invoked, i's implementation should have
    90  			// already marked run as in-flight. Trigger second attempt to ingest run,
    91  			// and pause a little to let it error-out.
    92  			startSecondIngestRun <- true
    93  			time.Sleep(time.Millisecond * 10)
    94  			return &metrics.TestResultsReport{}, nil
    95  		})
    96  		i.IngestRun(run)
    97  	}()
    98  	go func() {
    99  		defer wg.Done()
   100  		<-startSecondIngestRun
   101  		// Expect error during second concurrent attempt to ingest run.
   102  		assert.NotNil(t, i.IngestRun(run))
   103  	}()
   104  	wg.Wait()
   105  }
   106  
   107  func TestIngestRun_loaderError(t *testing.T) {
   108  	ctrl := gomock.NewController(t)
   109  	defer ctrl.Finish()
   110  	loader := NewMockReportLoader(ctrl)
   111  	i, err := NewShardedWPTIndex(loader, 1)
   112  	assert.Nil(t, err)
   113  	run := shared.TestRun{
   114  		ID:            1,
   115  		RawResultsURL: "http://example.com/results.json",
   116  	}
   117  	loaderErr := errors.New("Failed to load test results")
   118  	loader.EXPECT().Load(run).Return(nil, loaderErr)
   119  	assert.Equal(t, loaderErr, i.IngestRun(run))
   120  }
   121  
   122  func TestEvictNonEmpty(t *testing.T) {
   123  	ctrl := gomock.NewController(t)
   124  	defer ctrl.Finish()
   125  	loader := NewMockReportLoader(ctrl)
   126  	i, err := NewShardedWPTIndex(loader, 1)
   127  	assert.Nil(t, err)
   128  	run := shared.TestRun{
   129  		ID:            1,
   130  		RawResultsURL: "http://example.com/results.json",
   131  	}
   132  	results := &metrics.TestResultsReport{
   133  		Results: []*metrics.TestResults{
   134  			{
   135  				Test:     "a",
   136  				Status:   "PASS",
   137  				Subtests: []metrics.SubTest{},
   138  			},
   139  			{
   140  				Test:   "b",
   141  				Status: "OK",
   142  				Subtests: []metrics.SubTest{
   143  					{
   144  						Name:   "sub",
   145  						Status: "FAIL",
   146  					},
   147  				},
   148  			},
   149  		},
   150  	}
   151  	loader.EXPECT().Load(run).Return(results, nil)
   152  	assert.Nil(t, i.IngestRun(run))
   153  	n, err := i.EvictRuns(0.0)
   154  	assert.Nil(t, err)
   155  	assert.Equal(t, 1, n)
   156  }
   157  
   158  func TestEvictMultiple(t *testing.T) {
   159  	ctrl := gomock.NewController(t)
   160  	defer ctrl.Finish()
   161  	loader := NewMockReportLoader(ctrl)
   162  	i, err := NewShardedWPTIndex(loader, 1)
   163  	assert.Nil(t, err)
   164  
   165  	run1 := shared.TestRun{
   166  		ID:            1,
   167  		RawResultsURL: "http://example.com/results1.json",
   168  	}
   169  	results1 := &metrics.TestResultsReport{
   170  		Results: []*metrics.TestResults{
   171  			{
   172  				Test:     "a",
   173  				Status:   "PASS",
   174  				Subtests: []metrics.SubTest{},
   175  			},
   176  			{
   177  				Test:   "b",
   178  				Status: "OK",
   179  				Subtests: []metrics.SubTest{
   180  					{
   181  						Name:   "sub",
   182  						Status: "FAIL",
   183  					},
   184  				},
   185  			},
   186  		},
   187  	}
   188  	run2 := shared.TestRun{
   189  		ID:            2,
   190  		RawResultsURL: "http://example.com/results2.json",
   191  	}
   192  	results2 := &metrics.TestResultsReport{
   193  		Results: []*metrics.TestResults{
   194  			{
   195  				Test:     "a",
   196  				Status:   "FAIL",
   197  				Subtests: []metrics.SubTest{},
   198  			},
   199  			{
   200  				Test:   "b",
   201  				Status: "OK",
   202  				Subtests: []metrics.SubTest{
   203  					{
   204  						Name:   "sub",
   205  						Status: "TIMEOUT",
   206  					},
   207  				},
   208  			},
   209  		},
   210  	}
   211  	run3 := shared.TestRun{
   212  		ID:            3,
   213  		RawResultsURL: "http://example.com/results2.json",
   214  	}
   215  	results3 := &metrics.TestResultsReport{
   216  		Results: []*metrics.TestResults{
   217  			{
   218  				Test:     "a",
   219  				Status:   "PASS",
   220  				Subtests: []metrics.SubTest{},
   221  			},
   222  			{
   223  				Test:     "b",
   224  				Status:   "TIMEOUT",
   225  				Subtests: []metrics.SubTest{},
   226  			},
   227  		},
   228  	}
   229  
   230  	loader.EXPECT().Load(run1).Return(results1, nil)
   231  	loader.EXPECT().Load(run2).Return(results2, nil)
   232  	loader.EXPECT().Load(run3).Return(results3, nil)
   233  
   234  	assert.Nil(t, i.IngestRun(run1))
   235  	assert.Nil(t, i.IngestRun(run2))
   236  	assert.Nil(t, i.IngestRun(run3))
   237  
   238  	n, err := i.EvictRuns(0.7)
   239  	assert.Nil(t, err)
   240  	assert.Equal(t, 2, n)
   241  }
   242  
   243  func TestSync(t *testing.T) {
   244  	ctrl := gomock.NewController(t)
   245  	defer ctrl.Finish()
   246  	loader := NewMockReportLoader(ctrl)
   247  	i, err := NewShardedWPTIndex(loader, 1)
   248  	assert.Nil(t, err)
   249  
   250  	// Populate data with predictable set of two results for each run.
   251  	loader.EXPECT().Load(gomock.Any()).DoAndReturn(func(run shared.TestRun) (*metrics.TestResultsReport, error) {
   252  		strID := strconv.FormatInt(run.ID, 10)
   253  		strStatus := shared.TestStatus(run.ID % 7).String()
   254  		return &metrics.TestResultsReport{
   255  			Results: []*metrics.TestResults{
   256  				{
   257  					Test:   "shared",
   258  					Status: strStatus,
   259  				},
   260  				{
   261  					Test:   "test" + strID,
   262  					Status: "PASS",
   263  				},
   264  			},
   265  		}, nil
   266  	}).AnyTimes()
   267  
   268  	// Baseline before running things in parallel: Index already contains 8 runs.
   269  	i.IngestRun(makeRun(1))
   270  	i.IngestRun(makeRun(2))
   271  	i.IngestRun(makeRun(3))
   272  	i.IngestRun(makeRun(4))
   273  	i.IngestRun(makeRun(5))
   274  	i.IngestRun(makeRun(6))
   275  	i.IngestRun(makeRun(7))
   276  	i.IngestRun(makeRun(8))
   277  
   278  	// Eight times (from run IDs 9 through 16), in parallel:
   279  	// - Evict one run,
   280  	// - Add one run,
   281  	// - Attempt one query (that may fail to bind if it references an already
   282  	//   already evicted run).
   283  	var wg sync.WaitGroup
   284  	for j := 9; j <= 16; j++ {
   285  		wg.Add(1)
   286  		go func(n int) {
   287  			defer wg.Done()
   288  			n, err := i.EvictRuns(0.0)
   289  			assert.Nil(t, err)
   290  			assert.Equal(t, 1, n)
   291  		}(j)
   292  		wg.Add(1)
   293  		go func(id int64) {
   294  			defer wg.Done()
   295  			i.IngestRun(makeRun(id))
   296  		}(int64(j))
   297  		wg.Add(1)
   298  		go func(n int) {
   299  			defer wg.Done()
   300  			runs := []shared.TestRun{
   301  				makeRun(int64(n - 1)),
   302  				makeRun(int64(n - 2)),
   303  				makeRun(int64(n - 3)),
   304  				makeRun(int64(n - 4)),
   305  			}
   306  			c := shared.ParseProductSpecUnsafe("Chrome")
   307  			plan, err := i.Bind(runs, query.TestStatusEq{
   308  				Product: &c,
   309  				Status:  shared.TestStatusPass,
   310  			}.BindToRuns(runs...))
   311  			if err != nil {
   312  				return
   313  			}
   314  
   315  			plan.Execute(runs, query.AggregationOpts{})
   316  		}(j)
   317  	}
   318  	wg.Wait()
   319  
   320  	// Number of runs should now be 8 + 8 - 8 = 8.
   321  	// Shards, taken together, should contain data for two predictable run results
   322  	// for each run still in the index. (See loader.EXPECT()...DoAndReturn(...)
   323  	// callback above for predictable test names and values.)
   324  
   325  	// TODO: Should Index have a Runs() getter for purposes such as this check?
   326  	idx, ok := i.(*shardedWPTIndex)
   327  	assert.True(t, ok)
   328  
   329  	assert.Equal(t, 8, len(idx.runs))
   330  	sharedTestID, err := computeTestID("shared", nil)
   331  	assert.Nil(t, err)
   332  	numResults := 0
   333  	for _, s := range idx.shards {
   334  		// TODO: Should Results have a getter for purposes such as this check?
   335  		results, ok := s.results.(*resultsMap)
   336  		assert.True(t, ok)
   337  		numRuns := 0
   338  		results.byRunTest.Range(func(key, value interface{}) bool {
   339  			numRuns++
   340  			return true
   341  		})
   342  		assert.Equal(t, 8, numRuns)
   343  
   344  		for _, run := range idx.runs {
   345  			value, ok := results.byRunTest.Load(RunID(run.ID))
   346  			assert.True(t, ok)
   347  			// TODO: Should Results have a getter for purposes such as this check?
   348  			res, ok := value.(*runResultsMap)
   349  			assert.True(t, ok)
   350  
   351  			strID := strconv.FormatInt(run.ID, 10)
   352  			expectedTestID, err := computeTestID("test"+strID, nil)
   353  			assert.Nil(t, err)
   354  
   355  			for testID, resultID := range res.byTest {
   356  				// Either test is the "shared test" with varied result values across
   357  				// runs or it is the "test-specific test" with name `test<test ID>` and
   358  				// result value of "PASS".
   359  				assert.True(t, sharedTestID == testID || (expectedTestID == testID && resultID == ResultID(shared.TestStatusPass)))
   360  				numResults++
   361  			}
   362  		}
   363  	}
   364  
   365  	// Total number of results is 8 runs * 2 results per run = 16.
   366  	assert.Equal(t, 16, numResults)
   367  }
   368  
   369  var browsers = []string{
   370  	"Chrome",
   371  	"Edge",
   372  	"Firefox",
   373  	"Safari",
   374  }
   375  
   376  func makeRun(id int64) shared.TestRun {
   377  	browserName := browsers[id%int64(len(browsers))]
   378  	return shared.TestRun{
   379  		ID: id,
   380  		ProductAtRevision: shared.ProductAtRevision{
   381  			Product: shared.Product{
   382  				BrowserName: browserName,
   383  			},
   384  		},
   385  	}
   386  }
   387  
   388  // TODO: Add synchronization test to check for race conditions once Bind+Execute
   389  // are fully implemented over indexes and filters.