datadog-ci 1.0.0.beta1 → 1.0.0.beta3

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -60
  3. data/README.md +37 -46
  4. data/lib/datadog/ci/configuration/components.rb +51 -9
  5. data/lib/datadog/ci/configuration/settings.rb +6 -0
  6. data/lib/datadog/ci/contrib/cucumber/formatter.rb +10 -5
  7. data/lib/datadog/ci/contrib/cucumber/patcher.rb +3 -0
  8. data/lib/datadog/ci/contrib/cucumber/step.rb +27 -0
  9. data/lib/datadog/ci/contrib/minitest/patcher.rb +2 -2
  10. data/lib/datadog/ci/contrib/minitest/test.rb +105 -0
  11. data/lib/datadog/ci/contrib/rspec/example.rb +11 -5
  12. data/lib/datadog/ci/ext/environment/providers/local_git.rb +8 -79
  13. data/lib/datadog/ci/ext/environment.rb +11 -16
  14. data/lib/datadog/ci/ext/settings.rb +1 -0
  15. data/lib/datadog/ci/ext/test.rb +8 -1
  16. data/lib/datadog/ci/ext/transport.rb +8 -0
  17. data/lib/datadog/ci/git/local_repository.rb +238 -0
  18. data/lib/datadog/ci/git/packfiles.rb +70 -0
  19. data/lib/datadog/ci/git/search_commits.rb +77 -0
  20. data/lib/datadog/ci/git/tree_uploader.rb +90 -0
  21. data/lib/datadog/ci/git/upload_packfile.rb +66 -0
  22. data/lib/datadog/ci/git/user.rb +29 -0
  23. data/lib/datadog/ci/itr/coverage/event.rb +18 -1
  24. data/lib/datadog/ci/itr/coverage/writer.rb +114 -0
  25. data/lib/datadog/ci/itr/runner.rb +134 -11
  26. data/lib/datadog/ci/itr/skippable.rb +108 -0
  27. data/lib/datadog/ci/span.rb +16 -0
  28. data/lib/datadog/ci/test.rb +37 -12
  29. data/lib/datadog/ci/test_module.rb +2 -2
  30. data/lib/datadog/ci/test_session.rb +2 -2
  31. data/lib/datadog/ci/test_suite.rb +2 -2
  32. data/lib/datadog/ci/test_visibility/null_recorder.rb +4 -1
  33. data/lib/datadog/ci/test_visibility/recorder.rb +47 -9
  34. data/lib/datadog/ci/test_visibility/serializers/base.rb +3 -2
  35. data/lib/datadog/ci/test_visibility/serializers/factories/test_level.rb +3 -3
  36. data/lib/datadog/ci/test_visibility/serializers/factories/test_suite_level.rb +6 -6
  37. data/lib/datadog/ci/test_visibility/serializers/test_v2.rb +14 -2
  38. data/lib/datadog/ci/test_visibility/transport.rb +6 -2
  39. data/lib/datadog/ci/transport/http.rb +24 -4
  40. data/lib/datadog/ci/transport/remote_settings_api.rb +14 -6
  41. data/lib/datadog/ci/utils/configuration.rb +2 -2
  42. data/lib/datadog/ci/utils/git.rb +6 -67
  43. data/lib/datadog/ci/utils/parsing.rb +16 -0
  44. data/lib/datadog/ci/utils/test_run.rb +25 -0
  45. data/lib/datadog/ci/version.rb +1 -1
  46. data/lib/datadog/ci/worker.rb +35 -0
  47. data/lib/datadog/ci.rb +4 -0
  48. metadata +17 -6
  49. data/lib/datadog/ci/contrib/minitest/hooks.rb +0 -75
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../ext/test"
4
- require_relative "../../utils/git"
4
+ require_relative "../../git/local_repository"
5
+ require_relative "../../utils/test_run"
5
6
  require_relative "ext"
6
7
 
7
8
  module Datadog
@@ -55,13 +56,18 @@ module Datadog
55
56
  def on_test_case_started(event)
56
57
  test_suite_name = test_suite_name(event.test_case)
57
58
 
59
+ # @type var tags: Hash[String, String]
58
60
  tags = {
59
61
  CI::Ext::Test::TAG_FRAMEWORK => Ext::FRAMEWORK,
60
62
  CI::Ext::Test::TAG_FRAMEWORK_VERSION => CI::Contrib::Cucumber::Integration.version.to_s,
61
- CI::Ext::Test::TAG_SOURCE_FILE => Utils::Git.relative_to_root(event.test_case.location.file),
63
+ CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(event.test_case.location.file),
62
64
  CI::Ext::Test::TAG_SOURCE_START => event.test_case.location.line.to_s
63
65
  }
64
66
 
67
+ if (parameters = extract_parameters_hash(event.test_case))
68
+ tags[CI::Ext::Test::TAG_PARAMETERS] = Utils::TestRun.test_parameters(arguments: parameters)
69
+ end
70
+
65
71
  start_test_suite(test_suite_name) unless same_test_suite_as_current?(test_suite_name)
66
72
 
67
73
  test_span = CI.start_test(
@@ -70,9 +76,8 @@ module Datadog
70
76
  tags: tags,
71
77
  service: configuration[:service_name]
72
78
  )
73
-
74
- if (parameters = extract_parameters_hash(event.test_case))
75
- test_span&.set_parameters(parameters)
79
+ if event.test_case.match_tags?("@#{CI::Ext::Test::ITR_UNSKIPPABLE_OPTION}")
80
+ test_span&.itr_unskippable!
76
81
  end
77
82
  end
78
83
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "datadog/tracing/contrib/patcher"
4
+
4
5
  require_relative "instrumentation"
6
+ require_relative "step"
5
7
 
6
8
  module Datadog
7
9
  module CI
@@ -19,6 +21,7 @@ module Datadog
19
21
 
20
22
  def patch
21
23
  ::Cucumber::Runtime.include(Instrumentation)
24
+ ::Cucumber::Core::Test::Step.include(Datadog::CI::Contrib::Cucumber::Step)
22
25
  end
23
26
  end
24
27
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ module Cucumber
7
+ # instruments Cucumber::Core::Test::Step from cucumber-ruby-core to change
8
+ module Step
9
+ def self.included(base)
10
+ base.prepend(InstanceMethods)
11
+ end
12
+
13
+ module InstanceMethods
14
+ def execute(*args)
15
+ test_span = CI.active_test
16
+ if test_span&.skipped_by_itr?
17
+ @action.skip(*args)
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "runner"
4
4
  require_relative "reporter"
5
- require_relative "hooks"
5
+ require_relative "test"
6
6
  require_relative "runnable"
7
7
 
8
8
  module Datadog
@@ -25,7 +25,7 @@ module Datadog
25
25
  # test suites (when not executed concurrently)
26
26
  ::Minitest::Runnable.include(Runnable)
27
27
  # tests; test suites (when executed concurrently)
28
- ::Minitest::Test.include(Hooks)
28
+ ::Minitest::Test.include(Test)
29
29
  # test session finish
30
30
  ::Minitest::CompositeReporter.include(Reporter)
31
31
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../ext/test"
4
+ require_relative "../../git/local_repository"
5
+ require_relative "ext"
6
+ require_relative "helpers"
7
+
8
+ module Datadog
9
+ module CI
10
+ module Contrib
11
+ module Minitest
12
+ # Lifecycle hooks to instrument Minitest::Test
13
+ module Test
14
+ def self.included(base)
15
+ base.prepend(InstanceMethods)
16
+ base.singleton_class.prepend(ClassMethods)
17
+ end
18
+
19
+ module InstanceMethods
20
+ def before_setup
21
+ super
22
+ return unless datadog_configuration[:enabled]
23
+
24
+ test_suite_name = Helpers.test_suite_name(self.class, name)
25
+ if Helpers.parallel?(self.class)
26
+ test_suite_name = "#{test_suite_name} (#{name} concurrently)"
27
+
28
+ # for parallel execution we need to start a new test suite for each test
29
+ CI.start_test_suite(test_suite_name)
30
+ end
31
+
32
+ source_file, line_number = method(name).source_location
33
+
34
+ test_span = CI.start_test(
35
+ name,
36
+ test_suite_name,
37
+ tags: {
38
+ CI::Ext::Test::TAG_FRAMEWORK => Ext::FRAMEWORK,
39
+ CI::Ext::Test::TAG_FRAMEWORK_VERSION => CI::Contrib::Minitest::Integration.version.to_s,
40
+ CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(source_file),
41
+ CI::Ext::Test::TAG_SOURCE_START => line_number.to_s
42
+ },
43
+ service: datadog_configuration[:service_name]
44
+ )
45
+ test_span&.itr_unskippable! if self.class.dd_suite_unskippable? || self.class.dd_test_unskippable?(name)
46
+ skip(CI::Ext::Test::ITR_TEST_SKIP_REASON) if test_span&.skipped_by_itr?
47
+ end
48
+
49
+ def after_teardown
50
+ test_span = CI.active_test
51
+ return super unless test_span
52
+
53
+ finish_with_result(test_span, result_code)
54
+ if Helpers.parallel?(self.class)
55
+ finish_with_result(test_span.test_suite, result_code)
56
+ end
57
+
58
+ super
59
+ end
60
+
61
+ private
62
+
63
+ def finish_with_result(span, result_code)
64
+ return unless span
65
+
66
+ case result_code
67
+ when "."
68
+ span.passed!
69
+ when "E", "F"
70
+ span.failed!(exception: failure)
71
+ when "S"
72
+ span.skipped!(reason: failure.message)
73
+ end
74
+ span.finish
75
+ end
76
+
77
+ def datadog_configuration
78
+ Datadog.configuration.ci[:minitest]
79
+ end
80
+ end
81
+
82
+ module ClassMethods
83
+ def datadog_itr_unskippable(*args)
84
+ if args.nil? || args.empty?
85
+ @datadog_itr_unskippable_suite = true
86
+ else
87
+ @datadog_itr_unskippable_tests = args
88
+ end
89
+ end
90
+
91
+ def dd_suite_unskippable?
92
+ @datadog_itr_unskippable_suite
93
+ end
94
+
95
+ def dd_test_unskippable?(test_name)
96
+ return false unless @datadog_itr_unskippable_tests
97
+
98
+ @datadog_itr_unskippable_tests.include?(test_name)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../ext/test"
4
- require_relative "../../utils/git"
4
+ require_relative "../../git/local_repository"
5
+ require_relative "../../utils/test_run"
5
6
  require_relative "ext"
6
7
 
7
8
  module Datadog
@@ -41,14 +42,19 @@ module Datadog
41
42
  tags: {
42
43
  CI::Ext::Test::TAG_FRAMEWORK => Ext::FRAMEWORK,
43
44
  CI::Ext::Test::TAG_FRAMEWORK_VERSION => CI::Contrib::RSpec::Integration.version.to_s,
44
- CI::Ext::Test::TAG_SOURCE_FILE => Utils::Git.relative_to_root(metadata[:file_path]),
45
- CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s
45
+ CI::Ext::Test::TAG_SOURCE_FILE => Git::LocalRepository.relative_to_root(metadata[:file_path]),
46
+ CI::Ext::Test::TAG_SOURCE_START => metadata[:line_number].to_s,
47
+ CI::Ext::Test::TAG_PARAMETERS => Utils::TestRun.test_parameters(
48
+ metadata: {"scoped_id" => metadata[:scoped_id]}
49
+ )
46
50
  },
47
51
  service: datadog_configuration[:service_name]
48
52
  ) do |test_span|
49
- result = super
53
+ test_span&.itr_unskippable! if metadata[CI::Ext::Test::ITR_UNSKIPPABLE_OPTION]
50
54
 
51
- test_span&.set_parameters({}, {"scoped_id" => metadata[:scoped_id]})
55
+ metadata[:skip] = CI::Ext::Test::ITR_TEST_SKIP_REASON if test_span&.skipped_by_itr?
56
+
57
+ result = super
52
58
 
53
59
  case execution_result.status
54
60
  when :passed
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
- require_relative "../../../utils/git"
4
+ require_relative "../../../git/local_repository"
5
5
 
6
6
  module Datadog
7
7
  module CI
@@ -11,48 +11,23 @@ module Datadog
11
11
  # As a fallback we try to fetch git information from the local git repository
12
12
  class LocalGit < Base
13
13
  def git_repository_url
14
- Utils::Git.exec_git_command("git ls-remote --get-url")
15
- rescue => e
16
- Datadog.logger.debug(
17
- "Unable to read git repository url: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
18
- )
19
- nil
14
+ CI::Git::LocalRepository.git_repository_url
20
15
  end
21
16
 
22
17
  def git_commit_sha
23
- Utils::Git.exec_git_command("git rev-parse HEAD")
24
- rescue => e
25
- Datadog.logger.debug(
26
- "Unable to read git commit SHA: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
27
- )
28
- nil
18
+ CI::Git::LocalRepository.git_commit_sha
29
19
  end
30
20
 
31
21
  def git_branch
32
- Utils::Git.exec_git_command("git rev-parse --abbrev-ref HEAD")
33
- rescue => e
34
- Datadog.logger.debug(
35
- "Unable to read git branch: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
36
- )
37
- nil
22
+ CI::Git::LocalRepository.git_branch
38
23
  end
39
24
 
40
25
  def git_tag
41
- Utils::Git.exec_git_command("git tag --points-at HEAD")
42
- rescue => e
43
- Datadog.logger.debug(
44
- "Unable to read git tag: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
45
- )
46
- nil
26
+ CI::Git::LocalRepository.git_tag
47
27
  end
48
28
 
49
29
  def git_commit_message
50
- Utils::Git.exec_git_command("git show -s --format=%s")
51
- rescue => e
52
- Datadog.logger.debug(
53
- "Unable to read git commit message: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
54
- )
55
- nil
30
+ CI::Git::LocalRepository.git_commit_message
56
31
  end
57
32
 
58
33
  def git_commit_author_name
@@ -80,12 +55,7 @@ module Datadog
80
55
  end
81
56
 
82
57
  def workspace_path
83
- Utils::Git.exec_git_command("git rev-parse --show-toplevel")
84
- rescue => e
85
- Datadog.logger.debug(
86
- "Unable to read git base directory: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
87
- )
88
- nil
58
+ CI::Git::LocalRepository.git_root
89
59
  end
90
60
 
91
61
  private
@@ -105,48 +75,7 @@ module Datadog
105
75
  end
106
76
 
107
77
  def set_git_commit_users
108
- # Get committer and author information in one command.
109
- output = Utils::Git.exec_git_command("git show -s --format='%an\t%ae\t%at\t%cn\t%ce\t%ct'")
110
- unless output
111
- Datadog.logger.debug(
112
- "Unable to read git commit users: git command output is nil"
113
- )
114
- @author = @committer = NilUser.new
115
- return
116
- end
117
-
118
- author_name, author_email, author_timestamp,
119
- committer_name, committer_email, committer_timestamp = output.split("\t").each(&:strip!)
120
-
121
- @author = GitUser.new(author_name, author_email, author_timestamp)
122
- @committer = GitUser.new(committer_name, committer_email, committer_timestamp)
123
- rescue => e
124
- Datadog.logger.debug(
125
- "Unable to read git commit users: #{e.class.name} #{e.message} at #{Array(e.backtrace).first}"
126
- )
127
- @author = @committer = NilUser.new
128
- end
129
-
130
- class GitUser
131
- attr_reader :name, :email, :timestamp
132
-
133
- def initialize(name, email, timestamp)
134
- @name = name
135
- @email = email
136
- @timestamp = timestamp
137
- end
138
-
139
- def date
140
- return nil if timestamp.nil?
141
-
142
- Time.at(timestamp.to_i).utc.to_datetime.iso8601
143
- end
144
- end
145
-
146
- class NilUser < GitUser
147
- def initialize
148
- super(nil, nil, nil)
149
- end
78
+ @author, @committer = CI::Git::LocalRepository.git_commit_users
150
79
  end
151
80
  end
152
81
  end
@@ -3,6 +3,8 @@
3
3
  require_relative "git"
4
4
  require_relative "environment/extractor"
5
5
 
6
+ require_relative "../utils/git"
7
+
6
8
  module Datadog
7
9
  module CI
8
10
  module Ext
@@ -21,8 +23,6 @@ module Datadog
21
23
  TAG_NODE_NAME = "ci.node.name"
22
24
  TAG_CI_ENV_VARS = "_dd.ci.env_vars"
23
25
 
24
- HEX_NUMBER_REGEXP = /[0-9a-f]{40}/i.freeze
25
-
26
26
  module_function
27
27
 
28
28
  def tags(env)
@@ -57,24 +57,19 @@ module Datadog
57
57
  end
58
58
 
59
59
  def validate_git_sha(git_sha)
60
- message = "DD_GIT_COMMIT_SHA must be a full-length git SHA."
60
+ return if Utils::Git.valid_commit_sha?(git_sha)
61
61
 
62
- if git_sha.nil? || git_sha.empty?
63
- message += " No value was set and no SHA was automatically extracted."
64
- Datadog.logger.error(message)
65
- return
66
- end
62
+ message = "DD_GIT_COMMIT_SHA must be a full-length git SHA."
67
63
 
68
- if git_sha.length < Git::SHA_LENGTH
69
- message += " Expected SHA length #{Git::SHA_LENGTH}, was #{git_sha.length}."
70
- Datadog.logger.error(message)
71
- return
64
+ message += if git_sha.nil? || git_sha.empty?
65
+ " No value was set and no SHA was automatically extracted."
66
+ elsif git_sha.length < Git::SHA_LENGTH
67
+ " Expected SHA length #{Git::SHA_LENGTH}, was #{git_sha.length}."
68
+ else
69
+ " Expected SHA to be a valid HEX number, got #{git_sha}."
72
70
  end
73
71
 
74
- unless HEX_NUMBER_REGEXP.match?(git_sha)
75
- message += " Expected SHA to be a valid HEX number, got #{git_sha}."
76
- Datadog.logger.error(message)
77
- end
72
+ Datadog.logger.error(message)
78
73
  end
79
74
  end
80
75
  end
@@ -11,6 +11,7 @@ module Datadog
11
11
  ENV_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED = "DD_CIVISIBILITY_EXPERIMENTAL_TEST_SUITE_LEVEL_VISIBILITY_ENABLED"
12
12
  ENV_FORCE_TEST_LEVEL_VISIBILITY = "DD_CIVISIBILITY_FORCE_TEST_LEVEL_VISIBILITY"
13
13
  ENV_ITR_ENABLED = "DD_CIVISIBILITY_ITR_ENABLED"
14
+ ENV_GIT_METADATA_UPLOAD_ENABLED = "DD_CIVISIBILITY_GIT_METADATA_UPLOAD_ENABLED"
14
15
 
15
16
  # Source: https://docs.datadoghq.com/getting_started/site/
16
17
  DD_SITE_ALLOWLIST = [
@@ -8,7 +8,6 @@ module Datadog
8
8
  module Test
9
9
  CONTEXT_ORIGIN = "ciapp-test"
10
10
 
11
- TAG_ARGUMENTS = "test.arguments"
12
11
  TAG_FRAMEWORK = "test.framework"
13
12
  TAG_FRAMEWORK_VERSION = "test.framework_version"
14
13
  TAG_NAME = "test.name"
@@ -26,6 +25,11 @@ module Datadog
26
25
  # ITR tags
27
26
  TAG_ITR_TEST_SKIPPING_ENABLED = "test.itr.tests_skipping.enabled"
28
27
  TAG_ITR_TEST_SKIPPING_TYPE = "test.itr.tests_skipping.type"
28
+ TAG_ITR_TEST_SKIPPING_COUNT = "test.itr.tests_skipping.count"
29
+ TAG_ITR_SKIPPED_BY_ITR = "test.skipped_by_itr"
30
+ TAG_ITR_TESTS_SKIPPED = "_dd.ci.itr.tests_skipped"
31
+ TAG_ITR_UNSKIPPABLE = "test.itr.unskippable"
32
+ TAG_ITR_FORCED_RUN = "test.itr.forced_run"
29
33
 
30
34
  # Code coverage tags
31
35
  TAG_CODE_COVERAGE_ENABLED = "test.code_coverage.enabled"
@@ -43,6 +47,7 @@ module Datadog
43
47
  # Environment runtime tags
44
48
  TAG_OS_ARCHITECTURE = "os.architecture"
45
49
  TAG_OS_PLATFORM = "os.platform"
50
+ TAG_OS_VERSION = "os.version"
46
51
  TAG_RUNTIME_NAME = "runtime.name"
47
52
  TAG_RUNTIME_VERSION = "runtime.version"
48
53
 
@@ -53,6 +58,8 @@ module Datadog
53
58
  # could be either "test" or "suite" depending on whether we skip individual tests or whole suites
54
59
  # we use test skipping for Ruby
55
60
  ITR_TEST_SKIPPING_MODE = "test"
61
+ ITR_TEST_SKIP_REASON = "Skipped by Datadog's intelligent test runner"
62
+ ITR_UNSKIPPABLE_OPTION = :datadog_itr_unskippable
56
63
 
57
64
  # test status as recognized by Datadog
58
65
  module Status
@@ -27,6 +27,7 @@ module Datadog
27
27
  TEST_COVERAGE_INTAKE_PATH = "/api/v2/citestcov"
28
28
 
29
29
  DD_API_HOST_PREFIX = "api"
30
+
30
31
  DD_API_SETTINGS_PATH = "/api/v2/libraries/tests/services/setting"
31
32
  DD_API_SETTINGS_TYPE = "ci_app_test_service_libraries_settings"
32
33
  DD_API_SETTINGS_RESPONSE_DIG_KEYS = %w[data attributes].freeze
@@ -36,6 +37,13 @@ module Datadog
36
37
  DD_API_SETTINGS_RESPONSE_REQUIRE_GIT_KEY = "require_git"
37
38
  DD_API_SETTINGS_RESPONSE_DEFAULT = {DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY => false}.freeze
38
39
 
40
+ DD_API_GIT_SEARCH_COMMITS_PATH = "/api/v2/git/repository/search_commits"
41
+
42
+ DD_API_GIT_UPLOAD_PACKFILE_PATH = "/api/v2/git/repository/packfile"
43
+
44
+ DD_API_SKIPPABLE_TESTS_PATH = "/api/v2/ci/tests/skippable"
45
+ DD_API_SKIPPABLE_TESTS_TYPE = "test_params"
46
+
39
47
  CONTENT_TYPE_MESSAGEPACK = "application/msgpack"
40
48
  CONTENT_TYPE_JSON = "application/json"
41
49
  CONTENT_TYPE_MULTIPART_FORM_DATA = "multipart/form-data"