From 750b30a7762a2d1d5c7399695df6b783ea5c3e2e Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Tue, 21 Nov 2023 13:35:32 +0100 Subject: [PATCH] add test session public API --- lib/datadog/ci.rb | 53 +++++++++ lib/datadog/ci/context/global.rb | 31 ++++++ lib/datadog/ci/ext/app_types.rb | 1 + lib/datadog/ci/recorder.rb | 61 ++++++++--- lib/datadog/ci/test.rb | 1 - lib/datadog/ci/test_session.rb | 27 +++++ .../ci/test_visibility/serializers/test_v1.rb | 2 +- sig/datadog/ci.rbs | 6 ++ sig/datadog/ci/context/global.rbs | 19 ++++ sig/datadog/ci/ext/app_types.rbs | 3 +- sig/datadog/ci/recorder.rbs | 11 ++ sig/datadog/ci/test_session.rbs | 7 ++ .../test_visibility/serializers/test_v1.rbs | 2 +- spec/datadog/ci/context/global_spec.rb | 51 +++++++++ spec/datadog/ci/recorder_spec.rb | 101 +++++++++++++----- spec/datadog/ci/test_session_spec.rb | 17 +++ spec/datadog/ci_spec.rb | 34 ++++++ 17 files changed, 385 insertions(+), 42 deletions(-) create mode 100644 lib/datadog/ci/context/global.rb create mode 100644 lib/datadog/ci/test_session.rb create mode 100644 sig/datadog/ci/context/global.rbs create mode 100644 sig/datadog/ci/test_session.rbs create mode 100644 spec/datadog/ci/context/global_spec.rb create mode 100644 spec/datadog/ci/test_session_spec.rb diff --git a/lib/datadog/ci.rb b/lib/datadog/ci.rb index 7bfd382e..96dc9347 100644 --- a/lib/datadog/ci.rb +++ b/lib/datadog/ci.rb @@ -10,6 +10,54 @@ module Datadog # @public_api module CI class << self + # Return a {Datadog::CI::TestSesstion ci_test_session} that represents the whole test session run. + # Raises an error if a session is already active. + # + # + # The {#start_test_session} method is used to mark the start : + # ``` + # Datadog::CI.start_test_session( + # service: "my-web-site-tests", + # tags: { Datadog::CI::Ext::Test::TAG_FRAMEWORK => "my-test-framework" } + # ) + # + # # Somewhere else after test run has ended + # Datadog::CI.active_test_session.finish + # ``` + # + # Remember that calling {Datadog::CI::TestSession#finish} is mandatory. + # + # @param [String] service_name the service name for this session + # @param [Hash] tags extra tags which should be added to the test. + # @return [Datadog::CI::TestSession] returns the active, running {Datadog::CI::TestSession}. + # + # @public_api + def start_test_session(service_name: nil, tags: {}) + recorder.start_test_session(service_name: service_name, tags: tags) + end + + # The active, unfinished test session span. + # + # Usage: + # + # ``` + # # start a test session + # Datadog::CI.start_test_session( + # service: "my-web-site-tests", + # tags: { Datadog::CI::Ext::Test::TAG_FRAMEWORK => "my-test-framework" } + # ) + # + # # somewhere else, access the session + # test_session = Datadog::CI.active_test_session + # test_session.finish + # ``` + # + # @return [Datadog::CI::TestSession] the active test session + # @return [nil] if no test session is active + def active_test_session + recorder.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. # @@ -189,6 +237,11 @@ def deactivate_test(test) recorder.deactivate_test(test) end + # Internal only, to finish a test session use Datadog::CI::TestSession#finish + def deactivate_test_session + recorder.deactivate_test_session + end + private def components diff --git a/lib/datadog/ci/context/global.rb b/lib/datadog/ci/context/global.rb new file mode 100644 index 00000000..0f7ebfdd --- /dev/null +++ b/lib/datadog/ci/context/global.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Datadog + module CI + module Context + # This context is shared between threads and represents the current test session. + class Global + def initialize + @mutex = Mutex.new + @test_session = nil + end + + def active_test_session + @test_session + end + + def activate_test_session!(test_session) + @mutex.synchronize do + raise "Nested test sessions are not supported. Currently active test session: #{@test_session}" unless @test_session.nil? + + @test_session = test_session + end + end + + def deactivate_test_session! + @mutex.synchronize { @test_session = nil } + end + end + end + end +end diff --git a/lib/datadog/ci/ext/app_types.rb b/lib/datadog/ci/ext/app_types.rb index e83ad3c3..f28230a7 100644 --- a/lib/datadog/ci/ext/app_types.rb +++ b/lib/datadog/ci/ext/app_types.rb @@ -5,6 +5,7 @@ module CI module Ext module AppTypes TYPE_TEST = "test" + TYPE_TEST_SESSION = "test_session_end" end end end diff --git a/lib/datadog/ci/recorder.rb b/lib/datadog/ci/recorder.rb index 0d41bd9a..e6fe12dd 100644 --- a/lib/datadog/ci/recorder.rb +++ b/lib/datadog/ci/recorder.rb @@ -8,10 +8,12 @@ require_relative "ext/test" require_relative "ext/environment" +require_relative "context/global" require_relative "context/local" require_relative "span" require_relative "test" +require_relative "test_session" module Datadog module CI @@ -22,6 +24,24 @@ class Recorder def initialize @environment_tags = Ext::Environment.tags(ENV).freeze @local_context = Context::Local.new + @global_context = Context::Global.new + end + + def start_test_session(service_name: nil, tags: {}) + span_options = { + service: service_name, + span_type: Ext::AppTypes::TYPE_TEST_SESSION + } + + tracer_span = Datadog::Tracing.trace("test.session", **span_options) + trace = Datadog::Tracing.active_trace + + set_trace_origin(trace) + + test_session = build_test_session(tracer_span, tags) + @global_context.activate_test_session!(test_session) + + test_session end # Creates a new span for a CI test @@ -73,17 +93,26 @@ def trace(span_type, span_name, tags: {}, &block) end end + def active_span + tracer_span = Datadog::Tracing.active_span + Span.new(tracer_span) if tracer_span + end + def active_test @local_context.active_test end + def active_test_session + @global_context.active_test_session + end + + # TODO: does it make sense to have a paramter here? def deactivate_test(test) @local_context.deactivate_test!(test) end - def active_span - tracer_span = Datadog::Tracing.active_span - Span.new(tracer_span) if tracer_span + def deactivate_test_session + @global_context.deactivate_test_session! end private @@ -93,26 +122,30 @@ def set_trace_origin(trace) trace.origin = Ext::Test::CONTEXT_ORIGIN if trace end + def build_test_session(tracer_span, tags) + test_session = TestSession.new(tracer_span) + set_initial_tags(test_session, tags) + test_session + end + def build_test(tracer_span, tags) test = Test.new(tracer_span) - - test.set_default_tags - test.set_environment_runtime_tags - - test.set_tags(tags) - test.set_tags(environment_tags) - + set_initial_tags(test, tags) test end def build_span(tracer_span, tags) span = Span.new(tracer_span) + set_initial_tags(span, tags) + span + end - span.set_default_tags - span.set_environment_runtime_tags - span.set_tags(tags) + def set_initial_tags(ci_span, tags) + ci_span.set_default_tags + ci_span.set_environment_runtime_tags - span + ci_span.set_tags(tags) + ci_span.set_tags(environment_tags) end end end diff --git a/lib/datadog/ci/test.rb b/lib/datadog/ci/test.rb index 6cf43de2..d0cc5187 100644 --- a/lib/datadog/ci/test.rb +++ b/lib/datadog/ci/test.rb @@ -5,7 +5,6 @@ module Datadog module CI # Represents a single part of a test run. - # Could be a session, suite, test, or any custom span. # # @public_api class Test < Span diff --git a/lib/datadog/ci/test_session.rb b/lib/datadog/ci/test_session.rb new file mode 100644 index 00000000..c6e49217 --- /dev/null +++ b/lib/datadog/ci/test_session.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative "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 + + # Finishes the current test session. + # @return [void] + def finish + super + + CI.deactivate_test_session + end + end + end +end diff --git a/lib/datadog/ci/test_visibility/serializers/test_v1.rb b/lib/datadog/ci/test_visibility/serializers/test_v1.rb index c2547234..ed9fb740 100644 --- a/lib/datadog/ci/test_visibility/serializers/test_v1.rb +++ b/lib/datadog/ci/test_visibility/serializers/test_v1.rb @@ -37,7 +37,7 @@ def content_map_size end def type - "test" + Ext::AppTypes::TYPE_TEST end def name diff --git a/sig/datadog/ci.rbs b/sig/datadog/ci.rbs index 9a1b76b4..2340fccd 100644 --- a/sig/datadog/ci.rbs +++ b/sig/datadog/ci.rbs @@ -4,14 +4,20 @@ module Datadog def self.start_test: (String span_name, ?service_name: String?, ?operation_name: String, ?tags: Hash[untyped, untyped]) -> Datadog::CI::Test + def self.start_test_session: (?service_name: String?, ?tags: Hash[untyped, untyped]) -> Datadog::CI::TestSession + def self.trace: (String span_type, String span_name, ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped + def self.active_test_session: () -> Datadog::CI::TestSession? + def self.active_test: () -> Datadog::CI::Test? def self.active_span: (String span_type) -> Datadog::CI::Span? def self.deactivate_test: (Datadog::CI::Test test) -> void + def self.deactivate_test_session: () -> void + def self.components: () -> Datadog::CI::Configuration::Components def self.recorder: () -> Datadog::CI::Recorder diff --git a/sig/datadog/ci/context/global.rbs b/sig/datadog/ci/context/global.rbs new file mode 100644 index 00000000..ce319b46 --- /dev/null +++ b/sig/datadog/ci/context/global.rbs @@ -0,0 +1,19 @@ +module Datadog + module CI + module Context + class Global + @mutex: Thread::Mutex + + @test_session: Datadog::CI::TestSession? + + def initialize: () -> void + + def active_test_session: () -> Datadog::CI::TestSession? + + def activate_test_session!: (Datadog::CI::TestSession test_session) -> void + + def deactivate_test_session!: () -> void + end + end + end +end diff --git a/sig/datadog/ci/ext/app_types.rbs b/sig/datadog/ci/ext/app_types.rbs index 8188d06c..83cd3e79 100644 --- a/sig/datadog/ci/ext/app_types.rbs +++ b/sig/datadog/ci/ext/app_types.rbs @@ -2,7 +2,8 @@ module Datadog module CI module Ext module AppTypes - TYPE_TEST: String + TYPE_TEST: "test" + TYPE_TEST_SESSION: "test_session_end" end end end diff --git a/sig/datadog/ci/recorder.rbs b/sig/datadog/ci/recorder.rbs index ff06b0c7..6bafc806 100644 --- a/sig/datadog/ci/recorder.rbs +++ b/sig/datadog/ci/recorder.rbs @@ -3,6 +3,7 @@ module Datadog class Recorder @environment_tags: Hash[String, String] @local_context: Datadog::CI::Context::Local + @global_context: Datadog::CI::Context::Global attr_reader environment_tags: Hash[String, String] @@ -10,12 +11,18 @@ module Datadog def trace: (String span_type, String span_name, ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped + def start_test_session: (?service_name: String?, ?tags: Hash[untyped, untyped]) -> Datadog::CI::TestSession + + def active_test_session: () -> Datadog::CI::TestSession? + def active_test: () -> Datadog::CI::Test? def active_span: () -> Datadog::CI::Span? def deactivate_test: (Datadog::CI::Test test) -> void + def deactivate_test_session: () -> void + def create_datadog_span: (String span_name, ?span_options: Hash[untyped, untyped], ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Span span) -> untyped } -> untyped def set_trace_origin: (Datadog::Tracing::TraceOperation trace) -> untyped @@ -24,7 +31,11 @@ module Datadog def build_test: (Datadog::Tracing::SpanOperation tracer_span, Hash[untyped, untyped] tags) -> Datadog::CI::Test + def build_test_session: (Datadog::Tracing::SpanOperation tracer_span, Hash[untyped, untyped] tags) -> Datadog::CI::TestSession + def build_span: (Datadog::Tracing::SpanOperation tracer_span, Hash[untyped, untyped] tags) -> Datadog::CI::Span + + def set_initial_tags: (Datadog::CI::Span ci_span, Hash[untyped, untyped] tags) -> void end end end diff --git a/sig/datadog/ci/test_session.rbs b/sig/datadog/ci/test_session.rbs new file mode 100644 index 00000000..9f439e47 --- /dev/null +++ b/sig/datadog/ci/test_session.rbs @@ -0,0 +1,7 @@ +module Datadog + module CI + class TestSession < Span + @mutex: Thread::Mutex + end + end +end diff --git a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs index a590d687..3c365885 100644 --- a/sig/datadog/ci/test_visibility/serializers/test_v1.rbs +++ b/sig/datadog/ci/test_visibility/serializers/test_v1.rbs @@ -11,7 +11,7 @@ module Datadog def content_fields: () -> Array[String | Hash[String, String]] def content_map_size: () -> Integer - def type: () -> "test" + def type: () -> ::String def name: () -> ::String diff --git a/spec/datadog/ci/context/global_spec.rb b/spec/datadog/ci/context/global_spec.rb new file mode 100644 index 00000000..a9330479 --- /dev/null +++ b/spec/datadog/ci/context/global_spec.rb @@ -0,0 +1,51 @@ +RSpec.describe Datadog::CI::Context::Global do + subject { described_class.new } + + let(:tracer_span) { double(Datadog::Tracing::SpanOperation, name: "test.session") } + let(:session) { Datadog::CI::TestSession.new(tracer_span) } + + describe "#activate_test_session!" do + context "when a test session is already active" do + before do + subject.activate_test_session!(Datadog::CI::TestSession.new(tracer_span)) + end + + it "raises an error" do + expect { subject.activate_test_session!(session) }.to( + raise_error( + RuntimeError, + "Nested test sessions are not supported. Currently active test session: " \ + "#{session}" + ) + ) + end + end + + context "when no test session is active" do + it "activates the test session" do + subject.activate_test_session!(session) + expect(subject.active_test_session).to be(session) + end + end + end + + describe "#deactivate_test_session!" do + context "when a test session is active" do + before do + subject.activate_test_session!(session) + end + + it "deactivates the test session" do + subject.deactivate_test_session! + expect(subject.active_test_session).to be_nil + end + end + + context "when no test session is active" do + it "does nothing" do + subject.deactivate_test_session! + expect(subject.active_test_session).to be_nil + end + end + end +end diff --git a/spec/datadog/ci/recorder_spec.rb b/spec/datadog/ci/recorder_spec.rb index 7f4ee337..c14f514a 100644 --- a/spec/datadog/ci/recorder_spec.rb +++ b/spec/datadog/ci/recorder_spec.rb @@ -6,6 +6,10 @@ let(:tags) { {} } let(:environment_tags) { Datadog::CI::Ext::Environment.tags(ENV) } + let(:ci_span) do + spy("CI object spy") + end + subject(:recorder) { described_class.new } before do @@ -21,15 +25,16 @@ end end - describe "#trace_test" do - def expect_initialized_test - allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_test) - expect(ci_test).to receive(:set_default_tags) - expect(ci_test).to receive(:set_environment_runtime_tags) - expect(ci_test).to receive(:set_tags).with(tags) - expect(ci_test).to receive(:set_tags).with(environment_tags) + shared_examples_for "initialize ci span with tags" do + 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(environment_tags) end + end + describe "#trace_test" do context "when given a block" do subject(:trace) do recorder.trace_test( @@ -42,7 +47,6 @@ def expect_initialized_test end let(:span_op) { Datadog::Tracing::SpanOperation.new(operation_name) } - let(:ci_test) { instance_double(Datadog::CI::Test) } let(:block) { proc { |s| block_spy.call(s) } } let(:block_result) { double("result") } let(:block_spy) { spy("block") } @@ -61,7 +65,7 @@ def expect_initialized_test } ) - expect_initialized_test + allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) trace_block.call(span_op, trace_op) end @@ -70,7 +74,9 @@ def expect_initialized_test end it_behaves_like "internal tracing context" - it { expect(block_spy).to have_received(:call).with(ci_test) } + it_behaves_like "initialize ci span with tags" + + it { expect(block_spy).to have_received(:call).with(ci_span) } it { is_expected.to be(block_result) } end @@ -84,7 +90,6 @@ def expect_initialized_test ) end let(:span_op) { Datadog::Tracing::SpanOperation.new(operation_name) } - let(:ci_test) { instance_double(Datadog::CI::Test) } before do allow(Datadog::Tracing) @@ -99,24 +104,18 @@ def expect_initialized_test ) .and_return(span_op) - expect_initialized_test + allow(Datadog::CI::Test).to receive(:new).with(span_op).and_return(ci_span) trace end it_behaves_like "internal tracing context" - it { is_expected.to be(ci_test) } + it_behaves_like "initialize ci span with tags" + it { is_expected.to be(ci_span) } end end describe "#trace" do - def expect_initialized_span - allow(Datadog::CI::Span).to receive(:new).with(span_op).and_return(ci_span) - expect(ci_span).to receive(:set_default_tags) - expect(ci_span).to receive(:set_environment_runtime_tags) - expect(ci_span).to receive(:set_tags).with(tags) - end - let(:tags) { {"my_tag" => "my_value"} } let(:span_type) { "step" } let(:span_name) { "span name" } @@ -134,7 +133,6 @@ def expect_initialized_span end let(:span_op) { Datadog::Tracing::SpanOperation.new(span_name) } - let(:ci_span) { instance_double(Datadog::CI::Span) } let(:block) { proc { |s| block_spy.call(s) } } let(:block_result) { double("result") } let(:block_spy) { spy("block") } @@ -154,11 +152,12 @@ def expect_initialized_span trace_block.call(span_op, trace_op) end - expect_initialized_span + allow(Datadog::CI::Span).to receive(:new).with(span_op).and_return(ci_span) trace end + it_behaves_like "initialize ci span with tags" it { expect(block_spy).to have_received(:call).with(ci_span) } it { is_expected.to be(block_result) } end @@ -173,7 +172,6 @@ def expect_initialized_span end let(:span_op) { Datadog::Tracing::SpanOperation.new(span_name) } - let(:ci_span) { instance_double(Datadog::CI::Span) } before do allow(Datadog::Tracing) @@ -187,11 +185,12 @@ def expect_initialized_span ) .and_return(span_op) - expect_initialized_span + allow(Datadog::CI::Span).to receive(:new).with(span_op).and_return(ci_span) trace end + it_behaves_like "initialize ci span with tags" it { is_expected.to be(ci_span) } end end @@ -251,4 +250,58 @@ def expect_initialized_span it { is_expected.to be_nil } end end + + describe "#start_test_session" do + subject(:start_test_session) { recorder.start_test_session(service_name: service) } + + let(:session_operation_name) { "test.session" } + + let(:span_op) { Datadog::Tracing::SpanOperation.new(session_operation_name) } + + before do + allow(Datadog::Tracing) + .to receive(:trace) + .with( + session_operation_name, + { + span_type: Datadog::CI::Ext::AppTypes::TYPE_TEST_SESSION, + service: service + } + ) + .and_return(span_op) + + allow(Datadog::CI::TestSession).to receive(:new).with(span_op).and_return(ci_span) + + start_test_session + end + + it_behaves_like "internal tracing context" + it_behaves_like "initialize ci span with tags" + + it { is_expected.to be(ci_span) } + end + + describe "#active_test_session" do + subject(:active_test_session) { recorder.active_test_session } + + let(:ci_session) do + recorder.start_test_session(service_name: service) + end + + before { ci_session } + + it { is_expected.to be(ci_session) } + end + + describe "#deactivate_test_session" do + subject(:deactivate_test_session) { recorder.deactivate_test_session } + + let(:ci_session) do + recorder.start_test_session(service_name: service) + end + + before { deactivate_test_session } + + it { expect(recorder.active_test_session).to be_nil } + end end diff --git a/spec/datadog/ci/test_session_spec.rb b/spec/datadog/ci/test_session_spec.rb new file mode 100644 index 00000000..e2a6e148 --- /dev/null +++ b/spec/datadog/ci/test_session_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.describe Datadog::CI::TestSession do + let(:tracer_span) { instance_double(Datadog::Tracing::SpanOperation, finish: true) } + + describe "#finish" do + subject(:ci_test_session) { described_class.new(tracer_span) } + + before { allow(Datadog::CI).to receive(:deactivate_test_session) } + + it "deactivates the test" do + ci_test_session.finish + + expect(Datadog::CI).to have_received(:deactivate_test_session) + end + end +end diff --git a/spec/datadog/ci_spec.rb b/spec/datadog/ci_spec.rb index bee49fbd..d8a71aa3 100644 --- a/spec/datadog/ci_spec.rb +++ b/spec/datadog/ci_spec.rb @@ -98,4 +98,38 @@ it { is_expected.to be_nil } end end + + describe "::start_test_session" do + subject(:start_test_session) { described_class.start_test_session } + + let(:ci_test_session) { instance_double(Datadog::CI::TestSession) } + + before do + allow(recorder).to receive(:start_test_session).and_return(ci_test_session) + end + + it { is_expected.to be(ci_test_session) } + end + + describe "::active_test_session" do + subject(:active_test_session) { described_class.active_test_session } + + let(:ci_test_session) { instance_double(Datadog::CI::TestSession) } + + before do + allow(recorder).to receive(:active_test_session).and_return(ci_test_session) + end + + it { is_expected.to be(ci_test_session) } + end + + describe "::deactivate_test_session" do + subject(:deactivate_test_session) { described_class.deactivate_test_session } + + before do + allow(recorder).to receive(:deactivate_test_session) + end + + it { is_expected.to be_nil } + end end