package state import ( "context" "errors" "fmt" "math/rand" "sort" "testing" "time" "github.com/benbjohnson/clock" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" ) // Not for parallel tests. type CountingImageService struct { Called int } func (c *CountingImageService) NewImage(_ context.Context, _ *ngmodels.AlertRule) (*ngmodels.Image, error) { c.Called += 1 return &ngmodels.Image{ Token: fmt.Sprint(rand.Int()), }, nil } func TestStateIsStale(t *testing.T) { now := time.Now() intervalSeconds := rand.Int63n(10) + 5 testCases := []struct { name string lastEvaluation time.Time expectedResult bool }{ { name: "false if last evaluation is now", lastEvaluation: now, expectedResult: false, }, { name: "false if last evaluation is 1 interval before now", lastEvaluation: now.Add(-time.Duration(intervalSeconds)), expectedResult: false, }, { name: "false if last evaluation is little less than 2 interval before now", lastEvaluation: now.Add(-time.Duration(intervalSeconds) * time.Second * 2).Add(100 * time.Millisecond), expectedResult: false, }, { name: "true if last evaluation is 2 intervals from now", lastEvaluation: now.Add(-time.Duration(intervalSeconds) * time.Second * 2), expectedResult: true, }, { name: "true if last evaluation is 3 intervals from now", lastEvaluation: now.Add(-time.Duration(intervalSeconds) * time.Second * 3), expectedResult: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.expectedResult, stateIsStale(now, tc.lastEvaluation, intervalSeconds)) }) } } // TestProcessEvalResults_StateTransitions tests how state.Manager's ProcessEvalResults processes results and creates or changes states. // In other words, it tests the state transition. // // The tests use a micro-framework that has the following features: // 1. It uses a base rule definition and allows each test case mutate its copy. // 2. Expected State definition omits several fields which are patched before assertion // if they are not specified explicitly (see function "patchState" for patched fields). // This allows specifications to be more condense and mention only important fields. // 3. Expected State definition uses some shortcut functions to make the specification more clear. // Expected labels are populated from a labels map where keys = description of what labels included in its values. // This allows us to specify the list of labels expected to be in the state in one line, e.g. "system + rule + labels1" // Evaluations are populated using function `newEvaluation` that pre-set all important fields. // 4. Each test case can contain multiple consecutive evaluations at different times with assertions at every interval. // The framework offers variables t1, t2, t3 and function tN(n) that provide timestamps of different evaluations. // 5. NoData and Error tests require assertions for all possible execution options for the same input. // // # Naming convention for tests cases. // // The tests are formatted to the input characteristics, such as rule definition, // result format (multi- or single- dimensional) and at which times the assertions are defined. // //