From 723c398d2405a1bc37d229af93dc42131fce043b Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 12 Apr 2024 12:10:10 +0200 Subject: [PATCH 1/2] add skippable tests api client --- lib/datadog/ci/ext/transport.rb | 3 + lib/datadog/ci/itr/skippable.rb | 113 +++++++++++ .../ci/transport/remote_settings_api.rb | 2 +- sig/datadog/ci/ext/transport.rbs | 4 + sig/datadog/ci/itr/skippable.rbs | 47 +++++ .../datadog/ci/configuration/settings_spec.rb | 2 + spec/datadog/ci/itr/skippable_spec.rb | 180 ++++++++++++++++++ .../ci/transport/remote_settings_api_spec.rb | 2 + 8 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 lib/datadog/ci/itr/skippable.rb create mode 100644 sig/datadog/ci/itr/skippable.rbs create mode 100644 spec/datadog/ci/itr/skippable_spec.rb diff --git a/lib/datadog/ci/ext/transport.rb b/lib/datadog/ci/ext/transport.rb index b1559ac6..c71ac7fd 100644 --- a/lib/datadog/ci/ext/transport.rb +++ b/lib/datadog/ci/ext/transport.rb @@ -41,6 +41,9 @@ module Transport DD_API_GIT_UPLOAD_PACKFILE_PATH = "/api/v2/git/repository/packfile" + DD_API_SKIPPABLE_TESTS_PATH = "/api/v2/ci/tests/skippable" + DD_API_SKIPPABLE_TESTS_TYPE = "test_params" + CONTENT_TYPE_MESSAGEPACK = "application/msgpack" CONTENT_TYPE_JSON = "application/json" CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data" diff --git a/lib/datadog/ci/itr/skippable.rb b/lib/datadog/ci/itr/skippable.rb new file mode 100644 index 00000000..0c155227 --- /dev/null +++ b/lib/datadog/ci/itr/skippable.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "json" + +require_relative "../ext/transport" +require_relative "../ext/test" + +module Datadog + module CI + module ITR + class Skippable + class Test + attr_reader :name, :suite + + def initialize(name:, suite:) + @name = name + @suite = suite + end + + def ==(other) + name == other.name && suite == other.suite + end + end + + class Response + def initialize(http_response) + @http_response = http_response + @json = nil + end + + def ok? + resp = @http_response + !resp.nil? && resp.ok? + end + + def correlation_id + payload.dig("meta", "correlation_id") + end + + def tests + payload.fetch("data", []) + .filter_map do |test_data| + next unless test_data["type"] == Ext::Test::ITR_TEST_SKIPPING_MODE + + attrs = test_data["attributes"] || {} + Test.new(name: attrs["name"], suite: attrs["suite"]) + end + end + + private + + def payload + cached = @json + return cached unless cached.nil? + + resp = @http_response + return @json = {} if resp.nil? || !ok? + + begin + @json = JSON.parse(resp.payload) + rescue JSON::ParserError => e + Datadog.logger.error("Failed to parse skippable tests response payload: #{e}. Payload was: #{resp.payload}") + @json = {} + end + end + end + + def initialize(api: nil, dd_env: nil) + @api = api + @dd_env = dd_env + end + + def fetch_skippable_tests(test_session) + api = @api + return Response.new(nil) unless api + + request_payload = payload(test_session) + Datadog.logger.debug("Fetching skippable tests with request: #{request_payload}") + + http_response = api.api_request( + path: Ext::Transport::DD_API_SKIPPABLE_TESTS_PATH, + payload: request_payload + ) + + Response.new(http_response) + end + + private + + def payload(test_session) + { + "data" => { + "type" => Ext::Transport::DD_API_SKIPPABLE_TESTS_TYPE, + "attributes" => { + "test_level" => Ext::Test::ITR_TEST_SKIPPING_MODE, + "service" => test_session.service, + "env" => @dd_env, + "repository_url" => test_session.git_repository_url, + "sha" => test_session.git_commit_sha, + "configurations" => { + "os.platform" => test_session.os_platform, + "os.architecture" => 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/lib/datadog/ci/transport/remote_settings_api.rb b/lib/datadog/ci/transport/remote_settings_api.rb index d3ab46ad..ec252bd0 100644 --- a/lib/datadog/ci/transport/remote_settings_api.rb +++ b/lib/datadog/ci/transport/remote_settings_api.rb @@ -29,7 +29,7 @@ def payload return cached unless cached.nil? resp = @http_response - return @json = default_payload if resp.nil? || !resp.ok? + return @json = default_payload if resp.nil? || !ok? begin @json = JSON.parse(resp.payload).dig(*Ext::Transport::DD_API_SETTINGS_RESPONSE_DIG_KEYS) || diff --git a/sig/datadog/ci/ext/transport.rbs b/sig/datadog/ci/ext/transport.rbs index 8f6aa7dc..12ea3544 100644 --- a/sig/datadog/ci/ext/transport.rbs +++ b/sig/datadog/ci/ext/transport.rbs @@ -52,6 +52,10 @@ module Datadog DD_API_GIT_UPLOAD_PACKFILE_PATH: "/api/v2/git/repository/packfile" + DD_API_SKIPPABLE_TESTS_PATH: "/api/v2/ci/tests/skippable" + + DD_API_SKIPPABLE_TESTS_TYPE: "test_params" + CONTENT_TYPE_MESSAGEPACK: "application/msgpack" CONTENT_TYPE_JSON: "application/json" diff --git a/sig/datadog/ci/itr/skippable.rbs b/sig/datadog/ci/itr/skippable.rbs new file mode 100644 index 00000000..bb06b5e9 --- /dev/null +++ b/sig/datadog/ci/itr/skippable.rbs @@ -0,0 +1,47 @@ +module Datadog + module CI + module ITR + class Skippable + @api: Datadog::CI::Transport::Api::Base? + @dd_env: String? + + class Test + @name: String? + + @suite: String? + + attr_reader name: String? + + attr_reader suite: String? + + def initialize: (name: String?, suite: String?) -> void + end + + 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 correlation_id: () -> String? + + def tests: () -> Array[Test] + + private + + def payload: () -> Hash[String, untyped] + end + + def initialize: (?api: Datadog::CI::Transport::Api::Base?, ?dd_env: String?) -> void + + def fetch_skippable_tests: (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/settings_spec.rb b/spec/datadog/ci/configuration/settings_spec.rb index f82dd250..5cea8e5a 100644 --- a/spec/datadog/ci/configuration/settings_spec.rb +++ b/spec/datadog/ci/configuration/settings_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Dummy Integration class FakeIntegration include Datadog::CI::Contrib::Integration diff --git a/spec/datadog/ci/itr/skippable_spec.rb b/spec/datadog/ci/itr/skippable_spec.rb new file mode 100644 index 00000000..be209ba5 --- /dev/null +++ b/spec/datadog/ci/itr/skippable_spec.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/datadog/ci/itr/skippable" + +RSpec.describe Datadog::CI::ITR::Skippable do + let(:api) { spy("api") } + let(:dd_env) { "ci" } + + subject(:client) { described_class.new(api: api, dd_env: dd_env) } + + describe "#fetch_skippable_tests" 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_SKIPPABLE_TESTS_PATH } + + it "requests the skippable tests" do + client.fetch_skippable_tests(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["type"]).to eq(Datadog::CI::Ext::Transport::DD_API_SKIPPABLE_TESTS_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["sha"]).to eq("commit_sha") + + configurations = attributes["configurations"] + expect(configurations["os.platform"]).to eq("platform") + expect(configurations["os.architecture"]).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_skippable_tests(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: { + "meta" => { + "correlation_id" => "correlation_id_123" + }, + "data" => [ + { + "id" => "123", + "type" => Datadog::CI::Ext::Test::ITR_TEST_SKIPPING_MODE, + "attributes" => { + "suite" => "test_suite_name", + "name" => "test_name", + "parameters" => "string", + "configurations" => { + "os.platform" => "linux", + "os.version" => "bionic", + "os.architecture" => "amd64", + "runtime.vendor" => "string", + "runtime.architecture" => "amd64" + } + } + } + ] + }.to_json + ) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.correlation_id).to eq("correlation_id_123") + expect(response.tests.first).to eq( + Datadog::CI::ITR::Skippable::Test.new(name: "test_name", suite: "test_suite_name") + ) + 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.correlation_id).to be_nil + expect(response.tests).to be_empty + 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 skippable tests response payload/) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.correlation_id).to be_nil + expect(response.tests).to be_empty + end + end + + context "when response is OK but JSON has different format" do + let(:http_response) do + double( + "http_response", + ok?: true, + payload: { + "attributes" => { + "suite" => "test_suite_name", + "name" => "test_name", + "parameters" => "string", + "configurations" => { + "os.platform" => "linux", + "os.version" => "bionic", + "os.architecture" => "amd64", + "runtime.vendor" => "string", + "runtime.architecture" => "amd64" + } + } + }.to_json + ) + end + + it "parses the response" do + expect(response.ok?).to be true + expect(response.correlation_id).to be_nil + expect(response.tests).to be_empty + 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.correlation_id).to be_nil + expect(response.tests).to be_empty + end + end + 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 index e11cdbf6..a46f30c9 100644 --- a/spec/datadog/ci/transport/remote_settings_api_spec.rb +++ b/spec/datadog/ci/transport/remote_settings_api_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "../../../../lib/datadog/ci/transport/remote_settings_api" RSpec.describe Datadog::CI::Transport::RemoteSettingsApi do From 3d52ba1252c3c467da916e7b96e07c8705e50d6c Mon Sep 17 00:00:00 2001 From: Andrey Marchenko Date: Fri, 12 Apr 2024 12:12:04 +0200 Subject: [PATCH 2/2] add additional logging for the case where git metadata upload was not processed by backend on library configuration stage --- lib/datadog/ci/test_visibility/recorder.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/datadog/ci/test_visibility/recorder.rb b/lib/datadog/ci/test_visibility/recorder.rb index 538adc4d..83b92e44 100644 --- a/lib/datadog/ci/test_visibility/recorder.rb +++ b/lib/datadog/ci/test_visibility/recorder.rb @@ -224,6 +224,10 @@ def configure_library(test_session) Datadog.logger.debug { "Requesting library configuration again..." } remote_configuration = @remote_settings_api.fetch_library_settings(test_session) + + if remote_configuration.require_git? + Datadog.logger.debug { "git metadata upload did not complete in time when configuring library" } + end end @itr.configure(remote_configuration.payload, test_session) end