Skip to content

Commit

Permalink
introduce concurrency-safe Span class and inherit service name from t…
Browse files Browse the repository at this point in the history
…he active test session in #trace_test
  • Loading branch information
anmarchenko committed Nov 22, 2023
1 parent 750b30a commit 076d24c
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 39 deletions.
6 changes: 4 additions & 2 deletions lib/datadog/ci.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 <tt>do-block</tt> like:
#
Expand Down
88 changes: 88 additions & 0 deletions lib/datadog/ci/concurrent_span.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions lib/datadog/ci/ext/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions lib/datadog/ci/recorder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions lib/datadog/ci/span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 2 additions & 8 deletions lib/datadog/ci/test_session.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
# frozen_string_literal: true

require_relative "span"
require_relative "concurrent_span"

module Datadog
module CI
# Represents the whole test session process.
# 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
Expand Down
23 changes: 23 additions & 0 deletions sig/datadog/ci/concurrent_span.rbs
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions sig/datadog/ci/ext/test.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ module Datadog
TAG_TRAITS: String

TAG_TYPE: String

INHERITED_TAGS: Array[String]

TAG_OS_ARCHITECTURE: String

TAG_OS_PLATFORM: String
Expand Down
2 changes: 2 additions & 0 deletions sig/datadog/ci/span.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module Datadog

def name: () -> String

def service: () -> String

def passed!: () -> void

def failed!: (?exception: untyped?) -> void
Expand Down
3 changes: 1 addition & 2 deletions sig/datadog/ci/test_session.rbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module Datadog
module CI
class TestSession < Span
@mutex: Thread::Mutex
class TestSession < ConcurrentSpan
end
end
end
125 changes: 98 additions & 27 deletions spec/datadog/ci/recorder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand Down

0 comments on commit 076d24c

Please sign in to comment.