diff --git a/api/testharness/harness.go b/api/testharness/harness.go new file mode 100644 index 000000000..4af4123a7 --- /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 000000000..37cdeae55 --- /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/internal/matchers/expectation.go b/internal/matchers/expectation.go new file mode 100644 index 000000000..2a8cf211d --- /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 . Use `ToBeNil` or `NotToBeNil` instead.") + } +} diff --git a/internal/matchers/expecter.go b/internal/matchers/expecter.go new file mode 100644 index 000000000..f4df75b6a --- /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 000000000..57eeac75f --- /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" diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 3c2512251..c9e88d27e 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 {