datadog-ci 1.18.0 → 1.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4673e9680749b01863ddde129169fb985f7be2f46ae83da674b856bc70900dc9
4
- data.tar.gz: 5e0c4f0a055d7a3017dc2984d39b142cfcf9f4bfb39dca5713a18af0481276f6
3
+ metadata.gz: 6c730f78f9fefc725ba4499a38729eff14f5fc48a4a8e6a055ade2b4cedc030a
4
+ data.tar.gz: 86a9682f7623bc85a8e8a05fd8b1f91ecb12f459e7f2425fd2fb79d4ce217227
5
5
  SHA512:
6
- metadata.gz: 58d120417a247f9e6be958efeea4094e6d3f5dc861df5dbe74428f7968e2dc96156b19d5c3db512f70afa15686997e6b38aae39dbeb8d0bb2f4f19fdb8b524b4
7
- data.tar.gz: 7d91c8fc4e85bede1f57dc64a151d4ebfb222be88710f958e8a6538ad37e44cedf56c6379fb1476fb472ebf65db57023915100254e180e6087b2c7c0eb7d78ca
6
+ metadata.gz: ebb3fca5a5cf1db85795b017ffefef4bb09560c4c8c65f54b4d4cc88b93016776a861ec5c48bf700bdff4ee18c4416f6aee632b5abd392ae930a1a1b00042f0c
7
+ data.tar.gz: 9b5b171277dc7bb8510dec4bd332a937431ac4bb3f277c9ad3221dd38f3d5eec27b1c92415aa47c0289259543d2ee21fe8ae1dc532e0f9d9376706a38e59e712
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.19.0] - 2025-06-16
4
+
5
+ ### Changed
6
+
7
+ * Impacted tests detection works with line level granularity ([#335][])
8
+
9
+ ### Fixed
10
+
11
+ * Fix stdin close in Command.popen_with_stdin ([#339][])
12
+
3
13
  ## [1.18.0] - 2025-06-06
4
14
 
5
15
  ### Added
@@ -453,7 +463,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
453
463
 
454
464
  - Ruby versions < 2.7 no longer supported ([#8][])
455
465
 
456
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.18.0...main
466
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.19.0...main
467
+ [1.19.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.18.0...v1.19.0
457
468
  [1.18.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.17.0...v1.18.0
458
469
  [1.17.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.16.0...v1.17.0
459
470
  [1.16.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.15.0...v1.16.0
@@ -647,4 +658,6 @@ Currently test suite level visibility is not used by our instrumentation: it wil
647
658
  [#321]: https://github.com/DataDog/datadog-ci-rb/issues/321
648
659
  [#323]: https://github.com/DataDog/datadog-ci-rb/issues/323
649
660
  [#327]: https://github.com/DataDog/datadog-ci-rb/issues/327
650
- [#329]: https://github.com/DataDog/datadog-ci-rb/issues/329
661
+ [#329]: https://github.com/DataDog/datadog-ci-rb/issues/329
662
+ [#335]: https://github.com/DataDog/datadog-ci-rb/issues/335
663
+ [#339]: https://github.com/DataDog/datadog-ci-rb/issues/339
@@ -85,7 +85,7 @@ module Datadog
85
85
 
86
86
  def git_pull_request_base_branch
87
87
  # from docs: build branch. For Pull Request commits it is base branch PR is merging into
88
- env["APPVEYOR_REPO_BRANCH"]
88
+ env["APPVEYOR_REPO_BRANCH"] if env["APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH"]
89
89
  end
90
90
 
91
91
  private
@@ -85,6 +85,10 @@ module Datadog
85
85
  env["BUILDKITE_MESSAGE"]
86
86
  end
87
87
 
88
+ def git_pull_request_base_branch
89
+ env["BUILDKITE_PULL_REQUEST_BASE_BRANCH"]
90
+ end
91
+
88
92
  def ci_env_vars
89
93
  {
90
94
  "BUILDKITE_BUILD_ID" => env["BUILDKITE_BUILD_ID"],
@@ -104,10 +104,6 @@ module Datadog
104
104
  env["CI_MERGE_REQUEST_TARGET_BRANCH_NAME"]
105
105
  end
106
106
 
107
- def git_pull_request_base_branch_sha
108
- env["CI_MERGE_REQUEST_TARGET_BRANCH_SHA"]
109
- end
110
-
111
107
  def git_commit_head_sha
112
108
  env["CI_MERGE_REQUEST_SOURCE_BRANCH_SHA"]
113
109
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Git
6
+ # Helper class to efficiently store and query changed line intervals for a single file
7
+ # Uses merged sorted intervals with binary search for O(log n) query performance
8
+ class ChangedLines
9
+ def initialize
10
+ @intervals = [] # Array of [start, end] pairs
11
+ @built = false
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ # Add an interval (defers merging until build! is called)
16
+ def add_interval(start_line, end_line)
17
+ return if start_line > end_line
18
+
19
+ @mutex.synchronize do
20
+ @intervals << [start_line, end_line]
21
+ @built = false
22
+ end
23
+ end
24
+
25
+ # Sort and merge all intervals
26
+ # Call this after all intervals have been added
27
+ def build!
28
+ @mutex.synchronize do
29
+ return false if @built
30
+
31
+ @built = true
32
+ return false if @intervals.empty?
33
+
34
+ # Sort intervals by start line
35
+ @intervals.sort_by!(&:first)
36
+
37
+ # Merge overlapping intervals
38
+ merged = []
39
+
40
+ # @type var current_start: Integer
41
+ # @type var current_end: Integer
42
+ current_start, current_end = @intervals.first
43
+
44
+ @intervals.each_with_index do |interval, index|
45
+ next if index == 0
46
+ # @type var start_line: Integer
47
+ # @type var end_line: Integer
48
+ start_line, end_line = interval
49
+
50
+ if start_line <= current_end + 1
51
+ # Overlapping or adjacent intervals, merge them
52
+ current_end = [current_end, end_line].max
53
+ else
54
+ # Non-overlapping interval, save current and start new
55
+ merged << [current_start, current_end]
56
+ current_start = start_line
57
+ current_end = end_line
58
+ end
59
+ end
60
+
61
+ merged << [current_start, current_end]
62
+
63
+ @intervals = merged
64
+ true
65
+ end
66
+ end
67
+
68
+ # Check if any line in the query interval overlaps with changed lines
69
+ # Uses binary search for O(log n) performance
70
+ def overlaps?(query_start, query_end)
71
+ build! unless @built
72
+
73
+ return false if @intervals.empty? || query_start > query_end
74
+
75
+ # Binary search for the first interval that might overlap
76
+ left = 0
77
+ right = @intervals.length - 1
78
+
79
+ while left <= right
80
+ mid = (left + right) / 2
81
+ # @type var interval_start: Integer
82
+ # @type var interval_end: Integer
83
+ interval_start, interval_end = @intervals[mid]
84
+
85
+ if interval_end < query_start
86
+ left = mid + 1
87
+ elsif interval_start > query_end
88
+ right = mid - 1
89
+ else
90
+ # Found overlap
91
+ return true
92
+ end
93
+ end
94
+
95
+ false
96
+ end
97
+
98
+ def empty?
99
+ @intervals.empty?
100
+ end
101
+
102
+ def intervals
103
+ build! unless @built
104
+ @intervals.dup
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require_relative "changed_lines"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Git
9
+ class Diff
10
+ FILE_CHANGE_REGEX = /^diff --git a\/(?<file>.+?) b\//.freeze
11
+ LINES_CHANGE_REGEX = /^@@ -\d+(?:,\d+)? \+(?<start>\d+)(?:,(?<count>\d+))? @@/.freeze
12
+
13
+ def initialize(changed_files: {})
14
+ @changed_files = changed_files # Hash of file_path => ChangedLines
15
+ end
16
+
17
+ # Check if any lines in the given range are changed for the specified file
18
+ def lines_changed?(file_path, start_line: nil, end_line: nil)
19
+ changed_lines = @changed_files[file_path]
20
+ unless changed_lines
21
+ Datadog.logger.debug { "No changes found for file: #{file_path}" }
22
+ return false
23
+ end
24
+
25
+ # If either start_line or end_line is nil, return true if file is present
26
+ return true if start_line.nil? || end_line.nil?
27
+
28
+ changed_lines.overlaps?(start_line, end_line)
29
+ end
30
+
31
+ def empty?
32
+ @changed_files.empty?
33
+ end
34
+
35
+ # for debug purposes
36
+ def size
37
+ @changed_files.size
38
+ end
39
+
40
+ # for debug purposes
41
+ def inspect
42
+ @changed_files.inspect
43
+ end
44
+
45
+ def self.parse_diff_output(output)
46
+ return new if output.nil? || output.empty?
47
+
48
+ changed_files = {}
49
+ current_file = nil
50
+
51
+ output.each_line do |line|
52
+ # Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
53
+ # This captures git changes on file level
54
+ match = FILE_CHANGE_REGEX.match(line)
55
+ if match && match[:file]
56
+ # this path here is already relative from the git root
57
+ changed_file = match[:file]
58
+
59
+ unless changed_file.nil? || changed_file.empty?
60
+ current_file = changed_file
61
+ changed_files[current_file] ||= ChangedLines.new
62
+ end
63
+
64
+ Datadog.logger.debug { "matched changed_file: #{changed_file} from git diff line: #{line}" }
65
+
66
+ next
67
+ end
68
+
69
+ # Match lines like: @@ -1,2 +3,4 @@
70
+ match = LINES_CHANGE_REGEX.match(line)
71
+ if match && match[:start] && current_file
72
+ start_line = match[:start].to_i
73
+
74
+ line_count = 1 # Default to 1 line if count not specified
75
+ line_count = match[:count].to_i if match[:count]
76
+
77
+ end_line = start_line + line_count - 1
78
+
79
+ changed_files[current_file].add_interval(start_line, end_line)
80
+
81
+ Datadog.logger.debug { "Added interval [#{start_line}, #{end_line}] for file: #{current_file}" }
82
+ end
83
+ end
84
+
85
+ new(changed_files: changed_files)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -8,6 +8,7 @@ require_relative "../ext/telemetry"
8
8
  require_relative "../utils/command"
9
9
  require_relative "base_branch_sha_detector"
10
10
  require_relative "cli"
11
+ require_relative "diff"
11
12
  require_relative "telemetry"
12
13
  require_relative "user"
13
14
 
@@ -37,14 +38,22 @@ module Datadog
37
38
  # that root is a prefix of the path
38
39
  return "" if path.size < prefix_index
39
40
 
40
- prefix_index += 1 if path[prefix_index] == File::SEPARATOR
41
- res = path[prefix_index..]
41
+ # this means that the root is not a prefix of this path somehow
42
+ return "" if path[prefix_index] != File::SEPARATOR
43
+
44
+ res = path[prefix_index + 1..]
42
45
  else
43
46
  # prefix_to_root is a difference between the root path and the given path
44
- if @prefix_to_root == ""
45
- return path
46
- elsif @prefix_to_root
47
- return File.join(@prefix_to_root, path)
47
+ if defined?(@prefix_to_root)
48
+ # if path starts with ./ remove the dot before applying the optimization
49
+ # @type var path: String
50
+ path = path[1..] if path.start_with?("./")
51
+
52
+ if @prefix_to_root == ""
53
+ return path
54
+ elsif @prefix_to_root
55
+ return File.join(@prefix_to_root, path)
56
+ end
48
57
  end
49
58
 
50
59
  pathname = Pathname.new(File.expand_path(path))
@@ -308,10 +317,10 @@ module Datadog
308
317
  res
309
318
  end
310
319
 
311
- # Returns a Set of normalized file paths changed since the given base_commit.
320
+ # Returns a Diff object with relative file paths for files that were changed since the given base_commit.
312
321
  # If base_commit is nil, returns nil. On error, returns nil.
313
- def self.get_changed_files_from_diff(base_commit)
314
- return nil if base_commit.nil?
322
+ def self.get_changes_since(base_commit)
323
+ return Diff.new if base_commit.nil?
315
324
 
316
325
  Datadog.logger.debug { "calculating git diff from base_commit: #{base_commit}" }
317
326
 
@@ -329,29 +338,14 @@ module Datadog
329
338
 
330
339
  Datadog.logger.debug { "git diff output: #{output}" }
331
340
 
332
- return nil if output.nil?
333
-
334
- # 2. Parse the output to extract which files changed
335
- changed_files = Set.new
336
- output.each_line do |line|
337
- # Match lines like: diff --git a/foo/bar.rb b/foo/bar.rb
338
- # This captures git changes on file level
339
- match = /^diff --git a\/(?<file>.+?) b\//.match(line)
340
- if match && match[:file]
341
- changed_file = match[:file]
342
- # Normalize to repo root
343
- normalized_changed_file = relative_to_root(changed_file)
344
- changed_files << normalized_changed_file unless normalized_changed_file.nil? || normalized_changed_file.empty?
345
-
346
- Datadog.logger.debug { "matched changed_file: #{changed_file} from line: #{line}" }
347
- Datadog.logger.debug { "normalized_changed_file: #{normalized_changed_file}" }
348
- end
349
- end
350
- changed_files
341
+ return Diff.new if output.nil?
342
+
343
+ # 2. Parse the output using Git::Diff
344
+ Diff.parse_diff_output(output)
351
345
  rescue => e
352
346
  Telemetry.track_error(e, Ext::Telemetry::Command::DIFF)
353
347
  log_failure(e, "get changed files from diff")
354
- nil
348
+ Diff.new
355
349
  end
356
350
  end
357
351
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  require_relative "../ext/test"
6
4
  require_relative "../git/local_repository"
7
5
 
@@ -11,7 +9,7 @@ module Datadog
11
9
  class Component
12
10
  def initialize(enabled:)
13
11
  @enabled = enabled
14
- @changed_files = Set.new
12
+ @git_diff = Git::Diff.new
15
13
  end
16
14
 
17
15
  def configure(library_settings, test_session)
@@ -29,21 +27,21 @@ module Datadog
29
27
  return
30
28
  end
31
29
 
32
- changed_files = Git::LocalRepository.get_changed_files_from_diff(base_commit_sha)
33
- if changed_files.nil?
30
+ git_diff = Git::LocalRepository.get_changes_since(base_commit_sha)
31
+ if git_diff.empty?
34
32
  Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
35
33
  @enabled = false
36
34
  return
37
35
  end
38
36
 
39
37
  Datadog.logger.debug do
40
- "Impacted tests detection: found #{changed_files.size} changed files"
38
+ "Impacted tests detection: found #{git_diff.size} changed files"
41
39
  end
42
40
  Datadog.logger.debug do
43
- "Impacted tests detection: changed files: #{changed_files.inspect}"
41
+ "Impacted tests detection: changed files: #{git_diff.inspect}"
44
42
  end
45
43
 
46
- @changed_files = changed_files
44
+ @git_diff = git_diff
47
45
  @enabled = true
48
46
  end
49
47
 
@@ -57,7 +55,15 @@ module Datadog
57
55
  source_file = test_span.source_file
58
56
  return false if source_file.nil?
59
57
 
60
- @changed_files.include?(source_file)
58
+ # convert to relative path without leading slash
59
+ # @type var source_file: String
60
+ source_file = source_file[1..] if source_file.start_with?("/")
61
+
62
+ result = @git_diff.lines_changed?(source_file, start_line: test_span.start_line, end_line: test_span.end_line)
63
+ Datadog.logger.debug do
64
+ "Impacted tests detection: test #{test_span.name} with source file #{source_file} is modified: #{result}"
65
+ end
66
+ result
61
67
  end
62
68
 
63
69
  def tag_modified_test(test_span)
@@ -66,6 +66,22 @@ module Datadog
66
66
  get_tag(Ext::Test::TAG_TEST_SESSION_ID)
67
67
  end
68
68
 
69
+ # Returns the starting line number of the test in the source file.
70
+ # @return [Integer] the starting line number
71
+ # @return [nil] if the starting line is not available
72
+ def start_line
73
+ line = get_tag(Ext::Test::TAG_SOURCE_START)
74
+ line&.to_i
75
+ end
76
+
77
+ # Returns the ending line number of the test in the source file.
78
+ # @return [Integer] the ending line number
79
+ # @return [nil] if the ending line is not available
80
+ def end_line
81
+ line = get_tag(Ext::Test::TAG_SOURCE_END)
82
+ line&.to_i
83
+ end
84
+
69
85
  # Returns "true" if test span represents a retry.
70
86
  # @return [Boolean] true if this test is a retry, false otherwise.
71
87
  def is_retry?
@@ -92,6 +92,7 @@ module Datadog
92
92
  end
93
93
 
94
94
  def self.popen_with_stdin(command, stdin_data: nil, retries_left: OPEN_STDIN_RETRY_COUNT)
95
+ stdin = nil
95
96
  result = Open3.popen2e(*command)
96
97
  stdin = result.first
97
98
 
@@ -108,7 +109,7 @@ module Datadog
108
109
 
109
110
  result
110
111
  ensure
111
- stdin.close
112
+ stdin&.close
112
113
  end
113
114
  end
114
115
  end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 18
7
+ MINOR = 19
8
8
  PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datadog-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.18.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
@@ -196,7 +196,9 @@ files:
196
196
  - lib/datadog/ci/git/base_branch_sha_detection/guesser.rb
197
197
  - lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb
198
198
  - lib/datadog/ci/git/base_branch_sha_detector.rb
199
+ - lib/datadog/ci/git/changed_lines.rb
199
200
  - lib/datadog/ci/git/cli.rb
201
+ - lib/datadog/ci/git/diff.rb
200
202
  - lib/datadog/ci/git/local_repository.rb
201
203
  - lib/datadog/ci/git/packfiles.rb
202
204
  - lib/datadog/ci/git/search_commits.rb