diff --git a/lib/datadog/ci.rb b/lib/datadog/ci.rb index 96dc9347..0d7a1a9f 100644 --- a/lib/datadog/ci.rb +++ b/lib/datadog/ci.rb @@ -14,7 +14,7 @@ class << self # Raises an error if a session is already active. # # - # The {#start_test_session} method is used to mark the start : + # The {#start_test_session} method is used to mark the start of the test session: # ``` # Datadog::CI.start_test_session( # service: "my-web-site-tests", @@ -59,7 +59,9 @@ def active_test_session end # Return a {Datadog::CI::Test ci_test} that will trace a test called `test_name`. - # Raises an error if a test is already active. + # Raises an error if a test is already active. If there is an active test session, + # the new test will be connected to the session. The test will inherit service name and tags from + # the running test session if none provided. # # You could trace your test using a do-block like: # diff --git a/lib/datadog/ci/concurrent_span.rb b/lib/datadog/ci/concurrent_span.rb new file mode 100644 index 00000000..9910d0dc --- /dev/null +++ b/lib/datadog/ci/concurrent_span.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require_relative "span" + +module Datadog + module CI + # Represents a single part of a test run that can be safely shared between threads. + # Examples of shared objects are: TestSession, TestModule, TestSpan. + # + # @public_api + class ConcurrentSpan < Span + def initialize(tracer_span) + super + + @mutex = Mutex.new + end + + # Sets the status of the span to "pass". This method is thread-safe. + # @return [void] + def passed! + synchronize { super } + end + + # Sets the status of the span to "fail". This method is thread-safe. + # @param [Exception] exception the exception that caused the test to fail. + # @return [void] + def failed!(exception: nil) + synchronize { super } + end + + # Sets the status of the span to "skip". This method is thread-safe. + # @param [Exception] exception the exception that caused the test to fail. + # @param [String] reason the reason why the test was skipped. + # @return [void] + def skipped!(exception: nil, reason: nil) + synchronize { super } + end + + # Gets tag value by key. This method is thread-safe. + # @param [String] key the key of the tag. + # @return [String] the value of the tag. + def get_tag(key) + synchronize { super } + end + + # Sets tag value by key. This method is thread-safe. + # @param [String] key the key of the tag. + # @param [String] value the value of the tag. + # @return [void] + def set_tag(key, value) + synchronize { super } + end + + # Sets metric value by key. This method is thread-safe. + # @param [String] key the key of the metric. + # @param [Numeric] value the value of the metric. + # @return [void] + def set_metric(key, value) + synchronize { super } + end + + # Finishes the span. This method is thread-safe. + # @return [void] + def finish + synchronize { super } + end + + # Sets multiple tags at once. This method is thread-safe. + # @param [Hash[String, String]] tags the tags to set. + # @return [void] + def set_tags(tags) + synchronize { super } + end + + def set_environment_runtime_tags + synchronize { super } + end + + def set_default_tags + synchronize { super } + end + + def synchronize + @mutex.synchronize { yield } + end + end + end +end diff --git a/lib/datadog/ci/ext/test.rb b/lib/datadog/ci/ext/test.rb index af7199fb..5f062705 100644 --- a/lib/datadog/ci/ext/test.rb +++ b/lib/datadog/ci/ext/test.rb @@ -17,6 +17,9 @@ module Test TAG_TRAITS = "test.traits" TAG_TYPE = "test.type" + # tags that are inherited from the test session + INHERITED_TAGS = [TAG_FRAMEWORK, TAG_FRAMEWORK_VERSION, TAG_TYPE].freeze + # Environment runtime tags TAG_OS_ARCHITECTURE = "os.architecture" TAG_OS_PLATFORM = "os.platform" diff --git a/lib/datadog/ci/recorder.rb b/lib/datadog/ci/recorder.rb index e6fe12dd..b076b38f 100644 --- a/lib/datadog/ci/recorder.rb +++ b/lib/datadog/ci/recorder.rb @@ -46,6 +46,11 @@ def start_test_session(service_name: nil, tags: {}) # Creates a new span for a CI test def trace_test(test_name, service_name: nil, operation_name: "test", tags: {}, &block) + test_session = active_test_session + if test_session + service_name ||= test_session.service + end + span_options = { resource: test_name, service: service_name, diff --git a/lib/datadog/ci/span.rb b/lib/datadog/ci/span.rb index 19e45a85..4c77fbb1 100644 --- a/lib/datadog/ci/span.rb +++ b/lib/datadog/ci/span.rb @@ -20,6 +20,11 @@ def name tracer_span.name end + # @return [String] the service name of the span. + def service + tracer_span.service + end + # @return [String] the type of the span (for example "test" or type that was provided to [Datadog::CI.trace]). def span_type tracer_span.type diff --git a/lib/datadog/ci/test_session.rb b/lib/datadog/ci/test_session.rb index c6e49217..2adede5d 100644 --- a/lib/datadog/ci/test_session.rb +++ b/lib/datadog/ci/test_session.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "span" +require_relative "concurrent_span" module Datadog module CI @@ -8,13 +8,7 @@ module CI # This object can be shared between multiple threads. # # @public_api - class TestSession < Span - def initialize(tracer_span) - super - - @mutex = Mutex.new - end - + class TestSession < ConcurrentSpan # Finishes the current test session. # @return [void] def finish diff --git a/sig/datadog/ci/concurrent_span.rbs b/sig/datadog/ci/concurrent_span.rbs new file mode 100644 index 00000000..ff834140 --- /dev/null +++ b/sig/datadog/ci/concurrent_span.rbs @@ -0,0 +1,23 @@ +module Datadog + module CI + class ConcurrentSpan < Span + @mutex: Thread::Mutex + + def initialize: (Datadog::Tracing::SpanOperation tracer_span) -> void + def passed!: () -> void + def failed!: (?exception: untyped?) -> void + def skipped!: (?exception: untyped?, ?reason: String?) -> void + def get_tag: (String key) -> untyped? + def set_tag: (String key, untyped? value) -> void + def set_metric: (String key, untyped value) -> void + def finish: () -> void + def set_tags: (Hash[untyped, untyped] tags) -> void + + def set_environment_runtime_tags: () -> void + + def set_default_tags: () -> void + + def synchronize: () { () -> untyped } -> untyped + end + end +end diff --git a/sig/datadog/ci/ext/test.rbs b/sig/datadog/ci/ext/test.rbs index 6f0c0c0c..dada2f87 100644 --- a/sig/datadog/ci/ext/test.rbs +++ b/sig/datadog/ci/ext/test.rbs @@ -21,6 +21,9 @@ module Datadog TAG_TRAITS: String TAG_TYPE: String + + INHERITED_TAGS: Array[String] + TAG_OS_ARCHITECTURE: String TAG_OS_PLATFORM: String diff --git a/sig/datadog/ci/span.rbs b/sig/datadog/ci/span.rbs index 32ae69a9..b81df6ca 100644 --- a/sig/datadog/ci/span.rbs +++ b/sig/datadog/ci/span.rbs @@ -9,6 +9,8 @@ module Datadog def name: () -> String + def service: () -> String + def passed!: () -> void def failed!: (?exception: untyped?) -> void diff --git a/sig/datadog/ci/test_session.rbs b/sig/datadog/ci/test_session.rbs index 9f439e47..32d99305 100644 --- a/sig/datadog/ci/test_session.rbs +++ b/sig/datadog/ci/test_session.rbs @@ -1,7 +1,6 @@ module Datadog module CI - class TestSession < Span - @mutex: Thread::Mutex + class TestSession < ConcurrentSpan end end end diff --git a/spec/datadog/ci/recorder_spec.rb b/spec/datadog/ci/recorder_spec.rb index c14f514a..f511e4e9 100644 --- a/spec/datadog/ci/recorder_spec.rb +++ b/spec/datadog/ci/recorder_spec.rb @@ -4,6 +4,7 @@ let(:operation_name) { "span name" } let(:test_name) { "test name" } let(:tags) { {} } + let(:expected_tags) { tags } let(:environment_tags) { Datadog::CI::Ext::Environment.tags(ENV) } let(:ci_span) do @@ -29,7 +30,7 @@ it do expect(ci_span).to have_received(:set_default_tags) expect(ci_span).to have_received(:set_environment_runtime_tags) - expect(ci_span).to have_received(:set_tags).with(tags) + expect(ci_span).to have_received(:set_tags).with(expected_tags) expect(ci_span).to have_received(:set_tags).with(environment_tags) end end @@ -81,37 +82,109 @@ end context "when not given a block" do - subject(:trace) do - recorder.trace_test( - test_name, - service_name: service, - operation_name: operation_name, - tags: tags - ) - end let(:span_op) { Datadog::Tracing::SpanOperation.new(operation_name) } - before do - allow(Datadog::Tracing) - .to receive(:trace) - .with( - operation_name, - { - span_type: Datadog::CI::Ext::AppTypes::TYPE_TEST, - resource: test_name, - service: service - } + context "without active test session" do + subject(:trace) do + recorder.trace_test( + test_name, + service_name: service, + operation_name: operation_name, + tags: tags ) - .and_return(span_op) + end - allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) + before do + allow(Datadog::Tracing) + .to receive(:trace) + .with( + operation_name, + { + span_type: Datadog::CI::Ext::AppTypes::TYPE_TEST, + resource: test_name, + service: service + } + ) + .and_return(span_op) - trace + allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) + + trace + end + + it_behaves_like "internal tracing context" + it_behaves_like "initialize ci span with tags" + it { is_expected.to be(ci_span) } end - it_behaves_like "internal tracing context" - it_behaves_like "initialize ci span with tags" - it { is_expected.to be(ci_span) } + context "when test session is active" do + let(:test_session_service) { "my-test-service" } + let(:test_session) { instance_double(Datadog::CI::TestSession, service: test_session_service) } + before do + allow(recorder).to receive(:active_test_session).and_return(test_session) + end + + context "when service name is not given" do + subject(:trace) do + recorder.trace_test( + test_name, + operation_name: operation_name, + tags: tags + ) + end + + before do + allow(Datadog::Tracing) + .to receive(:trace) + .with( + operation_name, + { + span_type: Datadog::CI::Ext::AppTypes::TYPE_TEST, + resource: test_name, + service: test_session_service + } + ) + .and_return(span_op) + + allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) + + trace + end + + it { is_expected.to be(ci_span) } + end + + context "when service name is given" do + subject(:trace) do + recorder.trace_test( + test_name, + service_name: service, + operation_name: operation_name, + tags: tags + ) + end + + before do + allow(Datadog::Tracing) + .to receive(:trace) + .with( + operation_name, + { + span_type: Datadog::CI::Ext::AppTypes::TYPE_TEST, + resource: test_name, + service: service + } + ) + .and_return(span_op) + + allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) + + trace + end + + it { is_expected.to be(ci_span) } + end + end end end @@ -120,8 +193,6 @@ let(:span_type) { "step" } let(:span_name) { "span name" } - let(:expected_tags) { tags } - context "when given a block" do subject(:trace) do recorder.trace(