Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDTEST-172] Remote configuration component #210

Merged
merged 10 commits into from
Jul 30, 2024
Merged
22 changes: 13 additions & 9 deletions lib/datadog/ci/configuration/components.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

require_relative "../ext/settings"
require_relative "../git/tree_uploader"
require_relative "../remote/component"
require_relative "../remote/library_settings_client"
require_relative "../test_optimisation/component"
require_relative "../test_optimisation/coverage/transport"
require_relative "../test_optimisation/coverage/writer"
Expand All @@ -15,7 +17,6 @@
require_relative "../test_visibility/transport"
require_relative "../transport/adapters/telemetry_webmock_safe_adapter"
require_relative "../transport/api/builder"
require_relative "../transport/remote_settings_api"
require_relative "../utils/identity"
require_relative "../utils/parsing"
require_relative "../utils/test_run"
Expand All @@ -26,11 +27,13 @@ module CI
module Configuration
# Adds CI behavior to Datadog trace components
module Components
attr_reader :test_visibility, :test_optimisation
attr_reader :test_visibility, :test_optimisation, :git_tree_upload_worker, :ci_remote

def initialize(settings)
@test_optimisation = nil
@test_visibility = TestVisibility::NullComponent.new
@git_tree_upload_worker = DummyWorker.new
@ci_remote = nil

# Activate CI mode if enabled
if settings.ci.enabled
Expand All @@ -45,6 +48,7 @@ def shutdown!(replacement = nil)

@test_visibility&.shutdown!
@test_optimisation&.shutdown!
@git_tree_upload_worker&.stop
end

def activate_ci!(settings)
Expand Down Expand Up @@ -102,14 +106,14 @@ def activate_ci!(settings)

settings.tracing.test_mode.writer_options = trace_writer_options

@git_tree_upload_worker = build_git_upload_worker(settings, test_visibility_api)
@ci_remote = Remote::Component.new(
library_settings_client: build_library_settings_client(settings, test_visibility_api)
)
# @type ivar @test_optimisation: Datadog::CI::TestOptimisation::Component
@test_optimisation = build_test_optimisation(settings, test_visibility_api)

@test_visibility = TestVisibility::Component.new(
test_optimisation: @test_optimisation,
test_suite_level_visibility_enabled: !settings.ci.force_test_level_visibility,
remote_settings_api: build_remote_settings_client(settings, test_visibility_api),
git_tree_upload_worker: build_git_upload_worker(settings, test_visibility_api)
test_suite_level_visibility_enabled: !settings.ci.force_test_level_visibility
)
end

Expand Down Expand Up @@ -216,8 +220,8 @@ def build_git_upload_worker(settings, api)
end
end

def build_remote_settings_client(settings, api)
Transport::RemoteSettingsApi.new(
def build_library_settings_client(settings, api)
Remote::LibrarySettingsClient.new(
api: api,
dd_env: settings.env,
config_tags: custom_configuration(settings)
Expand Down
45 changes: 45 additions & 0 deletions lib/datadog/ci/remote/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Datadog
module CI
module Remote
# Remote configuration component.
# Responsible for fetching library settings and configuring the library accordingly.
class Component
def initialize(library_settings_client:)
@library_settings_client = library_settings_client
end

# called on test session start, uses test session info to send configuration request to the backend
def configure(test_session)
library_configuration = @library_settings_client.fetch(test_session)
# sometimes we can skip code coverage for default branch if there are no changes in the repository
# backend needs git metadata uploaded for this test session to check if we can skip code coverage
if library_configuration.require_git?
Datadog.logger.debug { "Library configuration endpoint requires git upload to be finished, waiting..." }
git_tree_upload_worker.wait_until_done

Datadog.logger.debug { "Requesting library configuration again..." }
library_configuration = @library_settings_client.fetch(test_session)

if library_configuration.require_git?
Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" }
end
end

test_optimisation.configure(library_configuration, test_session)
end

private

def test_optimisation
Datadog.send(:components).test_optimisation
end

def git_tree_upload_worker
Datadog.send(:components).git_tree_upload_worker
end
end
end
end
end
85 changes: 85 additions & 0 deletions lib/datadog/ci/remote/library_settings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# frozen_string_literal: true

require "json"

require_relative "../ext/telemetry"
require_relative "../ext/transport"
require_relative "../transport/telemetry"
require_relative "../utils/parsing"

module Datadog
module CI
module Remote
# Wrapper around the settings HTTP response
class LibrarySettings
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? || !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}")

Transport::Telemetry.api_requests_errors(
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
1,
error_type: "invalid_json",
status_code: nil
)

@json = default_payload
end
end

def require_git?
return @require_git if defined?(@require_git)

@require_git = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY)
end

def itr_enabled?
return @itr_enabled if defined?(@itr_enabled)

@itr_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY)
end

def code_coverage_enabled?
return @code_coverage_enabled if defined?(@code_coverage_enabled)

@code_coverage_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY)
end

def tests_skipping_enabled?
return @tests_skipping_enabled if defined?(@tests_skipping_enabled)

@tests_skipping_enabled = bool(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY)
end

private

def bool(key)
Utils::Parsing.convert_to_bool(payload.fetch(key, false))
end

def default_payload
Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,28 @@

require "datadog/core/environment/identity"

require_relative "library_settings"

require_relative "../ext/test"
require_relative "../ext/telemetry"
require_relative "../ext/transport"
require_relative "../transport/telemetry"
require_relative "../utils/parsing"
require_relative "../utils/telemetry"

module Datadog
module CI
module Transport
# Datadog API client
module Remote
# 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? || !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}")

Transport::Telemetry.api_requests_errors(
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_ERRORS,
1,
error_type: "invalid_json",
status_code: nil
)

@json = default_payload
end
end

def require_git?
Utils::Parsing.convert_to_bool(payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY])
end

private

def default_payload
Ext::Transport::DD_API_SETTINGS_RESPONSE_DEFAULT
end
end

class LibrarySettingsClient
def initialize(dd_env:, api: nil, config_tags: {})
@api = api
@dd_env = dd_env
@config_tags = config_tags || {}
end

def fetch_library_settings(test_session)
def fetch(test_session)
api = @api
return Response.new(nil) unless api
return LibrarySettings.new(nil) unless api

request_payload = payload(test_session)
Datadog.logger.debug("Fetching library settings with request: #{request_payload}")
Expand All @@ -96,18 +51,18 @@ def fetch_library_settings(test_session)
)
end

response = Response.new(http_response)
library_settings = LibrarySettings.new(http_response)

Utils::Telemetry.inc(
Ext::Telemetry::METRIC_GIT_REQUESTS_SETTINGS_RESPONSE,
1,
{
Ext::Telemetry::TAG_COVERAGE_ENABLED => response.payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY],
Ext::Telemetry::TAG_ITR_SKIP_ENABLED => response.payload[Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY]
Ext::Telemetry::TAG_COVERAGE_ENABLED => library_settings.code_coverage_enabled?.to_s,
Ext::Telemetry::TAG_ITR_SKIP_ENABLED => library_settings.tests_skipping_enabled?.to_s
}
)

response
library_settings
end

private
Expand Down
28 changes: 12 additions & 16 deletions lib/datadog/ci/test_optimisation/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

require_relative "../ext/test"
require_relative "../ext/telemetry"
require_relative "../ext/transport"

require_relative "../git/local_repository"

Expand Down Expand Up @@ -65,32 +64,25 @@ def initialize(
Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
end

def configure(remote_configuration, test_session:, git_tree_upload_worker:)
def configure(remote_configuration, test_session)
return unless enabled?

Datadog.logger.debug("Configuring TestOptimisation with remote configuration: #{remote_configuration}")

@enabled = Utils::Parsing.convert_to_bool(
remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY, false)
)
@test_skipping_enabled = @enabled && Utils::Parsing.convert_to_bool(
remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false)
)
@code_coverage_enabled = @enabled && Utils::Parsing.convert_to_bool(
remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false)
)
@enabled = remote_configuration.itr_enabled?
@test_skipping_enabled = @enabled && remote_configuration.tests_skipping_enabled?
@code_coverage_enabled = @enabled && remote_configuration.code_coverage_enabled?

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)

load_datadog_cov! if @code_coverage_enabled

Datadog.logger.debug("Configured TestOptimisation with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")

fetch_skippable_tests(test_session: test_session, git_tree_upload_worker: git_tree_upload_worker)
fetch_skippable_tests(test_session)
end

def enabled?
Expand Down Expand Up @@ -225,7 +217,7 @@ def ensure_test_source_covered(test_source_file, coverage)
coverage[absolute_test_source_file_path] = true
end

def fetch_skippable_tests(test_session:, git_tree_upload_worker:)
def fetch_skippable_tests(test_session)
return unless skipping_tests?

# we can only request skippable tests if git metadata is already uploaded
Expand All @@ -248,6 +240,10 @@ def fetch_skippable_tests(test_session:, git_tree_upload_worker:)
def code_coverage_mode
@use_single_threaded_coverage ? :single : :multi
end

def git_tree_upload_worker
Datadog.send(:components).git_tree_upload_worker
end
end
end
end
Expand Down
Loading
Loading