datadog-ci 1.17.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -2
  3. data/ext/datadog_ci_native/ci.c +10 -0
  4. data/ext/{datadog_cov → datadog_ci_native}/datadog_cov.c +119 -147
  5. data/ext/datadog_ci_native/datadog_cov.h +3 -0
  6. data/ext/datadog_ci_native/datadog_source_code.c +28 -0
  7. data/ext/datadog_ci_native/datadog_source_code.h +3 -0
  8. data/ext/{datadog_cov → datadog_ci_native}/extconf.rb +1 -1
  9. data/lib/datadog/ci/contrib/minitest/test.rb +17 -7
  10. data/lib/datadog/ci/contrib/rspec/example.rb +14 -7
  11. data/lib/datadog/ci/ext/environment/providers/appveyor.rb +1 -1
  12. data/lib/datadog/ci/ext/environment/providers/buildkite.rb +4 -0
  13. data/lib/datadog/ci/ext/environment/providers/gitlab.rb +0 -4
  14. data/lib/datadog/ci/ext/telemetry.rb +1 -2
  15. data/lib/datadog/ci/ext/test.rb +1 -0
  16. data/lib/datadog/ci/git/base_branch_sha_detection/base.rb +66 -0
  17. data/lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb +34 -0
  18. data/lib/datadog/ci/git/base_branch_sha_detection/guesser.rb +137 -0
  19. data/lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb +29 -0
  20. data/lib/datadog/ci/git/base_branch_sha_detector.rb +63 -0
  21. data/lib/datadog/ci/git/changed_lines.rb +109 -0
  22. data/lib/datadog/ci/git/cli.rb +56 -0
  23. data/lib/datadog/ci/git/diff.rb +90 -0
  24. data/lib/datadog/ci/git/local_repository.rb +94 -321
  25. data/lib/datadog/ci/git/telemetry.rb +14 -0
  26. data/lib/datadog/ci/impacted_tests_detection/component.rb +15 -11
  27. data/lib/datadog/ci/test.rb +16 -0
  28. data/lib/datadog/ci/test_optimisation/component.rb +10 -6
  29. data/lib/datadog/ci/test_optimisation/coverage/ddcov.rb +1 -1
  30. data/lib/datadog/ci/test_visibility/telemetry.rb +3 -0
  31. data/lib/datadog/ci/utils/command.rb +117 -0
  32. data/lib/datadog/ci/utils/source_code.rb +31 -0
  33. data/lib/datadog/ci/version.rb +1 -1
  34. metadata +18 -5
  35. data/lib/datadog/ci/impacted_tests_detection/telemetry.rb +0 -16
@@ -1,10 +1,7 @@
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
- require_relative "telemetry"
8
5
 
9
6
  module Datadog
10
7
  module CI
@@ -12,7 +9,7 @@ module Datadog
12
9
  class Component
13
10
  def initialize(enabled:)
14
11
  @enabled = enabled
15
- @changed_files = Set.new
12
+ @git_diff = Git::Diff.new
16
13
  end
17
14
 
18
15
  def configure(library_settings, test_session)
@@ -30,21 +27,21 @@ module Datadog
30
27
  return
31
28
  end
32
29
 
33
- changed_files = Git::LocalRepository.get_changed_files_from_diff(base_commit_sha)
34
- if changed_files.nil?
30
+ git_diff = Git::LocalRepository.get_changes_since(base_commit_sha)
31
+ if git_diff.empty?
35
32
  Datadog.logger.debug { "Impacted tests detection disabled: could not get changed files" }
36
33
  @enabled = false
37
34
  return
38
35
  end
39
36
 
40
37
  Datadog.logger.debug do
41
- "Impacted tests detection: found #{changed_files.size} changed files"
38
+ "Impacted tests detection: found #{git_diff.size} changed files"
42
39
  end
43
40
  Datadog.logger.debug do
44
- "Impacted tests detection: changed files: #{changed_files.inspect}"
41
+ "Impacted tests detection: changed files: #{git_diff.inspect}"
45
42
  end
46
43
 
47
- @changed_files = changed_files
44
+ @git_diff = git_diff
48
45
  @enabled = true
49
46
  end
50
47
 
@@ -58,7 +55,15 @@ module Datadog
58
55
  source_file = test_span.source_file
59
56
  return false if source_file.nil?
60
57
 
61
- @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
62
67
  end
63
68
 
64
69
  def tag_modified_test(test_span)
@@ -69,7 +74,6 @@ module Datadog
69
74
  end
70
75
 
71
76
  test_span.set_tag(Ext::Test::TAG_TEST_IS_MODIFIED, "true")
72
- Telemetry.impacted_test_detected
73
77
  end
74
78
 
75
79
  private
@@ -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?
@@ -150,7 +150,7 @@ module Datadog
150
150
  def skippable?(datadog_test_id)
151
151
  return false if !enabled? || !skipping_tests?
152
152
 
153
- @skippable_tests.include?(datadog_test_id)
153
+ @mutex.synchronize { @skippable_tests.include?(datadog_test_id) }
154
154
  end
155
155
 
156
156
  def mark_if_skippable(test)
@@ -200,8 +200,10 @@ module Datadog
200
200
  end
201
201
 
202
202
  def restore_state(state)
203
- @correlation_id = state[:correlation_id]
204
- @skippable_tests = state[:skippable_tests]
203
+ @mutex.synchronize do
204
+ @correlation_id = state[:correlation_id]
205
+ @skippable_tests = state[:skippable_tests]
206
+ end
205
207
  end
206
208
 
207
209
  def storage_key
@@ -225,7 +227,7 @@ module Datadog
225
227
  end
226
228
 
227
229
  def load_datadog_cov!
228
- require "datadog_cov.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
230
+ require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
229
231
 
230
232
  Datadog.logger.debug("Loaded Datadog code coverage collector, using coverage mode: #{code_coverage_mode}")
231
233
  rescue LoadError => e
@@ -253,8 +255,10 @@ module Datadog
253
255
  .fetch_skippable_tests(test_session)
254
256
  @skippable_tests_fetch_error = skippable_response.error_message unless skippable_response.ok?
255
257
 
256
- @correlation_id = skippable_response.correlation_id
257
- @skippable_tests = skippable_response.tests
258
+ @mutex.synchronize do
259
+ @correlation_id = skippable_response.correlation_id
260
+ @skippable_tests = skippable_response.tests
261
+ end
258
262
 
259
263
  Datadog.logger.debug { "Fetched skippable tests: \n #{@skippable_tests}" }
260
264
  Datadog.logger.debug { "Found #{@skippable_tests.count} skippable tests." }
@@ -5,7 +5,7 @@ module Datadog
5
5
  module TestOptimisation
6
6
  module Coverage
7
7
  # Placeholder for code coverage collection
8
- # Implementation in ext/datadog_cov
8
+ # Implementation in ext/datadog_ci_native/datadog_cov.c
9
9
  class DDCov
10
10
  end
11
11
  end
@@ -75,6 +75,9 @@ module Datadog
75
75
  tags[Ext::Telemetry::TAG_IS_TEST_DISABLED] = "true" if span.get_tag(Ext::Test::TAG_IS_TEST_DISABLED)
76
76
  tags[Ext::Telemetry::TAG_HAS_FAILED_ALL_RETRIES] = "true" if span.get_tag(Ext::Test::TAG_HAS_FAILED_ALL_RETRIES)
77
77
 
78
+ # impacted tests tags
79
+ tags[Ext::Telemetry::TAG_IS_MODIFIED] = "true" if span.get_tag(Ext::Test::TAG_TEST_IS_MODIFIED)
80
+
78
81
  tags
79
82
  end
80
83
 
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "datadog/core/utils/time"
5
+
6
+ module Datadog
7
+ module CI
8
+ module Utils
9
+ # Provides a way to call external commands with timeout
10
+ module Command
11
+ DEFAULT_TIMEOUT = 10 # seconds
12
+ BUFFER_SIZE = 1024
13
+
14
+ OPEN_STDIN_RETRY_COUNT = 3
15
+
16
+ # Executes a command with optional timeout and stdin data
17
+ #
18
+ # @param command [Array<String>] Command to execute.
19
+ # @param stdin_data [String, nil] Data to write to stdin
20
+ # @param timeout [Integer] Maximum execution time in seconds
21
+ # @return [Array<String, Process::Status?>] Output and exit status
22
+ #
23
+ # @example Safe usage with array (recommended)
24
+ # Command.exec_command(["git", "log", "-n", "1"])
25
+ #
26
+ #
27
+ def self.exec_command(command, stdin_data: nil, timeout: DEFAULT_TIMEOUT)
28
+ output = +""
29
+ exit_value = nil
30
+ timeout_reached = false
31
+
32
+ begin
33
+ start = Core::Utils::Time.get_time
34
+
35
+ _, stderrout, thread = popen_with_stdin(command, stdin_data: stdin_data)
36
+ pid = thread[:pid]
37
+
38
+ # wait for output and read from stdout/stderr
39
+ while (Core::Utils::Time.get_time - start) < timeout
40
+ # wait for data to appear in stderrout channel
41
+ # maximum wait time 100ms
42
+ Kernel.select([stderrout], [], [], 0.1)
43
+
44
+ begin
45
+ output << stderrout.read_nonblock(1024)
46
+ rescue IO::WaitReadable
47
+ rescue EOFError
48
+ # we're done here, we return from this cycle when we processed the whole output of the command
49
+ break
50
+ end
51
+ end
52
+
53
+ if (Core::Utils::Time.get_time - start) > timeout
54
+ timeout_reached = true
55
+ end
56
+
57
+ if thread.alive?
58
+ begin
59
+ Process.kill("TERM", pid)
60
+ rescue
61
+ # Process already terminated
62
+ end
63
+ end
64
+
65
+ thread.join(1)
66
+ exit_value = thread.value
67
+ rescue Errno::EPIPE
68
+ return ["Error writing to stdin", nil]
69
+ ensure
70
+ stderrout&.close
71
+ end
72
+
73
+ # we read command's output as binary so now we need to set an appropriate encoding for the result
74
+ encoding = Encoding.default_external
75
+
76
+ # Sometimes Encoding.default_external is somehow set to US-ASCII which breaks
77
+ # commit messages with UTF-8 characters like emojis
78
+ # We force output's encoding to be UTF-8 in this case
79
+ # This is safe to do as UTF-8 is compatible with US-ASCII
80
+ if Encoding.default_external == Encoding::US_ASCII
81
+ encoding = Encoding::UTF_8
82
+ end
83
+
84
+ output.force_encoding(encoding)
85
+ output.strip! # There's always a "\n" at the end of the command output
86
+
87
+ if timeout_reached && output.empty?
88
+ output = "Command timed out after #{timeout} seconds"
89
+ end
90
+
91
+ [output, exit_value]
92
+ end
93
+
94
+ def self.popen_with_stdin(command, stdin_data: nil, retries_left: OPEN_STDIN_RETRY_COUNT)
95
+ stdin = nil
96
+ result = Open3.popen2e(*command)
97
+ stdin = result.first
98
+
99
+ # write input to stdin
100
+ begin
101
+ stdin.write(stdin_data) if stdin_data
102
+ rescue Errno::EPIPE => e
103
+ if retries_left > 0
104
+ return popen_with_stdin(command, stdin_data: stdin_data, retries_left: retries_left - 1)
105
+ else
106
+ raise e
107
+ end
108
+ end
109
+
110
+ result
111
+ ensure
112
+ stdin&.close
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Utils
6
+ module SourceCode
7
+ begin
8
+ require "datadog_ci_native.#{RUBY_VERSION}_#{RUBY_PLATFORM}"
9
+
10
+ LAST_LINE_AVAILABLE = true
11
+ rescue LoadError
12
+ LAST_LINE_AVAILABLE = false
13
+ end
14
+
15
+ def self.last_line(target)
16
+ return nil if target.nil?
17
+ return nil unless LAST_LINE_AVAILABLE
18
+
19
+ # Ruby has outdated RBS for RubyVM::InstructionSequence where method `of` is not defined
20
+ # steep:ignore:start
21
+ iseq = RubyVM::InstructionSequence.of(target)
22
+ return nil unless iseq.is_a?(RubyVM::InstructionSequence)
23
+ # steep:ignore:end
24
+
25
+ # this function is implemented in ext/datadog_ci_native/datadog_source_code.c
26
+ _native_last_line_from_iseq(iseq)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,7 +4,7 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 17
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.17.0
4
+ version: 1.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
@@ -60,7 +60,7 @@ email:
60
60
  executables:
61
61
  - ddcirb
62
62
  extensions:
63
- - ext/datadog_cov/extconf.rb
63
+ - ext/datadog_ci_native/extconf.rb
64
64
  extra_rdoc_files: []
65
65
  files:
66
66
  - CHANGELOG.md
@@ -71,8 +71,12 @@ files:
71
71
  - NOTICE
72
72
  - README.md
73
73
  - exe/ddcirb
74
- - ext/datadog_cov/datadog_cov.c
75
- - ext/datadog_cov/extconf.rb
74
+ - ext/datadog_ci_native/ci.c
75
+ - ext/datadog_ci_native/datadog_cov.c
76
+ - ext/datadog_ci_native/datadog_cov.h
77
+ - ext/datadog_ci_native/datadog_source_code.c
78
+ - ext/datadog_ci_native/datadog_source_code.h
79
+ - ext/datadog_ci_native/extconf.rb
76
80
  - lib/datadog/ci.rb
77
81
  - lib/datadog/ci/async_writer.rb
78
82
  - lib/datadog/ci/auto_instrument.rb
@@ -187,6 +191,14 @@ files:
187
191
  - lib/datadog/ci/ext/telemetry.rb
188
192
  - lib/datadog/ci/ext/test.rb
189
193
  - lib/datadog/ci/ext/transport.rb
194
+ - lib/datadog/ci/git/base_branch_sha_detection/base.rb
195
+ - lib/datadog/ci/git/base_branch_sha_detection/branch_metric.rb
196
+ - lib/datadog/ci/git/base_branch_sha_detection/guesser.rb
197
+ - lib/datadog/ci/git/base_branch_sha_detection/merge_base_extractor.rb
198
+ - lib/datadog/ci/git/base_branch_sha_detector.rb
199
+ - lib/datadog/ci/git/changed_lines.rb
200
+ - lib/datadog/ci/git/cli.rb
201
+ - lib/datadog/ci/git/diff.rb
190
202
  - lib/datadog/ci/git/local_repository.rb
191
203
  - lib/datadog/ci/git/packfiles.rb
192
204
  - lib/datadog/ci/git/search_commits.rb
@@ -195,7 +207,6 @@ files:
195
207
  - lib/datadog/ci/git/upload_packfile.rb
196
208
  - lib/datadog/ci/git/user.rb
197
209
  - lib/datadog/ci/impacted_tests_detection/component.rb
198
- - lib/datadog/ci/impacted_tests_detection/telemetry.rb
199
210
  - lib/datadog/ci/logs/component.rb
200
211
  - lib/datadog/ci/logs/transport.rb
201
212
  - lib/datadog/ci/readonly_test_module.rb
@@ -265,11 +276,13 @@ files:
265
276
  - lib/datadog/ci/transport/http.rb
266
277
  - lib/datadog/ci/transport/telemetry.rb
267
278
  - lib/datadog/ci/utils/bundle.rb
279
+ - lib/datadog/ci/utils/command.rb
268
280
  - lib/datadog/ci/utils/configuration.rb
269
281
  - lib/datadog/ci/utils/file_storage.rb
270
282
  - lib/datadog/ci/utils/git.rb
271
283
  - lib/datadog/ci/utils/parsing.rb
272
284
  - lib/datadog/ci/utils/rum.rb
285
+ - lib/datadog/ci/utils/source_code.rb
273
286
  - lib/datadog/ci/utils/stateful.rb
274
287
  - lib/datadog/ci/utils/telemetry.rb
275
288
  - lib/datadog/ci/utils/test_run.rb
@@ -1,16 +0,0 @@
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