diff --git a/README.md b/README.md index 3613ef0f..2f414437 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Webmock accordingly. ```ruby # when using agentless mode # note to use the correct datadog site (e.g. datadoghq.eu, etc) -WebMock.disable_net_connect!(:allow => "citestcycle-intake.datadoghq.com") +WebMock.disable_net_connect!(:allow => /datadoghq.com/) # when using agent WebMock.disable_net_connect!(:allow_localhost => true) @@ -199,7 +199,7 @@ VCR.configure do |config| # when using agentless mode # note to use the correct datadog site (e.g. datadoghq.eu, etc) - config.ignore_hosts "citestcycle-intake.datadoghq.com" + config.ignore_hosts "citestcycle-intake.datadoghq.com", "api.datadoghq.com" end ``` diff --git a/lib/datadog/ci/configuration/components.rb b/lib/datadog/ci/configuration/components.rb index 1ce8e2c3..9977a401 100644 --- a/lib/datadog/ci/configuration/components.rb +++ b/lib/datadog/ci/configuration/components.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative "../ext/settings" +require_relative "../itr/runner" require_relative "../test_visibility/flush" require_relative "../test_visibility/recorder" require_relative "../test_visibility/null_recorder" @@ -8,6 +9,7 @@ require_relative "../test_visibility/serializers/factories/test_suite_level" require_relative "../test_visibility/transport" require_relative "../transport/api/builder" +require_relative "../transport/remote_settings_api" module Datadog module CI @@ -59,13 +61,30 @@ def activate_ci!(settings) writer_options[:buffer_size] = 10_000 settings.tracing.test_mode.async = true + else + # only legacy APM protocol is supported, so no test suite level visibility + settings.ci.force_test_level_visibility = true + + # ITR is not supported with APM protocol + settings.ci.itr_enabled = false end settings.tracing.test_mode.writer_options = writer_options + itr = Datadog::CI::ITR::Runner.new( + enabled: settings.ci.enabled && settings.ci.itr_enabled + ) + + remote_settings_api = Transport::RemoteSettingsApi.new( + api: test_visibility_api, + dd_env: settings.env + ) + # CI visibility recorder global instance @ci_recorder = TestVisibility::Recorder.new( - test_suite_level_visibility_enabled: !settings.ci.force_test_level_visibility + test_suite_level_visibility_enabled: !settings.ci.force_test_level_visibility, + itr: itr, + remote_settings_api: remote_settings_api ) end @@ -95,9 +114,6 @@ def build_test_visibility_api(settings) Datadog.logger.debug( "Old agent version detected, no evp_proxy support. Forcing test level visibility mode" ) - - # CI visibility is still enabled but in legacy test level visibility mode - settings.ci.force_test_level_visibility = true end end diff --git a/lib/datadog/ci/configuration/settings.rb b/lib/datadog/ci/configuration/settings.rb index 141a6786..2e85a023 100644 --- a/lib/datadog/ci/configuration/settings.rb +++ b/lib/datadog/ci/configuration/settings.rb @@ -55,6 +55,12 @@ def self.add_settings!(base) end end + option :itr_enabled do |o| + o.type :bool + o.env CI::Ext::Settings::ENV_ITR_ENABLED + o.default false + end + define_method(:instrument) do |integration_name, options = {}, &block| return unless enabled diff --git a/lib/datadog/ci/ext/settings.rb b/lib/datadog/ci/ext/settings.rb index 7357d790..a6d16a30 100644 --- a/lib/datadog/ci/ext/settings.rb +++ b/lib/datadog/ci/ext/settings.rb @@ -10,6 +10,7 @@ module Settings ENV_AGENTLESS_URL = "DD_CIVISIBILITY_AGENTLESS_URL" ENV_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED = "DD_CIVISIBILITY_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED" ENV_FORCE_TEST_LEVEL_VISIBILITY = "DD_CIVISIBILITY_FORCE_TEST_LEVEL_VISIBILITY" + ENV_ITR_ENABLED = "DD_CIVISIBILITY_ITR_ENABLED" # Source: https://docs.datadoghq.com/getting_started/site/ DD_SITE_ALLOWLIST = [ diff --git a/lib/datadog/ci/ext/test.rb b/lib/datadog/ci/ext/test.rb index b5bbea31..7c548203 100644 --- a/lib/datadog/ci/ext/test.rb +++ b/lib/datadog/ci/ext/test.rb @@ -23,6 +23,13 @@ module Test TAG_CODEOWNERS = "test.codeowners" TAG_PARAMETERS = "test.parameters" + # ITR tags + TAG_ITR_TEST_SKIPPING_ENABLED = "test.itr.tests_skipping.enabled" + TAG_ITR_TEST_SKIPPING_TYPE = "test.itr.tests_skipping.type" + + # Code coverage tags + TAG_CODE_COVERAGE_ENABLED = "test.code_coverage.enabled" + # those tags are special and used to correlate tests with the test sessions, suites, and modules # they are transient and not sent to the backend TAG_TEST_SESSION_ID = "_test.session_id" @@ -43,6 +50,10 @@ module Test TAG_SPAN_KIND = "span.kind" SPAN_KIND_TEST = "test" + # could be either "test" or "suite" depending on whether we skip individual tests or whole suites + # we use test skipping for Ruby + ITR_TEST_SKIPPING_MODE = "test" + # test status as recognized by Datadog module Status PASS = "pass" diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index d7da4acf..3658abf0 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -23,7 +23,18 @@ module Transport TEST_VISIBILITY_INTAKE_HOST_PREFIX = "citestcycle-intake" TEST_VISIBILITY_INTAKE_PATH = "/api/v2/citestcycle" + DD_API_HOST_PREFIX = "api" + DD_API_SETTINGS_PATH = "/api/v2/libraries/tests/services/setting" + DD_API_SETTINGS_TYPE = "ci_app_test_service_libraries_settings" + DD_API_SETTINGS_RESPONSE_DIG_KEYS = %w[data attributes].freeze + DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY = "itr_enabled" + DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY = "code_coverage" + DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY = "tests_skipping" + DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY = "require_git" + DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze + CONTENT_TYPE_MESSAGEPACK = "application/msgpack" + CONTENT_TYPE_JSON = "application/json" CONTENT_ENCODING_GZIP = "gzip" end end diff --git a/lib/datadog/ci/itr/runner.rb b/lib/datadog/ci/itr/runner.rb new file mode 100644 index 00000000..f78d8146 --- /dev/null +++ b/lib/datadog/ci/itr/runner.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require_relative "../ext/test" +require_relative "../ext/transport" + +module Datadog + module CI + module ITR + # Intelligent test runner implementation + # Integrates with backend to provide test impact analysis data and + # skip tests that are not impacted by the changes + class Runner + def initialize( + enabled: false + ) + @enabled = enabled + @test_skipping_enabled = false + @code_coverage_enabled = false + + Datadog.logger.debug("ITR Runner initialized with enabled: #{@enabled}") + end + + def configure(remote_configuration, test_session) + Datadog.logger.debug("Configuring ITR Runner with remote configuration: #{remote_configuration}") + + @enabled = convert_to_bool( + remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY, false) + ) + @test_skipping_enabled = @enabled && convert_to_bool( + remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false) + ) + @code_coverage_enabled = @enabled && convert_to_bool( + remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false) + ) + + test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, @test_skipping_enabled) + # currently we set this tag when ITR requires collecting code coverage + # this will change as soon as we implement total code coverage support in this library + test_session.set_tag(Ext::Test::TAG_CODE_COVERAGE_ENABLED, @code_coverage_enabled) + + # we skip tests, not suites + test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE, Ext::Test::ITR_TEST_SKIPPING_MODE) + + Datadog.logger.debug("Configured ITR Runner with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}") + end + + def enabled? + @enabled + end + + def skipping_tests? + @test_skipping_enabled + end + + def code_coverage? + @code_coverage_enabled + end + + private + + def convert_to_bool(value) + value.to_s == "true" + end + end + end + end +end diff --git a/lib/datadog/ci/span.rb b/lib/datadog/ci/span.rb index e09c9b52..481c87e1 100644 --- a/lib/datadog/ci/span.rb +++ b/lib/datadog/ci/span.rb @@ -121,6 +121,48 @@ def set_tags(tags) tracer_span.set_tags(tags) end + # Returns the git repository URL extracted from the environment. + # @return [String] the repository URL. + def git_repository_url + tracer_span.get_tag(Ext::Git::TAG_REPOSITORY_URL) + end + + # Returns the latest commit SHA extracted from the environment. + # @return [String] the commit SHA of the last commit. + def git_commit_sha + tracer_span.get_tag(Ext::Git::TAG_COMMIT_SHA) + end + + # Returns the git branch name extracted from the environment. + # @return [String] the branch. + def git_branch + tracer_span.get_tag(Ext::Git::TAG_BRANCH) + end + + # Returns the OS architecture extracted from the environment. + # @return [String] OS arch. + def os_architecture + tracer_span.get_tag(Ext::Test::TAG_OS_ARCHITECTURE) + end + + # Returns the OS platform extracted from the environment. + # @return [String] OS platform. + def os_platform + tracer_span.get_tag(Ext::Test::TAG_OS_PLATFORM) + end + + # Returns the runtime name extracted from the environment. + # @return [String] runtime name. + def runtime_name + tracer_span.get_tag(Ext::Test::TAG_RUNTIME_NAME) + end + + # Returns the runtime version extracted from the environment. + # @return [String] runtime version. + def runtime_version + tracer_span.get_tag(Ext::Test::TAG_RUNTIME_VERSION) + end + def set_environment_runtime_tags tracer_span.set_tag(Ext::Test::TAG_OS_ARCHITECTURE, ::RbConfig::CONFIG["host_cpu"]) tracer_span.set_tag(Ext::Test::TAG_OS_PLATFORM, ::RbConfig::CONFIG["host_os"]) diff --git a/lib/datadog/ci/test_session.rb b/lib/datadog/ci/test_session.rb index 354abf1b..7ffdefbc 100644 --- a/lib/datadog/ci/test_session.rb +++ b/lib/datadog/ci/test_session.rb @@ -26,6 +26,14 @@ def name get_tag(Ext::Test::TAG_COMMAND) end + def skipping_tests? + get_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED) == "true" + end + + def code_coverage? + get_tag(Ext::Test::TAG_CODE_COVERAGE_ENABLED) == "true" + end + # Return the test session tags that could be inherited by sub-spans # @return [Hash] the tags to be inherited by sub-spans. def inheritable_tags diff --git a/lib/datadog/ci/test_visibility/recorder.rb b/lib/datadog/ci/test_visibility/recorder.rb index c274590f..29330254 100644 --- a/lib/datadog/ci/test_visibility/recorder.rb +++ b/lib/datadog/ci/test_visibility/recorder.rb @@ -29,7 +29,7 @@ class Recorder attr_reader :environment_tags, :test_suite_level_visibility_enabled def initialize( - test_suite_level_visibility_enabled: false, + itr:, remote_settings_api:, test_suite_level_visibility_enabled: false, codeowners: Codeowners::Parser.new(Utils::Git.root).parse ) @test_suite_level_visibility_enabled = test_suite_level_visibility_enabled @@ -37,7 +37,11 @@ def initialize( @environment_tags = Ext::Environment.tags(ENV).freeze @local_context = Context::Local.new @global_context = Context::Global.new + @codeowners = codeowners + + @itr = itr + @remote_settings_api = remote_settings_api end def start_test_session(service: nil, tags: {}) @@ -49,7 +53,11 @@ def start_test_session(service: nil, tags: {}) ) set_session_context(tags, tracer_span) - build_test_session(tracer_span, tags) + test_session = build_test_session(tracer_span, tags) + + configure_library(test_session) + + test_session end end @@ -175,8 +183,20 @@ def deactivate_test_suite(test_suite_name) @global_context.deactivate_test_suite!(test_suite_name) end + def itr_enabled? + @itr.enabled? + end + private + def configure_library(test_session) + # this will change when EFD is implemented + return unless itr_enabled? + + remote_configuration = @remote_settings_api.fetch_library_settings(test_session) + @itr.configure(remote_configuration.payload, test_session) + end + def skip_tracing(block = nil) block.call(nil) if block end diff --git a/lib/datadog/ci/test_visibility/transport.rb b/lib/datadog/ci/test_visibility/transport.rb index cd0d17f3..45f79a17 100644 --- a/lib/datadog/ci/test_visibility/transport.rb +++ b/lib/datadog/ci/test_visibility/transport.rb @@ -55,10 +55,6 @@ def send_traces(traces) response = send_payload(encoded_payload) - Datadog.logger.debug do - "Received server response: #{response.inspect}" - end - responses << response end @@ -68,7 +64,7 @@ def send_traces(traces) private def send_payload(encoded_payload) - api.request( + api.citestcycle_request( path: Datadog::CI::Ext::Transport::TEST_VISIBILITY_INTAKE_PATH, payload: encoded_payload ) diff --git a/lib/datadog/ci/transport/api/agentless.rb b/lib/datadog/ci/transport/api/agentless.rb index 3e4503f6..fb6ad7ac 100644 --- a/lib/datadog/ci/transport/api/agentless.rb +++ b/lib/datadog/ci/transport/api/agentless.rb @@ -10,15 +10,48 @@ module Api class Agentless < Base attr_reader :api_key - def initialize(api_key:, http:) + def initialize(api_key:, citestcycle_url:, api_url:) @api_key = api_key + @citestcycle_http = build_http_client(citestcycle_url, compress: true) + @api_http = build_http_client(api_url, compress: false) + end + + def citestcycle_request(path:, payload:, headers: {}, verb: "post") + super + + perform_request(@citestcycle_http, path: path, payload: payload, headers: headers, verb: verb) + end + + def api_request(path:, payload:, headers: {}, verb: "post") + super - super(http: http) + perform_request(@api_http, path: path, payload: payload, headers: headers, verb: verb) end private - def headers + def perform_request(http_client, path:, payload:, headers:, verb:) + http_client.request( + path: path, + payload: payload, + headers: headers_with_default(headers), + verb: verb + ) + end + + def build_http_client(url, compress:) + uri = URI.parse(url) + raise "Invalid agentless mode URL: #{url}" if uri.host.nil? + + Datadog::CI::Transport::HTTP.new( + host: uri.host, + port: uri.port, + ssl: uri.scheme == "https" || uri.port == 443, + compress: compress + ) + end + + def default_headers headers = super headers[Ext::Transport::HEADER_DD_API_KEY] = api_key headers diff --git a/lib/datadog/ci/transport/api/base.rb b/lib/datadog/ci/transport/api/base.rb index 6230b887..ff11e7e8 100644 --- a/lib/datadog/ci/transport/api/base.rb +++ b/lib/datadog/ci/transport/api/base.rb @@ -7,27 +7,23 @@ module CI module Transport module Api class Base - attr_reader :http + def api_request(path:, payload:, headers: {}, verb: "post") + headers[Ext::Transport::HEADER_CONTENT_TYPE] ||= Ext::Transport::CONTENT_TYPE_JSON + end - def initialize(http:) - @http = http + def citestcycle_request(path:, payload:, headers: {}, verb: "post") + headers[Ext::Transport::HEADER_CONTENT_TYPE] ||= Ext::Transport::CONTENT_TYPE_MESSAGEPACK end - def request(path:, payload:, verb: "post") - http.request( - path: path, - payload: payload, - verb: verb, - headers: headers - ) + def headers_with_default(headers) + request_headers = default_headers + request_headers.merge!(headers) end private - def headers - { - Ext::Transport::HEADER_CONTENT_TYPE => Ext::Transport::CONTENT_TYPE_MESSAGEPACK - } + def default_headers + {} end end end diff --git a/lib/datadog/ci/transport/api/builder.rb b/lib/datadog/ci/transport/api/builder.rb index 2e845deb..fddc3002 100644 --- a/lib/datadog/ci/transport/api/builder.rb +++ b/lib/datadog/ci/transport/api/builder.rb @@ -17,20 +17,14 @@ def self.build_agentless_api(settings) return nil if settings.api_key.nil? dd_site = settings.site || Ext::Transport::DEFAULT_DD_SITE - url = settings.ci.agentless_url || - "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" - uri = URI.parse(url) - raise "Invalid agentless mode URL: #{url}" if uri.host.nil? + citestcycle_url = settings.ci.agentless_url || + "https://#{Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX}.#{dd_site}:443" - http = Datadog::CI::Transport::HTTP.new( - host: uri.host, - port: uri.port, - ssl: uri.scheme == "https" || uri.port == 443, - compress: true - ) + api_url = settings.ci.agentless_url || + "https://#{Ext::Transport::DD_API_HOST_PREFIX}.#{dd_site}:443" - Agentless.new(api_key: settings.api_key, http: http) + Agentless.new(api_key: settings.api_key, citestcycle_url: citestcycle_url, api_url: api_url) end def self.build_evp_proxy_api(settings) @@ -46,15 +40,7 @@ def self.build_evp_proxy_api(settings) return nil if evp_proxy_path_prefix.nil? - http = Datadog::CI::Transport::HTTP.new( - host: agent_settings.hostname, - port: agent_settings.port, - ssl: agent_settings.ssl, - timeout: agent_settings.timeout_seconds, - compress: Ext::Transport::EVP_PROXY_COMPRESSION_SUPPORTED[evp_proxy_path_prefix] - ) - - EvpProxy.new(http: http, path_prefix: evp_proxy_path_prefix) + EvpProxy.new(agent_settings: agent_settings, path_prefix: evp_proxy_path_prefix) end end end diff --git a/lib/datadog/ci/transport/api/evp_proxy.rb b/lib/datadog/ci/transport/api/evp_proxy.rb index 3e7067a2..8c93e32d 100644 --- a/lib/datadog/ci/transport/api/evp_proxy.rb +++ b/lib/datadog/ci/transport/api/evp_proxy.rb @@ -10,24 +10,48 @@ module CI module Transport module Api class EvpProxy < Base - def initialize(http:, path_prefix: Ext::Transport::EVP_PROXY_V2_PATH_PREFIX) - super(http: http) + def initialize(agent_settings:, path_prefix: Ext::Transport::EVP_PROXY_V2_PATH_PREFIX) + @agent_intake_http = build_http_client( + agent_settings, + compress: Ext::Transport::EVP_PROXY_COMPRESSION_SUPPORTED[path_prefix] + ) + + @agent_api_http = build_http_client(agent_settings, compress: false) path_prefix = "#{path_prefix}/" unless path_prefix.end_with?("/") @path_prefix = path_prefix end - def request(path:, payload:, verb: "post") - path = "#{@path_prefix}#{path.sub(/^\//, "")}" + def citestcycle_request(path:, payload:, headers: {}, verb: "post") + super + + headers[Ext::Transport::HEADER_EVP_SUBDOMAIN] = Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX + + perform_request(@agent_intake_http, path: path, payload: payload, headers: headers, verb: verb) + end + + def api_request(path:, payload:, headers: {}, verb: "post") + super + + headers[Ext::Transport::HEADER_EVP_SUBDOMAIN] = Ext::Transport::DD_API_HOST_PREFIX + + perform_request(@agent_api_http, path: path, payload: payload, headers: headers, verb: verb) + end + + private - super( - path: path, + def perform_request(http_client, path:, payload:, headers:, verb:) + http_client.request( + path: path_with_prefix(path), payload: payload, + headers: headers_with_default(headers), verb: verb ) end - private + def path_with_prefix(path) + "#{@path_prefix}#{path.sub(/^\//, "")}" + end def container_id return @container_id if defined?(@container_id) @@ -35,15 +59,24 @@ def container_id @container_id = Datadog::Core::Environment::Container.container_id end - def headers + def default_headers headers = super - headers[Ext::Transport::HEADER_EVP_SUBDOMAIN] = Ext::Transport::TEST_VISIBILITY_INTAKE_HOST_PREFIX c_id = container_id headers[Ext::Transport::HEADER_CONTAINER_ID] = c_id unless c_id.nil? headers end + + def build_http_client(agent_settings, compress:) + Datadog::CI::Transport::HTTP.new( + host: agent_settings.hostname, + port: agent_settings.port, + ssl: agent_settings.ssl, + timeout: agent_settings.timeout_seconds, + compress: compress + ) + end end end end diff --git a/lib/datadog/ci/transport/http.rb b/lib/datadog/ci/transport/http.rb index 87bdffc8..5477669d 100644 --- a/lib/datadog/ci/transport/http.rb +++ b/lib/datadog/ci/transport/http.rb @@ -40,11 +40,17 @@ def request(path:, payload:, headers:, verb: "post") "compression_enabled=#{compress}; path=#{path}; payload_size=#{payload.size}" end - ResponseDecorator.new( + response = ResponseDecorator.new( adapter.call( build_env(path: path, payload: payload, headers: headers, verb: verb) ) ) + + Datadog.logger.debug do + "Received server response: #{response.inspect}" + end + + response end private diff --git a/lib/datadog/ci/transport/remote_settings_api.rb b/lib/datadog/ci/transport/remote_settings_api.rb new file mode 100644 index 00000000..634339ca --- /dev/null +++ b/lib/datadog/ci/transport/remote_settings_api.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require "json" + +require "datadog/core/environment/identity" + +require_relative "../ext/transport" + +module Datadog + module CI + module Transport + # Datadog API client + # Calls settings endpoint to fetch library settings for given service and env + class RemoteSettingsApi + class Response + def initialize(http_response) + @http_response = http_response + @json = nil + end + + def ok? + resp = @http_response + !resp.nil? && resp.ok? + end + + def payload + cached = @json + return cached unless cached.nil? + + resp = @http_response + return @json = default_payload if resp.nil? || !resp.ok? + + begin + @json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) || + default_payload + rescue JSON::ParserError => e + Datadog.logger.error("Failed to parse settings response payload: #{e}. Payload was: #{resp.payload}") + @json = default_payload + end + end + + private + + def default_payload + Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT + end + end + + def initialize(api: nil, dd_env: nil) + @api = api + @dd_env = dd_env + end + + def fetch_library_settings(test_session) + api = @api + return Response.new(nil) unless api + + request_payload = payload(test_session) + Datadog.logger.debug("Fetching library settings with request: #{request_payload}") + + http_response = api.api_request( + path: Ext::Transport::DD_API_SETTINGS_PATH, + payload: request_payload + ) + + Response.new(http_response) + end + + private + + def payload(test_session) + { + "data" => { + "id" => Datadog::Core::Environment::Identity.id, + "type" => Ext::Transport::DD_API_SETTINGS_TYPE, + "attributes" => { + "service" => test_session.service, + "env" => @dd_env, + "repository_url" => test_session.git_repository_url, + "branch" => test_session.git_branch, + "sha" => test_session.git_commit_sha, + "test_level" => Ext::Test::ITR_TEST_SKIPPING_MODE, + "configurations" => { + "os.platform" => test_session.os_platform, + "os.arch" => test_session.os_architecture, + "runtime.name" => test_session.runtime_name, + "runtime.version" => test_session.runtime_version + } + } + } + }.to_json + end + end + end + end +end diff --git a/sig/datadog/ci/ext/settings.rbs b/sig/datadog/ci/ext/settings.rbs index db4fba33..2ead3eab 100644 --- a/sig/datadog/ci/ext/settings.rbs +++ b/sig/datadog/ci/ext/settings.rbs @@ -7,6 +7,7 @@ module Datadog ENV_AGENTLESS_URL: String ENV_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED: String ENV_FORCE_TEST_LEVEL_VISIBILITY: String + ENV_ITR_ENABLED: String DD_SITE_ALLOWLIST: Array[String] end diff --git a/sig/datadog/ci/ext/test.rbs b/sig/datadog/ci/ext/test.rbs index a2c6588b..5b235a54 100644 --- a/sig/datadog/ci/ext/test.rbs +++ b/sig/datadog/ci/ext/test.rbs @@ -32,6 +32,12 @@ module Datadog TAG_PARAMETERS: "test.parameters" + TAG_ITR_TEST_SKIPPING_ENABLED: "test.itr.tests_skipping.enabled" + + TAG_ITR_TEST_SKIPPING_TYPE: "test.itr.tests_skipping.type" + + TAG_CODE_COVERAGE_ENABLED: "test.code_coverage.enabled" + TAG_TEST_SESSION_ID: "_test.session_id" TAG_TEST_MODULE_ID: "_test.module_id" @@ -54,6 +60,8 @@ module Datadog SPAN_KIND_TEST: "test" + ITR_TEST_SKIPPING_MODE: "test" + module Status PASS: "pass" diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index a52918f7..59c9a935 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -26,8 +26,28 @@ module Datadog TEST_VISIBILITY_INTAKE_PATH: "/api/v2/citestcycle" + DD_API_HOST_PREFIX: "api" + + DD_API_SETTINGS_PATH: "/api/v2/ci/libraries/tests/services/setting" + + DD_API_SETTINGS_TYPE: "ci_app_test_service_libraries_settings" + + DD_API_SETTINGS_RESPONSE_DIG_KEYS: Array[String] + + DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY: "itr_enabled" + + DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY: "code_coverage" + + DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY: "tests_skipping" + + DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY: "require_git" + + DD_API_SETTINGS_RESPONSE_DEFAULT: Hash[String, untyped] + CONTENT_TYPE_MESSAGEPACK: "application/msgpack" + CONTENT_TYPE_JSON: "application/json" + CONTENT_ENCODING_GZIP: "gzip" end end diff --git a/sig/datadog/ci/itr/runner.rbs b/sig/datadog/ci/itr/runner.rbs new file mode 100644 index 00000000..1944eeb7 --- /dev/null +++ b/sig/datadog/ci/itr/runner.rbs @@ -0,0 +1,25 @@ +module Datadog + module CI + module ITR + class Runner + @enabled: bool + @test_skipping_enabled: bool + @code_coverage_enabled: bool + + def initialize: (?enabled: bool) -> void + + def configure: (Hash[String, untyped] remote_configuration, Datadog::CI::TestSession test_session) -> void + + def enabled?: () -> bool + + def skipping_tests?: () -> bool + + def code_coverage: () -> bool + + private + + def convert_to_bool: (untyped value) -> bool + end + end + end +end diff --git a/sig/datadog/ci/span.rbs b/sig/datadog/ci/span.rbs index 7dcf3f5d..cbf14fff 100644 --- a/sig/datadog/ci/span.rbs +++ b/sig/datadog/ci/span.rbs @@ -45,6 +45,20 @@ module Datadog def set_parameters: (Hash[String, Object] arguments, ?Hash[String, Object] metadata) -> void + def git_repository_url: () -> String? + + def git_commit_sha: () -> String? + + def git_branch: () -> String? + + def os_architecture: () -> String? + + def os_platform: () -> String? + + def runtime_name: () -> String? + + def runtime_version: () -> String? + private def recorder: () -> Datadog::CI::TestVisibility::Recorder diff --git a/sig/datadog/ci/test_visibility/recorder.rbs b/sig/datadog/ci/test_visibility/recorder.rbs index 490f7302..1f1c96a2 100644 --- a/sig/datadog/ci/test_visibility/recorder.rbs +++ b/sig/datadog/ci/test_visibility/recorder.rbs @@ -7,12 +7,14 @@ module Datadog @environment_tags: Hash[String, String] @local_context: Datadog::CI::TestVisibility::Context::Local @global_context: Datadog::CI::TestVisibility::Context::Global + @itr: Datadog::CI::ITR::Runner + @remote_settings_api: Datadog::CI::Transport::RemoteSettingsApi @codeowners: Datadog::CI::Codeowners::Matcher attr_reader environment_tags: Hash[String, String] attr_reader test_suite_level_visibility_enabled: bool - def initialize: (?test_suite_level_visibility_enabled: bool, ?codeowners: Datadog::CI::Codeowners::Matcher) -> void + def initialize: (?test_suite_level_visibility_enabled: bool, ?codeowners: Datadog::CI::Codeowners::Matcher, itr: Datadog::CI::ITR::Runner, remote_settings_api: Datadog::CI::Transport::RemoteSettingsApi) -> void def trace_test: (String span_name, String test_suite_name, ?service: String?, ?tags: Hash[untyped, untyped]) ?{ (Datadog::CI::Test span) -> untyped } -> untyped @@ -42,8 +44,12 @@ module Datadog def deactivate_test_suite: (String test_suite_name) -> void + def itr_enabled?: () -> bool + private + def configure_library: (Datadog::CI::TestSession 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 diff --git a/sig/datadog/ci/transport/api/agentless.rbs b/sig/datadog/ci/transport/api/agentless.rbs index cbe63b14..04ae60da 100644 --- a/sig/datadog/ci/transport/api/agentless.rbs +++ b/sig/datadog/ci/transport/api/agentless.rbs @@ -6,14 +6,20 @@ module Datadog attr_reader api_key: String @api_key: String + @citestcycle_http: Datadog::CI::Transport::HTTP + @api_http: Datadog::CI::Transport::HTTP - def initialize: (api_key: String, http: Datadog::CI::Transport::HTTP) -> void + def initialize: (api_key: String, citestcycle_url: String, api_url: String) -> void - def request: (path: String, payload: String, ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + def request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator private - def headers: () -> Hash[String, String] + def perform_request: (Datadog::CI::Transport::HTTP client, path: String, payload: String, headers: Hash[String, String], verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + def build_http_client: (String url, compress: bool) -> Datadog::CI::Transport::HTTP + + def default_headers: () -> Hash[String, String] end end end diff --git a/sig/datadog/ci/transport/api/base.rbs b/sig/datadog/ci/transport/api/base.rbs index 6fd14d70..21cc7f49 100644 --- a/sig/datadog/ci/transport/api/base.rbs +++ b/sig/datadog/ci/transport/api/base.rbs @@ -3,17 +3,15 @@ module Datadog module Transport module Api class Base - attr_reader http: Datadog::CI::Transport::HTTP + def api_request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> untyped - @http: Datadog::CI::Transport::HTTP - - def initialize: (http: Datadog::CI::Transport::HTTP) -> void - - def request: (path: String, payload: String, ?verb: ::String) -> untyped + def citestcycle_request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> untyped private - def headers: () -> Hash[String, String] + def headers_with_default: (Hash[String, String] headers) -> Hash[String, String] + + def default_headers: () -> Hash[String, String] end end end diff --git a/sig/datadog/ci/transport/api/evp_proxy.rbs b/sig/datadog/ci/transport/api/evp_proxy.rbs index 7f1a5221..56d68f38 100644 --- a/sig/datadog/ci/transport/api/evp_proxy.rbs +++ b/sig/datadog/ci/transport/api/evp_proxy.rbs @@ -3,18 +3,30 @@ module Datadog module Transport module Api class EvpProxy < Base + @agent_intake_http: Datadog::CI::Transport::HTTP + @agent_api_http: Datadog::CI::Transport::HTTP @container_id: String? @path_prefix: String - def initialize: (http: Datadog::CI::Transport::HTTP, ?path_prefix: String) -> void + def initialize: (agent_settings: Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings, ?path_prefix: String) -> void - def request: (path: String, payload: String, ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + def request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + def citestcycle_request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + def api_request: (path: String, payload: String, ?headers: Hash[String, String], ?verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator private + def perform_request: (Datadog::CI::Transport::HTTP client, path: String, payload: String, headers: Hash[String, String], verb: ::String) -> Datadog::CI::Transport::HTTP::ResponseDecorator + + def build_http_client: (Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings agent_settings, compress: bool) -> Datadog::CI::Transport::HTTP + + def path_with_prefix: (String path) -> String + def container_id: () -> String? - def headers: () -> Hash[String, String] + def default_headers: () -> Hash[String, String] end end end diff --git a/sig/datadog/ci/transport/remote_settings_api.rbs b/sig/datadog/ci/transport/remote_settings_api.rbs new file mode 100644 index 00000000..cbf422a3 --- /dev/null +++ b/sig/datadog/ci/transport/remote_settings_api.rbs @@ -0,0 +1,33 @@ +module Datadog + module CI + module Transport + class RemoteSettingsApi + class Response + @http_response: Datadog::Core::Transport::HTTP::Adapters::Net::Response? + @json: Hash[String, untyped]? + + def initialize: (Datadog::Core::Transport::HTTP::Adapters::Net::Response? http_response) -> void + + def ok?: () -> bool + + def payload: () -> Hash[String, untyped] + + private + + def default_payload: () -> Hash[String, untyped] + end + + @api: Datadog::CI::Transport::Api::Base? + @dd_env: String? + + def initialize: (?api: Datadog::CI::Transport::Api::Base?, ?dd_env: String?) -> void + + def fetch_library_settings: (Datadog::CI::TestSession test_session) -> Response + + private + + def payload: (Datadog::CI::TestSession test_session) -> String + end + end + end +end diff --git a/spec/datadog/ci/configuration/components_spec.rb b/spec/datadog/ci/configuration/components_spec.rb index bf10ac1e..62e00e90 100644 --- a/spec/datadog/ci/configuration/components_spec.rb +++ b/spec/datadog/ci/configuration/components_spec.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/datadog/ci/configuration/components" + RSpec.describe Datadog::CI::Configuration::Components do context "when used to extend Datadog::Core::Configuration::Components" do subject(:components) do @@ -33,30 +37,15 @@ describe "::new" do context "when #ci" do before do - # Stub CI mode behavior - allow(settings.ci) - .to receive(:enabled) - .and_return(enabled) - - allow(settings.ci) - .to receive(:agentless_mode_enabled) - .and_return(agentless_enabled) - - allow(settings.ci) - .to receive(:force_test_level_visibility) - .and_return(force_test_level_visibility) + # Configure CI mode + settings.ci.enabled = enabled + settings.ci.agentless_mode_enabled = agentless_enabled - allow(settings.ci) - .to receive(:agentless_url) - .and_return(agentless_url) - - allow(settings) - .to receive(:site) - .and_return(dd_site) - - allow(settings) - .to receive(:api_key) - .and_return(api_key) + settings.ci.force_test_level_visibility = force_test_level_visibility + settings.ci.agentless_url = agentless_url + settings.ci.itr_enabled = itr_enabled + settings.site = dd_site + settings.api_key = api_key negotiation = double(:negotiation) @@ -74,22 +63,19 @@ # Spy on test mode behavior allow(settings.tracing.test_mode) - .to receive(:enabled=) + .to receive(:enabled=).and_call_original allow(settings.tracing.test_mode) - .to receive(:trace_flush=) + .to receive(:trace_flush=).and_call_original allow(settings.tracing.test_mode) - .to receive(:writer_options=) + .to receive(:writer_options=).and_call_original allow(settings.tracing.test_mode) - .to receive(:async=) - - allow(settings.ci) - .to receive(:enabled=) + .to receive(:async=).and_call_original - allow(settings.ci) - .to receive(:force_test_level_visibility=) + allow(Datadog.logger) + .to receive(:debug) allow(Datadog.logger) .to receive(:warn) @@ -110,6 +96,7 @@ let(:force_test_level_visibility) { false } let(:evp_proxy_v2_supported) { false } let(:evp_proxy_v4_supported) { false } + let(:itr_enabled) { false } context "is enabled" do let(:enabled) { true } @@ -119,6 +106,8 @@ end context "when #force_test_level_visibility" do + let(:evp_proxy_v2_supported) { true } + context "is false" do it "creates a CI recorder with test_suite_level_visibility_enabled=true" do expect(components.ci_recorder).to be_kind_of(Datadog::CI::TestVisibility::Recorder) @@ -173,7 +162,9 @@ end context "and when agent does not support EVP proxy" do - it "falls back to default transport and disables test suite level visibility" do + let(:itr_enabled) { true } + + it "falls back to default transport and disables test suite level visibility and ITR" do expect(settings.tracing.test_mode) .to have_received(:enabled=) .with(true) @@ -182,13 +173,14 @@ .to have_received(:trace_flush=) .with(settings.ci.trace_flush || kind_of(Datadog::CI::TestVisibility::Flush::Partial)) - expect(settings.ci) - .to have_received(:force_test_level_visibility=) - .with(true) + expect(settings.ci.force_test_level_visibility).to eq(true) + expect(settings.ci.itr_enabled).to eq(false) expect(settings.tracing.test_mode).to have_received(:writer_options=) do |options| expect(options[:transport]).to be_nil end + + expect(components.ci_recorder.itr_enabled?).to eq(false) end end end @@ -213,34 +205,53 @@ expect(options[:shutdown_timeout]).to eq(60) end end - end - context "when DD_SITE is set to a wrong value" do - let(:dd_site) { "wrong" } + context "when DD_SITE is set to a wrong value" do + let(:dd_site) { "wrong" } - it "logs a warning" do - expect(Datadog.logger).to have_received(:warn) do |*_args, &block| - expect(block.call).to match( - /CI VISIBILITY CONFIGURATION Agentless mode was enabled but DD_SITE is not set to one of the following/ - ) + it "logs a warning" do + expect(Datadog.logger).to have_received(:warn) do |*_args, &block| + expect(block.call).to match( + /CI VISIBILITY CONFIGURATION Agentless mode was enabled but DD_SITE is not set to one of the following/ + ) + end end end - end - context "when DD_SITE is set to a correct value" do - let(:dd_site) { "datadoghq.eu" } + context "when DD_SITE is set to a correct value" do + let(:dd_site) { "datadoghq.eu" } - it "logs a warning" do - expect(Datadog.logger).not_to have_received(:warn) + it "does not log a warning" do + expect(Datadog.logger).not_to have_received(:warn) + end + end + + context "when ITR is disabled" do + let(:itr_enabled) { false } + + it "creates a CI recorder with ITR disabled" do + expect(components.ci_recorder.itr_enabled?).to eq(false) + end + end + + context "when ITR is enabled" do + let(:itr_enabled) { true } + + it "creates a CI recorder with ITR enabled" do + expect(components.ci_recorder.itr_enabled?).to eq(true) + end end end context "when api key is not set" do let(:api_key) { nil } + let(:itr_enabled) { true } it "logs an error message and disables CI visibility" do expect(Datadog.logger).to have_received(:error) - expect(settings.ci).to have_received(:enabled=).with(false) + + expect(settings.ci.enabled).to eq(false) + expect(components.ci_recorder.itr_enabled?).to eq(false) end end end diff --git a/spec/datadog/ci/configuration/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index 8139da30..730f0aa4 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -217,6 +217,47 @@ def patcher end end + describe "#itr_enabled" do + subject(:itr_enabled) { settings.ci.itr_enabled } + + it { is_expected.to be false } + + context "when #{Datadog::CI::Ext::Settings::ENV_ITR_ENABLED}" do + around do |example| + ClimateControl.modify(Datadog::CI::Ext::Settings::ENV_ITR_ENABLED => enable) do + example.run + end + end + + context "is not defined" do + let(:enable) { nil } + + it { is_expected.to be false } + end + + context "is set to true" do + let(:enable) { "true" } + + it { is_expected.to be true } + end + + context "is set to false" do + let(:enable) { "false" } + + it { is_expected.to be false } + end + end + end + + describe "#itr_enabled=" do + it "updates the #enabled setting" do + expect { settings.ci.itr_enabled = true } + .to change { settings.ci.itr_enabled } + .from(false) + .to(true) + end + end + describe "#instrument" do let(:integration_name) { :fake } diff --git a/spec/datadog/ci/itr/runner_spec.rb b/spec/datadog/ci/itr/runner_spec.rb new file mode 100644 index 00000000..630e8ee9 --- /dev/null +++ b/spec/datadog/ci/itr/runner_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/datadog/ci/itr/runner" + +RSpec.describe Datadog::CI::ITR::Runner do + let(:itr_enabled) { true } + + subject(:runner) { described_class.new(enabled: itr_enabled) } + + describe "#configure" do + let(:tracer_span) { Datadog::Tracing::SpanOperation.new("session") } + let(:test_session) { Datadog::CI::TestSession.new(tracer_span) } + + before do + runner.configure(remote_configuration, test_session) + end + + context "when remote configuration call failed" do + let(:remote_configuration) { {"itr_enabled" => false} } + + it "configures the runner and test session" do + expect(runner.enabled?).to be false + expect(runner.skipping_tests?).to be false + expect(runner.code_coverage?).to be false + end + end + + context "when remote configuration call returned correct response" do + let(:remote_configuration) { {"itr_enabled" => true, "code_coverage" => true, "tests_skipping" => false} } + + it "configures the runner" do + expect(runner.enabled?).to be true + expect(runner.skipping_tests?).to be false + expect(runner.code_coverage?).to be true + end + + it "sets test session tags" do + expect(test_session.skipping_tests?).to be false + expect(test_session.code_coverage?).to be true + expect(test_session.get_tag(Datadog::CI::Ext::Test::TAG_ITR_TEST_SKIPPING_TYPE)).to eq( + Datadog::CI::Ext::Test::ITR_TEST_SKIPPING_MODE + ) + end + end + + context "when remote configuration call returned correct response with strings instead of bools" do + let(:remote_configuration) { {"itr_enabled" => "true", "code_coverage" => "true", "tests_skipping" => "false"} } + + it "configures the runner" do + expect(runner.enabled?).to be true + expect(runner.skipping_tests?).to be false + expect(runner.code_coverage?).to be true + end + end + + context "when remote configuration call returns empty hash" do + let(:remote_configuration) { {} } + + it "configures the runner" do + expect(runner.enabled?).to be false + expect(runner.skipping_tests?).to be false + expect(runner.code_coverage?).to be false + end + end + end +end diff --git a/spec/datadog/ci/span_spec.rb b/spec/datadog/ci/span_spec.rb index 897b779a..b7709d34 100644 --- a/spec/datadog/ci/span_spec.rb +++ b/spec/datadog/ci/span_spec.rb @@ -212,4 +212,60 @@ expect(span.type).to eq("test") end end + + describe "#git_repository_url" do + it "returns the git repository URL" do + expect(tracer_span).to receive(:get_tag).with("git.repository_url").and_return("url") + + expect(span.git_repository_url).to eq("url") + end + end + + describe "#git_commit_sha" do + it "returns the git commit SHA" do + expect(tracer_span).to receive(:get_tag).with("git.commit.sha").and_return("sha") + + expect(span.git_commit_sha).to eq("sha") + end + end + + describe "#git_branch" do + it "returns the git branch" do + expect(tracer_span).to receive(:get_tag).with("git.branch").and_return("branch") + + expect(span.git_branch).to eq("branch") + end + end + + describe "#os_architecture" do + it "returns the OS architecture" do + expect(tracer_span).to receive(:get_tag).with("os.architecture").and_return("arch") + + expect(span.os_architecture).to eq("arch") + end + end + + describe "#os_platform" do + it "returns the OS platform" do + expect(tracer_span).to receive(:get_tag).with("os.platform").and_return("platform") + + expect(span.os_platform).to eq("platform") + end + end + + describe "#runtime_name" do + it "returns the runtime name" do + expect(tracer_span).to receive(:get_tag).with("runtime.name").and_return("name") + + expect(span.runtime_name).to eq("name") + end + end + + describe "#runtime_version" do + it "returns the runtime version" do + expect(tracer_span).to receive(:get_tag).with("runtime.version").and_return("version") + + expect(span.runtime_version).to eq("version") + end + end end diff --git a/spec/datadog/ci/test_session_spec.rb b/spec/datadog/ci/test_session_spec.rb index 52ce638e..c20a18ec 100644 --- a/spec/datadog/ci/test_session_spec.rb +++ b/spec/datadog/ci/test_session_spec.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true RSpec.describe Datadog::CI::TestSession do - let(:tracer_span) { instance_double(Datadog::Tracing::SpanOperation, finish: true) } + let(:tracer_span) { Datadog::Tracing::SpanOperation.new("session") } let(:recorder) { spy("recorder") } before { allow_any_instance_of(described_class).to receive(:recorder).and_return(recorder) } + subject(:ci_test_session) { described_class.new(tracer_span) } describe "#finish" do - subject(:ci_test_session) { described_class.new(tracer_span) } - it "deactivates the test session" do ci_test_session.finish @@ -19,11 +18,9 @@ describe "#inheritable_tags" do subject(:inheritable_tags) { ci_test_session.inheritable_tags } - let(:ci_test_session) { described_class.new(tracer_span) } - before do Datadog::CI::Ext::Test::INHERITABLE_TAGS.each do |tag| - allow(tracer_span).to receive(:get_tag).with(tag).and_return("value for #{tag}") + tracer_span.set_tag(tag, "value for #{tag}") end end @@ -39,12 +36,50 @@ describe "#name" do subject(:name) { ci_test_session.name } - let(:ci_test_session) { described_class.new(tracer_span) } - before do - allow(tracer_span).to receive(:get_tag).with(Datadog::CI::Ext::Test::TAG_COMMAND).and_return("test command") + tracer_span.set_tag(Datadog::CI::Ext::Test::TAG_COMMAND, "test command") end it { is_expected.to eq("test command") } end + + describe "#skipping_tests?" do + subject(:skipping_tests?) { ci_test_session.skipping_tests? } + + context "when not set" do + it { is_expected.to be false } + end + + context "when true" do + before { tracer_span.set_tag(Datadog::CI::Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, true) } + + it { is_expected.to be true } + end + + context "when false" do + before { tracer_span.set_tag(Datadog::CI::Ext::Test::TAG_ITR_TEST_SKIPPING_ENABLED, false) } + + it { is_expected.to be false } + end + end + + describe "#code_coverage?" do + subject(:code_coverage?) { ci_test_session.code_coverage? } + + context "when not set" do + it { is_expected.to be false } + end + + context "when true" do + before { tracer_span.set_tag(Datadog::CI::Ext::Test::TAG_CODE_COVERAGE_ENABLED, true) } + + it { is_expected.to be true } + end + + context "when false" do + before { tracer_span.set_tag(Datadog::CI::Ext::Test::TAG_CODE_COVERAGE_ENABLED, false) } + + it { is_expected.to be false } + end + end end diff --git a/spec/datadog/ci/test_visibility/recorder_spec.rb b/spec/datadog/ci/test_visibility/recorder_spec.rb index 7232d36e..da2d558e 100644 --- a/spec/datadog/ci/test_visibility/recorder_spec.rb +++ b/spec/datadog/ci/test_visibility/recorder_spec.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/datadog/ci/test_visibility/recorder" + RSpec.describe Datadog::CI::TestVisibility::Recorder do shared_examples_for "trace with ciapp-test origin" do let(:trace_under_test) { subject } @@ -108,153 +112,108 @@ end context "when test suite level visibility is enabled" do - include_context "CI mode activated" + context "without ITR" do + include_context "CI mode activated" - describe "#trace" do - let(:type) { "step" } - let(:span_name) { "my test step" } - let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } + describe "#trace" do + let(:type) { "step" } + let(:span_name) { "my test step" } + let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } - context "when given a block" do - before do - recorder.trace(span_name, type: type, tags: tags) do |span| - span.set_metric("my.metric", 42) + context "when given a block" do + before do + recorder.trace(span_name, type: type, tags: tags) do |span| + span.set_metric("my.metric", 42) + end end - end - subject { span } - - it "traces the block" do - expect(subject.resource).to eq(span_name) - expect(subject.type).to eq(type) - end + subject { span } - it "sets the custom metric correctly" do - expect(subject.get_metric("my.metric")).to eq(42) - end - - it "sets the tags correctly" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("my.tag", "my_value") - end - - it_behaves_like "span with environment tags" - it_behaves_like "span with default tags" - it_behaves_like "span with runtime tags" - end - - context "without a block" do - subject { recorder.trace("my test step", type: type, tags: tags) } - - it "returns a new CI span" do - expect(subject).to be_kind_of(Datadog::CI::Span) - end + it "traces the block" do + expect(subject.resource).to eq(span_name) + expect(subject.type).to eq(type) + end - it "sets the tags correctly" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("my.tag", "my_value") - end + it "sets the custom metric correctly" do + expect(subject.get_metric("my.metric")).to eq(42) + end - it "sets correct resource and span type for the underlying tracer span" do - subject.finish + it "sets the tags correctly" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("my.tag", "my_value") + end - expect(span.resource).to eq(span_name) - expect(span.type).to eq(type) + it_behaves_like "span with environment tags" + it_behaves_like "span with default tags" + it_behaves_like "span with runtime tags" end - it_behaves_like "span with environment tags" - it_behaves_like "span with default tags" - it_behaves_like "span with runtime tags" - end - end + context "without a block" do + subject { recorder.trace("my test step", type: type, tags: tags) } - describe "#trace_test" do - let(:test_name) { "my test" } - let(:test_suite_name) { "my suite" } - let(:test_service) { "my-service" } - let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } - - context "without a block" do - subject do - recorder.trace_test( - test_name, - test_suite_name, - service: test_service, - tags: tags - ) - end - - context "when there is no active test session" do - it "returns a new CI test span" do - expect(subject).to be_kind_of(Datadog::CI::Test) - expect(subject.name).to eq(test_name) - expect(subject.service).to eq(test_service) - expect(subject.tracer_span.name).to eq(test_name) - expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST) + it "returns a new CI span" do + expect(subject).to be_kind_of(Datadog::CI::Span) end - it "sets the provided tags correctly" do + it "sets the tags correctly" do expect(subject).to have_test_tag("test.framework", "my-framework") expect(subject).to have_test_tag("my.tag", "my_value") end - it "does not connect the test span to the test session" do - expect(subject).not_to have_test_tag(:test_session_id) - end + it "sets correct resource and span type for the underlying tracer span" do + subject.finish - it "sets the test suite name as one of the tags" do - expect(subject).to have_test_tag(:suite, test_suite_name) - expect(subject).not_to have_test_tag(:test_suite_id) + expect(span.resource).to eq(span_name) + expect(span.type).to eq(type) end it_behaves_like "span with environment tags" it_behaves_like "span with default tags" it_behaves_like "span with runtime tags" - it_behaves_like "trace with ciapp-test origin" do - let(:trace_under_test) do - subject.finish - - trace - end - end end + end - context "when there is an active test session" do - let(:test_session_tags) { {"test.framework_version" => "1.0", "my.session.tag" => "my_session_value"} } - let(:session_service) { "my-session-service" } - let(:test_service) { nil } - - let(:test_session) { recorder.start_test_session(service: session_service, tags: test_session_tags) } + describe "#trace_test" do + let(:test_name) { "my test" } + let(:test_suite_name) { "my suite" } + let(:test_service) { "my-service" } + let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } - before do - test_session + context "without a block" do + subject do + recorder.trace_test( + test_name, + test_suite_name, + service: test_service, + tags: tags + ) end - context "when there is no active test module" do - it "returns a new CI test span using service from the test session" do + context "when there is no active test session" do + it "returns a new CI test span" do expect(subject).to be_kind_of(Datadog::CI::Test) expect(subject.name).to eq(test_name) - expect(subject.service).to eq(session_service) + expect(subject.service).to eq(test_service) + expect(subject.tracer_span.name).to eq(test_name) + expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST) end - it "sets the provided tags correctly while inheriting some tags from the session" do + it "sets the provided tags correctly" do expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("test.framework_version", "1.0") expect(subject).to have_test_tag("my.tag", "my_value") - expect(subject).not_to have_test_tag("my.session.tag") end - it "connects the test span to the test session" do - expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) + it "does not connect the test span to the test session" do + expect(subject).not_to have_test_tag(:test_session_id) end - it "starts a new trace" do - expect(subject.tracer_span.trace_id).not_to eq(test_session.tracer_span.trace_id) + it "sets the test suite name as one of the tags" do + expect(subject).to have_test_tag(:suite, test_suite_name) + expect(subject).not_to have_test_tag(:test_suite_id) end it_behaves_like "span with environment tags" it_behaves_like "span with default tags" it_behaves_like "span with runtime tags" - it_behaves_like "trace with ciapp-test origin" do let(:trace_under_test) do subject.finish @@ -264,69 +223,152 @@ end end - context "when there is an active test module" do - let(:module_name) { "my-module" } + context "when there is an active test session" do + let(:test_session_tags) { {"test.framework_version" => "1.0", "my.session.tag" => "my_session_value"} } + let(:session_service) { "my-session-service" } + let(:test_service) { nil } - let(:test_module) do - recorder.start_test_module(module_name) - end + let(:test_session) { recorder.start_test_session(service: session_service, tags: test_session_tags) } before do - test_module + test_session end - it "returns a new CI test span" do - expect(subject).to be_kind_of(Datadog::CI::Test) - expect(subject.name).to eq(test_name) - end + context "when there is no active test module" do + it "returns a new CI test span using service from the test session" do + expect(subject).to be_kind_of(Datadog::CI::Test) + expect(subject.name).to eq(test_name) + expect(subject.service).to eq(session_service) + end - it "sets the provided tags correctly while inheriting some tags from the session" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("test.framework_version", "1.0") - expect(subject).to have_test_tag("my.tag", "my_value") - end + it "sets the provided tags correctly while inheriting some tags from the session" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("test.framework_version", "1.0") + expect(subject).to have_test_tag("my.tag", "my_value") + expect(subject).not_to have_test_tag("my.session.tag") + end - it "connects the test span to the test module" do - expect(subject).to have_test_tag(:test_module_id, test_module.id.to_s) - expect(subject).to have_test_tag(:module, module_name) + it "connects the test span to the test session" do + expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) + end + + it "starts a new trace" do + expect(subject.tracer_span.trace_id).not_to eq(test_session.tracer_span.trace_id) + end + + it_behaves_like "span with environment tags" + it_behaves_like "span with default tags" + it_behaves_like "span with runtime tags" + + it_behaves_like "trace with ciapp-test origin" do + let(:trace_under_test) do + subject.finish + + trace + end + end end - context "when there is an active test suite" do - let(:test_suite) do - recorder.start_test_suite(test_suite_name) + context "when there is an active test module" do + let(:module_name) { "my-module" } + + let(:test_module) do + recorder.start_test_module(module_name) end before do - test_suite + test_module + end + + it "returns a new CI test span" do + expect(subject).to be_kind_of(Datadog::CI::Test) + expect(subject.name).to eq(test_name) + end + + it "sets the provided tags correctly while inheriting some tags from the session" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("test.framework_version", "1.0") + expect(subject).to have_test_tag("my.tag", "my_value") + end + + it "connects the test span to the test module" do + expect(subject).to have_test_tag(:test_module_id, test_module.id.to_s) + expect(subject).to have_test_tag(:module, module_name) end - it "connects the test span to the test suite" do - expect(subject).to have_test_tag(:test_suite_id, test_suite.id.to_s) - expect(subject).to have_test_tag(:suite, test_suite_name) + context "when there is an active test suite" do + let(:test_suite) do + recorder.start_test_suite(test_suite_name) + end + + before do + test_suite + end + + it "connects the test span to the test suite" do + expect(subject).to have_test_tag(:test_suite_id, test_suite.id.to_s) + expect(subject).to have_test_tag(:suite, test_suite_name) + end end end end end - end - context "when given a block" do - before do - recorder.trace_test( - test_name, - test_suite_name, - service: test_service, - tags: tags - ) do |test_span| - test_span.set_metric("my.metric", 42) + context "when given a block" do + before do + recorder.trace_test( + test_name, + test_suite_name, + service: test_service, + tags: tags + ) do |test_span| + test_span.set_metric("my.metric", 42) + end + end + subject { span } + + it "traces and finishes a test" do + expect(subject).to have_test_tag(:name, test_name) + expect(subject.service).to eq(test_service) + expect(subject.name).to eq(test_name) + expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST) + end + + it "sets the provided tags correctly" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("my.tag", "my_value") + end + + it "sets the suite name in tags" do + expect(subject).to have_test_tag(:suite, test_suite_name) + end + + it_behaves_like "span with environment tags" + it_behaves_like "span with default tags" + it_behaves_like "span with runtime tags" + it_behaves_like "trace with ciapp-test origin" do + let(:trace_under_test) do + trace + end end end - subject { span } + end + + describe "#start_test_session" do + let(:service) { "my-service" } + let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } + + subject { recorder.start_test_session(service: service, tags: tags) } - it "traces and finishes a test" do - expect(subject).to have_test_tag(:name, test_name) - expect(subject.service).to eq(test_service) - expect(subject.name).to eq(test_name) - expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST) + it "returns a new CI test_session span" do + expect(subject).to be_kind_of(Datadog::CI::TestSession) + expect(subject.name).to eq(test_command) + expect(subject.service).to eq(service) + expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_SESSION) + end + + it "sets the test session id" do + expect(subject).to have_test_tag(:test_session_id, subject.id.to_s) end it "sets the provided tags correctly" do @@ -334,351 +376,355 @@ expect(subject).to have_test_tag("my.tag", "my_value") end - it "sets the suite name in tags" do - expect(subject).to have_test_tag(:suite, test_suite_name) - end - it_behaves_like "span with environment tags" it_behaves_like "span with default tags" it_behaves_like "span with runtime tags" it_behaves_like "trace with ciapp-test origin" do let(:trace_under_test) do + subject.finish + trace end end end - end - - describe "#start_test_session" do - let(:service) { "my-service" } - let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } - subject { recorder.start_test_session(service: service, tags: tags) } + describe "#start_test_module" do + let(:module_name) { "my-module" } + let(:service) { "my-service" } + let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } - it "returns a new CI test_session span" do - expect(subject).to be_kind_of(Datadog::CI::TestSession) - expect(subject.name).to eq(test_command) - expect(subject.service).to eq(service) - expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_SESSION) - end + subject { recorder.start_test_module(module_name, service: service, tags: tags) } - it "sets the test session id" do - expect(subject).to have_test_tag(:test_session_id, subject.id.to_s) - end + context "when there is no active test session" do + it "returns a new CI test_module span" do + expect(subject).to be_kind_of(Datadog::CI::TestModule) + expect(subject.name).to eq(module_name) + expect(subject.service).to eq(service) + expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_MODULE) + end - it "sets the provided tags correctly" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("my.tag", "my_value") - end + it "sets the test module id" do + expect(subject).to have_test_tag(:test_module_id, subject.id.to_s) + end - it_behaves_like "span with environment tags" - it_behaves_like "span with default tags" - it_behaves_like "span with runtime tags" - it_behaves_like "trace with ciapp-test origin" do - let(:trace_under_test) do - subject.finish + it "sets the test module tag" do + expect(subject).to have_test_tag(:module, module_name) + end - trace - end - end - end + it "doesn't connect the test module span to the test session" do + expect(subject).not_to have_test_tag(:test_session_id) + end - describe "#start_test_module" do - let(:module_name) { "my-module" } - let(:service) { "my-service" } - let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } + it "sets the provided tags correctly" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("my.tag", "my_value") + end - subject { recorder.start_test_module(module_name, service: service, tags: tags) } + it_behaves_like "span with environment tags" + it_behaves_like "span with default tags" + it_behaves_like "span with runtime tags" + it_behaves_like "trace with ciapp-test origin" do + let(:trace_under_test) do + subject.finish - context "when there is no active test session" do - it "returns a new CI test_module span" do - expect(subject).to be_kind_of(Datadog::CI::TestModule) - expect(subject.name).to eq(module_name) - expect(subject.service).to eq(service) - expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_MODULE) + trace + end + end end - it "sets the test module id" do - expect(subject).to have_test_tag(:test_module_id, subject.id.to_s) - end + context "when there is an active test session" do + let(:service) { nil } + let(:session_service) { "session_service" } + let(:session_tags) { {"test.framework_version" => "1.0", "my.session.tag" => "my_session_value"} } + let(:test_session) { recorder.start_test_session(service: session_service, tags: session_tags) } - it "sets the test module tag" do - expect(subject).to have_test_tag(:module, module_name) - end + before do + test_session + end - it "doesn't connect the test module span to the test session" do - expect(subject).not_to have_test_tag(:test_session_id) - end + it "returns a new CI module span using service from the test session" do + expect(subject).to be_kind_of(Datadog::CI::TestModule) + expect(subject.name).to eq(module_name) + expect(subject.service).to eq(session_service) + end - it "sets the provided tags correctly" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("my.tag", "my_value") - end + it "sets the provided tags correctly while inheriting some tags from the session" do + expect(subject).to have_test_tag("test.framework", "my-framework") + expect(subject).to have_test_tag("test.framework_version", "1.0") + expect(subject).to have_test_tag("my.tag", "my_value") + expect(subject).not_to have_test_tag("my.session.tag") + end - it_behaves_like "span with environment tags" - it_behaves_like "span with default tags" - it_behaves_like "span with runtime tags" - it_behaves_like "trace with ciapp-test origin" do - let(:trace_under_test) do - subject.finish + it "connects the test module span to the test session" do + expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) + end - trace + it "does not start a new trace" do + expect(subject.tracer_span.trace_id).to eq(test_session.tracer_span.trace_id) end end end - context "when there is an active test session" do - let(:service) { nil } + describe "start_test_suite" do + let(:module_name) { "my-module" } let(:session_service) { "session_service" } let(:session_tags) { {"test.framework_version" => "1.0", "my.session.tag" => "my_session_value"} } + let(:test_session) { recorder.start_test_session(service: session_service, tags: session_tags) } + let(:test_module) { recorder.start_test_module(module_name) } before do test_session + test_module end - it "returns a new CI module span using service from the test session" do - expect(subject).to be_kind_of(Datadog::CI::TestModule) - expect(subject.name).to eq(module_name) - expect(subject.service).to eq(session_service) - end + context "when test suite with given name is not started yet" do + let(:suite_name) { "my-suite" } + let(:tags) { {"my.tag" => "my_value"} } - it "sets the provided tags correctly while inheriting some tags from the session" do - expect(subject).to have_test_tag("test.framework", "my-framework") - expect(subject).to have_test_tag("test.framework_version", "1.0") - expect(subject).to have_test_tag("my.tag", "my_value") - expect(subject).not_to have_test_tag("my.session.tag") - end + subject { recorder.start_test_suite(suite_name, tags: tags) } - it "connects the test module span to the test session" do - expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) - end + it "returns a new CI test_suite span" do + expect(subject).to be_kind_of(Datadog::CI::TestSuite) + expect(subject.name).to eq(suite_name) + expect(subject.service).to eq(session_service) + expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_SUITE) + end - it "does not start a new trace" do - expect(subject.tracer_span.trace_id).to eq(test_session.tracer_span.trace_id) - end - end - end + it "sets the provided tags correctly while inheriting some tags from the session" do + expect(subject).to have_test_tag("test.framework_version", "1.0") + expect(subject).to have_test_tag("my.tag", "my_value") + expect(subject).not_to have_test_tag("my.session.tag") + end - describe "start_test_suite" do - let(:module_name) { "my-module" } - let(:session_service) { "session_service" } - let(:session_tags) { {"test.framework_version" => "1.0", "my.session.tag" => "my_session_value"} } + it "sets the test suite context" do + expect(subject).to have_test_tag(:test_suite_id, subject.id.to_s) + expect(subject).to have_test_tag(:suite, suite_name) + end - let(:test_session) { recorder.start_test_session(service: session_service, tags: session_tags) } - let(:test_module) { recorder.start_test_module(module_name) } + it "sets test session and test module contexts" do + expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) + expect(subject).to have_test_tag(:test_module_id, test_module.id.to_s) + expect(subject).to have_test_tag(:module, module_name) + end - before do - test_session - test_module - end + it_behaves_like "span with environment tags" + it_behaves_like "span with default tags" + it_behaves_like "span with runtime tags" + end - context "when test suite with given name is not started yet" do - let(:suite_name) { "my-suite" } - let(:tags) { {"my.tag" => "my_value"} } + context "when test suite with given name is already started" do + let(:suite_name) { "my-suite" } + let(:tags) { {"my.tag" => "my_value"} } + let(:already_running_test_suite) { recorder.start_test_suite(suite_name, tags: tags) } - subject { recorder.start_test_suite(suite_name, tags: tags) } + before do + already_running_test_suite + end - it "returns a new CI test_suite span" do - expect(subject).to be_kind_of(Datadog::CI::TestSuite) - expect(subject.name).to eq(suite_name) - expect(subject.service).to eq(session_service) - expect(subject.type).to eq(Datadog::CI::Ext::AppTypes::TYPE_TEST_SUITE) - end + subject { recorder.start_test_suite(suite_name) } - it "sets the provided tags correctly while inheriting some tags from the session" do - expect(subject).to have_test_tag("test.framework_version", "1.0") - expect(subject).to have_test_tag("my.tag", "my_value") - expect(subject).not_to have_test_tag("my.session.tag") + it "returns the already running test suite" do + expect(subject.id).to eq(already_running_test_suite.id) + expect(subject).to have_test_tag("my.tag", "my_value") + end end + end - it "sets the test suite context" do - expect(subject).to have_test_tag(:test_suite_id, subject.id.to_s) - expect(subject).to have_test_tag(:suite, suite_name) + describe "#active_test_session" do + subject { recorder.active_test_session } + context "when there is no active test session" do + it { is_expected.to be_nil } end - it "sets test session and test module contexts" do - expect(subject).to have_test_tag(:test_session_id, test_session.id.to_s) - expect(subject).to have_test_tag(:test_module_id, test_module.id.to_s) - expect(subject).to have_test_tag(:module, module_name) - end + context "when test session is started" do + let(:test_session) { recorder.start_test_session } + before do + test_session + end - it_behaves_like "span with environment tags" - it_behaves_like "span with default tags" - it_behaves_like "span with runtime tags" + it "returns the active test session" do + expect(subject).to be(test_session) + end + end end - context "when test suite with given name is already started" do - let(:suite_name) { "my-suite" } - let(:tags) { {"my.tag" => "my_value"} } - let(:already_running_test_suite) { recorder.start_test_suite(suite_name, tags: tags) } - - before do - already_running_test_suite + describe "#active_test_module" do + subject { recorder.active_test_module } + context "when there is no active test module" do + it { is_expected.to be_nil } end - subject { recorder.start_test_suite(suite_name) } + context "when test module is started" do + let(:test_module) { recorder.start_test_module("my module") } + before do + test_module + end - it "returns the already running test suite" do - expect(subject.id).to eq(already_running_test_suite.id) - expect(subject).to have_test_tag("my.tag", "my_value") + it "returns the active test module" do + expect(subject).to be(test_module) + end end end - end - - describe "#active_test_session" do - subject { recorder.active_test_session } - context "when there is no active test session" do - it { is_expected.to be_nil } - end - context "when test session is started" do - let(:test_session) { recorder.start_test_session } - before do - test_session - end + describe "#active_test" do + subject { recorder.active_test } - it "returns the active test session" do - expect(subject).to be(test_session) + context "when there is no active test" do + it { is_expected.to be_nil } end - end - end - describe "#active_test_module" do - subject { recorder.active_test_module } - context "when there is no active test module" do - it { is_expected.to be_nil } - end + context "when test is started" do + let(:ci_test) { recorder.trace_test("my test", "my suite") } - context "when test module is started" do - let(:test_module) { recorder.start_test_module("my module") } - before do - test_module - end + before do + ci_test + end - it "returns the active test module" do - expect(subject).to be(test_module) + it "returns the active test" do + expect(subject).to be(ci_test) + end end end - end - describe "#active_test" do - subject { recorder.active_test } + describe "#active_span" do + subject { recorder.active_span } - context "when there is no active test" do - it { is_expected.to be_nil } - end + context "when there is no active span" do + it { is_expected.to be_nil } + end - context "when test is started" do - let(:ci_test) { recorder.trace_test("my test", "my suite") } + context "when span is started" do + let(:ci_span) { recorder.trace("my test step", type: "step") } - before do - ci_test - end + before do + ci_span + end - it "returns the active test" do - expect(subject).to be(ci_test) + it "returns a wrapper around the active tracer span" do + expect(subject).to be_kind_of(Datadog::CI::Span) + expect(subject.tracer_span.name).to eq("my test step") + end end end - end - - describe "#active_span" do - subject { recorder.active_span } - context "when there is no active span" do - it { is_expected.to be_nil } - end + describe "#deactivate_test" do + subject { recorder.deactivate_test } - context "when span is started" do - let(:ci_span) { recorder.trace("my test step", type: "step") } + context "when there is no active test" do + let(:ci_test) { Datadog::CI::Test.new(double("tracer span")) } - before do - ci_span + it { is_expected.to be_nil } end - it "returns a wrapper around the active tracer span" do - expect(subject).to be_kind_of(Datadog::CI::Span) - expect(subject.tracer_span.name).to eq("my test step") + context "when deactivating the currently active test" do + let(:ci_test) { recorder.trace_test("my test", "my suite") } + + it "deactivates the test" do + subject + + expect(recorder.active_test).to be_nil + end end end - end - describe "#deactivate_test" do - subject { recorder.deactivate_test } + describe "#deactivate_test_session" do + subject { recorder.deactivate_test_session } - context "when there is no active test" do - let(:ci_test) { Datadog::CI::Test.new(double("tracer span")) } - - it { is_expected.to be_nil } - end + context "when there is no active test session" do + it { is_expected.to be_nil } + end - context "when deactivating the currently active test" do - let(:ci_test) { recorder.trace_test("my test", "my suite") } + context "when deactivating the currently active test session" do + before do + recorder.start_test_session + end - it "deactivates the test" do - subject + it "deactivates the test session" do + subject - expect(recorder.active_test).to be_nil + expect(recorder.active_test_session).to be_nil + end end end - end - - describe "#deactivate_test_session" do - subject { recorder.deactivate_test_session } - context "when there is no active test session" do - it { is_expected.to be_nil } - end + describe "#deactivate_test_module" do + subject { recorder.deactivate_test_module } - context "when deactivating the currently active test session" do - before do - recorder.start_test_session + context "when there is no active test module" do + it { is_expected.to be_nil } end - it "deactivates the test session" do - subject + context "when deactivating the currently active test module" do + before do + recorder.start_test_module("my module") + end + + it "deactivates the test module" do + subject - expect(recorder.active_test_session).to be_nil + expect(recorder.active_test_module).to be_nil + end end end - end - describe "#deactivate_test_module" do - subject { recorder.deactivate_test_module } + describe "#deactivate_test_suite" do + subject { recorder.deactivate_test_suite("my suite") } - context "when there is no active test module" do - it { is_expected.to be_nil } - end - - context "when deactivating the currently active test module" do - before do - recorder.start_test_module("my module") + context "when there is no active test suite" do + it { is_expected.to be_nil } end - it "deactivates the test module" do - subject + context "when deactivating the currently active test suite" do + before do + recorder.start_test_suite("my suite") + end + + it "deactivates the test suite" do + subject - expect(recorder.active_test_module).to be_nil + expect(recorder.active_test_suite("my suite")).to be_nil + end end end end - describe "#deactivate_test_suite" do - subject { recorder.deactivate_test_suite("my suite") } - - context "when there is no active test suite" do - it { is_expected.to be_nil } + context "with ITR" do + include_context "CI mode activated" do + let(:itr_enabled) { true } end - context "when deactivating the currently active test suite" do - before do - recorder.start_test_suite("my suite") - end - - it "deactivates the test suite" do - subject + before do + settings_http_response = double( + "http-response", + ok?: true, + payload: { + "data" => { + "attributes" => { + "itr_enabled" => true, + "tests_skipping" => true, + "code_coverage" => true + } + } + }.to_json + ) + allow_any_instance_of(Datadog::CI::Transport::RemoteSettingsApi).to receive(:fetch_library_settings).and_return( + Datadog::CI::Transport::RemoteSettingsApi::Response.new(settings_http_response) + ) + end + + describe "#start_test_session" do + let(:service) { "my-service" } + let(:tags) { {"test.framework" => "my-framework", "my.tag" => "my_value"} } + + subject { recorder.start_test_session(service: service, tags: tags) } + + it "returns a new CI test_session span with ITR tags" do + expect(subject).to be_kind_of(Datadog::CI::TestSession) + expect(subject.service).to eq(service) - expect(recorder.active_test_suite("my suite")).to be_nil + expect(subject.skipping_tests?).to be true + expect(subject.code_coverage?).to be true end end end diff --git a/spec/datadog/ci/test_visibility/transport_spec.rb b/spec/datadog/ci/test_visibility/transport_spec.rb index ebf0f2f0..e3fe6b5c 100644 --- a/spec/datadog/ci/test_visibility/transport_spec.rb +++ b/spec/datadog/ci/test_visibility/transport_spec.rb @@ -33,7 +33,7 @@ it "sends correct payload" do subject.send_traces([trace]) - expect(api).to have_received(:request) do |args| + expect(api).to have_received(:citestcycle_request) do |args| expect(args[:path]).to eq("/api/v2/citestcycle") payload = MessagePack.unpack(args[:payload]) @@ -59,7 +59,7 @@ it "sends correct payload including env" do subject.send_traces([trace]) - expect(api).to have_received(:request) do |args| + expect(api).to have_received(:citestcycle_request) do |args| payload = MessagePack.unpack(args[:payload]) metadata = payload["metadata"]["*"] @@ -79,7 +79,7 @@ it "sends event for each of spans" do subject.send_traces(traces) - expect(api).to have_received(:request) do |args| + expect(api).to have_received(:citestcycle_request) do |args| payload = MessagePack.unpack(args[:payload]) events = payload["events"] expect(events.count).to eq(expected_events_count) @@ -97,7 +97,7 @@ it "filters out invalid events" do subject.send_traces(traces) - expect(api).to have_received(:request) do |args| + expect(api).to have_received(:citestcycle_request) do |args| payload = MessagePack.unpack(args[:payload]) events = payload["events"] @@ -127,7 +127,7 @@ it "filters out invalid events" do responses = subject.send_traces(traces) - expect(api).to have_received(:request).twice + expect(api).to have_received(:citestcycle_request).twice expect(responses.count).to eq(2) end end @@ -140,7 +140,7 @@ it "does not send events that are larger than max size" do subject.send_traces(traces) - expect(api).not_to have_received(:request) + expect(api).not_to have_received(:citestcycle_request) end end end @@ -155,7 +155,7 @@ it "does not send anything" do subject.send_traces(traces) - expect(api).not_to have_received(:request) + expect(api).not_to have_received(:citestcycle_request) end end end diff --git a/spec/datadog/ci/transport/api/agentless_spec.rb b/spec/datadog/ci/transport/api/agentless_spec.rb index cfeb413c..77ca6884 100644 --- a/spec/datadog/ci/transport/api/agentless_spec.rb +++ b/spec/datadog/ci/transport/api/agentless_spec.rb @@ -4,30 +4,139 @@ subject do described_class.new( api_key: api_key, - http: http + citestcycle_url: citestcycle_url, + api_url: api_url ) end let(:api_key) { "api_key" } - let(:http) { double(:http) } - describe "#request" do + context "malformed urls" do + let(:citestcycle_url) { "" } + let(:api_url) { "api.datadoghq.com" } + + it { expect { subject }.to raise_error(/Invalid agentless mode URL:/) } + end + + context "http urls" do + let(:citestcycle_url) { "http://localhost:5555" } + let(:citestcycle_http) { double(:http) } + + let(:api_url) { "http://localhost:5555" } + let(:api_http) { double(:http) } + + before do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "localhost", + port: 5555, + ssl: false, + compress: true + ).and_return(citestcycle_http) + + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "localhost", + port: 5555, + ssl: false, + compress: false + ).and_return(api_http) + end + + describe "#citestcycle_request" do + let(:expected_headers) do + { + "DD-API-KEY" => "api_key", + "Content-Type" => "application/msgpack" + } + end + + it "produces correct headers and forwards request to HTTP layer" do + expect(citestcycle_http).to receive(:request).with( + path: "path", + payload: "payload", + verb: "post", + headers: expected_headers + ) + + subject.citestcycle_request(path: "path", payload: "payload") + end + end + end + + context "valid urls" do + let(:citestcycle_url) { "https://citestcycle-intake.datadoghq.com:443" } + let(:citestcycle_http) { double(:http) } + + let(:api_url) { "https://api.datadoghq.com:443" } + let(:api_http) { double(:http) } + before do - allow(Datadog::CI::Transport::HTTP).to receive(:new).and_return(http) + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "citestcycle-intake.datadoghq.com", + port: 443, + ssl: true, + compress: true + ).and_return(citestcycle_http) + + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: "api.datadoghq.com", + port: 443, + ssl: true, + compress: false + ).and_return(api_http) end - it "produces correct headers and forwards request to HTTP layer" do - expect(http).to receive(:request).with( - path: "path", - payload: "payload", - verb: "post", - headers: { + describe "#citestcycle_request" do + let(:expected_headers) do + { "DD-API-KEY" => "api_key", "Content-Type" => "application/msgpack" } - ) + end + + it "produces correct headers and forwards request to HTTP layer" do + expect(citestcycle_http).to receive(:request).with( + path: "path", + payload: "payload", + verb: "post", + headers: expected_headers + ) + + subject.citestcycle_request(path: "path", payload: "payload") + end + + it "alows to override headers" do + expect(citestcycle_http).to receive(:request).with( + path: "path", + payload: "payload", + verb: "post", + headers: expected_headers.merge({"Content-Type" => "application/json"}) + ) + + subject.citestcycle_request(path: "path", payload: "payload", headers: {"Content-Type" => "application/json"}) + end + end + + describe "#api_request" do + let(:expected_headers) do + { + "DD-API-KEY" => "api_key", + "Content-Type" => "application/msgpack" + } + end + + it "produces correct headers and forwards request to HTTP layer" do + expect(api_http).to receive(:request).with( + path: "path", + payload: "payload", + verb: "post", + headers: { + "DD-API-KEY" => "api_key", + "Content-Type" => "application/json" + } + ) - subject.request(path: "path", payload: "payload") + subject.api_request(path: "path", payload: "payload") + end end end end diff --git a/spec/datadog/ci/transport/api/builder_spec.rb b/spec/datadog/ci/transport/api/builder_spec.rb index 8fdb6c98..e966107b 100644 --- a/spec/datadog/ci/transport/api/builder_spec.rb +++ b/spec/datadog/ci/transport/api/builder_spec.rb @@ -16,7 +16,6 @@ subject { described_class.build_agentless_api(settings) } let(:api) { double(:api) } - let(:http) { double(:http) } let(:agentless_url) { nil } let(:dd_site) { nil } let(:api_key) { "api_key" } @@ -37,15 +36,8 @@ end it "creates and configures http client and Agentless api" do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "citestcycle-intake.datadoghq.com", - port: 443, - ssl: true, - compress: true - ).and_return(http) - expect(Datadog::CI::Transport::Api::Agentless).to receive(:new).with( - api_key: "api_key", http: http + api_key: "api_key", citestcycle_url: "https://citestcycle-intake.datadoghq.com:443", api_url: "https://api.datadoghq.com:443" ).and_return(api) expect(subject).to eq(api) @@ -55,15 +47,8 @@ let(:agentless_url) { "http://localhost:5555" } it "configures transport to use intake URL from settings" do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "localhost", - port: 5555, - ssl: false, - compress: true - ).and_return(http) - expect(Datadog::CI::Transport::Api::Agentless).to receive(:new).with( - api_key: "api_key", http: http + api_key: "api_key", citestcycle_url: "http://localhost:5555", api_url: "http://localhost:5555" ).and_return(api) expect(subject).to eq(api) @@ -74,15 +59,8 @@ let(:dd_site) { "datadoghq.eu" } it "construct intake url using provided host" do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "citestcycle-intake.datadoghq.eu", - port: 443, - ssl: true, - compress: true - ).and_return(http) - expect(Datadog::CI::Transport::Api::Agentless).to receive(:new).with( - api_key: "api_key", http: http + api_key: "api_key", citestcycle_url: "https://citestcycle-intake.datadoghq.eu:443", api_url: "https://api.datadoghq.eu:443" ).and_return(api) expect(subject).to eq(api) @@ -94,7 +72,6 @@ subject { described_class.build_evp_proxy_api(settings) } let(:api) { double(:api) } - let(:http) { double(:http) } let(:agent_settings) do Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings.new( @@ -133,16 +110,8 @@ end it "creates and configures http client and EvpProxy" do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "localhost", - port: 5555, - ssl: false, - timeout: 42, - compress: false - ).and_return(http) - expect(Datadog::CI::Transport::Api::EvpProxy).to( - receive(:new).with(http: http, path_prefix: "/evp_proxy/v2/").and_return(api) + receive(:new).with(agent_settings: agent_settings, path_prefix: "/evp_proxy/v2/").and_return(api) ) expect(subject).to eq(api) @@ -157,16 +126,8 @@ end it "creates and configures http client and EvpProxy" do - expect(Datadog::CI::Transport::HTTP).to receive(:new).with( - host: "localhost", - port: 5555, - ssl: false, - timeout: 42, - compress: true - ).and_return(http) - expect(Datadog::CI::Transport::Api::EvpProxy).to( - receive(:new).with(http: http, path_prefix: "/evp_proxy/v4/").and_return(api) + receive(:new).with(agent_settings: agent_settings, path_prefix: "/evp_proxy/v4/").and_return(api) ) expect(subject).to eq(api) diff --git a/spec/datadog/ci/transport/api/evp_proxy_spec.rb b/spec/datadog/ci/transport/api/evp_proxy_spec.rb index 94d0571f..50a1c521 100644 --- a/spec/datadog/ci/transport/api/evp_proxy_spec.rb +++ b/spec/datadog/ci/transport/api/evp_proxy_spec.rb @@ -2,86 +2,199 @@ RSpec.describe Datadog::CI::Transport::Api::EvpProxy do subject do - described_class.new(http: http, path_prefix: path_prefix) + described_class.new(agent_settings: agent_settings, path_prefix: path_prefix) end - let(:http) { double(:http) } - let(:path_prefix) { "/evp_proxy/v2/" } + let(:agent_settings) do + Datadog::Core::Configuration::AgentSettingsResolver::AgentSettings.new( + adapter: :net_http, + ssl: false, + hostname: "localhost", + port: 5555, + uds_path: nil, + timeout_seconds: 42, + deprecated_for_removal_transport_configuration_proc: nil + ) + end + let(:intake_http) { double(:http) } + let(:api_http) { double(:http) } + + let(:container_id) { nil } + before do + expect(Datadog::Core::Environment::Container).to receive(:container_id).and_return(container_id) + end + + let(:citestcycle_headers) do + { + "Content-Type" => "application/msgpack", + "X-Datadog-EVP-Subdomain" => "citestcycle-intake" + } + end - describe "#request" do - let(:container_id) { nil } + let(:api_headers) do + { + "Content-Type" => "application/json", + "X-Datadog-EVP-Subdomain" => "api" + } + end + + context "with evp proxy v2" do + let(:path_prefix) { Datadog::CI::Ext::Transport::EVP_PROXY_V2_PATH_PREFIX } before do - allow(Datadog::CI::Transport::HTTP).to receive(:new).and_return(http) - expect(Datadog::Core::Environment::Container).to receive(:container_id).and_return(container_id) + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: agent_settings.hostname, + port: agent_settings.port, + ssl: agent_settings.ssl, + timeout: agent_settings.timeout_seconds, + compress: false + ).and_return(intake_http, api_http) end - context "with path starting from / character" do - it "produces correct headers and forwards request to HTTP layer prepending path with evp_proxy" do - expect(http).to receive(:request).with( - path: "/evp_proxy/v2/path", - payload: "payload", - verb: "post", - headers: { - "Content-Type" => "application/msgpack", - "X-Datadog-EVP-Subdomain" => "citestcycle-intake" - } - ) + describe "#citestcycle_request" do + context "with path starting from / character" do + it "produces correct headers and forwards request to HTTP layer prepending path with evp_proxy" do + expect(intake_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: citestcycle_headers + ) - subject.request(path: "/path", payload: "payload") + subject.citestcycle_request(path: "/path", payload: "payload") + end + end + + context "with path without / in the beginning" do + it "constructs evp proxy path correctly" do + expect(intake_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: citestcycle_headers + ) + + subject.citestcycle_request(path: "path", payload: "payload") + end + end + + context "with container id" do + let(:container_id) { "container-id" } + + it "adds an additional Datadog-Container-ID header" do + expect(intake_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: citestcycle_headers.merge("Datadog-Container-ID" => "container-id") + ) + + subject.citestcycle_request(path: "/path", payload: "payload") + end + end + + context "overriding content-type" do + it "uses content type header from the request parameter" do + expect(intake_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: citestcycle_headers.merge({"Content-Type" => "application/json"}) + ) + + subject.citestcycle_request(path: "/path", payload: "payload", headers: {"Content-Type" => "application/json"}) + end end end - context "with path without / in the beginning" do - it "constructs evp proxy path correctly" do - expect(http).to receive(:request).with( - path: "/evp_proxy/v2/path", - payload: "payload", - verb: "post", - headers: { - "Content-Type" => "application/msgpack", - "X-Datadog-EVP-Subdomain" => "citestcycle-intake" - } - ) + describe "#api_request" do + context "with path starting from / character" do + it "produces correct headers and forwards request to HTTP layer prepending path with evp_proxy" do + expect(api_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: api_headers + ) + + subject.api_request(path: "/path", payload: "payload") + end + end + + context "with path without / in the beginning" do + it "constructs evp proxy path correctly" do + expect(api_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: api_headers + ) + + subject.api_request(path: "path", payload: "payload") + end + end - subject.request(path: "path", payload: "payload") + context "with container id" do + let(:container_id) { "container-id" } + + it "adds an additional Datadog-Container-ID header" do + expect(api_http).to receive(:request).with( + path: "/evp_proxy/v2/path", + payload: "payload", + verb: "post", + headers: api_headers.merge("Datadog-Container-ID" => "container-id") + ) + + subject.api_request(path: "/path", payload: "payload") + end end end + end - context "with different path_prefix" do - let(:path_prefix) { "/evp_proxy/v4" } + context "with evp proxy v4" do + let(:path_prefix) { Datadog::CI::Ext::Transport::EVP_PROXY_V4_PATH_PREFIX } + + before do + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: agent_settings.hostname, + port: agent_settings.port, + ssl: agent_settings.ssl, + timeout: agent_settings.timeout_seconds, + compress: true + ).and_return(intake_http) - it "constructs evp proxy path using this prefix" do - expect(http).to receive(:request).with( + expect(Datadog::CI::Transport::HTTP).to receive(:new).with( + host: agent_settings.hostname, + port: agent_settings.port, + ssl: agent_settings.ssl, + timeout: agent_settings.timeout_seconds, + compress: false + ).and_return(api_http) + end + + describe "#citestcycle_request" do + it "constructs evp proxy path using v4 prefix" do + expect(intake_http).to receive(:request).with( path: "/evp_proxy/v4/path", payload: "payload", verb: "post", - headers: { - "Content-Type" => "application/msgpack", - "X-Datadog-EVP-Subdomain" => "citestcycle-intake" - } + headers: citestcycle_headers ) - subject.request(path: "path", payload: "payload") + subject.citestcycle_request(path: "path", payload: "payload") end end - context "with container id" do - let(:container_id) { "container-id" } - - it "adds an additional Datadog-Container-ID header" do - expect(http).to receive(:request).with( - path: "/evp_proxy/v2/path", + describe "#api_request" do + it "constructs evp proxy path using v4 prefix" do + expect(api_http).to receive(:request).with( + path: "/evp_proxy/v4/path", payload: "payload", verb: "post", - headers: { - "Content-Type" => "application/msgpack", - "X-Datadog-EVP-Subdomain" => "citestcycle-intake", - "Datadog-Container-ID" => "container-id" - } + headers: api_headers ) - subject.request(path: "/path", payload: "payload") + subject.api_request(path: "path", payload: "payload") end end end diff --git a/spec/datadog/ci/transport/remote_settings_api_spec.rb b/spec/datadog/ci/transport/remote_settings_api_spec.rb new file mode 100644 index 00000000..faf31eaf --- /dev/null +++ b/spec/datadog/ci/transport/remote_settings_api_spec.rb @@ -0,0 +1,161 @@ +require_relative "../../../../lib/datadog/ci/transport/remote_settings_api" + +RSpec.describe Datadog::CI::Transport::RemoteSettingsApi do + let(:api) { spy("api") } + let(:dd_env) { "ci" } + + subject(:client) { described_class.new(api: api, dd_env: dd_env) } + + describe "#fetch_library_settings" do + let(:service) { "service" } + let(:tracer_span) do + Datadog::Tracing::SpanOperation.new("session", service: service).tap do |span| + span.set_tags({ + "git.repository_url" => "repository_url", + "git.branch" => "branch", + "git.commit.sha" => "commit_sha", + "os.platform" => "platform", + "os.architecture" => "arch", + "runtime.name" => "runtime_name", + "runtime.version" => "runtime_version" + }) + end + end + let(:test_session) { Datadog::CI::TestSession.new(tracer_span) } + + let(:path) { Datadog::CI::Ext::Transport::DD_API_SETTINGS_PATH } + + it "requests the settings" do + client.fetch_library_settings(test_session) + + expect(api).to have_received(:api_request) do |args| + expect(args[:path]).to eq(path) + + data = JSON.parse(args[:payload])["data"] + + expect(data["id"]).to eq(Datadog::Core::Environment::Identity.id) + expect(data["type"]).to eq(Datadog::CI::Ext::Transport::DD_API_SETTINGS_TYPE) + + attributes = data["attributes"] + expect(attributes["service"]).to eq(service) + expect(attributes["env"]).to eq(dd_env) + expect(attributes["test_level"]).to eq("test") + expect(attributes["repository_url"]).to eq("repository_url") + expect(attributes["branch"]).to eq("branch") + expect(attributes["sha"]).to eq("commit_sha") + + configurations = attributes["configurations"] + expect(configurations["os.platform"]).to eq("platform") + expect(configurations["os.arch"]).to eq("arch") + expect(configurations["runtime.name"]).to eq("runtime_name") + expect(configurations["runtime.version"]).to eq("runtime_version") + end + end + + context "parsing response" do + subject(:response) { client.fetch_library_settings(test_session) } + + context "when api is present" do + before do + allow(api).to receive(:api_request).and_return(http_response) + end + + context "when response is OK" do + let(:http_response) do + double( + "http_response", + ok?: true, + payload: { + "data" => { + "id" => "123", + "type" => Datadog::CI::Ext::Transport::DD_API_SETTINGS_TYPE, + "attributes" => { + "code_coverage" => true, + "tests_skipping" => false, + "itr_enabled" => true, + "require_git" => false + } + } + }.to_json + ) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.payload).to eq({ + "code_coverage" => true, + "tests_skipping" => false, + "itr_enabled" => true, + "require_git" => false + }) + end + end + + context "when response is not OK" do + let(:http_response) do + double( + "http_response", + ok?: false, + payload: "" + ) + end + + it "parses the response" do + expect(response.ok?).to be false + expect(response.payload).to eq("itr_enabled" => false) + end + end + + context "when response is OK but JSON is malformed" do + let(:http_response) do + double( + "http_response", + ok?: true, + payload: "not json" + ) + end + + before do + expect(Datadog.logger).to receive(:error).with(/Failed to parse settings response payload/) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.payload).to eq("itr_enabled" => false) + end + end + + context "when response is OK but JSON has different format" do + let(:http_response) do + double( + "http_response", + ok?: true, + payload: { + "attributes" => { + "code_coverage" => true, + "tests_skipping" => false, + "itr_enabled" => true, + "require_git" => false + } + }.to_json + ) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.payload).to eq("itr_enabled" => false) + end + end + end + + context "when there is no api" do + let(:api) { nil } + + it "returns an empty response" do + expect(response.ok?).to be false + expect(response.payload).to eq("itr_enabled" => false) + end + end + end + end +end diff --git a/spec/support/contexts/ci_mode.rb b/spec/support/contexts/ci_mode.rb index 6030a278..17da1f1b 100644 --- a/spec/support/contexts/ci_mode.rb +++ b/spec/support/contexts/ci_mode.rb @@ -13,6 +13,7 @@ let(:ci_enabled) { true } let(:force_test_level_visibility) { false } + let(:itr_enabled) { false } let(:recorder) { Datadog.send(:components).ci_recorder } @@ -26,6 +27,7 @@ Datadog.configure do |c| c.ci.enabled = ci_enabled c.ci.force_test_level_visibility = force_test_level_visibility + c.ci.itr_enabled = itr_enabled unless integration_name == :no_instrument c.ci.instrument integration_name, integration_options end