Repository: open-telemetry/opentelemetry-go
Pull request #191 (2019-10-11 00:22:01)

Provide conformance tests for tracers
The test harness may be used to ensure that a given tracer behaves according to the expectations set by the API.

This PR also provides further testing of the SDK's tracer.

iredelmeier: Note: this PR currently fails due to #189 

Patch:
diff --git a/internal/matchers/expectation.go b/internal/matchers/expectation.go
new file mode 100644
index 00000000..2a8cf211
--- /dev/null
+++ b/internal/matchers/expectation.go
@@ -0,0 +1,114 @@
+// Copyright 2019, OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package matchers
+
+import (
+	"reflect"
+	"testing"
+)
+
+type Expectation struct {
+	t      *testing.T
+	actual interface{}
+}
+
+func (e *Expectation) ToEqual(expected interface{}) {
+	e.verifyExpectedNotNil(expected)
+
+	if !reflect.DeepEqual(e.actual, expected) {
+		e.t.Fatalf("Expected\n\t%v\nto equal\n\t%v\n", e.actual, expected)
+	}
+}
+
+func (e *Expectation) NotToEqual(expected interface{}) {
+	e.verifyExpectedNotNil(expected)
+
+	if reflect.DeepEqual(e.actual, expected) {
+		e.t.Fatalf("Expected\n\t%v\nnot to equal\n\t%v\n", e.actual, expected)
+	}
+}
+
+func (e *Expectation) ToBeNil() {
+	if e.actual != nil {
+		e.t.Fatalf("Expected\n\t%v\nto be nil\n", e.actual)
+	}
+}
+
+func (e *Expectation) NotToBeNil() {
+	if e.actual == nil {
+		e.t.Fatalf("Expected\n\t%v\nnot to be nil\n", e.actual)
+	}
+}
+
+func (e *Expectation) ToBeTrue() {
+	switch a := e.actual.(type) {
+	case bool:
+		if e.actual == false {
+			e.t.Fatalf("Expected\n\t%v\nto be true\n", e.actual)
+		}
+	default:
+		e.t.Fatalf("Cannot check if non-bool value\n\t%v\nis truthy\n", a)
+	}
+}
+
+func (e *Expectation) ToBeFalse() {
+	switch a := e.actual.(type) {
+	case bool:
+		if e.actual == true {
+			e.t.Fatalf("Expected\n\t%v\nto be false\n", e.actual)
+		}
+	default:
+		e.t.Fatalf("Cannot check if non-bool value\n\t%v\nis truthy\n", a)
+	}
+}
+
+func (e *Expectation) ToSucceed() {
+	switch actual := e.actual.(type) {
+	case error:
+		if actual != nil {
+			e.t.Fatalf("Expected error\n\t%v\nto have succeeded\n", actual)
+		}
+	default:
+		e.t.Fatalf("Cannot check if non-error value\n\t%v\nsucceeded\n", actual)
+	}
+}
+
+func (e *Expectation) ToMatchError(expected interface{}) {
+	e.verifyExpectedNotNil(expected)
+
+	actual, ok := e.actual.(error)
+	if !ok {
+		e.t.Fatalf("Cannot check if non-error value\n\t%v\nmatches error\n", e.actual)
+	}
+
+	switch expected := expected.(type) {
+	case error:
+		if !reflect.DeepEqual(actual, expected) {
+			e.t.Fatalf("Expected\n\t%v\nto match error\n\t%v\n", actual, expected)
+		}
+	case string:
+		if actual.Error() != expected {
+			e.t.Fatalf("Expected\n\t%v\nto match error\n\t%v\n", actual, expected)
+		}
+	default:
+		e.t.Fatalf("Cannot match\n\t%v\nagainst non-error\n\t%v\n", actual, expected)
+	}
+}
+
+func (e *Expectation) verifyExpectedNotNil(expected interface{}) {
+	if expected == nil {
+		e.t.Fatal("Refusing to compare with <nil>. Use `ToBeNil` or `NotToBeNil` instead.")
+	}
+}
diff --git a/internal/matchers/expecter.go b/internal/matchers/expecter.go
new file mode 100644
index 00000000..f4df75b6
--- /dev/null
+++ b/internal/matchers/expecter.go
@@ -0,0 +1,36 @@
+// Copyright 2019, OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package matchers
+
+import (
+	"testing"
+)
+
+type Expecter struct {
+	t *testing.T
+}
+
+func NewExpecter(t *testing.T) *Expecter {
+	return &Expecter{
+		t: t,
+	}
+}
+
+func (a *Expecter) Expect(actual interface{}) *Expectation {
+	return &Expectation{
+		t:      a.t,
+		actual: actual,
+	}
+}
diff --git a/internal/matchers/package.go b/internal/matchers/package.go
new file mode 100644
index 00000000..57eeac75
--- /dev/null
+++ b/internal/matchers/package.go
@@ -0,0 +1,15 @@
+// Copyright 2019, OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package matchers // import "go.opentelemetry.io/internal/matchers"


Test patch:
diff --git a/api/testharness/harness.go b/api/testharness/harness.go
new file mode 100644
index 00000000..4af4123a
--- /dev/null
+++ b/api/testharness/harness.go
@@ -0,0 +1,290 @@
+// Copyright 2019, OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package testharness
+
+import (
+	"context"
+	"errors"
+	"testing"
+
+	"go.opentelemetry.io/api/core"
+	"go.opentelemetry.io/api/trace"
+	"go.opentelemetry.io/internal/matchers"
+)
+
+type Harness struct {
+	t *testing.T
+}
+
+func NewHarness(t *testing.T) *Harness {
+	return &Harness{
+		t: t,
+	}
+}
+
+func (h *Harness) TestTracer(subjectFactory func() trace.Tracer) {
+	h.t.Run("#Start", func(t *testing.T) {
+		t.Run("propagates the original context", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			ctxKey := testCtxKey{}
+			ctxValue := "ctx value"
+			ctx := context.WithValue(context.Background(), ctxKey, ctxValue)
+
+			ctx, _ = subject.Start(ctx, "test")
+
+			e.Expect(ctx.Value(ctxKey)).ToEqual(ctxValue)
+		})
+
+		t.Run("returns a span containing the expected properties", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			_, span := subject.Start(context.Background(), "test")
+
+			e.Expect(span).NotToBeNil()
+
+			e.Expect(span.Tracer()).ToEqual(subject)
+			e.Expect(span.SpanContext().IsValid()).ToBeTrue()
+		})
+
+		t.Run("stores the span on the provided context", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			ctx, span := subject.Start(context.Background(), "test")
+
+			e.Expect(span).NotToBeNil()
+			e.Expect(span.SpanContext()).NotToEqual(core.EmptySpanContext())
+			e.Expect(trace.CurrentSpan(ctx)).ToEqual(span)
+		})
+
+		t.Run("starts spans with unique trace and span IDs", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			_, span1 := subject.Start(context.Background(), "span1")
+			_, span2 := subject.Start(context.Background(), "span2")
+
+			sc1 := span1.SpanContext()
+			sc2 := span2.SpanContext()
+
+			e.Expect(sc1.TraceID).NotToEqual(sc2.TraceID)
+			e.Expect(sc1.SpanID).NotToEqual(sc2.SpanID)
+		})
+
+		t.Run("records the span if specified", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			_, span := subject.Start(context.Background(), "span", trace.WithRecord())
+
+			e.Expect(span.IsRecording()).ToBeTrue()
+		})
+
+		t.Run("propagates a parent's trace ID through the context", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			ctx, parent := subject.Start(context.Background(), "parent")
+			_, child := subject.Start(ctx, "child")
+
+			psc := parent.SpanContext()
+			csc := child.SpanContext()
+
+			e.Expect(csc.TraceID).ToEqual(psc.TraceID)
+			e.Expect(csc.SpanID).NotToEqual(psc.SpanID)
+		})
+
+		t.Run("propagates a parent's trace ID through `ChildOf`", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			_, parent := subject.Start(context.Background(), "parent")
+			_, child := subject.Start(context.Background(), "child", trace.ChildOf(parent.SpanContext()))
+
+			psc := parent.SpanContext()
+			csc := child.SpanContext()
+
+			e.Expect(csc.TraceID).ToEqual(psc.TraceID)
+			e.Expect(csc.SpanID).NotToEqual(psc.SpanID)
+		})
+
+		t.Run("propagates a parent's trace ID through `FollowsFrom`", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			_, parent := subject.Start(context.Background(), "parent")
+			_, child := subject.Start(context.Background(), "child", trace.FollowsFrom(parent.SpanContext()))
+
+			psc := parent.SpanContext()
+			csc := child.SpanContext()
+
+			e.Expect(csc.TraceID).ToEqual(psc.TraceID)
+			e.Expect(csc.SpanID).NotToEqual(psc.SpanID)
+		})
+	})
+
+	h.t.Run("#WithSpan", func(t *testing.T) {
+		t.Run("returns nil if the body does not return an error", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			err := subject.WithSpan(context.Background(), "test", func(ctx context.Context) error {
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+		})
+
+		t.Run("propagates the error from the body if the body errors", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			expectedErr := errors.New("test error")
+
+			err := subject.WithSpan(context.Background(), "test", func(ctx context.Context) error {
+				return expectedErr
+			})
+
+			e.Expect(err).ToMatchError(expectedErr)
+		})
+
+		t.Run("propagates the original context to the body", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			ctxKey := testCtxKey{}
+			ctxValue := "ctx value"
+			ctx := context.WithValue(context.Background(), ctxKey, ctxValue)
+
+			var actualCtx context.Context
+
+			err := subject.WithSpan(ctx, "test", func(ctx context.Context) error {
+				actualCtx = ctx
+
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+
+			e.Expect(actualCtx.Value(ctxKey)).ToEqual(ctxValue)
+		})
+
+		t.Run("stores a span containing the expected properties on the context provided to the body function", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			var span trace.Span
+
+			err := subject.WithSpan(context.Background(), "test", func(ctx context.Context) error {
+				span = trace.CurrentSpan(ctx)
+
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+
+			e.Expect(span).NotToBeNil()
+
+			e.Expect(span.Tracer()).ToEqual(subject)
+			e.Expect(span.SpanContext().IsValid()).ToBeTrue()
+		})
+
+		t.Run("starts spans with unique trace and span IDs", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			var span1 trace.Span
+			var span2 trace.Span
+
+			err := subject.WithSpan(context.Background(), "span1", func(ctx context.Context) error {
+				span1 = trace.CurrentSpan(ctx)
+
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+
+			err = subject.WithSpan(context.Background(), "span2", func(ctx context.Context) error {
+				span2 = trace.CurrentSpan(ctx)
+
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+
+			sc1 := span1.SpanContext()
+			sc2 := span2.SpanContext()
+
+			e.Expect(sc1.TraceID).NotToEqual(sc2.TraceID)
+			e.Expect(sc1.SpanID).NotToEqual(sc2.SpanID)
+		})
+
+		t.Run("propagates a parent's trace ID through the context", func(t *testing.T) {
+			t.Parallel()
+
+			e := matchers.NewExpecter(t)
+			subject := subjectFactory()
+
+			ctx, parent := subject.Start(context.Background(), "parent")
+
+			var child trace.Span
+
+			err := subject.WithSpan(ctx, "child", func(ctx context.Context) error {
+				child = trace.CurrentSpan(ctx)
+
+				return nil
+			})
+
+			e.Expect(err).ToBeNil()
+
+			psc := parent.SpanContext()
+			csc := child.SpanContext()
+
+			e.Expect(csc.TraceID).ToEqual(psc.TraceID)
+			e.Expect(csc.SpanID).NotToEqual(psc.SpanID)
+		})
+	})
+}
+
+type testCtxKey struct{}
diff --git a/api/testharness/package.go b/api/testharness/package.go
new file mode 100644
index 00000000..37cdeae5
--- /dev/null
+++ b/api/testharness/package.go
@@ -0,0 +1,15 @@
+// Copyright 2019, OpenTelemetry Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package testharness // import "go.opentelemetry.io/api/testharness"
diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go
index 3c251225..c9e88d27 100644
--- a/sdk/trace/trace_test.go
+++ b/sdk/trace/trace_test.go
@@ -27,6 +27,8 @@ import (
 
 	"go.opentelemetry.io/api/core"
 	"go.opentelemetry.io/api/key"
+	"go.opentelemetry.io/api/testharness"
+	"go.opentelemetry.io/api/trace"
 	apitrace "go.opentelemetry.io/api/trace"
 	"go.opentelemetry.io/sdk/export"
 )
@@ -41,6 +43,15 @@ func init() {
 	setupDefaultSamplerConfig()
 }
 
+func TestTracerFollowsExpectedAPIBehaviour(t *testing.T) {
+	harness := testharness.NewHarness(t)
+	subjectFactory := func() trace.Tracer {
+		return apitrace.GlobalTracer()
+	}
+
+	harness.TestTracer(subjectFactory)
+}
+
 func setupDefaultSamplerConfig() {
 	// no random sampling, but sample children of sampled spans.
 	ApplyConfig(Config{DefaultSampler: ProbabilitySampler(0)})
@@ -54,14 +65,6 @@ func (t *testExporter) ExportSpan(ctx context.Context, d *export.SpanData) {
 	t.spans = append(t.spans, d)
 }
 
-func TestStartSpan(t *testing.T) {
-	_, span := apitrace.GlobalTracer().Start(context.Background(), "StartSpan")
-	defer span.End()
-	if span == nil {
-		t.Errorf("span not started")
-	}
-}
-
 func TestSetName(t *testing.T) {
 	samplerIsCalled := false
 	fooSampler := Sampler(func(p SamplingParameters) SamplingDecision {