blob: e9e2789b5df13bf35b63261c22544381c0449010 [file]
package internal
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/pinpoint/go/common"
"go.skia.org/infra/pinpoint/go/compare"
"go.skia.org/infra/pinpoint/go/sql/schema"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/testsuite"
"go.temporal.io/sdk/workflow"
"go.skia.org/infra/pinpoint/go/workflows"
pinpoint_proto "go.skia.org/infra/pinpoint/proto/v1"
)
const mockChart = "chart"
var mockCommit = pinpoint_proto.CombinedCommit{
Main: common.NewChromiumCommit("fake-commit"),
}
var mockBuild = pinpoint_proto.CASReference{
CasInstance: "projects/chromium-swarm/instances/default_instance",
Digest: &pinpoint_proto.CASReference_Digest{
Hash: "98f6b9deb73c81bd80f63149e66446547fa1fd332c382d358a6f6f1afb1b1ebb",
SizeBytes: 732,
},
}
func registerJobStoreActivities(env *testsuite.TestWorkflowEnvironment) {
env.RegisterActivityWithOptions(
func(context.Context, *pinpoint_proto.SchedulePairwiseRequest, string) error { return nil },
activity.RegisterOptions{Name: AddInitialJob},
)
env.RegisterActivityWithOptions(
func(context.Context, string, string, int64) error { return nil },
activity.RegisterOptions{Name: UpdateJobStatus},
)
env.RegisterActivityWithOptions(
func(context.Context, string, string) error { return nil },
activity.RegisterOptions{Name: SetErrors},
)
env.RegisterActivityWithOptions(
func(context.Context, string, map[string]*pinpoint_proto.PairwiseExecution_WilcoxonResult) error {
return nil
},
activity.RegisterOptions{Name: AddResults},
)
env.RegisterActivityWithOptions(
func(context.Context, string, *schema.CommitRunData, *schema.CommitRunData) error { return nil },
activity.RegisterOptions{Name: AddCommitRuns},
)
}
func TestPairwiseWorkflow_GivenUnsuccessfulWorkflow_ReturnsError(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
registerJobStoreActivities(env)
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.OnWorkflow(workflows.PairwiseCommitsRunner, mock.Anything, mock.Anything).Return(nil, skerr.Fmt("some error")).Once()
env.OnActivity(AddInitialJob, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(SetErrors, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, running, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, failed, mock.Anything).Return(nil).Once()
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
StartCommit: &mockCommit,
EndCommit: &mockCommit,
Chart: mockChart,
},
})
require.True(t, env.IsWorkflowCompleted())
require.Error(t, env.GetWorkflowError())
var pe *pinpoint_proto.PairwiseExecution
require.Error(t, env.GetWorkflowResult(&pe))
assert.Nil(t, pe)
env.AssertExpectations(t)
}
func TestPairwiseWorkflow_GivenSuccessfulWorkflow_ReturnsCorrectPValues(t *testing.T) {
mockedValuesA := []float64{1.2, 2.2, 1.6, 2.0}
mockedValuesB := []float64{5.5, 6.6, 5.8, 6.3}
mockResult := &PairwiseRun{
Left: CommitRun{
Runs: []*workflows.TestRun{
{
Values: map[string][]float64{
mockChart: mockedValuesA,
},
},
},
},
Right: CommitRun{
Runs: []*workflows.TestRun{
{
Values: map[string][]float64{
mockChart: mockedValuesB,
},
},
},
},
}
statResults, err := compare.ComparePairwise(mockedValuesA, mockedValuesB, compare.UnknownDir)
require.NoError(t, err)
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
registerJobStoreActivities(env)
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.OnWorkflow(workflows.PairwiseCommitsRunner, mock.Anything, mock.Anything).Return(mockResult, nil).Once()
env.OnActivity(ComparePairwiseActivity, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(statResults, nil).Once()
env.OnActivity(AddInitialJob, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(AddCommitRuns, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, running, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, completed, mock.Anything).Return(nil).Once()
env.OnActivity(AddResults, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
StartCommit: &mockCommit,
EndCommit: &mockCommit,
Chart: mockChart,
},
})
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var pe *pinpoint_proto.PairwiseExecution
require.NoError(t, env.GetWorkflowResult(&pe))
assert.NotNil(t, pe)
assert.NotEmpty(t, pe.JobId)
assert.InDelta(t, statResults.PValue, pe.Results[mockChart].PValue, 1e-9)
assert.Nil(t, pe.CulpritCandidate)
env.AssertExpectations(t)
}
func TestPairwiseWorkflow_GivenSuccessfulWorkflowWithCulprit_ReturnsCulprit(t *testing.T) {
mockedValuesA := []float64{1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9}
mockedValuesB := []float64{10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9}
mockResult := &PairwiseRun{
Left: CommitRun{
Runs: []*workflows.TestRun{
{
Values: map[string][]float64{
mockChart: mockedValuesA,
},
},
},
},
Right: CommitRun{
Runs: []*workflows.TestRun{
{
Values: map[string][]float64{
mockChart: mockedValuesB,
},
},
},
},
}
statResults, err := compare.ComparePairwise(mockedValuesA, mockedValuesB, compare.UnknownDir)
require.NoError(t, err)
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
registerJobStoreActivities(env)
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.OnWorkflow(workflows.PairwiseCommitsRunner, mock.Anything, mock.Anything).Return(mockResult, nil).Once()
env.OnActivity(ComparePairwiseActivity, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(statResults, nil).Once()
env.OnActivity(AddInitialJob, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(AddCommitRuns, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, running, mock.Anything).Return(nil).Once()
env.OnActivity(UpdateJobStatus, mock.Anything, mock.Anything, completed, mock.Anything).Return(nil).Once()
env.OnActivity(AddResults, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
Chart: mockChart,
StartBuild: &mockBuild,
EndCommit: &mockCommit,
},
CulpritVerify: true, // if this is true, return the culprit
})
require.True(t, env.IsWorkflowCompleted())
require.NoError(t, env.GetWorkflowError())
var pe *pinpoint_proto.PairwiseExecution
require.NoError(t, env.GetWorkflowResult(&pe))
assert.NotNil(t, pe)
assert.NotEmpty(t, pe.JobId)
assert.InDelta(t, statResults.PValue, pe.Results[mockChart].PValue, 1e-9)
assert.Equal(t, "fake-commit", pe.CulpritCandidate.Main.GitHash)
env.AssertExpectations(t)
}
func TestPairwiseWorkflow_GivenNoStartBuild_ReturnsError(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
Chart: mockChart,
},
})
require.True(t, env.IsWorkflowCompleted())
require.Error(t, env.GetWorkflowError())
require.ErrorContains(t, env.GetWorkflowError(), "Base build and commit are empty")
var pe *pinpoint_proto.PairwiseExecution
require.Error(t, env.GetWorkflowResult(&pe))
assert.Nil(t, pe)
env.AssertExpectations(t)
}
func TestPairwiseWorkflow_GivenNoEndBuild_ReturnsError(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
StartBuild: &mockBuild,
Chart: mockChart,
},
})
require.True(t, env.IsWorkflowCompleted())
require.Error(t, env.GetWorkflowError())
require.ErrorContains(t, env.GetWorkflowError(), "Experiment build and commit are empty")
var pe *pinpoint_proto.PairwiseExecution
require.Error(t, env.GetWorkflowResult(&pe))
assert.Nil(t, pe)
env.AssertExpectations(t)
}
func TestPairwiseWorkflow_GivenBadCas_ReturnsError(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
env.RegisterWorkflowWithOptions(PairwiseCommitsRunnerWorkflow, workflow.RegisterOptions{Name: workflows.PairwiseCommitsRunner})
env.ExecuteWorkflow(PairwiseWorkflow, &workflows.PairwiseParams{
Request: &pinpoint_proto.SchedulePairwiseRequest{
StartBuild: &mockBuild,
EndBuild: &pinpoint_proto.CASReference{},
Chart: mockChart,
},
})
require.True(t, env.IsWorkflowCompleted())
require.Error(t, env.GetWorkflowError())
require.ErrorContains(t, env.GetWorkflowError(), "end build is invalid")
var pe *pinpoint_proto.PairwiseExecution
require.Error(t, env.GetWorkflowResult(&pe))
assert.Nil(t, pe)
env.AssertExpectations(t)
}
func TestSplitExtraArgs_GivenComplexArgs_CorrectlyParses(t *testing.T) {
testCases := []struct {
name string
input string
expected []string
expectErr bool
}{
{
name: "empty string",
input: "",
expected: nil,
},
{
name: "standard space separated",
input: "--flag1 --flag2",
expected: []string{"--flag1", "--flag2"},
},
{
name: "comma separated feature flag",
input: "--enable-features=featureA,featureB",
expected: []string{"--enable-features=featureA,featureB"},
},
{
name: "mixed comma and space separated",
input: "--enable-features=featureA,featureB --js-flags=turbolev",
expected: []string{"--enable-features=featureA,featureB", "--js-flags=turbolev"},
},
{
name: "unbalanced quotes error",
input: `--flag="unbalanced`,
expectErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res, err := splitExtraArgs(tc.input)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, res)
} else {
require.NoError(t, err)
assert.Equal(t, tc.expected, res)
}
})
}
}