Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions spec/extension/array_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,83 @@ def <=> (other)
expect(array.to_s).to eq expectation.to_s
end
end

# Ensure that Array's built-in sorts are unstable. In some Ruby
# implementations, sorts are stable. These tests force the built-in
# sorts to be unstable, which we ensure here.
describe "built-in sort" do

describe "Array#sort" do
it "is unstable" do
sorted = array.sort
expect(sorted.to_s).not_to eq expectation.to_s
end
end

describe "Array#sort with block" do
it "is unstable" do
sorted = array.sort do |a, b|
a.length <=> b.length
end
expect(sorted.to_s).not_to eq expectation.to_s
end
end

describe "Array#sort!" do
it "is unstable" do
sorted = array.dup
sorted.sort!
expect(sorted.to_s).not_to eq expectation.to_s
end
end

describe "Array#sort! with block" do
it "is unstable" do
sorted = array.dup
sorted.sort! do |a, b|
a.length <=> b.length
end
expect(sorted.to_s).not_to eq expectation.to_s
end
end

describe "Array#sort_by" do
it "is unstable" do
unsorted = ['a', 'c', 'bd', 'fe', 'b']
enum = unsorted.sort_by
sorted = enum.with_index { |e, _i| e.length }
expect(enum).to be_an(Enumerator)
expect(sorted.to_a).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

describe "Array#sort_by with block" do
it "is unstable" do
unsorted = ['a', 'c', 'bd', 'fe', 'b']
sorted = unsorted.sort_by { |e| e.length }
expect(sorted).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

describe "Array#sort_by!" do
it "is unstable" do
sorted = ['a', 'c', 'bd', 'fe', 'b'].dup
enum = sorted.sort_by!
enum.with_index { |e, _i| e.length }
expect(enum).to be_an(Enumerator)
expect(sorted.to_a).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

describe "Array#sort_by! with block" do
it "is unstable" do
sorted = ['a', 'c', 'bd', 'fe', 'b'].dup
sorted.sort_by! { |e| e.length }
expect(sorted).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

end

end

52 changes: 46 additions & 6 deletions spec/extension/enumerable_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
require 'spec_helper'

describe Enumerable do
KeyValue = Struct.new(:key, :value) do

KeyValue = Struct.new(:key, :value) do
def <=> (other)
self.key <=> other.key
end
Expand All @@ -12,32 +13,71 @@ def <=> (other)

describe "#stable_sort_by (Array)" do
it 'sorts stably' do
expect(['a', 'c', 'bd', 'fe', 'b'].sort_by { |x| x.length }).not_to eq ['a', 'c', 'b', 'bd', 'fe']
expect(['a', 'c', 'bd', 'fe', 'b'].stable_sort_by { |x| x.length }).to eq ['a', 'c', 'b', 'bd', 'fe']
expect(['a', 'c', 'bd', 'fe', 'b'].stable_sort_by.is_a?(Enumerator)).to be true
end
end

describe "#stable_sort_by (Enumerator)" do
it 'sorts stably' do
expect(['a', 'c', 'bd', 'fe', 'b'].each.sort_by { |x| x.length }).to eq ['a', 'c', 'b', 'fe', 'bd']
expect(['a', 'c', 'bd', 'fe', 'b'].each.stable_sort_by { |x| x.length }).to eq ['a', 'c', 'b', 'bd', 'fe']
expect(['a', 'c', 'bd', 'fe', 'b'].each.stable_sort_by.is_a?(Enumerator)).to be true
end
end

describe "#stable_sort (Array)" do
it 'sorts stably' do
expect(array.sort.to_s).not_to eq expectation.to_s
expect(array.stable_sort.to_s).to eq expectation.to_s
end
end

describe "#stable_sort (Enumerator)" do
it 'sorts stably' do
expect(array.each.sort.to_s).not_to eq expectation.to_s
expect(array.each.stable_sort.to_s).to eq expectation.to_s
end
end
end

# Ensure that Enumerable's built-in sorts are unstable. In some
# Ruby implementations, sorts are stable. These tests force the
# built-in sorts to be unstable, which we ensure here.
describe "built-in sort" do

describe "Enumerable#sort_by" do
it "is unstable" do
unsorted = ['a', 'c', 'bd', 'fe', 'b'].each
enum = unsorted.sort_by
sorted = enum.with_index { |e, _i| e.length }
expect(enum).to be_an(Enumerator)
expect(sorted.to_a).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

describe "Enumerable#sort_by with block" do
it "is unstable" do
unsorted = ['a', 'c', 'bd', 'fe', 'b'].each
sorted = unsorted.sort_by { |e| e.length }
expect(sorted).not_to eq ['a', 'c', 'b', 'bd', 'fe']
end
end

describe "Enumerable#sort" do
it "is unstable" do
unsorted = array.each
sorted = unsorted.sort
expect(sorted.to_s).not_to eq expectation.to_s
end
end

describe "Enumerable#sort with block" do
it "is unstable" do
unsorted = array.each
sorted = unsorted.sort do |a, b|
a.length <=> b.length
end
expect(sorted.to_s).not_to eq expectation.to_s
end
end

end

end
5 changes: 5 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@

$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
require 'stable_sort'

glob = File.join(File.dirname(__FILE__), "support/**/*.rb")
Dir[glob].sort.each do |path|
require path
end
102 changes: 102 additions & 0 deletions spec/support/force_unstable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Monkey-patch the default sorts to ensure that they are unstable. We
# do that because sort is stable in some Ruby implementations
# (e.g. MRI >= 2.2 on Linux). Tests run under those implementations
# could pass even if this library were broken.
#
# We monkey-patch all sort methods: Even though in MRI some sort
# methods use other sort methods (for example, Enumerable#sort uses
# Array#sort), some of those bindings are static, and others dynamic.
# A minimal monkey-patch would bind the tests tightly to the Ruby
# implementation, making them fragile. Patching all sort methods
# makes the test more resiliant when the implementation changes.

RSpec.configure do |c|
c.before do
# To make tests reproducible, set the random number seed before
# each test. Since the tests depend upon sorting relatively short
# lists, and noticing whether the sorted lists are stably sorted
# or not, occasional false test failures result. There's nothing
# special about this seed: it's just one that caused all the tests
# to be reproducible.
#
# This is a bit of a hack, dependent upon the random number
# generator in a given implementation.
#
# An alternative to this is to make the test arrays large enough
# that false negatives are unlikely enough to not be a problem.
# That would be less of a hack, but a larger change to the
# existing tests.
Random.srand(10)
end
end

class Array

original_sort_by = instance_method(:sort_by)
define_method(:sort_by) do |*args, &block|
return to_enum(:sort_by) unless block
original_sort_by.bind(self).call do |o|
[block.call(o), rand]
end
end

original_sort_by_bang = instance_method(:sort_by!)
define_method(:sort_by!) do |*args, &block|
return to_enum(:sort_by!) unless block
original_sort_by_bang.bind(self).call do |o|
[block.call(o), rand]
end
end

original_sort = instance_method(:sort)
define_method(:sort) do |*args, &block|
if block_given?
original_sort.bind(self).call do |a, b|
[yield(a, b), rand]
end
else
original_sort.bind(self).call do |a, b|
[a, rand] <=> [b, rand]
end
end
end

original_sort_bang = instance_method(:sort!)
define_method(:sort!) do
if block_given?
original_sort_bang.bind(self).call do |a, b|
[yield(a, b), rand]
end
else
original_sort_bang.bind(self).call do |a, b|
[a, rand] <=> [b, rand]
end
end
end

end

module Enumerable

original_sort = instance_method(:sort)
define_method(:sort) do
if block_given?
original_sort.bind(self).call do |a, b|
[yield(a, b), rand]
end
else
original_sort.bind(self).call do |a, b|
[a, rand] <=> [b, rand]
end
end
end

original_sort_by = instance_method(:sort_by)
define_method(:sort_by) do |&block|
return to_enum(:sort_by) unless block
original_sort_by.bind(self).call do |o|
[block.call(o), rand]
end
end

end