turbo_tests2 3.0.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.
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
8
+
9
+ ## Security contact information
10
+
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
13
+ Tidelift will coordinate the fix and disclosure.
14
+
15
+ ## Additional Support
16
+
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
20
+
21
+ [README]: README.md
data/certs/pboling.pem ADDED
@@ -0,0 +1,27 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
3
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
4
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
5
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
6
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
7
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
8
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
9
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
10
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
11
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
12
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
13
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
14
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
15
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
16
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
17
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
18
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
19
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
20
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
21
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
22
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
23
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
24
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
25
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
26
+ L9nRqA==
27
+ -----END CERTIFICATE-----
data/exe/turbo_tests2 ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Alias for `turbo_tests` — turbo_tests2 is a drop-in replacement for turbo_tests.
5
+ require "turbo_tests2"
6
+
7
+ TurboTests::CLI.new(ARGV).run
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module TurboTests
6
+ class CLI
7
+ def initialize(argv)
8
+ @argv = argv
9
+ end
10
+
11
+ def run
12
+ handle_shim_command if shim_command?
13
+
14
+ requires = []
15
+ formatters = []
16
+ tags = []
17
+ count = nil
18
+ runtime_log = nil
19
+ verbose = false
20
+ fail_fast = nil
21
+ seed = nil
22
+ print_failed_group = false
23
+ create = false
24
+ nice = false
25
+
26
+ OptionParser.new do |opts|
27
+ opts.banner = <<~BANNER
28
+ Run all tests in parallel, giving each process ENV['TEST_ENV_NUMBER'] ('1', '2', '3', ...).
29
+
30
+ Reports test results incrementally. Uses methods from `parallel_tests` gem to split files to groups.
31
+
32
+ Source code of `turbo_tests2` gem is based on Discourse and RubyGems work in this area (see README file of the source repository).
33
+
34
+ Usage: turbo_tests2 [options]
35
+
36
+ [optional] Only selected files & folders:
37
+ turbo_tests2 spec/bar spec/baz/xxx_spec.rb
38
+
39
+ Options:
40
+ BANNER
41
+
42
+ opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: available CPUs") { |n| count = n }
43
+
44
+ opts.on("-r", "--require PATH", "Require a file.") do |filename|
45
+ requires << filename
46
+ end
47
+
48
+ opts.on(
49
+ "-f",
50
+ "--format FORMATTER",
51
+ "Choose a formatter. Available formatters: progress (p), documentation (d). Default: progress",
52
+ ) do |name|
53
+ formatters << {
54
+ name: name,
55
+ outputs: [],
56
+ }
57
+ end
58
+
59
+ opts.on("-t", "--tag TAG", "Run examples with the specified tag.") do |tag|
60
+ tags << tag
61
+ end
62
+
63
+ opts.on("-o", "--out FILE", "Write output to a file instead of $stdout") do |filename|
64
+ if formatters.empty?
65
+ formatters << {
66
+ name: "progress",
67
+ outputs: [],
68
+ }
69
+ end
70
+ formatters.last[:outputs] << filename
71
+ end
72
+
73
+ opts.on("--runtime-log FILE", "Location of previously recorded test runtimes") do |filename|
74
+ runtime_log = filename
75
+ end
76
+
77
+ opts.on("-v", "--verbose", "More output") do
78
+ verbose = true
79
+ end
80
+
81
+ opts.on("--fail-fast=[N]") do |n|
82
+ n = begin
83
+ Integer(n)
84
+ rescue StandardError
85
+ nil
86
+ end
87
+ fail_fast = (n.nil? || n < 1) ? 1 : n
88
+ end
89
+
90
+ opts.on("--seed SEED", "Seed for rspec") do |s|
91
+ seed = s
92
+ end
93
+
94
+ opts.on("--create", "Create databases") do
95
+ create = true
96
+ end
97
+
98
+ opts.on("--print_failed_group", "Prints group that had failures in it") do
99
+ print_failed_group = true
100
+ end
101
+
102
+ opts.on("--nice", "execute test commands with low priority") do
103
+ nice = true
104
+ end
105
+ end.parse!(@argv)
106
+
107
+ if create
108
+ return TurboTests::Runner.create(count)
109
+ end
110
+
111
+ requires.each { |f| require(f) }
112
+
113
+ if formatters.empty?
114
+ formatters << {
115
+ name: "progress",
116
+ outputs: [],
117
+ }
118
+ end
119
+
120
+ formatters.each do |formatter|
121
+ formatter[:outputs] << "-" if formatter[:outputs].empty?
122
+ end
123
+
124
+ load_rake
125
+
126
+ invoke_rake_task("turbo_tests:setup")
127
+
128
+ files = @argv.empty? ? ["spec"] : @argv
129
+ parallel_options = {}
130
+
131
+ exitstatus = TurboTests::Runner.run(
132
+ formatters: formatters,
133
+ tags: tags,
134
+ files: files,
135
+ runtime_log: runtime_log,
136
+ verbose: verbose,
137
+ fail_fast: fail_fast,
138
+ count: count,
139
+ seed: seed,
140
+ nice: nice,
141
+ print_failed_group: print_failed_group,
142
+ parallel_options: parallel_options,
143
+ )
144
+
145
+ invoke_rake_task("turbo_tests:cleanup")
146
+
147
+ # From https://github.com/galtzo-floss/turbo_tests2/pull/20/
148
+ exit(exitstatus)
149
+ end
150
+
151
+ private
152
+
153
+ def shim_command?
154
+ @argv.first == "shim"
155
+ end
156
+
157
+ def handle_shim_command
158
+ command = @argv[1]
159
+ args = @argv.drop(2)
160
+
161
+ result =
162
+ case command
163
+ when "install"
164
+ TurboTests::Shim.install(project_root: Dir.pwd, path: parse_shim_path(args, command: command))
165
+ when "remove"
166
+ TurboTests::Shim.remove(project_root: Dir.pwd, path: parse_shim_path(args, command: command))
167
+ else
168
+ warn(shim_usage(command))
169
+ exit(1)
170
+ end
171
+
172
+ io = result.exit_code.zero? ? $stdout : $stderr
173
+ io.puts(result.message)
174
+ exit(result.exit_code)
175
+ end
176
+
177
+ def parse_shim_path(args, command:)
178
+ path_override = nil
179
+ parser = OptionParser.new do |opts|
180
+ opts.banner = "Usage: turbo_tests2 shim #{command} [--path PATH]"
181
+ opts.on("--path PATH", "Use a custom shim path instead of bin/turbo_tests") { |value| path_override = value }
182
+ end
183
+
184
+ remaining = parser.parse(args.dup)
185
+ if remaining.size > 1 || (remaining.any? && path_override)
186
+ warn(shim_usage(command))
187
+ exit(1)
188
+ end
189
+
190
+ remaining.first || path_override
191
+ rescue OptionParser::ParseError => e
192
+ warn(e.message)
193
+ warn(shim_usage(command))
194
+ exit(1)
195
+ end
196
+
197
+ def shim_usage(command = nil)
198
+ lines = [
199
+ "Usage: turbo_tests2 shim install [--path PATH]",
200
+ " turbo_tests2 shim remove [--path PATH]",
201
+ ]
202
+ lines << "Unknown shim command: #{command}" if command && !%w[install remove].include?(command)
203
+ lines.join("\n")
204
+ end
205
+
206
+ def load_rake
207
+ begin
208
+ require "rake"
209
+ rescue LoadError
210
+ # :nocov:
211
+ return # rake is optional
212
+ # :nocov:
213
+ end
214
+
215
+ # Pass an empty argv so Rake doesn't parse the current process's ARGV,
216
+ # which may contain non-Rake arguments (e.g. RSpec's --pattern flag when
217
+ # tests are run via `rake spec`).
218
+ Rake.application.init("rake", [])
219
+ Rake.application.load_rakefile
220
+ end
221
+
222
+ def invoke_rake_task(name)
223
+ return unless defined?(Rake) && Rake::Task.task_defined?(name)
224
+
225
+ Rake::Task[name].invoke
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rspec/core"
5
+ require "rspec/core/formatters"
6
+ require "rspec/core/notifications"
7
+
8
+ module RSpecExt
9
+ def handle_interrupt
10
+ if RSpec.world.wants_to_quit
11
+ exit!(1)
12
+ else
13
+ RSpec.world.wants_to_quit = true
14
+ end
15
+ end
16
+ end
17
+
18
+ RSpec::Core::Runner.singleton_class.prepend(RSpecExt)
19
+
20
+ module TurboTests
21
+ # An RSpec formatter used for each subprocess during parallel test execution
22
+ class JsonRowsFormatter
23
+ RSpec::Core::Formatters.register(
24
+ self,
25
+ :start,
26
+ :close,
27
+ :example_failed,
28
+ :example_passed,
29
+ :example_pending,
30
+ :example_group_started,
31
+ :example_group_finished,
32
+ :message,
33
+ :seed,
34
+ )
35
+
36
+ attr_reader :output
37
+
38
+ def initialize(output)
39
+ @output = output
40
+ end
41
+
42
+ def start(notification)
43
+ output_row(
44
+ type: :load_summary,
45
+ summary: load_summary_to_json(notification),
46
+ )
47
+ end
48
+
49
+ def example_group_started(notification)
50
+ output_row(
51
+ type: :group_started,
52
+ group: group_to_json(notification),
53
+ )
54
+ end
55
+
56
+ def example_group_finished(notification)
57
+ output_row(
58
+ type: :group_finished,
59
+ group: group_to_json(notification),
60
+ )
61
+ end
62
+
63
+ def example_passed(notification)
64
+ output_row(
65
+ type: :example_passed,
66
+ example: example_to_json(notification.example),
67
+ )
68
+ end
69
+
70
+ def example_pending(notification)
71
+ output_row(
72
+ type: :example_pending,
73
+ example: example_to_json(notification.example),
74
+ )
75
+ end
76
+
77
+ def example_failed(notification)
78
+ output_row(
79
+ type: :example_failed,
80
+ example: example_to_json(notification.example),
81
+ )
82
+ end
83
+
84
+ def seed(notification)
85
+ output_row(
86
+ type: :seed,
87
+ seed: notification.seed,
88
+ )
89
+ end
90
+
91
+ def close(_notification)
92
+ output_row(
93
+ type: :close,
94
+ )
95
+ end
96
+
97
+ def message(notification)
98
+ output_row(
99
+ type: :message,
100
+ message: notification.message,
101
+ )
102
+ end
103
+
104
+ private
105
+
106
+ def exception_to_json(exception)
107
+ return unless exception
108
+
109
+ {
110
+ class_name: exception.class.name.to_s,
111
+ backtrace: exception.backtrace,
112
+ message: exception.message,
113
+ cause: exception_to_json(exception.cause),
114
+ }
115
+ end
116
+
117
+ def execution_result_to_json(result)
118
+ {
119
+ example_skipped?: result.example_skipped?,
120
+ pending_message: result.pending_message,
121
+ status: result.status,
122
+ pending_fixed?: result.pending_fixed?,
123
+ exception: exception_to_json(result.exception || result.pending_exception),
124
+ }
125
+ end
126
+
127
+ def stack_frame_to_json(frame)
128
+ {
129
+ shared_group_name: frame.shared_group_name,
130
+ inclusion_location: frame.inclusion_location,
131
+ }
132
+ end
133
+
134
+ def example_to_json(example)
135
+ {
136
+ execution_result: execution_result_to_json(example.execution_result),
137
+ location: example.location,
138
+ description: example.description,
139
+ full_description: example.full_description,
140
+ metadata: {
141
+ shared_group_inclusion_backtrace:
142
+ example
143
+ .metadata[:shared_group_inclusion_backtrace]
144
+ .map { |frame| stack_frame_to_json(frame) },
145
+ extra_failure_lines: example.metadata[:extra_failure_lines],
146
+ },
147
+ location_rerun_argument: example.location_rerun_argument,
148
+ }
149
+ end
150
+
151
+ def load_summary_to_json(notification)
152
+ {
153
+ count: notification.count,
154
+ load_time: notification.load_time,
155
+ }
156
+ end
157
+
158
+ def group_to_json(notification)
159
+ {
160
+ group: {
161
+ description: notification.group.description,
162
+ },
163
+ }
164
+ end
165
+
166
+ def output_row(obj)
167
+ output.puts ENV["RSPEC_FORMATTER_OUTPUT_ID"] + obj.to_json
168
+ output.flush
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTests
4
+ class Reporter
5
+ attr_writer :load_time
6
+ attr_reader :pending_examples, :failed_examples
7
+
8
+ class << self
9
+ def from_config(formatter_config, start_time, seed, seed_used, files, parallel_options)
10
+ reporter = new(start_time, seed, seed_used, files, parallel_options)
11
+
12
+ formatter_config.each do |config|
13
+ name, outputs = config.values_at(:name, :outputs)
14
+
15
+ outputs.map! do |filename|
16
+ (filename == "-") ? $stdout : File.open(filename, "w")
17
+ end
18
+
19
+ reporter.add(name, outputs)
20
+ end
21
+
22
+ reporter
23
+ end
24
+ end
25
+
26
+ def initialize(start_time, seed, seed_used, files, parallel_options)
27
+ @formatters = []
28
+ @pending_examples = []
29
+ @failed_examples = []
30
+ @all_examples = []
31
+ @messages = []
32
+ @start_time = start_time
33
+ @seed = seed
34
+ @seed_used = seed_used
35
+ @load_time = 0
36
+ @errors_outside_of_examples_count = 0
37
+ @files = files
38
+ @parallel_options = parallel_options
39
+ end
40
+
41
+ def add(name, outputs)
42
+ outputs.each do |output|
43
+ formatter_class =
44
+ case name
45
+ when "p", "progress"
46
+ RSpec::Core::Formatters::ProgressFormatter
47
+ when "d", "documentation"
48
+ RSpec::Core::Formatters::DocumentationFormatter
49
+ else
50
+ Kernel.const_get(name)
51
+ end
52
+
53
+ @formatters << formatter_class.new(output)
54
+ end
55
+ end
56
+
57
+ # Borrowed from RSpec::Core::Reporter
58
+ # https://github.com/rspec/rspec-core/blob/5699fcdc4723087ff6139af55bd155ad9ad61a7b/lib/rspec/core/reporter.rb#L71
59
+ def report(example_groups)
60
+ start(example_groups)
61
+ begin
62
+ yield self
63
+ ensure
64
+ finish
65
+ end
66
+ end
67
+
68
+ def start(example_groups, time = RSpec::Core::Time.now)
69
+ @start = time
70
+ @load_time = (@start - @start_time).to_f
71
+
72
+ report_number_of_tests(example_groups)
73
+ expected_example_count = example_groups.flatten(1).count
74
+
75
+ delegate_to_formatters(:seed, RSpec::Core::Notifications::SeedNotification.new(@seed, @seed_used))
76
+ delegate_to_formatters(
77
+ :start,
78
+ RSpec::Core::Notifications::StartNotification.new(expected_example_count, @load_time),
79
+ )
80
+ end
81
+
82
+ def report_number_of_tests(groups)
83
+ name = ParallelTests::RSpec::Runner.test_file_name
84
+
85
+ num_processes = groups.size
86
+ num_tests = groups.map(&:size).sum
87
+ tests_per_process = ((num_processes == 0) ? 0 : num_tests.to_f / num_processes).round
88
+
89
+ puts "#{num_processes} processes for #{num_tests} #{name}s, ~ #{tests_per_process} #{name}s per process"
90
+ end
91
+
92
+ def group_started(notification)
93
+ delegate_to_formatters(:example_group_started, notification)
94
+ end
95
+
96
+ def group_finished
97
+ delegate_to_formatters(:example_group_finished, nil)
98
+ end
99
+
100
+ def example_passed(example)
101
+ delegate_to_formatters(:example_passed, example.notification)
102
+
103
+ @all_examples << example
104
+ end
105
+
106
+ def example_pending(example)
107
+ delegate_to_formatters(:example_pending, example.notification)
108
+
109
+ @all_examples << example
110
+ @pending_examples << example
111
+ end
112
+
113
+ def example_failed(example)
114
+ delegate_to_formatters(:example_failed, example.notification)
115
+
116
+ @all_examples << example
117
+ @failed_examples << example
118
+ end
119
+
120
+ def message(message)
121
+ delegate_to_formatters(:message, RSpec::Core::Notifications::MessageNotification.new(message))
122
+ @messages << message
123
+ end
124
+
125
+ def error_outside_of_examples(error_message)
126
+ @errors_outside_of_examples_count += 1
127
+ message(error_message)
128
+ end
129
+
130
+ def finish
131
+ end_time = RSpec::Core::Time.now
132
+
133
+ @duration = end_time - @start_time
134
+ delegate_to_formatters(:stop, RSpec::Core::Notifications::ExamplesNotification.new(self))
135
+
136
+ delegate_to_formatters(:start_dump, RSpec::Core::Notifications::NullNotification)
137
+ delegate_to_formatters(
138
+ :dump_pending,
139
+ RSpec::Core::Notifications::ExamplesNotification.new(
140
+ self,
141
+ ),
142
+ )
143
+ delegate_to_formatters(
144
+ :dump_failures,
145
+ RSpec::Core::Notifications::ExamplesNotification.new(
146
+ self,
147
+ ),
148
+ )
149
+ delegate_to_formatters(
150
+ :dump_summary,
151
+ RSpec::Core::Notifications::SummaryNotification.new(
152
+ end_time - @start_time,
153
+ @all_examples,
154
+ @failed_examples,
155
+ @pending_examples,
156
+ @load_time,
157
+ @errors_outside_of_examples_count,
158
+ ),
159
+ )
160
+ delegate_to_formatters(
161
+ :seed,
162
+ RSpec::Core::Notifications::SeedNotification.new(
163
+ @seed,
164
+ @seed_used,
165
+ ),
166
+ )
167
+ ensure
168
+ delegate_to_formatters(:close, RSpec::Core::Notifications::NullNotification)
169
+ end
170
+
171
+ protected
172
+
173
+ def delegate_to_formatters(method, *args)
174
+ @formatters.each do |formatter|
175
+ formatter.send(method, *args) if formatter.respond_to?(method)
176
+ end
177
+ end
178
+ end
179
+ end