github.com/mongodb/grip@v0.0.0-20240213223901-f906268d82b9/message/composer_test.go (about) 1 package message 2 3 import ( 4 "fmt" 5 "os" 6 "runtime" 7 "strings" 8 "testing" 9 10 "github.com/mongodb/grip/level" 11 "github.com/pkg/errors" 12 "github.com/stretchr/testify/assert" 13 "github.com/stretchr/testify/require" 14 ) 15 16 func TestPopulatedMessageComposerConstructors(t *testing.T) { 17 const testMsg = "hello" 18 assert := assert.New(t) 19 // map objects to output 20 cases := map[Composer]string{ 21 NewString(testMsg): testMsg, 22 NewDefaultMessage(level.Error, testMsg): testMsg, 23 NewExtendedString(testMsg): testMsg, 24 NewExtendedDefaultMessage(level.Error, testMsg): testMsg, 25 NewBytes([]byte(testMsg)): testMsg, 26 NewBytesMessage(level.Error, []byte(testMsg)): testMsg, 27 NewExtendedBytes([]byte(testMsg)): testMsg, 28 NewExtendedBytesMessage(level.Error, []byte(testMsg)): testMsg, 29 NewError(errors.New(testMsg)): testMsg, 30 NewErrorMessage(level.Error, errors.New(testMsg)): testMsg, 31 NewExtendedError(errors.New(testMsg)): testMsg, 32 NewExtendedErrorMessage(level.Error, errors.New(testMsg)): testMsg, 33 NewErrorWrap(errors.New(testMsg), ""): testMsg, 34 NewErrorWrapMessage(level.Error, errors.New(testMsg), ""): testMsg, 35 NewFormatted(string(testMsg[0])+"%s", testMsg[1:]): testMsg, 36 NewFormattedMessage(level.Error, string(testMsg[0])+"%s", testMsg[1:]): testMsg, 37 WrapError(errors.New(testMsg), ""): testMsg, 38 WrapErrorf(errors.New(testMsg), ""): testMsg, 39 NewLine(testMsg, ""): testMsg, 40 NewLineMessage(level.Error, testMsg, ""): testMsg, 41 NewLine(testMsg): testMsg, 42 NewLineMessage(level.Error, testMsg): testMsg, 43 MakeGroupComposer(NewString(testMsg)): testMsg, 44 NewGroupComposer([]Composer{NewString(testMsg)}): testMsg, 45 MakeJiraMessage(&JiraIssue{Summary: testMsg, Type: "Something"}): testMsg, 46 NewJiraMessage("", testMsg, JiraField{Key: "type", Value: "Something"}): testMsg, 47 NewFieldsMessage(level.Error, testMsg, Fields{}): fmt.Sprintf("[message='%s']", testMsg), 48 NewFields(level.Error, Fields{"test": testMsg}): fmt.Sprintf("[test='%s']", testMsg), 49 MakeFieldsMessage(testMsg, Fields{}): fmt.Sprintf("[message='%s']", testMsg), 50 MakeFields(Fields{"test": testMsg}): fmt.Sprintf("[test='%s']", testMsg), 51 NewExtendedFieldsMessage(level.Error, testMsg, Fields{}): fmt.Sprintf("[message='%s']", testMsg), 52 NewExtendedFields(level.Error, Fields{"test": testMsg}): fmt.Sprintf("[test='%s']", testMsg), 53 MakeExtendedFieldsMessage(testMsg, Fields{}): fmt.Sprintf("[message='%s']", testMsg), 54 MakeExtendedFields(Fields{"test": testMsg}): fmt.Sprintf("[test='%s']", testMsg), 55 NewErrorWrappedComposer(errors.New("hello"), NewString("world")): "world: hello", 56 When(true, testMsg): testMsg, 57 Whenf(true, testMsg): testMsg, 58 Whenln(true, testMsg): testMsg, 59 NewEmailMessage(level.Error, Email{ 60 Recipients: []string{"someone@example.com"}, 61 Subject: "Test msg", 62 Body: testMsg, 63 }): fmt.Sprintf("To: someone@example.com; Body: %s", testMsg), 64 NewGithubStatusMessage(level.Error, "tests", GithubStateError, "https://example.com", testMsg): fmt.Sprintf("tests error: %s (https://example.com)", testMsg), 65 NewGithubStatusMessageWithRepo(level.Error, GithubStatus{ 66 Owner: "mongodb", 67 Repo: "grip", 68 Ref: "master", 69 70 Context: "tests", 71 State: GithubStateError, 72 URL: "https://example.com", 73 Description: testMsg, 74 }): fmt.Sprintf("mongodb/grip@master tests error: %s (https://example.com)", testMsg), 75 NewJIRACommentMessage(level.Error, "ABC-123", testMsg): testMsg, 76 NewSlackMessage(level.Error, "@someone", testMsg, nil): fmt.Sprintf("@someone: %s", testMsg), 77 } 78 79 for msg, output := range cases { 80 assert.NotNil(msg) 81 assert.NotEmpty(output) 82 assert.Implements((*Composer)(nil), msg) 83 assert.True(msg.Loggable()) 84 assert.NotNil(msg.Raw()) 85 86 if strings.HasPrefix(output, "[") { 87 output = strings.Trim(output, "[]") 88 assert.True(strings.Contains(msg.String(), output), fmt.Sprintf("%T: %s (%s)", msg, msg.String(), output)) 89 90 } else { 91 // run the string test to make sure it doesn't change: 92 assert.Equal(msg.String(), output, "%T", msg) 93 assert.Equal(msg.String(), output, "%T", msg) 94 } 95 96 if msg.Priority() != level.Invalid { 97 assert.Equal(msg.Priority(), level.Error) 98 } 99 100 // check message annotation functionality 101 switch msg.(type) { 102 case *GroupComposer: 103 continue 104 case *slackMessage: 105 continue 106 default: 107 assert.NoError(msg.Annotate("k1", "foo"), "%T", msg) 108 assert.Error(msg.Annotate("k1", "foo"), "%T", msg) 109 assert.NoError(msg.Annotate("k2", "foo"), "%T", msg) 110 } 111 } 112 } 113 114 func TestUnpopulatedMessageComposers(t *testing.T) { 115 assert := assert.New(t) 116 // map objects to output 117 cases := []Composer{ 118 &stringMessage{}, 119 NewString(""), 120 NewDefaultMessage(level.Error, ""), 121 &bytesMessage{}, 122 NewBytes([]byte{}), 123 NewBytesMessage(level.Error, []byte{}), 124 &ProcessInfo{}, 125 &SystemInfo{}, 126 &lineMessenger{}, 127 NewLine(), 128 NewLineMessage(level.Error), 129 &formatMessenger{}, 130 NewFormatted(""), 131 NewFormattedMessage(level.Error, ""), 132 NewStack(1, ""), 133 NewStackLines(1), 134 NewStackFormatted(1, ""), 135 MakeGroupComposer(), 136 &GroupComposer{}, 137 &GoRuntimeInfo{}, 138 When(false, ""), 139 Whenf(false, "", ""), 140 Whenln(false, "", ""), 141 NewEmailMessage(level.Error, Email{}), 142 NewGithubStatusMessage(level.Error, "", GithubState(""), "", ""), 143 NewGithubStatusMessageWithRepo(level.Error, GithubStatus{}), 144 NewJIRACommentMessage(level.Error, "", ""), 145 NewSlackMessage(level.Error, "", "", nil), 146 } 147 148 for idx, msg := range cases { 149 assert.False(msg.Loggable(), "%d:%T", idx, msg) 150 } 151 } 152 153 func TestDataCollecterComposerConstructors(t *testing.T) { 154 const testMsg = "hello" 155 // map objects to output (prefix) 156 157 t.Run("Single", func(t *testing.T) { 158 for _, test := range []struct { 159 Name string 160 Msg Composer 161 Expected string 162 ShouldSkip bool 163 }{ 164 { 165 Name: "ProcessInfoCurrentProc", 166 Msg: NewProcessInfo(level.Error, int32(os.Getpid()), testMsg), 167 }, 168 { 169 Name: "NewSystemInfo", 170 Msg: NewSystemInfo(level.Error, testMsg), 171 Expected: testMsg, 172 }, 173 174 { 175 Name: "MakeSystemInfo", 176 Msg: MakeSystemInfo(testMsg), 177 Expected: testMsg, 178 }, 179 { 180 Name: "CollectProcInfoPidOne", 181 Msg: CollectProcessInfo(int32(1)), 182 ShouldSkip: runtime.GOOS == "windows", 183 }, 184 { 185 Name: "CollectProcInfoSelf", 186 Msg: CollectProcessInfoSelf(), 187 }, 188 { 189 Name: "CollectSystemInfo", 190 Msg: CollectSystemInfo(), 191 }, 192 { 193 Name: "CollectBasicGoStats", 194 Msg: CollectBasicGoStats(), 195 }, 196 { 197 Name: "CollectGoStatsDeltas", 198 Msg: CollectGoStatsDeltas(), 199 }, 200 { 201 Name: "CollectGoStatsRates", 202 Msg: CollectGoStatsRates(), 203 }, 204 { 205 Name: "CollectGoStatsTotals", 206 Msg: CollectGoStatsTotals(), 207 }, 208 { 209 Name: "MakeGoStatsDelta", 210 Msg: MakeGoStatsDeltas(testMsg), 211 Expected: testMsg, 212 }, 213 { 214 Name: "MakeGoStatsRates", 215 Msg: MakeGoStatsRates(testMsg), 216 Expected: testMsg, 217 }, 218 { 219 Name: "MakeGoStatsTotals", 220 Msg: MakeGoStatsTotals(testMsg), 221 Expected: testMsg, 222 }, 223 { 224 Name: "NewGoStatsDeltas", 225 Msg: NewGoStatsDeltas(level.Error, testMsg), 226 Expected: testMsg, 227 }, 228 { 229 Name: "NewGoStatsRates", 230 Msg: NewGoStatsRates(level.Error, testMsg), 231 Expected: testMsg, 232 }, 233 { 234 Name: "NewGoStatsTotals", 235 Msg: NewGoStatsTotals(level.Error, testMsg), 236 Expected: testMsg, 237 }, 238 } { 239 if test.ShouldSkip { 240 continue 241 } 242 t.Run(test.Name, func(t *testing.T) { 243 assert.NotNil(t, test.Msg) 244 assert.NotNil(t, test.Msg.Raw()) 245 assert.Implements(t, (*Composer)(nil), test.Msg) 246 assert.True(t, test.Msg.Loggable()) 247 assert.True(t, strings.HasPrefix(test.Msg.String(), test.Expected), "%T: %s", test.Msg, test.Msg) 248 }) 249 } 250 }) 251 252 t.Run("Multi", func(t *testing.T) { 253 for _, test := range []struct { 254 Name string 255 Group []Composer 256 ShouldSkip bool 257 }{ 258 { 259 Name: "SelfWithChildren", 260 Group: CollectProcessInfoSelfWithChildren(), 261 }, 262 { 263 Name: "PidOneWithChildren", 264 Group: CollectProcessInfoWithChildren(int32(1)), 265 ShouldSkip: runtime.GOOS == "windows", 266 }, 267 { 268 Name: "AllProcesses", 269 Group: CollectAllProcesses(), 270 }, 271 } { 272 if test.ShouldSkip { 273 continue 274 } 275 t.Run(test.Name, func(t *testing.T) { 276 require.True(t, len(test.Group) >= 1) 277 for _, msg := range test.Group { 278 assert.NotNil(t, msg) 279 assert.Implements(t, (*Composer)(nil), msg) 280 assert.NotEqual(t, "", msg.String()) 281 assert.True(t, msg.Loggable()) 282 } 283 }) 284 285 } 286 }) 287 } 288 289 func TestStackMessages(t *testing.T) { 290 const testMsg = "hello" 291 var stackMsg = "message/composer_test" 292 293 assert := assert.New(t) 294 // map objects to output (prefix) 295 cases := map[Composer]string{ 296 NewStack(1, testMsg): testMsg, 297 NewStackLines(1, testMsg): testMsg, 298 NewStackLines(1): "", 299 NewStackFormatted(1, "%s", testMsg): testMsg, 300 NewStackFormatted(1, string(testMsg[0])+"%s", testMsg[1:]): testMsg, 301 302 // with 0 frame 303 NewStack(0, testMsg): testMsg, 304 NewStackLines(0, testMsg): testMsg, 305 NewStackLines(0): "", 306 NewStackFormatted(0, "%s", testMsg): testMsg, 307 NewStackFormatted(0, string(testMsg[0])+"%s", testMsg[1:]): testMsg, 308 } 309 310 for msg, text := range cases { 311 assert.NotNil(msg) 312 assert.Implements((*Composer)(nil), msg) 313 assert.NotNil(msg.Raw()) 314 if text != "" { 315 assert.True(msg.Loggable()) 316 } 317 318 diagMsg := fmt.Sprintf("%T: %+v", msg, msg) 319 assert.True(strings.Contains(msg.String(), text), diagMsg) 320 assert.True(strings.Contains(msg.String(), stackMsg), diagMsg) 321 } 322 } 323 324 func TestComposerConverter(t *testing.T) { 325 const testMsg = "hello world" 326 assert := assert.New(t) 327 328 cases := []interface{}{ 329 NewLine(testMsg), 330 testMsg, 331 errors.New(testMsg), 332 []string{testMsg}, 333 []interface{}{testMsg}, 334 []byte(testMsg), 335 []Composer{NewString(testMsg)}, 336 } 337 338 for _, msg := range cases { 339 comp := ConvertToComposer(level.Error, msg) 340 assert.True(comp.Loggable()) 341 assert.Equal(testMsg, comp.String(), "%T", msg) 342 } 343 344 cases = []interface{}{ 345 nil, 346 "", 347 []interface{}{}, 348 []string{}, 349 []byte{}, 350 Fields{}, 351 map[string]interface{}{}, 352 } 353 354 for _, msg := range cases { 355 comp := ConvertToComposer(level.Error, msg) 356 assert.False(comp.Loggable()) 357 assert.Equal("", comp.String(), "%T", msg) 358 } 359 360 outputCases := map[string]interface{}{ 361 "1": 1, 362 "2": int32(2), 363 "[message='3'": Fields{"message": 3}, 364 "[message='4'": map[string]interface{}{"message": "4"}, 365 } 366 367 for out, in := range outputCases { 368 comp := ConvertToComposer(level.Error, in) 369 assert.True(comp.Loggable()) 370 assert.True(strings.HasPrefix(comp.String(), out)) 371 } 372 } 373 374 func TestJiraMessageComposerConstructor(t *testing.T) { 375 const testMsg = "hello" 376 assert := assert.New(t) 377 reporterField := JiraField{Key: "Reporter", Value: "Annie"} 378 assigneeField := JiraField{Key: "Assignee", Value: "Sejin"} 379 typeField := JiraField{Key: "Type", Value: "Bug"} 380 labelsField := JiraField{Key: "Labels", Value: []string{"Soul", "Pop"}} 381 unknownField := JiraField{Key: "Artist", Value: "Adele"} 382 msg := NewJiraMessage("project", testMsg, reporterField, assigneeField, typeField, labelsField, unknownField) 383 issue := msg.Raw().(*JiraIssue) 384 385 assert.Equal(issue.Project, "project") 386 assert.Equal(issue.Summary, testMsg) 387 assert.Equal(issue.Reporter, reporterField.Value) 388 assert.Equal(issue.Assignee, assigneeField.Value) 389 assert.Equal(issue.Type, typeField.Value) 390 assert.Equal(issue.Labels, labelsField.Value) 391 assert.Equal(issue.Fields[unknownField.Key], unknownField.Value) 392 } 393 394 func TestProcessTreeDoesNotHaveDuplicates(t *testing.T) { 395 assert := assert.New(t) 396 397 procs := CollectProcessInfoWithChildren(1) 398 seen := make(map[int32]struct{}) 399 400 for _, p := range procs { 401 pinfo, ok := p.(*ProcessInfo) 402 assert.True(ok) 403 seen[pinfo.Pid] = struct{}{} 404 } 405 406 assert.Equal(len(seen), len(procs)) 407 } 408 409 func TestJiraIssueAnnotationOnlySupportsStrings(t *testing.T) { 410 assert := assert.New(t) 411 412 m := &jiraMessage{ 413 issue: &JiraIssue{}, 414 } 415 416 assert.Error(m.Annotate("k", 1)) 417 assert.Error(m.Annotate("k", true)) 418 assert.Error(m.Annotate("k", nil)) 419 } 420 421 type causer interface { 422 Cause() error 423 } 424 425 func TestErrorComposers(t *testing.T) { 426 for name, cmp := range map[string]ErrorComposer{ 427 "Wrapped": WrapError(errors.New("err"), "msg"), 428 "Plain": NewError(errors.New("err")), 429 } { 430 t.Run(name, func(t *testing.T) { 431 t.Run("Interfaces", func(t *testing.T) { 432 assert.Implements(t, (*error)(nil), cmp) 433 assert.Implements(t, (*causer)(nil), cmp) 434 }) 435 t.Run("Value", func(t *testing.T) { 436 assert.Equal(t, cmp.Error(), cmp.String()) 437 }) 438 t.Run("Causer", func(t *testing.T) { 439 cause := errors.Cause(cmp) 440 assert.NotEqual(t, cause, cmp) 441 }) 442 t.Run("ExtendedFormat", func(t *testing.T) { 443 assert.NotEqual(t, fmt.Sprintf("%+v", cmp), fmt.Sprintf("%v", cmp)) 444 }) 445 }) 446 } 447 }