datadog-ci 1.16.0 → 1.17.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 +24 -2
- data/lib/datadog/ci/codeowners/rule.rb +5 -0
- data/lib/datadog/ci/configuration/components.rb +17 -5
- data/lib/datadog/ci/configuration/settings.rb +6 -0
- data/lib/datadog/ci/contrib/knapsack/patcher.rb +1 -3
- data/lib/datadog/ci/contrib/knapsack/runner.rb +2 -0
- data/lib/datadog/ci/contrib/minitest/runner.rb +1 -0
- data/lib/datadog/ci/contrib/minitest/test.rb +7 -2
- data/lib/datadog/ci/contrib/parallel_tests/patcher.rb +1 -3
- data/lib/datadog/ci/contrib/patcher.rb +4 -0
- data/lib/datadog/ci/contrib/rspec/helpers.rb +1 -3
- data/lib/datadog/ci/ext/environment/extractor.rb +4 -6
- data/lib/datadog/ci/ext/environment/providers/appveyor.rb +5 -0
- data/lib/datadog/ci/ext/environment/providers/base.rb +7 -2
- data/lib/datadog/ci/ext/environment/providers/bitbucket.rb +6 -0
- data/lib/datadog/ci/ext/environment/providers/bitrise.rb +7 -1
- data/lib/datadog/ci/ext/environment/providers/buddy.rb +5 -0
- data/lib/datadog/ci/ext/environment/providers/github_actions.rb +37 -18
- data/lib/datadog/ci/ext/environment/providers/gitlab.rb +13 -1
- data/lib/datadog/ci/ext/environment/providers/user_defined_tags.rb +12 -0
- data/lib/datadog/ci/ext/git.rb +3 -0
- data/lib/datadog/ci/ext/settings.rb +1 -0
- data/lib/datadog/ci/ext/telemetry.rb +4 -0
- data/lib/datadog/ci/ext/test.rb +4 -1
- data/lib/datadog/ci/ext/transport.rb +1 -0
- data/lib/datadog/ci/git/local_repository.rb +238 -4
- data/lib/datadog/ci/git/tree_uploader.rb +10 -3
- data/lib/datadog/ci/impacted_tests_detection/component.rb +83 -0
- data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +16 -0
- data/lib/datadog/ci/remote/component.rb +6 -1
- data/lib/datadog/ci/remote/library_settings.rb +8 -0
- data/lib/datadog/ci/span.rb +7 -0
- data/lib/datadog/ci/test.rb +6 -0
- data/lib/datadog/ci/test_management/tests_properties.rb +2 -1
- data/lib/datadog/ci/test_retries/component.rb +8 -17
- data/lib/datadog/ci/test_retries/driver/{retry_new.rb → retry_flake_detection.rb} +1 -1
- data/lib/datadog/ci/test_retries/strategy/{retry_new.rb → retry_flake_detection.rb} +4 -4
- data/lib/datadog/ci/test_visibility/component.rb +6 -0
- data/lib/datadog/ci/version.rb +1 -1
- metadata +7 -5
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "open3"
|
4
4
|
require "pathname"
|
5
|
+
require "set"
|
5
6
|
|
6
7
|
require_relative "../ext/telemetry"
|
7
8
|
require_relative "telemetry"
|
@@ -23,6 +24,8 @@ module Datadog
|
|
23
24
|
end
|
24
25
|
|
25
26
|
COMMAND_RETRY_COUNT = 3
|
27
|
+
POSSIBLE_BASE_BRANCHES = %w[main master preprod prod dev development trunk].freeze
|
28
|
+
DEFAULT_LIKE_BRANCH_FILTER = /^(#{POSSIBLE_BASE_BRANCHES.join("|")}|release\/.*|hotfix\/.*)$/.freeze
|
26
29
|
|
27
30
|
def self.root
|
28
31
|
return @root if defined?(@root)
|
@@ -63,7 +66,7 @@ module Datadog
|
|
63
66
|
res = pathname.relative_path_from(root_path).to_s
|
64
67
|
|
65
68
|
unless defined?(@prefix_to_root)
|
66
|
-
@prefix_to_root = res
|
69
|
+
@prefix_to_root = res.gsub(path, "") if res.end_with?(path)
|
67
70
|
end
|
68
71
|
end
|
69
72
|
|
@@ -90,6 +93,7 @@ module Datadog
|
|
90
93
|
|
91
94
|
def self.git_repository_url
|
92
95
|
Telemetry.git_command(Ext::Telemetry::Command::GET_REPOSITORY)
|
96
|
+
# @type var res: String?
|
93
97
|
res = nil
|
94
98
|
|
95
99
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
@@ -120,6 +124,7 @@ module Datadog
|
|
120
124
|
|
121
125
|
def self.git_branch
|
122
126
|
Telemetry.git_command(Ext::Telemetry::Command::GET_BRANCH)
|
127
|
+
# @type var res: String?
|
123
128
|
res = nil
|
124
129
|
|
125
130
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
@@ -177,7 +182,9 @@ module Datadog
|
|
177
182
|
def self.git_commits
|
178
183
|
Telemetry.git_command(Ext::Telemetry::Command::GET_LOCAL_COMMITS)
|
179
184
|
|
185
|
+
# @type var output: String?
|
180
186
|
output = nil
|
187
|
+
|
181
188
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
182
189
|
output = exec_git_command("git log --format=%H -n 1000 --since=\"1 month ago\"")
|
183
190
|
end
|
@@ -186,7 +193,6 @@ module Datadog
|
|
186
193
|
|
187
194
|
return [] if output.nil?
|
188
195
|
|
189
|
-
# @type var output: String
|
190
196
|
output.split("\n")
|
191
197
|
rescue => e
|
192
198
|
log_failure(e, "git commits")
|
@@ -199,6 +205,7 @@ module Datadog
|
|
199
205
|
included_commits = filter_invalid_commits(included_commits).join(" ")
|
200
206
|
excluded_commits = filter_invalid_commits(excluded_commits).map! { |sha| "^#{sha}" }.join(" ")
|
201
207
|
|
208
|
+
# @type var res: String?
|
202
209
|
res = nil
|
203
210
|
|
204
211
|
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
@@ -264,6 +271,7 @@ module Datadog
|
|
264
271
|
|
265
272
|
def self.git_unshallow
|
266
273
|
Telemetry.git_command(Ext::Telemetry::Command::UNSHALLOW)
|
274
|
+
# @type var res: String?
|
267
275
|
res = nil
|
268
276
|
|
269
277
|
unshallow_command =
|
@@ -296,7 +304,7 @@ module Datadog
|
|
296
304
|
nil
|
297
305
|
end
|
298
306
|
|
299
|
-
break unless unshallowing_errored
|
307
|
+
break [] unless unshallowing_errored
|
300
308
|
end
|
301
309
|
end
|
302
310
|
|
@@ -304,6 +312,229 @@ module Datadog
|
|
304
312
|
res
|
305
313
|
end
|
306
314
|
|
315
|
+
# Returns a Set of normalized file paths changed since the given base_commit.
|
316
|
+
# If base_commit is nil, returns nil. On error, returns nil.
|
317
|
+
def self.get_changed_files_from_diff(base_commit)
|
318
|
+
return nil if base_commit.nil?
|
319
|
+
|
320
|
+
Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
|
321
|
+
|
322
|
+
Telemetry.git_command(Ext::Telemetry::Command::DIFF)
|
323
|
+
|
324
|
+
begin
|
325
|
+
# 1. Run the git diff command
|
326
|
+
|
327
|
+
# @type var output: String?
|
328
|
+
output = nil
|
329
|
+
duration_ms = Core::Utils::Time.measure(:float_millisecond) do
|
330
|
+
output = exec_git_command("git diff -U0 --word-diff=porcelain #{base_commit} HEAD")
|
331
|
+
end
|
332
|
+
Telemetry.git_command_ms(Ext::Telemetry::Command::DIFF, duration_ms)
|
333
|
+
|
334
|
+
Datadog.logger.debug { "git diff output: #{output}" }
|
335
|
+
|
336
|
+
return nil if output.nil?
|
337
|
+
|
338
|
+
# 2. Parse the output to extract which files changed
|
339
|
+
changed_files = Set.new
|
340
|
+
output.each_line do |line|
|
341
|
+
# Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
|
342
|
+
# This captures git changes on file level
|
343
|
+
match = /^diff --git a\/(?<file>.+) b\/(?<file2>.+)$/.match(line)
|
344
|
+
if match && match[:file]
|
345
|
+
changed_file = match[:file]
|
346
|
+
# Normalize to repo root
|
347
|
+
normalized_changed_file = relative_to_root(changed_file)
|
348
|
+
changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
|
349
|
+
|
350
|
+
Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
|
351
|
+
Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
|
352
|
+
end
|
353
|
+
end
|
354
|
+
changed_files
|
355
|
+
rescue => e
|
356
|
+
telemetry_track_error(e, Ext::Telemetry::Command::DIFF)
|
357
|
+
log_failure(e, "get changed files from diff")
|
358
|
+
nil
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# On best effort basis determines the git sha of the most likely
|
363
|
+
# base branch for the current PR.
|
364
|
+
def self.base_commit_sha(base_branch: nil)
|
365
|
+
Telemetry.git_command(Ext::Telemetry::Command::BASE_COMMIT_SHA)
|
366
|
+
|
367
|
+
remote_name = get_remote_name
|
368
|
+
Datadog.logger.debug { "Remote name: '#{remote_name}'" }
|
369
|
+
|
370
|
+
source_branch = get_source_branch
|
371
|
+
return nil if source_branch.nil?
|
372
|
+
|
373
|
+
Datadog.logger.debug { "Source branch: '#{source_branch}'" }
|
374
|
+
|
375
|
+
# Early exit if source is a main-like branch
|
376
|
+
if main_like_branch?(source_branch, remote_name)
|
377
|
+
Datadog.logger.debug { "Branch '#{source_branch}' already matches base branch filter (#{DEFAULT_LIKE_BRANCH_FILTER})" }
|
378
|
+
return nil
|
379
|
+
end
|
380
|
+
|
381
|
+
possible_base_branches = base_branch.nil? ? POSSIBLE_BASE_BRANCHES : [base_branch]
|
382
|
+
# Check and fetch base branches if they don't exist in local git repository
|
383
|
+
check_and_fetch_base_branches(possible_base_branches, remote_name)
|
384
|
+
|
385
|
+
default_branch = detect_default_branch(remote_name)
|
386
|
+
Datadog.logger.debug { "Default branch: '#{default_branch}'" }
|
387
|
+
|
388
|
+
candidates = build_candidate_list(remote_name, source_branch, base_branch)
|
389
|
+
if candidates.nil? || candidates.empty?
|
390
|
+
Datadog.logger.debug { "No candidate branches found." }
|
391
|
+
return nil
|
392
|
+
end
|
393
|
+
|
394
|
+
metrics = compute_branch_metrics(candidates, source_branch)
|
395
|
+
Datadog.logger.debug { "Branch metrics: '#{metrics}'" }
|
396
|
+
|
397
|
+
best_branch_sha = find_best_branch(metrics, default_branch, remote_name)
|
398
|
+
Datadog.logger.debug { "Best branch: '#{best_branch_sha}'" }
|
399
|
+
|
400
|
+
best_branch_sha
|
401
|
+
rescue => e
|
402
|
+
telemetry_track_error(e, Ext::Telemetry::Command::BASE_COMMIT_SHA)
|
403
|
+
log_failure(e, "git base ref")
|
404
|
+
nil
|
405
|
+
end
|
406
|
+
|
407
|
+
def self.check_and_fetch_branch(branch, remote_name)
|
408
|
+
# Check if branch exists locally
|
409
|
+
exec_git_command("git show-ref --verify --quiet refs/heads/#{branch}")
|
410
|
+
Datadog.logger.debug { "Branch '#{branch}' exists locally, skipping" }
|
411
|
+
rescue GitCommandExecutionError => e
|
412
|
+
Datadog.logger.debug { "Branch '#{branch}' doesn't exist locally, checking remote: #{e}" }
|
413
|
+
begin
|
414
|
+
remote_heads = exec_git_command("git ls-remote --heads #{remote_name} #{branch}")
|
415
|
+
if remote_heads.nil? || remote_heads.empty?
|
416
|
+
Datadog.logger.debug { "Branch '#{branch}' doesn't exist in remote" }
|
417
|
+
return
|
418
|
+
end
|
419
|
+
|
420
|
+
Datadog.logger.debug { "Branch '#{branch}' exists in remote, fetching" }
|
421
|
+
exec_git_command("git fetch --depth 1 #{remote_name} #{branch}:#{branch}")
|
422
|
+
rescue GitCommandExecutionError => e
|
423
|
+
Datadog.logger.debug { "Branch '#{branch}' couldn't be fetched from remote: #{e}" }
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
def self.check_and_fetch_base_branches(branches, remote_name)
|
428
|
+
branches.each do |branch|
|
429
|
+
check_and_fetch_branch(branch, remote_name)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def self.get_source_branch
|
434
|
+
source_branch = exec_git_command("git rev-parse --abbrev-ref HEAD")&.strip
|
435
|
+
if source_branch.nil?
|
436
|
+
Datadog.logger.debug { "Could not get current branch" }
|
437
|
+
return nil
|
438
|
+
end
|
439
|
+
|
440
|
+
exec_git_command("git rev-parse --verify --quiet #{source_branch} > /dev/null")
|
441
|
+
source_branch
|
442
|
+
end
|
443
|
+
|
444
|
+
def self.remove_remote_prefix(branch_name, remote_name)
|
445
|
+
branch_name&.sub(/^#{Regexp.escape(remote_name)}\//, "")
|
446
|
+
end
|
447
|
+
|
448
|
+
def self.main_like_branch?(branch_name, remote_name)
|
449
|
+
short_branch_name = remove_remote_prefix(branch_name, remote_name)
|
450
|
+
short_branch_name&.match?(DEFAULT_LIKE_BRANCH_FILTER)
|
451
|
+
end
|
452
|
+
|
453
|
+
def self.detect_default_branch(remote_name)
|
454
|
+
# @type var default_branch: String?
|
455
|
+
default_branch = nil
|
456
|
+
begin
|
457
|
+
default_ref = exec_git_command("git symbolic-ref --quiet --short \"refs/remotes/#{remote_name}/HEAD\" 2>/dev/null")
|
458
|
+
default_branch = remove_remote_prefix(default_ref, remote_name) unless default_ref.nil?
|
459
|
+
rescue
|
460
|
+
Datadog.logger.debug { "Could not get symbolic-ref, trying to find a fallback (main, master)..." }
|
461
|
+
end
|
462
|
+
|
463
|
+
default_branch = find_fallback_default_branch(remote_name) if default_branch.nil?
|
464
|
+
default_branch
|
465
|
+
end
|
466
|
+
|
467
|
+
def self.find_fallback_default_branch(remote_name)
|
468
|
+
["main", "master"].each do |fallback|
|
469
|
+
exec_git_command("git show-ref --verify --quiet \"refs/remotes/#{remote_name}/#{fallback}\"")
|
470
|
+
Datadog.logger.debug { "Found fallback default branch '#{fallback}'" }
|
471
|
+
return fallback
|
472
|
+
rescue
|
473
|
+
next
|
474
|
+
end
|
475
|
+
nil
|
476
|
+
end
|
477
|
+
|
478
|
+
def self.build_candidate_list(remote_name, source_branch, base_branch)
|
479
|
+
unless base_branch.nil?
|
480
|
+
return [base_branch]
|
481
|
+
end
|
482
|
+
|
483
|
+
candidates = exec_git_command("git for-each-ref --format='%(refname:short)' refs/heads \"refs/remotes/#{remote_name}\"")&.lines&.map(&:strip)
|
484
|
+
Datadog.logger.debug { "Available branches: '#{candidates}'" }
|
485
|
+
candidates&.select! { |b| b.match?(DEFAULT_LIKE_BRANCH_FILTER) && b != source_branch }
|
486
|
+
Datadog.logger.debug { "Candidate branches: '#{candidates}'" }
|
487
|
+
candidates
|
488
|
+
end
|
489
|
+
|
490
|
+
def self.compute_branch_metrics(candidates, source_branch)
|
491
|
+
metrics = {}
|
492
|
+
candidates.each do |cand|
|
493
|
+
base_sha = exec_git_command("git merge-base #{cand} #{source_branch} 2>/dev/null")&.strip
|
494
|
+
next if base_sha.nil? || base_sha.empty?
|
495
|
+
|
496
|
+
behind, ahead = exec_git_command("git rev-list --left-right --count #{cand}...#{source_branch}")&.strip&.split&.map(&:to_i)
|
497
|
+
metrics[cand] = {behind: behind, ahead: ahead, base_sha: base_sha}
|
498
|
+
end
|
499
|
+
metrics
|
500
|
+
end
|
501
|
+
|
502
|
+
def self.find_best_branch(metrics, default_branch, remote_name)
|
503
|
+
return nil if metrics.empty?
|
504
|
+
|
505
|
+
_, best_data = metrics.min_by do |cand, data|
|
506
|
+
[
|
507
|
+
data[:ahead],
|
508
|
+
default_branch?(cand, default_branch, remote_name) ? 0 : 1 # prefer default branch on tie
|
509
|
+
]
|
510
|
+
end
|
511
|
+
|
512
|
+
best_data ? best_data[:base_sha] : nil
|
513
|
+
end
|
514
|
+
|
515
|
+
def self.default_branch?(branch, default_branch, remote_name)
|
516
|
+
branch == default_branch || branch == "#{remote_name}/#{default_branch}"
|
517
|
+
end
|
518
|
+
|
519
|
+
def self.get_remote_name
|
520
|
+
# Try to find remote from upstream tracking
|
521
|
+
upstream = nil
|
522
|
+
begin
|
523
|
+
upstream = exec_git_command("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}")&.strip
|
524
|
+
rescue => e
|
525
|
+
Datadog.logger.debug { "Error getting upstream: #{e}" }
|
526
|
+
end
|
527
|
+
|
528
|
+
if upstream
|
529
|
+
upstream.split("/").first
|
530
|
+
else
|
531
|
+
# Fallback to first remote if no upstream is set
|
532
|
+
first_remote_value = exec_git_command("git remote")&.split("\n")&.first
|
533
|
+
Datadog.logger.debug { "First remote value: '#{first_remote_value}'" }
|
534
|
+
first_remote_value || "origin"
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
307
538
|
# makes .exec_git_command private to make sure that this method
|
308
539
|
# is not called from outside of this module with insecure parameters
|
309
540
|
class << self
|
@@ -316,10 +547,13 @@ module Datadog
|
|
316
547
|
def exec_git_command(cmd, stdin: nil)
|
317
548
|
# Shell injection is alleviated by making sure that no outside modules call this method.
|
318
549
|
# It is called only internally with static parameters.
|
319
|
-
|
550
|
+
|
551
|
+
# @type var out: String
|
552
|
+
# @type var status: Process::Status?
|
320
553
|
out, status = Open3.capture2e(cmd, stdin_data: stdin)
|
321
554
|
|
322
555
|
if status.nil?
|
556
|
+
# @type var retry_count: Integer
|
323
557
|
retry_count = COMMAND_RETRY_COUNT
|
324
558
|
Datadog.logger.debug { "Opening pipe failed, starting retries..." }
|
325
559
|
while status.nil? && retry_count.positive?
|
@@ -15,10 +15,11 @@ module Datadog
|
|
15
15
|
module CI
|
16
16
|
module Git
|
17
17
|
class TreeUploader
|
18
|
-
attr_reader :api
|
18
|
+
attr_reader :api, :force_unshallow
|
19
19
|
|
20
|
-
def initialize(api:)
|
20
|
+
def initialize(api:, force_unshallow: false)
|
21
21
|
@api = api
|
22
|
+
@force_unshallow = force_unshallow
|
22
23
|
end
|
23
24
|
|
24
25
|
def call(repository_url)
|
@@ -45,7 +46,13 @@ module Datadog
|
|
45
46
|
# ask the backend for the list of commits it already has
|
46
47
|
known_commits, new_commits = fetch_known_commits_and_split(repository_url, latest_commits)
|
47
48
|
# if all commits are present in the backend, we don't need to upload anything
|
48
|
-
|
49
|
+
|
50
|
+
# We optimize unshallowing process by checking the latest available commits with backend:
|
51
|
+
# if they are already known to backend, then we don't have to unshallow.
|
52
|
+
#
|
53
|
+
# Sometimes we need to unshallow anyway: for impacted tests detection feature for example we need
|
54
|
+
# to calculate git diffs locally. In this case we skip the optimization and always unshallow.
|
55
|
+
if new_commits.empty? && !@force_unshallow
|
49
56
|
Datadog.logger.debug("No new commits to upload")
|
50
57
|
return
|
51
58
|
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require_relative "../ext/test"
|
6
|
+
require_relative "../git/local_repository"
|
7
|
+
require_relative "telemetry"
|
8
|
+
|
9
|
+
module Datadog
|
10
|
+
module CI
|
11
|
+
module ImpactedTestsDetection
|
12
|
+
class Component
|
13
|
+
def initialize(enabled:)
|
14
|
+
@enabled = enabled
|
15
|
+
@changed_files = Set.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure(library_settings, test_session)
|
19
|
+
@enabled &&= library_settings.impacted_tests_enabled?
|
20
|
+
|
21
|
+
return unless @enabled
|
22
|
+
|
23
|
+
# we must unshallow the repository before trying to find base_commit_sha or executing `git diff` command
|
24
|
+
git_tree_upload_worker.wait_until_done
|
25
|
+
|
26
|
+
base_commit_sha = test_session.base_commit_sha || Git::LocalRepository.base_commit_sha
|
27
|
+
if base_commit_sha.nil?
|
28
|
+
Datadog.logger.debug { "Impacted tests detection disabled: base commit not found" }
|
29
|
+
@enabled = false
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
changed_files = Git::LocalRepository.get_changed_files_from_diff(base_commit_sha)
|
34
|
+
if changed_files.nil?
|
35
|
+
Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
|
36
|
+
@enabled = false
|
37
|
+
return
|
38
|
+
end
|
39
|
+
|
40
|
+
Datadog.logger.debug do
|
41
|
+
"Impacted tests detection: found #{changed_files.size} changed files"
|
42
|
+
end
|
43
|
+
Datadog.logger.debug do
|
44
|
+
"Impacted tests detection: changed files: #{changed_files.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
@changed_files = changed_files
|
48
|
+
@enabled = true
|
49
|
+
end
|
50
|
+
|
51
|
+
def enabled?
|
52
|
+
@enabled
|
53
|
+
end
|
54
|
+
|
55
|
+
def modified?(test_span)
|
56
|
+
return false unless enabled?
|
57
|
+
|
58
|
+
source_file = test_span.source_file
|
59
|
+
return false if source_file.nil?
|
60
|
+
|
61
|
+
@changed_files.include?(source_file)
|
62
|
+
end
|
63
|
+
|
64
|
+
def tag_modified_test(test_span)
|
65
|
+
return unless modified?(test_span)
|
66
|
+
|
67
|
+
Datadog.logger.debug do
|
68
|
+
"Impacted tests detection: test #{test_span.name} with source file #{test_span.source_file} is modified"
|
69
|
+
end
|
70
|
+
|
71
|
+
test_span.set_tag(Ext::Test::TAG_TEST_IS_MODIFIED, "true")
|
72
|
+
Telemetry.impacted_test_detected
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def git_tree_upload_worker
|
78
|
+
Datadog.send(:components).git_tree_upload_worker
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../ext/telemetry"
|
4
|
+
require_relative "../utils/telemetry"
|
5
|
+
|
6
|
+
module Datadog
|
7
|
+
module CI
|
8
|
+
module ImpactedTestsDetection
|
9
|
+
module Telemetry
|
10
|
+
def self.impacted_test_detected
|
11
|
+
Utils::Telemetry.inc(Ext::Telemetry::METRIC_IMPACTED_TESTS_IS_MODIFIED, 1)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -46,7 +46,8 @@ module Datadog
|
|
46
46
|
Worker.new { test_optimisation.configure(@library_configuration, test_session) },
|
47
47
|
Worker.new { test_retries.configure(@library_configuration, test_session) },
|
48
48
|
Worker.new { test_visibility.configure(@library_configuration, test_session) },
|
49
|
-
Worker.new { test_management.configure(@library_configuration, test_session) }
|
49
|
+
Worker.new { test_management.configure(@library_configuration, test_session) },
|
50
|
+
Worker.new { impacted_tests_detection.configure(@library_configuration, test_session) }
|
50
51
|
]
|
51
52
|
|
52
53
|
# launch configuration workers
|
@@ -89,6 +90,10 @@ module Datadog
|
|
89
90
|
Datadog.send(:components).test_retries
|
90
91
|
end
|
91
92
|
|
93
|
+
def impacted_tests_detection
|
94
|
+
Datadog.send(:components).impacted_tests_detection
|
95
|
+
end
|
96
|
+
|
92
97
|
def git_tree_upload_worker
|
93
98
|
Datadog.send(:components).git_tree_upload_worker
|
94
99
|
end
|
@@ -106,6 +106,14 @@ module Datadog
|
|
106
106
|
)
|
107
107
|
end
|
108
108
|
|
109
|
+
def impacted_tests_enabled?
|
110
|
+
return @impacted_tests_enabled if defined?(@impacted_tests_enabled)
|
111
|
+
|
112
|
+
@impacted_tests_enabled = Utils::Parsing.convert_to_bool(
|
113
|
+
payload.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_IMPACTED_TESTS_ENABLED_KEY, false)
|
114
|
+
)
|
115
|
+
end
|
116
|
+
|
109
117
|
def slow_test_retries
|
110
118
|
return @slow_test_retries if defined?(@slow_test_retries)
|
111
119
|
|
data/lib/datadog/ci/span.rb
CHANGED
@@ -6,6 +6,7 @@ require "datadog/core/environment/platform"
|
|
6
6
|
|
7
7
|
require_relative "ext/test"
|
8
8
|
require_relative "utils/test_run"
|
9
|
+
require_relative "ext/git"
|
9
10
|
|
10
11
|
module Datadog
|
11
12
|
module CI
|
@@ -164,6 +165,12 @@ module Datadog
|
|
164
165
|
tracer_span.get_tag(Ext::Git::TAG_BRANCH)
|
165
166
|
end
|
166
167
|
|
168
|
+
# Returns the base commit SHA for the pull request, if available.
|
169
|
+
# @return [String, nil] the base commit SHA or nil if not set.
|
170
|
+
def base_commit_sha
|
171
|
+
tracer_span.get_tag(Ext::Git::TAG_PULL_REQUEST_BASE_BRANCH_SHA)
|
172
|
+
end
|
173
|
+
|
167
174
|
# Returns the OS architecture extracted from the environment.
|
168
175
|
# @return [String] OS arch.
|
169
176
|
def os_architecture
|
data/lib/datadog/ci/test.rb
CHANGED
@@ -102,6 +102,12 @@ module Datadog
|
|
102
102
|
get_tag(Ext::Test::TAG_IS_ATTEMPT_TO_FIX) == "true"
|
103
103
|
end
|
104
104
|
|
105
|
+
# Returns "true" if this test is marked as modified (e.g., impacted by code changes).
|
106
|
+
# @return [Boolean] true if this test is modified, false otherwise.
|
107
|
+
def modified?
|
108
|
+
get_tag(Ext::Test::TAG_TEST_IS_MODIFIED) == "true"
|
109
|
+
end
|
110
|
+
|
105
111
|
# Marks this test as unskippable by the Test Impact Analysis.
|
106
112
|
# This must be done before the test execution starts.
|
107
113
|
#
|
@@ -118,7 +118,8 @@ module Datadog
|
|
118
118
|
"type" => Ext::Transport::DD_API_TEST_MANAGEMENT_TESTS_TYPE,
|
119
119
|
"attributes" => {
|
120
120
|
"repository_url" => test_session.git_repository_url,
|
121
|
-
"commit_message" => test_session.git_commit_message
|
121
|
+
"commit_message" => test_session.git_commit_message,
|
122
|
+
"sha" => test_session.git_commit_sha
|
122
123
|
}
|
123
124
|
}
|
124
125
|
}.to_json
|
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
require_relative "driver/no_retry"
|
4
4
|
require_relative "driver/retry_failed"
|
5
|
-
require_relative "driver/
|
5
|
+
require_relative "driver/retry_flake_detection"
|
6
6
|
|
7
7
|
require_relative "strategy/no_retry"
|
8
8
|
require_relative "strategy/retry_failed"
|
9
|
-
require_relative "strategy/
|
9
|
+
require_relative "strategy/retry_flake_detection"
|
10
10
|
require_relative "strategy/retry_flaky_fixed"
|
11
11
|
|
12
12
|
require_relative "../ext/telemetry"
|
@@ -31,13 +31,13 @@ module Datadog
|
|
31
31
|
)
|
32
32
|
no_retries_strategy = Strategy::NoRetry.new
|
33
33
|
|
34
|
-
|
34
|
+
retry_failed_strategy = Strategy::RetryFailed.new(
|
35
35
|
enabled: retry_failed_tests_enabled,
|
36
36
|
max_attempts: retry_failed_tests_max_attempts,
|
37
37
|
total_limit: retry_failed_tests_total_limit
|
38
38
|
)
|
39
39
|
|
40
|
-
|
40
|
+
retry_flake_detection_strategy = Strategy::RetryFlakeDetection.new(
|
41
41
|
enabled: retry_new_tests_enabled
|
42
42
|
)
|
43
43
|
|
@@ -49,8 +49,8 @@ module Datadog
|
|
49
49
|
# order is important, we apply the first matching strategy
|
50
50
|
@retry_strategies = [
|
51
51
|
retry_flaky_fixed_strategy,
|
52
|
-
|
53
|
-
|
52
|
+
retry_flake_detection_strategy,
|
53
|
+
retry_failed_strategy,
|
54
54
|
no_retries_strategy
|
55
55
|
]
|
56
56
|
@mutex = Mutex.new
|
@@ -111,23 +111,14 @@ module Datadog
|
|
111
111
|
test_span&.set_tag(Ext::Test::TAG_HAS_FAILED_ALL_RETRIES, "true") if test_span&.all_executions_failed?
|
112
112
|
|
113
113
|
# if we are attempting to fix the test and all retries passed, we indicate that the fix might have worked
|
114
|
-
|
115
|
-
|
116
|
-
end
|
114
|
+
# otherwise we send "false" to show that it didn't work
|
115
|
+
test_span&.set_tag(Ext::Test::TAG_ATTEMPT_TO_FIX_PASSED, test_span&.all_executions_passed?.to_s) if test_span&.attempt_to_fix?
|
117
116
|
end
|
118
117
|
|
119
118
|
def should_retry?
|
120
119
|
!!current_retry_driver&.should_retry?
|
121
120
|
end
|
122
121
|
|
123
|
-
def auto_test_retries_feature_enabled
|
124
|
-
@retry_failed_strategy.enabled
|
125
|
-
end
|
126
|
-
|
127
|
-
def early_flake_detection_feature_enabled
|
128
|
-
@retry_new_strategy.enabled
|
129
|
-
end
|
130
|
-
|
131
122
|
private
|
132
123
|
|
133
124
|
def current_retry_driver
|
@@ -9,7 +9,7 @@ module Datadog
|
|
9
9
|
module TestRetries
|
10
10
|
module Driver
|
11
11
|
# retry every new test up to 10 times (early flake detection)
|
12
|
-
class
|
12
|
+
class RetryFlakeDetection < Base
|
13
13
|
def initialize(test_span, max_attempts_thresholds:)
|
14
14
|
@max_attempts_thresholds = max_attempts_thresholds
|
15
15
|
@attempts = 0
|
@@ -2,13 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative "base"
|
4
4
|
|
5
|
-
require_relative "../driver/
|
5
|
+
require_relative "../driver/retry_flake_detection"
|
6
6
|
|
7
7
|
module Datadog
|
8
8
|
module CI
|
9
9
|
module TestRetries
|
10
10
|
module Strategy
|
11
|
-
class
|
11
|
+
class RetryFlakeDetection < Base
|
12
12
|
DEFAULT_TOTAL_TESTS_COUNT = 100
|
13
13
|
|
14
14
|
attr_reader :enabled, :max_attempts_thresholds, :total_limit, :retried_count
|
@@ -31,7 +31,7 @@ module Datadog
|
|
31
31
|
mark_test_session_faulty(Datadog::CI.active_test_session)
|
32
32
|
end
|
33
33
|
|
34
|
-
@enabled && !test_span.skipped? && test_span.is_new?
|
34
|
+
@enabled && !test_span.skipped? && (test_span.is_new? || test_span.modified?)
|
35
35
|
end
|
36
36
|
|
37
37
|
def configure(library_settings, test_session)
|
@@ -52,7 +52,7 @@ module Datadog
|
|
52
52
|
end
|
53
53
|
@retried_count += 1
|
54
54
|
|
55
|
-
Driver::
|
55
|
+
Driver::RetryFlakeDetection.new(test_span, max_attempts_thresholds: @max_attempts_thresholds)
|
56
56
|
end
|
57
57
|
|
58
58
|
private
|
@@ -261,6 +261,8 @@ module Datadog
|
|
261
261
|
|
262
262
|
mark_test_as_new(test) if new_test?(test)
|
263
263
|
|
264
|
+
impacted_tests_detection.tag_modified_test(test)
|
265
|
+
|
264
266
|
test_management.tag_test_from_properties(test)
|
265
267
|
|
266
268
|
test_optimisation.mark_if_skippable(test)
|
@@ -435,6 +437,10 @@ module Datadog
|
|
435
437
|
Datadog.send(:components).test_management
|
436
438
|
end
|
437
439
|
|
440
|
+
def impacted_tests_detection
|
441
|
+
Datadog.send(:components).impacted_tests_detection
|
442
|
+
end
|
443
|
+
|
438
444
|
# DISTRIBUTED RUBY CONTEXT
|
439
445
|
def start_drb_service
|
440
446
|
return if @context_service_uri
|