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