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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/lib/datadog/ci/code_coverage/component.rb +55 -0
  4. data/lib/datadog/ci/code_coverage/null_component.rb +24 -0
  5. data/lib/datadog/ci/code_coverage/transport.rb +66 -0
  6. data/lib/datadog/ci/configuration/components.rb +17 -1
  7. data/lib/datadog/ci/configuration/settings.rb +20 -2
  8. data/lib/datadog/ci/contrib/minitest/test.rb +1 -1
  9. data/lib/datadog/ci/contrib/rspec/example.rb +48 -8
  10. data/lib/datadog/ci/contrib/rspec/example_group.rb +63 -31
  11. data/lib/datadog/ci/contrib/simplecov/ext.rb +2 -0
  12. data/lib/datadog/ci/contrib/simplecov/patcher.rb +2 -0
  13. data/lib/datadog/ci/contrib/simplecov/report_uploader.rb +59 -0
  14. data/lib/datadog/ci/ext/environment/providers/github_actions.rb +65 -2
  15. data/lib/datadog/ci/ext/environment.rb +10 -0
  16. data/lib/datadog/ci/ext/settings.rb +2 -0
  17. data/lib/datadog/ci/ext/telemetry.rb +5 -0
  18. data/lib/datadog/ci/ext/test.rb +0 -5
  19. data/lib/datadog/ci/ext/transport.rb +4 -0
  20. data/lib/datadog/ci/git/cli.rb +59 -1
  21. data/lib/datadog/ci/remote/component.rb +6 -1
  22. data/lib/datadog/ci/remote/library_settings.rb +8 -0
  23. data/lib/datadog/ci/test.rb +27 -18
  24. data/lib/datadog/ci/test_management/component.rb +9 -0
  25. data/lib/datadog/ci/test_management/null_component.rb +8 -0
  26. data/lib/datadog/ci/test_optimisation/component.rb +168 -16
  27. data/lib/datadog/ci/test_optimisation/null_component.rb +19 -5
  28. data/lib/datadog/ci/test_suite.rb +16 -21
  29. data/lib/datadog/ci/test_visibility/component.rb +1 -2
  30. data/lib/datadog/ci/test_visibility/known_tests.rb +59 -6
  31. data/lib/datadog/ci/transport/api/agentless.rb +8 -1
  32. data/lib/datadog/ci/transport/api/base.rb +21 -0
  33. data/lib/datadog/ci/transport/api/builder.rb +5 -1
  34. data/lib/datadog/ci/transport/api/evp_proxy.rb +8 -0
  35. data/lib/datadog/ci/version.rb +1 -1
  36. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1f81920de6f72669d918ab0ea703312868d380d069df7d17f131c0d64e6925a
4
- data.tar.gz: 90b37b85ef042fea0b15987d98fa5cf35aad89c599c54ad6ef01d7151dee1386
3
+ metadata.gz: 47e2dae93f942112cea718e6772f7c586214d97a4d3fb9d8ecf9643826c4e9a0
4
+ data.tar.gz: 80558ee95a33818f80f937981b9c0533c8e638444ac181a31683ea22d53c7ad7
5
5
  SHA512:
6
- metadata.gz: 3cd6c023dcd0c239605084ffc9822766ebbde643af95a44bfc2362e8b4a8339cb30ceef649791a1392322de5c0c25ab5d42b80cdbb777ff1f9da1eed717962a7
7
- data.tar.gz: f5a196eadb710b862e99d59560971ef5dc35bf5ace1a43733558be8bf1f8a467055b61c637d283ec9ed5cd1f511b6c734fb4085e28c518696ed31a0e90fbec14
6
+ metadata.gz: d442d8921f4ba7fd8ae372544f518edeaac97a6a4b89aa52d2cc8e6ff8035e3131d61104fc4bce7030634970fc2cd52a13d929944ad08497e7abfcf174634ccb
7
+ data.tar.gz: 4f58631b0792099fdff630a65c10f162e3c933ac2e5c32b53addbaebc4533daf4c87ae7112b69abbd4cf113257a4ac1cc58dc59fb2f8b63184ba3f9abb1aec8d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.27.0] - 2026-01-22
4
+
5
+
6
+ ### Added
7
+
8
+ * Add context-level code coverage tracking for RSpec to improve Test Impact Analysis correctness ([#460][])
9
+ * Add automatic uploading of code coverage reports to Datadog Code Coverage product ([#456][])
10
+
11
+ ### Changed
12
+
13
+ * Use pagination for known tests API ([#455][]) ([#458][])
14
+ * GitHub Actions: extract numeric job ID for accurate job URLs ([#457][])
15
+
16
+ ### Fixed
17
+
18
+ * RSpec: take into account Test Management disabled tests when deciding to skip context hooks ([#463][])
19
+ * Fix for attempt-to-fix flow: don't mask failures for flaky tests being fixed ([#464][])
20
+ * Pass -c safe.directory to all git commands to fix dubious ownership errors ([#454][])
21
+ * fix: rename DD_TRACE_CI_ENABLED to DD_CIVISIBILITY_ENABLED with backwards compatibility ([#453][])
22
+
3
23
  ## [1.26.0] - 2026-01-09
4
24
 
5
25
  ========== Changelog ==========
@@ -578,7 +598,8 @@ Currently test suite level visibility is not used by our instrumentation: it wil
578
598
 
579
599
  - Ruby versions < 2.7 no longer supported ([#8][])
580
600
 
581
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.26.0...main
601
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.27.0...main
602
+ [1.27.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.26.0...v1.27.0
582
603
  [1.26.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.25.0...v1.26.0
583
604
  [1.25.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.24.0...v1.25.0
584
605
  [1.24.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.3...v1.24.0
@@ -819,4 +840,13 @@ Currently test suite level visibility is not used by our instrumentation: it wil
819
840
  [#440]: https://github.com/DataDog/datadog-ci-rb/issues/440
820
841
  [#442]: https://github.com/DataDog/datadog-ci-rb/issues/442
821
842
  [#444]: https://github.com/DataDog/datadog-ci-rb/issues/444
822
- [#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
843
+ [#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
844
+ [#453]: https://github.com/DataDog/datadog-ci-rb/issues/453
845
+ [#454]: https://github.com/DataDog/datadog-ci-rb/issues/454
846
+ [#455]: https://github.com/DataDog/datadog-ci-rb/issues/455
847
+ [#456]: https://github.com/DataDog/datadog-ci-rb/issues/456
848
+ [#457]: https://github.com/DataDog/datadog-ci-rb/issues/457
849
+ [#458]: https://github.com/DataDog/datadog-ci-rb/issues/458
850
+ [#460]: https://github.com/DataDog/datadog-ci-rb/issues/460
851
+ [#463]: https://github.com/DataDog/datadog-ci-rb/issues/463
852
+ [#464]: https://github.com/DataDog/datadog-ci-rb/issues/464
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../ext/environment"
4
+ require_relative "transport"
5
+
6
+ module Datadog
7
+ module CI
8
+ module CodeCoverage
9
+ # CodeCoverage component is responsible for uploading code coverage reports
10
+ # to Datadog's Code Coverage product.
11
+ class Component
12
+ COVERAGE_REPORT_TYPE = "coverage_report"
13
+
14
+ attr_reader :enabled
15
+
16
+ def initialize(enabled:, transport:)
17
+ @enabled = enabled
18
+ @transport = transport
19
+ end
20
+
21
+ def configure(library_configuration)
22
+ @enabled &&= library_configuration.coverage_report_upload_enabled?
23
+
24
+ Datadog.logger.debug do
25
+ "[#{self.class.name}] Configured with enabled=#{@enabled}"
26
+ end
27
+ end
28
+
29
+ def upload(serialized_report:, format:)
30
+ return unless @enabled
31
+ return if serialized_report.nil?
32
+
33
+ Datadog.logger.debug { "[#{self.class.name}] Uploading coverage report..." }
34
+
35
+ event = build_event(format)
36
+
37
+ @transport.send_coverage_report(event: event, coverage_report: serialized_report)
38
+ end
39
+
40
+ def shutdown!
41
+ # noop - transport is synchronous
42
+ end
43
+
44
+ private
45
+
46
+ def build_event(format)
47
+ {
48
+ "type" => COVERAGE_REPORT_TYPE,
49
+ "format" => format
50
+ }.merge(Ext::Environment.tags(ENV))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module CodeCoverage
6
+ class NullComponent
7
+ attr_reader :enabled
8
+
9
+ def initialize
10
+ @enabled = false
11
+ end
12
+
13
+ def configure(library_configuration)
14
+ end
15
+
16
+ def upload(serialized_report:, format:)
17
+ end
18
+
19
+ def shutdown!
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../ext/telemetry"
6
+ require_relative "../ext/transport"
7
+ require_relative "../transport/gzip"
8
+ require_relative "../transport/telemetry"
9
+ require_relative "../utils/telemetry"
10
+
11
+ module Datadog
12
+ module CI
13
+ module CodeCoverage
14
+ class Transport
15
+ attr_reader :api
16
+
17
+ def initialize(api:)
18
+ @api = api
19
+ end
20
+
21
+ def send_coverage_report(event:, coverage_report:)
22
+ return nil if api.nil?
23
+
24
+ Datadog.logger.debug { "[#{self.class.name}] Sending coverage report..." }
25
+
26
+ compressed_coverage_report = CI::Transport::Gzip.compress(coverage_report)
27
+ event_json = event.to_json
28
+
29
+ response = api.cicovreprt_request(
30
+ path: Ext::Transport::CODE_COVERAGE_REPORT_INTAKE_PATH,
31
+ event_payload: event_json,
32
+ compressed_coverage_report: compressed_coverage_report
33
+ )
34
+
35
+ CI::Transport::Telemetry.api_requests(
36
+ Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST,
37
+ 1,
38
+ compressed: response.request_compressed
39
+ )
40
+ Utils::Telemetry.distribution(
41
+ Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_MS,
42
+ response.duration_ms
43
+ )
44
+ Utils::Telemetry.distribution(
45
+ Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_BYTES,
46
+ compressed_coverage_report.bytesize.to_f,
47
+ {Ext::Telemetry::TAG_REQUEST_COMPRESSED => response.request_compressed.to_s}
48
+ )
49
+
50
+ unless response.ok?
51
+ CI::Transport::Telemetry.api_requests_errors(
52
+ Ext::Telemetry::METRIC_COVERAGE_UPLOAD_REQUEST_ERRORS,
53
+ 1,
54
+ error_type: response.telemetry_error_type,
55
+ status_code: response.code
56
+ )
57
+
58
+ Datadog.logger.warn { "[#{self.class.name}] Failed to send coverage report: #{response.inspect}" }
59
+ end
60
+
61
+ response
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,6 +3,9 @@
3
3
  require "datadog/core/telemetry/ext"
4
4
 
5
5
  require_relative "../ext/settings"
6
+ require_relative "../code_coverage/component"
7
+ require_relative "../code_coverage/null_component"
8
+ require_relative "../code_coverage/transport"
6
9
  require_relative "../git/tree_uploader"
7
10
  require_relative "../impacted_tests_detection/component"
8
11
  require_relative "../impacted_tests_detection/null_component"
@@ -42,7 +45,7 @@ module Datadog
42
45
  # Adds CI behavior to Datadog trace components
43
46
  module Components
44
47
  attr_reader :test_visibility, :test_optimisation, :git_tree_upload_worker, :ci_remote, :test_retries,
45
- :test_management, :agentless_logs_submission, :impacted_tests_detection, :test_discovery
48
+ :test_management, :agentless_logs_submission, :impacted_tests_detection, :test_discovery, :code_coverage
46
49
 
47
50
  def initialize(settings)
48
51
  @test_optimisation = TestOptimisation::NullComponent.new
@@ -53,6 +56,7 @@ module Datadog
53
56
  @test_management = TestManagement::NullComponent.new
54
57
  @impacted_tests_detection = ImpactedTestsDetection::NullComponent.new
55
58
  @test_discovery = TestDiscovery::NullComponent.new
59
+ @code_coverage = CodeCoverage::NullComponent.new
56
60
 
57
61
  # Activate CI mode if enabled
58
62
  if settings.ci.enabled
@@ -69,6 +73,7 @@ module Datadog
69
73
  @test_optimisation&.shutdown!
70
74
  @agentless_logs_submission&.shutdown!
71
75
  @test_discovery&.shutdown!
76
+ @code_coverage&.shutdown!
72
77
  @git_tree_upload_worker&.stop
73
78
  end
74
79
 
@@ -158,6 +163,8 @@ module Datadog
158
163
  @agentless_logs_submission = build_agentless_logs_component(settings, test_visibility_api)
159
164
 
160
165
  @impacted_tests_detection = ImpactedTestsDetection::Component.new(enabled: settings.ci.impacted_tests_detection_enabled)
166
+
167
+ @code_coverage = build_code_coverage(settings, test_visibility_api)
161
168
  end
162
169
 
163
170
  def build_test_optimisation(settings, test_visibility_api)
@@ -283,6 +290,15 @@ module Datadog
283
290
  )
284
291
  end
285
292
 
293
+ def build_code_coverage(settings, api)
294
+ return CodeCoverage::NullComponent.new if api.nil? || settings.ci.discard_traces
295
+
296
+ CodeCoverage::Component.new(
297
+ enabled: settings.ci.code_coverage_report_upload_enabled,
298
+ transport: CodeCoverage::Transport.new(api: api)
299
+ )
300
+ end
301
+
286
302
  def build_agentless_logs_component(settings, api)
287
303
  if settings.ci.agentless_logs_submission_enabled && !settings.ci.agentless_mode_enabled
288
304
  Datadog.logger.warn(
@@ -3,6 +3,7 @@
3
3
  require_relative "../contrib/instrumentation"
4
4
  require_relative "../ext/settings"
5
5
  require_relative "../utils/bundle"
6
+ require_relative "../utils/parsing"
6
7
 
7
8
  module Datadog
8
9
  module CI
@@ -19,8 +20,19 @@ module Datadog
19
20
  settings :ci do
20
21
  option :enabled do |o|
21
22
  o.type :bool
22
- o.env CI::Ext::Settings::ENV_MODE_ENABLED
23
- o.default false
23
+ o.env CI::Ext::Settings::ENV_ENABLED
24
+ o.default do
25
+ env_value = ENV[CI::Ext::Settings::ENV_MODE_ENABLED]
26
+ if env_value && !ENV[CI::Ext::Settings::ENV_ENABLED]
27
+ Datadog::Core.log_deprecation do
28
+ "#{CI::Ext::Settings::ENV_MODE_ENABLED} environment variable is deprecated, " \
29
+ "use #{CI::Ext::Settings::ENV_ENABLED} instead."
30
+ end
31
+ Utils::Parsing.convert_to_bool(env_value)
32
+ else
33
+ false
34
+ end
35
+ end
24
36
  end
25
37
 
26
38
  option :test_session_name do |o|
@@ -174,6 +186,12 @@ module Datadog
174
186
  o.default false
175
187
  end
176
188
 
189
+ option :code_coverage_report_upload_enabled do |o|
190
+ o.type :bool
191
+ o.env CI::Ext::Settings::ENV_CODE_COVERAGE_REPORT_UPLOAD_ENABLED
192
+ o.default true
193
+ end
194
+
177
195
  define_method(:instrument) do |integration_name, options = {}, &block|
178
196
  return unless enabled
179
197
 
@@ -64,7 +64,7 @@ module Datadog
64
64
 
65
65
  finish_with_result(test_span, result_code)
66
66
 
67
- # remove failures if test passed at least once on retries or quarantined
67
+ # remove failures if failure can be ignored because of retries
68
68
  self.failures = [] if test_span.should_ignore_failures?
69
69
 
70
70
  super
@@ -39,6 +39,9 @@ module Datadog
39
39
  end_line = SourceCode::MethodInspect.last_line(@example_block)
40
40
  tags[CI::Ext::Test::TAG_SOURCE_END] = end_line.to_s if end_line
41
41
 
42
+ # we keep track of the last test failure if we encounter any
43
+ test_failure = nil
44
+
42
45
  test_retries_component.with_retries do
43
46
  test_visibility_component.trace_test(
44
47
  datadog_test_name,
@@ -46,6 +49,9 @@ module Datadog
46
49
  tags: tags,
47
50
  service: datadog_configuration[:service_name]
48
51
  ) do |test_span|
52
+ # Set context IDs on the test span for TIA context coverage merging
53
+ test_span&.context_ids = datadog_context_ids
54
+
49
55
  test_span&.itr_unskippable! if datadog_unskippable?
50
56
 
51
57
  metadata[:skip] = test_span&.datadog_skip_reason if test_span&.should_skip?
@@ -65,8 +71,10 @@ module Datadog
65
71
  test_span&.passed!
66
72
  when :failed
67
73
  test_span&.failed!(exception: execution_result.exception)
74
+
68
75
  # if any of the retries passed or test is quarantined, we don't fail the test run
69
76
  @exception = nil if test_span&.should_ignore_failures?
77
+ test_failure = @exception
70
78
  else
71
79
  # :pending or nil
72
80
  test_span&.skipped!(
@@ -92,6 +100,12 @@ module Datadog
92
100
  metadata[Ext::METADATA_DD_SKIPPED_BY_ITR] = true
93
101
  end
94
102
  end
103
+
104
+ # at this point if we have encountered any test failure in any of the previous retries
105
+ # we restore the @exception internal state if we should not skip failures for this run
106
+ if test_failure && !test_span&.should_ignore_failures?
107
+ @exception = test_failure
108
+ end
95
109
  end
96
110
  end
97
111
 
@@ -172,19 +186,45 @@ module Datadog
172
186
  Git::LocalRepository.relative_to_root(metadata[:rerun_file_path])
173
187
  end
174
188
 
189
+ # Returns list of context IDs for this example, from outermost to innermost.
190
+ # Used for merging context-level coverage into test coverage.
191
+ def datadog_context_ids
192
+ traverse_example_group_hierarchy unless defined?(@datadog_context_ids)
193
+ @datadog_context_ids
194
+ end
195
+
175
196
  private
176
197
 
177
- def fetch_top_level_example_group
198
+ def datadog_top_level_example_group
199
+ traverse_example_group_hierarchy unless defined?(@datadog_top_level_example_group)
200
+ @datadog_top_level_example_group
201
+ end
202
+
203
+ # Traverses the example group hierarchy once, populating both
204
+ # @datadog_context_ids and @top_level_example_group.
205
+ def traverse_example_group_hierarchy
206
+ context_ids = []
178
207
  example_group = metadata[:example_group]
179
- parent_example_group = example_group[:parent_example_group]
208
+ top_level = example_group
209
+
210
+ # Walk up the example group hierarchy
211
+ while example_group
212
+ # Use scoped_id as the stable identifier, fallback to file:line
213
+ context_id = example_group[:scoped_id] ||
214
+ "#{example_group[:file_path]}:#{example_group[:line_number]}"
215
+
216
+ context_ids << context_id
217
+ top_level = example_group
218
+
219
+ example_group = example_group[:parent_example_group]
220
+ end
180
221
 
181
- return example_group unless parent_example_group
222
+ @datadog_context_ids = context_ids
223
+ @datadog_top_level_example_group = top_level
182
224
 
183
- res = parent_example_group
184
- while (parent = res[:parent_example_group])
185
- res = parent
225
+ Datadog.logger.debug do
226
+ "RSpec: Built context chain for the test: #{context_ids.inspect}"
186
227
  end
187
- res
188
228
  end
189
229
 
190
230
  def datadog_integration
@@ -196,7 +236,7 @@ module Datadog
196
236
  end
197
237
 
198
238
  def datadog_test_suite_description
199
- @datadog_test_suite_description ||= fetch_top_level_example_group[:description]
239
+ @datadog_test_suite_description ||= datadog_top_level_example_group[:description]
200
240
  end
201
241
 
202
242
  def test_visibility_component
@@ -24,47 +24,79 @@ module Datadog
24
24
  # even if it is a nested context
25
25
  metadata[:skip] = true if all_examples_skipped_by_datadog?
26
26
 
27
- # return early because we create Datadog::CI::TestSuite only for top-level example groups
28
- return super unless top_level?
29
-
30
- suite_name = "#{description} at #{file_path}"
31
- suite_tags = {
32
- CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(metadata[:file_path]),
33
- CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s
34
- }
35
-
36
- test_suite =
37
- test_visibility_component&.start_test_suite(
38
- suite_name,
39
- tags: suite_tags,
40
- service: datadog_configuration[:service_name]
41
- )
42
-
43
- success = super
44
- return success unless test_suite
45
-
46
- if success && test_suite.any_passed?
47
- test_suite.passed!
48
- elsif success
49
- test_suite.skipped!
50
- else
51
- test_suite.failed!
27
+ # Start context coverage for this example group (for TIA suite-level coverage).
28
+ # This captures code executed in before(:context)/before(:all) hooks.
29
+ # The context_id uses scoped_id which is a stable identifier for RSpec example groups.
30
+ context_id = datadog_context_id
31
+ start_context_coverage(context_id)
32
+
33
+ begin
34
+ # return early because we create Datadog::CI::TestSuite only for top-level example groups
35
+ return super unless top_level?
36
+
37
+ suite_name = "#{description} at #{file_path}"
38
+ suite_tags = {
39
+ CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(metadata[:file_path]),
40
+ CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s
41
+ }
42
+
43
+ test_suite =
44
+ test_visibility_component&.start_test_suite(
45
+ suite_name,
46
+ tags: suite_tags,
47
+ service: datadog_configuration[:service_name]
48
+ )
49
+
50
+ success = super
51
+
52
+ return success unless test_suite
53
+
54
+ test_suite.finish
55
+
56
+ success
57
+ ensure
58
+ clear_context_coverage(context_id)
52
59
  end
53
-
54
- test_suite.finish
55
-
56
- success
57
60
  end
58
61
 
59
62
  private
60
63
 
61
64
  def all_examples_skipped_by_datadog?
62
65
  descendant_filtered_examples.all? do |example|
63
- !example.datadog_unskippable? && test_optimisation_component&.skippable?(example.datadog_test_id) &&
64
- !test_management_component&.attempt_to_fix?(example.datadog_fqn_test_id)
66
+ datadog_test_id = example.datadog_test_id
67
+ datadog_fqn_test_id = example.datadog_fqn_test_id
68
+
69
+ # Don't skip if the test is marked as attempt_to_fix (it should run and be retried)
70
+ next false if test_management_component&.attempt_to_fix?(datadog_fqn_test_id)
71
+
72
+ # Skip by Test Impact Analysis: test is skippable and not marked as unskippable
73
+ tia_skipped = !example.datadog_unskippable? &&
74
+ test_optimisation_component&.skippable?(datadog_test_id)
75
+
76
+ # Skip by Test Management: test is disabled
77
+ test_management_disabled = test_management_component&.disabled?(datadog_fqn_test_id)
78
+
79
+ tia_skipped || test_management_disabled
65
80
  end
66
81
  end
67
82
 
83
+ # Returns a stable context ID for this example group.
84
+ # Uses RSpec's scoped_id which uniquely identifies each example group.
85
+ def datadog_context_id
86
+ metadata[:scoped_id] || "#{metadata[:file_path]}:#{metadata[:line_number]}"
87
+ end
88
+
89
+ # Starts context coverage collection for this example group.
90
+ # This captures code executed in before(:context)/before(:all) hooks.
91
+ def start_context_coverage(context_id)
92
+ test_optimisation_component&.on_test_context_started(context_id)
93
+ end
94
+
95
+ # Clears context coverage for this example group after it finishes.
96
+ def clear_context_coverage(context_id)
97
+ test_optimisation_component&.clear_context_coverage(context_id)
98
+ end
99
+
68
100
  def datadog_configuration
69
101
  Datadog.configuration.ci[:rspec]
70
102
  end
@@ -8,6 +8,8 @@ module Datadog
8
8
  # @public_api
9
9
  module Ext
10
10
  ENV_ENABLED = "DD_CIVISIBILITY_SIMPLECOV_INSTRUMENTATION_ENABLED"
11
+
12
+ COVERAGE_FORMAT = "simplecov-internal"
11
13
  end
12
14
  end
13
15
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../patcher"
4
4
 
5
+ require_relative "report_uploader"
5
6
  require_relative "result_extractor"
6
7
 
7
8
  module Datadog
@@ -16,6 +17,7 @@ module Datadog
16
17
 
17
18
  def patch
18
19
  ::SimpleCov.include(ResultExtractor)
20
+ ::SimpleCov.include(ReportUploader)
19
21
  end
20
22
  end
21
23
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ext"
4
+
5
+ module Datadog
6
+ module CI
7
+ module Contrib
8
+ module Simplecov
9
+ # Module that hooks into SimpleCov.process_results_and_report_error to upload coverage reports
10
+ module ReportUploader
11
+ def self.included(base)
12
+ base.singleton_class.prepend(ClassMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ def process_results_and_report_error(*args)
17
+ result = super
18
+
19
+ upload_coverage_report
20
+
21
+ result
22
+ end
23
+
24
+ private
25
+
26
+ def upload_coverage_report
27
+ return unless datadog_configuration[:enabled]
28
+
29
+ code_coverage = ::Datadog.send(:components).code_coverage
30
+ return unless code_coverage.enabled
31
+
32
+ serialized_report = read_coverage_result
33
+ return if serialized_report.nil?
34
+
35
+ code_coverage.upload(serialized_report: serialized_report, format: Ext::COVERAGE_FORMAT)
36
+ rescue => e
37
+ Datadog.logger.warn("Failed to upload coverage report: #{e.message}")
38
+ end
39
+
40
+ def read_coverage_result
41
+ coverage_file = File.join(::SimpleCov.coverage_path, ".resultset.json")
42
+
43
+ unless File.exist?(coverage_file)
44
+ Datadog.logger.debug { "Coverage result file not found at #{coverage_file}" }
45
+ return nil
46
+ end
47
+
48
+ File.read(coverage_file)
49
+ end
50
+
51
+ def datadog_configuration
52
+ Datadog.configuration.ci[:simplecov]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end