diff --git a/ddtrace/testing/internal/api_client.py b/ddtrace/testing/internal/api_client.py index 76c45fbbf30..c1857ad35ce 100644 --- a/ddtrace/testing/internal/api_client.py +++ b/ddtrace/testing/internal/api_client.py @@ -12,6 +12,7 @@ from ddtrace.testing.internal.http import FileAttachment from ddtrace.testing.internal.settings_data import Settings from ddtrace.testing.internal.settings_data import TestProperties +from ddtrace.testing.internal.telemetry import ErrorType from ddtrace.testing.internal.telemetry import TelemetryAPI from ddtrace.testing.internal.test_data import ITRSkippingLevel from ddtrace.testing.internal.test_data import ModuleRef @@ -52,36 +53,50 @@ def get_settings(self) -> Settings: error="git_requests.settings_errors", ) - request_data = { - "data": { - "id": str(uuid.uuid4()), - "type": "ci_app_test_service_libraries_settings", - "attributes": { - "test_level": self.itr_skipping_level.value, - "service": self.service, - "env": self.env, - "repository_url": self.env_tags[GitTag.REPOSITORY_URL], - "sha": self.env_tags[GitTag.COMMIT_SHA], - "branch": self.env_tags[GitTag.BRANCH], - "configurations": self.configurations, - }, + try: + request_data = { + "data": { + "id": str(uuid.uuid4()), + "type": "ci_app_test_service_libraries_settings", + "attributes": { + "test_level": self.itr_skipping_level.value, + "service": self.service, + "env": self.env, + "repository_url": self.env_tags[GitTag.REPOSITORY_URL], + "sha": self.env_tags[GitTag.COMMIT_SHA], + "branch": self.env_tags[GitTag.BRANCH], + "configurations": self.configurations, + }, + } } - } + + except KeyError as e: + log.error("Git info not available, cannot fetch settings (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return Settings() try: result = self.connector.post_json( "/api/v2/libraries/tests/services/setting", request_data, telemetry=telemetry ) result.on_error_raise_exception() + + except Exception as e: + log.error("Error getting settings from API: %s", e) + return Settings() + + try: attributes = result.parsed_response["data"]["attributes"] settings = Settings.from_attributes(attributes) - self.telemetry_api.record_settings(settings) - return settings - except Exception: - log.exception("Error getting settings from API") + except Exception as e: + log.exception("Error getting settings from API: %s", e) + telemetry.record_error(ErrorType.BAD_JSON) return Settings() + self.telemetry_api.record_settings(settings) + return settings + def get_known_tests(self) -> t.Set[TestRef]: telemetry = self.telemetry_api.with_request_metric_names( count="known_tests.request", @@ -90,22 +105,34 @@ def get_known_tests(self) -> t.Set[TestRef]: error="known_tests.request_errors", ) - request_data = { - "data": { - "id": str(uuid.uuid4()), - "type": "ci_app_libraries_tests_request", - "attributes": { - "service": self.service, - "env": self.env, - "repository_url": self.env_tags[GitTag.REPOSITORY_URL], - "configurations": self.configurations, - }, + try: + request_data = { + "data": { + "id": str(uuid.uuid4()), + "type": "ci_app_libraries_tests_request", + "attributes": { + "service": self.service, + "env": self.env, + "repository_url": self.env_tags[GitTag.REPOSITORY_URL], + "configurations": self.configurations, + }, + } } - } + + except KeyError as e: + log.error("Git info not available, cannot fetch known tests (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return set() try: result = self.connector.post_json("/api/v2/ci/libraries/tests", request_data, telemetry=telemetry) result.on_error_raise_exception() + + except Exception as e: + log.exception("Error getting known tests from API: %s", e) + return set() + + try: tests_data = result.parsed_response["data"]["attributes"]["tests"] known_test_ids = set() @@ -116,13 +143,14 @@ def get_known_tests(self) -> t.Set[TestRef]: for test in tests: known_test_ids.add(TestRef(suite_ref, test)) - self.telemetry_api.record_known_tests_count(len(known_test_ids)) - return known_test_ids - except Exception: log.exception("Error getting known tests from API") + telemetry.record_error(ErrorType.BAD_JSON) return set() + self.telemetry_api.record_known_tests_count(len(known_test_ids)) + return known_test_ids + def get_test_management_properties(self) -> t.Dict[TestRef, TestProperties]: telemetry = self.telemetry_api.with_request_metric_names( count="test_management_tests.request", @@ -131,23 +159,35 @@ def get_test_management_properties(self) -> t.Dict[TestRef, TestProperties]: error="test_management_tests.request_errors", ) - request_data = { - "data": { - "id": str(uuid.uuid4()), - "type": "ci_app_libraries_tests_request", - "attributes": { - "repository_url": self.env_tags[GitTag.REPOSITORY_URL], - "commit_message": self.env_tags[GitTag.COMMIT_MESSAGE], - "sha": self.env_tags[GitTag.COMMIT_SHA], - }, + try: + request_data = { + "data": { + "id": str(uuid.uuid4()), + "type": "ci_app_libraries_tests_request", + "attributes": { + "repository_url": self.env_tags[GitTag.REPOSITORY_URL], + "commit_message": self.env_tags[GitTag.COMMIT_MESSAGE], + "sha": self.env_tags[GitTag.COMMIT_SHA], + }, + } } - } + + except KeyError as e: + log.error("Git info not available, cannot fetch Test Management properties (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return {} try: result = self.connector.post_json( "/api/v2/test/libraries/test-management/tests", request_data, telemetry=telemetry ) result.on_error_raise_exception() + + except Exception as e: + log.error("Error getting Test Management properties from API: %s", e) + return {} + + try: test_properties: t.Dict[TestRef, TestProperties] = {} modules = result.parsed_response["data"]["attributes"]["modules"] @@ -166,54 +206,105 @@ def get_test_management_properties(self) -> t.Dict[TestRef, TestProperties]: attempt_to_fix=properties.get("attempt_to_fix", False), ) - self.telemetry_api.record_test_management_tests_count(len(test_properties)) - - return test_properties - except Exception: - log.exception("Failed to parse Test Management tests data") + log.exception("Failed to parse Test Management tests data from API") + telemetry.record_error(ErrorType.BAD_JSON) return {} + self.telemetry_api.record_test_management_tests_count(len(test_properties)) + return test_properties + def get_known_commits(self, latest_commits: t.List[str]) -> t.List[str]: - request_data = { - "meta": { - "repository_url": self.env_tags[GitTag.REPOSITORY_URL], - }, - "data": [{"id": sha, "type": "commit"} for sha in latest_commits], - } + telemetry = self.telemetry_api.with_request_metric_names( + count="git_requests.search_commits", + duration="git_requests.search_commits_ms", + response_bytes=None, + error="git_requests.search_commits_errors", + ) + + try: + request_data = { + "meta": { + "repository_url": self.env_tags[GitTag.REPOSITORY_URL], + }, + "data": [{"id": sha, "type": "commit"} for sha in latest_commits], + } + + except KeyError as e: + log.error("Git info not available, cannot fetch known commits (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return [] try: - result = self.connector.post_json("/api/v2/git/repository/search_commits", request_data) + result = self.connector.post_json( + "/api/v2/git/repository/search_commits", request_data, telemetry=telemetry + ) result.on_error_raise_exception() - return [item["id"] for item in result.parsed_response["data"] if item["type"] == "commit"] + + except Exception as e: + log.error("Error getting known commits from API: %s", e) + return [] + + try: + known_commits = [item["id"] for item in result.parsed_response["data"] if item["type"] == "commit"] except Exception: log.exception("Failed to parse search_commits data") + telemetry.record_error(ErrorType.BAD_JSON) return [] - def send_git_pack_file(self, packfile: Path) -> None: - metadata = { - "data": {"id": self.env_tags[GitTag.COMMIT_SHA], "type": "commit"}, - "meta": {"repository_url": self.env_tags[GitTag.REPOSITORY_URL]}, - } - content = packfile.read_bytes() - files = [ - FileAttachment( - name="pushedSha", - filename=None, - content_type="application/json", - data=json.dumps(metadata).encode("utf-8"), - ), - FileAttachment( - name="packfile", filename=packfile.name, content_type="application/octet-stream", data=content - ), - ] + return known_commits + + def send_git_pack_file(self, packfile: Path) -> t.Optional[int]: + telemetry = self.telemetry_api.with_request_metric_names( + count="git_requests.objects_pack", + duration="git_requests.objects_pack_ms", + response_bytes=None, + error="git_requests.objects_pack_errors", + ) + + try: + metadata = { + "data": {"id": self.env_tags[GitTag.COMMIT_SHA], "type": "commit"}, + "meta": {"repository_url": self.env_tags[GitTag.REPOSITORY_URL]}, + } + + except KeyError as e: + log.error("Git info not available, cannot send git packfile (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return None + try: - result = self.connector.post_files("/api/v2/git/repository/packfile", files=files, send_gzip=False) + content = packfile.read_bytes() + + files = [ + FileAttachment( + name="pushedSha", + filename=None, + content_type="application/json", + data=json.dumps(metadata).encode("utf-8"), + ), + FileAttachment( + name="packfile", filename=packfile.name, content_type="application/octet-stream", data=content + ), + ] + + except Exception: + log.exception("Error sending Git pack data") + telemetry.record_error(ErrorType.UNKNOWN) + return None + + try: + result = self.connector.post_files( + "/api/v2/git/repository/packfile", files=files, send_gzip=False, telemetry=telemetry + ) result.on_error_raise_exception() except Exception: - log.warning("Failed to upload git pack data") + log.warning("Failed to upload Git pack data") + return None + + return len(content) def get_skippable_tests(self) -> t.Tuple[t.Set[t.Union[SuiteRef, TestRef]], t.Optional[str]]: telemetry = self.telemetry_api.with_request_metric_names( @@ -223,24 +314,36 @@ def get_skippable_tests(self) -> t.Tuple[t.Set[t.Union[SuiteRef, TestRef]], t.Op error="itr_skippable_tests.request_errors", ) - request_data = { - "data": { - "id": str(uuid.uuid4()), - "type": "test_params", - "attributes": { - "service": self.service, - "env": self.env, - "repository_url": self.env_tags[GitTag.REPOSITORY_URL], - "sha": self.env_tags[GitTag.COMMIT_SHA], - "configurations": self.configurations, - "test_level": self.itr_skipping_level.value, - }, + try: + request_data = { + "data": { + "id": str(uuid.uuid4()), + "type": "test_params", + "attributes": { + "service": self.service, + "env": self.env, + "repository_url": self.env_tags[GitTag.REPOSITORY_URL], + "sha": self.env_tags[GitTag.COMMIT_SHA], + "configurations": self.configurations, + "test_level": self.itr_skipping_level.value, + }, + } } - } + + except KeyError as e: + log.error("Git info not available, cannot get skippable items (missing key: %s)", e) + telemetry.record_error(ErrorType.UNKNOWN) + return set(), None + try: result = self.connector.post_json("/api/v2/ci/tests/skippable", request_data, telemetry=telemetry) result.on_error_raise_exception() + except Exception as e: + log.error("Error getting skippable tests from API: %s", e) + return set(), None + + try: skippable_items: t.Set[t.Union[SuiteRef, TestRef]] = set() for item in result.parsed_response["data"]: @@ -255,10 +358,11 @@ def get_skippable_tests(self) -> t.Tuple[t.Set[t.Union[SuiteRef, TestRef]], t.Op correlation_id = result.parsed_response["meta"]["correlation_id"] - self.telemetry_api.record_skippable_count(count=len(skippable_items), level=self.itr_skipping_level) - - return skippable_items, correlation_id - except Exception: - log.exception("Error getting skippable tests from API") + log.exception("Failed to parse skippable tests data from API") + telemetry.record_error(ErrorType.BAD_JSON) return set(), None + + self.telemetry_api.record_skippable_count(count=len(skippable_items), level=self.itr_skipping_level) + + return skippable_items, correlation_id diff --git a/ddtrace/testing/internal/session_manager.py b/ddtrace/testing/internal/session_manager.py index d80290f5eb0..a8081066788 100644 --- a/ddtrace/testing/internal/session_manager.py +++ b/ddtrace/testing/internal/session_manager.py @@ -290,8 +290,16 @@ def upload_git_data(self) -> None: excluded_commits=backend_commits, included_commits=commits_not_in_backend ) + uploaded_files = 0 + uploaded_bytes = 0 + for packfile in git.pack_objects(revisions_to_send): - self.api_client.send_git_pack_file(packfile) + nbytes = self.api_client.send_git_pack_file(packfile) + if nbytes is not None: + uploaded_bytes += nbytes + uploaded_files += 1 + + TelemetryAPI.get().record_git_pack_data(uploaded_files, uploaded_bytes) def is_skippable_test(self, test_ref: TestRef) -> bool: if not self.settings.skipping_enabled: diff --git a/ddtrace/testing/internal/telemetry.py b/ddtrace/testing/internal/telemetry.py index 02f568fb3c3..dfb88e29adf 100644 --- a/ddtrace/testing/internal/telemetry.py +++ b/ddtrace/testing/internal/telemetry.py @@ -274,6 +274,10 @@ def record_session_finished( self.add_count_metric("event_finished", 1, tags) + def record_git_pack_data(self, uploaded_files: int, uploaded_bytes: int) -> None: + self.add_distribution_metric("git_requests.objects_pack_files", uploaded_files) + self.add_distribution_metric("git_requests.objects_pack_bytes", uploaded_bytes) + @dataclasses.dataclass class TelemetryAPIRequestMetrics: diff --git a/tests/testing/internal/test_api_client.py b/tests/testing/internal/test_api_client.py new file mode 100644 index 00000000000..64897c44430 --- /dev/null +++ b/tests/testing/internal/test_api_client.py @@ -0,0 +1,1168 @@ +import json +import logging +from pathlib import Path +import typing as t +from unittest.mock import Mock +from unittest.mock import call +from unittest.mock import patch +import uuid + +import pytest + +from ddtrace.testing.internal.api_client import APIClient +from ddtrace.testing.internal.git import GitTag +from ddtrace.testing.internal.http import BackendResult +from ddtrace.testing.internal.http import FileAttachment +from ddtrace.testing.internal.logging import testing_logger +from ddtrace.testing.internal.settings_data import TestProperties +from ddtrace.testing.internal.telemetry import ErrorType +from ddtrace.testing.internal.test_data import ITRSkippingLevel +from ddtrace.testing.internal.test_data import ModuleRef +from ddtrace.testing.internal.test_data import SuiteRef +from ddtrace.testing.internal.test_data import TestRef +from tests.testing.mocks import mock_backend_connector + + +@pytest.fixture(scope="module", autouse=True) +def override_testing_logger(): + # This is needed for the caplog fixture to work correctly, if previous tests have caused + # `ddtrace.testing.internal.logging.setup_logging()` to be called. + testing_logger.propagate = True + + +class TestAPIClientGetSettings: + @pytest.mark.parametrize( + "efd_enabled, atr_enabled, test_management_enabled, attempt_to_fix_retries, " + "known_tests_enabled, coverage_enabled, skipping_enabled, require_git, itr_enabled", + [ + (True, True, True, 30, True, True, True, True, True), + (False, False, False, 40, False, False, False, False, False), + (True, False, True, 50, True, False, True, False, True), + ], + ) + def test_get_settings( + self, + mock_telemetry: Mock, + efd_enabled: bool, + atr_enabled: bool, + test_management_enabled: bool, + attempt_to_fix_retries: int, + known_tests_enabled: bool, + coverage_enabled: bool, + skipping_enabled: bool, + require_git: bool, + itr_enabled: bool, + ) -> None: + mock_connector = ( + mock_backend_connector() + .with_post_json_response( + endpoint="/api/v2/libraries/tests/services/setting", + response_data={ + "data": { + "attributes": { + "code_coverage": coverage_enabled, + "coverage_report_upload_enabled": False, + "di_enabled": False, + "early_flake_detection": { + "enabled": efd_enabled, + "faulty_session_threshold": 30, + "slow_test_retries": {"10s": 5, "30s": 3, "5m": 2, "5s": 10}, + }, + "flaky_test_retries_enabled": atr_enabled, + "impacted_tests_enabled": False, + "itr_enabled": itr_enabled, + "known_tests_enabled": known_tests_enabled, + "require_git": require_git, + "test_management": { + "attempt_to_fix_retries": attempt_to_fix_retries, + "enabled": test_management_enabled, + }, + "tests_skipping": skipping_enabled, + }, + "id": "00000000-0000-0000-0000-000000000000", + "type": "ci_app_tracers_test_service_settings", + } + }, + ) + .build() + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + settings = api_client.get_settings() + + assert mock_connector.post_json.call_args_list == [ + call( + "/api/v2/libraries/tests/services/setting", + { + "data": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "ci_app_test_service_libraries_settings", + "attributes": { + "test_level": "test", + "service": "some-service", + "env": "some-env", + "repository_url": "http://github.com/DataDog/some-repo.git", + "sha": "abcd1234", + "branch": "some-branch", + "configurations": {"os.platform": "Linux"}, + }, + } + }, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + assert settings.early_flake_detection.enabled == efd_enabled + assert settings.auto_test_retries.enabled == atr_enabled + assert settings.test_management.enabled == test_management_enabled + assert settings.test_management.attempt_to_fix_retries == attempt_to_fix_retries + assert settings.known_tests_enabled == known_tests_enabled + assert settings.coverage_enabled == coverage_enabled + assert settings.skipping_enabled == skipping_enabled + assert settings.require_git == require_git + assert settings.itr_enabled == itr_enabled + + def test_get_settings_missing_git_data(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + settings = api_client.get_settings() + + assert "Git info not available" in caplog.text + assert mock_connector.post_json.call_args_list == [] + + assert settings.early_flake_detection.enabled is False + assert settings.auto_test_retries.enabled is False + assert settings.test_management.enabled is False + assert settings.test_management.attempt_to_fix_retries == 20 + assert settings.known_tests_enabled is False + assert settings.coverage_enabled is False + assert settings.skipping_enabled is False + assert settings.require_git is False + assert settings.itr_enabled is False + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_get_settings_fail_http_request(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = Mock() + mock_connector.post_json.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + settings = api_client.get_settings() + + assert "Error getting settings from API: No can do" in caplog.text + + assert settings.early_flake_detection.enabled is False + assert settings.auto_test_retries.enabled is False + assert settings.test_management.enabled is False + assert settings.test_management.attempt_to_fix_retries == 20 + assert settings.known_tests_enabled is False + assert settings.coverage_enabled is False + assert settings.skipping_enabled is False + assert settings.require_git is False + assert settings.itr_enabled is False + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_get_settings_errors_in_response(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/libraries/tests/services/setting", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + settings = api_client.get_settings() + + assert "Error getting settings from API" in caplog.text + assert "KeyError" in caplog.text + + assert settings.early_flake_detection.enabled is False + assert settings.auto_test_retries.enabled is False + assert settings.test_management.enabled is False + assert settings.test_management.attempt_to_fix_retries == 20 + assert settings.known_tests_enabled is False + assert settings.coverage_enabled is False + assert settings.skipping_enabled is False + assert settings.require_git is False + assert settings.itr_enabled is False + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.BAD_JSON) + ] + + +class TestAPIClientGetKnownTests: + def test_get_known_tests(self, mock_telemetry: Mock) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/ci/libraries/tests", + response_data={ + "data": { + "attributes": { + "tests": { + "some-module": { + "test_simple.py": ["test_01", "test_02"], + "test_second.py": ["test_01", "test_02", "test_03"], + } + } + }, + "id": "F4Go_FYpcB0", + "type": "ci_app_libraries_tests", + } + }, + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + known_tests = api_client.get_known_tests() + + assert mock_connector.post_json.call_args_list == [ + call( + "/api/v2/ci/libraries/tests", + { + "data": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "ci_app_libraries_tests_request", + "attributes": { + "service": "some-service", + "env": "some-env", + "repository_url": "http://github.com/DataDog/some-repo.git", + "configurations": {"os.platform": "Linux"}, + }, + } + }, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + assert known_tests == { + TestRef(SuiteRef(ModuleRef("some-module"), "test_simple.py"), "test_01"), + TestRef(SuiteRef(ModuleRef("some-module"), "test_simple.py"), "test_02"), + TestRef(SuiteRef(ModuleRef("some-module"), "test_second.py"), "test_01"), + TestRef(SuiteRef(ModuleRef("some-module"), "test_second.py"), "test_02"), + TestRef(SuiteRef(ModuleRef("some-module"), "test_second.py"), "test_03"), + } + + def test_get_known_tests_missing_git_data(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + known_tests = api_client.get_known_tests() + + assert "Git info not available" in caplog.text + assert mock_connector.post_json.call_args_list == [] + + assert known_tests == set() + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_get_known_tests_fail_http_request(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = Mock() + mock_connector.post_json.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + known_tests = api_client.get_known_tests() + + assert "Error getting known tests from API: No can do" in caplog.text + + assert known_tests == set() + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_get_known_tests_errors_in_response(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/ci/libraries/tests", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + known_tests = api_client.get_known_tests() + + assert "Error getting known tests from API" in caplog.text + assert "KeyError" in caplog.text + + assert known_tests == set() + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.BAD_JSON) + ] + + +class TestAPIClientGetTestManagementTests: + def test_get_test_management_tests(self, mock_telemetry: Mock) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/test/libraries/test-management/tests", + response_data={ + "data": { + "attributes": { + "modules": { + "some_module": { + "suites": { + "first.py": { + "tests": { + "test_01": { + "properties": { + "attempt_to_fix": False, + "disabled": False, + "quarantined": True, + } + } + } + }, + "second.py": { + "tests": { + "test_02": { + "properties": { + "attempt_to_fix": False, + "disabled": True, + "quarantined": False, + } + } + } + }, + } + } + } + }, + "id": "e7e4d0b95cb68806", + "type": "ci_app_libraries_tests", + } + }, + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + properties = api_client.get_test_management_properties() + + assert mock_connector.post_json.call_args_list == [ + call( + "/api/v2/test/libraries/test-management/tests", + { + "data": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "ci_app_libraries_tests_request", + "attributes": { + "repository_url": "http://github.com/DataDog/some-repo.git", + "commit_message": "I am a commit", + "sha": "abcd1234", + }, + } + }, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + assert properties == { + TestRef(SuiteRef(ModuleRef("some_module"), "first.py"), "test_01"): TestProperties( + quarantined=True, disabled=False, attempt_to_fix=False + ), + TestRef(SuiteRef(ModuleRef("some_module"), "second.py"), "test_02"): TestProperties( + quarantined=False, disabled=True, attempt_to_fix=False + ), + } + + def test_get_test_management_properties_missing_git_data( + self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + properties = api_client.get_test_management_properties() + + assert "Git info not available" in caplog.text + assert mock_connector.post_json.call_args_list == [] + + assert properties == {} + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_get_test_management_properties_fail_http_request( + self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + mock_connector = Mock() + mock_connector.post_json.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + properties = api_client.get_test_management_properties() + + assert "Error getting Test Management properties from API" in caplog.text + + assert properties == {} + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_get_test_management_tests_errors_in_response( + self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/test/libraries/test-management/tests", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + properties = api_client.get_test_management_properties() + + assert "Failed to parse Test Management tests data from API" in caplog.text + assert "KeyError" in caplog.text + + assert properties == {} + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.BAD_JSON) + ] + + +class TestAPIClientGetKnownCommits: + def test_get_known_commits(self, mock_telemetry: Mock) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/git/repository/search_commits", + response_data={"data": [{"id": "abcd0123", "type": "commit"}, {"id": "dcba4321", "type": "commit"}]}, + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + commits = api_client.get_known_commits(latest_commits=["0000abcd", "1111abcd"]) + + assert mock_connector.post_json.call_args_list == [ + call( + "/api/v2/git/repository/search_commits", + { + "meta": {"repository_url": "http://github.com/DataDog/some-repo.git"}, + "data": [{"id": "0000abcd", "type": "commit"}, {"id": "1111abcd", "type": "commit"}], + }, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + assert commits == ["abcd0123", "dcba4321"] + + def test_get_known_commits_missing_git_data(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + commits = api_client.get_known_commits(latest_commits=["0000abcd", "1111abcd"]) + + assert "Git info not available" in caplog.text + assert mock_connector.post_json.call_args_list == [] + + assert commits == [] + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_get_known_commits_fail_http_request(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = Mock() + mock_connector.post_json.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + commits = api_client.get_known_commits(latest_commits=["0000abcd", "1111abcd"]) + + assert "Error getting known commits from API" in caplog.text + + assert commits == [] + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_get_known_commits_errors_in_response(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/git/repository/search_commits", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + commits = api_client.get_known_commits(latest_commits=["0000abcd", "1111abcd"]) + + assert "Failed to parse search_commits data" in caplog.text + assert "KeyError" in caplog.text + + assert commits == [] + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.BAD_JSON) + ] + + +class TestAPIClientGetSkippableTests: + def test_get_skippable_tests(self, mock_telemetry: Mock) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/ci/tests/skippable", + response_data={ + "data": [ + { + "attributes": { + "configurations": {"test.bundle": "tests"}, + "name": "test_01", + "suite": "test_simple.py", + }, + "id": "b64c9cec67b328f2", + "type": "test", + }, + { + "attributes": { + "configurations": {"test.bundle": "tests"}, + "name": "test_02", + "suite": "test_second.py", + }, + "id": "87197af576c002b3", + "type": "test", + }, + ], + "meta": { + "correlation_id": "8ac307ca693b2ffd365ab2c3b47cb555", + "coverage": { + "tests/test_second.py": "AAABpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "tests/test_simple.py": "XYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + }, + }, + }, + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + skippable_tests, correlation_id = api_client.get_skippable_tests() + + assert mock_connector.post_json.call_args_list == [ + call( + "/api/v2/ci/tests/skippable", + { + "data": { + "id": "00000000-0000-0000-0000-000000000000", + "type": "test_params", + "attributes": { + "service": "some-service", + "env": "some-env", + "repository_url": "http://github.com/DataDog/some-repo.git", + "sha": "abcd1234", + "configurations": {"os.platform": "Linux"}, + "test_level": "test", + }, + } + }, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + assert skippable_tests == { + TestRef(SuiteRef(ModuleRef("tests"), "test_simple.py"), "test_01"), + TestRef(SuiteRef(ModuleRef("tests"), "test_second.py"), "test_02"), + } + assert correlation_id == "8ac307ca693b2ffd365ab2c3b47cb555" + + def test_get_skippable_tests_missing_git_data(self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + skippable_tests, correlation_id = api_client.get_skippable_tests() + + assert "Git info not available" in caplog.text + assert mock_connector.post_json.call_args_list == [] + + assert skippable_tests == set() + assert correlation_id is None + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_get_skippable_tests_fail_http_request( + self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + mock_connector = Mock() + mock_connector.post_json.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + skippable_tests, correlation_id = api_client.get_skippable_tests() + + assert "Error getting skippable tests from API" in caplog.text + + assert skippable_tests == set() + assert correlation_id is None + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_get_skippable_tests_errors_in_response( + self, mock_telemetry: Mock, caplog: pytest.LogCaptureFixture + ) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/ci/tests/skippable", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + skippable_tests, correlation_id = api_client.get_skippable_tests() + + assert "Failed to parse skippable tests data" in caplog.text + assert "KeyError" in caplog.text + + assert skippable_tests == set() + assert correlation_id is None + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.BAD_JSON) + ] + + +@pytest.fixture +def packfile(tmpdir: t.Any) -> Path: + path = Path(str(tmpdir)) / "file.pack" + path.write_text("twelve bytes") + yield path + + +class TestAPIClientSendGitPackfile: + def test_send_git_pack_file(self, mock_telemetry: Mock, packfile: Path) -> None: + mock_connector = Mock() + mock_connector.post_files.return_value = BackendResult(response=Mock(status=200)) + + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + api_client.send_git_pack_file(packfile) + + assert mock_connector.post_files.call_args_list == [ + call( + "/api/v2/git/repository/packfile", + files=[ + FileAttachment( + name="pushedSha", + filename=None, + content_type="application/json", + data=json.dumps( + { + "data": {"id": "abcd1234", "type": "commit"}, + "meta": {"repository_url": "http://github.com/DataDog/some-repo.git"}, + } + ).encode("utf-8"), + ), + FileAttachment( + name="packfile", + filename="file.pack", + content_type="application/octet-stream", + data=b"twelve bytes", + ), + ], + send_gzip=False, + telemetry=mock_telemetry.with_request_metric_names.return_value, + ) + ] + + def test_send_git_pack_file_missing_git_data( + self, + mock_telemetry: Mock, + caplog: pytest.LogCaptureFixture, + packfile: Path, + ) -> None: + mock_connector = mock_backend_connector().build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={}, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + api_client.send_git_pack_file(packfile) + + assert "Git info not available" in caplog.text + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] + + def test_send_git_pack_file_fail_http_request( + self, + mock_telemetry: Mock, + caplog: pytest.LogCaptureFixture, + packfile: Path, + ) -> None: + mock_connector = Mock() + mock_connector.post_files.return_value = BackendResult( + error_type=ErrorType.UNKNOWN, error_description="No can do" + ) + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + api_client.send_git_pack_file(packfile) + + assert "Failed to upload Git pack data" in caplog.text + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [] + + def test_send_git_pack_file_errors_in_reading( + self, + mock_telemetry: Mock, + caplog: pytest.LogCaptureFixture, + tmpdir: t.Any, + ) -> None: + mock_connector = ( + mock_backend_connector().with_post_json_response( + endpoint="/api/v2/ci/tests/skippable", response_data={"errors": "Weird stuff"} + ) + ).build() + mock_connector_setup = Mock() + mock_connector_setup.get_connector_for_subdomain.return_value = mock_connector + + api_client = APIClient( + service="some-service", + env="some-env", + env_tags={ + GitTag.REPOSITORY_URL: "http://github.com/DataDog/some-repo.git", + GitTag.COMMIT_SHA: "abcd1234", + GitTag.BRANCH: "some-branch", + GitTag.COMMIT_MESSAGE: "I am a commit", + }, + itr_skipping_level=ITRSkippingLevel.TEST, + configurations={ + "os.platform": "Linux", + }, + connector_setup=mock_connector_setup, + telemetry_api=mock_telemetry, + ) + + with patch("uuid.uuid4", return_value=uuid.UUID("00000000-0000-0000-0000-000000000000")): + with caplog.at_level(level=logging.INFO, logger="ddtrace.testing"): + api_client.send_git_pack_file(Path(tmpdir) / "non_existent_file.pack") + + assert "Error sending Git pack data" in caplog.text + + assert mock_telemetry.with_request_metric_names.return_value.record_error.call_args_list == [ + call(ErrorType.UNKNOWN) + ] diff --git a/tests/testing/internal/test_telemetry.py b/tests/testing/internal/test_telemetry.py index 5e412ce5d34..27eb24f2041 100644 --- a/tests/testing/internal/test_telemetry.py +++ b/tests/testing/internal/test_telemetry.py @@ -487,3 +487,11 @@ def test_record_session_finished(self, telemetry_api: TelemetryAPI) -> None: ), ) ] + + def test_record_git_pack_data(self, telemetry_api: TelemetryAPI) -> None: + telemetry_api.record_git_pack_data(uploaded_files=5, uploaded_bytes=200) + + assert telemetry_api.writer.add_distribution_metric.call_args_list == [ + call(CIVISIBILITY, "git_requests.objects_pack_files", 5, ()), + call(CIVISIBILITY, "git_requests.objects_pack_bytes", 200, ()), + ] diff --git a/tests/testing/mocks.py b/tests/testing/mocks.py index 5c87e5d4df0..a1bc27d657d 100644 --- a/tests/testing/mocks.py +++ b/tests/testing/mocks.py @@ -456,7 +456,7 @@ def build(self) -> Mock: mock_connector = Mock() # Mock methods to prevent real HTTP calls - def mock_post_json(endpoint: str, data: t.Any) -> t.Tuple[Mock, t.Any]: + def mock_post_json(endpoint: str, data: t.Any, telemetry: t.Any = None) -> t.Tuple[Mock, t.Any]: if endpoint in self._post_json_responses: return BackendResult(response=Mock(status=200), parsed_response=self._post_json_responses[endpoint]) return self._make_404_response()