datadog-ci 1.0.0.beta1 → 1.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
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"