blob: 5c1ae53e7d0c96d71c8cb8af2f1523523b1ce2f4 [file] [log] [blame]
package frame
import (
"bytes"
"context"
"errors"
"fmt"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/testutils"
"go.skia.org/infra/perf/go/anomalies/cache"
"go.skia.org/infra/perf/go/chromeperf"
chromeperfMock "go.skia.org/infra/perf/go/chromeperf/mock"
"go.skia.org/infra/perf/go/config"
"go.skia.org/infra/perf/go/dataframe"
"go.skia.org/infra/perf/go/dataframe/mocks"
perfgit "go.skia.org/infra/perf/go/git"
"go.skia.org/infra/perf/go/git/gittest"
"go.skia.org/infra/perf/go/pivot"
"go.skia.org/infra/perf/go/progress"
"go.skia.org/infra/perf/go/shortcut"
shortcutStoreMock "go.skia.org/infra/perf/go/shortcut/mocks"
traceStoreMocks "go.skia.org/infra/perf/go/tracestore/mocks"
"go.skia.org/infra/perf/go/types"
)
const (
testShortcutKey = "some-key-value"
)
var (
testTimeBegin = time.Date(2020, 1, 1, 1, 0, 0, 0, time.UTC)
testTimeEnd = time.Date(2020, 1, 1, 2, 0, 0, 0, time.UTC)
errTestError = errors.New("my test error")
)
const (
traceName1 = ",benchmark=Blazor,bot=MacM1,master=ChromiumPerf,test=test1,"
traceName2 = ",benchmark=Blazor,bot=MacM1,master=ChromiumPerf,test=test2,"
testPath1 = "ChromiumPerf/MacM1/Blazor/test1"
testPath2 = "ChromiumPerf/MacM1/Blazor/test2"
startCommitPosition = 11
endCommitPosition = 21
)
var anomaly1 = chromeperf.Anomaly{
Id: "111",
TestPath: testPath1,
StartRevision: startCommitPosition,
EndRevision: endCommitPosition,
IsImprovement: false,
Recovered: false,
State: "unknown",
Statistics: "avg",
Unit: "ms",
PValue: 1.1,
}
var anomaly2 = chromeperf.Anomaly{
Id: "222",
TestPath: testPath2,
StartRevision: startCommitPosition,
EndRevision: endCommitPosition,
IsImprovement: false,
Recovered: false,
State: "unknown",
Statistics: "avg",
Unit: "ms",
PValue: 2.2,
}
var chromePerfAnomalyMap = chromeperf.AnomalyMap{
traceName1: map[types.CommitNumber]chromeperf.Anomaly{12: anomaly1},
traceName2: map[types.CommitNumber]chromeperf.Anomaly{15: anomaly2},
}
var traceSet = types.TraceSet{
traceName1: types.Trace([]float32{1.2, 2.1}),
traceName2: types.Trace([]float32{1.3, 3.1}),
}
var traceNames = []string{traceName1, traceName2}
var testPathes = []string{testPath1, testPath2}
var errMock = errors.New("this is my mock test error")
var ctx = context.Background()
func TestGetSkps_Success(t *testing.T) {
ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t)
g, err := perfgit.New(ctx, false, db, instanceConfig)
require.NoError(t, err)
instanceConfig.GitRepoConfig.FileChangeMarker = "bar.txt"
config.Config = instanceConfig
skps, err := getSkps(ctx, []*dataframe.ColumnHeader{
{
Offset: 0,
},
{
Offset: 7,
},
}, g)
require.NoError(t, err)
assert.Equal(t, []int{3, 6}, skps)
}
func TestGetSkps_SuccessIfFileChangeMarkerNotSet(t *testing.T) {
ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t)
g, err := perfgit.New(ctx, false, db, instanceConfig)
require.NoError(t, err)
instanceConfig.GitRepoConfig.FileChangeMarker = ""
config.Config = instanceConfig
skps, err := getSkps(ctx, []*dataframe.ColumnHeader{
{
Offset: 0,
},
{
Offset: 7,
},
}, g)
require.NoError(t, err)
assert.Empty(t, skps)
}
func TestGetSkps_ErrOnBadCommitNumber(t *testing.T) {
ctx, db, _, _, _, instanceConfig := gittest.NewForTest(t)
g, err := perfgit.New(ctx, false, db, instanceConfig)
require.NoError(t, err)
instanceConfig.GitRepoConfig.FileChangeMarker = "bar.txt"
config.Config = instanceConfig
_, err = getSkps(ctx, []*dataframe.ColumnHeader{
{
Offset: -3,
},
{
Offset: -1,
},
}, g)
require.Error(t, err)
}
func TestGetMetadataForTraces_Success(t *testing.T) {
// The dataframe contains 3 commits 1,2,3 and 2 traces
//
// [",arch=x86,config=8888,"] = {1, 2, 3}
// [",arch=x86,config=565,"] = {2, 4, 6}
_, df, _ := frameRequestForTest(t)
mockTracestore := traceStoreMocks.NewTraceStore(t)
mockMetadataStore := traceStoreMocks.NewMetadataStore(t)
config.Config = &config.InstanceConfig{
DataPointConfig: config.DataPointConfig{
KeysForUsefulLinks: []string{"link1", "link2", "link3", "link4", "link5", "link6"},
},
}
commits := []types.CommitNumber{1, 2, 3}
traceids := []string{",arch=x86,config=8888,", ",arch=x86,config=565,"}
sourceFileIds := []string{
"trace0_file1",
"trace0_file2",
"trace0_file3",
"trace1_file1",
"trace1_file2",
"trace1_file3",
}
sourceInfo := map[string]map[types.CommitNumber]string{
traceids[0]: {
1: sourceFileIds[0],
2: sourceFileIds[1],
3: sourceFileIds[2],
},
traceids[1]: {
1: sourceFileIds[3],
2: sourceFileIds[4],
3: sourceFileIds[5],
},
}
mockTracestore.On("GetSourceIds", ctx, commits, traceids).Return(sourceInfo, nil)
sourceFileLinks := map[string]map[string]string{
sourceFileIds[0]: {
"link1": "val1",
},
sourceFileIds[1]: {
"link2": "val2",
},
sourceFileIds[2]: {
"link3": "val3",
},
sourceFileIds[3]: {
"link4": "val4",
},
sourceFileIds[4]: {
"link5": "val5",
},
sourceFileIds[5]: {
"link6": "val6",
},
}
mockMetadataStore.On("GetMetadataMultiple", ctx, mock.Anything).Return(sourceFileLinks, nil)
ctx := context.Background()
traceMetadata, err := getMetadataForTraces(ctx, df, mockTracestore, mockMetadataStore)
assert.NoError(t, err)
assert.NotNil(t, traceMetadata)
assert.Equal(t, len(traceids), len(traceMetadata))
}
func TestProcessFrameRequest_InvalidQuery_ReturnsError(t *testing.T) {
fr := &FrameRequest{
Queries: []string{"http://[::1]a"}, // A known query that will fail to parse.
Progress: progress.New(),
}
err := ProcessFrameRequest(context.Background(), fr, nil, nil, nil, nil, nil, nil, false)
require.Error(t, err)
var b bytes.Buffer
err = fr.Progress.JSON(&b)
require.NoError(t, err)
assert.Equal(t, `{"status":"Running","messages":[{"key":"Loading","value":"Queries"}],"url":""}`, strings.TrimSpace(b.String()))
}
// frameRequestForTest returns a mock DataFrameBuilder, a frameRequestProcess,
// and a populated DateFrame for testing.
//
// The DataFrame returned has the following Traces:
//
// [",arch=x86,config=8888,"] = {1, 2, 3}
// [",arch=x86,config=565,"] = {2, 4, 6}
func frameRequestForTest(t *testing.T) (*mocks.DataFrameBuilder, *dataframe.DataFrame, *frameRequestProcess) {
t.Helper()
dfbMock := &mocks.DataFrameBuilder{}
ssMock := &shortcutStoreMock.Store{}
fr := &frameRequestProcess{
request: &FrameRequest{
Queries: []string{"arch=x86"},
RequestType: REQUEST_COMPACT,
Progress: progress.New(),
NumCommits: 10,
},
dfBuilder: dfbMock,
shortcutStore: ssMock,
}
df := dataframe.NewEmpty()
df.TraceSet[",arch=x86,config=8888,"] = types.Trace{1, 2, 3}
df.TraceSet[",arch=x86,config=565,"] = types.Trace{2, 4, 6}
const numHeaders = 3
df.Header = make([]*dataframe.ColumnHeader, numHeaders)
for i := 0; i < numHeaders; i++ {
df.Header[i] = &dataframe.ColumnHeader{
Offset: types.CommitNumber(i + 1),
Timestamp: dataframe.TimestampSeconds(testTimeBegin.Unix() + int64(i)),
}
}
df.BuildParamSet()
t.Cleanup(func() {
dfbMock.AssertExpectations(t)
})
return dfbMock, df, fr
}
func TestDoSearch_InvalidQuery_ReturnsError(t *testing.T) {
_, _, fr := frameRequestForTest(t)
_, err := fr.doSearch(context.Background(), "http://[::1]a", testTimeBegin, testTimeEnd)
require.Error(t, err)
}
func TestDoSearch_ValidQueryCompact_ReturnsDataFrameWithQueryResults(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
dfbMock.On("NewNFromQuery", testutils.AnyContext, testTimeEnd, mock.Anything, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doSearch(context.Background(), "config=8888", testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, df, actualDf)
}
func TestDoSearch_ValidQueryTimeRange_ReturnsDataFrameWithQueryResults(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.RequestType = REQUEST_TIME_RANGE
dfbMock.On("NewFromQueryAndRange", testutils.AnyContext, testTimeBegin, testTimeEnd, mock.Anything, true, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doSearch(context.Background(), "config=8888", testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, df, actualDf)
}
func TestDoKeys_ShortcutStoreReturnsError_ReturnsError(t *testing.T) {
_, _, fr := frameRequestForTest(t)
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
testShortcutKey := "some-key-value"
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(nil, errTestError)
_, err := fr.doKeys(context.Background(), testShortcutKey, testTimeBegin, testTimeEnd)
require.Error(t, err)
}
func TestDoKeys_ValidKeyID_ReturnsDataFrameWithTracesFromShortcut(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
// Create valid shortCut.Shortcut for "Get" to return.
shortCutKeys := []string{}
copy(shortCutKeys, df.ParamSet.Keys())
sort.Strings(shortCutKeys)
shortCut := &shortcut.Shortcut{
Keys: shortCutKeys,
}
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(shortCut, nil)
dfbMock.On("NewNFromKeys", testutils.AnyContext, testTimeEnd, shortCut.Keys, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doKeys(context.Background(), testShortcutKey, testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, df, actualDf)
}
func TestDoKeys_ValidKeyIDTimeRange_ReturnsDataFrameWithTracesFromShortcut(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
fr.request.RequestType = REQUEST_TIME_RANGE
// Create valid shortCut.Shortcut for "Get" to return.
shortCutKeys := []string{}
copy(shortCutKeys, df.ParamSet.Keys())
sort.Strings(shortCutKeys)
shortCut := &shortcut.Shortcut{
Keys: shortCutKeys,
}
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(shortCut, nil)
dfbMock.On("NewFromKeysAndRange", testutils.AnyContext, shortCut.Keys, testTimeBegin, testTimeEnd, true, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doKeys(context.Background(), testShortcutKey, testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, df, actualDf)
}
func TestDoCalc_InvalidFormulaCompact_ReturnsError(t *testing.T) {
_, _, fr := frameRequestForTest(t)
_, err := fr.doCalc(context.Background(), `sum(filter(`, testTimeBegin, testTimeEnd)
require.Error(t, err)
}
func TestDoCalc_ValidFormulaInvalidQueryCompact_ReturnsError(t *testing.T) {
_, _, fr := frameRequestForTest(t)
_, err := fr.doCalc(context.Background(), `sum(filter("this is not a valid query"))`, testTimeBegin, testTimeEnd)
require.Error(t, err)
}
func TestDoCalc_ValidQueryCompact_ReturnsDataFrameWithCalculatedTraces(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
dfbMock.On("NewNFromQuery", testutils.AnyContext, testTimeEnd, mock.Anything, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doCalc(context.Background(), `sum(filter("arch=x86"))`, testTimeBegin, testTimeEnd)
require.NoError(t, err)
assert.Equal(t, actualDf.TraceSet[`sum(filter("arch=x86"))`], types.Trace{3, 6, 9})
}
func TestDoCalc_ValidQueryTimeRange_ReturnsDataFrameWithCalculatedTraces(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.RequestType = REQUEST_TIME_RANGE
dfbMock.On("NewFromQueryAndRange", testutils.AnyContext, testTimeBegin, testTimeEnd, mock.Anything, true, fr.request.Progress).Return(df, nil)
actualDf, err := fr.doCalc(context.Background(), `sum(filter("arch=x86"))`, testTimeBegin, testTimeEnd)
require.NoError(t, err)
assert.Equal(t, actualDf.TraceSet[`sum(filter("arch=x86"))`], types.Trace{3, 6, 9})
}
func TestDoCalc_ValidFormulaInvalidShortcutCompact_ReturnsError(t *testing.T) {
_, _, fr := frameRequestForTest(t)
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(nil, errTestError)
_, err := fr.doCalc(context.Background(), fmt.Sprintf(`shortcut("%s")`, testShortcutKey), testTimeBegin, testTimeEnd)
require.Error(t, err)
}
func TestDoCalc_ValidFormulaValidShortcutCompact_ReturnsDataFrameWithCalculatedTracesFromShortcut(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
// Create valid shortCut.Shortcut for "Get" to return.
shortCutKeys := []string{}
copy(shortCutKeys, df.ParamSet.Keys())
sort.Strings(shortCutKeys)
shortCut := &shortcut.Shortcut{
Keys: shortCutKeys,
}
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(shortCut, nil)
dfbMock.On("NewNFromKeys", testutils.AnyContext, testTimeEnd, shortCut.Keys, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
formula := fmt.Sprintf(`sum(shortcut("%s"))`, testShortcutKey)
actualDf, err := fr.doCalc(context.Background(), formula, testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, actualDf.TraceSet[formula], types.Trace{3, 6, 9})
}
func TestDoCalc_ValidFormulaValidShortcutTimeRange_ReturnsDataFrameWithCalculatedTracesFromShortcut(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.RequestType = REQUEST_TIME_RANGE
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
// Create valid shortCut.Shortcut for "Get" to return.
shortCutKeys := []string{}
copy(shortCutKeys, df.ParamSet.Keys())
sort.Strings(shortCutKeys)
shortCut := &shortcut.Shortcut{
Keys: shortCutKeys,
}
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(shortCut, nil)
dfbMock.On("NewFromKeysAndRange", testutils.AnyContext, shortCut.Keys, testTimeBegin, testTimeEnd, true, fr.request.Progress).Return(df, nil)
formula := fmt.Sprintf(`sum(shortcut("%s"))`, testShortcutKey)
actualDf, err := fr.doCalc(context.Background(), formula, testTimeBegin, testTimeEnd)
require.NoError(t, err)
require.Equal(t, actualDf.TraceSet[formula], types.Trace{3, 6, 9})
}
func TestRun_QueryAndThenPivot_ReturnsPivotedDataFrame(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.Pivot = &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Sum,
}
fr.request.Begin = int(testTimeBegin.Unix())
fr.request.End = int(testTimeEnd.Unix())
dfbMock.On("NewNFromQuery", testutils.AnyContext, testTimeEnd, mock.Anything, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
actualDf, err := fr.run(context.Background())
require.NoError(t, err)
// You can tell this succeeded since the keys are changed to just include the pivot GroupBy keys.
require.Equal(t, actualDf.TraceSet[",config=565,"], types.Trace{2, 4, 6})
require.Equal(t, actualDf.TraceSet[",config=8888,"], types.Trace{1, 2, 3})
}
func TestRun_ValidQueryAndThenInvalidPivot_ReturnsError(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.Pivot = &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Operation("this-is-not-a-valid-operation"),
}
fr.request.Begin = int(testTimeBegin.Unix())
fr.request.End = int(testTimeEnd.Unix())
dfbMock.On("NewNFromQuery", testutils.AnyContext, testTimeEnd, mock.Anything, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
_, err := fr.run(context.Background())
require.Error(t, err)
}
func TestRun_KeysAndThenPivot_ReturnsPivotedDataFrame(t *testing.T) {
dfbMock, df, fr := frameRequestForTest(t)
fr.request.Pivot = &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Sum,
}
fr.request.Begin = int(testTimeBegin.Unix())
fr.request.End = int(testTimeEnd.Unix())
fr.request.Queries = []string{}
fr.request.Keys = testShortcutKey
// Create valid shortCut.Shortcut for "Get" to return.
shortCutKeys := []string{}
copy(shortCutKeys, df.ParamSet.Keys())
sort.Strings(shortCutKeys)
shortCut := &shortcut.Shortcut{
Keys: shortCutKeys,
}
ssMock := fr.shortcutStore.(*shortcutStoreMock.Store)
ssMock.On("Get", testutils.AnyContext, testShortcutKey).Return(shortCut, nil)
dfbMock.On("NewNFromKeys", testutils.AnyContext, testTimeEnd, shortCut.Keys, fr.request.NumCommits, fr.request.Progress).Return(df, nil)
actualDf, err := fr.run(context.Background())
require.NoError(t, err)
// You can tell this succeeded since the keys are changed to just include the pivot GroupBy keys.
require.Equal(t, actualDf.TraceSet[",config=565,"], types.Trace{2, 4, 6})
require.Equal(t, actualDf.TraceSet[",config=8888,"], types.Trace{1, 2, 3})
}
func TestResponseFromDataFrame_NullPivot_ReturnsDisplayModePlot(t *testing.T) {
_, df, _ := frameRequestForTest(t)
resp, err := ResponseFromDataFrame(context.Background(), nil, df, nil, false, progress.New())
require.NoError(t, err)
require.Equal(t, DisplayPlot, resp.DisplayMode)
}
func TestResponseFromDataFrame_ValidPivotRequestForPlot_ReturnsDisplayModePivotPlot(t *testing.T) {
_, df, _ := frameRequestForTest(t)
pivotRequest := &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Sum,
}
resp, err := ResponseFromDataFrame(context.Background(), pivotRequest, df, nil, false, progress.New())
require.NoError(t, err)
require.Equal(t, DisplayPivotPlot, resp.DisplayMode)
}
func TestResponseFromDataFrame_ValidPivotRequestForPivotTable_ReturnsDisplayModePivotTable(t *testing.T) {
_, df, _ := frameRequestForTest(t)
pivotRequest := &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Sum,
Summary: []pivot.Operation{pivot.Avg},
}
resp, err := ResponseFromDataFrame(context.Background(), pivotRequest, df, nil, false, progress.New())
require.NoError(t, err)
require.Equal(t, DisplayPivotTable, resp.DisplayMode)
}
func TestAddAnomaliesToResponse_GotAnomalies_Success(t *testing.T) {
resp := buildResponse(t)
mockChromePerf := chromeperfMock.NewAnomalyApiClient(t)
mockChromePerf.On("GetAnomalies", testutils.AnyContext, traceNames, startCommitPosition, endCommitPosition).Return(chromePerfAnomalyMap, nil)
anomayStore, err := cache.New(mockChromePerf)
require.NoError(t, err)
addRevisionBasedAnomaliesToResponse(ctx, resp, anomayStore, nil)
expectedAnomalyMap := chromeperf.AnomalyMap{
traceName1: map[types.CommitNumber]chromeperf.Anomaly{12: anomaly1},
traceName2: map[types.CommitNumber]chromeperf.Anomaly{15: anomaly2},
}
assert.Equal(t, expectedAnomalyMap, resp.AnomalyMap)
}
func TestAddAnomaliesToResponse_ErrorGetAnomalies_GotEmptyAnomalyMap(t *testing.T) {
resp := buildResponse(t)
mockChromePerf := chromeperfMock.NewAnomalyApiClient(t)
mockChromePerf.On("GetAnomalies", testutils.AnyContext, traceNames, startCommitPosition, endCommitPosition).Return(nil, errMock)
anomayStore, err := cache.New(mockChromePerf)
require.NoError(t, err)
addRevisionBasedAnomaliesToResponse(ctx, resp, anomayStore, nil)
assert.Equal(t, chromeperf.AnomalyMap{}, resp.AnomalyMap)
}
func buildResponse(t *testing.T) *FrameResponse {
_, df, _ := frameRequestForTest(t)
df.TraceSet = traceSet
df.Header[0].Offset = types.CommitNumber(startCommitPosition)
df.Header[len(df.Header)-1].Offset = types.CommitNumber(endCommitPosition)
pivotRequest := &pivot.Request{
GroupBy: []string{"config"},
Operation: pivot.Sum,
}
resp, err := ResponseFromDataFrame(context.Background(), pivotRequest, df, nil, false, progress.New())
require.NoError(t, err)
return resp
}