datadog-ci 1.23.3 → 1.25.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: a911ce98b1577d684374a00e8efd9ecbcf34fac28f24bb543411dbb762163d8c
4
- data.tar.gz: 93c277bf2fc40ccdede20e48102eb6b1c1721336d80b62e60c9a191b807f8754
3
+ metadata.gz: e68ff85fe19330eb395ad72da915dbad1bf98c02f5c917402da73b4f5ebab745
4
+ data.tar.gz: aa30fff0bb9f8349e3c9caa5d9ff77cbdd57b7c1cf8e61fdd19d98ebcb271711
5
5
  SHA512:
6
- metadata.gz: eb8fef68d0b4e9e6dc02aa76c603b98e854b09ef47913c30c986d4e397e9b48c79ec14d5ebb9dd1e48db0737def7d73f7e33aefbb35039441e65b4005884051a
7
- data.tar.gz: 072aa07eb4af30102c588a54c75398634e1b409cd1698a087b8e0cb030bc6bb6c5bd8b98ba2c57716e8d819b61ace6ed9ed4ea39a5eda0e1accedc0e232a1eaf
6
+ metadata.gz: '0668a4e35ef63218065e169ab00dfe7e7bfc4439c35f6020611d35a2a625dbdb20d38aa3e7edd3a5cf3769e15bd31077717141029223637cabeb34b172069aa8'
7
+ data.tar.gz: 119fef9be7bd8ab89c013fa545dd3d09b8013fa6ef79db438e59c12b8fd1acc42734bc9faa8f0f3ff63d28fec0b3480c7f43e89ca10a5ce83ee7b6e7a26b1422
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.25.0] - 2025-12-19
4
+
5
+ ### Added
6
+ * Minitest 6.0 support ([#446][])
7
+ * Support Ruby 4.0 ([#444][])
8
+
9
+ ## [1.24.0] - 2025-12-15
10
+
11
+ ### Added
12
+ * Create final status tag on test events ([#433][])
13
+
14
+ ### Changed
15
+ * Add `branch` parameter to `/test-management/tests` request ([#436][])
16
+
17
+ ### Fixed
18
+ * Fix an inheritance blind spot for Test Impact Analysis ([#440][])
19
+
3
20
  ## [1.23.3] - 2025-11-19
4
21
 
5
22
  ### Fixed
@@ -554,7 +571,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
554
571
 
555
572
  - Ruby versions < 2.7 no longer supported ([#8][])
556
573
 
557
- [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.3...main
574
+ [Unreleased]: https://github.com/DataDog/datadog-ci-rb/compare/v1.25.0...main
575
+ [1.25.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.24.0...v1.25.0
576
+ [1.24.0]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.3...v1.24.0
558
577
  [1.23.3]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.2...v1.23.3
559
578
  [1.23.2]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.1...v1.23.2
560
579
  [1.23.1]: https://github.com/DataDog/datadog-ci-rb/compare/v1.23.0...v1.23.1
@@ -786,4 +805,9 @@ Currently test suite level visibility is not used by our instrumentation: it wil
786
805
  [#416]: https://github.com/DataDog/datadog-ci-rb/issues/416
787
806
  [#418]: https://github.com/DataDog/datadog-ci-rb/issues/418
788
807
  [#425]: https://github.com/DataDog/datadog-ci-rb/issues/425
789
- [#430]: https://github.com/DataDog/datadog-ci-rb/issues/430
808
+ [#430]: https://github.com/DataDog/datadog-ci-rb/issues/430
809
+ [#433]: https://github.com/DataDog/datadog-ci-rb/issues/433
810
+ [#436]: https://github.com/DataDog/datadog-ci-rb/issues/436
811
+ [#440]: https://github.com/DataDog/datadog-ci-rb/issues/440
812
+ [#444]: https://github.com/DataDog/datadog-ci-rb/issues/444
813
+ [#446]: https://github.com/DataDog/datadog-ci-rb/issues/446
@@ -156,12 +156,12 @@ static VALUE dd_cov_allocate(VALUE klass) {
156
156
 
157
157
  // Checks if the filename is located under the root folder of the project (but
158
158
  // not in the ignored folder) and adds it to the impacted_files hash.
159
- static void record_impacted_file(struct dd_cov_data *dd_cov_data,
159
+ static bool record_impacted_file(struct dd_cov_data *dd_cov_data,
160
160
  VALUE filename) {
161
161
  char *filename_ptr = RSTRING_PTR(filename);
162
162
  // if the current filename is not located under the root, we skip it
163
163
  if (strncmp(dd_cov_data->root, filename_ptr, dd_cov_data->root_len) != 0) {
164
- return;
164
+ return false;
165
165
  }
166
166
 
167
167
  // if ignored_path is provided and the current filename is located under the
@@ -170,10 +170,11 @@ static void record_impacted_file(struct dd_cov_data *dd_cov_data,
170
170
  if (dd_cov_data->ignored_path_len != 0 &&
171
171
  strncmp(dd_cov_data->ignored_path, filename_ptr,
172
172
  dd_cov_data->ignored_path_len) == 0) {
173
- return;
173
+ return false;
174
174
  }
175
175
 
176
176
  rb_hash_aset(dd_cov_data->impacted_files, filename, Qtrue);
177
+ return true;
177
178
  }
178
179
 
179
180
  // Executed on RUBY_EVENT_LINE event and captures the filename from
@@ -221,29 +222,61 @@ static VALUE safely_get_source_location(VALUE klass_name) {
221
222
  return rescue_nil(get_source_location, klass_name);
222
223
  }
223
224
 
224
- // This function is called for each class that was instantiated during the test
225
- // run.
226
- static int process_instantiated_klass(st_data_t key, st_data_t _value,
227
- st_data_t data) {
228
- VALUE klass = (VALUE)key;
229
- struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
225
+ // Safely get class name, returns Qnil on any error
226
+ static VALUE safely_get_class_name(VALUE klass) {
227
+ return rescue_nil(rb_class_name, klass);
228
+ }
230
229
 
231
- VALUE klass_name = rb_class_name(klass);
230
+ // Safely get module ancestors, returns Qnil on any error
231
+ static VALUE safely_get_mod_ancestors(VALUE klass) {
232
+ return rescue_nil(rb_mod_ancestors, klass);
233
+ }
234
+
235
+ static bool record_impacted_klass(struct dd_cov_data *dd_cov_data,
236
+ VALUE klass) {
237
+ VALUE klass_name = safely_get_class_name(klass);
232
238
  if (klass_name == Qnil) {
233
- return ST_CONTINUE;
239
+ return false;
234
240
  }
235
241
 
236
242
  VALUE source_location = safely_get_source_location(klass_name);
237
- if (source_location == Qnil || RARRAY_LEN(source_location) == 0) {
238
- return ST_CONTINUE;
243
+ if (source_location == Qnil || !RB_TYPE_P(source_location, T_ARRAY) ||
244
+ RARRAY_LEN(source_location) == 0) {
245
+ return false;
239
246
  }
240
247
 
241
248
  VALUE filename = RARRAY_AREF(source_location, 0);
242
249
  if (filename == Qnil || !RB_TYPE_P(filename, T_STRING)) {
250
+ return false;
251
+ }
252
+
253
+ return record_impacted_file(dd_cov_data, filename);
254
+ }
255
+
256
+ // This function is called for each class that was instantiated during the test
257
+ // run.
258
+ static int each_instantiated_klass(st_data_t key, st_data_t _value,
259
+ st_data_t data) {
260
+ VALUE klass = (VALUE)key;
261
+ struct dd_cov_data *dd_cov_data = (struct dd_cov_data *)data;
262
+
263
+ // rb_mod_ancestors returns an array containing the "klass" itself
264
+ // and all the parent classes and/or included/prepended modules
265
+ VALUE ancestors = safely_get_mod_ancestors(klass);
266
+ if (ancestors == Qnil || !RB_TYPE_P(ancestors, T_ARRAY)) {
243
267
  return ST_CONTINUE;
244
268
  }
245
269
 
246
- record_impacted_file(dd_cov_data, filename);
270
+ long len = RARRAY_LEN(ancestors);
271
+ for (long i = 0; i < len; i++) {
272
+ VALUE mod = rb_ary_entry(ancestors, i);
273
+ if (mod == Qnil) {
274
+ continue;
275
+ }
276
+
277
+ record_impacted_klass(dd_cov_data, mod);
278
+ }
279
+
247
280
  return ST_CONTINUE;
248
281
  }
249
282
 
@@ -385,7 +418,7 @@ static VALUE dd_cov_stop(VALUE self) {
385
418
  }
386
419
 
387
420
  // process classes covered by allocation tracing
388
- st_foreach(dd_cov_data->klasses_table, process_instantiated_klass,
421
+ st_foreach(dd_cov_data->klasses_table, each_instantiated_klass,
389
422
  (st_data_t)dd_cov_data);
390
423
  st_clear(dd_cov_data->klasses_table);
391
424
 
@@ -18,7 +18,7 @@ module Datadog
18
18
 
19
19
  def loaded?
20
20
  !defined?(::Minitest).nil? && !defined?(::Minitest::Runnable).nil? && !defined?(::Minitest::Test).nil? &&
21
- !defined?(::Minitest::CompositeReporter).nil?
21
+ !defined?(::Minitest::CompositeReporter).nil? && !defined?(::Minitest::Parallel::Executor).nil?
22
22
  end
23
23
 
24
24
  def compatible?
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../ext/test"
4
+ require_relative "../../git/local_repository"
5
+ require_relative "../../utils/source_code"
6
+ require_relative "../instrumentation"
7
+ require_relative "ext"
8
+ require_relative "helpers"
9
+
10
+ module Datadog
11
+ module CI
12
+ module Contrib
13
+ module Minitest
14
+ # Lifecycle hooks to instrument Minitest::Test
15
+ module ParallelExecutorMinitest6
16
+ def self.included(base)
17
+ base.prepend(InstanceMethods)
18
+ end
19
+
20
+ module InstanceMethods
21
+ def start
22
+ return super unless datadog_configuration[:enabled]
23
+
24
+ @pool = Array.new(size) {
25
+ Thread.new @queue do |queue|
26
+ Thread.current.abort_on_exception = true
27
+ while (job = queue.pop)
28
+ klass, method, reporter = job
29
+ reporter.synchronize { reporter.prerecord klass, method }
30
+ result = ::Minitest.run_one_method(klass, method)
31
+ reporter.synchronize { reporter.record result }
32
+ end
33
+ end
34
+ }
35
+ end
36
+
37
+ private
38
+
39
+ def datadog_configuration
40
+ Datadog.configuration.ci[:minitest]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -5,6 +5,9 @@ require_relative "reporter"
5
5
  require_relative "test"
6
6
  require_relative "runnable"
7
7
 
8
+ require_relative "runnable_minitest_6"
9
+ require_relative "parallel_executor_minitest_6"
10
+
8
11
  module Datadog
9
12
  module CI
10
13
  module Contrib
@@ -18,8 +21,15 @@ module Datadog
18
21
  def patch
19
22
  # test session start
20
23
  ::Minitest.include(Runner)
24
+
21
25
  # test suites (when not executed concurrently)
22
- ::Minitest::Runnable.include(Runnable)
26
+ if ::Minitest::Runnable.respond_to?(:run_suite)
27
+ ::Minitest::Runnable.include(RunnableMinitest6)
28
+ ::Minitest::Parallel::Executor.include(ParallelExecutorMinitest6)
29
+ else
30
+ ::Minitest::Runnable.include(Runnable)
31
+ end
32
+
23
33
  # tests; test suites (when executed concurrently)
24
34
  ::Minitest::Test.include(Test)
25
35
  # test session finish
@@ -20,7 +20,6 @@ module Datadog
20
20
  return results unless test_suite
21
21
 
22
22
  test_suite.finish
23
-
24
23
  results
25
24
  end
26
25
 
@@ -29,10 +28,6 @@ module Datadog
29
28
  def datadog_configuration
30
29
  Datadog.configuration.ci[:minitest]
31
30
  end
32
-
33
- def test_visibility_component
34
- Datadog.send(:components).test_visibility
35
- end
36
31
  end
37
32
  end
38
33
  end
@@ -0,0 +1,49 @@
1
+ require_relative "helpers"
2
+
3
+ module Datadog
4
+ module CI
5
+ module Contrib
6
+ module Minitest
7
+ module RunnableMinitest6
8
+ def self.included(base)
9
+ base.singleton_class.prepend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def run_suite(*args)
14
+ return super unless datadog_configuration[:enabled]
15
+ return super if Helpers.parallel?(self)
16
+
17
+ test_suite = Helpers.start_test_suite(self)
18
+
19
+ results = super
20
+ return results unless test_suite
21
+
22
+ test_suite.finish
23
+ results
24
+ end
25
+
26
+ def run(klass, method_name, reporter)
27
+ reporter.prerecord klass, method_name
28
+ reporter.record ::Minitest.run_one_method(klass, method_name)
29
+ end
30
+
31
+ private
32
+
33
+ def datadog_configuration
34
+ Datadog.configuration.ci[:minitest]
35
+ end
36
+
37
+ def _dd_test_visibility_component
38
+ Datadog.send(:components).test_visibility
39
+ end
40
+
41
+ def _dd_test_retries_component
42
+ Datadog.send(:components).test_retries
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -35,14 +35,20 @@ module Datadog
35
35
  test_visibility_component.start_test_module(Ext::FRAMEWORK)
36
36
  end
37
37
 
38
+ def old_run_one_method(klass, method_name)
39
+ result = klass.new(method_name).run
40
+ raise "#{klass}#run _must_ return a Result" unless ::Minitest::Result === result
41
+ result
42
+ end
43
+
38
44
  def run_one_method(klass, method_name)
39
- return super unless datadog_configuration[:enabled]
45
+ return old_run_one_method(klass, method_name) unless datadog_configuration[:enabled]
40
46
 
41
47
  # @type var result: untyped
42
48
  result = nil
43
49
 
44
50
  test_retries_component.with_retries do
45
- result = super
51
+ result = old_run_one_method(klass, method_name)
46
52
  end
47
53
 
48
54
  # get the current test suite and mark this method as done, so we can check if all tests were executed
@@ -44,7 +44,7 @@ module Datadog
44
44
  tags[CI::Ext::Test::TAG_SOURCE_START] = first_line_number.to_s if first_line_number
45
45
  tags[CI::Ext::Test::TAG_SOURCE_END] = last_line_number.to_s if last_line_number
46
46
 
47
- test_span = test_visibility_component.trace_test(
47
+ test_span = _dd_test_visibility_component.trace_test(
48
48
  name,
49
49
  test_suite_name,
50
50
  tags: tags,
@@ -59,7 +59,7 @@ module Datadog
59
59
  end
60
60
 
61
61
  def after_teardown
62
- test_span = test_visibility_component.active_test
62
+ test_span = _dd_test_visibility_component.active_test
63
63
  return super unless test_span
64
64
 
65
65
  finish_with_result(test_span, result_code)
@@ -95,7 +95,7 @@ module Datadog
95
95
  Datadog.configuration.ci[:minitest]
96
96
  end
97
97
 
98
- def test_visibility_component
98
+ def _dd_test_visibility_component
99
99
  Datadog.send(:components).test_visibility
100
100
  end
101
101
  end
@@ -13,7 +13,21 @@ module Datadog
13
13
  TAG_FRAMEWORK_VERSION = "test.framework_version"
14
14
  TAG_NAME = "test.name"
15
15
  TAG_SKIP_REASON = "test.skip_reason"
16
+
17
+ # Status is the result of a single test run
18
+ # See the [Datadog::CI::Ext::Test::Status] module for the list of possible values of this tag
16
19
  TAG_STATUS = "test.status"
20
+ # Final status is the result that Datadog reports after all retries for a given test. It might be different
21
+ # from the status of the given test run:
22
+ #
23
+ # Example: new test was retried 10 times by Early Flake Detection. It succeeded 9 times and failed once.
24
+ # The final status will be "pass" because we keep CI green for flaky tests.
25
+ #
26
+ # This tag is useful to create monitors on hard failures: if test.final_status is "fail", then CI is red.
27
+ #
28
+ # See the [Datadog::CI::Ext::Test::Status] module for the list of possible values of this tag
29
+ TAG_FINAL_STATUS = "test.final_status"
30
+
17
31
  TAG_SUITE = "test.suite"
18
32
  TAG_MODULE = "test.module"
19
33
  TAG_TYPE = "test.type"
@@ -244,6 +244,32 @@ module Datadog
244
244
  get_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR) == "true"
245
245
  end
246
246
 
247
+ # @internal
248
+ def record_final_status
249
+ status = get_tag(Ext::Test::TAG_STATUS)
250
+ return if status.nil?
251
+
252
+ if [Ext::Test::Status::PASS, Ext::Test::Status::SKIP].include?(status)
253
+ set_tag(Ext::Test::TAG_FINAL_STATUS, status)
254
+ return
255
+ end
256
+
257
+ if should_ignore_failures?
258
+ set_tag(Ext::Test::TAG_FINAL_STATUS, Ext::Test::Status::PASS)
259
+ else
260
+ set_tag(Ext::Test::TAG_FINAL_STATUS, Ext::Test::Status::FAIL)
261
+ end
262
+ end
263
+
264
+ # @internal
265
+ def peek_duration
266
+ end_time = Core::Utils::Time.now.utc
267
+ start_time = tracer_span.start_time
268
+
269
+ return 0.0 if start_time.nil? || end_time.nil?
270
+ end_time - start_time
271
+ end
272
+
247
273
  private
248
274
 
249
275
  def record_test_result(datadog_status)
@@ -127,7 +127,8 @@ module Datadog
127
127
  "attributes" => {
128
128
  "repository_url" => test_session.git_repository_url,
129
129
  "commit_message" => test_session.original_git_commit_message,
130
- "sha" => test_session.original_git_commit_sha
130
+ "sha" => test_session.original_git_commit_sha,
131
+ "branch" => test_session.git_branch
131
132
  }
132
133
  }
133
134
  }.to_json
@@ -94,18 +94,27 @@ module Datadog
94
94
 
95
95
  def record_test_finished(test_span)
96
96
  if current_retry_driver.nil?
97
- # we always run test at least once and after the first pass create a correct retry driver
97
+ # We always run test at least once and after the first pass create a correct retry driver
98
98
  self.current_retry_driver = build_driver(test_span)
99
99
  else
100
- # after each retry we record the result, the driver will decide if we should retry again
100
+ # After each retry we let the driver to record the result.
101
+ # Then the driver will decide if we should retry again.
101
102
  current_retry_driver&.record_retry(test_span)
102
103
 
104
+ # We know that the test was already retried at least once so if we should not retry anymore, then this
105
+ # is the last retry.
103
106
  tag_last_retry(test_span) unless should_retry?
104
107
  end
105
- end
106
108
 
107
- def record_test_span_duration(tracer_span)
108
- current_retry_driver&.record_duration(tracer_span.duration)
109
+ # Some retry strategies such as Early Flake Detection change the number of retries based on
110
+ # how long the test was.
111
+ current_retry_driver&.record_duration(test_span.peek_duration)
112
+
113
+ # We need to set the final status of the test (what will be reported to the test framework) on the last execution
114
+ # no matter if test was retried or not
115
+ #
116
+ # If we should not retry at this point, it means that this execution is the last one (it might the only one as well).
117
+ test_span.record_final_status unless should_retry?
109
118
  end
110
119
 
111
120
  # this API is targeted on Cucumber instrumentation or any other that cannot leverage #with_retries method
@@ -13,8 +13,7 @@ module Datadog
13
13
  end
14
14
 
15
15
  def with_retries(&block)
16
- no_action = proc {}
17
- yield no_action
16
+ yield
18
17
  end
19
18
 
20
19
  def reset_retries!
@@ -121,8 +121,6 @@ module Datadog
121
121
 
122
122
  if block
123
123
  @context.trace_test(test_name, test_suite, service: service, tags: tags) do |test|
124
- subscribe_to_after_stop_event(test.tracer_span)
125
-
126
124
  on_test_started(test)
127
125
  res = block.call(test)
128
126
  on_test_finished(test)
@@ -130,7 +128,6 @@ module Datadog
130
128
  end
131
129
  else
132
130
  test = @context.trace_test(test_name, test_suite, service: service, tags: tags)
133
- subscribe_to_after_stop_event(test.tracer_span)
134
131
  on_test_started(test)
135
132
  test
136
133
  end
@@ -340,10 +337,6 @@ module Datadog
340
337
  Telemetry.event_finished(test)
341
338
  end
342
339
 
343
- def on_after_test_span_finished(tracer_span)
344
- test_retries.record_test_span_duration(tracer_span)
345
- end
346
-
347
340
  # HELPERS
348
341
  def single_active_test_suite
349
342
  # when fetching test_suite to use as test's context, try local context instance first
@@ -357,14 +350,6 @@ module Datadog
357
350
  block&.call(nil)
358
351
  end
359
352
 
360
- def subscribe_to_after_stop_event(tracer_span)
361
- events = tracer_span.send(:events)
362
-
363
- events.after_stop.subscribe do |span|
364
- on_after_test_span_finished(span)
365
- end
366
- end
367
-
368
353
  def set_codeowners(span)
369
354
  source = span.source_file
370
355
  owners = @codeowners.list_owners(source) if source
@@ -4,8 +4,8 @@ module Datadog
4
4
  module CI
5
5
  module VERSION
6
6
  MAJOR = 1
7
- MINOR = 23
8
- PATCH = 3
7
+ MINOR = 25
8
+ PATCH = 0
9
9
  PRE = nil
10
10
  BUILD = nil
11
11
  # PRE and BUILD above are modified for dev gems during gem build GHA workflow
@@ -22,7 +22,7 @@ module Datadog
22
22
  # To allow testing with the next unreleased version of Ruby, the version check is performed
23
23
  # as `< #{MAXIMUM_RUBY_VERSION}`, meaning prereleases of MAXIMUM_RUBY_VERSION are allowed
24
24
  # but not stable MAXIMUM_RUBY_VERSION releases.
25
- MAXIMUM_RUBY_VERSION = "4.0"
25
+ MAXIMUM_RUBY_VERSION = "4.1"
26
26
  end
27
27
  end
28
28
  end
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.23.3
4
+ version: 1.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Datadog, Inc.
@@ -127,9 +127,11 @@ files:
127
127
  - lib/datadog/ci/contrib/minitest/ext.rb
128
128
  - lib/datadog/ci/contrib/minitest/helpers.rb
129
129
  - lib/datadog/ci/contrib/minitest/integration.rb
130
+ - lib/datadog/ci/contrib/minitest/parallel_executor_minitest_6.rb
130
131
  - lib/datadog/ci/contrib/minitest/patcher.rb
131
132
  - lib/datadog/ci/contrib/minitest/reporter.rb
132
133
  - lib/datadog/ci/contrib/minitest/runnable.rb
134
+ - lib/datadog/ci/contrib/minitest/runnable_minitest_6.rb
133
135
  - lib/datadog/ci/contrib/minitest/runner.rb
134
136
  - lib/datadog/ci/contrib/minitest/test.rb
135
137
  - lib/datadog/ci/contrib/parallel_tests/cli.rb
@@ -315,7 +317,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
315
317
  version: 2.7.0
316
318
  - - "<"
317
319
  - !ruby/object:Gem::Version
318
- version: '4.0'
320
+ version: '4.1'
319
321
  required_rubygems_version: !ruby/object:Gem::Requirement
320
322
  requirements:
321
323
  - - ">="