datadog-ci 1.0.0.beta1 → 1.0.0.beta2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +37 -46
  4. data/lib/datadog/ci/configuration/components.rb +43 -9
  5. data/lib/datadog/ci/configuration/settings.rb +6 -0
  6. data/lib/datadog/ci/contrib/cucumber/formatter.rb +9 -7
  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/hooks.rb +4 -2
  10. data/lib/datadog/ci/contrib/rspec/example.rb +9 -5
  11. data/lib/datadog/ci/ext/environment/providers/local_git.rb +8 -79
  12. data/lib/datadog/ci/ext/environment.rb +11 -16
  13. data/lib/datadog/ci/ext/settings.rb +1 -0
  14. data/lib/datadog/ci/ext/test.rb +5 -0
  15. data/lib/datadog/ci/ext/transport.rb +8 -0
  16. data/lib/datadog/ci/git/local_repository.rb +238 -0
  17. data/lib/datadog/ci/git/packfiles.rb +70 -0
  18. data/lib/datadog/ci/git/search_commits.rb +77 -0
  19. data/lib/datadog/ci/git/tree_uploader.rb +90 -0
  20. data/lib/datadog/ci/git/upload_packfile.rb +66 -0
  21. data/lib/datadog/ci/git/user.rb +29 -0
  22. data/lib/datadog/ci/itr/coverage/event.rb +18 -1
  23. data/lib/datadog/ci/itr/coverage/writer.rb +108 -0
  24. data/lib/datadog/ci/itr/runner.rb +120 -11
  25. data/lib/datadog/ci/itr/skippable.rb +106 -0
  26. data/lib/datadog/ci/span.rb +9 -0
  27. data/lib/datadog/ci/test.rb +19 -12
  28. data/lib/datadog/ci/test_module.rb +2 -2
  29. data/lib/datadog/ci/test_session.rb +2 -2
  30. data/lib/datadog/ci/test_suite.rb +2 -2
  31. data/lib/datadog/ci/test_visibility/null_recorder.rb +4 -1
  32. data/lib/datadog/ci/test_visibility/recorder.rb +47 -9
  33. data/lib/datadog/ci/test_visibility/transport.rb +1 -1
  34. data/lib/datadog/ci/transport/http.rb +24 -4
  35. data/lib/datadog/ci/transport/remote_settings_api.rb +12 -6
  36. data/lib/datadog/ci/utils/configuration.rb +2 -2
  37. data/lib/datadog/ci/utils/git.rb +6 -67
  38. data/lib/datadog/ci/utils/parsing.rb +16 -0
  39. data/lib/datadog/ci/utils/test_run.rb +13 -0
  40. data/lib/datadog/ci/version.rb +1 -1
  41. data/lib/datadog/ci/worker.rb +35 -0
  42. data/lib/datadog/ci.rb +4 -0
  43. metadata +15 -4
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "msgpack"
4
4
 
5
+ require_relative "../../git/local_repository"
6
+
5
7
  module Datadog
6
8
  module CI
7
9
  module ITR
@@ -50,13 +52,28 @@ module Datadog
50
52
  files.each do |filename|
51
53
  packer.write_map_header(1)
52
54
  packer.write("filename")
53
- packer.write(filename)
55
+ packer.write(Git::LocalRepository.relative_to_root(filename))
54
56
  end
55
57
  end
56
58
 
57
59
  def to_s
58
60
  "Coverage::Event[test_id=#{test_id}, test_suite_id=#{test_suite_id}, test_session_id=#{test_session_id}, coverage=#{coverage}]"
59
61
  end
62
+
63
+ # Return a human readable version of the event
64
+ def pretty_print(q)
65
+ q.group 0 do
66
+ q.breakable
67
+ q.text "Test ID: #{@test_id}\n"
68
+ q.text "Test Suite ID: #{@test_suite_id}\n"
69
+ q.text "Test Session ID: #{@test_session_id}\n"
70
+ q.group(2, "Files: [", "]\n") do
71
+ q.seplist @coverage.keys.each do |key|
72
+ q.text key
73
+ end
74
+ end
75
+ end
76
+ end
60
77
  end
61
78
  end
62
79
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "datadog/core/workers/async"
4
+ require "datadog/core/workers/queue"
5
+ require "datadog/core/workers/polling"
6
+
7
+ require "datadog/core/buffer/cruby"
8
+ require "datadog/core/buffer/thread_safe"
9
+
10
+ require "datadog/core/environment/ext"
11
+
12
+ module Datadog
13
+ module CI
14
+ module ITR
15
+ module Coverage
16
+ class Writer
17
+ include Core::Workers::Queue
18
+ include Core::Workers::Polling
19
+
20
+ attr_reader :transport
21
+
22
+ DEFAULT_BUFFER_MAX_SIZE = 10_000
23
+ DEFAULT_SHUTDOWN_TIMEOUT = 60
24
+
25
+ def initialize(transport:, options: {})
26
+ @transport = transport
27
+
28
+ # Workers::Polling settings
29
+ self.enabled = options.fetch(:enabled, true)
30
+
31
+ # Workers::Async::Thread settings
32
+ self.fork_policy = Core::Workers::Async::Thread::FORK_POLICY_RESTART
33
+
34
+ # Workers::IntervalLoop settings
35
+ self.loop_base_interval = options[:interval] if options.key?(:interval)
36
+ self.loop_back_off_ratio = options[:back_off_ratio] if options.key?(:back_off_ratio)
37
+ self.loop_back_off_max = options[:back_off_max] if options.key?(:back_off_max)
38
+
39
+ @buffer_size = options.fetch(:buffer_size, DEFAULT_BUFFER_MAX_SIZE)
40
+
41
+ self.buffer = buffer_klass.new(@buffer_size)
42
+
43
+ @shutdown_timeout = options.fetch(:shutdown_timeout, DEFAULT_SHUTDOWN_TIMEOUT)
44
+
45
+ @stopped = false
46
+ end
47
+
48
+ def write(event)
49
+ return if @stopped
50
+
51
+ # Start worker thread. If the process has forked, it will trigger #after_fork to
52
+ # reconfigure the worker accordingly.
53
+ perform
54
+
55
+ enqueue(event)
56
+ end
57
+
58
+ def perform(*events)
59
+ responses = transport.send_events(events)
60
+
61
+ loop_back_off! if responses.find(&:server_error?)
62
+
63
+ nil
64
+ end
65
+
66
+ def stop(force_stop = false, timeout = @shutdown_timeout)
67
+ @stopped = true
68
+
69
+ buffer.close if running?
70
+
71
+ super
72
+ end
73
+
74
+ def enqueue(event)
75
+ buffer.push(event)
76
+ end
77
+
78
+ def dequeue
79
+ buffer.pop
80
+ end
81
+
82
+ def work_pending?
83
+ !buffer.empty?
84
+ end
85
+
86
+ def async?
87
+ true
88
+ end
89
+
90
+ def after_fork
91
+ # In multiprocess environments, forks will share the same buffer until its written to.
92
+ # A.K.A. copy-on-write. We don't want forks to write events generated from another process.
93
+ # Instead, we reset it after the fork. (Make sure any enqueue operations happen after this.)
94
+ self.buffer = buffer_klass.new(@buffer_size)
95
+ end
96
+
97
+ def buffer_klass
98
+ if Core::Environment::Ext::RUBY_ENGINE == "ruby"
99
+ Core::Buffer::CRuby
100
+ else
101
+ Core::Buffer::ThreadSafe
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pp"
4
+
5
+ require "datadog/core/utils/forking"
6
+
3
7
  require_relative "../ext/test"
4
8
  require_relative "../ext/transport"
5
9
 
6
- require_relative "../utils/git"
10
+ require_relative "../git/local_repository"
11
+
12
+ require_relative "../utils/parsing"
13
+
14
+ require_relative "coverage/event"
15
+ require_relative "skippable"
7
16
 
8
17
  module Datadog
9
18
  module CI
@@ -12,26 +21,44 @@ module Datadog
12
21
  # Integrates with backend to provide test impact analysis data and
13
22
  # skip tests that are not impacted by the changes
14
23
  class Runner
24
+ include Core::Utils::Forking
25
+
26
+ attr_reader :correlation_id, :skippable_tests, :skipped_tests_count
27
+
15
28
  def initialize(
29
+ dd_env:,
30
+ api: nil,
31
+ coverage_writer: nil,
16
32
  enabled: false
17
33
  )
18
34
  @enabled = enabled
35
+ @api = api
36
+ @dd_env = dd_env
37
+
19
38
  @test_skipping_enabled = false
20
39
  @code_coverage_enabled = false
21
40
 
41
+ @coverage_writer = coverage_writer
42
+
43
+ @correlation_id = nil
44
+ @skippable_tests = []
45
+
46
+ @skipped_tests_count = 0
47
+ @mutex = Mutex.new
48
+
22
49
  Datadog.logger.debug("ITR Runner initialized with enabled: #{@enabled}")
23
50
  end
24
51
 
25
- def configure(remote_configuration, test_session)
52
+ def configure(remote_configuration, test_session:, git_tree_upload_worker:)
26
53
  Datadog.logger.debug("Configuring ITR Runner with remote configuration: #{remote_configuration}")
27
54
 
28
- @enabled = convert_to_bool(
55
+ @enabled = Utils::Parsing.convert_to_bool(
29
56
  remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_ITR_ENABLED_KEY, false)
30
57
  )
31
- @test_skipping_enabled = @enabled && convert_to_bool(
58
+ @test_skipping_enabled = @enabled && Utils::Parsing.convert_to_bool(
32
59
  remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_TESTS_SKIPPING_KEY, false)
33
60
  )
34
- @code_coverage_enabled = @enabled && convert_to_bool(
61
+ @code_coverage_enabled = @enabled && Utils::Parsing.convert_to_bool(
35
62
  remote_configuration.fetch(Ext::Transport::DD_API_SETTINGS_RESPONSE_CODE_COVERAGE_KEY, false)
36
63
  )
37
64
 
@@ -46,6 +73,8 @@ module Datadog
46
73
  load_datadog_cov! if @code_coverage_enabled
47
74
 
48
75
  Datadog.logger.debug("Configured ITR Runner with enabled: #{@enabled}, skipping_tests: #{@test_skipping_enabled}, code_coverage: #{@code_coverage_enabled}")
76
+
77
+ fetch_skippable_tests(test_session: test_session, git_tree_upload_worker: git_tree_upload_worker)
49
78
  end
50
79
 
51
80
  def enabled?
@@ -60,22 +89,85 @@ module Datadog
60
89
  @code_coverage_enabled
61
90
  end
62
91
 
63
- def start_coverage
92
+ def start_coverage(test)
64
93
  return if !enabled? || !code_coverage?
65
94
 
66
95
  coverage_collector&.start
67
96
  end
68
97
 
69
- def stop_coverage(_test)
98
+ def stop_coverage(test)
70
99
  return if !enabled? || !code_coverage?
71
100
 
72
- coverage_collector&.stop
101
+ coverage = coverage_collector&.stop
102
+ return if coverage.nil? || coverage.empty?
103
+
104
+ return if test.skipped?
105
+
106
+ test_source_file = test.source_file
107
+
108
+ # cucumber's gherkin files are not covered by the code coverage collector
109
+ ensure_test_source_covered(test_source_file, coverage) unless test_source_file.nil?
110
+
111
+ event = Coverage::Event.new(
112
+ test_id: test.id.to_s,
113
+ test_suite_id: test.test_suite_id.to_s,
114
+ test_session_id: test.test_session_id.to_s,
115
+ coverage: coverage
116
+ )
117
+
118
+ Datadog.logger.debug { "Writing coverage event \n #{event.pretty_inspect}" }
119
+
120
+ write(event)
121
+
122
+ event
123
+ end
124
+
125
+ def mark_if_skippable(test)
126
+ return if !enabled? || !skipping_tests?
127
+
128
+ skippable_test_id = Utils::TestRun.skippable_test_id(test.name, test.test_suite_name, test.parameters)
129
+ if @skippable_tests.include?(skippable_test_id)
130
+ test.set_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR, "true")
131
+
132
+ Datadog.logger.debug { "Marked test as skippable: #{skippable_test_id}" }
133
+ else
134
+ Datadog.logger.debug { "Test is not skippable: #{skippable_test_id}" }
135
+ end
136
+ end
137
+
138
+ def count_skipped_test(test)
139
+ if forked?
140
+ Datadog.logger.warn { "ITR is not supported for forking test runners yet" }
141
+ return
142
+ end
143
+
144
+ return if !test.skipped? || !test.skipped_by_itr?
145
+
146
+ @mutex.synchronize do
147
+ @skipped_tests_count += 1
148
+ end
149
+ end
150
+
151
+ def write_test_session_tags(test_session)
152
+ return if !enabled?
153
+
154
+ test_session.set_tag(Ext::Test::TAG_ITR_TESTS_SKIPPED, @skipped_tests_count.positive?.to_s)
155
+ test_session.set_tag(Ext::Test::TAG_ITR_TEST_SKIPPING_COUNT, @skipped_tests_count)
156
+ end
157
+
158
+ def shutdown!
159
+ @coverage_writer&.stop
73
160
  end
74
161
 
75
162
  private
76
163
 
164
+ def write(event)
165
+ # skip sending events if writer is not configured
166
+ @coverage_writer&.write(event)
167
+ end
168
+
77
169
  def coverage_collector
78
- Thread.current[:dd_coverage_collector] ||= Coverage::DDCov.new(root: Utils::Git.root)
170
+ Thread.current[:dd_coverage_collector] ||= Coverage::DDCov.new(root: Git::LocalRepository.root)
79
171
  end
80
172
 
81
173
  def load_datadog_cov!
@@ -86,8 +178,25 @@ module Datadog
86
178
  @code_coverage_enabled = false
87
179
  end
88
180
 
89
- def convert_to_bool(value)
90
- value.to_s == "true"
181
+ def ensure_test_source_covered(test_source_file, coverage)
182
+ absolute_test_source_file_path = File.join(Git::LocalRepository.root, test_source_file)
183
+ return if coverage.key?(absolute_test_source_file_path)
184
+
185
+ coverage[absolute_test_source_file_path] = true
186
+ end
187
+
188
+ def fetch_skippable_tests(test_session:, git_tree_upload_worker:)
189
+ return unless skipping_tests?
190
+
191
+ # we can only request skippable tests if git metadata is already uploaded
192
+ git_tree_upload_worker.wait_until_done
193
+
194
+ skippable_response = Skippable.new(api: @api, dd_env: @dd_env).fetch_skippable_tests(test_session)
195
+ @correlation_id = skippable_response.correlation_id
196
+ @skippable_tests = skippable_response.tests
197
+
198
+ Datadog.logger.debug { "Fetched skippable tests: \n #{@skippable_tests}" }
199
+ Datadog.logger.debug { "ITR correlation ID: #{@correlation_id}" }
91
200
  end
92
201
  end
93
202
  end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ require_relative "../ext/transport"
6
+ require_relative "../ext/test"
7
+ require_relative "../utils/test_run"
8
+
9
+ module Datadog
10
+ module CI
11
+ module ITR
12
+ class Skippable
13
+ class Response
14
+ def initialize(http_response)
15
+ @http_response = http_response
16
+ @json = nil
17
+ end
18
+
19
+ def ok?
20
+ resp = @http_response
21
+ !resp.nil? && resp.ok?
22
+ end
23
+
24
+ def correlation_id
25
+ payload.dig("meta", "correlation_id")
26
+ end
27
+
28
+ def tests
29
+ res = Set.new
30
+
31
+ payload.fetch("data", [])
32
+ .each do |test_data|
33
+ next unless test_data["type"] == Ext::Test::ITR_TEST_SKIPPING_MODE
34
+
35
+ attrs = test_data["attributes"] || {}
36
+ res << Utils::TestRun.skippable_test_id(attrs["name"], attrs["suite"], attrs["parameters"])
37
+ end
38
+
39
+ res
40
+ end
41
+
42
+ private
43
+
44
+ def payload
45
+ cached = @json
46
+ return cached unless cached.nil?
47
+
48
+ resp = @http_response
49
+ return @json = {} if resp.nil? || !ok?
50
+
51
+ begin
52
+ @json = JSON.parse(resp.payload)
53
+ rescue JSON::ParserError => e
54
+ Datadog.logger.error("Failed to parse skippable tests response payload: #{e}. Payload was: #{resp.payload}")
55
+ @json = {}
56
+ end
57
+ end
58
+ end
59
+
60
+ def initialize(dd_env:, api: nil)
61
+ @api = api
62
+ @dd_env = dd_env
63
+ end
64
+
65
+ def fetch_skippable_tests(test_session)
66
+ api = @api
67
+ return Response.new(nil) unless api
68
+
69
+ request_payload = payload(test_session)
70
+ Datadog.logger.debug("Fetching skippable tests with request: #{request_payload}")
71
+
72
+ http_response = api.api_request(
73
+ path: Ext::Transport::DD_API_SKIPPABLE_TESTS_PATH,
74
+ payload: request_payload
75
+ )
76
+
77
+ Response.new(http_response)
78
+ end
79
+
80
+ private
81
+
82
+ def payload(test_session)
83
+ {
84
+ "data" => {
85
+ "type" => Ext::Transport::DD_API_SKIPPABLE_TESTS_TYPE,
86
+ "attributes" => {
87
+ "test_level" => Ext::Test::ITR_TEST_SKIPPING_MODE,
88
+ "service" => test_session.service,
89
+ "env" => @dd_env,
90
+ "repository_url" => test_session.git_repository_url,
91
+ "sha" => test_session.git_commit_sha,
92
+ "configurations" => {
93
+ Ext::Test::TAG_OS_PLATFORM => test_session.os_platform,
94
+ Ext::Test::TAG_OS_ARCHITECTURE => test_session.os_architecture,
95
+ Ext::Test::TAG_OS_VERSION => test_session.os_version,
96
+ Ext::Test::TAG_RUNTIME_NAME => test_session.runtime_name,
97
+ Ext::Test::TAG_RUNTIME_VERSION => test_session.runtime_version
98
+ }
99
+ }
100
+ }
101
+ }.to_json
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "datadog/core/environment/platform"
4
+
3
5
  require_relative "ext/test"
4
6
  require_relative "utils/test_run"
5
7
 
@@ -151,6 +153,12 @@ module Datadog
151
153
  tracer_span.get_tag(Ext::Test::TAG_OS_PLATFORM)
152
154
  end
153
155
 
156
+ # Returns the OS version extracted from the environment.
157
+ # @return [String] OS version.
158
+ def os_version
159
+ tracer_span.get_tag(Ext::Test::TAG_OS_VERSION)
160
+ end
161
+
154
162
  # Returns the runtime name extracted from the environment.
155
163
  # @return [String] runtime name.
156
164
  def runtime_name
@@ -166,6 +174,7 @@ module Datadog
166
174
  def set_environment_runtime_tags
167
175
  tracer_span.set_tag(Ext::Test::TAG_OS_ARCHITECTURE, ::RbConfig::CONFIG["host_cpu"])
168
176
  tracer_span.set_tag(Ext::Test::TAG_OS_PLATFORM, ::RbConfig::CONFIG["host_os"])
177
+ tracer_span.set_tag(Ext::Test::TAG_OS_VERSION, Core::Environment::Platform.kernel_release)
169
178
  tracer_span.set_tag(Ext::Test::TAG_RUNTIME_NAME, Core::Environment::Ext::LANG_ENGINE)
170
179
  tracer_span.set_tag(Ext::Test::TAG_RUNTIME_VERSION, Core::Environment::Ext::ENGINE_VERSION)
171
180
  tracer_span.set_tag(Ext::Test::TAG_COMMAND, Utils::TestRun.command)
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
 
5
5
  require_relative "span"
6
+ require_relative "utils/test_run"
6
7
 
7
8
  module Datadog
8
9
  module CI
@@ -18,9 +19,9 @@ module Datadog
18
19
  # Finishes the current test.
19
20
  # @return [void]
20
21
  def finish
21
- super
22
-
23
22
  recorder.deactivate_test
23
+
24
+ super
24
25
  end
25
26
 
26
27
  # Running test suite that this test is part of (if any).
@@ -62,6 +63,12 @@ module Datadog
62
63
  get_tag(Ext::Test::TAG_SOURCE_FILE)
63
64
  end
64
65
 
66
+ # Returns "true" if the test is skipped by the intelligent test runner.
67
+ # @return [Boolean] true if the test is skipped by the intelligent test runner, false otherwise.
68
+ def skipped_by_itr?
69
+ get_tag(Ext::Test::TAG_ITR_SKIPPED_BY_ITR) == "true"
70
+ end
71
+
65
72
  # Sets the status of the span to "pass".
66
73
  # @return [void]
67
74
  def passed!
@@ -89,7 +96,7 @@ module Datadog
89
96
  record_test_result(Ext::Test::Status::SKIP)
90
97
  end
91
98
 
92
- # Sets the parameters for this test (e.g. Cucumber example or RSpec shared specs).
99
+ # Sets the parameters for this test (e.g. Cucumber example or RSpec specs).
93
100
  # Parameters are needed to compute test fingerprint to distinguish between different tests having same names.
94
101
  #
95
102
  # @param [Hash] arguments the arguments that test accepts as key-value hash
@@ -98,15 +105,15 @@ module Datadog
98
105
  def set_parameters(arguments, metadata = {})
99
106
  return if arguments.nil?
100
107
 
101
- set_tag(
102
- Ext::Test::TAG_PARAMETERS,
103
- JSON.generate(
104
- {
105
- arguments: arguments,
106
- metadata: metadata
107
- }
108
- )
109
- )
108
+ set_tag(Ext::Test::TAG_PARAMETERS, Utils::TestRun.test_parameters(arguments: arguments, metadata: metadata))
109
+ end
110
+
111
+ # Gets the parameters for this test (e.g. Cucumber example or RSpec specs) as a serialized JSON.
112
+ #
113
+ # @return [String] the serialized JSON of the parameters
114
+ # @return [nil] if this test does not have parameters
115
+ def parameters
116
+ get_tag(Ext::Test::TAG_PARAMETERS)
110
117
  end
111
118
 
112
119
  private
@@ -14,9 +14,9 @@ module Datadog
14
14
  # Finishes this test module.
15
15
  # @return [void]
16
16
  def finish
17
- super
18
-
19
17
  recorder.deactivate_test_module
18
+
19
+ super
20
20
  end
21
21
  end
22
22
  end
@@ -15,9 +15,9 @@ module Datadog
15
15
  # Finishes the current test session.
16
16
  # @return [void]
17
17
  def finish
18
- super
19
-
20
18
  recorder.deactivate_test_session
19
+
20
+ super
21
21
  end
22
22
 
23
23
  # Return the test session's name which is equal to test command used
@@ -26,9 +26,9 @@ module Datadog
26
26
  # we try to derive test suite status from execution stats if no status was set explicitly
27
27
  set_status_from_stats! if undefined?
28
28
 
29
- super
30
-
31
29
  recorder.deactivate_test_suite(name)
30
+
31
+ super
32
32
  end
33
33
  end
34
34
 
@@ -23,7 +23,7 @@ module Datadog
23
23
  skip_tracing(block)
24
24
  end
25
25
 
26
- def trace(type, span_name, tags: {}, &block)
26
+ def trace(span_name, type: "span", tags: {}, &block)
27
27
  skip_tracing(block)
28
28
  end
29
29
 
@@ -42,6 +42,9 @@ module Datadog
42
42
  def active_test_suite(test_suite_name)
43
43
  end
44
44
 
45
+ def shutdown!
46
+ end
47
+
45
48
  private
46
49
 
47
50
  def skip_tracing(block = nil)