diff --git a/features/command_line/order.feature b/features/command_line/order.feature index 65a4048d49..ad6ca50f51 100644 --- a/features/command_line/order.feature +++ b/features/command_line/order.feature @@ -1,29 +1,25 @@ Feature: --order (new in rspec-core-2.8) Use the `--order` option to tell RSpec how to order the files, groups, and - examples. Options are `default` and `rand`: + examples. Options are `defined` and `rand`: - Default is: + `defined` is the default, which executes groups and examples in the + order they are defined as the spec files are loaded. - * files are ordered based on the underlying file system's order (typically - case-sensitive alpha on *nix OS's and case-insenstive alpha in Windows) - * groups/examples are loaded in the order in which they are declared - - Use `rand` to randomize the order of files, groups within files, and - examples within groups.* + Use `rand` to randomize the order of groups and examples within groups.* * Nested groups are always run from top-level to bottom-level in order to avoid executing `before(:all)` and `after(:all)` hooks more than once, but the order of groups at each level is randomized. - You can also specify a seed + You can also specify a seed:

Examples

- --order default + --order defined --order rand --order rand:123 --seed 123 # same as --order rand:123 - The `default` option is only necessary when you have `--order rand` stored in a + The `defined` option is only necessary when you have `--order rand` stored in a config file (e.g. `.rspec`) and you want to override it from the command line. diff --git a/features/command_line/require_option.feature b/features/command_line/require_option.feature index a07e325eca..8eb8c064ce 100644 --- a/features/command_line/require_option.feature +++ b/features/command_line/require_option.feature @@ -20,8 +20,8 @@ Feature: --require option super end - def puts(message) - [@file, __getobj__].each { |out| out.puts message } + def puts(*args) + [@file, __getobj__].each { |out| out.puts(*args) } end def close diff --git a/features/configuration/overriding_global_ordering.feature b/features/configuration/overriding_global_ordering.feature new file mode 100644 index 0000000000..dea03b0a29 --- /dev/null +++ b/features/configuration/overriding_global_ordering.feature @@ -0,0 +1,93 @@ +Feature: Overriding global ordering + + You can customize how RSpec orders examples and example groups. + For an individual group, you can control it by tagging it with + `:order` metadata: + + * `:defined` runs the examples (and sub groups) in defined order. + * `:random` runs them in random order. + + If you have more specialized needs, you can register your own ordering + using the `register_ordering` configuration option. If you register + an ordering as `:global`, it will be the global default, used by all + groups that do not have `:order` metadata (and by RSpec to order the + top-level groups). + + Scenario: running a specific examples group in order + Given a file named "order_dependent_spec.rb" with: + """ruby + describe "examples only pass when they are run in order", :order => :defined do + before(:all) { @list = [] } + + it "passes when run first" do + @list << 1 + expect(@list).to eq([1]) + end + + it "passes when run second" do + @list << 2 + expect(@list).to eq([1, 2]) + end + + it "passes when run third" do + @list << 3 + expect(@list).to eq([1, 2, 3]) + end + end + """ + + When I run `rspec order_dependent_spec.rb --order random:1` + Then the examples should all pass + + Scenario: Registering a custom ordering + Given a file named "register_custom_ordering_spec.rb" with: + """ruby + RSpec.configure do |rspec| + rspec.register_ordering(:reverse) do |items| + items.reverse + end + end + + describe "A group that must run in reverse order", :order => :reverse do + before(:all) { @list = [] } + + it "passes when run second" do + @list << 2 + expect(@list).to eq([1, 2]) + end + + it "passes when run first" do + @list << 1 + expect(@list).to eq([1]) + end + end + """ + When I run `rspec register_custom_ordering_spec.rb` + Then the examples should all pass + + Scenario: Using a custom global ordering + Given a file named "register_global_ordering_spec.rb" with: + """ruby + RSpec.configure do |rspec| + rspec.register_ordering(:global) do |items| + items.reverse + end + end + + describe "A group without :order metadata" do + before(:all) { @list = [] } + + it "passes when run second" do + @list << 2 + expect(@list).to eq([1, 2]) + end + + it "passes when run first" do + @list << 1 + expect(@list).to eq([1]) + end + end + """ + When I run `rspec register_global_ordering_spec.rb` + Then the examples should all pass + diff --git a/lib/rspec/core.rb b/lib/rspec/core.rb index 3c56ffc602..22f11a5bfa 100644 --- a/lib/rspec/core.rb +++ b/lib/rspec/core.rb @@ -15,7 +15,6 @@ require_rspec['core/flat_map'] require_rspec['core/filter_manager'] require_rspec['core/dsl'] -require_rspec['core/extensions/ordered'] require_rspec['core/deprecation'] require_rspec['core/reporter'] @@ -24,6 +23,7 @@ require_rspec['core/metadata'] require_rspec['core/pending'] require_rspec['core/formatters'] +require_rspec['core/ordering'] require_rspec['core/world'] require_rspec['core/configuration'] diff --git a/lib/rspec/core/command_line.rb b/lib/rspec/core/command_line.rb index ee98dc7a00..e5438e1d9f 100644 --- a/lib/rspec/core/command_line.rb +++ b/lib/rspec/core/command_line.rb @@ -22,10 +22,10 @@ def run(err, out) @configuration.load_spec_files @world.announce_filters - @configuration.reporter.report(@world.example_count, @configuration.randomize? ? @configuration.seed : nil) do |reporter| + @configuration.reporter.report(@world.example_count) do |reporter| begin @configuration.run_hook(:before, :suite) - @world.example_groups.ordered.map {|g| g.run(reporter) }.all? ? 0 : @configuration.failure_exit_code + @world.ordered_example_groups.map {|g| g.run(reporter) }.all? ? 0 : @configuration.failure_exit_code ensure @configuration.run_hook(:after, :suite) end diff --git a/lib/rspec/core/configuration.rb b/lib/rspec/core/configuration.rb index 48ba5287c6..b384f2aad2 100644 --- a/lib/rspec/core/configuration.rb +++ b/lib/rspec/core/configuration.rb @@ -119,11 +119,6 @@ def self.add_setting(name, opts={}) # The exit code to return if there are any failures (default: 1). add_setting :failure_exit_code - # @macro define_reader - # Determines the order in which examples are run (default: OS standard - # load order for files, declaration order for groups and examples). - define_reader :order - # @macro define_reader # Indicates files configured to be required define_reader :requires @@ -188,20 +183,6 @@ def pattern= value # @param [Symbol] color one of the following: [:black, :white, :red, :green, :yellow, :blue, :magenta, :cyan] add_setting :detail_color - # @macro define_reader - # Seed for random ordering (default: generated randomly each run). - # - # When you run specs with `--order random`, RSpec generates a random seed - # for the randomization and prints it to the `output_stream` (assuming - # you're using RSpec's built-in formatters). If you discover an ordering - # dependency (i.e. examples fail intermittently depending on order), set - # this (on Configuration or on the command line with `--seed`) to run - # using the same seed while you debug the issue. - # - # We recommend, actually, that you use the command line approach so you - # don't accidentally leave the seed encoded. - define_reader :seed - # @macro add_setting # When a block passed to pending fails (as expected), display the failure # without reporting it as a failure (default: false). @@ -224,8 +205,8 @@ def treat_symbols_as_metadata_keys_with_true_values=(value) add_setting :expecting_with_rspec # @private attr_accessor :filter_manager - - attr_reader :backtrace_formatter + # @private + attr_reader :backtrace_formatter, :ordering_manager # Alias for rspec-2.x's backtrace_cleaner (now backtrace_formatter) # @@ -248,8 +229,8 @@ def initialize @default_path = 'spec' @deprecation_stream = $stderr @filter_manager = FilterManager.new + @ordering_manager = Ordering::ConfigurationManager.new @preferred_options = {} - @seed = srand % 0xFFFF @failure_color = :red @success_color = :green @pending_color = :yellow @@ -265,11 +246,7 @@ def initialize # # Used to set higher priority option values from the command line. def force(hash) - if hash.has_key?(:seed) - hash[:order], hash[:seed] = order_and_seed_from_seed(hash[:seed]) - elsif hash.has_key?(:order) - set_order_and_seed(hash) - end + ordering_manager.force(hash) @preferred_options.merge!(hash) self.warnings = value_for :warnings, nil end @@ -593,7 +570,7 @@ def reporter @reporter ||= begin add_formatter('progress') if formatters.empty? add_formatter(RSpec::Core::Formatters::DeprecationFormatter, deprecation_stream, output_stream) - Reporter.new(*formatters) + Reporter.new(self, *formatters) end end @@ -917,98 +894,67 @@ def format_docstrings_block @format_docstrings_block ||= DEFAULT_FORMATTER end - # @api - # - # Sets the seed value and sets `order='rand'` - def seed=(seed) - order_and_seed_from_seed(seed) + # @private + def self.delegate_to_ordering_manager(*methods) + methods.each do |method| + define_method method do |*args, &block| + ordering_manager.__send__(method, *args, &block) + end + end end - # @api + # @macro delegate_to_ordering_manager # - # Sets the order and, if order is `'rand:'`, also sets the seed. - def order=(type) - order_and_seed_from_order(type) - end - - def randomize? - order.to_s.match(/rand/) - end - - # @private - DEFAULT_ORDERING = lambda { |list| list } - - # @private - RANDOM_ORDERING = lambda do |list| - Kernel.srand RSpec.configuration.seed - ordering = list.shuffle - Kernel.srand # reset random generation - ordering - end + # Sets the seed value and sets the default global ordering to random. + delegate_to_ordering_manager :seed= - # Sets a strategy by which to order examples. + # @macro delegate_to_ordering_manager + # Seed for random ordering (default: generated randomly each run). # - # @example - # RSpec.configure do |config| - # config.order_examples do |examples| - # examples.reverse - # end - # end + # When you run specs with `--order random`, RSpec generates a random seed + # for the randomization and prints it to the `output_stream` (assuming + # you're using RSpec's built-in formatters). If you discover an ordering + # dependency (i.e. examples fail intermittently depending on order), set + # this (on Configuration or on the command line with `--seed`) to run + # using the same seed while you debug the issue. # - # @see #order_groups - # @see #order_groups_and_examples - # @see #order= - # @see #seed= - def order_examples(&block) - @example_ordering_block = block - @order = "custom" unless built_in_orderer?(block) - end + # We recommend, actually, that you use the command line approach so you + # don't accidentally leave the seed encoded. + delegate_to_ordering_manager :seed - # @private - def example_ordering_block - @example_ordering_block ||= DEFAULT_ORDERING - end + # @macro delegate_to_ordering_manager + # + # Sets the default global order and, if order is `'rand:'`, also sets the seed. + delegate_to_ordering_manager :order= - # Sets a strategy by which to order groups. + # @macro delegate_to_ordering_manager + # Registers a named ordering strategy that can later be + # used to order an example group's subgroups by adding + # `:order => ` metadata to the example group. + # + # @param name [Symbol] The name of the ordering. + # @yield Block that will order the given examples or example groups + # @yieldparam list [Array, Array] The examples or groups to order + # @yieldreturn [Array, Array] The re-ordered examples or groups # # @example - # RSpec.configure do |config| - # config.order_groups do |groups| - # groups.reverse + # RSpec.configure do |rspec| + # rspec.register_ordering :reverse do |list| + # list.reverse # end # end # - # @see #order_examples - # @see #order_groups_and_examples - # @see #order= - # @see #seed= - def order_groups(&block) - @group_ordering_block = block - @order = "custom" unless built_in_orderer?(block) - end - - # @private - def group_ordering_block - @group_ordering_block ||= DEFAULT_ORDERING - end - - # Sets a strategy by which to order groups and examples. - # - # @example - # RSpec.configure do |config| - # config.order_groups_and_examples do |groups_or_examples| - # groups_or_examples.reverse - # end + # describe MyClass, :order => :reverse do + # # ... # end # - # @see #order_groups - # @see #order_examples - # @see #order= - # @see #seed= - def order_groups_and_examples(&block) - order_groups(&block) - order_examples(&block) - end + # @note Pass the symbol `:global` to set the ordering strategy that + # will be used to order the top-level example groups and any example + # groups that do not have declared `:order` metadata. + delegate_to_ordering_manager :register_ordering + + # @private + delegate_to_ordering_manager :seed_used?, :ordering_registry # Set Ruby warnings on or off def warnings= value @@ -1120,37 +1066,6 @@ def file_at(path) FileUtils.mkdir_p(File.dirname(path)) File.new(path, 'w') end - - def order_and_seed_from_seed(value) - order_groups_and_examples(&RANDOM_ORDERING) - @order, @seed = 'rand', value.to_i - [@order, @seed] - end - - def set_order_and_seed(hash) - hash[:order], seed = order_and_seed_from_order(hash[:order]) - hash[:seed] = seed if seed - end - - def order_and_seed_from_order(type) - order, seed = type.to_s.split(':') - @order = order - @seed = seed = seed.to_i if seed - - if randomize? - order_groups_and_examples(&RANDOM_ORDERING) - elsif order == 'default' - @order, @seed = nil, nil - order_groups_and_examples(&DEFAULT_ORDERING) - end - - return order, seed - end - - def built_in_orderer?(block) - [DEFAULT_ORDERING, RANDOM_ORDERING].include?(block) - end - end end end diff --git a/lib/rspec/core/example_group.rb b/lib/rspec/core/example_group.rb index cf9411afaa..5233d4f7da 100644 --- a/lib/rspec/core/example_group.rb +++ b/lib/rspec/core/example_group.rb @@ -302,7 +302,7 @@ def self.subclass(parent, args, &example_group_block) # @private def self.children - @children ||= [].extend(Extensions::Ordered::ExampleGroups) + @children ||= [] end # @private @@ -344,6 +344,7 @@ def self.set_it_up(*args) args << Metadata.build_hash_from(args) args.unshift(symbol_description) if symbol_description @metadata = RSpec::Core::Metadata.new(superclass_metadata).process(*args) + @order = nil hooks.register_globals(self, RSpec.configuration.hooks) world.configure_group(self) end @@ -417,7 +418,7 @@ def self.run(reporter) begin run_before_all_hooks(new) result_for_this_group = run_examples(reporter) - results_for_descendants = children.ordered.map { |child| child.run(reporter) }.all? + results_for_descendants = ordering_strategy.order(children).map { |child| child.run(reporter) }.all? result_for_this_group && results_for_descendants rescue Exception => ex RSpec.wants_to_quit = true if fail_fast? @@ -429,9 +430,25 @@ def self.run(reporter) end end + # @private + def self.ordering_strategy + order = metadata.fetch(:order, :global) + registry = RSpec.configuration.ordering_registry + + registry.fetch(order) do + warn <<-WARNING.gsub(/^ +\|/, '') + |WARNING: Ignoring unknown ordering specified using `:order => #{order.inspect}` metadata. + | Falling back to configured global ordering. + | Unrecognized ordering specified at: #{metadata[:example_group][:location]} + WARNING + + registry.fetch(:global) + end + end + # @private def self.run_examples(reporter) - filtered_examples.ordered.map do |example| + ordering_strategy.order(filtered_examples).map do |example| next if RSpec.wants_to_quit instance = new set_ivars(instance, before_all_ivars) diff --git a/lib/rspec/core/extensions/ordered.rb b/lib/rspec/core/extensions/ordered.rb deleted file mode 100644 index 7264909597..0000000000 --- a/lib/rspec/core/extensions/ordered.rb +++ /dev/null @@ -1,27 +0,0 @@ -module RSpec - module Core - # @private - module Extensions - # @private - # Used to extend lists of examples and groups to support ordering - # strategies like randomization. - module Ordered - # @private - module ExampleGroups - # @private - def ordered - RSpec.configuration.group_ordering_block.call(self) - end - end - - # @private - module Examples - # @private - def ordered - RSpec.configuration.example_ordering_block.call(self) - end - end - end - end - end -end diff --git a/lib/rspec/core/ordering.rb b/lib/rspec/core/ordering.rb new file mode 100644 index 0000000000..8c6f049389 --- /dev/null +++ b/lib/rspec/core/ordering.rb @@ -0,0 +1,132 @@ +module RSpec + module Core + # @private + module Ordering + # @private + # The default global ordering (defined order). + class Identity + def order(items) + items + end + end + + # @private + # Orders items randomly. + class Random + def initialize(configuration) + @configuration = configuration + @used = false + end + + def used? + @used + end + + def order(items) + @used = true + Kernel.srand @configuration.seed + ordering = items.shuffle + Kernel.srand # reset random generation + ordering + end + end + + # @private + # Orders items based on a custom block. + class Custom + def initialize(callable) + @callable = callable + end + + def order(list) + @callable.call(list) + end + end + + # @private + # Stores the different ordering strategies. + class Registry + def initialize(configuration) + @configuration = configuration + @strategies = {} + + register(:random, Random.new(configuration)) + + identity = Identity.new + register(:defined, identity) + + # The default global ordering is --defined. + register(:global, identity) + end + + def fetch(name, &fallback) + @strategies.fetch(name, &fallback) + end + + def register(sym, strategy) + @strategies[sym] = strategy + end + + def used_random_seed? + @strategies[:random].used? + end + end + + # @private + # Manages ordering configuration. + # + # @note This is not intended to be used externally. Use + # the APIs provided by `RSpec::Core::Configuration` instead. + class ConfigurationManager + attr_reader :seed, :ordering_registry + + def initialize + @ordering_registry = Registry.new(self) + @seed = srand % 0xFFFF + @seed_forced = false + @order_forced = false + end + + def seed_used? + ordering_registry.used_random_seed? + end + + def seed=(seed) + return if @seed_forced + register_ordering(:global, ordering_registry.fetch(:random)) + @seed = seed.to_i + end + + def order=(type) + order, seed = type.to_s.split(':') + @seed = seed = seed.to_i if seed + + ordering_name = if order.include?('rand') + :random + elsif order == 'defined' + :defined + end + + register_ordering(:global, ordering_registry.fetch(ordering_name)) if ordering_name + end + + def force(hash) + if hash.has_key?(:seed) + self.seed = hash[:seed] + @seed_forced = true + @order_forced = true + elsif hash.has_key?(:order) + self.order = hash[:order] + @order_forced = true + end + end + + def register_ordering(name, strategy = Custom.new(Proc.new { |l| yield l })) + return if @order_forced && name == :global + ordering_registry.register(name, strategy) + end + end + end + end +end + diff --git a/lib/rspec/core/reporter.rb b/lib/rspec/core/reporter.rb index 1ada380410..0a8c1f0ed4 100644 --- a/lib/rspec/core/reporter.rb +++ b/lib/rspec/core/reporter.rb @@ -4,7 +4,8 @@ class Reporter example_passed example_failed example_pending start_dump dump_pending dump_failures dump_summary seed close stop deprecation deprecation_summary].map(&:to_sym) - def initialize(*formatters) + def initialize(configuration, *formatters) + @configuration = configuration @listeners = Hash.new { |h,k| h[k] = [] } formatters.each do |formatter| register_listener(formatter, *NOTIFICATIONS) @@ -32,32 +33,26 @@ def registered_listeners(notification) # @api # @overload report(count, &block) - # @overload report(count, seed, &block) + # @overload report(count, &block) # @param [Integer] count the number of examples being run - # @param [Integer] seed the seed used to randomize the spec run # @param [Block] block yields itself for further reporting. # # Initializes the report run and yields itself for further reporting. The # block is required, so that the reporter can manage cleaning up after the # run. # - # ### Warning: - # - # The `seed` argument is an internal API and is not guaranteed to be - # supported in the future. - # # @example # # reporter.report(group.examples.size) do |r| # example_groups.map {|g| g.run(r) } # end # - def report(expected_example_count, seed=nil) + def report(expected_example_count) start(expected_example_count) begin yield self ensure - finish(seed) + finish end end @@ -101,7 +96,7 @@ def deprecation(message) notify :deprecation, message end - def finish(seed) + def finish begin stop notify :start_dump @@ -109,7 +104,7 @@ def finish(seed) notify :dump_failures notify :dump_summary, @duration, @example_count, @failure_count, @pending_count notify :deprecation_summary - notify :seed, seed if seed + notify :seed, @configuration.seed if seed_used? ensure notify :close end @@ -127,5 +122,11 @@ def notify(event, *args, &block) formatter.send(event, *args, &block) end end + + private + + def seed_used? + @configuration.seed && @configuration.seed_used? + end end end diff --git a/lib/rspec/core/world.rb b/lib/rspec/core/world.rb index 25886954e9..55317f1483 100644 --- a/lib/rspec/core/world.rb +++ b/lib/rspec/core/world.rb @@ -9,17 +9,22 @@ class World def initialize(configuration=RSpec.configuration) @configuration = configuration - @example_groups = [].extend(Extensions::Ordered::ExampleGroups) + @example_groups = [] @filtered_examples = Hash.new { |hash,group| hash[group] = begin examples = group.examples.dup examples = filter_manager.prune(examples) examples.uniq! - examples.extend(Extensions::Ordered::Examples) + examples end } end + def ordered_example_groups + ordering_strategy = @configuration.ordering_registry.fetch(:global) + ordering_strategy.order(@example_groups) + end + def reset example_groups.clear SharedExampleGroup.registry.clear diff --git a/spec/command_line/order_spec.rb b/spec/command_line/order_spec.rb index 988276f764..21778c73df 100644 --- a/spec/command_line/order_spec.rb +++ b/spec/command_line/order_spec.rb @@ -83,9 +83,9 @@ describe '--order rand' do it 'runs the examples and groups in a different order each time' do - run_command 'tmp/aruba/spec/order_spec.rb --order rand -f doc' + run_command 'spec/order_spec.rb --order rand -f doc' RSpec.configuration.seed = srand && srand # reset seed in same process - run_command 'tmp/aruba/spec/order_spec.rb --order rand -f doc' + run_command 'spec/order_spec.rb --order rand -f doc' expect(stdout.string).to match(/Randomized with seed \d+/) @@ -98,7 +98,7 @@ describe '--order rand:SEED' do it 'runs the examples and groups in the same order each time' do - 2.times { run_command 'tmp/aruba/spec/order_spec.rb --order rand:123 -f doc' } + 2.times { run_command 'spec/order_spec.rb --order rand:123 -f doc' } expect(stdout.string).to match(/Randomized with seed 123/) @@ -111,9 +111,9 @@ describe '--seed SEED' do it "forces '--order rand' and runs the examples and groups in the same order each time" do - 2.times { run_command 'tmp/aruba/spec/order_spec.rb --seed 123 -f doc' } + 2.times { run_command 'spec/order_spec.rb --seed 123 -f doc' } - expect(stdout.string).to match(/Randomized with seed \d+/) + expect(stdout.string).to match(/Randomized with seed 123/) top_level_groups {|first_run, second_run| expect(first_run).to eq(second_run)} nested_groups {|first_run, second_run| expect(first_run).to eq(second_run)} @@ -122,19 +122,21 @@ end it "runs examples in the same order, regardless of the order in which files are given" do - run_command 'tmp/aruba/spec/simple_spec.rb tmp/aruba/spec/simple_spec2.rb --seed 1337 -f doc' - run_command 'tmp/aruba/spec/simple_spec2.rb tmp/aruba/spec/simple_spec.rb --seed 1337 -f doc' + run_command 'spec/simple_spec.rb spec/simple_spec2.rb --seed 1337 -f doc' + run_command 'spec/simple_spec2.rb spec/simple_spec.rb --seed 1337 -f doc' top_level_groups {|first_run, second_run| expect(first_run).to eq(second_run)} nested_groups {|first_run, second_run| expect(first_run).to eq(second_run)} end end - describe '--order default on CLI with --order rand in .rspec' do - it "overrides --order rand with --order default" do + describe '--order defined on CLI with --order rand in .rspec' do + after { remove_file '.rspec' } + + it "overrides --order rand with --order defined" do write_file '.rspec', '--order rand' - run_command 'tmp/aruba/spec/order_spec.rb --order default -f doc' + run_command 'spec/order_spec.rb --order defined -f doc' expect(stdout.string).not_to match(/Randomized/) @@ -145,10 +147,12 @@ end context 'when a custom order is configured' do + after { remove_file 'spec/custom_order_spec.rb' } + before do write_file 'spec/custom_order_spec.rb', """ RSpec.configure do |config| - config.order_groups_and_examples do |list| + config.register_ordering :global do |list| list.sort_by { |item| item.description } end end @@ -167,7 +171,7 @@ end it 'orders the groups and examples by the provided strategy' do - run_command 'tmp/aruba/spec/custom_order_spec.rb -f doc' + run_command 'spec/custom_order_spec.rb -f doc' top_level_groups { |groups| expect(groups.flatten).to eq(['group A', 'group B']) } examples('group B') do |examples| @@ -199,6 +203,8 @@ def split_in_half(array) end def run_command(cmd) - RSpec::Core::Runner.run(cmd.split, stderr, stdout) + in_current_dir do + RSpec::Core::Runner.run(cmd.split, stderr, stdout) + end end end diff --git a/spec/rspec/core/configuration_spec.rb b/spec/rspec/core/configuration_spec.rb index 8dfae30d50..7470548270 100644 --- a/spec/rspec/core/configuration_spec.rb +++ b/spec/rspec/core/configuration_spec.rb @@ -1262,34 +1262,43 @@ def metadata_hash(*args) end describe "#force" do - it "forces order" do - config.force :order => "default" - config.order = "rand" - expect(config.order).to eq("default") - end + context "for ordering options" do + let(:list) { [1, 2, 3, 4] } + let(:ordering_strategy) { config.ordering_registry.fetch(:global) } - it "forces order and seed with :order => 'rand:37'" do - config.force :order => "rand:37" - config.order = "default" - expect(config.order).to eq("rand") - expect(config.seed).to eq(37) - end + let(:shuffled) do + Kernel.srand(37) + list.shuffle + end - it "forces order and seed with :seed => '37'" do - config.force :seed => "37" - config.order = "default" - expect(config.seed).to eq(37) - expect(config.order).to eq("rand") - end + specify "CLI `--order defined` takes precedence over `config.order = rand`" do + config.force :order => "defined" + config.order = "rand" + + expect(ordering_strategy.order(list)).to eq([1, 2, 3, 4]) + end + + specify "CLI `--order rand:37` takes precedence over `config.order = defined`" do + config.force :order => "rand:37" + config.order = "defined" + + expect(ordering_strategy.order(list)).to eq(shuffled) + end + + specify "CLI `--seed 37` forces order and seed" do + config.force :seed => 37 + config.order = "defined" + config.seed = 145 + + expect(ordering_strategy.order(list)).to eq(shuffled) + expect(config.seed).to eq(37) + end - it 'can set random ordering' do - config.force :order => "rand:37" - RSpec.stub(:configuration => config) - list = [1, 2, 3, 4].extend(Extensions::Ordered::Examples) - Kernel.should_receive(:srand).ordered.once.with(37) - list.should_receive(:shuffle).ordered.and_return([2, 4, 1, 3]) - Kernel.should_receive(:srand).ordered.once.with(no_args) - expect(list.ordered).to eq([2, 4, 1, 3]) + specify "CLI `--order defined` takes precedence over `config.register_ordering(:global)`" do + config.force :order => "defined" + config.register_ordering(:global, &:reverse) + expect(ordering_strategy.order(list)).to eq([1, 2, 3, 4]) + end end it "forces 'false' value" do @@ -1310,145 +1319,97 @@ def metadata_hash(*args) end end - describe '#randomize?' do - context 'with order set to :random' do - before { config.order = :random } - - it 'returns true' do - expect(config.randomize?).to be_truthy - end + describe "#seed_used?" do + def use_seed_on(registry) + registry.fetch(:random).order([1, 2]) end - context 'with order set to nil' do - before { config.order = nil } + it 'returns false if neither ordering registry used the seed' do + expect(config.seed_used?).to be false + end - it 'returns false' do - expect(config.randomize?).to be_falsey - end + it 'returns true if the ordering registry used the seed' do + use_seed_on(config.ordering_registry) + expect(config.seed_used?).to be true end end describe '#order=' do context 'given "random"' do - before { config.order = 'random:123' } + before do + config.seed = 7654 + config.order = 'random' + end - it 'sets order to "random"' do - expect(config.order).to eq('random') + it 'does not change the seed' do + expect(config.seed).to eq(7654) end it 'sets up random ordering' do RSpec.stub(:configuration => config) - list = [1, 2, 3, 4].extend(Extensions::Ordered::Examples) - Kernel.should_receive(:srand).ordered.with(123) - list.should_receive(:shuffle).ordered.and_return([2, 4, 1, 3]) - Kernel.should_receive(:srand).ordered.with(no_args) - expect(list.ordered).to eq([2, 4, 1, 3]) + global_ordering = config.ordering_registry.fetch(:global) + expect(global_ordering).to be_an_instance_of(Ordering::Random) end end context 'given "random:123"' do before { config.order = 'random:123' } - it 'sets order to "random"' do - expect(config.order).to eq('random') - end - it 'sets seed to 123' do expect(config.seed).to eq(123) end it 'sets up random ordering' do RSpec.stub(:configuration => config) - list = [1, 2, 3, 4].extend(Extensions::Ordered::Examples) - Kernel.should_receive(:srand).ordered.with(123) - list.should_receive(:shuffle).ordered.and_return([2, 4, 1, 3]) - Kernel.should_receive(:srand).ordered.with(no_args) - expect(list.ordered).to eq([2, 4, 1, 3]) + global_ordering = config.ordering_registry.fetch(:global) + expect(global_ordering).to be_an_instance_of(Ordering::Random) end end - context 'given "default"' do + context 'given "defined"' do before do config.order = 'rand:123' - config.order = 'default' + config.order = 'defined' end - it "sets the order to nil" do - expect(config.order).to be_nil - end - - it "sets the seed to nil" do - expect(config.seed).to be_nil + it "does not change the seed" do + expect(config.seed).to eq(123) end it 'clears the random ordering' do RSpec.stub(:configuration => config) - list = [1, 2, 3, 4].extend(Extensions::Ordered::Examples) - Kernel.should_not_receive(:rand) - expect(list.ordered).to eq([1, 2, 3, 4]) + list = [1, 2, 3, 4] + ordering_strategy = config.ordering_registry.fetch(:global) + expect(ordering_strategy.order(list)).to eq([1, 2, 3, 4]) end end end - describe "#order_examples" do - before { RSpec.stub(:configuration => config) } - - it 'sets a block that determines the ordering of a collection extended with Extensions::Ordered::Examples' do - examples = [1, 2, 3, 4] - examples.extend Extensions::Ordered::Examples - config.order_examples { |examples_to_order| examples_to_order.reverse } - expect(examples.ordered).to eq([4, 3, 2, 1]) - end - - it 'sets #order to "custom"' do - config.order_examples { |examples| examples.reverse } - expect(config.order).to eq("custom") - end - end - - describe "#example_ordering_block" do - it 'defaults to a block that returns the passed argument' do - expect(config.example_ordering_block.call([1, 2, 3])).to eq([1, 2, 3]) - end - end - - describe "#order_groups" do - before { RSpec.stub(:configuration => config) } - - it 'sets a block that determines the ordering of a collection extended with Extensions::Ordered::ExampleGroups' do - groups = [1, 2, 3, 4] - groups.extend Extensions::Ordered::ExampleGroups - config.order_groups { |groups_to_order| groups_to_order.reverse } - expect(groups.ordered).to eq([4, 3, 2, 1]) + describe "#register_ordering" do + def register_reverse_ordering + config.register_ordering(:reverse, &:reverse) end - it 'sets #order to "custom"' do - config.order_groups { |groups| groups.reverse } - expect(config.order).to eq("custom") - end - end + it 'stores the ordering for later use' do + register_reverse_ordering - describe "#group_ordering_block" do - it 'defaults to a block that returns the passed argument' do - expect(config.group_ordering_block.call([1, 2, 3])).to eq([1, 2, 3]) + list = [1, 2, 3] + strategy = config.ordering_registry.fetch(:reverse) + expect(strategy).to be_a(Ordering::Custom) + expect(strategy.order(list)).to eq([3, 2, 1]) end - end - - describe "#order_groups_and_examples" do - let(:examples) { [1, 2, 3, 4].extend Extensions::Ordered::Examples } - let(:groups) { [1, 2, 3, 4].extend Extensions::Ordered::ExampleGroups } - before do - RSpec.stub(:configuration => config) - config.order_groups_and_examples { |list| list.reverse } - end - - it 'sets a block that determines the ordering of a collection extended with Extensions::Ordered::Examples' do - expect(examples.ordered).to eq([4, 3, 2, 1]) - end + it 'can register an ordering object' do + strategy = Object.new + def strategy.order(list) + list.reverse + end - it 'sets a block that determines the ordering of a collection extended with Extensions::Ordered::ExampleGroups' do - expect(groups.ordered).to eq([4, 3, 2, 1]) + config.register_ordering(:reverse, strategy) + list = [1, 2, 3] + fetched = config.ordering_registry.fetch(:reverse) + expect(fetched).to be(strategy) + expect(fetched.order(list)).to eq([3, 2, 1]) end end diff --git a/spec/rspec/core/example_group_spec.rb b/spec/rspec/core/example_group_spec.rb index 3367f92776..201b9f8c8b 100644 --- a/spec/rspec/core/example_group_spec.rb +++ b/spec/rspec/core/example_group_spec.rb @@ -37,6 +37,110 @@ def metadata_hash(*args) expect(group.description).to eq("symbol") end + describe "ordering" do + context "when tagged with `:order => :defined`" do + it 'orders the subgroups and examples in defined order regardless of global order' do + RSpec.configuration.order = :random + + run_order = [] + group = ExampleGroup.describe "outer", :order => :defined do + context "subgroup 1" do + example { run_order << :g1_e1 } + example { run_order << :g1_e2 } + end + + context "subgroup 2" do + example { run_order << :g2_e1 } + example { run_order << :g2_e2 } + end + end + + group.run + expect(run_order).to eq([:g1_e1, :g1_e2, :g2_e1, :g2_e2]) + end + end + + context "when tagged with an unrecognized ordering" do + let(:run_order) { [] } + let(:definition_line) { __LINE__ + 4 } + let(:group) do + order = self.run_order + + ExampleGroup.describe "group", :order => :unrecognized do + example { order << :ex_1 } + example { order << :ex_2 } + end + end + + before do + RSpec.configuration.register_ordering(:global, &:reverse) + allow(group).to receive(:warn) + end + + it 'falls back to the global ordering' do + group.run + expect(run_order).to eq([:ex_2, :ex_1]) + end + + it 'prints a warning so users are notified of their mistake' do + warning = nil + allow(group).to receive(:warn) { |msg| warning = msg } + + group.run + + expect(warning).to match(/unrecognized/) + expect(warning).to match(/#{File.basename __FILE__}:#{definition_line}/) + end + end + + context "when tagged with a custom ordering" do + def ascending_numbers + lambda { |g| Integer(g.description[/\d+/]) } + end + + it 'uses the custom orderings' do + RSpec.configure do |c| + c.register_ordering :custom do |items| + items.sort_by(&ascending_numbers) + end + end + + run_order = [] + group = ExampleGroup.describe "outer", :order => :custom do + example("e2") { run_order << :e2 } + example("e1") { run_order << :e1 } + + context "subgroup 2" do + example("ex 3") { run_order << :g2_e3 } + example("ex 1") { run_order << :g2_e1 } + example("ex 2") { run_order << :g2_e2 } + end + + context "subgroup 1" do + example("ex 2") { run_order << :g1_e2 } + example("ex 1") { run_order << :g1_e1 } + example("ex 3") { run_order << :g1_e3 } + end + + context "subgroup 3" do + example("ex 2") { run_order << :g3_e2 } + example("ex 3") { run_order << :g3_e3 } + example("ex 1") { run_order << :g3_e1 } + end + end + + group.run + + expect(run_order).to eq([ + :e1, :e2, + :g1_e1, :g1_e2, :g1_e3, + :g2_e1, :g2_e2, :g2_e3, + :g3_e1, :g3_e2, :g3_e3 + ]) + end + end + end + describe "top level group" do it "runs its children" do examples_run = [] @@ -752,7 +856,7 @@ def define_and_run_group(define_outer_example = false) example('ex 1') { expect(1).to eq(1) } example('ex 2') { expect(1).to eq(1) } end - group.stub(:filtered_examples) { group.examples.extend(Extensions::Ordered::Examples) } + group.stub(:filtered_examples) { group.examples } expect(group.run(reporter)).to be_truthy end @@ -761,7 +865,7 @@ def define_and_run_group(define_outer_example = false) example('ex 1') { expect(1).to eq(1) } example('ex 2') { expect(1).to eq(2) } end - group.stub(:filtered_examples) { group.examples.extend(Extensions::Ordered::Examples) } + group.stub(:filtered_examples) { group.examples } expect(group.run(reporter)).to be_falsey end @@ -770,7 +874,7 @@ def define_and_run_group(define_outer_example = false) example('ex 1') { expect(1).to eq(2) } example('ex 2') { expect(1).to eq(1) } end - group.stub(:filtered_examples) { group.examples.extend(Extensions::Ordered::Examples) } + group.stub(:filtered_examples) { group.examples } group.filtered_examples.each do |example| example.should_receive(:run) end diff --git a/spec/rspec/core/formatters/documentation_formatter_spec.rb b/spec/rspec/core/formatters/documentation_formatter_spec.rb index 3d7ff51059..3647f46570 100644 --- a/spec/rspec/core/formatters/documentation_formatter_spec.rb +++ b/spec/rspec/core/formatters/documentation_formatter_spec.rb @@ -46,7 +46,7 @@ module RSpec::Core::Formatters context2.example("nested example 2.1"){} context2.example("nested example 2.2"){} - group.run(RSpec::Core::Reporter.new(formatter)) + group.run(RSpec::Core::Reporter.new(RSpec.configuration, formatter)) expect(output.string).to eql(" root @@ -74,7 +74,7 @@ module RSpec::Core::Formatters context1.example(" example 2 ", :pending => true){} context1.example(" example 3 ") { fail } - group.run(RSpec::Core::Reporter.new(formatter)) + group.run(RSpec::Core::Reporter.new(RSpec.configuration, formatter)) expect(output.string).to eql(" root diff --git a/spec/rspec/core/formatters/html_formatter_spec.rb b/spec/rspec/core/formatters/html_formatter_spec.rb index a7ac794d67..9047b5bf81 100644 --- a/spec/rspec/core/formatters/html_formatter_spec.rb +++ b/spec/rspec/core/formatters/html_formatter_spec.rb @@ -23,7 +23,7 @@ module Formatters end let(:generated_html) do - options = %w[spec/rspec/core/resources/formatter_specs.rb --format html --order default] + options = %w[spec/rspec/core/resources/formatter_specs.rb --format html --order defined] err, out = StringIO.new, RSpec.configuration.output err.set_encoding("utf-8") if err.respond_to?(:set_encoding) diff --git a/spec/rspec/core/formatters/json_formatter_spec.rb b/spec/rspec/core/formatters/json_formatter_spec.rb index bbed846ea5..4c2a9fbd31 100644 --- a/spec/rspec/core/formatters/json_formatter_spec.rb +++ b/spec/rspec/core/formatters/json_formatter_spec.rb @@ -14,7 +14,8 @@ describe RSpec::Core::Formatters::JsonFormatter do let(:output) { StringIO.new } let(:formatter) { RSpec::Core::Formatters::JsonFormatter.new(output) } - let(:reporter) { RSpec::Core::Reporter.new(formatter) } + let(:config) { RSpec::Core::Configuration.new } + let(:reporter) { RSpec::Core::Reporter.new(config, formatter) } it "outputs json (brittle high level functional test)" do group = RSpec::Core::ExampleGroup.describe("one apiece") do diff --git a/spec/rspec/core/ordering_spec.rb b/spec/rspec/core/ordering_spec.rb new file mode 100644 index 0000000000..8c20b4dfd7 --- /dev/null +++ b/spec/rspec/core/ordering_spec.rb @@ -0,0 +1,100 @@ +require "spec_helper" + +module RSpec + module Core + module Ordering + describe Identity do + it "does not affect the ordering of the items" do + expect(Identity.new.order([1, 2, 3])).to eq([1, 2, 3]) + end + end + + describe Random do + before { allow(Kernel).to receive(:srand).and_call_original } + + def order_of(input, seed) + Kernel.srand(seed) + input.shuffle + end + + let(:configuration) { RSpec::Core::Configuration.new } + + it 'shuffles the items randomly' do + configuration.seed = 900 + + expected = order_of([1, 2, 3, 4], 900) + + strategy = Random.new(configuration) + expect(strategy.order([1, 2, 3, 4])).to eq(expected) + end + + it 'seeds the random number generator' do + expect(Kernel).to receive(:srand).with(1234).once + + configuration.seed = 1234 + + strategy = Random.new(configuration) + strategy.order([1, 2, 3, 4]) + end + + it 'resets random number generation' do + expect(Kernel).to receive(:srand).with(no_args) + + strategy = Random.new(configuration) + strategy.order([]) + end + end + + describe Custom do + it 'uses the block to order the list' do + strategy = Custom.new(proc { |list| list.reverse }) + + expect(strategy.order([1, 2, 3, 4])).to eq([4, 3, 2, 1]) + end + end + + describe Registry do + let(:configuration) { Configuration.new } + subject(:registry) { Registry.new(configuration) } + + describe "#used_random_seed?" do + it 'returns false if the random orderer has not been used' do + expect(registry.used_random_seed?).to be false + end + + it 'returns false if the random orderer has been fetched but not used' do + expect(registry.fetch(:random)).to be_a(Random) + expect(registry.used_random_seed?).to be false + end + + it 'returns true if the random orderer has been used' do + registry.fetch(:random).order([1, 2]) + expect(registry.used_random_seed?).to be true + end + end + + describe "#fetch" do + it "gives the registered ordering when called with a symbol" do + ordering = Object.new + subject.register(:falcon, ordering) + + expect(subject.fetch(:falcon)).to be ordering + end + + context "when given an unrecognized symbol" do + it 'invokes the given block and returns its value' do + expect(subject.fetch(:falcon) { :fallback }).to eq(:fallback) + end + + it 'raises an error if no block is given' do + expect { + subject.fetch(:falcon) + }.to raise_error(IndexError) + end + end + end + end + end + end +end + diff --git a/spec/rspec/core/reporter_spec.rb b/spec/rspec/core/reporter_spec.rb index fd14f8349e..8fb71252a5 100644 --- a/spec/rspec/core/reporter_spec.rb +++ b/spec/rspec/core/reporter_spec.rb @@ -2,19 +2,25 @@ module RSpec::Core describe Reporter do + let(:config) { Configuration.new } + + def reporter_for(*formatters) + Reporter.new(config, *formatters) + end + describe "abort" do let(:formatter) { double("formatter") } let(:example) { double("example") } - let(:reporter) { Reporter.new(formatter) } + let(:reporter) { reporter_for(formatter) } %w[start_dump dump_pending dump_failures dump_summary close].each do |message| it "sends #{message} to the formatter(s) that respond to message" do formatter.as_null_object.should_receive(message) - reporter.abort(nil) + reporter.abort end it "doesnt notify formatters about messages they dont implement" do - expect { reporter.abort(nil) }.to_not raise_error + expect { reporter.abort }.to_not raise_error end end end @@ -23,7 +29,7 @@ module RSpec::Core it "passes messages to that formatter" do formatter = double("formatter", :example_started => nil) example = double("example") - reporter = Reporter.new(formatter) + reporter = reporter_for(formatter) formatter.should_receive(:example_started). with(example) @@ -46,7 +52,7 @@ module RSpec::Core example("ignore") {} end - group.run(Reporter.new(formatter)) + group.run(reporter_for(formatter)) expect(order).to eq([ "Started: root", @@ -67,7 +73,7 @@ module RSpec::Core group = ExampleGroup.describe("root") - group.run(Reporter.new(formatter)) + group.run(reporter_for(formatter)) end end @@ -75,7 +81,7 @@ module RSpec::Core it "passes messages to all formatters" do formatters = (1..2).map { double("formatter", :example_started => nil) } example = double("example") - reporter = Reporter.new(*formatters) + reporter = reporter_for(*formatters) formatters.each do |formatter| formatter. @@ -89,15 +95,11 @@ module RSpec::Core describe "#report" do it "supports one arg (count)" do - Reporter.new.report(1) {} - end - - it "supports two args (count, seed)" do - Reporter.new.report(1, 2) {} + reporter_for.report(1) {} end it "yields itself" do - reporter = Reporter.new + reporter = reporter_for yielded = nil reporter.report(3) {|r| yielded = r} expect(yielded).to eq(reporter) @@ -105,7 +107,7 @@ module RSpec::Core end describe "#register_listener" do - let(:reporter) { Reporter.new } + let(:reporter) { reporter_for } let(:listener) { double("listener", :start => nil) } before { reporter.register_listener listener, :start } @@ -123,7 +125,7 @@ module RSpec::Core describe "timing" do it "uses RSpec::Core::Time as to not be affected by changes to time in examples" do formatter = double(:formatter).as_null_object - reporter = Reporter.new formatter + reporter = reporter_for formatter reporter.start 1 Time.stub(:now => ::Time.utc(2012, 10, 1)) @@ -132,7 +134,7 @@ module RSpec::Core duration = dur end - reporter.finish 1234 + reporter.finish expect(duration).to be < 0.2 end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 07d8a69d8a..aa7122ef78 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -125,6 +125,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.formatters.none? + c.formatter = 'doc' + end + c.expect_with :rspec do |expectations| expectations.syntax = :expect end