Skip to content

Commit c69b2d7

Browse files
committed
incidents: add some basic ProcessEvent testing
1 parent ec519bc commit c69b2d7

1 file changed

Lines changed: 166 additions & 74 deletions

File tree

internal/incident/incidents_test.go

Lines changed: 166 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package incident
22

33
import (
44
"context"
5+
"testing"
6+
"time"
7+
58
"github.com/icinga/icinga-go-library/database"
69
"github.com/icinga/icinga-go-library/logging"
710
baseEv "github.com/icinga/icinga-go-library/notifications/event"
@@ -13,14 +16,16 @@ import (
1316
"github.com/jmoiron/sqlx"
1417
"github.com/stretchr/testify/assert"
1518
"github.com/stretchr/testify/require"
19+
"go.uber.org/zap"
20+
"go.uber.org/zap/zapcore"
1621
"go.uber.org/zap/zaptest"
17-
"testing"
18-
"time"
1922
)
2023

21-
func TestLoadOpenIncidents(t *testing.T) {
22-
ctx := context.Background()
23-
db := testutils.GetTestDB(ctx, t)
24+
func TestIncidents(t *testing.T) {
25+
db := testutils.GetTestDB(t.Context(), t)
26+
logs := logging.NewLoggingWithFactory("testing", zapcore.DebugLevel, time.Second, func(level zap.AtomicLevel) zapcore.Core {
27+
return zaptest.NewLogger(t, zaptest.Level(level.Level())).Core()
28+
})
2429

2530
// Insert a dummy source for our test cases!
2631
source := &config.Source{
@@ -31,7 +36,7 @@ func TestLoadOpenIncidents(t *testing.T) {
3136
source.ChangedAt = types.UnixMilli(time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
3237
source.Deleted = types.Bool{Bool: false, Valid: true}
3338

34-
err := db.ExecTx(ctx, nil, func(ctx context.Context, tx *sqlx.Tx) error {
39+
err := db.ExecTx(t.Context(), nil, func(ctx context.Context, tx *sqlx.Tx) error {
3540
id, err := database.InsertObtainID(ctx, tx, database.BuildInsertStmtWithout(db, source, "id"), source)
3641
require.NoError(t, err, "populating source table should not fail")
3742

@@ -40,80 +45,160 @@ func TestLoadOpenIncidents(t *testing.T) {
4045
})
4146
require.NoError(t, err, "db.ExecTx should not fail")
4247

43-
// Reduce the default placeholders per statement to a meaningful number, so that we can
44-
// test some parallelism when loading the incidents.
45-
db.Options.MaxPlaceholdersPerStatement = 100
48+
runtimeConfig := config.NewRuntimeConfig(logs, db)
49+
require.NoError(t, runtimeConfig.UpdateFromDatabase(t.Context()))
4650

47-
// Due to the 10*maxPlaceholders constraint, only 10 goroutines are going to process simultaneously.
48-
// Therefore, reduce the default maximum number of connections per table to 4 in order to fully simulate
49-
// semaphore lock wait cycles for a given table.
50-
db.Options.MaxConnectionsPerTable = 4
51+
t.Run("LoadOpenIncidents", func(t *testing.T) {
52+
// Reduce the default placeholders per statement to a meaningful number, so that we can
53+
// test some parallelism when loading the incidents.
54+
db.Options.MaxPlaceholdersPerStatement = 100
5155

52-
testData := make(map[string]*Incident, 10*db.Options.MaxPlaceholdersPerStatement)
53-
for j := 1; j <= 10*db.Options.MaxPlaceholdersPerStatement; j++ {
54-
i := makeIncident(ctx, db, t, source.ID, false)
55-
testData[i.ObjectID.String()] = i
56-
}
56+
// Due to the 10*maxPlaceholders constraint, only 10 goroutines are going to process simultaneously.
57+
// Therefore, reduce the default maximum number of connections per table to 4 in order to fully simulate
58+
// semaphore lock wait cycles for a given table.
59+
db.Options.MaxConnectionsPerTable = 4
5760

58-
t.Run("WithNoRecoveredIncidents", func(t *testing.T) {
59-
assertIncidents(ctx, db, t, testData)
60-
})
61+
testData := make(map[string]*Incident, 10*db.Options.MaxPlaceholdersPerStatement)
62+
for j := 1; j <= 10*db.Options.MaxPlaceholdersPerStatement; j++ {
63+
i := makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityCrit)))
64+
testData[i.ObjectID.String()] = i
65+
}
66+
67+
t.Run("WithNoRecoveredIncidents", func(t *testing.T) {
68+
assertIncidents(t.Context(), db, runtimeConfig, t, testData)
69+
})
6170

62-
t.Run("WithSomeRecoveredIncidents", func(t *testing.T) {
63-
tx, err := db.BeginTxx(ctx, nil)
64-
require.NoError(t, err, "starting a transaction should not fail")
71+
t.Run("WithSomeRecoveredIncidents", func(t *testing.T) {
72+
// Drop all cached incidents before re-loading them!
73+
for _, i := range GetCurrentIncidents() {
74+
// Mark some of the existing incidents as recovered.
75+
if i.Id%20 == 0 { // 1000 / 20 => 50 existing incidents will be marked as recovered!
76+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withClose(), withTags(i.Object.Tags))))
77+
require.False(t, i.RecoveredAt.Time().IsZero(), "incident should be marked as recovered after processing a close event")
6578

66-
// Drop all cached incidents before re-loading them!
67-
for _, i := range GetCurrentIncidents() {
68-
RemoveCurrent(i.Object)
79+
// Drop it from our test data as it's recovered
80+
delete(testData, i.ObjectID.String())
81+
} else {
82+
RemoveCurrent(i.Object) // Drop it from the cache to simulate a daemon reload.
83+
}
84+
}
6985

70-
// Mark some of the existing incidents as recovered.
71-
if i.Id%20 == 0 { // 1000 / 20 => 50 existing incidents will be marked as recovered!
72-
i.RecoveredAt = types.UnixMilli(time.Now())
86+
assert.Len(t, GetCurrentIncidents(), 0, "there should be no cached incidents")
7387

74-
require.NoError(t, i.Sync(ctx, tx), "failed to update/insert incident")
88+
for j := 1; j <= db.Options.MaxPlaceholdersPerStatement/2; j++ {
89+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withIncident(), withClose(), withSeverity(baseEv.SeverityAlert))))
7590

76-
// Drop it from our test data as it's recovered
77-
delete(testData, i.ObjectID.String())
91+
if j%2 == 0 {
92+
// Add some extra new not recovered incidents to fully simulate a daemon reload.
93+
i := makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityWarning)))
94+
testData[i.ObjectID.String()] = i
95+
}
7896
}
79-
}
80-
require.NoError(t, tx.Commit(), "committing a transaction should not fail")
81-
82-
assert.Len(t, GetCurrentIncidents(), 0, "there should be no cached incidents")
8397

84-
for j := 1; j <= db.Options.MaxPlaceholdersPerStatement/2; j++ {
85-
// We don't need to cache recovered incidents in memory.
86-
_ = makeIncident(ctx, db, t, source.ID, true)
98+
assertIncidents(t.Context(), db, runtimeConfig, t, testData)
8799

88-
if j%2 == 0 {
89-
// Add some extra new not recovered incidents to fully simulate a daemon reload.
90-
i := makeIncident(ctx, db, t, source.ID, false)
91-
testData[i.ObjectID.String()] = i
100+
// Close all remaining incidents to clean up the database for the next test run.
101+
for _, i := range GetCurrentIncidents() {
102+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withClose(), withTags(i.Object.Tags))))
92103
}
93-
}
104+
assert.Len(t, GetCurrentIncidents(), 0, "there should be no cached incidents")
105+
testData = make(map[string]*Incident) // Reset test data for the next test run.
106+
})
107+
})
94108

95-
assertIncidents(ctx, db, t, testData)
109+
t.Run("Severity Change", func(t *testing.T) {
110+
i := makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityDebug)))
111+
assert.NotZero(t, i.ID())
112+
assert.True(t, i.RecoveredAt.Time().IsZero())
113+
assert.Equal(t, baseEv.SeverityDebug, i.Severity)
114+
115+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityEmerg), withTags(i.Object.Tags))))
116+
assert.Equal(t, baseEv.SeverityEmerg, i.Severity)
117+
118+
err := ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withSeverity(baseEv.SeverityNotice), withTags(i.Object.Tags)))
119+
require.ErrorIs(t, err, ErrSeverityChangeWithoutIncidentFlag)
120+
assert.Equal(t, baseEv.SeverityEmerg, i.Severity)
121+
})
122+
123+
t.Run("Close Flag", func(t *testing.T) {
124+
// Incident opened and closed immediately, so it won't be in the cache anymore.
125+
require.Nil(t, makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withClose(), withSeverity(baseEv.SeverityDebug))))
126+
127+
i := makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityInfo)))
128+
assert.True(t, i.RecoveredAt.Time().IsZero())
129+
assert.Equal(t, baseEv.SeverityInfo, i.Severity)
130+
131+
// Closing incident with the incident flag will mark it as recovered and update the severity at the same time.
132+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withIncident(), withClose(), withSeverity(baseEv.SeverityEmerg), withTags(i.Object.Tags))))
133+
assert.False(t, i.RecoveredAt.Time().IsZero())
134+
assert.Equal(t, baseEv.SeverityEmerg, i.Severity)
135+
136+
i = makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityWarning)))
137+
assert.True(t, i.RecoveredAt.Time().IsZero())
138+
assert.Equal(t, baseEv.SeverityWarning, i.Severity)
139+
140+
// Closing incident without the incident flag have no other side effects than marking it as recovered.
141+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withClose(), withTags(i.Object.Tags))))
142+
assert.False(t, i.RecoveredAt.Time().IsZero())
143+
assert.Equal(t, baseEv.SeverityWarning, i.Severity)
144+
})
145+
146+
t.Run("Notify Flag", func(t *testing.T) {
147+
t.Skipf("Skipping Notify Flag test, as it requires to verify whether notifications were sent")
148+
})
149+
150+
t.Run("Muted Flag", func(t *testing.T) {
151+
i := makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityDebug), withMuted(true)))
152+
assert.Equal(t, baseEv.SeverityDebug, i.Severity)
153+
assert.True(t, i.IsMuted())
154+
assert.Equal(t, "You're gonna have a bad time!", i.MuteReason.String)
155+
156+
// Unmute it with the incident flag still set...
157+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withIncident(), withMuted(false), withTags(i.Object.Tags))))
158+
assert.Equal(t, baseEv.SeverityDebug, i.Severity)
159+
assert.False(t, i.IsMuted())
160+
assert.Equal(t, "", i.MuteReason.String)
161+
162+
i = makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withIncident(), withSeverity(baseEv.SeverityDebug), withMuted(true)))
163+
assert.Equal(t, baseEv.SeverityDebug, i.Severity)
164+
assert.True(t, i.IsMuted())
165+
assert.Equal(t, "You're gonna have a bad time!", i.MuteReason.String)
166+
167+
// Unmute it without the incident flag set...
168+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, makeEvent(t, source.ID, withMuted(false), withTags(i.Object.Tags))))
169+
assert.Equal(t, baseEv.SeverityDebug, i.Severity)
170+
assert.False(t, i.IsMuted())
171+
assert.Equal(t, "", i.MuteReason.String)
172+
173+
// Muted flag without the incident flag has no effect on non-existing incidents.
174+
i = makeIncident(db, logs, runtimeConfig, t, makeEvent(t, source.ID, withMuted(true)))
175+
require.Nil(t, i)
96176
})
97177
}
98178

99179
// assertIncidents restores all not recovered incidents from the database and asserts them based on the given testData.
100180
//
101181
// The incident loading process is limited to a maximum duration of 10 seconds and will be
102182
// aborted and causes the entire test suite to fail immediately, if it takes longer.
103-
func assertIncidents(ctx context.Context, db *database.DB, t *testing.T, testData map[string]*Incident) {
183+
func assertIncidents(ctx context.Context, db *database.DB, runtimeConfig *config.RuntimeConfig, t *testing.T, testData map[string]*Incident) {
104184
logger := logging.NewLogger(zaptest.NewLogger(t).Sugar(), time.Hour)
105185

106186
// Since we have been using object.FromEvent() to persist the test objects to the database,
107187
// these will be automatically added to the objects cache as well. So clear the cache before
108188
// reloading the incidents, otherwise it will panic in object.RestoreObjects().
109189
object.ClearCache()
110190

191+
// Clear the incidents for the same reasons as above.
192+
currentIncidentsMu.Lock()
193+
currentIncidents = make(map[*object.Object]*Incident)
194+
currentIncidentsMu.Unlock()
195+
111196
// The incident loading process may hang due to unknown bugs or semaphore lock waits.
112197
// Therefore, give it maximum time of 10s to finish normally, otherwise give up and fail.
113198
ctx, cancelFunc := context.WithDeadline(ctx, time.Now().Add(10*time.Second))
114199
defer cancelFunc()
115200

116-
err := LoadOpenIncidents(ctx, db, logger, &config.RuntimeConfig{})
201+
err := LoadOpenIncidents(ctx, db, logger, runtimeConfig)
117202
require.NoError(t, err, "failed to load not recovered incidents")
118203

119204
incidents := GetCurrentIncidents()
@@ -141,38 +226,45 @@ func assertIncidents(ctx context.Context, db *database.DB, t *testing.T, testDat
141226
}
142227
}
143228

144-
// makeIncident returns a fully initialised recovered/un-recovered incident.
229+
// makeIncident creates a new incident by processing the given event and returns the resulting incident object.
145230
//
146-
// This will firstly create and synchronise a new object from a freshly generated dummy event with distinct
147-
// tags and name, and ensures that no error is returned, otherwise it will cause the entire test suite to fail.
148-
// Once the object has been successfully synchronised, an incident is created and synced with the database.
149-
func makeIncident(ctx context.Context, db *database.DB, t *testing.T, sourceID int64, recovered bool) *Incident {
231+
// The incident is guaranteed to be fully initialized and ready for assertions but might be nil if it's immediately closed.
232+
func makeIncident(db *database.DB, logs *logging.Logging, runtimeConfig *config.RuntimeConfig, t *testing.T, ev *event.Event) *Incident {
233+
require.NoError(t, ProcessEvent(t.Context(), db, logs, runtimeConfig, ev))
234+
return GetCurrent(db, object.Get(db, ev), logs.GetChildLogger("incident"), runtimeConfig, false)
235+
}
236+
237+
// makeEvent returns a fully initialized event based on the given parameters.
238+
func makeEvent(t *testing.T, sourceID int64, opts ...eventOption) *event.Event {
150239
ev := &event.Event{
151-
Time: time.Time{},
240+
Time: time.Now().Add(-2 * time.Hour).Truncate(time.Second),
152241
SourceId: sourceID,
153-
Event: baseEv.Event{
154-
Name: testutils.MakeRandomString(t),
155-
Tags: map[string]string{ // Always generate unique object tags not to produce same object ID!
156-
"host": testutils.MakeRandomString(t),
157-
"service": testutils.MakeRandomString(t),
158-
},
159-
},
242+
Event: baseEv.Event{Name: testutils.MakeRandomString(t)},
243+
}
244+
for _, opt := range opts {
245+
opt(ev)
246+
}
247+
if ev.Tags == nil {
248+
ev.Tags = map[string]string{ // Always generate unique object tags not to produce same object ID!
249+
"host": testutils.MakeRandomString(t),
250+
"service": testutils.MakeRandomString(t),
251+
}
160252
}
161253

162-
i := NewIncident(db, object.Get(db, ev), &config.RuntimeConfig{}, nil)
163-
i.StartedAt = types.UnixMilli(time.Now().Add(-2 * time.Hour).Truncate(time.Second))
164-
i.Severity = baseEv.SeverityCrit
165-
if recovered {
166-
i.Severity = baseEv.SeverityOK
167-
i.RecoveredAt = types.UnixMilli(time.Now())
254+
if ev.IsMuted() {
255+
ev.MutedReason = "You're gonna have a bad time!"
168256
}
257+
require.NoError(t, ev.Validate(), "failed to validate event")
258+
return ev
259+
}
169260

170-
require.NoError(t, db.ExecTx(ctx, nil, func(ctx context.Context, tx *sqlx.Tx) error {
171-
if err := i.Object.SyncFromEvent(ctx, tx, ev); err != nil {
172-
return err
173-
}
174-
return i.Sync(ctx, tx)
175-
}))
261+
// eventOption is a functional option type for modifying an event.
262+
type eventOption func(*event.Event)
176263

177-
return i
264+
func withIncident() eventOption { return func(ev *event.Event) { ev.Incident = types.MakeBool(true) } }
265+
func withClose() eventOption { return func(ev *event.Event) { ev.Close = types.MakeBool(true) } }
266+
func withMuted(v bool) eventOption { return func(ev *event.Event) { ev.Muted = types.MakeBool(v) } }
267+
func withTags(tags map[string]string) eventOption { return func(ev *event.Event) { ev.Tags = tags } }
268+
func withSeverity(sev baseEv.Severity) eventOption {
269+
return func(ev *event.Event) { ev.Severity = sev }
178270
}

0 commit comments

Comments
 (0)