datadog-ci 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -2
- data/lib/datadog/ci/code_coverage/component.rb +55 -0
- data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
- data/lib/datadog/ci/code_coverage/transport.rb +66 -0
- data/lib/datadog/ci/configuration/components.rb +17 -1
- data/lib/datadog/ci/configuration/settings.rb +20 -2
- data/lib/datadog/ci/contrib/minitest/test.rb +1 -1
- data/lib/datadog/ci/contrib/rspec/example.rb +48 -8
- data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
- data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
- data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
- data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
- data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
- data/lib/datadog/ci/ext/environment.rb +10 -0
- data/lib/datadog/ci/ext/settings.rb +2 -0
- data/lib/datadog/ci/ext/telemetry.rb +5 -0
- data/lib/datadog/ci/ext/test.rb +0 -5
- data/lib/datadog/ci/ext/transport.rb +4 -0
- data/lib/datadog/ci/git/cli.rb +59 -1
- data/lib/datadog/ci/remote/component.rb +6 -1
- data/lib/datadog/ci/remote/library_settings.rb +8 -0
- data/lib/datadog/ci/test.rb +27 -18
- data/lib/datadog/ci/test_management/component.rb +9 -0
- data/lib/datadog/ci/test_management/null_component.rb +8 -0
- data/lib/datadog/ci/test_optimisation/component.rb +168 -16
- data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
- data/lib/datadog/ci/test_suite.rb +16 -21
- data/lib/datadog/ci/test_visibility/component.rb +1 -2
- data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
- data/lib/datadog/ci/transport/api/agentless.rb +8 -1
- data/lib/datadog/ci/transport/api/base.rb +21 -0
- data/lib/datadog/ci/transport/api/builder.rb +5 -1
- data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +5 -1
|
@@ -15,6 +15,13 @@ module Datadog
|
|
|
15
15
|
# Github Actions: https://github.com/features/actions
|
|
16
16
|
# Environment variables docs: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
|
|
17
17
|
class GithubActions < Base
|
|
18
|
+
# Paths to GitHub Actions runner diagnostics folder
|
|
19
|
+
# GitHub-hosted (SaaS) runners use the cached path, self-hosted runners use the non-cached path
|
|
20
|
+
GITHUB_RUNNER_DIAG_PATHS = [
|
|
21
|
+
"/home/runner/actions-runner/cached/_diag", # GitHub-hosted (SaaS) runners
|
|
22
|
+
"/home/runner/actions-runner/_diag" # Self-hosted runners
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
18
25
|
def self.handles?(env)
|
|
19
26
|
env.key?("GITHUB_SHA")
|
|
20
27
|
end
|
|
@@ -28,11 +35,16 @@ module Datadog
|
|
|
28
35
|
end
|
|
29
36
|
|
|
30
37
|
def job_url
|
|
31
|
-
|
|
38
|
+
numeric_id = numeric_job_id
|
|
39
|
+
if numeric_id
|
|
40
|
+
"#{github_server_url}/#{env["GITHUB_REPOSITORY"]}/actions/runs/#{env["GITHUB_RUN_ID"]}/job/#{numeric_id}"
|
|
41
|
+
else
|
|
42
|
+
"#{github_server_url}/#{env["GITHUB_REPOSITORY"]}/commit/#{env["GITHUB_SHA"]}/checks"
|
|
43
|
+
end
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
def job_id
|
|
35
|
-
env["GITHUB_JOB"]
|
|
47
|
+
numeric_job_id || env["GITHUB_JOB"]
|
|
36
48
|
end
|
|
37
49
|
|
|
38
50
|
def pipeline_id
|
|
@@ -141,6 +153,57 @@ module Datadog
|
|
|
141
153
|
|
|
142
154
|
@github_server_url ||= Datadog::Core::Utils::Url.filter_basic_auth(env["GITHUB_SERVER_URL"])
|
|
143
155
|
end
|
|
156
|
+
|
|
157
|
+
# Returns numeric job ID from environment variable or runner diagnostics.
|
|
158
|
+
# Priority:
|
|
159
|
+
# 1. JOB_CHECK_RUN_ID environment variable (GitHub Actions feature pending)
|
|
160
|
+
# 2. Worker_*.log files in the runner's _diag folder (fallback)
|
|
161
|
+
def numeric_job_id
|
|
162
|
+
return @numeric_job_id if defined?(@numeric_job_id)
|
|
163
|
+
|
|
164
|
+
@numeric_job_id = env["JOB_CHECK_RUN_ID"] || extract_numeric_job_id_from_diag_files
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def extract_numeric_job_id_from_diag_files
|
|
168
|
+
GITHUB_RUNNER_DIAG_PATHS.each do |diag_path|
|
|
169
|
+
next unless Dir.exist?(diag_path)
|
|
170
|
+
|
|
171
|
+
worker_files = Dir.glob(File.join(diag_path, "Worker_*.log")).sort
|
|
172
|
+
next if worker_files.empty?
|
|
173
|
+
|
|
174
|
+
# Use the most recent worker file (last in sorted order)
|
|
175
|
+
worker_file = worker_files.last
|
|
176
|
+
check_run_id = extract_check_run_id_from_worker_file(worker_file)
|
|
177
|
+
return check_run_id if check_run_id
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
nil
|
|
181
|
+
rescue => e
|
|
182
|
+
Datadog.logger.debug("Failed to extract numeric job ID from GitHub Actions runner diagnostics: #{e}")
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Regex to extract check_run_id value from Worker log files.
|
|
187
|
+
# The log format varies between GitHub-hosted and self-hosted runners,
|
|
188
|
+
# so we use regex instead of JSON parsing for robustness.
|
|
189
|
+
# Matches patterns like: "k": "check_run_id" ... "v": 12345 or "v": 12345.0
|
|
190
|
+
CHECK_RUN_ID_REGEX = /"k":\s*"check_run_id"[^}]*"v":\s*(\d+)(?:\.\d+)?/
|
|
191
|
+
|
|
192
|
+
def extract_check_run_id_from_worker_file(file_path)
|
|
193
|
+
return nil unless File.exist?(file_path)
|
|
194
|
+
|
|
195
|
+
content = File.read(file_path)
|
|
196
|
+
|
|
197
|
+
# Find all check_run_id values in the file.
|
|
198
|
+
# On self-hosted runners, Worker_*.log files can be appended across multiple jobs,
|
|
199
|
+
# so we use the last match to get the current job's ID.
|
|
200
|
+
# flatten because scan with capture groups returns Array[Array[String]]
|
|
201
|
+
matches = content.scan(CHECK_RUN_ID_REGEX).flatten
|
|
202
|
+
matches.last
|
|
203
|
+
rescue => e
|
|
204
|
+
Datadog.logger.debug("Failed to parse Worker log file #{file_path}: #{e}")
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
144
207
|
end
|
|
145
208
|
end
|
|
146
209
|
end
|
|
@@ -53,6 +53,14 @@ module Datadog
|
|
|
53
53
|
module_function
|
|
54
54
|
|
|
55
55
|
def tags(env)
|
|
56
|
+
@tags ||= extract_tags(env).freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def reset!
|
|
60
|
+
@tags = nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_tags(env)
|
|
56
64
|
# Extract metadata from CI provider environment variables
|
|
57
65
|
tags = Environment::Extractor.new(env).tags
|
|
58
66
|
|
|
@@ -91,6 +99,8 @@ module Datadog
|
|
|
91
99
|
tags
|
|
92
100
|
end
|
|
93
101
|
|
|
102
|
+
private_class_method :extract_tags
|
|
103
|
+
|
|
94
104
|
def ensure_post_conditions(tags)
|
|
95
105
|
validate_repository_url(tags[Git::TAG_REPOSITORY_URL])
|
|
96
106
|
validate_git_sha(tags[Git::TAG_COMMIT_SHA])
|
|
@@ -6,6 +6,7 @@ module Datadog
|
|
|
6
6
|
# Defines constants for test tags
|
|
7
7
|
module Settings
|
|
8
8
|
ENV_MODE_ENABLED = "DD_TRACE_CI_ENABLED"
|
|
9
|
+
ENV_ENABLED = "DD_CIVISIBILITY_ENABLED"
|
|
9
10
|
ENV_AGENTLESS_MODE_ENABLED = "DD_CIVISIBILITY_AGENTLESS_ENABLED"
|
|
10
11
|
ENV_AGENTLESS_URL = "DD_CIVISIBILITY_AGENTLESS_URL"
|
|
11
12
|
ENV_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED = "DD_CIVISIBILITY_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED"
|
|
@@ -30,6 +31,7 @@ module Datadog
|
|
|
30
31
|
ENV_TEST_DISCOVERY_OUTPUT_PATH = "DD_TEST_OPTIMIZATION_DISCOVERY_FILE"
|
|
31
32
|
ENV_AUTO_INSTRUMENTATION_PROVIDER = "DD_CIVISIBILITY_AUTO_INSTRUMENTATION_PROVIDER"
|
|
32
33
|
ENV_TIA_STATIC_DEPENDENCIES_TRACKING_ENABLED = "DD_TEST_OPTIMIZATION_TIA_STATIC_DEPS_COVERAGE_ENABLED"
|
|
34
|
+
ENV_CODE_COVERAGE_REPORT_UPLOAD_ENABLED = "DD_CIVISIBILITY_CODE_COVERAGE_REPORT_UPLOAD_ENABLED"
|
|
33
35
|
|
|
34
36
|
# Source: https://docs.datadoghq.com/getting_started/site/
|
|
35
37
|
DD_SITE_ALLOWLIST = %w[
|
|
@@ -54,6 +54,11 @@ module Datadog
|
|
|
54
54
|
METRIC_CODE_COVERAGE_IS_EMPTY = "code_coverage.is_empty"
|
|
55
55
|
METRIC_CODE_COVERAGE_FILES = "code_coverage.files"
|
|
56
56
|
|
|
57
|
+
METRIC_COVERAGE_UPLOAD_REQUEST = "coverage_upload.request"
|
|
58
|
+
METRIC_COVERAGE_UPLOAD_REQUEST_ERRORS = "coverage_upload.request_errors"
|
|
59
|
+
METRIC_COVERAGE_UPLOAD_REQUEST_MS = "coverage_upload.request_ms"
|
|
60
|
+
METRIC_COVERAGE_UPLOAD_REQUEST_BYTES = "coverage_upload.request_bytes"
|
|
61
|
+
|
|
57
62
|
METRIC_KNOWN_TESTS_REQUEST = "known_tests.request"
|
|
58
63
|
METRIC_KNOWN_TESTS_REQUEST_MS = "known_tests.request_ms"
|
|
59
64
|
METRIC_KNOWN_TESTS_REQUEST_ERRORS = "known_tests.request_errors"
|
data/lib/datadog/ci/ext/test.rb
CHANGED
|
@@ -155,11 +155,6 @@ module Datadog
|
|
|
155
155
|
SKIP = "skip"
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
# test statuses that we use for execution stats but don't report to Datadog (e.g. fail_ignored)
|
|
159
|
-
module ExecutionStatsStatus
|
|
160
|
-
FAIL_IGNORED = "fail_ignored"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
158
|
# test types (e.g. test, benchmark, browser)
|
|
164
159
|
module Type
|
|
165
160
|
TEST = "test"
|
|
@@ -28,6 +28,9 @@ module Datadog
|
|
|
28
28
|
TEST_COVERAGE_INTAKE_HOST_PREFIX = "citestcov-intake"
|
|
29
29
|
TEST_COVERAGE_INTAKE_PATH = "/api/v2/citestcov"
|
|
30
30
|
|
|
31
|
+
CODE_COVERAGE_REPORT_INTAKE_HOST_PREFIX = "ci-intake"
|
|
32
|
+
CODE_COVERAGE_REPORT_INTAKE_PATH = "/api/v2/cicovreprt"
|
|
33
|
+
|
|
31
34
|
LOGS_INTAKE_HOST_PREFIX = "http-intake.logs"
|
|
32
35
|
|
|
33
36
|
DD_API_HOST_PREFIX = "api"
|
|
@@ -49,6 +52,7 @@ module Datadog
|
|
|
49
52
|
DD_API_SETTINGS_RESPONSE_ATTEMPT_TO_FIX_RETRIES_KEY = "attempt_to_fix_retries"
|
|
50
53
|
DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze
|
|
51
54
|
DD_API_SETTINGS_RESPONSE_IMPACTED_TESTS_ENABLED_KEY = "impacted_tests_enabled"
|
|
55
|
+
DD_API_SETTINGS_RESPONSE_COVERAGE_REPORT_UPLOAD_KEY = "coverage_report_upload_enabled"
|
|
52
56
|
|
|
53
57
|
DD_API_GIT_SEARCH_COMMITS_PATH = "/api/v2/git/repository/search_commits"
|
|
54
58
|
|
data/lib/datadog/ci/git/cli.rb
CHANGED
|
@@ -25,6 +25,10 @@ module Datadog
|
|
|
25
25
|
|
|
26
26
|
# Execute a git command with optional stdin input and timeout
|
|
27
27
|
#
|
|
28
|
+
# All git commands are executed with the `-c safe.directory` option
|
|
29
|
+
# to handle cases where the repository is owned by a different user
|
|
30
|
+
# (common in CI environments with containerized builds).
|
|
31
|
+
#
|
|
28
32
|
# @param cmd [Array<String>] The git command as an array of strings
|
|
29
33
|
# @param stdin [String, nil] Optional stdin data to pass to the command
|
|
30
34
|
# @param timeout [Integer] Timeout in seconds for the command execution
|
|
@@ -33,7 +37,11 @@ module Datadog
|
|
|
33
37
|
def self.exec_git_command(cmd, stdin: nil, timeout: SHORT_TIMEOUT)
|
|
34
38
|
# @type var out: String
|
|
35
39
|
# @type var status: Process::Status?
|
|
36
|
-
out, status = Utils::Command.exec_command(
|
|
40
|
+
out, status = Utils::Command.exec_command(
|
|
41
|
+
["git", "-c", "safe.directory=#{safe_directory}"] + cmd,
|
|
42
|
+
stdin_data: stdin,
|
|
43
|
+
timeout: timeout
|
|
44
|
+
)
|
|
37
45
|
|
|
38
46
|
if status.nil? || !status.success?
|
|
39
47
|
# Convert command to string representation for error message
|
|
@@ -50,6 +58,56 @@ module Datadog
|
|
|
50
58
|
|
|
51
59
|
out
|
|
52
60
|
end
|
|
61
|
+
|
|
62
|
+
# Returns the directory to use for git's safe.directory config.
|
|
63
|
+
# This is cached to avoid repeated filesystem lookups.
|
|
64
|
+
#
|
|
65
|
+
# Traverses up from current directory to find the nearest .git folder
|
|
66
|
+
# and returns its parent (the repository root). Falls back to current
|
|
67
|
+
# working directory if no .git folder is found.
|
|
68
|
+
#
|
|
69
|
+
# @return [String] The safe directory path
|
|
70
|
+
def self.safe_directory
|
|
71
|
+
return @safe_directory if defined?(@safe_directory)
|
|
72
|
+
|
|
73
|
+
@safe_directory = find_git_directory(Dir.pwd)
|
|
74
|
+
Datadog.logger.debug { "Git safe.directory configured to: #{@safe_directory}" }
|
|
75
|
+
@safe_directory
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Traverses up from the given directory to find the nearest .git folder or file.
|
|
79
|
+
# Returns the repository root (parent of .git) if found, otherwise the original directory.
|
|
80
|
+
#
|
|
81
|
+
# Note: .git can be either a directory (regular repos) or a file (worktrees/submodules).
|
|
82
|
+
# In worktrees and submodules, .git is a file containing a pointer to the actual git directory.
|
|
83
|
+
#
|
|
84
|
+
# @param start_dir [String] The directory to start searching from
|
|
85
|
+
# @return [String] The repository root path or the start directory if not found
|
|
86
|
+
def self.find_git_directory(start_dir)
|
|
87
|
+
Datadog.logger.debug { "Searching for .git starting from: #{start_dir}" }
|
|
88
|
+
current_dir = File.expand_path(start_dir)
|
|
89
|
+
|
|
90
|
+
loop do
|
|
91
|
+
git_path = File.join(current_dir, ".git")
|
|
92
|
+
|
|
93
|
+
# Check for both directory (.git in regular repos) and file (.git in worktrees/submodules)
|
|
94
|
+
if File.exist?(git_path)
|
|
95
|
+
Datadog.logger.debug { "Found .git at: #{git_path} (#{File.directory?(git_path) ? "directory" : "file"})" }
|
|
96
|
+
return current_dir
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
parent_dir = File.dirname(current_dir)
|
|
100
|
+
|
|
101
|
+
# Reached the root directory
|
|
102
|
+
break if parent_dir == current_dir
|
|
103
|
+
|
|
104
|
+
current_dir = parent_dir
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Fallback to original directory if no .git found
|
|
108
|
+
Datadog.logger.debug { "No .git found, using fallback: #{start_dir}" }
|
|
109
|
+
start_dir
|
|
110
|
+
end
|
|
53
111
|
end
|
|
54
112
|
end
|
|
55
113
|
end
|
|
@@ -29,7 +29,8 @@ module Datadog
|
|
|
29
29
|
Worker.new { test_retries.configure(@library_configuration, test_session) },
|
|
30
30
|
Worker.new { test_visibility.configure(@library_configuration, test_session) },
|
|
31
31
|
Worker.new { test_management.configure(@library_configuration, test_session) },
|
|
32
|
-
Worker.new { impacted_tests_detection.configure(@library_configuration, test_session) }
|
|
32
|
+
Worker.new { impacted_tests_detection.configure(@library_configuration, test_session) },
|
|
33
|
+
Worker.new { code_coverage.configure(@library_configuration) }
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
# launch configuration workers
|
|
@@ -123,6 +124,10 @@ module Datadog
|
|
|
123
124
|
def git_tree_upload_worker
|
|
124
125
|
Datadog.send(:components).git_tree_upload_worker
|
|
125
126
|
end
|
|
127
|
+
|
|
128
|
+
def code_coverage
|
|
129
|
+
Datadog.send(:components).code_coverage
|
|
130
|
+
end
|
|
126
131
|
end
|
|
127
132
|
end
|
|
128
133
|
end
|
|
@@ -117,6 +117,14 @@ module Datadog
|
|
|
117
117
|
)
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
def coverage_report_upload_enabled?
|
|
121
|
+
return @coverage_report_upload_enabled if defined?(@coverage_report_upload_enabled)
|
|
122
|
+
|
|
123
|
+
@coverage_report_upload_enabled = Utils::Parsing.convert_to_bool(
|
|
124
|
+
payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_COVERAGE_REPORT_UPLOAD_KEY, false)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
120
128
|
def slow_test_retries
|
|
121
129
|
return @slow_test_retries if defined?(@slow_test_retries)
|
|
122
130
|
|
data/lib/datadog/ci/test.rb
CHANGED
|
@@ -12,6 +12,11 @@ module Datadog
|
|
|
12
12
|
#
|
|
13
13
|
# @public_api
|
|
14
14
|
class Test < Span
|
|
15
|
+
# Context IDs for this test (used for TIA context coverage merging).
|
|
16
|
+
# Contains list of context identifiers from outermost to innermost.
|
|
17
|
+
# @return [Array<String>] list of context IDs
|
|
18
|
+
attr_accessor :context_ids
|
|
19
|
+
|
|
15
20
|
# @return [String] the name of the test.
|
|
16
21
|
def name
|
|
17
22
|
get_tag(Ext::Test::TAG_NAME)
|
|
@@ -166,13 +171,7 @@ module Datadog
|
|
|
166
171
|
def failed!(exception: nil)
|
|
167
172
|
super
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
if should_ignore_failures?
|
|
171
|
-
# use a special "fail_ignored" status to mark this test as failed but ignored
|
|
172
|
-
record_test_result(Ext::Test::ExecutionStatsStatus::FAIL_IGNORED)
|
|
173
|
-
else
|
|
174
|
-
record_test_result(Ext::Test::Status::FAIL)
|
|
175
|
-
end
|
|
174
|
+
record_test_result(Ext::Test::Status::FAIL)
|
|
176
175
|
end
|
|
177
176
|
|
|
178
177
|
# Sets the status of the span to "skip".
|
|
@@ -236,7 +235,10 @@ module Datadog
|
|
|
236
235
|
|
|
237
236
|
# @internal
|
|
238
237
|
def should_ignore_failures?
|
|
239
|
-
quarantined? || disabled?
|
|
238
|
+
return true if quarantined? || disabled?
|
|
239
|
+
return false if attempt_to_fix?
|
|
240
|
+
|
|
241
|
+
any_retry_passed?
|
|
240
242
|
end
|
|
241
243
|
|
|
242
244
|
# @internal
|
|
@@ -249,16 +251,9 @@ module Datadog
|
|
|
249
251
|
status = get_tag(Ext::Test::TAG_STATUS)
|
|
250
252
|
return if status.nil?
|
|
251
253
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
if should_ignore_failures?
|
|
258
|
-
set_tag(Ext::Test::TAG_FINAL_STATUS, Ext::Test::Status::PASS)
|
|
259
|
-
else
|
|
260
|
-
set_tag(Ext::Test::TAG_FINAL_STATUS, Ext::Test::Status::FAIL)
|
|
261
|
-
end
|
|
254
|
+
final_status = compute_final_status(status)
|
|
255
|
+
set_tag(Ext::Test::TAG_FINAL_STATUS, final_status)
|
|
256
|
+
test_suite&.record_test_final_status(datadog_test_id, final_status)
|
|
262
257
|
end
|
|
263
258
|
|
|
264
259
|
# @internal
|
|
@@ -272,6 +267,20 @@ module Datadog
|
|
|
272
267
|
|
|
273
268
|
private
|
|
274
269
|
|
|
270
|
+
def compute_final_status(status)
|
|
271
|
+
# Skip status is always preserved
|
|
272
|
+
return status if status == Ext::Test::Status::SKIP
|
|
273
|
+
|
|
274
|
+
# For attempt_to_fix tests (not quarantined/disabled), any failure means the fix didn't work
|
|
275
|
+
if attempt_to_fix? && !quarantined? && !disabled?
|
|
276
|
+
return all_executions_passed? ? Ext::Test::Status::PASS : Ext::Test::Status::FAIL
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
return status if status == Ext::Test::Status::PASS
|
|
280
|
+
|
|
281
|
+
should_ignore_failures? ? Ext::Test::Status::PASS : Ext::Test::Status::FAIL
|
|
282
|
+
end
|
|
283
|
+
|
|
275
284
|
def record_test_result(datadog_status)
|
|
276
285
|
# if this test was already executed in this test suite, mark it as retried
|
|
277
286
|
if test_suite&.test_executed?(datadog_test_id)
|
|
@@ -75,6 +75,15 @@ module Datadog
|
|
|
75
75
|
test_properties.fetch("attempt_to_fix", false)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
def disabled?(datadog_fqn_test_id)
|
|
79
|
+
return false unless @enabled
|
|
80
|
+
|
|
81
|
+
test_properties = @tests_properties[datadog_fqn_test_id]
|
|
82
|
+
return false if test_properties.nil?
|
|
83
|
+
|
|
84
|
+
test_properties.fetch("disabled", false)
|
|
85
|
+
end
|
|
86
|
+
|
|
78
87
|
def restore_state_from_datadog_test_runner
|
|
79
88
|
Datadog.logger.debug { "Restoring test management tests from DDTest cache" }
|
|
80
89
|
|
|
@@ -69,6 +69,16 @@ module Datadog
|
|
|
69
69
|
|
|
70
70
|
@mutex = Mutex.new
|
|
71
71
|
|
|
72
|
+
# Context coverage: stores coverage collected during before(:context)/before(:all) hooks
|
|
73
|
+
# keyed by context_id (e.g., RSpec scoped_id for example groups)
|
|
74
|
+
# Only used when use_single_threaded_coverage is false (multi-threaded mode)
|
|
75
|
+
@context_coverages = {}
|
|
76
|
+
@context_coverages_mutex = Mutex.new
|
|
77
|
+
|
|
78
|
+
# Currently active context ID for context coverage collection
|
|
79
|
+
@current_context_id = nil
|
|
80
|
+
@current_context_id_mutex = Mutex.new
|
|
81
|
+
|
|
72
82
|
Datadog.logger.debug("TestOptimisation initialized with enabled: #{@enabled}")
|
|
73
83
|
end
|
|
74
84
|
|
|
@@ -117,24 +127,105 @@ module Datadog
|
|
|
117
127
|
@code_coverage_enabled
|
|
118
128
|
end
|
|
119
129
|
|
|
120
|
-
|
|
130
|
+
# Starts coverage collection.
|
|
131
|
+
# This is a low-level method that only starts the collector.
|
|
132
|
+
#
|
|
133
|
+
# @return [void]
|
|
134
|
+
def start_coverage
|
|
121
135
|
return if !enabled? || !code_coverage?
|
|
122
136
|
|
|
123
|
-
Telemetry.code_coverage_started(test)
|
|
124
137
|
coverage_collector&.start
|
|
125
138
|
end
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
# Stops coverage collection and returns raw coverage data.
|
|
141
|
+
# This is a low-level method that only stops the collector.
|
|
142
|
+
#
|
|
143
|
+
# @return [Hash, nil] Raw coverage data or nil
|
|
144
|
+
def stop_coverage
|
|
145
|
+
return if !enabled? || !code_coverage?
|
|
146
|
+
|
|
147
|
+
coverage_collector&.stop
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Called when a test context (e.g., RSpec example group with before(:context)) starts.
|
|
151
|
+
# Starts collecting coverage that will be merged into all tests within this context.
|
|
152
|
+
#
|
|
153
|
+
# @param context_id [String] A stable identifier for the context (e.g., RSpec scoped_id)
|
|
154
|
+
# @return [void]
|
|
155
|
+
def on_test_context_started(context_id)
|
|
156
|
+
return unless context_coverage_enabled?
|
|
157
|
+
|
|
158
|
+
# Stop and store any existing context coverage before starting new one.
|
|
159
|
+
# This ensures that outer context coverage is preserved when nested contexts start.
|
|
160
|
+
stop_context_coverage_and_store
|
|
161
|
+
|
|
162
|
+
Datadog.logger.debug { "Starting context coverage collection for context [#{context_id}]" }
|
|
163
|
+
|
|
164
|
+
# Store the context_id we're collecting for
|
|
165
|
+
@current_context_id_mutex.synchronize do
|
|
166
|
+
@current_context_id = context_id
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
coverage_collector&.start
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Called when a test starts within a context. This method:
|
|
173
|
+
# 1. Stops any in-progress context coverage collection and stores it
|
|
174
|
+
# 2. Starts coverage collection for the test itself
|
|
175
|
+
#
|
|
176
|
+
# @param test [Datadog::CI::Test] The test that is starting
|
|
177
|
+
# @return [void]
|
|
178
|
+
def on_test_started(test)
|
|
128
179
|
return if !enabled? || !code_coverage?
|
|
129
180
|
|
|
181
|
+
# Stop any in-progress context coverage and store it
|
|
182
|
+
stop_context_coverage_and_store
|
|
183
|
+
|
|
184
|
+
Telemetry.code_coverage_started(test)
|
|
185
|
+
|
|
186
|
+
context_ids = test.context_ids || []
|
|
187
|
+
|
|
188
|
+
Datadog.logger.debug do
|
|
189
|
+
"Starting test coverage for [#{test.name}] with context chain: #{context_ids.inspect}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
coverage_collector&.start
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Called when a test finishes. This method:
|
|
196
|
+
# 1. Stops test coverage collection
|
|
197
|
+
# 2. Merges context coverage from all relevant contexts
|
|
198
|
+
# 3. Writes the combined coverage event
|
|
199
|
+
# 4. Records ITR statistics if test was skipped by TIA
|
|
200
|
+
#
|
|
201
|
+
# @param test [Datadog::CI::Test] The test that finished
|
|
202
|
+
# @param context [Datadog::CI::TestVisibility::Context] The test visibility context for ITR stats
|
|
203
|
+
# @return [Datadog::CI::TestOptimisation::Coverage::Event, nil] The coverage event or nil
|
|
204
|
+
def on_test_finished(test, context)
|
|
205
|
+
return unless enabled?
|
|
206
|
+
|
|
207
|
+
# Handle ITR statistics
|
|
208
|
+
if test.skipped_by_test_impact_analysis?
|
|
209
|
+
Telemetry.itr_skipped
|
|
210
|
+
|
|
211
|
+
context.incr_tests_skipped_by_tia_count
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Handle code coverage
|
|
215
|
+
return unless code_coverage?
|
|
130
216
|
Telemetry.code_coverage_finished(test)
|
|
131
217
|
|
|
132
218
|
coverage = coverage_collector&.stop
|
|
133
219
|
|
|
134
220
|
# if test was skipped, we discard coverage data
|
|
135
221
|
return if test.skipped?
|
|
222
|
+
coverage ||= {}
|
|
223
|
+
|
|
224
|
+
# Merge context coverage from all relevant contexts
|
|
225
|
+
context_ids = test.context_ids || []
|
|
226
|
+
merge_context_coverages_into_test(coverage, context_ids)
|
|
136
227
|
|
|
137
|
-
if coverage.
|
|
228
|
+
if coverage.empty?
|
|
138
229
|
Telemetry.code_coverage_is_empty
|
|
139
230
|
return
|
|
140
231
|
end
|
|
@@ -149,18 +240,41 @@ module Datadog
|
|
|
149
240
|
|
|
150
241
|
Telemetry.code_coverage_files(coverage.size)
|
|
151
242
|
|
|
152
|
-
|
|
243
|
+
coverage_event = Coverage::Event.new(
|
|
153
244
|
test_id: test.id.to_s,
|
|
154
245
|
test_suite_id: test.test_suite_id.to_s,
|
|
155
246
|
test_session_id: test.test_session_id.to_s,
|
|
156
247
|
coverage: coverage
|
|
157
248
|
)
|
|
158
249
|
|
|
159
|
-
Datadog.logger.debug { "Writing coverage event \n #{
|
|
250
|
+
Datadog.logger.debug { "Writing coverage event \n #{coverage_event.pretty_inspect}" }
|
|
160
251
|
|
|
161
|
-
write(
|
|
252
|
+
write(coverage_event)
|
|
162
253
|
|
|
163
|
-
|
|
254
|
+
coverage_event
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Clears stored context coverage for a specific context.
|
|
258
|
+
# Should be called when a context finishes (e.g., after(:context) completes).
|
|
259
|
+
#
|
|
260
|
+
# @param context_id [String] The context ID to clear
|
|
261
|
+
# @return [void]
|
|
262
|
+
def clear_context_coverage(context_id)
|
|
263
|
+
return unless context_coverage_enabled?
|
|
264
|
+
|
|
265
|
+
@context_coverages_mutex.synchronize do
|
|
266
|
+
@context_coverages.delete(context_id)
|
|
267
|
+
|
|
268
|
+
Datadog.logger.debug { "Cleared context coverage for [#{context_id}]" }
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Returns whether context coverage collection is enabled.
|
|
273
|
+
# Context coverage is disabled in single-threaded mode.
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean]
|
|
276
|
+
def context_coverage_enabled?
|
|
277
|
+
enabled? && code_coverage? && !@use_single_threaded_coverage
|
|
164
278
|
end
|
|
165
279
|
|
|
166
280
|
def skippable?(datadog_test_id)
|
|
@@ -181,14 +295,6 @@ module Datadog
|
|
|
181
295
|
end
|
|
182
296
|
end
|
|
183
297
|
|
|
184
|
-
def on_test_finished(test, context)
|
|
185
|
-
return if !test.skipped? || !test.skipped_by_test_impact_analysis?
|
|
186
|
-
|
|
187
|
-
Telemetry.itr_skipped
|
|
188
|
-
|
|
189
|
-
context.incr_tests_skipped_by_tia_count
|
|
190
|
-
end
|
|
191
|
-
|
|
192
298
|
def write_test_session_tags(test_session, skipped_tests_count)
|
|
193
299
|
return if !enabled?
|
|
194
300
|
|
|
@@ -390,6 +496,52 @@ module Datadog
|
|
|
390
496
|
def git_tree_upload_worker
|
|
391
497
|
Datadog.send(:components).git_tree_upload_worker
|
|
392
498
|
end
|
|
499
|
+
|
|
500
|
+
# Stops any in-progress context coverage collection and stores it.
|
|
501
|
+
# Called when a test starts to capture coverage from before(:context) hooks.
|
|
502
|
+
def stop_context_coverage_and_store
|
|
503
|
+
return unless context_coverage_enabled?
|
|
504
|
+
|
|
505
|
+
context_id = @current_context_id_mutex.synchronize do
|
|
506
|
+
id = @current_context_id
|
|
507
|
+
@current_context_id = nil
|
|
508
|
+
id
|
|
509
|
+
end
|
|
510
|
+
return if context_id.nil?
|
|
511
|
+
|
|
512
|
+
coverage = coverage_collector&.stop
|
|
513
|
+
return if coverage.nil? || coverage.empty?
|
|
514
|
+
|
|
515
|
+
@context_coverages_mutex.synchronize do
|
|
516
|
+
@context_coverages[context_id] = coverage
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
Datadog.logger.debug do
|
|
520
|
+
"Stored context coverage for [#{context_id}] with #{coverage.size} files"
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Merges context coverage from all relevant contexts into the test's coverage.
|
|
525
|
+
#
|
|
526
|
+
# @param coverage [Hash] The test's coverage hash to merge into
|
|
527
|
+
# @param context_ids [Array<String>] List of context IDs to merge coverage from
|
|
528
|
+
def merge_context_coverages_into_test(coverage, context_ids)
|
|
529
|
+
return if @use_single_threaded_coverage
|
|
530
|
+
return if context_ids.empty?
|
|
531
|
+
|
|
532
|
+
@context_coverages_mutex.synchronize do
|
|
533
|
+
context_ids.each do |context_id|
|
|
534
|
+
context_coverage = @context_coverages[context_id]
|
|
535
|
+
next unless context_coverage
|
|
536
|
+
|
|
537
|
+
coverage.merge!(context_coverage)
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
Datadog.logger.debug do
|
|
542
|
+
"Merged context coverage for contexts: #{context_ids.inspect} into test coverage"
|
|
543
|
+
end
|
|
544
|
+
end
|
|
393
545
|
end
|
|
394
546
|
end
|
|
395
547
|
end
|
|
@@ -34,21 +34,35 @@ module Datadog
|
|
|
34
34
|
false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
def start_coverage
|
|
37
|
+
def start_coverage
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def stop_coverage
|
|
40
|
+
def stop_coverage
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def
|
|
44
|
+
def on_test_context_started(_context_id)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
false
|
|
47
|
+
def on_test_started(_test)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def on_test_finished(_test, _context)
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear_context_coverage(_context_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def context_coverage_enabled?
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def mark_if_skippable(_test)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def skippable?(_datadog_test_id)
|
|
65
|
+
false
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
def write_test_session_tags(_test_session, _skipped_tests_count)
|