diff --git a/features/configuration/read_options_from_file.feature b/features/configuration/read_options_from_file.feature index 8c067e73d8..b7ce3c9c15 100644 --- a/features/configuration/read_options_from_file.feature +++ b/features/configuration/read_options_from_file.feature @@ -45,7 +45,7 @@ Feature: read command line configuration options from files """ruby describe "formatter set in custom options file" do it "sets formatter" do - expect(RSpec.configuration.formatters.first). + expect(RSpec.configuration.formatters.to_a.first). to be_a(RSpec::Core::Formatters::DocumentationFormatter) end end @@ -82,7 +82,7 @@ Feature: read command line configuration options from files """ruby describe "formatter" do it "is set to documentation" do - expect(RSpec.configuration.formatters.first).to be_an(RSpec::Core::Formatters::DocumentationFormatter) + expect(RSpec.configuration.formatters.to_a.first).to be_an(RSpec::Core::Formatters::DocumentationFormatter) end end """ diff --git a/lib/rspec/core/command_line.rb b/lib/rspec/core/command_line.rb index 0206da91b0..578397ba38 100644 --- a/lib/rspec/core/command_line.rb +++ b/lib/rspec/core/command_line.rb @@ -20,6 +20,7 @@ def run(err, out) @configuration.output_stream = out if @configuration.output_stream == $stdout @options.configure(@configuration) @configuration.load_spec_files + @configuration.setup_default_formatters @world.announce_filters @configuration.reporter.report(@world.example_count) do |reporter| diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index e86c1d45bb..4d893c5736 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -255,7 +255,6 @@ def initialize @include_or_extend_modules = [] @mock_framework = nil @files_to_run = [] - @formatters = [] @color = false @pattern = '**/*_spec.rb' @failure_exit_code = 1 @@ -294,7 +293,7 @@ def force(hash) def reset @spec_files_loaded = false @reporter = nil - @formatters.clear + @formatters = nil end # @overload add_setting(name) @@ -593,28 +592,24 @@ def full_description # and paths to use for output streams, but you should consider that a # private api that may change at any time without notice. def add_formatter(formatter_to_use, *paths) - formatter_class = - built_in_formatter(formatter_to_use) || - custom_formatter(formatter_to_use) || - (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - maybe you meant 'documentation' or 'progress'?.") - paths << output_stream if paths.empty? - new_formatter = formatter_class.new(*paths.map {|p| String === p ? file_at(p) : p}) - formatters << new_formatter unless duplicate_formatter_exists?(new_formatter) + formatters.add formatter_to_use, *paths end - alias_method :formatter=, :add_formatter + # @api private def formatters - @formatters ||= [] + @formatters ||= Formatters::Collection.new(reporter) end + # @api private + def setup_default_formatters + formatters.setup_default output_stream, deprecation_stream + end + + # @api private def reporter - @reporter ||= begin - add_formatter('progress') if formatters.empty? - add_formatter(RSpec::Core::Formatters::DeprecationFormatter, deprecation_stream, output_stream) - Reporter.new(self, *formatters) - end + @reporter ||= Reporter.new(self) end # @api private @@ -1098,69 +1093,6 @@ def assert_no_example_groups_defined(config_option) def output_to_tty?(output=output_stream) tty? || (output.respond_to?(:tty?) && output.tty?) end - - def built_in_formatter(key) - case key.to_s - when 'd', 'doc', 'documentation', 's', 'n', 'spec', 'nested' - require 'rspec/core/formatters/documentation_formatter' - RSpec::Core::Formatters::DocumentationFormatter - when 'h', 'html' - require 'rspec/core/formatters/html_formatter' - RSpec::Core::Formatters::HtmlFormatter - when 'p', 'progress' - require 'rspec/core/formatters/progress_formatter' - RSpec::Core::Formatters::ProgressFormatter - when 'j', 'json' - require 'rspec/core/formatters/json_formatter' - RSpec::Core::Formatters::JsonFormatter - end - end - - def custom_formatter(formatter_ref) - if Class === formatter_ref - formatter_ref - elsif string_const?(formatter_ref) - begin - formatter_ref.gsub(/^::/,'').split('::').inject(Object) { |const,string| const.const_get string } - rescue NameError - require( path_for(formatter_ref) ) ? retry : raise - end - end - end - - def duplicate_formatter_exists?(new_formatter) - formatters.any? do |formatter| - formatter.class === new_formatter && formatter.output == new_formatter.output - end - end - - def string_const?(str) - str.is_a?(String) && /\A[A-Z][a-zA-Z0-9_:]*\z/ =~ str - end - - def path_for(const_ref) - underscore_with_fix_for_non_standard_rspec_naming(const_ref) - end - - def underscore_with_fix_for_non_standard_rspec_naming(string) - underscore(string).sub(%r{(^|/)r_spec($|/)}, '\\1rspec\\2') - end - - # activesupport/lib/active_support/inflector/methods.rb, line 48 - def underscore(camel_cased_word) - word = camel_cased_word.to_s.dup - word.gsub!(/::/, '/') - word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') - word.tr!("-", "_") - word.downcase! - word - end - - def file_at(path) - FileUtils.mkdir_p(File.dirname(path)) - File.new(path, 'w') - end end end end diff --git a/lib/rspec/core/formatters.rb b/lib/rspec/core/formatters.rb index 8d484841f7..b24e38a74d 100644 --- a/lib/rspec/core/formatters.rb +++ b/lib/rspec/core/formatters.rb @@ -51,4 +51,114 @@ # @see RSpec::Core::Formatters::BaseTextFormatter # @see RSpec::Core::Reporter module RSpec::Core::Formatters + + class Collection + def initialize(reporter) + @formatters = [] + @reporter = reporter + end + + # @api private + def setup_default output_stream, deprecation_stream + if @formatters.empty? + add 'progress', output_stream + end + add DeprecationFormatter, deprecation_stream, output_stream + end + + # @api private + def add formatter_to_use, *paths + formatter_class = + built_in_formatter(formatter_to_use) || + custom_formatter(formatter_to_use) || + (raise ArgumentError, "Formatter '#{formatter_to_use}' unknown - maybe you meant 'documentation' or 'progress'?.") + formatter = formatter_class.new(*paths.map {|p| String === p ? file_at(p) : p}) + + if formatter.respond_to?(:notifications) + @reporter.register_listener formatter, *formatter.notifications + @formatters << formatter unless duplicate_formatter_exists?(formatter) + else + raise 'Legacy formatter support yet to be implemented' + end + end + + # @api private + def clear + @formatters.clear + end + + def empty? + @formatters.empty? + end + + # @api private + def to_a + @formatters + end + + private + + def duplicate_formatter_exists?(new_formatter) + @formatters.any? do |formatter| + formatter.class === new_formatter && formatter.output == new_formatter.output + end + end + + def built_in_formatter(key) + case key.to_s + when 'd', 'doc', 'documentation', 's', 'n', 'spec', 'nested' + require 'rspec/core/formatters/documentation_formatter' + DocumentationFormatter + when 'h', 'html' + require 'rspec/core/formatters/html_formatter' + HtmlFormatter + when 'p', 'progress' + require 'rspec/core/formatters/progress_formatter' + ProgressFormatter + when 'j', 'json' + require 'rspec/core/formatters/json_formatter' + JsonFormatter + end + end + + def custom_formatter(formatter_ref) + if Class === formatter_ref + formatter_ref + elsif string_const?(formatter_ref) + begin + formatter_ref.gsub(/^::/,'').split('::').inject(Object) { |const,string| const.const_get string } + rescue NameError + require( path_for(formatter_ref) ) ? retry : raise + end + end + end + + def string_const?(str) + str.is_a?(String) && /\A[A-Z][a-zA-Z0-9_:]*\z/ =~ str + end + + def path_for(const_ref) + underscore_with_fix_for_non_standard_rspec_naming(const_ref) + end + + def underscore_with_fix_for_non_standard_rspec_naming(string) + underscore(string).sub(%r{(^|/)r_spec($|/)}, '\\1rspec\\2') + end + + # activesupport/lib/active_support/inflector/methods.rb, line 48 + def underscore(camel_cased_word) + word = camel_cased_word.to_s.dup + word.gsub!(/::/, '/') + word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') + word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + word.tr!("-", "_") + word.downcase! + word + end + + def file_at(path) + FileUtils.mkdir_p(File.dirname(path)) + File.new(path, 'w') + end + end end diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index e411f120eb..4d11b14a36 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -806,87 +806,11 @@ def metadata_hash(*args) end end - describe '#formatter=' do - it "delegates to add_formatter (better API for user-facing configuration)" do - expect(config).to receive(:add_formatter).with('these','options') - config.add_formatter('these','options') - end - end - - describe "#add_formatter" do - let(:path) { File.join(Dir.tmpdir, 'output.txt') } - - it "adds to the list of formatters" do - config.add_formatter :documentation - expect(config.formatters.first).to be_an_instance_of(Formatters::DocumentationFormatter) - end - - it "finds a formatter by name (w/ Symbol)" do - config.add_formatter :documentation - expect(config.formatters.first).to be_an_instance_of(Formatters::DocumentationFormatter) - end - - it "finds a formatter by name (w/ String)" do - config.add_formatter 'documentation' - expect(config.formatters.first).to be_an_instance_of(Formatters::DocumentationFormatter) - end - - it "finds a formatter by class" do - formatter_class = Class.new(Formatters::BaseTextFormatter) - config.add_formatter formatter_class - expect(config.formatters.first).to be_an_instance_of(formatter_class) - end - - it "finds a formatter by class name" do - stub_const("CustomFormatter", Class.new(Formatters::BaseFormatter)) - config.add_formatter "CustomFormatter" - expect(config.formatters.first).to be_an_instance_of(CustomFormatter) - end - - it "finds a formatter by class fully qualified name" do - stub_const("RSpec::CustomFormatter", Class.new(Formatters::BaseFormatter)) - config.add_formatter "RSpec::CustomFormatter" - expect(config.formatters.first).to be_an_instance_of(RSpec::CustomFormatter) - end - - it "requires a formatter file based on its fully qualified name" do - expect(config).to receive(:require).with('rspec/custom_formatter') do - stub_const("RSpec::CustomFormatter", Class.new(Formatters::BaseFormatter)) - end - config.add_formatter "RSpec::CustomFormatter" - expect(config.formatters.first).to be_an_instance_of(RSpec::CustomFormatter) - end - - it "raises NameError if class is unresolvable" do - expect(config).to receive(:require).with('rspec/custom_formatter3') - expect(lambda { config.add_formatter "RSpec::CustomFormatter3" }).to raise_error(NameError) - end - - it "raises ArgumentError if formatter is unknown" do - expect(lambda { config.add_formatter :progresss }).to raise_error(ArgumentError) - end - - context "with a 2nd arg defining the output" do - it "creates a file at that path and sets it as the output" do - config.add_formatter('doc', path) - expect(config.formatters.first.output).to be_a(File) - expect(config.formatters.first.output.path).to eq(path) - end - end - - context "when a duplicate formatter exists" do - before { config.add_formatter :documentation } - - it "doesn't add the formatter for the same output target" do - expect { - config.add_formatter :documentation - }.not_to change { config.formatters.length } - end - - it "adds the formatter for different output targets" do - expect { - config.add_formatter :documentation, path - }.to change { config.formatters.length } + %w[formatter= add_formatter].each do |config_method| + describe "##{config_method}" do + it "delegates to formatters#add" do + expect(config.formatters).to receive(:add).with('these','options') + config.send(config_method,'these','options') end end end @@ -1493,6 +1417,7 @@ def strategy.order(list) it 'causes deprecations to raise errors rather than printing to the deprecation stream' do config.deprecation_stream = stream = StringIO.new config.raise_errors_for_deprecations! + config.setup_default_formatters expect { config.reporter.deprecation(:deprecated => "foo", :call_site => "foo.rb:1") diff --git a/spec/rspec/core/formatters_spec.rb b/spec/rspec/core/formatters_spec.rb new file mode 100644 index 0000000000..a8acc88fbb --- /dev/null +++ b/spec/rspec/core/formatters_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +module RSpec::Core::Formatters + describe Collection do + + describe "#add(formatter)" do + let(:collection) { Collection.new reporter } + let(:output) { StringIO.new } + let(:path) { File.join(Dir.tmpdir, 'output.txt') } + let(:reporter) { double "reporter", :register_listener => nil } + + it "adds to the list of formatters" do + collection.add :documentation, output + expect(collection.to_a.first).to be_an_instance_of(DocumentationFormatter) + end + + it "finds a formatter by name (w/ Symbol)" do + collection.add :documentation, output + expect(collection.to_a.first).to be_an_instance_of(DocumentationFormatter) + end + + it "finds a formatter by name (w/ String)" do + collection.add 'documentation', output + expect(collection.to_a.first).to be_an_instance_of(DocumentationFormatter) + end + + it "finds a formatter by class" do + formatter_class = Class.new(BaseTextFormatter) + collection.add formatter_class, output + expect(collection.to_a.first).to be_an_instance_of(formatter_class) + end + + it "finds a formatter by class name" do + stub_const("CustomFormatter", Class.new(BaseFormatter)) + collection.add "CustomFormatter", output + expect(collection.to_a.first).to be_an_instance_of(CustomFormatter) + end + + it "finds a formatter by class fully qualified name" do + stub_const("RSpec::CustomFormatter", Class.new(BaseFormatter)) + collection.add "RSpec::CustomFormatter", output + expect(collection.to_a.first).to be_an_instance_of(RSpec::CustomFormatter) + end + + it "requires a formatter file based on its fully qualified name" do + expect(collection).to receive(:require).with('rspec/custom_formatter') do + stub_const("RSpec::CustomFormatter", Class.new(BaseFormatter)) + end + collection.add "RSpec::CustomFormatter", output + expect(collection.to_a.first).to be_an_instance_of(RSpec::CustomFormatter) + end + + it "raises NameError if class is unresolvable" do + expect(collection).to receive(:require).with('rspec/custom_formatter3') + expect { collection.add "RSpec::CustomFormatter3", output }.to raise_error(NameError) + end + + it "raises ArgumentError if formatter is unknown" do + expect { collection.add :progresss, output }.to raise_error(ArgumentError) + end + + context "with a 2nd arg defining the output" do + it "creates a file at that path and sets it as the output" do + collection.add('doc', path) + expect(collection.to_a.first.output).to be_a(File) + expect(collection.to_a.first.output.path).to eq(path) + end + end + + context "when a duplicate formatter exists" do + before { collection.add :documentation, output } + + it "doesn't add the formatter for the same output target" do + expect { + collection.add :documentation, output + }.not_to change { collection.to_a.length } + end + + it "adds the formatter for different output targets" do + expect { + collection.add :documentation, path + }.to change { collection.to_a.length } + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a7b197a2b8..11fdde8f69 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -126,6 +126,13 @@ def without_env_vars(*vars) :file_path => /spec\/command_line/ } + # Use the doc formatter when running individual files. + # This is too verbose when running all spec files but + # is nice for a single file. + if c.files_to_run.one? + c.formatter = 'doc' + end + c.expect_with :rspec do |expectations| expectations.syntax = :expect end