github.com/observiq/carbon@v0.9.11-0.20200820160507-1b872e368a5e/operator/builtin/parser/time_test.go (about) 1 package parser 2 3 import ( 4 "context" 5 "math" 6 "testing" 7 "time" 8 9 "github.com/observiq/carbon/entry" 10 "github.com/observiq/carbon/operator" 11 "github.com/observiq/carbon/operator/helper" 12 "github.com/observiq/carbon/testutil" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 ) 16 17 func TestIsZero(t *testing.T) { 18 require.True(t, (&helper.TimeParser{}).IsZero()) 19 require.False(t, (&helper.TimeParser{Layout: "strptime"}).IsZero()) 20 } 21 22 func TestTimeParser(t *testing.T) { 23 24 testCases := []struct { 25 name string 26 sample interface{} 27 gotimeLayout string 28 strptimeLayout string 29 }{ 30 { 31 name: "unix", 32 sample: "Mon Jan 2 15:04:05 MST 2006", 33 gotimeLayout: "Mon Jan 2 15:04:05 MST 2006", 34 strptimeLayout: "%a %b %e %H:%M:%S %Z %Y", 35 }, 36 { 37 name: "almost-unix", 38 sample: "Mon Jan 02 15:04:05 MST 2006", 39 gotimeLayout: "Mon Jan 02 15:04:05 MST 2006", 40 strptimeLayout: "%a %b %d %H:%M:%S %Z %Y", 41 }, 42 { 43 name: "kitchen", 44 sample: "12:34PM", 45 gotimeLayout: time.Kitchen, 46 strptimeLayout: "%H:%M%p", 47 }, 48 { 49 name: "kitchen-bytes", 50 sample: []byte("12:34PM"), 51 gotimeLayout: time.Kitchen, 52 strptimeLayout: "%H:%M%p", 53 }, 54 { 55 name: "countdown", 56 sample: "-0100 01 01 01 01 01 01", 57 gotimeLayout: "-0700 06 05 04 03 02 01", 58 strptimeLayout: "%z %y %S %M %H %e %m", 59 }, 60 { 61 name: "debian-syslog", 62 sample: "Jun 09 11:39:45", 63 gotimeLayout: "Jan 02 15:04:05", 64 strptimeLayout: "%b %d %H:%M:%S", 65 }, 66 { 67 name: "opendistro", 68 sample: "2020-06-09T15:39:58", 69 gotimeLayout: "2006-01-02T15:04:05", 70 strptimeLayout: "%Y-%m-%dT%H:%M:%S", 71 }, 72 { 73 name: "postgres", 74 sample: "2019-11-05 10:38:35.118 EST", 75 gotimeLayout: "2006-01-02 15:04:05.999 MST", 76 strptimeLayout: "%Y-%m-%d %H:%M:%S.%L %Z", 77 }, 78 { 79 name: "ibm-mq", 80 sample: "3/4/2018 11:52:29", 81 gotimeLayout: "1/2/2006 15:04:05", 82 strptimeLayout: "%q/%g/%Y %H:%M:%S", 83 }, 84 { 85 name: "cassandra", 86 sample: "2019-11-27T09:34:32.901-0500", 87 gotimeLayout: "2006-01-02T15:04:05.999-0700", 88 strptimeLayout: "%Y-%m-%dT%H:%M:%S.%L%z", 89 }, 90 { 91 name: "oracle", 92 sample: "2019-10-15T10:42:01.900436-04:00", 93 gotimeLayout: "2006-01-02T15:04:05.999999-07:00", 94 strptimeLayout: "%Y-%m-%dT%H:%M:%S.%f%j", 95 }, 96 { 97 name: "oracle-listener", 98 sample: "22-JUL-2019 15:16:13", 99 gotimeLayout: "02-Jan-2006 15:04:05", 100 strptimeLayout: "%d-%b-%Y %H:%M:%S", 101 }, 102 { 103 name: "k8s", 104 sample: "2019-03-08T18:41:12.152531115Z", 105 gotimeLayout: "2006-01-02T15:04:05.999999999Z", 106 strptimeLayout: "%Y-%m-%dT%H:%M:%S.%sZ", 107 }, 108 { 109 name: "jetty", 110 sample: "05/Aug/2019:20:38:46 +0000", 111 gotimeLayout: "02/Jan/2006:15:04:05 -0700", 112 strptimeLayout: "%d/%b/%Y:%H:%M:%S %z", 113 }, 114 { 115 name: "puppet", 116 sample: "Aug 4 03:26:02", 117 gotimeLayout: "Jan _2 15:04:05", 118 strptimeLayout: "%b %e %H:%M:%S", 119 }, 120 } 121 122 rootField := entry.NewRecordField() 123 someField := entry.NewRecordField("some_field") 124 125 for _, tc := range testCases { 126 t.Run(tc.name, func(t *testing.T) { 127 var sampleStr string 128 switch s := tc.sample.(type) { 129 case string: 130 sampleStr = s 131 case []byte: 132 sampleStr = string(s) 133 default: 134 require.FailNow(t, "unexpected sample type") 135 } 136 137 expected, err := time.ParseInLocation(tc.gotimeLayout, sampleStr, time.Local) 138 require.NoError(t, err, "Test configuration includes invalid timestamp or layout") 139 140 gotimeRootCfg := parseTimeTestConfig(helper.GotimeKey, tc.gotimeLayout, rootField) 141 t.Run("gotime-root", runTimeParseTest(t, gotimeRootCfg, makeTestEntry(rootField, tc.sample), false, false, expected)) 142 143 gotimeNonRootCfg := parseTimeTestConfig(helper.GotimeKey, tc.gotimeLayout, someField) 144 t.Run("gotime-non-root", runTimeParseTest(t, gotimeNonRootCfg, makeTestEntry(someField, tc.sample), false, false, expected)) 145 146 strptimeRootCfg := parseTimeTestConfig(helper.StrptimeKey, tc.strptimeLayout, rootField) 147 t.Run("strptime-root", runTimeParseTest(t, strptimeRootCfg, makeTestEntry(rootField, tc.sample), false, false, expected)) 148 149 strptimeNonRootCfg := parseTimeTestConfig(helper.StrptimeKey, tc.strptimeLayout, someField) 150 t.Run("strptime-non-root", runTimeParseTest(t, strptimeNonRootCfg, makeTestEntry(someField, tc.sample), false, false, expected)) 151 }) 152 } 153 } 154 155 func TestTimeEpochs(t *testing.T) { 156 157 testCases := []struct { 158 name string 159 sample interface{} 160 layout string 161 expected time.Time 162 maxLoss time.Duration 163 }{ 164 { 165 name: "s-default-string", 166 sample: "1136214245", 167 layout: "s", 168 expected: time.Unix(1136214245, 0), 169 }, 170 { 171 name: "s-default-bytes", 172 sample: []byte("1136214245"), 173 layout: "s", 174 expected: time.Unix(1136214245, 0), 175 }, 176 { 177 name: "s-default-int", 178 sample: 1136214245, 179 layout: "s", 180 expected: time.Unix(1136214245, 0), 181 }, 182 { 183 name: "s-default-float", 184 sample: 1136214245.0, 185 layout: "s", 186 expected: time.Unix(1136214245, 0), 187 }, 188 { 189 name: "ms-default-string", 190 sample: "1136214245123", 191 layout: "ms", 192 expected: time.Unix(1136214245, 123000000), 193 }, 194 { 195 name: "ms-default-int", 196 sample: 1136214245123, 197 layout: "ms", 198 expected: time.Unix(1136214245, 123000000), 199 }, 200 { 201 name: "ms-default-float", 202 sample: 1136214245123.0, 203 layout: "ms", 204 expected: time.Unix(1136214245, 123000000), 205 }, 206 { 207 name: "us-default-string", 208 sample: "1136214245123456", 209 layout: "us", 210 expected: time.Unix(1136214245, 123456000), 211 }, 212 { 213 name: "us-default-int", 214 sample: 1136214245123456, 215 layout: "us", 216 expected: time.Unix(1136214245, 123456000), 217 }, 218 { 219 name: "us-default-float", 220 sample: 1136214245123456.0, 221 layout: "us", 222 expected: time.Unix(1136214245, 123456000), 223 }, 224 { 225 name: "ns-default-string", 226 sample: "1136214245123456789", 227 layout: "ns", 228 expected: time.Unix(1136214245, 123456789), 229 }, 230 { 231 name: "ns-default-int", 232 sample: 1136214245123456789, 233 layout: "ns", 234 expected: time.Unix(1136214245, 123456789), 235 }, 236 { 237 name: "ns-default-float", 238 sample: 1136214245123456789.0, 239 layout: "ns", 240 expected: time.Unix(1136214245, 123456789), 241 maxLoss: time.Nanosecond * 100, 242 }, 243 { 244 name: "s.ms-default-string", 245 sample: "1136214245.123", 246 layout: "s.ms", 247 expected: time.Unix(1136214245, 123000000), 248 }, 249 { 250 name: "s.ms-default-int", 251 sample: 1136214245, 252 layout: "s.ms", 253 expected: time.Unix(1136214245, 0), // drops subseconds 254 maxLoss: time.Nanosecond * 100, 255 }, 256 { 257 name: "s.ms-default-float", 258 sample: 1136214245.123, 259 layout: "s.ms", 260 expected: time.Unix(1136214245, 123000000), 261 }, 262 { 263 name: "s.us-default-string", 264 sample: "1136214245.123456", 265 layout: "s.us", 266 expected: time.Unix(1136214245, 123456000), 267 }, 268 { 269 name: "s.us-default-int", 270 sample: 1136214245, 271 layout: "s.us", 272 expected: time.Unix(1136214245, 0), // drops subseconds 273 maxLoss: time.Nanosecond * 100, 274 }, 275 { 276 name: "s.us-default-float", 277 sample: 1136214245.123456, 278 layout: "s.us", 279 expected: time.Unix(1136214245, 123456000), 280 }, 281 { 282 name: "s.ns-default-string", 283 sample: "1136214245.123456789", 284 layout: "s.ns", 285 expected: time.Unix(1136214245, 123456789), 286 }, 287 { 288 name: "s.ns-default-int", 289 sample: 1136214245, 290 layout: "s.ns", 291 expected: time.Unix(1136214245, 0), // drops subseconds 292 maxLoss: time.Nanosecond * 100, 293 }, 294 { 295 name: "s.ns-default-float", 296 sample: 1136214245.123456789, 297 layout: "s.ns", 298 expected: time.Unix(1136214245, 123456789), 299 maxLoss: time.Nanosecond * 100, 300 }, 301 } 302 303 rootField := entry.NewRecordField() 304 someField := entry.NewRecordField("some_field") 305 306 for _, tc := range testCases { 307 t.Run(tc.name, func(t *testing.T) { 308 rootCfg := parseTimeTestConfig(helper.EpochKey, tc.layout, rootField) 309 t.Run("epoch-root", runLossyTimeParseTest(t, rootCfg, makeTestEntry(rootField, tc.sample), false, false, tc.expected, tc.maxLoss)) 310 311 nonRootCfg := parseTimeTestConfig(helper.EpochKey, tc.layout, someField) 312 t.Run("epoch-non-root", runLossyTimeParseTest(t, nonRootCfg, makeTestEntry(someField, tc.sample), false, false, tc.expected, tc.maxLoss)) 313 }) 314 } 315 } 316 317 func TestTimeErrors(t *testing.T) { 318 319 testCases := []struct { 320 name string 321 sample interface{} 322 layoutType string 323 layout string 324 buildErr bool 325 parseErr bool 326 }{ 327 { 328 name: "bad-layout-type", 329 layoutType: "fake", 330 buildErr: true, 331 }, 332 { 333 name: "bad-strptime-directive", 334 layoutType: "strptime", 335 layout: "%1", 336 buildErr: true, 337 }, 338 { 339 name: "bad-epoch-layout", 340 layoutType: "epoch", 341 layout: "years", 342 buildErr: true, 343 }, 344 { 345 name: "bad-native-value", 346 layoutType: "native", 347 sample: 1, 348 parseErr: true, 349 }, 350 { 351 name: "bad-gotime-value", 352 layoutType: "gotime", 353 layout: time.Kitchen, 354 sample: 1, 355 parseErr: true, 356 }, 357 { 358 name: "bad-epoch-value", 359 layoutType: "epoch", 360 layout: "s", 361 sample: "not-a-number", 362 parseErr: true, 363 }, 364 } 365 366 rootField := entry.NewRecordField() 367 someField := entry.NewRecordField("some_field") 368 369 for _, tc := range testCases { 370 t.Run(tc.name, func(t *testing.T) { 371 rootCfg := parseTimeTestConfig(tc.layoutType, tc.layout, rootField) 372 t.Run("err-root", runTimeParseTest(t, rootCfg, makeTestEntry(rootField, tc.sample), tc.buildErr, tc.parseErr, time.Now())) 373 374 nonRootCfg := parseTimeTestConfig(tc.layoutType, tc.layout, someField) 375 t.Run("err-non-root", runTimeParseTest(t, nonRootCfg, makeTestEntry(someField, tc.sample), tc.buildErr, tc.parseErr, time.Now())) 376 }) 377 } 378 } 379 380 func makeTestEntry(field entry.Field, value interface{}) *entry.Entry { 381 e := entry.New() 382 e.Set(field, value) 383 return e 384 } 385 386 func runTimeParseTest(t *testing.T, cfg *TimeParserConfig, ent *entry.Entry, buildErr bool, parseErr bool, expected time.Time) func(*testing.T) { 387 return runLossyTimeParseTest(t, cfg, ent, buildErr, parseErr, expected, time.Duration(0)) 388 } 389 390 func runLossyTimeParseTest(t *testing.T, cfg *TimeParserConfig, ent *entry.Entry, buildErr bool, parseErr bool, expected time.Time, maxLoss time.Duration) func(*testing.T) { 391 392 return func(t *testing.T) { 393 buildContext := testutil.NewBuildContext(t) 394 395 gotimeOperator, err := cfg.Build(buildContext) 396 if buildErr { 397 require.Error(t, err, "expected error when configuring operator") 398 return 399 } 400 require.NoError(t, err) 401 402 mockOutput := &testutil.Operator{} 403 resultChan := make(chan *entry.Entry, 1) 404 mockOutput.On("Process", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { 405 resultChan <- args.Get(1).(*entry.Entry) 406 }).Return(nil) 407 408 timeParser := gotimeOperator.(*TimeParserOperator) 409 timeParser.OutputOperators = []operator.Operator{mockOutput} 410 411 err = timeParser.Process(context.Background(), ent) 412 if parseErr { 413 require.Error(t, err, "expected error when configuring operator") 414 return 415 } 416 require.NoError(t, err) 417 418 select { 419 case e := <-resultChan: 420 diff := time.Duration(math.Abs(float64(expected.Sub(e.Timestamp)))) 421 require.True(t, diff <= maxLoss) 422 case <-time.After(time.Second): 423 require.FailNow(t, "Timed out waiting for entry to be processed") 424 } 425 } 426 } 427 428 func parseTimeTestConfig(layoutType, layout string, parseFrom entry.Field) *TimeParserConfig { 429 cfg := NewTimeParserConfig("test_operator_id") 430 cfg.OutputIDs = []string{"output1"} 431 cfg.TimeParser = helper.TimeParser{ 432 LayoutType: layoutType, 433 Layout: layout, 434 ParseFrom: &parseFrom, 435 } 436 return cfg 437 }