1/* 2 3Package harness provides a suite of API compatibility checks. They were originally ported from the 4OpenTracing Python library's "harness" module. 5 6To run this test suite against your tracer, call harness.RunAPIChecks and provide it a function 7that returns a Tracer implementation and a function to call to close it. The function will be 8called to create a new tracer before each test in the suite is run, and the returned closer function 9will be called after each test is finished. 10 11Several options provide additional checks for your Tracer's behavior: CheckBaggageValues(true) 12indicates your tracer supports baggage propagation, CheckExtract(true) tells the suite to test if 13the Tracer can extract a trace context from text and binary carriers, and CheckInject(true) tests 14if the Tracer can inject the trace context into a carrier. 15 16The UseProbe option provides an APICheckProbe implementation that allows the test suite to 17additionally check if two Spans are part of the same trace, and if a Span and a SpanContext 18are part of the same trace. Implementing an APICheckProbe provides additional assertions that 19your tracer is working properly. 20 21*/ 22package harness 23 24import ( 25 "bytes" 26 "testing" 27 "time" 28 29 "github.com/opentracing/opentracing-go" 30 "github.com/opentracing/opentracing-go/log" 31 "github.com/stretchr/testify/assert" 32 "github.com/stretchr/testify/suite" 33) 34 35// APICheckCapabilities describes capabilities of a Tracer that should be checked by APICheckSuite. 36type APICheckCapabilities struct { 37 CheckBaggageValues bool // whether to check for propagation of baggage values 38 CheckExtract bool // whether to check if extracting contexts from carriers works 39 CheckInject bool // whether to check if injecting contexts works 40 Probe APICheckProbe // optional interface providing methods to check recorded data 41} 42 43// APICheckProbe exposes methods for testing data recorded by a Tracer. 44type APICheckProbe interface { 45 // SameTrace helps tests assert that this tracer's spans are from the same trace. 46 SameTrace(first, second opentracing.Span) bool 47 // SameSpanContext helps tests assert that a span and a context are from the same trace and span. 48 SameSpanContext(opentracing.Span, opentracing.SpanContext) bool 49} 50 51// APICheckSuite is a testify suite for checking a Tracer against the OpenTracing API. 52type APICheckSuite struct { 53 suite.Suite 54 opts APICheckCapabilities 55 newTracer func() (tracer opentracing.Tracer, closer func()) 56 tracer opentracing.Tracer 57 closer func() 58} 59 60// RunAPIChecks runs a test suite to check a Tracer against the OpenTracing API. 61// It is provided a function that will be executed to create and destroy a tracer for each test 62// in the suite, and the given APICheckOption functional options `opts`. 63func RunAPIChecks( 64 t *testing.T, 65 newTracer func() (tracer opentracing.Tracer, closer func()), 66 opts ...APICheckOption, 67) { 68 s := &APICheckSuite{newTracer: newTracer} 69 for _, opt := range opts { 70 opt(s) 71 } 72 suite.Run(t, s) 73} 74 75// APICheckOption instances may be passed to NewAPICheckSuite. 76type APICheckOption func(*APICheckSuite) 77 78// CheckBaggageValues returns an option that sets whether to check for propagation of baggage values. 79func CheckBaggageValues(val bool) APICheckOption { 80 return func(s *APICheckSuite) { 81 s.opts.CheckBaggageValues = val 82 } 83} 84 85// CheckExtract returns an option that sets whether to check if extracting contexts from carriers works. 86func CheckExtract(val bool) APICheckOption { 87 return func(s *APICheckSuite) { 88 s.opts.CheckExtract = val 89 } 90} 91 92// CheckInject returns an option that sets whether to check if injecting contexts works. 93func CheckInject(val bool) APICheckOption { 94 return func(s *APICheckSuite) { 95 s.opts.CheckInject = val 96 } 97} 98 99// CheckEverything returns an option that enables all API checks. 100func CheckEverything() APICheckOption { 101 return func(s *APICheckSuite) { 102 s.opts.CheckBaggageValues = true 103 s.opts.CheckExtract = true 104 s.opts.CheckInject = true 105 } 106} 107 108// UseProbe returns an option that specifies an APICheckProbe implementation to use. 109func UseProbe(probe APICheckProbe) APICheckOption { 110 return func(s *APICheckSuite) { 111 s.opts.Probe = probe 112 } 113} 114 115// SetupTest creates a tracer for this specific test invocation. 116func (s *APICheckSuite) SetupTest() { 117 s.tracer, s.closer = s.newTracer() 118 if s.tracer == nil { 119 s.T().Fatalf("newTracer returned nil Tracer") 120 } 121} 122 123// TearDownTest closes the tracer, and clears the test-specific tracer. 124func (s *APICheckSuite) TearDownTest() { 125 if s.closer != nil { 126 s.closer() 127 } 128 s.tracer, s.closer = nil, nil 129} 130 131// TestStartSpan checks if a Tracer can start a span and calls some span API methods. 132func (s *APICheckSuite) TestStartSpan() { 133 span := s.tracer.StartSpan( 134 "Fry", 135 opentracing.Tag{Key: "birthday", Value: "August 14 1974"}) 136 span.LogFields( 137 log.String("hospital", "Brooklyn Pre-Med Hospital"), 138 log.String("city", "Old New York")) 139 span.Finish() 140} 141 142// TestStartSpanWithParent checks if a Tracer can start a span with a specified parent. 143func (s *APICheckSuite) TestStartSpanWithParent() { 144 parentSpan := s.tracer.StartSpan("Turanga Munda") 145 s.NotNil(parentSpan) 146 147 childFns := []func(opentracing.SpanContext) opentracing.SpanReference{ 148 opentracing.ChildOf, 149 opentracing.FollowsFrom, 150 } 151 for _, childFn := range childFns { 152 span := s.tracer.StartSpan( 153 "Leela", 154 childFn(parentSpan.Context()), 155 opentracing.Tag{Key: "birthplace", Value: "sewers"}) 156 span.Finish() 157 if s.opts.Probe != nil { 158 s.True(s.opts.Probe.SameTrace(parentSpan, span)) 159 } else { 160 s.T().Log("harness.Probe not specified, skipping") 161 } 162 } 163 164 parentSpan.Finish() 165} 166 167// TestSetOperationName attempts to set the operation name on a span after it has been created. 168func (s *APICheckSuite) TestSetOperationName() { 169 span := s.tracer.StartSpan("").SetOperationName("Farnsworth") 170 span.Finish() 171} 172 173// TestSpanTagValueTypes sets tags using values of different types. 174func (s *APICheckSuite) TestSpanTagValueTypes() { 175 span := s.tracer.StartSpan("ManyTypes") 176 span. 177 SetTag("an_int", 9). 178 SetTag("a_bool", true). 179 SetTag("a_string", "aoeuidhtns") 180} 181 182// TestSpanTagsWithChaining tests chaining of calls to SetTag. 183func (s *APICheckSuite) TestSpanTagsWithChaining() { 184 span := s.tracer.StartSpan("Farnsworth") 185 span. 186 SetTag("birthday", "9 April, 2841"). 187 SetTag("loves", "different lengths of wires") 188 span. 189 SetTag("unicode_val", "non-ascii: \u200b"). 190 SetTag("unicode_key_\u200b", "ascii val") 191 span.Finish() 192} 193 194// TestSpanLogs tests calls to log keys and values with spans. 195func (s *APICheckSuite) TestSpanLogs() { 196 span := s.tracer.StartSpan("Fry") 197 span.LogKV( 198 "event", "frozen", 199 "year", 1999, 200 "place", "Cryogenics Labs") 201 span.LogKV( 202 "event", "defrosted", 203 "year", 2999, 204 "place", "Cryogenics Labs") 205 206 ts := time.Now() 207 span.FinishWithOptions(opentracing.FinishOptions{ 208 LogRecords: []opentracing.LogRecord{ 209 { 210 Timestamp: ts, 211 Fields: []log.Field{ 212 log.String("event", "job-assignment"), 213 log.String("type", "delivery boy"), 214 }, 215 }, 216 }}) 217 218 // Test deprecated log methods 219 span.LogEvent("an arbitrary event") 220 span.LogEventWithPayload("y", "z") 221 span.Log(opentracing.LogData{Event: "y", Payload: "z"}) 222} 223 224func assertEmptyBaggage(t *testing.T, spanContext opentracing.SpanContext) { 225 if !assert.NotNil(t, spanContext, "assertEmptyBaggage got empty context") { 226 return 227 } 228 spanContext.ForeachBaggageItem(func(k, v string) bool { 229 assert.Fail(t, "new span shouldn't have baggage") 230 return false 231 }) 232} 233 234// TestSpanBaggage tests calls to set and get span baggage, and if the CheckBaggageValues option 235// is set, asserts that baggage values were successfully retrieved. 236func (s *APICheckSuite) TestSpanBaggage() { 237 span := s.tracer.StartSpan("Fry") 238 assertEmptyBaggage(s.T(), span.Context()) 239 240 spanRef := span.SetBaggageItem("Kiff-loves", "Amy") 241 s.Exactly(spanRef, span) 242 243 val := span.BaggageItem("Kiff-loves") 244 if s.opts.CheckBaggageValues { 245 s.Equal("Amy", val) 246 } else { 247 s.T().Log("CheckBaggageValues capability not set, skipping") 248 } 249 span.Finish() 250} 251 252// TestContextBaggage tests calls to set and get span baggage, and if the CheckBaggageValues option 253// is set, asserts that baggage values were successfully retrieved from the span's SpanContext. 254func (s *APICheckSuite) TestContextBaggage() { 255 span := s.tracer.StartSpan("Fry") 256 assertEmptyBaggage(s.T(), span.Context()) 257 258 span.SetBaggageItem("Kiff-loves", "Amy") 259 if s.opts.CheckBaggageValues { 260 called := false 261 span.Context().ForeachBaggageItem(func(k, v string) bool { 262 s.False(called) 263 called = true 264 s.Equal("Kiff-loves", k) 265 s.Equal("Amy", v) 266 return true 267 }) 268 } else { 269 s.T().Log("CheckBaggageValues capability not set, skipping") 270 } 271 span.Finish() 272} 273 274// TestTextPropagation tests if the Tracer can Inject a span into a TextMapCarrier, and later Extract it. 275// If CheckExtract is set, it will check if Extract was successful (returned no error). If a Probe is set, 276// it will check if the extracted context is in the same trace as the original span. 277func (s *APICheckSuite) TestTextPropagation() { 278 span := s.tracer.StartSpan("Bender") 279 textCarrier := opentracing.TextMapCarrier{} 280 err := span.Tracer().Inject(span.Context(), opentracing.TextMap, textCarrier) 281 assert.NoError(s.T(), err) 282 283 extractedContext, err := s.tracer.Extract(opentracing.TextMap, textCarrier) 284 if s.opts.CheckExtract { 285 s.NoError(err) 286 assertEmptyBaggage(s.T(), extractedContext) 287 } else { 288 s.T().Log("CheckExtract capability not set, skipping") 289 } 290 if s.opts.Probe != nil { 291 s.True(s.opts.Probe.SameSpanContext(span, extractedContext)) 292 } else { 293 s.T().Log("harness.Probe not specified, skipping") 294 } 295 span.Finish() 296} 297 298// TestHTTPPropagation tests if the Tracer can Inject a span into HTTP headers, and later Extract it. 299// If CheckExtract is set, it will check if Extract was successful (returned no error). If a Probe is set, 300// it will check if the extracted context is in the same trace as the original span. 301func (s *APICheckSuite) TestHTTPPropagation() { 302 span := s.tracer.StartSpan("Bender") 303 textCarrier := opentracing.HTTPHeadersCarrier{} 304 err := span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, textCarrier) 305 s.NoError(err) 306 307 extractedContext, err := s.tracer.Extract(opentracing.HTTPHeaders, textCarrier) 308 if s.opts.CheckExtract { 309 s.NoError(err) 310 assertEmptyBaggage(s.T(), extractedContext) 311 } else { 312 s.T().Log("CheckExtract capability not set, skipping") 313 } 314 if s.opts.Probe != nil { 315 s.True(s.opts.Probe.SameSpanContext(span, extractedContext)) 316 } else { 317 s.T().Log("harness.Probe not specified, skipping") 318 } 319 span.Finish() 320} 321 322// TestBinaryPropagation tests if the Tracer can Inject a span into a binary buffer, and later Extract it. 323// If CheckExtract is set, it will check if Extract was successful (returned no error). If a Probe is set, 324// it will check if the extracted context is in the same trace as the original span. 325func (s *APICheckSuite) TestBinaryPropagation() { 326 span := s.tracer.StartSpan("Bender") 327 buf := new(bytes.Buffer) 328 err := span.Tracer().Inject(span.Context(), opentracing.Binary, buf) 329 s.NoError(err) 330 331 extractedContext, err := s.tracer.Extract(opentracing.Binary, buf) 332 if s.opts.CheckExtract { 333 s.NoError(err) 334 assertEmptyBaggage(s.T(), extractedContext) 335 } else { 336 s.T().Log("CheckExtract capability not set, skipping") 337 } 338 if s.opts.Probe != nil { 339 s.True(s.opts.Probe.SameSpanContext(span, extractedContext)) 340 } else { 341 s.T().Log("harness.Probe not specified, skipping") 342 } 343 span.Finish() 344} 345 346// TestMandatoryFormats tests if all mandatory carrier formats are supported. If CheckExtract is set, it 347// will check if the call to Extract was successful (returned no error such as ErrUnsupportedFormat). 348func (s *APICheckSuite) TestMandatoryFormats() { 349 formats := []struct{ Format, Carrier interface{} }{ 350 {opentracing.TextMap, opentracing.TextMapCarrier{}}, 351 {opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier{}}, 352 {opentracing.Binary, new(bytes.Buffer)}, 353 } 354 span := s.tracer.StartSpan("Bender") 355 for _, fmtCarrier := range formats { 356 err := span.Tracer().Inject(span.Context(), fmtCarrier.Format, fmtCarrier.Carrier) 357 s.NoError(err) 358 spanCtx, err := s.tracer.Extract(fmtCarrier.Format, fmtCarrier.Carrier) 359 if s.opts.CheckExtract { 360 s.NoError(err) 361 assertEmptyBaggage(s.T(), spanCtx) 362 } else { 363 s.T().Log("CheckExtract capability not set, skipping") 364 } 365 } 366} 367 368// TestUnknownFormat checks if attempting to Inject or Extract using an unsupported format 369// returns ErrUnsupportedFormat, if CheckInject and CheckExtract are set. 370func (s *APICheckSuite) TestUnknownFormat() { 371 customFormat := "kiss my shiny metal ..." 372 span := s.tracer.StartSpan("Bender") 373 374 err := span.Tracer().Inject(span.Context(), customFormat, nil) 375 if s.opts.CheckInject { 376 s.Equal(opentracing.ErrUnsupportedFormat, err) 377 } else { 378 s.T().Log("CheckInject capability not set, skipping") 379 } 380 ctx, err := s.tracer.Extract(customFormat, nil) 381 s.Nil(ctx) 382 if s.opts.CheckExtract { 383 s.Equal(opentracing.ErrUnsupportedFormat, err) 384 } else { 385 s.T().Log("CheckExtract capability not set, skipping") 386 } 387} 388 389// ForeignSpanContext satisfies the opentracing.SpanContext interface, but otherwise does nothing. 390type ForeignSpanContext struct{} 391 392// ForeachBaggageItem could call handler for each baggage KV, but does nothing. 393func (f ForeignSpanContext) ForeachBaggageItem(handler func(k, v string) bool) {} 394 395// NotACarrier does not satisfy any of the opentracing carrier interfaces. 396type NotACarrier struct{} 397 398// TestInvalidInject checks if errors are returned when Inject is called with invalid inputs. 399func (s *APICheckSuite) TestInvalidInject() { 400 if !s.opts.CheckInject { 401 s.T().Skip("CheckInject capability not set, skipping") 402 } 403 span := s.tracer.StartSpan("op") 404 405 // binary inject 406 err := span.Tracer().Inject(ForeignSpanContext{}, opentracing.Binary, new(bytes.Buffer)) 407 s.Equal(opentracing.ErrInvalidSpanContext, err, "Foreign SpanContext should return invalid error") 408 err = span.Tracer().Inject(span.Context(), opentracing.Binary, NotACarrier{}) 409 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not io.Writer should return error") 410 411 // text inject 412 err = span.Tracer().Inject(ForeignSpanContext{}, opentracing.TextMap, opentracing.TextMapCarrier{}) 413 s.Equal(opentracing.ErrInvalidSpanContext, err, "Foreign SpanContext should return invalid error") 414 err = span.Tracer().Inject(span.Context(), opentracing.TextMap, NotACarrier{}) 415 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not TextMapWriter should return error") 416 417 // HTTP inject 418 err = span.Tracer().Inject(ForeignSpanContext{}, opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier{}) 419 s.Equal(opentracing.ErrInvalidSpanContext, err, "Foreign SpanContext should return invalid error") 420 err = span.Tracer().Inject(span.Context(), opentracing.HTTPHeaders, NotACarrier{}) 421 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not TextMapWriter should return error") 422} 423 424// TestInvalidExtract checks if errors are returned when Extract is called with invalid inputs. 425func (s *APICheckSuite) TestInvalidExtract() { 426 if !s.opts.CheckExtract { 427 s.T().Skip("CheckExtract capability not set, skipping") 428 } 429 span := s.tracer.StartSpan("op") 430 431 // binary extract 432 ctx, err := span.Tracer().Extract(opentracing.Binary, NotACarrier{}) 433 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not io.Reader should return error") 434 s.Nil(ctx) 435 436 // text extract 437 ctx, err = span.Tracer().Extract(opentracing.TextMap, NotACarrier{}) 438 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not TextMapReader should return error") 439 s.Nil(ctx) 440 441 // HTTP extract 442 ctx, err = span.Tracer().Extract(opentracing.HTTPHeaders, NotACarrier{}) 443 s.Equal(opentracing.ErrInvalidCarrier, err, "Carrier that's not TextMapReader should return error") 444 s.Nil(ctx) 445 446 span.Finish() 447} 448 449// TestMultiBaggage tests calls to set multiple baggage items, and if the CheckBaggageValues option 450// is set, asserts that a baggage value was successfully retrieved from the span's SpanContext. 451// It also ensures that returning false from the ForeachBaggageItem handler aborts iteration. 452func (s *APICheckSuite) TestMultiBaggage() { 453 span := s.tracer.StartSpan("op") 454 assertEmptyBaggage(s.T(), span.Context()) 455 456 span.SetBaggageItem("Bag1", "BaggageVal1") 457 span.SetBaggageItem("Bag2", "BaggageVal2") 458 if s.opts.CheckBaggageValues { 459 s.Equal("BaggageVal1", span.BaggageItem("Bag1")) 460 s.Equal("BaggageVal2", span.BaggageItem("Bag2")) 461 called := false 462 span.Context().ForeachBaggageItem(func(k, v string) bool { 463 s.False(called) // should only be called once 464 called = true 465 return false 466 }) 467 s.True(called) 468 } else { 469 s.T().Log("CheckBaggageValues capability not set, skipping") 470 } 471 span.Finish() 472} 473