diff --git a/flatware-rspec.gemspec b/flatware-rspec.gemspec index d534c32..809690b 100644 --- a/flatware-rspec.gemspec +++ b/flatware-rspec.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ['>= 2.6', '< 3.5'] s.require_paths = ['lib'] s.add_dependency %(flatware), Flatware::VERSION + s.add_dependency %(fuubar), '>= 2.5' s.add_dependency %(rspec), '>= 3.6' # s.metadata['rubygems_mfa_required'] = 'true' end diff --git a/lib/flatware/broadcaster.rb b/lib/flatware/broadcaster.rb index 12b2a88..f0c69dd 100644 --- a/lib/flatware/broadcaster.rb +++ b/lib/flatware/broadcaster.rb @@ -11,6 +11,7 @@ class Broadcaster started summarize summarize_remaining + worker_ready ].freeze attr_reader :formatters diff --git a/lib/flatware/cli.rb b/lib/flatware/cli.rb index 8d3a1b9..3261b10 100644 --- a/lib/flatware/cli.rb +++ b/lib/flatware/cli.rb @@ -28,6 +28,14 @@ def self.worker_option desc: 'Print debug messages to $stderr' ) + class_option( + :formatter, + aliases: '-f', + type: :string, + desc: 'Formatter to use', + default: :console + ) + worker_option desc 'fan [COMMAND]', 'executes the given job on all of the workers' def fan(*command) diff --git a/lib/flatware/rspec/cli.rb b/lib/flatware/rspec/cli.rb index c0417f7..cc90759 100644 --- a/lib/flatware/rspec/cli.rb +++ b/lib/flatware/rspec/cli.rb @@ -3,6 +3,7 @@ require 'flatware/cli' require 'flatware/rspec' require 'flatware/rspec/formatters/console' +require 'flatware/rspec/formatters/fuubar' module Flatware # rspec thor command @@ -17,14 +18,20 @@ class CLI def rspec(*rspec_args) jobs = RSpec.extract_jobs_from_args rspec_args, workers: workers - formatter = Flatware::RSpec::Formatters::Console.new( - ::RSpec.configuration.output_stream, - deprecation_stream: ::RSpec.configuration.deprecation_stream - ) - Flatware.verbose = options[:log] Worker.spawn count: workers, runner: RSpec, sink: options['sink-endpoint'] start_sink(jobs: jobs, workers: workers, formatter: formatter) end + + def formatter + @formatter ||= begin + formatter_klass = "Flatware::RSpec::Formatters::#{options[:formatter].capitalize}".constantize + + formatter_klass.new( + ::RSpec.configuration.output_stream, + deprecation_stream: ::RSpec.configuration.deprecation_stream + ) + end + end end end diff --git a/lib/flatware/rspec/formatter.rb b/lib/flatware/rspec/formatter.rb index 71c5d60..0800669 100644 --- a/lib/flatware/rspec/formatter.rb +++ b/lib/flatware/rspec/formatter.rb @@ -4,8 +4,6 @@ module Flatware module RSpec - ProgressMessage = Struct.new(:progress) - class Formatter extend Forwardable @@ -17,16 +15,20 @@ def initialize(stdout) @output = stdout end - def example_passed(_example) - send_progress :passed + def start(notification) + Sink.client.worker_ready notification + end + + def example_passed(notification) + send_progress marshaled_progress_notification(notification) end - def example_failed(_example) - send_progress :failed + def example_failed(notification) + send_progress marshaled_progress_notification(notification) end - def example_pending(_example) - send_progress :pending + def example_pending(notification) + send_progress marshaled_progress_notification(notification) end def message(message) @@ -40,20 +42,25 @@ def close(*) private - def send_progress(status) - Sink.client.progress ProgressMessage.new status + def send_progress(notification) + Sink.client.progress notification end def checkpoint @checkpoint ||= Checkpoint.new end + def marshaled_progress_notification(notification) + Flatware::RSpec::Marshalable::ExampleNotification.from_rspec(notification) + end + ::RSpec::Core::Formatters.register( self, *Checkpoint::EVENTS, :example_passed, :example_failed, :example_pending, + :start, :message, :close ) diff --git a/lib/flatware/rspec/formatters/console.rb b/lib/flatware/rspec/formatters/console.rb index 5429a77..b8c79cf 100644 --- a/lib/flatware/rspec/formatters/console.rb +++ b/lib/flatware/rspec/formatters/console.rb @@ -14,7 +14,7 @@ def initialize(out, deprecation_stream: StringIO.new) end def progress(result) - progress_formatter.public_send(message_for(result), nil) + progress_formatter.public_send(message_for(result), result) end def message(message) @@ -75,12 +75,12 @@ def colorizer ::RSpec::Core::Formatters::ConsoleCodes end - def message_for(result) + def message_for(notification) { passed: :example_passed, failed: :example_failed, pending: :example_pending - }.fetch result.progress + }.fetch notification.example.execution_result.status end end end diff --git a/lib/flatware/rspec/formatters/fuubar.rb b/lib/flatware/rspec/formatters/fuubar.rb new file mode 100644 index 0000000..ee7f8f9 --- /dev/null +++ b/lib/flatware/rspec/formatters/fuubar.rb @@ -0,0 +1,96 @@ +require 'fuubar' + +module Flatware + module RSpec + module Formatters + class Fuubar + attr_reader :progress_formatter, :out, :deprecation_stream + + def initialize(out, deprecation_stream: StringIO.new) + @out = out + @deprecation_stream = deprecation_stream + ::RSpec.configuration.backtrace_exclusion_patterns += [%r{/lib/flatware/worker}, %r{/lib/flatware/rspec}] + @progress_formatter = ::Fuubar.new(out) + end + + def worker_ready(notification) + if progress_formatter.progress.total.positive? + progress_formatter.progress.total += notification.count + else + progress_formatter.start(notification) + end + end + + def progress(result) + progress_formatter.public_send(message_for(result), result) + end + + def message(message) + out.puts(message.message) + end + + def summarize(checkpoints) + return if checkpoints.empty? + + result = checkpoints.reduce :+ + + progress_formatter.dump_pending(result) if result.pending_examples.any? + progress_formatter.dump_failures(result) + dump_deprecations(result.deprecations) + dump_profile(result.profile) if result.profile + progress_formatter.dump_summary(result.summary) + end + + def summarize_remaining(remaining) + out.puts(colorizer.wrap(<<~MESSAGE, :detail)) + + The following specs weren't run: + + #{spec_list(remaining)} + + MESSAGE + end + + private + + def dump_deprecations(deprecations) + formatter = ::RSpec::Core::Formatters::DeprecationFormatter.new( + deprecation_stream, + out + ) + + deprecations.each(&formatter.method(:deprecation)) + formatter.deprecation_summary(nil) + end + + def dump_profile(profile) + ::RSpec::Core::Formatters::ProfileFormatter.new(out).dump_profile(profile) + end + + def spec_list(remaining) + remaining + .flat_map(&:id).sort.each_with_index + .map do |example, index| + format( + '%4d) %s', + index: index.next, + example: example + ) + end.join("\n") + end + + def colorizer + ::RSpec::Core::Formatters::ConsoleCodes + end + + def message_for(notification) + { + passed: :example_passed, + failed: :example_failed, + pending: :example_pending + }.fetch notification.example.execution_result.status + end + end + end + end +end diff --git a/lib/flatware/rspec/marshalable.rb b/lib/flatware/rspec/marshalable.rb index 6355696..d50c88f 100644 --- a/lib/flatware/rspec/marshalable.rb +++ b/lib/flatware/rspec/marshalable.rb @@ -3,6 +3,7 @@ module RSpec module Marshalable require 'flatware/rspec/marshalable/deprecation_notification' require 'flatware/rspec/marshalable/examples_notification' + require 'flatware/rspec/marshalable/example_notification' require 'flatware/rspec/marshalable/profile_notification' require 'flatware/rspec/marshalable/summary_notification' diff --git a/lib/flatware/rspec/marshalable/example_notification.rb b/lib/flatware/rspec/marshalable/example_notification.rb new file mode 100644 index 0000000..be11576 --- /dev/null +++ b/lib/flatware/rspec/marshalable/example_notification.rb @@ -0,0 +1,21 @@ +require 'rspec/core' +require 'flatware/rspec/marshalable/example' + +module Flatware + module RSpec + module Marshalable + class ExampleNotification < ::RSpec::Core::Notifications::ExampleNotification + def self.from_rspec(rspec_notification) + new(Example.new(rspec_notification.example)) + end + + def fully_formatted(failure_number, colorizer = ::RSpec::Core::Formatters::ConsoleCodes) + return if example.execution_result.status != :failed + + @exception_presenter ||= ::RSpec::Core::Formatters::ExceptionPresenter::Factory.new(example).build + @exception_presenter.fully_formatted(failure_number, colorizer) + end + end + end + end +end